fix!: bind signed REST responses to their request - #699
Open
sakinaroufid wants to merge 3 commits into
Open
Conversation
Response signatures cover only @status, content-digest and content-type. Nothing binds a response to the request's method, path, query or body, and `created` is optional and never read by the verifier, so there is no freshness bound either. A correctly signed response therefore stays valid after it is detached from the request it answered. That matters for the adversary UCP already designs against in the Intermediary Warning: something that terminates TLS and can relay bytes but cannot mint signatures, such as a CDN edge, WAF or gateway sitting in front of a Business. Two replays follow. Rollback: a signed response captured earlier is replayed verbatim against a later request for the same resource. Every covered component still matches, so the Platform accepts a stale totals or status as current. Checking the body does not help, because the body is a real, correctly signed earlier state of the correct object, and it passes the Platform side totals check in checkout.md since it was internally consistent when issued. The Business stays authoritative and settles on its own state, so this is not a path to theft. The damage is that the Platform and buyer act on stale terms, which matters for autonomous completion and for AP2 mandates formed over that data. Cross-request substitution: a signed response to one request served against a different request to the same origin, since neither method, path nor query is covered. Mostly mitigated already by self-describing bodies carrying id, but the same fix closes it for free. Use RFC 9421 section 2.4 request-response binding. Response signatures now cover @authority, @method and @path from the associated request via the `;req` parameter, plus @query and content-digest when the request carried them. Verifiers rebuild those components from the request they actually sent, not from anything the response claims. Make `created` required on response signatures with an enforced window (300s recommended). Binding alone does not bound how long a captured response can be replayed against a repeated identical request, which is the polling case above. Replay Protection read as though idempotency keys covered both directions. They are request only. That section now says so and points at the response side mechanism. BREAKING CHANGE: verifiers reject response signatures that omit the `;req` components or `created`. Signers and verifiers have to move together.
10 tasks
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.
Description
Signed REST responses are not bound to the request that produced them.
Proposal: #698. This PR is the reference spec change for it, opened for review alongside the proposal rather than as a request to merge ahead of TC approval.
Problem
REST Response Signing covers three components:
@status,content-digest,content-type. The verifier enforces only those. Nothing ties a response to the request's method, path, query or body, nothing ties it to the serving origin, andcreatedis OPTIONAL and never read by the verifier, so there is no freshness bound either.A correctly signed response therefore stays valid once detached from its request. The adversary that matters here is not a network attacker, since TLS covers that. It is something that terminates TLS and can relay bytes but cannot mint signatures, because the signing key lives at the origin and not at the edge: a CDN, WAF, API gateway or reverse proxy in front of the Business. That is the same adversary the spec's own Intermediary Warning already assumes.
Rollback / stale replay. A signed
200captured earlier is replayed verbatim against a later request for the same resource:GET /checkout-sessions/chk_123and gets a signed200withtotalsof $40.00.@statusstill matches,content-digestmatches the stale body's own bytes,content-typematches, and the Business's key is live. Verification passes.Checking the body does not catch this. The body is a real, correctly signed earlier state of the correct object, and since it was internally consistent when issued it also passes the Platform side totals check in checkout.md.
Scope of the damage: the Business stays authoritative and settles against its own state (checkout.md), so it charges $60.00 and this is not a route to theft. What breaks is that the Platform and buyer act on stale terms. That matters where a Platform completes autonomously, and it matters for AP2 mandates, whose whole point is non-repudiable authorization over data that turns out to be attacker-selected. The same trick applies to replaying a superseded
"status": "completed"or an old payment authorization response.Cross-request substitution. A signed response to one request served against a different request to the same origin, since neither method, path nor query is covered. Mostly mitigated in practice by self-describing bodies carrying
id. It is the weaker of the two, and it gets closed for free by the same fix.Solution
Use RFC 9421 §2.4 request-response binding. It is a parameter in an RFC the spec already depends on, so there is no new UCP construct here. Response signatures cover
@authority,@methodand@pathfrom the associated request via;req, plus@queryandcontent-digestwhen the request carried them.Design decisions:
;reqcomponents from the request they actually sent, never from anything the response claims. Rebuilding from the response would be self-certifying and would bind nothing. This is stated normatively at the reconstruction step.createdbecomes REQUIRED on responses, with an enforced window (300s RECOMMENDED, not MUST, so deployments with worse clock skew can widen it). Binding alone does not bound how long a captured response can be replayed against a repeated identical request, which is the polling case above. It stays OPTIONAL on requests, where idempotency keys already handle replay.Before / After
createdwindow)/checkout-sessions/Aserved for/checkout-sessions/B@path;req)@authority;req)createdTrade-offs
keys[]to canonical profile signing key field #566.;reqsupport is uneven across RFC 9421 libraries. Signers on a library without it have to build the signature base by hand. That is the main adoption cost and the most likely thing to get pushback.Scope
Docs only in this repo. Nothing under
source/encodes covered components (profile.jsonandap2_mandate.jsonmention signatures only in key and JWK descriptions), and no Python inmain.py,hooks.pyorscripts/parsesSignature-Input. The spec text is the product, so the prose change is the protocol change.Per CONTRIBUTING § Significant Changes this counts twice over, as a Protocol Change and as Backwards Incompatibility, so it needs the proposal in #698 and TC approval before merge.
Category (Required)
Please select one or more categories that apply to this change.
ucp-schematool (resolver, linter, validator). (Requires Maintainer approval)Related Issues
Proposal: #698
Checklist
!for breaking changes).No schema or model changes: no JSON Schema encodes signature covered components, so there is nothing to update or regenerate. No tests added because nothing in this repo can assert on covered components.
validate_examples.pyvalidates fenced JSON bodies against schemas and never parsesSignature-Input, so this belongs in the conformance suite. The Test Plan in #698 covers it: a signer emitting the;reqset, plus negative cases for a response replayed against a different request, one outside thecreatedwindow, and a verifier that rebuilds;reqfrom the response instead of its own request. Verification: doc example corpus 292 passed / 0 failed / 48 skipped,markdownlintclean acrossdocs/specification/*.md,cspellclean on the three changed files.Screenshots / Logs (if applicable)
No rendering change. Table rows, fenced
textandhttpblocks, and prose.