You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
verifyParsedCredential() verifies the JWT proof (proof.jwt) but does not reconcile the outer credential object against the decoded payload. On the parsed-credential input path, all subsequent checks therefore run against caller-supplied fields rather than the signed content.
Where
packages/vc/src/verification/verify-proof.ts — verifyJwtProof() calls verifyCredential(proof.jwt, resolver) and discards the decoded result; it only confirms the proof JWT is validly signed.
packages/vc/src/verification/verify-parsed-credential.ts — expiry, revocation, the trustedIssuers check (reads credential.issuer.id), and every ClaimVerifier then operate on the outer object passed by the caller.
Impact
When a caller passes a pre-parsed Verifiable<W3CCredential> (rather than a JWT string), they can attach any validly-signed proof.jwt and freely set the outer credentialSubject, issuer, expiry, etc. Concretely:
The trustedIssuers check compares the caller-supplied credential.issuer.id, so it can be set to a trusted DID while proof.jwt is signed by an attacker's own (resolvable) DID.
The JWT-string input path is unaffected, since parseJwtCredential() reconstructs the credential from the signed payload.
Proposed fix
After verifyProof() validates a JWT proof, derive the credential fields from the decoded proof.jwt payload (or reject parsed credentials whose outer object does not match the signed payload), so downstream checks operate on signed content for every credential type. This is cross-cutting, so it warrants its own change + tests rather than being handled per-consumer.
Interim mitigation
#88 hardens the receipt path specifically by re-deriving receipt fields from proof.jwt when given a parsed credential. This issue tracks the general fix in @agentcommercekit/vc.
Summary
verifyParsedCredential()verifies the JWT proof (proof.jwt) but does not reconcile the outer credential object against the decoded payload. On the parsed-credential input path, all subsequent checks therefore run against caller-supplied fields rather than the signed content.Where
packages/vc/src/verification/verify-proof.ts—verifyJwtProof()callsverifyCredential(proof.jwt, resolver)and discards the decoded result; it only confirms the proof JWT is validly signed.packages/vc/src/verification/verify-parsed-credential.ts— expiry, revocation, thetrustedIssuerscheck (readscredential.issuer.id), and everyClaimVerifierthen operate on the outer object passed by the caller.Impact
When a caller passes a pre-parsed
Verifiable<W3CCredential>(rather than a JWT string), they can attach any validly-signedproof.jwtand freely set the outercredentialSubject,issuer, expiry, etc. Concretely:paymentOptionId/paymentRequestToken).trustedIssuerscheck compares the caller-suppliedcredential.issuer.id, so it can be set to a trusted DID whileproof.jwtis signed by an attacker's own (resolvable) DID.The JWT-string input path is unaffected, since
parseJwtCredential()reconstructs the credential from the signed payload.Proposed fix
After
verifyProof()validates a JWT proof, derive the credential fields from the decodedproof.jwtpayload (or reject parsed credentials whose outer object does not match the signed payload), so downstream checks operate on signed content for every credential type. This is cross-cutting, so it warrants its own change + tests rather than being handled per-consumer.Interim mitigation
#88 hardens the receipt path specifically by re-deriving receipt fields from
proof.jwtwhen given a parsed credential. This issue tracks the general fix in@agentcommercekit/vc.