Repository navigation
Conversation
…ction The verifier could encode transaction_data but nothing ever supplied it: CreateRequestObject's only caller passed nil, the request struct had no payload or transaction_data_hashes_alg, and the session did not keep the strings that were sent, so there was nothing to validate a response against. - openid4vp.TransactionData gains payload and transaction_data_hashes_alg (an array, always sent: TS12 4.2 makes it REQUIRED). - openid4vp.NewTS12PaymentTransactionData builds urn:eudi:sca:payment:1 with the schema's required members (transaction_id 1..36, payee name+id, ISO 4217 currency, amount as a JSON number > 0) and refuses the rest. - /ui/interaction accepts an optional "payment" object (verifier.ts12.enable, default off). The credential query it binds to must be dc+sd-jwt with holder binding; anything else is refused with 400. - The encoded strings are stored on the AuthorizationContext and sent in the request object (and its DC API variant), identical bytes. - A payment session is never reused, and a payment session is never served to a request that did not ask for one. - Golden vectors from the WMP spec repo pin the hash input (raw string). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ponse_mode) The validator already compared transaction_data_hashes, but no caller ever set TransactionData, so a payment request was never checked. Wire it into the standalone verifier's direct_post path and add what TS12 v1.0.1 3.6 asks of the KB-JWT. - Per-credential scoping: a presentation owes the hashes of the entries whose credential_ids reference its query id, in request order; every entry must be covered by some presented credential (OpenID4VP and TS12 leave the scope open, noted in the PR). - transaction_data_hashes_alg: a string in the KB-JWT, one the request allowed, sha-256 when the request named none; an array (the wallet-common shape) is refused. Hashes are over the strings as sent. - TS12 claims, applied when an entry is a urn:eudi:sca:* type: amr as an array of single-key objects from the closed v1.0.1 vocabulary with >= 2 different categories; response_mode echoing one the request was issued with; jti present and never seen before (replay cache, atomic SetNX). - Fail closed: no replay store, no configured response modes, an unreadable entry, or a non-SD-JWT answer to a bound query all refuse with invalid_transaction_data. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
/payment (verifier.ts12.enable) is a small form - amount, currency, payee, SCA attestation vct - that posts a payment to /ui/interaction, shows the QR code and wallet link, and reports whether the wallet's response was accepted. docs/TS12_PAYMENT_SCA.md gives the same flow as curl commands. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Reject a credential_id that names more than one credential query instead of silently binding the last one. - Count transaction_id characters as code points (JSON Schema maxLength), not UTF-8 bytes. - Check currency against an ISO 4217 alpha-3 snapshot instead of any three capital letters. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ng (review) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Review: the KB-JWT response_mode must be checked against what the session was issued with, not the receiving node's current configuration. Record the modes on the AuthorizationContext at issuance (B reads them). The payment builder also insists the bound query names the attestation with meta.vct_values, which B compares against the presented credential's vct. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ued response modes (review) - Refuse a credential whose vct is not one of the bound query's vct_values (trust evaluation does not enforce the requested type). - Check response_mode against the modes recorded on the session at issuance, not the receiving node's current configuration. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…sult fetch (review) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…sued modes (review) - Return the shared pkg/oauth2.OAuthError so RegEndpoint renders the status and description (the verifier-local type fell through to the generic handler). - A wrongly-typed or truncated /ui/interaction body is a 400, not a 500. - Resuming a payment session does not issue a DC API variant the session was not issued with. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ed (review) Validate no longer touches the replay store: it reports PresentedJTI, and the direct_post handler claims it after issuer trust and the vct check. A fabricated credential can no longer reserve chosen jti values. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…iew) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…pper guess (review) json.Number made the JSON decoder reject "token" with a plain error that the status mapper read as 401/404. PaymentAmount keeps the literal and the payment builder refuses it as invalid_request. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…efused (review) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
PresentedJTI is an output, and the caller CLAIMS a non-empty one in a JTIStore after issuer authentication - a state change that refuses any later presentation carrying that jti. A validator reused for a second presentation that reports no jti kept the first one, so the caller claimed it again and rejected an unrelated, valid presentation as a replay. Cleared at the top of Validate rather than at the point of use: the field is written only on the SCA path, so every path that does not write it has to leave it empty. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AB3apg5kCEGNPozrhSggum
Both DCQL refusals returned an unstructured error, which carries no status, so httphelpers.StatusCode inferred one from the message text - and the message names the credential query id, which the CALLER chose. A query named "token" matched the "unauthorized, authentication, token" case and was answered 401, before the "invalid" case was reached, with the description dropped on the way out. A caller must not be able to choose the status code of its own refusal. The empty-DCQL refusal was worse than the reported case: with nothing in its text to infer from it fell through to 500. Payment requests keep the "payment:" prefix, so one refused by this loop - before buildPaymentTransactionData runs - reads like every other payment refusal rather than like a different class of failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AB3apg5kCEGNPozrhSggum
One decode with an empty fallback served both outcomes, so a successful
/ui/interaction response whose body could not be read left reply = {}.
The page then set the QR image to "data:image/png;base64,undefined",
linked to "undefined" and polled session_id=undefined 150 times: the
operator sees a payment waiting for a wallet that was never asked for
anything.
The fallback now lives on the error branch, where a non-JSON body is
legitimate. A successful response must decode, and must carry the three
members the page goes on to use - a 200 with a well-formed body missing
session_id did exactly the same thing as a truncated one.
There is no JS test runner here, so the regression test pins the
structure of the handler instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AB3apg5kCEGNPozrhSggum
…device The link was an ordinary anchor, so no session was ever marked same-device and /verification/direct_post returned no redirect_uri: the wallet had nowhere to send the user back to, and the operator had to find the polling page by hand. It hands off through wallet-handoff.js rather than a second copy of it. That ordering is not incidental - Chrome on Android only lets a page launch a custom scheme while it still holds the click's transient activation, and awaiting the preference POST first loses it silently, with no exception and no intent. Armed on the LINK only. Scanning the QR code is a cross-device flow and must not be marked same-device, and a modified click opens somewhere else, which is not this browser following the redirect back. The arming lives in payment-handoff.js so it can be driven by node --test like the handoff it wraps: which clicks hand off, what the preference says, that the href is read at click time, and that nothing is awaited before a custom-scheme launch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SonarCloud's quality gate failed on new_reliability_rating, from the one reliability issue in the new code: handOffToWallet's promise was neither awaited nor caught. A click handler cannot await it - that is the whole point of the module - so the rejection is caught instead, and a failed handoff is a console line rather than an unhandled rejection nobody sees. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The decoder reports two errors for "there is no value here": io.EOF when the body ends before one starts, io.ErrUnexpectedEOF when it stops part-way through. Only the second was mapped, so a whitespace-only body with a positive Content-Length fell through to the status mapper - no status to read - and the caller was told the verifier had broken. Probing that turned up the worse case next to it. The binding skips decoding a body of ZERO length entirely: no decode, no validation, no error. So an empty POST reached UIInteraction as a zero-valued request, and everything there dereferences the query - a nil pointer dereference, a stack trace, and a 500 where a structured 400 was owed. Guarded in apiv1 rather than at the endpoint, so every caller of UIInteraction gets it and the rule lives where the requirement does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…feat/ts12-response-validation
Every multi-entry case here used entries carrying identical defaults, so the intersection was never exercised: reading only the first entry's list, or unioning the lists instead of intersecting them, passed all of them. The new cases make the entries disagree on purpose - overlapping, disjoint, and an explicit list paired with an absent one - and check both directions, since a union bug shows up as an ACCEPTED algorithm only one entry allowed. That is a wallet hashing with something the other entry's issuer never agreed to. Each entry carries a distinct payload, so an entry silently dropped is a count mismatch rather than a coincidental pass. The overlapping case also asserts that the first entry ALONE would have allowed sha-256, which is what makes refusing it meaningful rather than a blanket no. Probed both regressions the coverage gap hid: first-entry-only and union each fail all four subtests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nto feat/ts12-operator-page
A wallet response refused for transaction_data reached the status mapper as unstructured text, so its status was guessed from the message: "missing" answered 404 and "VP Token" matched "token" for 401. Refusals now go through invalidTransactionData, a structured oauth2.OAuthError with invalid_transaction_data and status 400. This covers the validator's transaction_data failures, an unreadable session entry, a non-SD-JWT answer, an uncovered entry, the vct check and the jti check. The jti was claimed inside the per-credential loop, so a response refused later (another credential, the coverage check) had already used up its jti. The jti values are now collected during the loop and claimed only after every check has passed. Remove the unused openid4vp.JTIStore interface and the comments that referred to it. Tests: every TS12 refusal case now also asserts 400, and a new test checks that a refusal after the SCA credential passes leaves its jti unclaimed.
|
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.



No description provided.