fix(vc): bind credential verification to the signed proof (#105, #108)#110
Conversation
verifyProof() now returns the credential decoded from proof.jwt, and verifyParsedCredential() runs expiry, revocation, trusted-issuer, and claim-verifier checks against that verified credential instead of the caller-supplied object. It also returns the verified credential so consumers can read signed fields from the return value rather than the object they passed in. On the parsed-credential input path a caller could previously attach a valid proof.jwt while mutating the outer credentialSubject / issuer / type; those fields were trusted directly. They are now ignored in favor of the signed payload. Adds regression tests proving a spoofed outer issuer, a mutated outer subject, and a mutated outer type cannot bypass verification, and that the returned credential is the signed one. Fixes agentcommercekit#105 Fixes agentcommercekit#108 Signed-off-by: EfeDurmaz16 <efebarandurmaz05@gmail.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WalkthroughThis PR updates the ChangesProof Binding and Verification
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/vc/src/verification/verify-proof.test.ts (1)
16-18:⚠️ Potential issue | 🟡 MinorRemove dead
./verify-credential-jwtmock inverify-proof.test.ts.
packages/vc/src/verification/verify-proof.tsdoesn’t import./verify-credential-jwt, andverifyCredentialJwtonly appears in the mock (lines 16-18) and in the test name—so the mock is unused. Delete it to keep the test focused.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/vc/src/verification/verify-proof.test.ts` around lines 16 - 18, Delete the unused mock declaration vi.mock("./verify-credential-jwt", () => ({ verifyCredentialJwt: vi.fn(), })) from the test file (it’s dead code because verify-proof.ts does not import that module); also update the test name if it still references verifyCredentialJwt so the test name accurately reflects the behavior being tested (remove or rename that phrase).
🧹 Nitpick comments (1)
packages/vc/src/verification/verify-parsed-credential.ts (1)
64-73: 🏗️ Heavy liftDownstream caller may still read spoofed fields from
parsedCredential.Context snippet shows
verifyPaymentReceiptcallsverifyParsedCredentialbut continues usingparsedCredential.credentialSubject.paymentRequestTokenafterward without capturing the returned verified credential. This undermines the binding guarantee for that call site.Consider updating downstream callers to use the returned verified credential, or document clearly in the JSDoc that callers must use the return value.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/vc/src/verification/verify-parsed-credential.ts` around lines 64 - 73, The caller is still reading unverified fields from parsedCredential after calling verifyParsedCredential; update downstream callers (e.g., verifyPaymentReceipt) to use the verified credential returned by verifyParsedCredential (assign the function's return to a variable like verifiedCredential and read paymentRequestToken from verifiedCredential.credentialSubject) rather than parsedCredential, or alternatively update the JSDoc on verifyParsedCredential to explicitly state that callers must use the returned verified credential and then change each call site to follow that contract (ensure any place referencing parsedCredential.credentialSubject.* is changed to reference the returned verified credential or marked as requiring no change if covered by the new JSDoc policy).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@packages/vc/src/verification/verify-proof.test.ts`:
- Around line 16-18: Delete the unused mock declaration
vi.mock("./verify-credential-jwt", () => ({ verifyCredentialJwt: vi.fn(), }))
from the test file (it’s dead code because verify-proof.ts does not import that
module); also update the test name if it still references verifyCredentialJwt so
the test name accurately reflects the behavior being tested (remove or rename
that phrase).
---
Nitpick comments:
In `@packages/vc/src/verification/verify-parsed-credential.ts`:
- Around line 64-73: The caller is still reading unverified fields from
parsedCredential after calling verifyParsedCredential; update downstream callers
(e.g., verifyPaymentReceipt) to use the verified credential returned by
verifyParsedCredential (assign the function's return to a variable like
verifiedCredential and read paymentRequestToken from
verifiedCredential.credentialSubject) rather than parsedCredential, or
alternatively update the JSDoc on verifyParsedCredential to explicitly state
that callers must use the returned verified credential and then change each call
site to follow that contract (ensure any place referencing
parsedCredential.credentialSubject.* is changed to reference the returned
verified credential or marked as requiring no change if covered by the new JSDoc
policy).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 72d9beac-de44-467f-bdfa-9ed19bf983af
📒 Files selected for processing (5)
.changeset/bind-parsed-credential-to-proof.mdpackages/vc/src/verification/verify-parsed-credential.test.tspackages/vc/src/verification/verify-parsed-credential.tspackages/vc/src/verification/verify-proof.test.tspackages/vc/src/verification/verify-proof.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 794e138e17
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ) | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Consume the verified credential before reading receipt fields
Because this now only returns the credential decoded from proof.jwt and leaves the caller-supplied object unchanged, internal call sites that ignore the return value can still consume unsigned outer fields. For example, verifyPaymentReceipt calls verifyParsedCredential(...) and then reads parsedCredential.credentialSubject.paymentRequestToken at packages/ack-pay/src/verify-payment-receipt.ts:82-90; with parsed-object input, a caller can keep a valid signed receipt proof while swapping the outer paymentRequestToken, so the signed subject is verified but the swapped token is the one returned/verified. Please update those consumers to use this returned credential, or otherwise replace/mutate the input before returning from verification.
Useful? React with 👍 / 👎.
TL;DR
On the parsed-credential input path,
verifyParsedCredential()verifiedproof.jwtbut then ran expiry, revocation,trustedIssuers, and claim-verifier checks against the caller-supplied outer object. A caller could present a validly-signedproof.jwtwhile mutating the outercredentialSubject/issuer/type, and those mutated fields were trusted. This binds all verification to the signed payload. Fixes #105 and #108.What changed (
@agentcommercekit/vc)verifyProof()now returns the credential decoded fromproof.jwt(previouslyvoid).verifyParsedCredential()runs every downstream check against that verified credential, and returns it, so consumers can read signed fields from the return value instead of the object they passed in.Where to look
packages/vc/src/verification/verify-proof.ts—verifyJwtProofreturns the decoded credential.packages/vc/src/verification/verify-parsed-credential.ts— expiry / revocation / trusted-issuer / claim-verifier checks and the return value now use the verified credential.Risk / compatibility
verifyProofandverifyParsedCredentialchange fromvoidto the verified credential. This is backward-compatible: callers that ignore the return value are unaffected. Patch changeset included.Tests
verify-parsed-credential.test.tsonto real signed credentials (no mocked proof).issuer(pointed at a trusted DID) is rejected; claim verifiers receive the signed subject rather than a mutated one; claim-verifier selection uses the signedtype; and the returned credential is the signed one.verify-proof.test.tsasserts the decoded credential is returned.@agentcommercekit/vc: 51 tests passing. Repo-wide:check:types,test,lint(0 errors), andoxfmt --checkall pass.Relationship to #88
#88 hardens the ACK-Pay receipt path specifically and adds the
paymentOptionId↔ Payment Request binding, which lives at the ACK-Pay layer and is not covered here. This PR is the general fix at the@agentcommercekit/vclayer and is complementary. WithverifyParsedCredentialnow returning the canonical credential, the receipt path's local re-parse in #88 can later be simplified to consume the return value.Reviewed with ChatGPT 5.5 (xhigh reasoning) in addition to the author. Areas that may still warrant human review: (1) consumer adoption —
verifyParsedCredentialnow returns the canonical credential, and call sites that read fields after verification should switch to the return value (the receipt path is separately hardened in #88); (2) expiry / revocation binding is covered structurally because those checks now receive the verified credential, but the unit tests mockisExpired/isRevoked, so a maintainer may want an end-to-end test with a genuinely expired or revoked signed credential.Summary by CodeRabbit
Bug Fixes
Tests