Skip to content

api: stamp kid/alg/use on Transit JWKS so Vault's go-oidc picks the key#192

Merged
ExtraToast merged 1 commit into
mainfrom
platform/jwks-stamp-kid
May 11, 2026
Merged

api: stamp kid/alg/use on Transit JWKS so Vault's go-oidc picks the key#192
ExtraToast merged 1 commit into
mainfrom
platform/jwks-stamp-kid

Conversation

@ExtraToast

Copy link
Copy Markdown
Contributor

Summary

Vault sign-in after #190 + #191 still failed:

Provider.VerifyIDToken: invalid id_token: failed to verify signature:
failed to verify id token signature: invalid signature

Diagnosed cryptographically against the live cluster: I signed a known payload via transit/sign/api-jwt, then verified it directly with both the JWKS-published public key and the Transit-engine PEM. Both verify cleanly. Same RSA modulus, RS256 with PKCS1v15+SHA256, signature length 256 — the crypto is correct end-to-end. So the failure was upstream of the actual signature check.

Root cause. The published JWK had no kid, no alg, no use — just {kty, e, n}. VaultTransitJwtEncoder writes kid="api-jwt:v1" into the JWT header. Vault's verifier is coreos/go-oidc-v3, which filters JWKS keys by kid:

for _, key := range keys {
    if keyID == "" || key.KeyID == keyID {        // header.kid="api-jwt:v1" != JWK.KeyID=""
        if payload, err := jws.Verify(&key); err == nil { return payload, nil }
    }
}
return nil, errors.New("failed to verify id token signature")

Every key skipped → error. Nimbus's RSAKey.parseFromPEMEncodedObjects leaves kid/alg/use unset, and VaultTransitJwkSource.refresh() published the parsed key as-is.

Fix

Three lines in VaultTransitJwkSource.refresh() — rebuild each parsed RSAKey with kid/alg/use stamped to match what the encoder writes:

RSAKey.Builder(parsed)
    .keyID("$keyName:v${vk.keyVersion}")
    .algorithm(JWSAlgorithm.RS256)
    .keyUse(KeyUse.SIGNATURE)
    .build()

Unit test now asserts kid="$keyName:v1", alg="RS256", use="sig" on the published JWK. System test (VaultTransitJwksPlaywrightTest) asserts the same on /oauth2/jwks against the live api in the oidc-e2e compose stack.

Verified locally

JWKS public key sha256(n)        = 37cfe9d8…
Transit api-jwt v1 sha256(n)     = 37cfe9d8…   ← identical
JWKS[0] verify of vault-signed:  ✓ VERIFIES
Transit v1 verify of vault-signed: ✓ VERIFIES

After fix, /oauth2/jwks returns:

{"kty":"RSA","e":"AQAB","use":"sig","kid":"api-jwt:v1","alg":"RS256","n":"..."}

Test plan

  • ./gradlew :services:api:test --tests VaultTransitJwkSourceTest (4/4)
  • ./gradlew :services:system-tests:vaultOidcLiveTest against compose stack — green
  • After merge + deploy: vault login -method=oidc completes
  • Headlamp OIDC completes

…cks the key

After #190 fixed the /oauth2/jwks 500, the next Vault sign-in failed with:

  Provider.VerifyIDToken: invalid id_token: failed to verify signature:
  failed to verify id token signature: invalid signature

Diagnosed by signing a known payload via Vault Transit and verifying with
the published JWKS key directly — the crypto is fine, the JWKS public key
matches the Transit-engine public key, RSA-PKCS1v15-SHA256 verifies
cleanly. So the failure was upstream of the actual signature check.

Root cause: VaultTransitJwtEncoder writes kid="api-jwt:v1" into the JWT
header, but VaultTransitJwkSource published JWK objects with no kid/alg/
use (Nimbus's RSAKey.parseFromPEMEncodedObjects leaves those unset). The
verifier (coreos/go-oidc-v3, used by Vault's OIDC auth) filters JWKS keys
by kid:

  for _, key := range keys {
      if keyID == "" || key.KeyID == keyID {
          if payload, err := jws.Verify(&key); err == nil { return payload }
      }
  }
  return errors.New("failed to verify id token signature")

JWT header.kid="api-jwt:v1" != JWK.KeyID="" → every key skipped → error.

Fix is mechanical: rebuild each published JWK with kid="$keyName:v$version"
(matching the encoder), alg=RS256, use=sig.

Unit test now asserts kid/alg/use on the published JWK. System test
(VaultTransitJwksPlaywrightTest) asserts the same on /oauth2/jwks against
the live api. Verified locally: signing-then-verifying round-trips
through the JWKS public key.
@ExtraToast ExtraToast added the bug Something isn't working label May 11, 2026
@ExtraToast ExtraToast self-assigned this May 11, 2026
@ExtraToast ExtraToast merged commit 58ea6dc into main May 11, 2026
27 of 28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant