8000 JWT validation problem on 3.0.0-rc.1 (kid format mismatch) · Issue #4533 · distribution/distribution · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

JWT validation problem on 3.0.0-rc.1 (kid format mismatch) #4533

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
ericwyles opened this issue Dec 12, 2024 · 2 comments
Open

JWT validation problem on 3.0.0-rc.1 (kid format mismatch) #4533

ericwyles opened this issue Dec 12, 2024 · 2 comments

Comments

@ericwyles
Copy link

Description

Hi, I'm testing 3.0.0-rc.1 using keycloak 26.0.5 as the auth server.

I am downloading the jwks keys from keycloak's openid-connect/certs endpoint: https://sso.uds.dev/realms/uds/protocol/openid-connect/certs and configuring registry to use that file for the REGISTRY_AUTH_TOKEN_JWKS in my docker compose yaml below:

version: '3.8'

services:
  registry:
    image: registry:3.0.0-rc.1
    container_name: docker-registry
    ports:
      - "5001:5000"
    environment:
      REGISTRY_AUTH: token
      REGISTRY_AUTH_TOKEN_REALM: https://sso.uds.dev/realms/uds/protocol/docker-v2/auth
      REGISTRY_AUTH_TOKEN_SERVICE: docker-registry
      REGISTRY_AUTH_TOKEN_ISSUER: https://sso.uds.dev/realms/uds
      REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /etc/certs/keycloak.pem
      REGISTRY_AUTH_TOKEN_JWKS: /etc/jwks/jwks_original.json
      OTEL_TRACES_EXPORTER: none
    volumes:
      - ./data:/var/lib/registry
      - ./jwks:/etc/jwks
      - ./certs:/etc/certs
    restart: unless-stopped

With this configuration, I'm unable to perform docker login because there seems to be a problem resolving the token by kid.

The following error is printed in the registry log when I authenticate

time="2024-12-12T18:40:32.10842518Z" level=info msg="failed to verify token: token signed by untrusted key with ID: \"24KL:52WX:HWNC:R2YW:I4TZ:T6UR:TKBS:TRGT:JCI6:NPHB:QTZW:7KCB\"

The JWT is actually signed by the correct key and is valid, but it registry can't look it up to validate it.

If I modify the jwks file so the format of the kid field matches the expected then it works. An example modification is below:

Original certs file from keycloak:

    {
      "kid": "1xS-6tc9mijrFkcnmfqRmoMpxNNIkea84YTzb6hBgeE",
      "kty": "RSA",
      "alg": "RS256",
      "use": "sig",
      "n": "REPLACED_FOR_BREVITY",
      "e": "AQAB",
      "x5c": [
        "REPLACED_FOR_BREVITY"
      ],
      "x5t": "REPLACED_FOR_BREVITY",
      "x5t#S256": "REPLACED_FOR_BREVITY"
    }

Modified certs file to match the kid format that works with registry:

    {
      "kid": "24KL:52WX:HWNC:R2YW:I4TZ:T6UR:TKBS:TRGT:JCI6:NPHB:QTZW:7KCB",
      "kty": "RSA",
      "alg": "RS256",
      "use": "sig",
      "n": "REPLACED_FOR_BREVITY",
      "e": "AQAB",
      "x5c": [
        "REPLACED_FOR_BREVITY"
      ],
      "x5t": "REPLACED_FOR_BREVITY",
      "x5t#S256": "REPLACED_FOR_BREVITY"
    }

Another data point. On registry version 2.8.3, I don't need to pass REGISTRY_AUTH_TOKEN_JWKS -- Instead, it is able to validate my token using the key in the REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE pem file, which was created by exporting the contents x5c[0] field above and saving as a pem file. This works against the exact same installation as keycloak.

Any suggestions on how to get the tokens to validate in registry v3 without having to modify the jwks file? It seems like this should work directly with the file exported from the keycloak realm /certs endpoint but with the kid field mismatch I'm not sure if that's possible.

