diff --git a/docs/specification/shopping/cart/rest.md b/docs/specification/shopping/cart/rest.md index 9ad0f01bc..483b4c5d7 100644 --- a/docs/specification/shopping/cart/rest.md +++ b/docs/specification/shopping/cart/rest.md @@ -482,8 +482,8 @@ operations unless otherwise noted. * **Idempotency-Key**: Operations that modify state **SHOULD** support idempotency. When provided, the server **MUST**: 1. Store the key with the operation result for at least 24 hours. - 2. Return the cached result for duplicate keys whose request body matches the original. - 3. Return `409 Conflict` if the key is reused with a mismatched body. + 2. Return the cached result for duplicate keys whose payload matches the original. + 3. Return `409 Conflict` if the key is reused with a mismatched payload. See [Message Signatures — Idempotency Key Requirements](../../signatures.md#replay-protection) for the full payload-matching contract. diff --git a/docs/specification/shopping/checkout/mcp.md b/docs/specification/shopping/checkout/mcp.md index d0f281c94..b95a5cf0e 100644 --- a/docs/specification/shopping/checkout/mcp.md +++ b/docs/specification/shopping/checkout/mcp.md @@ -109,6 +109,14 @@ The `meta["ucp-agent"]` field is **required** on all requests to enable `meta["idempotency-key"]` for retry safety. Platforms **MAY** include additional metadata fields. +When the request is signed, the covered `Idempotency-Key` header (see +[Message Signing](#message-signing)) is authoritative for the idempotency +key; `meta["idempotency-key"]` **MUST** equal it, and the Business +**MUST** reject a mismatch without executing. On unsigned requests, +`meta["idempotency-key"]` alone is authoritative. See +[Message Signatures — Replay Protection](../../signatures.md#replay-protection) +for the full payload-matching contract. + ## Tools UCP Capabilities map 1:1 to MCP Tools. diff --git a/docs/specification/shopping/checkout/rest.md b/docs/specification/shopping/checkout/rest.md index 1faa4e40f..492e465ed 100644 --- a/docs/specification/shopping/checkout/rest.md +++ b/docs/specification/shopping/checkout/rest.md @@ -1308,8 +1308,8 @@ operations unless otherwise noted. * **Idempotency-Key**: Operations that modify state **SHOULD** support idempotency. When provided, the server **MUST**: 1. Store the key with the operation result for at least 24 hours. - 2. Return the cached result for duplicate keys whose request body matches the original. - 3. Return `409 Conflict` if the key is reused with a mismatched body. + 2. Return the cached result for duplicate keys whose payload matches the original. + 3. Return `409 Conflict` if the key is reused with a mismatched payload. See [Message Signatures — Idempotency Key Requirements](../../signatures.md#replay-protection) for the full payload-matching contract. diff --git a/docs/specification/signatures.md b/docs/specification/signatures.md index 88683e25b..7056de19e 100644 --- a/docs/specification/signatures.md +++ b/docs/specification/signatures.md @@ -823,6 +823,13 @@ Signature-Input: sig1=("@method" "@authority" "@path" "idempotency-key" ...);key Signature: sig1=:6G4i8TS6oUkGrx8KnCFUpsSPwd74...: ``` +Some bindings also carry the key inside the request payload (over MCP, +`meta["idempotency-key"]` in the tool arguments). When the request is +signed, the covered `Idempotency-Key` header is authoritative for the +idempotency key; a payload copy of the key **MUST** equal it, and +Businesses **MUST** reject a mismatch without executing. On unsigned +requests, the payload copy alone is authoritative. + **Idempotency Key Requirements:** | Requirement | Value | @@ -834,18 +841,57 @@ Signature: sig1=:6G4i8TS6oUkGrx8KnCFUpsSPwd74...: | **On duplicate (mismatched payload)** | Reject with `409 Conflict` (REST) / `-32000` (MCP); do not execute | | **On storage failure** | Fail closed (reject request with 503) | -**Payload Matching:** Businesses **MUST** detect whether the payload of -a duplicate-key request matches the payload of the original by -comparing the SHA-256 hash of the raw body bytes — the same digest -RFC 9530 mandates as `Content-Digest`. When signing is in use, this -value is supplied in the `Content-Digest` header and the Intermediary -Warning above guarantees byte fidelity end-to-end; businesses persist -it alongside the idempotency key. For unsigned requests, businesses -compute the same digest from the received body bytes. Platforms -therefore **MUST** generate a fresh idempotency key whenever they -modify the request payload — including retries with modified payment -instruments, updated shipping addresses, swapped line items, or any -other change to the request body. +**Payload Matching:** A stored idempotency key binds the operation it +was first used with and, where one exists, the target resource +identifier (the path parameter for REST, the top-level `id` argument +for MCP). Businesses **MUST** reject, without executing, any request +that reuses a stored key with a different operation or a different +target resource identifier. Within that binding, payload identity is +defined per operation, by whether the operation's arguments carry +anything beyond `meta` and the target resource identifier — a class +decidable from the operation's input schema alone, not from the +transport envelope: + +* **Target-only operations.** The operation's arguments carry nothing + beyond `meta` and the target resource identifier (the path parameter + for REST, the top-level `id` argument for MCP) — `cancel_checkout` is + in this class by schema. Payload identity is the key binding itself: + Businesses persist the operation and the target resource identifier + alongside the key, and **MUST** treat a request as a replay, and + return the stored result, when key, operation, and identifier all + match the stored record. No hashing or canonicalization applies to + this class. +* **Payload-carrying operations.** The operation's arguments carry + anything beyond `meta` and the target resource identifier — + `complete_checkout` is in this class by schema. Businesses **MUST** + detect a payload mismatch by comparing the SHA-256 hash of the + operation's arguments with the transport envelope excluded — not the + full JSON-RPC message bytes, whose envelope fields (the JSON-RPC + `id`, `meta`) are guaranteed to vary per retry. The hash is compared + within the key binding above, so a reused key is rejected on an + operation or target mismatch before any hash comparison, and + Businesses persist this hash alongside the key. The hashed input is + transport specific: + + * For REST, the raw request body bytes as received, the same digest + RFC 9530 mandates as `Content-Digest`. No canonicalization: the + body is a single byte string on the wire and is hashed as such. + * For MCP, the `params.arguments` object with its top-level + `meta` member removed, serialized with the JSON + Canonicalization Scheme + ([RFC 8785](https://datatracker.ietf.org/doc/html/rfc8785)), the + canonicalization this specification already uses for durable + artifacts in + [AP2 Mandates](payment/extensions/ap2-mandates.md#canonicalization). + Removing a member is an operation on a parsed value, not on bytes, + so this class adopts canonicalization explicitly: logically + identical retries hash identically regardless of member order or + serializer, tolerating re-serialization across client stacks. + +Platforms **MUST** generate a fresh idempotency key whenever they modify +a payload-carrying operation's arguments — including retries with +modified payment instruments, updated shipping addresses, swapped line +items, or any other change to the request payload. **Note:** For **default UCP** signatures, the RFC 9421 `created` parameter is **OPTIONAL** and replay protection is handled at the @@ -906,8 +952,10 @@ Signature: sig1=:6G4i8TS6oUkGrx8KnCFUpsSPwd74...: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"complete_checkout","arguments":{"id":"chk_123","checkout":{...}}}} ``` -The JSON-RPC message is the HTTP body. `Content-Digest` binds it to the signature. -No JSON canonicalization is required. +The JSON-RPC message is the HTTP body. `Content-Digest` binds it to the signature; +no JSON canonicalization is required for signing. For payload matching, the +payload-carrying class canonicalizes `params.arguments` with JCS, as defined under +[Replay Protection](#replay-protection). ## Error Handling