Skip to content

Message Signatures

This specification defines how UCP messages are cryptographically signed to ensure authenticity and integrity.

This specification defines how to sign and verify UCP messages using RFC 9421 HTTP Message Signatures.

HTTP Message Signatures 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

UCP uses HTTP Message Signatures (RFC 9421) for all HTTP-based transports:

┌─────────────────────────────────────────────────────────────────┐
│ SHARED FOUNDATION │
├─────────────────────────────────────────────────────────────────┤
│ Signature Format: RFC 9421 (HTTP Message Signatures) │
│ Body Digest: RFC 9530 (Content-Digest, raw bytes) │
│ Algorithms: ES256 (required), ES384 (optional) │
│ 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.

The following cryptographic primitives are shared across all UCP HTTP transports.

UCP supports ECDSA signatures with the following curves:

CurveJWK algHash
P-256ES256SHA-256
P-384ES384SHA-384

Implementation requirements:

  • All implementations MUST support verifying P-256 (ES256) signatures
  • Support for P-384 (ES384) is OPTIONAL

Usage guidance:

  • Signers SHOULD use P-256 for maximum compatibility
  • Signers MAY use P-384 when both parties support it
  • The algorithm is derived from the key’s crv field in the JWK; alg is NOT included in Signature-Input parameters

Public keys MUST be represented using JSON Web Key (JWK) format as defined in RFC 7517.

EC Key Structure:

FieldTypeRequiredDescription
kidstringYesKey ID (referenced in signatures)
ktystringYesKey type (EC for elliptic curve)
crvstringYes*Curve name (P-256, P-384)
xstringYes*X coordinate (base64url encoded)
ystringYes*Y coordinate (base64url encoded)
usestringNoKey usage (sig for signing)
algstringNoAlgorithm (ES256, ES384)

* Required for EC keys

Example:

{
"kid": "key-2024-01-15",
"kty": "EC",
"crv": "P-256",
"x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis",
"y": "y77t-RvAHRKTsSGdIYUfweuOvwrvDD-Q3Hv5J0fSKbE",
"use": "sig",
"alg": "ES256"
}

Public keys are published in the signing_keys array of the party’s UCP profile at /.well-known/ucp.

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

For HTTP REST transport, UCP uses RFC 9421 (HTTP Message Signatures).

HeaderDirectionRequiredDescription
Signature-InputRequest/ResponseYesDescribes signed components
SignatureRequest/ResponseYesContains signature value
Content-DigestRequest/ResponseCond.*SHA-256 hash of request/response body

* Required when request/response has a body

Content-Digest follows RFC 9530 and hashes the raw body bytes. This binds the message body to the signature without requiring JSON 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.

Signed Components:

ComponentRequiredDescription
@methodYesHTTP method (GET, POST, etc.)
@authorityYesTarget host (prevents cross-host relay)
@pathYesRequest path
@queryCond.*Query string (if present)
ucp-agentCond.**Profile URL (binds identity)
idempotency-keyCond.***Idempotency header (state-changing)
content-digestCond.†Body digest (if body present)
content-typeCond.†Content-Type (if body present)

* Required if request has query parameters ** Required if UCP-Agent header is present *** Required for POST, PUT, DELETE, PATCH † Required if request has a body

Signature Encoding: ECDSA signatures MUST use fixed-width raw r||s encoding per RFC 9421, not ASN.1/DER. The signature value is the concatenation of r and s as fixed-length unsigned big-endian integers: 64 bytes for P-256 (32 + 32), 96 bytes for P-384 (48 + 48).

Complete Request Example:

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" "@authority" "@path" "ucp-agent" "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):

GET /checkout-sessions/chk_123 HTTP/1.1
Host: merchant.example.com
Signature-Input: sig1=("@method" "@authority" "@path");keyid="platform-2026"
Signature: sig1=:MEQCIBx7kL9nM2oP5qR8sT1uV4wX6yZaB3cD...:

Response signatures use @status instead of @method:

Signed Components:

ComponentRequiredDescription
@statusYesHTTP status code (200, 201, etc.)
content-digestCond.*Body digest (if body present)
content-typeCond.*Content-Type (if body present)

* Required if response has a body

Complete Response Example:

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"}}

Determining Signer’s Profile URL:

The signer’s profile URL is obtained from the UCP-Agent header, which uses RFC 8941 Dictionary syntax:

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. For business profiles, URL MUST point to /.well-known/ucp; platform profile URLs are not path-constrained
  5. Reject non-HTTPS URLs

Applicability:

  • Platform → Business requests: Profile URL from UCP-Agent header
  • Business → Platform webhooks: Profile URL from UCP-Agent header

UCP handles replay protection at the business layer through idempotency keys, not at the signature layer.

LayerResponsibility
SignatureAuthentication (who), Integrity (what)
IdempotencySafe 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 Requirements:

RequirementValue
EntropyMinimum 128 bits (e.g., UUID v4, 22+ char alphanumeric)
UniquenessPer-client, per-operation type
Server storageMinimum 24 hours, recommended 48 hours
On duplicateReturn cached response, do not re-execute
On storage failureFail closed (reject request with 503)

Requests: Platforms SHOULD sign all requests when using HTTP Message Signatures. Alternative authentication mechanisms (API keys, OAuth, mTLS) may be used instead.

Webhooks: Webhook notifications MUST be signed. Recipients cannot otherwise verify authenticity of server-initiated push messages.

Other responses: Signatures are RECOMMENDED for:

  • Payment authorization responses
  • Checkout completion responses

Signatures are OPTIONAL for:

  • Cart operations (low-value, synchronous)
  • Catalog queries (read-only)
  • Error responses (4xx, 5xx)

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:

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" "@authority" "@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":{...}}}}

Signature-specific errors:

CodeHTTPDescription
signature_missing401Required signature header/field not present
signature_invalid401Signature verification failed
key_not_found401Key ID not found in signer’s signing_keys
digest_mismatch400Body digest doesn’t match Content-Digest header
algorithm_unsupported400Signature algorithm not supported

Profile-related errors:

CodeHTTPDescription
invalid_profile_url400Profile URL malformed or invalid scheme
profile_unreachable424Unable to fetch signer’s profile
profile_not_trusted403Profile URL not in registry of pre-approved platforms
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"code": "signature_invalid",
"content": "Request signature verification failed for key kid=platform-2026"
}
{
"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"
}
}
}