This REGISTRY_AUTH_TOKEN_JWKS seems new in v3 so wanted to report this since I'm running onto it.

Reproduce

  1. Deploy Keycloak 26 with 'docker' feature enabled
  2. Create a docker-v2 protocol client called docker-registry in the keycloak realm
  3. Create a user in the keycloak realm and set the password
  4. Download the realm certificates from https://KEYCLOAK_HOST/realms/KEYCLOAK_REALM/protocol/openid-connect/certs and save as ./jwks/jwks.json
  5. Use the docker-compose.yaml above (modified to match your keycloak host and realm) to start registry using docker compose up
  6. Execute docker login using the user and password you configured in Step 3

Expected behavior

User is logged in to the registry

registry version

registry:3.0.0-rc.1

Additional Info

The same keycloak realm keys work fine on registry:2.8.3 but the mechanism is different. In that scenario, save the signing public key to a pem and pass that as the root bundle, and do not set REGISTRY_AUTH_TOKEN_JWKS

Looks related to #4470 though this situation is slightly different. However, if I was able to not specify REGISTRY_AUTH_TOKEN_JWKS and have it fall back to the behavior on 2.8.3 using the ROOTCERTBUNDLE that would be a workaround.

@tomjo
Copy link
tomjo commented Apr 23, 2025

Can confirm the workaround of renaming the kid to the fingerprint of the JWK works.

According to the spec, the structure of the kid value is unspecified (https://datatracker.ietf.org/doc/html/rfc7517#section-4.5), so for the real fix distribution should also calculate the fingerprint based on the modulo and exponent of the JWK instead of expecting the kid to be the fingerprint. When I find some free time I'll look into providing a PR.

Following script can be used to generate the fingerprint programmatically:

#!/bin/bash

# usage:  cat jwks.json | jwks_fingerprint.sh
#         curl -s https://<keycloakhost>/realms/<realm>/protocol/openid-connect/certs | jwks_fingerprint.sh

set -euo pipefail

decode_b64url() {
  local input="${1//_//}"
  input="${input//-/+}"
  local mod4=$(( ${#input} % 4 ))
  if [ "$mod4" -eq 2 ]; then input="$input=="
  elif [ "$mod4" -eq 3 ]; then input="$input="
  elif [ "$mod4" -eq 1 ]; then
    echo "Invalid base64url input length" >&2
    return 1
  fi
  echo "$input" | base64 -d
}

JWKS_JSON=$(cat)

N_B64=$(echo "$JWKS_JSON" | jq -r '.keys[] | select(.alg == "RS256") | .n' | head -n1)
E_B64=$(echo "$JWKS_JSON" | jq -r '.keys[] | select(.alg == "RS256") | .e' | head -n1)

if [[ "$N_B64" == "null" || -z "$N_B64" ]]; then
  echo "No modulus n found in JWKS matching criteria"
  exit 1
fi

if [[ "$E_B64" == "null" || -z "$E_B64" ]]; then
  echo "No exponent e found in JWKS matching criteria"
  exit 1
fi

N_HEX=$(decode_b64url "$N_B64" | xxd -p | tr -d '\n')
E_HEX=$(decode_b64url "$E_B64" | xxd -p | tr -d '\n')

FINGERPRINT=$( \
  openssl asn1parse -genconf <(cat <<EOF
  asn1=SEQUENCE:pubkey
  [pubkey]
  modulus=INTEGER:0x$N_HEX
  publicExponent=INTEGER:0x$E_HEX
EOF
  ) -out /dev/stdout 2>/dev/null | \
  openssl rsa -RSAPublicKey_in -inform DER -pubout -outform DER 2>/dev/null | \
  sha256sum | \
  awk '{print $1}' | \
  xxd -r -p | \
  base32 | \
  fold -w4 | head -n12 | paste -sd:
)

echo "$FINGERPRINT"

@max-wittig
Copy link
max-wittig commented May 2, 2025

Hi! We just ran into the same issue in the release 3.0.0 version, so we downgraded back to 2.X for now.

/cc @fh1ch @ercanucan

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants
0