fix(security): bind UCAN JWT verification to the iss key, not the kid header#588
Merged
Merged
Conversation
… header UCAN.fromJWT verified the EdDSA signature against the public key named in the attacker-controlled `kid` header, then trusted the `iss` claim without checking the two agree. An attacker could sign a token with their own key, place that key in `kid`, and claim any `iss` (e.g. a trusted root) — an issuer-spoofing authorization bypass. The forged token passed validateJWT / parseTransportUCANs, and via UCANValidator.capabilitiesFor(..., issuer=root) yielded capabilities attributed to the root. Reachable from transport input (e.g. DlfsMcpTools). Fix: derive the verification key from the `iss` DID (did:key encodes the issuer key) and verify against it; the `kid` header is no longer trusted for key selection. Legitimately signed tokens are unaffected (kid == iss key already). The CAD3 path (UCAN.verifySignature / validate) was already correct. Audit of the other JWT verification paths: - PeerAuth.verifySelfIssued: safe — binds sub to the signing key (sub == did:key(kid)) - PeerAuth.verifyPeerSigned / OAuthService (JWKS RS256) / JWKSKeys: safe Added a SECURITY WARNING to the kid-trusting JWT.verifyEdDSA()/verifyPublic(jwt). Adversarial tests (UCANTest): forge iss=ROOT signed by a rogue key and assert rejection at fromJWT, validateJWT, the transport boundary, and in a proof chain; plus a regression that genuine tokens still verify. Confirmed all four fail against the pre-fix kid-based verification. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Vulnerability
UCAN.fromJWTverified the EdDSA signature against the public key named in the attacker-controlledkidheader, then trusted theissclaim without checking the two agree. An attacker could sign a token with their own key, place that key inkid, and claim anyiss(e.g. a trusted root) — an issuer-spoofing authorization bypass.The forged token passed
validateJWT/parseTransportUCANs, and viaUCANValidator.capabilitiesFor(..., issuer=root)yielded capabilities attributed to the root. Reachable from transport input (e.g.DlfsMcpTools).Fix
Derive the verification key from the
issDID (did:keyencodes the issuer's public key) and verify against it. Thekidheader is no longer trusted for key selection. Legitimately signed tokens are unaffected (kidalready equals theisskey). The CAD3 path (UCAN.verifySignature/validate) was already correct.Audit of other JWT verification paths
UCAN.fromJWTPeerAuth.verifySelfIssuedsub == did:key(kid)(covered bytestSelfIssuedJWTKidMismatch,testBadlySignedJWTWithCorrectAudience)PeerAuth.verifyPeerSignedOAuthService(JWKS RS256)kidselects among the provider's trusted keys;iss/audvalidated)JWKSKeysAdded a
SECURITY WARNINGto thekid-trustingJWT.verifyEdDSA()/verifyPublic(jwt)to prevent reintroduction.Adversarial tests
UCANTestnow forgesiss=ROOTsigned by a rogue key and asserts rejection atfromJWT,validateJWT, the transport boundary (parseTransportUCANs→capabilitiesFor), and inside a proof chain; plus a regression that genuine tokens still verify. These four were confirmed to fail against the pre-fixkid-based verification (the forged token was accepted asiss=ROOTgrantingcan:"*"), then pass after the fix.Testing
PeerAuthTest): 20 pass🤖 Generated with Claude Code