Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down
2 changes: 2 additions & 0 deletions .cspell/custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,11 @@ repudiable
schemas
sdjwt
shopify
streamable
superfences
upsell
upsells
vulnz
yaml
yml
keyid
42 changes: 26 additions & 16 deletions docs/specification/ap2-mandates.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
8 changes: 4 additions & 4 deletions docs/specification/cart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
73 changes: 73 additions & 0 deletions docs/specification/checkout-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**:
Expand All @@ -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
Expand Down
63 changes: 63 additions & 0 deletions docs/specification/checkout-rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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).
84 changes: 61 additions & 23 deletions docs/specification/order.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
Loading
Loading