diff --git a/.cspell.json b/.cspell.json index 27f65385..3825edcf 100644 --- a/.cspell.json +++ b/.cspell.json @@ -18,7 +18,11 @@ "ignoreRegExpList": [ "src\\s*=\\s*(\"[^\"]*\"|'[^']*')", "(\"token\"|token)\\s*:\\s*(\"[^\"]*\"|'[^']*')", - "(\"auth_jwt\"|auth_jwt)\\s*:\\s*(\"[^\"]*\"|'[^']*')" + "(\"auth_jwt\"|auth_jwt)\\s*:\\s*(\"[^\"]*\"|'[^']*')", + "(\"x\"|\"y\")\\s*:\\s*\"[A-Za-z0-9_-]+\"", + "sig1=:[A-Za-z0-9+/=_.]+:", + "eyJ[A-Za-z0-9_-]+\\.\\.?[A-Za-z0-9_-]*", + "M[A-Z][A-Za-z0-9]{3,}\\.\\.\\." ], "dictionaryDefinitions": [ { diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index bfae6fd2..de4517fa 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -79,9 +79,11 @@ repudiable schemas sdjwt shopify +streamable superfences upsell upsells vulnz yaml yml +keyid diff --git a/docs/specification/ap2-mandates.md b/docs/specification/ap2-mandates.md index aad35f84..5df9d352 100644 --- a/docs/specification/ap2-mandates.md +++ b/docs/specification/ap2-mandates.md @@ -123,15 +123,16 @@ If a public key cannot be resolved, or if the signature is invalid, the business ## Cryptographic Requirements -### Signature Algorithm +This extension uses the cryptographic primitives defined in the +[Message Signatures](signatures.md) specification: -All signatures **MUST** use one of the following algorithms: +* **Algorithms:** ES256 (required), ES384, ES512 +* **Canonicalization:** JCS ([RFC 8785](https://datatracker.ietf.org/doc/html/rfc8785)) +* **Key Format:** JWK ([RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517)) +* **Key Discovery:** `signing_keys[]` in `/.well-known/ucp` -| Algorithm | Description | -| :-------- | :---------------------------------------------------- | -| `ES256` | ECDSA using P-256 curve and SHA-256 (**RECOMMENDED**) | -| `ES384` | ECDSA using P-384 curve and SHA-384 | -| `ES512` | ECDSA using P-521 curve and SHA-512 | +See [Message Signatures](signatures.md) for complete details on algorithms, +key format, and key rotation. ### Business Authorization @@ -213,17 +214,26 @@ selective disclosure, key binding) is defined by the ### Canonicalization -For signature computation over JSON payloads, implementations **MUST** use -**JSON Canonicalization Scheme (JCS)** as defined in -[RFC 8785](https://datatracker.ietf.org/doc/html/rfc8785). +All JSON payloads **MUST** be canonicalized using **JSON Canonicalization +Scheme (JCS)** per [RFC 8785](https://datatracker.ietf.org/doc/html/rfc8785). -JCS produces a deterministic, byte-for-byte identical representation of -JSON data, ensuring signatures can be verified regardless of whitespace, -key ordering, or Unicode normalization differences. +**Why JCS for Mandates?** UCP request signatures use `Content-Digest` (raw +bytes) without canonicalization — the request is signed and verified +immediately over the same HTTP connection. Mandates are different: -**Canonicalization Rule:** When computing the business's signature, exclude -the `ap2` field entirely. This ensures future AP2 fields are automatically -handled. +* **Durability** — Mandates are stored as evidence of user consent. They may + be retrieved and verified days or months later. +* **Cross-system transmission** — Mandates pass through multiple systems + (platform → business → PSP → card network) that may re-serialize JSON. +* **Reproducibility** — Any party must reconstruct the exact signed bytes + from the logical JSON content, regardless of serialization differences. + +JCS ensures that semantically identical JSON produces byte-identical output, +making signatures reproducible across implementations and time. + +**AP2-Specific Rule:** When computing the business's `merchant_authorization` +signature, exclude the `ap2` field entirely. This ensures future AP2 fields +are automatically handled. ## The Mandate Flow diff --git a/docs/specification/cart.md b/docs/specification/cart.md index 29756608..6c00a575 100644 --- a/docs/specification/cart.md +++ b/docs/specification/cart.md @@ -86,7 +86,7 @@ SHOULD be linked for the duration of the checkout. * **After checkout completion** — Business MAY clear the cart based on TTL, completion of the checkout, or other business logic. Subsequent operations - on a cleared cart ID return `NOT_FOUND`; the platform can start a new + on a cleared cart ID return `not_found`; the platform can start a new session with `create_cart`. ## Guidelines @@ -96,7 +96,7 @@ SHOULD be linked for the duration of the checkout. * **MAY** use carts for pre-purchase exploration and session persistence. * **SHOULD** convert cart to checkout when user expresses purchase intent. * **MAY** display `continue_url` for handoff to business UI. -* **SHOULD** handle `NOT_FOUND` gracefully when cart expires or is canceled. +* **SHOULD** handle `not_found` gracefully when cart expires or is canceled. ### Business @@ -134,7 +134,7 @@ information for localized pricing estimates. ### Get Cart -Retrieves the latest state of a cart session. Returns `NOT_FOUND` if the cart +Retrieves the latest state of a cart session. Returns `not_found` if the cart does not exist, has expired, or was canceled. * [REST Binding](cart-rest.md#get-cart) @@ -152,7 +152,7 @@ state on the business side. ### Cancel Cart Cancels a cart session. Business MUST return the cart state before deletion. -Subsequent operations for this cart ID SHOULD return `NOT_FOUND`. +Subsequent operations for this cart ID SHOULD return `not_found`. * [REST Binding](cart-rest.md#cancel-cart) * [MCP Binding](cart-mcp.md#cancel_cart) diff --git a/docs/specification/checkout-mcp.md b/docs/specification/checkout-mcp.md index 68792956..65042d72 100644 --- a/docs/specification/checkout-mcp.md +++ b/docs/specification/checkout-mcp.md @@ -650,6 +650,73 @@ as JSON-RPC `result` with `structuredContent` containing the UCP envelope and } ``` +## Message Signing + +Platforms **SHOULD** authenticate agents when using MCP transport. When using +HTTP Message Signatures, all checkout operations follow the +[Message Signatures](signatures.md) specification. + +### Request Signing + +UCP's MCP transport uses **streamable HTTP**, allowing the same RFC 9421 +signature mechanism as REST. The signature is applied at the HTTP layer: + +| Header | Required | Description | +| :----------------------- | :------- | :--------------------------------------- | +| `Signature-Input` | Yes | Describes signed components | +| `Signature` | Yes | Contains the signature value | +| `Content-Digest` | Yes | SHA-256 hash of request body | +| `UCP-Agent` | Yes | Signer identity (profile URL) | +| `Idempotency-Key` | Cond.* | Unique key for replay protection | + +\* Required for `complete_checkout` and `cancel_checkout` + +**Example Signed Request:** + +```http +POST /mcp HTTP/1.1 +Host: business.example.com +Content-Type: application/json +UCP-Agent: profile="https://platform.example/.well-known/ucp" +Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 +Content-Digest: sha-256=:RK/0qy18MlBSVnWgjwz6lZEWjP/lF5HF9bvEF8FabDg=: +Signature-Input: sig1=("@method" "@path" "content-digest" "content-type" "ucp-agent" "idempotency-key");keyid="platform-2026" +Signature: sig1=:MEUCIQDXyK9N3p5Rt...: + +{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"complete_checkout","arguments":{"id":"checkout_abc123","checkout":{"payment":{...}}}}} +``` + +The `Content-Digest` binds the JSON-RPC body to the signature. No JSON +canonicalization is required. + +See [Message Signatures - MCP Transport](signatures.md#mcp-transport) +for details. + +### Response Signing + +Response signatures are **RECOMMENDED** for: + +* `complete_checkout` responses (order confirmation) + +Response signatures are **OPTIONAL** for: + +* `create_checkout`, `get_checkout`, `update_checkout`, `cancel_checkout` + +**Example Signed Response:** + +```http +HTTP/1.1 200 OK +Content-Type: application/json +Content-Digest: sha-256=:Y5fK8nLmPqRsT3vWxYzAbCdEfGhIjKlMnO...: +Signature-Input: sig1=("@status" "content-digest" "content-type");keyid="merchant-2026" +Signature: sig1=:MFQCIH7kL9nM2oP5qR8sT1uV4wX6yZaB3cD...: + +{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"..."}],"structuredContent":{"checkout":{"id":"checkout_abc123","status":"completed"}}}} +``` + +See [Message Signatures - REST Response Signing](signatures.md#rest-response-signing) +for the signing algorithm (identical for MCP over HTTP). + ## Conformance A conforming MCP transport implementation **MUST**: @@ -662,6 +729,12 @@ A conforming MCP transport implementation **MUST**: 5. Validate tool inputs against UCP schemas. 6. Support HTTP transport with streaming. +A conforming implementation **SHOULD**: + +1. Authenticate agents using one of the supported mechanisms (API keys, OAuth, + mTLS, or HTTP Message Signatures per [Message Signatures](signatures.md)). +2. Verify authentication on incoming requests before processing. + ## Implementation UCP operations are defined using [OpenRPC](https://open-rpc.org/) (JSON-RPC diff --git a/docs/specification/checkout-rest.md b/docs/specification/checkout-rest.md index 56d59348..e68aeb00 100644 --- a/docs/specification/checkout-rest.md +++ b/docs/specification/checkout-rest.md @@ -1282,6 +1282,67 @@ with HTTP 200 and the UCP envelope containing `messages`: } ``` +## Message Signing + +Platforms **SHOULD** authenticate agents when using REST transport. When using +HTTP Message Signatures, checkout operations follow the +[Message Signatures](signatures.md) specification. + +### Request Signing + +Platforms using HTTP Message Signatures **SHOULD** sign requests using RFC 9421: + +| Header | Required | Description | +| :----------------------- | :------- | :--------------------------------------- | +| `Signature-Input` | Yes | Describes signed components | +| `Signature` | Yes | Contains the signature value | +| `Content-Digest` | Cond.* | SHA-256 hash of request body | + +\* Required for requests with a body (POST, PUT) + +**Example Signed Request:** + +```http +POST /checkout-sessions HTTP/1.1 +Host: merchant.example.com +Content-Type: application/json +UCP-Agent: profile="https://platform.example/.well-known/ucp" +Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 +Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=: +Signature-Input: sig1=("@method" "@path" "idempotency-key" "content-digest" "content-type");keyid="platform-2025" +Signature: sig1=:MEUCIQDTxNq8h7LGHpvVZQp1iHkFp9+3N8Mxk2zH1wK4YuVN8w...: + +{"line_items":[{"item":{"id":"item_123"},"quantity":2}]} +``` + +See [Message Signatures - REST Request Signing](signatures.md#rest-request-signing) +for the complete signing algorithm. + +### Response Signing + +Response signatures are **RECOMMENDED** for: + +* `complete_checkout` responses (order confirmation) + +Response signatures are **OPTIONAL** for: + +* `create_checkout`, `get_checkout`, `update_checkout`, `cancel_checkout` + +**Example Signed Response:** + +```http +HTTP/1.1 200 OK +Content-Type: application/json +Content-Digest: sha-256=:Y5fK8nLmPqRsT3vWxYzAbCdEfGhIjKlMnO...: +Signature-Input: sig1=("@status" "content-digest" "content-type");keyid="merchant-2025" +Signature: sig1=:MFQCIH7kL9nM2oP5qR8sT1uV4wX6yZaB3cD...: + +{"id":"chk_123","status":"completed","order":{"id":"ord_456"}} +``` + +See [Message Signatures - REST Response Signing](signatures.md#rest-response-signing) +for the complete signing algorithm. + ## Security Considerations ### Authentication @@ -1294,6 +1355,8 @@ authentication is required, the REST transport **MAY** use: 3. **OAuth 2.0**: Via `Authorization: Bearer {token}` header, following [RFC 6749](https://tools.ietf.org/html/rfc6749){ target="_blank" }. 4. **Mutual TLS**: For high-security environments. +5. **HTTP Message Signatures**: Per [RFC 9421](https://www.rfc-editor.org/rfc/rfc9421) + (see [Message Signing](#message-signing) above). Businesses **MAY** require authentication for some operations while leaving others open (e.g., public checkout without authentication). diff --git a/docs/specification/order.md b/docs/specification/order.md index 2188eefb..34a8eb09 100644 --- a/docs/specification/order.md +++ b/docs/specification/order.md @@ -293,34 +293,72 @@ platform's profile and uses it to send order lifecycle events. ### Webhook Signature Verification Webhook payloads **MUST** be signed by the business and verified by the platform -to ensure authenticity and integrity. +to ensure authenticity and integrity. Signatures follow the +[Message Signatures](signatures.md) specification using the REST binding +(RFC 9421). + +**Required Headers:** + +| Header | Description | +| :--------------- | :----------------------------------------- | +| `UCP-Agent` | Business profile URL (RFC 8941 Dictionary) | +| `Signature-Input`| Describes signed components | +| `Signature` | Contains the signature value | +| `Content-Digest` | Body digest (RFC 9530) | + +**Example Webhook Request:** + +```http +POST /webhooks/ucp/orders HTTP/1.1 +Host: platform.example.com +Content-Type: application/json +UCP-Agent: profile="https://merchant.example/.well-known/ucp" +Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=: +Signature-Input: sig1=("@method" "@path" "content-digest" "content-type");keyid="merchant-2026" +Signature: sig1=:MEUCIQDTxNq8h7LGHpvVZQp1iHkFp9+3N8Mxk2zH1wK4YuVN8w...: + +{"id":"order_abc123","event_id":"evt_123","created_time":"2026-01-15T12:00:00Z",...} +``` #### Signing (Business) -1. Select a key from the `signing_keys` array in UCP profile. -2. Create a detached JWT (RFC 7797) over the request body using the selected key. -3. Include the JWT in the `Request-Signature` header. -4. Include the key ID in the JWT header's `kid` claim to allow the receiver to - identify which key to use for verification. +1. Compute SHA-256 digest of the raw request body and set `Content-Digest` header +2. Build signature base per [RFC 9421](https://www.rfc-editor.org/rfc/rfc9421) +3. Sign using a key from `signing_keys` in the business's UCP profile +4. Set `Signature-Input` and `Signature` headers + +See [Message Signatures - REST Request Signing](signatures.md#rest-request-signing) +for complete algorithm. #### Verification (Platform) -1. Extract the `Request-Signature` header from the incoming webhook request. -2. Parse the JWT header to retrieve the `kid` (key ID). -3. Fetch the business's UCP profile from `/.well-known/ucp` (cache as appropriate). -4. Locate the key in `signing_keys` with the matching `kid`. -5. Verify the JWT signature against the request body using the public key. -6. If verification fails, reject the webhook with an appropriate error response. +**Authentication** (signature verification): -#### Key Rotation +1. Parse `Signature-Input` to extract `keyid` and signed components +2. Fetch business's UCP profile from `/.well-known/ucp` (cache as appropriate) +3. Locate key in `signing_keys` with matching `kid` +4. Verify `Content-Digest` matches SHA-256 of raw body +5. Reconstruct signature base and verify signature + +See [Message Signatures - REST Request Verification](signatures.md#rest-request-verification) +for complete algorithm. + +**Authorization** (order ownership): -The `signing_keys` array supports multiple keys to enable zero-downtime -rotation: +After verifying the signature, the platform **MUST** confirm the signer is +authorized to send events for the referenced order: + +1. Extract the order ID from the webhook payload +2. Verify the order was created with this business (profile URL matches) +3. Reject webhooks where the signer's profile doesn't match the order's business + +This prevents a malicious business from sending fake events for another +business's orders, even with a valid signature. + +#### Key Rotation -* **Adding a new key:** Add the new key to `signing_keys`, then start signing - with it. Verifiers will find it by `kid`. -* **Removing an old key:** After sufficient time for all in-flight webhooks to - be delivered, remove the old key from `signing_keys`. +See [Message Signatures - Key Rotation](signatures.md#key-rotation) for +zero-downtime key rotation procedures. ## Guidelines @@ -331,13 +369,13 @@ rotation: **Business:** -* **MUST** sign all webhook payloads using a key from their `signing_keys` - array (published in `/.well-known/ucp`). The signature **MUST** be included - in the `Request-Signature` header as a detached JWT (RFC 7797). +* **MUST** include `UCP-Agent` header with profile URL for signer identification +* **MUST** sign all webhook payloads per the + [Message Signatures](signatures.md) specification using RFC 9421 headers + (`Signature`, `Signature-Input`, `Content-Digest`). * **MUST** send "Order created" event with fully populated order entity * **MUST** send full order entity on updates (not incremental deltas) * **MUST** retry failed webhook deliveries -* **MUST** include business identifier in webhook path or headers ## Entities diff --git a/docs/specification/overview.md b/docs/specification/overview.md index 2681d6b0..0fb3da81 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -598,6 +598,18 @@ These failure types require different handling: | `capabilities_incompatible` | No compatible capabilities in intersection | 200 | result | | `version_unsupported` | Platform's UCP version is not supported | 200 | result | +**Signature Errors:** + +| Code | Description | REST | MCP | +| ---------------------- | ------------------------------------------------------ | ---- | ------ | +| `signature_missing` | Required signature header/field not present | 401 | -32000 | +| `signature_invalid` | Signature verification failed | 401 | -32000 | +| `key_not_found` | Key ID not found in signer's `signing_keys` | 401 | -32000 | +| `digest_mismatch` | Body digest doesn't match `Content-Digest` header | 400 | -32600 | +| `algorithm_unsupported`| Signature algorithm not supported | 400 | -32600 | + +See [Message Signatures](signatures.md) for signature verification details. + **Protocol Errors:** | HTTP | Description | MCP | diff --git a/docs/specification/signatures.md b/docs/specification/signatures.md new file mode 100644 index 00000000..8ecb603b --- /dev/null +++ b/docs/specification/signatures.md @@ -0,0 +1,678 @@ + + +# Message Signatures + +This specification defines how UCP messages are cryptographically signed to +ensure authenticity and integrity. + +## Overview + +Businesses **SHOULD** authenticate agents to prevent impersonation and ensure +message integrity. UCP supports multiple authentication mechanisms: + +* **API Keys** — Pre-shared secrets exchanged out-of-band +* **OAuth 2.0** — Client credentials or other OAuth flows +* **mTLS** — Mutual TLS with client certificates +* **HTTP Message Signatures** — Cryptographic signatures per RFC 9421 (this spec) + +HTTP Message Signatures are particularly valuable for **permissionless agent +onboarding** — merchants can declaratively trust agents by their advertised +public keys without negotiating shared secrets. + +When using HTTP Message Signatures, they protect against: + +* **Impersonation** — Attackers sending messages claiming to be legitimate + participants +* **Tampering** — Modification of message contents in transit +* **Replay attacks** — Captured messages resent to different endpoints or at + different times +* **Method/endpoint confusion** — Signed payloads replayed with different + HTTP methods or to different paths + +### Architecture + +UCP uses HTTP Message Signatures ([RFC 9421](https://www.rfc-editor.org/rfc/rfc9421)) +for all HTTP-based transports: + +```text +┌─────────────────────────────────────────────────────────────────┐ +│ SHARED FOUNDATION │ +├─────────────────────────────────────────────────────────────────┤ +│ Signature Format: RFC 9421 (HTTP Message Signatures) │ +│ Body Digest: RFC 9530 (Content-Digest, raw bytes) │ +│ Algorithms: ES256 (required), ES384, ES512 │ +│ Key Format: JWK (RFC 7517) │ +│ Key Discovery: signing_keys[] in /.well-known/ucp │ +│ Replay Protection: idempotency-key (business layer) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ HTTP TRANSPORTS │ +├─────────────────────────────────────────────────────────────────┤ +│ REST API: Standard HTTP requests │ +│ MCP: Streamable HTTP transport (JSON-RPC over HTTP) │ +├─────────────────────────────────────────────────────────────────┤ +│ Headers: │ +│ Signature-Input (describes signed components) │ +│ Signature (contains signature value) │ +│ Content-Digest (body hash, raw bytes) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Note:** UCP specifies streamable HTTP for MCP transport, replacing SSE-based +transports. This allows the same RFC 9421 signature mechanism to apply uniformly +across all UCP transports. + +## Shared Foundation + +The following cryptographic primitives are shared across all UCP HTTP transports. + +### Signature Algorithms + +UCP supports ECDSA signatures with the following algorithms: + +| Algorithm | Curve | Hash | +| :-------- | :------ | :------ | +| `ES256` | P-256 | SHA-256 | +| `ES384` | P-384 | SHA-384 | +| `ES512` | P-521 | SHA-512 | + +**Implementation requirements:** + +* All implementations **MUST** support verifying `ES256` signatures +* Support for `ES384` and `ES512` is **OPTIONAL** + +**Usage guidance:** + +* Signers **SHOULD** use `ES256` for maximum compatibility +* Signers **MAY** use `ES384` or `ES512` when both parties support them +* The algorithm is indicated by the `alg` field in the signing key's JWK + +### Key Format (JWK) + +Public keys **MUST** be represented using **JSON Web Key (JWK)** format as +defined in [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517). + +**EC Key Structure:** + +| Field | Type | Required | Description | +| :---- | :----- | :------- | :--------------------------------------- | +| `kid` | string | Yes | Key ID (referenced in signatures) | +| `kty` | string | Yes | Key type (`EC` for elliptic curve) | +| `crv` | string | Yes* | Curve name (`P-256`, `P-384`, `P-521`) | +| `x` | string | Yes* | X coordinate (base64url encoded) | +| `y` | string | Yes* | Y coordinate (base64url encoded) | +| `use` | string | No | Key usage (`sig` for signing) | +| `alg` | string | No | Algorithm (`ES256`, `ES384`, `ES512`) | + +\* Required for EC keys + +**Example:** + +```json +{ + "kid": "key-2024-01-15", + "kty": "EC", + "crv": "P-256", + "x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis", + "y": "y77t-RvAHRKTsSGdIYUfweuOvwrvDD-Q3Hv5J0fSKbE", + "use": "sig", + "alg": "ES256" +} +``` + +### Key Discovery + +Public keys are published in the `signing_keys` array of the party's UCP +profile at `/.well-known/ucp`. + +**Business Profile:** + +```json +{ + "ucp": { ... }, + "signing_keys": [ + { + "kid": "merchant-2026", + "kty": "EC", + "crv": "P-256", + "x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis", + "y": "y77t-RvAHRKTsSGdIYUfweuOvwrvDD-Q3Hv5J0fSKbE", + "alg": "ES256" + } + ] +} +``` + +**Platform Profile:** + +```json +{ + "ucp": { ... }, + "signing_keys": [ + { + "kid": "platform-2026", + "kty": "EC", + "crv": "P-256", + "x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", + "y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", + "alg": "ES256" + } + ] +} +``` + +**Key Lookup:** + +1. Extract `kid` (key ID) from signature header/parameter +2. Fetch signer's UCP profile from `/.well-known/ucp` +3. Search `signing_keys[]` for matching `kid` +4. Use the corresponding public key for verification + +### Profile Trust Model + +**Profile URLs** identify the signer and locate their public keys. Implementations +**MUST** validate profile URLs to prevent attacks where malicious actors use +attacker-controlled profiles. + +**Validation Requirements:** + +1. **HTTPS required** — Profile URLs **MUST** use `https://` scheme +2. **Well-known path** — URL **MUST** end with `/.well-known/ucp` +3. **No open redirects** — Reject profiles that redirect to different domains +4. **Domain binding** — The profile domain identifies the organization + +Profile trust is typically established through: + +* **Pre-registration** — Platform/business exchange profile URLs during onboarding +* **Capability negotiation** — Profile URL discovered from partner's profile +* **Allowlists** — Implementations SHOULD maintain explicit allowlists of trusted profiles + +**Example Allowlist Check:** + +```text +validate_profile_url(url, allowlist): + // Parse and validate URL structure + parsed = parse_url(url) + if parsed.scheme != "https": + return error("invalid_profile_url") + if not parsed.path.endsWith("/.well-known/ucp"): + return error("invalid_profile_url") + + // Check against allowlist (if configured) + if allowlist and parsed.host not in allowlist: + return error("profile_not_trusted") + + return success() +``` + +**Profile Caching:** + +Implementations **SHOULD** cache fetched profiles to reduce latency and network +load. Recommended cache policy: + +* **TTL:** 5-15 minutes for normal operations +* **Stale-while-revalidate:** Accept stale profile during background refresh +* **Force refresh:** On signature verification failure with unknown `kid` +* **No cache:** For key compromise scenarios (see Key Rotation) + +### Key Rotation + +To rotate keys without service interruption: + +1. **Add new key** — Publish new key in `signing_keys[]` alongside existing keys +2. **Start signing** — Begin signing with the new key +3. **Grace period** — Continue accepting signatures from old keys (minimum 7 days) +4. **Remove old key** — Remove the old key from `signing_keys[]` + +**Recommendations:** + +* Rotate keys every 90 days +* Support multiple active keys during transitions +* Verifiers: accept any key in `signing_keys[]` + +**Key Compromise Response:** + +1. Immediately remove compromised key from profile +2. Add new key with different `kid` +3. Reject all signatures made with compromised key + +## REST Binding + +For HTTP REST transport, UCP uses +[RFC 9421 (HTTP Message Signatures)](https://www.rfc-editor.org/rfc/rfc9421). + +### Headers + +| Header | Direction | Required | Description | +| :---------------- | :--------------- | :------- | :------------------------------------ | +| `Signature-Input` | Request/Response | Yes | Describes signed components | +| `Signature` | Request/Response | Yes | Contains signature value | +| `Content-Digest` | Request/Response | Cond.* | SHA-256 hash of request/response body | + +\* Required when request/response has a body + +`Content-Digest` follows [RFC 9530](https://www.rfc-editor.org/rfc/rfc9530) and +hashes the raw body bytes. This binds the message body to the signature without +requiring JSON canonicalization. Implementations **MUST** use `sha-256`. For +durable artifacts requiring canonicalization, see +[AP2 Mandates - Canonicalization](ap2-mandates.md#canonicalization). + +**Intermediary Warning:** Proxies, API gateways, and other intermediaries +**MUST NOT** re-serialize JSON bodies, as this would invalidate the signature. +The `Content-Digest` is computed over raw bytes; any modification breaks +verification. + +### REST Request Signing + +**Signed Components:** + +| Component | Required | Description | +| :---------------- | :------- | :-------------------------------------- | +| `@method` | Yes | HTTP method (GET, POST, etc.) | +| `@path` | Yes | Request path | +| `@query` | Cond.* | Query string (if present) | +| `idempotency-key` | Cond.** | Idempotency header (state-changing ops) | +| `content-digest` | Cond.*** | Body digest (if body present) | +| `content-type` | Cond.*** | Content-Type (if body present) | + +\* Required if request has query parameters +\** Required for POST, PUT, DELETE, PATCH +\*** Required if request has a body + +**Signature Generation:** + +```text +sign_rest_request(method, path, query, body_bytes, idempotency_key, private_key, kid): + // 1. Compute body digest (if body present) + if body_bytes: + digest = sha256(body_bytes) // Hash raw bytes, no canonicalization + digest_header = "sha-256=:" + base64(digest) + ":" + + // 2. Build component list + components = ["@method", "@path"] + if query: components.append("@query") + if idempotency_key: components.append("idempotency-key") + if body: components.extend(["content-digest", "content-type"]) + + // 3. Build signature base (RFC 9421) + signature_base = build_signature_base( + components=components, + method=method, + path=path, + query=query, + headers={ + "idempotency-key": idempotency_key, + "content-digest": digest_header, + "content-type": "application/json" + }, + keyid=kid + ) + + // 4. Sign + signature = ecdsa_sign(signature_base, private_key) + + // 5. Return headers + return { + "Idempotency-Key": idempotency_key, + "Content-Digest": digest_header, + "Signature-Input": format_signature_input(components, kid), + "Signature": "sig1=:" + base64(signature) + ":" + } +``` + +**Complete Request Example:** + +```http +POST /checkout-sessions HTTP/1.1 +Host: merchant.example.com +Content-Type: application/json +Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 +Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=: +Signature-Input: sig1=("@method" "@path" "idempotency-key" "content-digest" "content-type");keyid="platform-2026" +Signature: sig1=:MEUCIQDTxNq8h7LGHpvVZQp1iHkFp9+3N8Mxk2zH1wK4YuVN8w...: + +{"checkout":{"line_items":[{"id":"prod_123","quantity":2}]}} +``` + +**GET Request Example (no body, no idempotency):** + +```http +GET /checkout-sessions/chk_123 HTTP/1.1 +Host: merchant.example.com +Signature-Input: sig1=("@method" "@path");keyid="platform-2026" +Signature: sig1=:MEQCIBx7kL9nM2oP5qR8sT1uV4wX6yZaB3cD...: +``` + +### REST Response Signing + +Response signatures use `@status` instead of `@method`: + +**Signed Components:** + +| Component | Required | Description | +| :--------------- | :------- | :-------------------------------- | +| `@status` | Yes | HTTP status code (200, 201, etc.) | +| `content-digest` | Cond.* | Body digest (if body present) | +| `content-type` | Cond.* | Content-Type (if body present) | + +\* Required if response has a body + +**Complete Response Example:** + +```http +HTTP/1.1 201 Created +Content-Type: application/json +Content-Digest: sha-256=:Y5fK8nLmPqRsT3vWxYzAbCdEfGhIjKlMnO...: +Signature-Input: sig1=("@status" "content-digest" "content-type");created=1738617601;keyid="merchant-2026" +Signature: sig1=:MFQCIH7kL9nM2oP5qR8sT1uV4wX6yZaB3cD...: + +{"checkout":{"id":"chk_123","status":"ready_for_complete"}} +``` + +**Response Signature Generation:** + +Response signing mirrors request signing with `@status` replacing `@method`: + +```text +sign_rest_response(status, body_bytes, private_key, kid): + // 1. Compute body digest (if body present) + if body_bytes: + digest = sha256(body_bytes) // Hash raw bytes, no canonicalization + digest_header = "sha-256=:" + base64(digest) + ":" + + // 2. Build signature base (RFC 9421) + signature_base = build_signature_base( + components=["@status", "content-digest", "content-type"], + status=status, + headers={"content-digest": digest_header, "content-type": "application/json"}, + created=current_timestamp(), + keyid=kid + ) + + // 3. Sign + signature = ecdsa_sign(signature_base, private_key) + + // 4. Return headers + return { + "Content-Digest": digest_header, + "Signature-Input": 'sig1=("@status" "content-digest" "content-type");created=...;keyid="..."', + "Signature": "sig1=:" + base64(signature) + ":" + } +``` + +### REST Request Verification + +**Determining Signer's Profile URL:** + +The signer's profile URL is obtained from the `UCP-Agent` header, which uses +[RFC 8941 Dictionary](https://www.rfc-editor.org/rfc/rfc8941#section-3.2) syntax: + +```text +UCP-Agent: profile="https://platform.example/.well-known/ucp" +``` + +**Parsing Rules:** + +1. Parse as RFC 8941 Dictionary +2. Extract the `profile` key (REQUIRED) +3. Value MUST be a quoted string containing an HTTPS URL +4. URL MUST point to `/.well-known/ucp` at the signer's domain +5. Reject non-HTTPS URLs + +**Example:** + +```text +// Header +UCP-Agent: profile="https://platform.example/.well-known/ucp" + +// Parsed +profile_url = "https://platform.example/.well-known/ucp" +``` + +**Applicability:** + +* **Platform → Business requests:** Profile URL from `UCP-Agent` header +* **Business → Platform webhooks:** Profile URL from `UCP-Agent` header + +```text +verify_rest_request(request): + // 1. Parse Signature-Input + sig_input = parse_signature_input(request.headers["Signature-Input"]) + keyid = sig_input.keyid + components = sig_input.components + + // 2. Fetch signer's public key + profile_url = get_profile_url_from_ucp_agent(request.headers["UCP-Agent"]) + validate_profile_url(profile_url) + profile = fetch_profile(profile_url) + public_key = find_key_by_kid(profile.signing_keys, keyid) + if not public_key: + return error("key_not_found") + + // 3. Verify body digest (if body present) + if "content-digest" in components: + expected = "sha-256=:" + base64(sha256(request.body_bytes)) + ":" + if request.headers["Content-Digest"] != expected: + return error("digest_mismatch") + + // 4. Reconstruct signature base + signature_base = build_signature_base( + components, request.method, request.path, request.query, + request.headers, keyid + ) + + // 5. Verify signature + signature = parse_signature(request.headers["Signature"]) + if not ecdsa_verify(signature_base, signature, public_key): + return error("signature_invalid") + + return success() + + // Note: Replay protection handled by idempotency keys in request payload +``` + +### REST Response Verification + +Response verification mirrors request verification with `@status` replacing +`@method`: + +```text +verify_rest_response(response, signer_profile_url): + // 1. Parse Signature-Input + sig_input = parse_signature_input(response.headers["Signature-Input"]) + keyid = sig_input.keyid + components = sig_input.components + + // 2. Fetch signer's public key + profile = fetch_profile(signer_profile_url) + public_key = find_key_by_kid(profile.signing_keys, keyid) + if not public_key: + return error("key_not_found") + + // 3. Verify body digest (if body present) + if "content-digest" in components: + expected = "sha-256=:" + base64(sha256(response.body_bytes)) + ":" + if response.headers["Content-Digest"] != expected: + return error("digest_mismatch") + + // 4. Reconstruct signature base + signature_base = build_signature_base( + components, response.status, + response.headers, keyid + ) + + // 5. Verify signature + signature = parse_signature(response.headers["Signature"]) + if not ecdsa_verify(signature_base, signature, public_key): + return error("signature_invalid") + + return success() +``` + +### Replay Protection + +UCP handles replay protection at the **business layer** through idempotency keys, +not at the signature layer. This provides separation of concerns: + +| Layer | Responsibility | +| :---- | :------------- | +| **Signature** | Authentication (who), Integrity (what) | +| **Idempotency** | Safe retries, Replay protection | + +**How it works:** + +1. State-changing operations include an `idempotency-key` in the request +2. The idempotency key is part of the signed payload +3. Attackers cannot modify the key without invalidating the signature +4. Duplicate requests return cached responses (no new side effects) + +**Idempotency Key Placement:** + +The `Idempotency-Key` header is included in the signed components: + +```http +POST /checkout-sessions HTTP/1.1 +Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 +Signature-Input: sig1=("@method" "@path" "idempotency-key" ...);keyid="platform-2026" +Signature: sig1=:MEUCIQD...: +``` + +**Idempotency Key Requirements:** + +| Requirement | Value | +| :---------- | :---- | +| **Entropy** | Minimum 128 bits (e.g., UUID v4, 22+ char alphanumeric) | +| **Uniqueness** | Per-client, per-operation type | +| **Server storage** | Minimum 24 hours, recommended 48 hours | +| **On duplicate** | Return cached response, do not re-execute | +| **On storage failure** | Fail closed (reject request with 503) | + +**Note:** The RFC 9421 `created` parameter is **OPTIONAL**. UCP handles replay +protection at the business layer through idempotency keys, not signature timestamps. +Key rotation (removing compromised keys from `signing_keys`) provides the mechanism +for invalidating old signatures. + +### When Signatures Are Recommended + +**Requests:** Platforms **SHOULD** sign all requests when using HTTP Message +Signatures. Alternative authentication mechanisms (API keys, OAuth, mTLS) may +be used instead. + +**Responses:** Signatures are **RECOMMENDED** for: + +* Order webhook notifications +* Payment authorization responses +* Checkout completion responses + +Signatures are **OPTIONAL** for: + +* Cart operations (low-value, synchronous) +* Catalog queries (read-only) +* Error responses (4xx, 5xx) + +## MCP Transport + +UCP specifies **streamable HTTP** for MCP transport, replacing SSE-based transports. +Since MCP requests are standard HTTP requests with JSON-RPC bodies, the same +RFC 9421 signature mechanism applies: + +* The `Content-Digest` header covers the JSON-RPC message body +* The `Signature-Input` and `Signature` headers provide authentication +* The `UCP-Agent` and `Idempotency-Key` headers work identically to REST + +**Example MCP Request with Signature:** + +```http +POST /mcp HTTP/1.1 +Host: business.example.com +Content-Type: application/json +UCP-Agent: profile="https://platform.example/.well-known/ucp" +Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 +Content-Digest: sha-256=:RK/0qy18MlBSVnWgjwz6lZEWjP/lF5HF9bvEF8FabDg=: +Signature-Input: sig1=("@method" "@path" "content-digest" "content-type" "ucp-agent" "idempotency-key");keyid="platform-2026" +Signature: sig1=:MEUCIQDXyK9N3p5Rt...: + +{"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. + +## Error Handling + +Signature verification errors use standard UCP error codes. See +[Error Handling](overview.md#error-handling) in the specification overview for +the complete error code registry and transport bindings. + +**Signature-specific errors:** + +| Code | HTTP | Description | +| :---------------------- | :--- | :--------------------------------------------------- | +| `signature_missing` | 401 | Required signature header/field not present | +| `signature_invalid` | 401 | Signature verification failed | +| `key_not_found` | 401 | Key ID not found in signer's `signing_keys` | +| `digest_mismatch` | 400 | Body digest doesn't match `Content-Digest` header | +| `algorithm_unsupported` | 400 | Signature algorithm not supported | + +**Profile-related errors** (also used for capability negotiation): + +| Code | HTTP | Description | +| :---------------------- | :--- | :--------------------------------------------------- | +| `invalid_profile_url` | 400 | Profile URL malformed or invalid scheme | +| `profile_unreachable` | 424 | Unable to fetch signer's profile | +| `profile_not_trusted` | 403 | Profile URL not in trusted allowlist | + +**Note:** Replay protection is handled at the business layer through idempotency +keys, not at the signature layer. Duplicate requests return cached responses +rather than signature errors. + +### REST Error Response + +```http +HTTP/1.1 401 Unauthorized +Content-Type: application/json + +{ + "code": "signature_invalid", + "content": "Request signature verification failed for key kid=platform-2026" +} +``` + +### MCP Error Response + +```json +{ + "jsonrpc": "2.0", + "id": 42, + "error": { + "code": -32000, + "message": "Signature verification failed", + "data": { + "code": "signature_invalid", + "content": "Signature verification failed for key kid=platform-2026" + } + } +} +``` + +## References + +* [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517) — JSON Web Key (JWK) +* [RFC 9421](https://www.rfc-editor.org/rfc/rfc9421) — HTTP Message Signatures +* [RFC 9530](https://www.rfc-editor.org/rfc/rfc9530) — Digest Fields (Content-Digest) diff --git a/mkdocs.yml b/mkdocs.yml index 6686c99c..d0acaaf3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -54,9 +54,10 @@ nav: - Processor Tokenizer: specification/examples/processor-tokenizer-payment-handler.md - Platform Tokenizer: specification/examples/platform-tokenizer-payment-handler.md - Encrypted Credential: specification/examples/encrypted-credential-handler.md - - Schema Authoring: documentation/schema-authoring.md - - Reference: specification/reference.md + - Signatures: specification/signatures.md - Versioning: specification/versioning.md + - Authoring: documentation/schema-authoring.md + - Reference: specification/reference.md - UCP and AP2: documentation/ucp-and-ap2.md - Roadmap: documentation/roadmap.md @@ -197,6 +198,8 @@ plugins: - documentation/roadmap.md Specification Overview: - specification/overview.md + Message Signatures: + - specification/signatures.md Checkout Capability: - specification/checkout.md - specification/checkout-rest.md diff --git a/source/services/shopping/openapi.json b/source/services/shopping/openapi.json index 54282406..c64c9a24 100644 --- a/source/services/shopping/openapi.json +++ b/source/services/shopping/openapi.json @@ -31,7 +31,13 @@ "$ref": "#/components/parameters/x_api_key" }, { - "$ref": "#/components/parameters/request_signature" + "$ref": "#/components/parameters/signature" + }, + { + "$ref": "#/components/parameters/signature_input" + }, + { + "$ref": "#/components/parameters/content_digest" }, { "$ref": "#/components/parameters/idempotency_key" @@ -70,8 +76,14 @@ "201": { "description": "Checkout session created", "headers": { - "X-Detached-JWT": { - "$ref": "#/components/headers/x_detached_jwt" + "Signature": { + "$ref": "#/components/headers/signature" + }, + "Signature-Input": { + "$ref": "#/components/headers/signature_input" + }, + "Content-Digest": { + "$ref": "#/components/headers/content_digest" } }, "content": { @@ -101,7 +113,10 @@ "$ref": "#/components/parameters/x_api_key" }, { - "$ref": "#/components/parameters/request_signature" + "$ref": "#/components/parameters/signature" + }, + { + "$ref": "#/components/parameters/signature_input" }, { "$ref": "#/components/parameters/request_id" @@ -129,8 +144,14 @@ "200": { "description": "Checkout session retrieved", "headers": { - "X-Detached-JWT": { - "$ref": "#/components/headers/x_detached_jwt" + "Signature": { + "$ref": "#/components/headers/signature" + }, + "Signature-Input": { + "$ref": "#/components/headers/signature_input" + }, + "Content-Digest": { + "$ref": "#/components/headers/content_digest" } }, "content": { @@ -155,7 +176,13 @@ "$ref": "#/components/parameters/x_api_key" }, { - "$ref": "#/components/parameters/request_signature" + "$ref": "#/components/parameters/signature" + }, + { + "$ref": "#/components/parameters/signature_input" + }, + { + "$ref": "#/components/parameters/content_digest" }, { "$ref": "#/components/parameters/idempotency_key" @@ -194,8 +221,14 @@ "200": { "description": "Checkout session updated", "headers": { - "X-Detached-JWT": { - "$ref": "#/components/headers/x_detached_jwt" + "Signature": { + "$ref": "#/components/headers/signature" + }, + "Signature-Input": { + "$ref": "#/components/headers/signature_input" + }, + "Content-Digest": { + "$ref": "#/components/headers/content_digest" } }, "content": { @@ -225,7 +258,13 @@ "$ref": "#/components/parameters/x_api_key" }, { - "$ref": "#/components/parameters/request_signature" + "$ref": "#/components/parameters/signature" + }, + { + "$ref": "#/components/parameters/signature_input" + }, + { + "$ref": "#/components/parameters/content_digest" }, { "$ref": "#/components/parameters/idempotency_key" @@ -279,8 +318,14 @@ "200": { "description": "Checkout session completed", "headers": { - "X-Detached-JWT": { - "$ref": "#/components/headers/x_detached_jwt" + "Signature": { + "$ref": "#/components/headers/signature" + }, + "Signature-Input": { + "$ref": "#/components/headers/signature_input" + }, + "Content-Digest": { + "$ref": "#/components/headers/content_digest" } }, "content": { @@ -308,7 +353,13 @@ "$ref": "#/components/parameters/x_api_key" }, { - "$ref": "#/components/parameters/request_signature" + "$ref": "#/components/parameters/signature" + }, + { + "$ref": "#/components/parameters/signature_input" + }, + { + "$ref": "#/components/parameters/content_digest" }, { "$ref": "#/components/parameters/idempotency_key" @@ -339,8 +390,14 @@ "200": { "description": "Checkout session canceled", "headers": { - "X-Detached-JWT": { - "$ref": "#/components/headers/x_detached_jwt" + "Signature": { + "$ref": "#/components/headers/signature" + }, + "Signature-Input": { + "$ref": "#/components/headers/signature_input" + }, + "Content-Digest": { + "$ref": "#/components/headers/content_digest" } }, "content": { @@ -362,7 +419,9 @@ "parameters": [ { "$ref": "#/components/parameters/authorization" }, { "$ref": "#/components/parameters/x_api_key" }, - { "$ref": "#/components/parameters/request_signature" }, + { "$ref": "#/components/parameters/signature" }, + { "$ref": "#/components/parameters/signature_input" }, + { "$ref": "#/components/parameters/content_digest" }, { "$ref": "#/components/parameters/idempotency_key" }, { "$ref": "#/components/parameters/request_id" }, { "$ref": "#/components/parameters/user_agent" }, @@ -383,7 +442,9 @@ "201": { "description": "Cart created", "headers": { - "X-Detached-JWT": { "$ref": "#/components/headers/x_detached_jwt" } + "Signature": { "$ref": "#/components/headers/signature" }, + "Signature-Input": { "$ref": "#/components/headers/signature_input" }, + "Content-Digest": { "$ref": "#/components/headers/content_digest" } }, "content": { "application/json": { @@ -405,7 +466,8 @@ "parameters": [ { "$ref": "#/components/parameters/authorization" }, { "$ref": "#/components/parameters/x_api_key" }, - { "$ref": "#/components/parameters/request_signature" }, + { "$ref": "#/components/parameters/signature" }, + { "$ref": "#/components/parameters/signature_input" }, { "$ref": "#/components/parameters/request_id" }, { "$ref": "#/components/parameters/user_agent" }, { "$ref": "#/components/parameters/ucp_agent" }, @@ -418,7 +480,9 @@ "200": { "description": "Cart retrieved", "headers": { - "X-Detached-JWT": { "$ref": "#/components/headers/x_detached_jwt" } + "Signature": { "$ref": "#/components/headers/signature" }, + "Signature-Input": { "$ref": "#/components/headers/signature_input" }, + "Content-Digest": { "$ref": "#/components/headers/content_digest" } }, "content": { "application/json": { @@ -438,7 +502,9 @@ "parameters": [ { "$ref": "#/components/parameters/authorization" }, { "$ref": "#/components/parameters/x_api_key" }, - { "$ref": "#/components/parameters/request_signature" }, + { "$ref": "#/components/parameters/signature" }, + { "$ref": "#/components/parameters/signature_input" }, + { "$ref": "#/components/parameters/content_digest" }, { "$ref": "#/components/parameters/idempotency_key" }, { "$ref": "#/components/parameters/request_id" }, { "$ref": "#/components/parameters/user_agent" }, @@ -460,7 +526,9 @@ "200": { "description": "Cart updated", "headers": { - "X-Detached-JWT": { "$ref": "#/components/headers/x_detached_jwt" } + "Signature": { "$ref": "#/components/headers/signature" }, + "Signature-Input": { "$ref": "#/components/headers/signature_input" }, + "Content-Digest": { "$ref": "#/components/headers/content_digest" } }, "content": { "application/json": { @@ -482,7 +550,9 @@ "parameters": [ { "$ref": "#/components/parameters/authorization" }, { "$ref": "#/components/parameters/x_api_key" }, - { "$ref": "#/components/parameters/request_signature" }, + { "$ref": "#/components/parameters/signature" }, + { "$ref": "#/components/parameters/signature_input" }, + { "$ref": "#/components/parameters/content_digest" }, { "$ref": "#/components/parameters/idempotency_key" }, { "$ref": "#/components/parameters/request_id" }, { "$ref": "#/components/parameters/user_agent" }, @@ -496,7 +566,9 @@ "200": { "description": "Cart cancelled", "headers": { - "X-Detached-JWT": { "$ref": "#/components/headers/x_detached_jwt" } + "Signature": { "$ref": "#/components/headers/signature" }, + "Signature-Input": { "$ref": "#/components/headers/signature_input" }, + "Content-Digest": { "$ref": "#/components/headers/content_digest" } }, "content": { "application/json": { @@ -515,7 +587,10 @@ "summary": "Order Event Webhook", "description": "Merchant sends order lifecycle events to the platform's registered webhook URL. The platform provides the webhook URL during partner onboarding.", "parameters": [ - { "$ref": "#/components/parameters/request_signature" }, + { "$ref": "#/components/parameters/signature" }, + { "$ref": "#/components/parameters/signature_input" }, + { "$ref": "#/components/parameters/content_digest" }, + { "$ref": "#/components/parameters/ucp_agent" }, { "$ref": "#/components/parameters/x_api_key" } ], "requestBody": { @@ -604,14 +679,32 @@ }, "description": "Authenticates the platform with a reusable api key allocated to the platform by the business." }, - "request_signature": { - "name": "Request-Signature", + "signature": { + "name": "Signature", "in": "header", "required": true, "schema": { "type": "string" }, - "description": "Ensure the authenticity and integrity of an HTTP message." + "description": "RFC 9421 HTTP Message Signature. Contains the signature value in the format `sig1=::`." + }, + "signature_input": { + "name": "Signature-Input", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "description": "RFC 9421 Signature-Input header. Describes signed components, timestamp, and key ID. Format: `sig1=(\"@method\" \"@path\" ...);created=;keyid=\"\"`." + }, + "content_digest": { + "name": "Content-Digest", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Body digest per RFC 9530. Required for requests/responses with a body. Format: `sha-256=::`." }, "idempotency_key": { "name": "Idempotency-Key", @@ -649,7 +742,7 @@ "schema": { "type": "string" }, - "description": "Identifies the UCP agent making the call. All requests MUST include the UCP-Agent header containing the platform profile URI using Dictionary Structured Field syntax (RFC 8941). Format: profile=\"https://platform.example/profile\"." + "description": "Identifies the UCP agent making the call. All requests MUST include the UCP-Agent header containing the signer's profile URI using RFC 8941 Dictionary syntax. The URL MUST point to /.well-known/ucp. Format: profile=\"https://example.com/.well-known/ucp\"." }, "content_type": { "name": "Content-Type", @@ -689,12 +782,26 @@ } }, "headers": { - "x_detached_jwt": { + "signature": { + "required": false, + "schema": { + "type": "string" + }, + "description": "RFC 9421 HTTP Message Signature for response. Contains the signature value in the format `sig1=::`." + }, + "signature_input": { + "required": false, + "schema": { + "type": "string" + }, + "description": "RFC 9421 Signature-Input header for response. Describes signed components, timestamp, and key ID." + }, + "content_digest": { "required": false, "schema": { "type": "string" }, - "description": "Optional detached JWT signature for the response body. Verifies the response came from the server." + "description": "JCS-canonicalized body digest per RFC 8785. Format: `sha-256=::`." } }, "schemas": { diff --git a/source/services/shopping/openrpc.json b/source/services/shopping/openrpc.json index fc061701..c347234a 100644 --- a/source/services/shopping/openrpc.json +++ b/source/services/shopping/openrpc.json @@ -42,6 +42,10 @@ "type": "string", "format": "uuid", "description": "Unique key for retry safety. Maps to HTTP Idempotency-Key header." + }, + "signature": { + "type": "string", + "description": "Detached JWS signature (RFC 7515 Appendix F) in format `..`. See Message Signatures specification." } } } @@ -174,6 +178,11 @@ "summary": "Create a cart", "description": "Create a new cart session.", "params": [ + { + "name": "meta", + "required": true, + "schema": {"$ref": "#/components/schemas/meta"} + }, { "name": "cart", "required": true, @@ -189,6 +198,11 @@ "name": "get_cart", "summary": "Get cart", "params": [ + { + "name": "meta", + "required": true, + "schema": {"$ref": "#/components/schemas/meta"} + }, { "name": "id", "required": true, @@ -204,6 +218,11 @@ "name": "update_cart", "summary": "Update cart", "params": [ + { + "name": "meta", + "required": true, + "schema": {"$ref": "#/components/schemas/meta"} + }, { "name": "id", "required": true,