Skip to content

feat: signing for UCP requests & responses#156

Merged
wry-ry merged 7 commits intomainfrom
feat/signatures
Feb 17, 2026
Merged

feat: signing for UCP requests & responses#156
wry-ry merged 7 commits intomainfrom
feat/signatures

Conversation

@igrigorik
Copy link
Copy Markdown
Contributor

Context / Closes #135. Defines a layered signature architecture:

  SHARED FOUNDATION
  ├── Canonicalization: JCS (RFC 8785)
  ├── Algorithms: ES256 (required), ES384, ES512
  ├── Key Format: JWK (RFC 7517)
  ├── Key Discovery: signing_keys[] in /.well-known/ucp
  └── Replay Protection: idempotency-key (business layer)
          │
          ├── REST BINDING (RFC 9421)
          │   Headers: Signature, Signature-Input, UCP-Content-Digest-JCS
          │
          └── MCP BINDING (RFC 7515 Appendix F)
              Fields: meta.signature, meta.idempotency-key, meta.ucp-agent

JCS canonicalization (RFC 8785):

  • Deterministic JSON serialization before signing
  • Avoids whitespace/key-ordering verification failures
  • Custom header UCP-Content-Digest-JCS (not RFC 9530 Content-Digest)
    because we hash canonicalized JSON, not raw bytes

Transport-specific formats:

  • REST uses RFC 9421 HTTP Message Signatures (modern standard)
  • MCP uses detached JWS (RFC 7515 Appendix F) in meta.signature
  • Both sign the full message; MCP excludes only meta.signature field

Changes to OpenAPI:

  • Request-Signature header → Signature + Signature-Input headers
  • X-Detached-JWT response header → Signature + Signature-Input
  • UCP-Content-Digest-JCS header for body digest

Other updates:

  • checkout-rest.md, checkout-mcp.md: Added Message Signing sections
  • order.md: Rewrote webhook signing to use RFC 9421
  • ap2-mandates.md: Now references signatures.md for crypto primitives
  • openrpc.json: Added signature field to meta, added meta to cart methods
  • mkdocs.yml: Added signatures.md to nav and llmstxt sections

Checklist

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change
  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings

@igrigorik igrigorik changed the title feat!(signatures): signing for UCP requests & responses feat!: signing for UCP requests & responses Feb 4, 2026
Copy link
Copy Markdown
Collaborator

@drewolson-google drewolson-google left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally looks very good to me. I've included a few small comments.

Comment thread docs/specification/ap2-mandates.md
Comment thread docs/specification/signatures.md Outdated
Comment thread docs/specification/signatures.md Outdated
Comment thread docs/specification/ap2-mandates.md
@igrigorik
Copy link
Copy Markdown
Contributor Author

A couple of key takeaways from today's working group call...

1. Do we really need request signatures?

Yes and no.

  • No when alternative auth mechanisms exist: API keys, OAuth client credentials, mTLS. If you've already established trust out-of-band, signatures are redundant for authentication.
  • Yes for the distributed web use case. A self-hosted site can't negotiate API keys with every agent. Signatures with advertised public keys solve the one-to-many scaling problem — merchants can declaratively say "I trust agent A & B" without any out-of-band coordination.

Recommendation: "Businesses SHOULD authenticate agents". The spec should allow multiple mechanisms (API keys, signatures, MTLS) without mandating a single approach. Signatures become valuable when you want permissionless agent onboarding, and we should spec this path.

2. If agent auth is the primary goal, can we simplify what we're signing?

Likely, yes. Current proposal signs the entire request body with JCS canonicalization, which is complex and might be unnecessary — see 3. We could, potentially, skip signing body or consider Content-Digest: sha-256 of the payload bytes, which is a lot simpler. For identity we could lean on RFC 9421 request binding (method, path, idempotency-key, ucp-agent, content-digest?) which would cover request payload & agent attestation.

As a next step, we'll tap security folks to review and help converge on a recommendation.

3. Bonus: can we unify on RFC 9421?

Likely, yes. UCP's MCP transport runs over HTTP, which means we can rely on 9421: single signature mechanism, CDN/edge can validate, no JCS needed. This would be a significant simplification.

@igrigorik
Copy link
Copy Markdown
Contributor Author

@ACSchil fyi, updated to capture shape we discussed yesterday, ptal.

@igrigorik igrigorik changed the title feat!: signing for UCP requests & responses feat: signing for UCP requests & responses Feb 6, 2026
@igrigorik igrigorik self-assigned this Feb 6, 2026
@igrigorik igrigorik added this to the Working Draft milestone Feb 6, 2026
@igrigorik igrigorik marked this pull request as ready for review February 6, 2026 17:12
@igrigorik igrigorik requested a review from a team February 6, 2026 17:12
@igrigorik igrigorik added the TC review Ready for TC review label Feb 11, 2026
  Context / Closes #135.

  Defines a layered signature architecture:

  SHARED FOUNDATION
  ├── Canonicalization: JCS (RFC 8785)
  ├── Algorithms: ES256 (required), ES384, ES512
  ├── Key Format: JWK (RFC 7517)
  ├── Key Discovery: signing_keys[] in /.well-known/ucp
  └── Replay Protection: idempotency-key (business layer)
          │
          ├── REST BINDING (RFC 9421)
          │   Headers: Signature, Signature-Input, UCP-Content-Digest-JCS
          │
          └── MCP BINDING (RFC 7515 Appendix F)
              Fields: meta.signature, meta.idempotency-key, meta.ucp-agent

  JCS canonicalization (RFC 8785):
  - Deterministic JSON serialization before signing
  - Avoids whitespace/key-ordering verification failures
  - Custom header `UCP-Content-Digest-JCS` (not RFC 9530 Content-Digest)
    because we hash canonicalized JSON, not raw bytes

  Transport-specific formats:
  - REST uses RFC 9421 HTTP Message Signatures (modern standard)
  - MCP uses detached JWS (RFC 7515 Appendix F) in meta.signature
  - Both sign the full message; MCP excludes only meta.signature field

  Changes to OpenAPI:
  - `Request-Signature` header → `Signature` + `Signature-Input` headers
  - `X-Detached-JWT` response header → `Signature` + `Signature-Input`
  - `UCP-Content-Digest-JCS` header for body digest

  Other updates:
  - checkout-rest.md, checkout-mcp.md: Added Message Signing sections
  - order.md: Rewrote webhook signing to use RFC 9421
  - ap2-mandates.md: Now references signatures.md for crypto primitives
  - openrpc.json: Added signature field to meta, added meta to cart methods
  - mkdocs.yml: Added signatures.md to nav and llmstxt sections
  - Separate "Implementation requirements" (MUST verify ES256) from
    "Usage guidance" (SHOULD use ES256 for compatibility)
  - Removed "Signers: use newest key" - no way to determine key age
    from JWK spec, and verifiers accept any key in signing_keys[]
  This restructures UCP's signature specification based on working group
  discussion. Key changes:

  Multiple authentication mechanisms:
  - HTTP Message Signatures, API keys, OAuth 2.0, mTLS, choose whichever
  - Businesses choose what fits their security model and integration patterns
  - Implementations SHOULD maintain allowlists of trusted profiles

  RFC 9421 for all HTTP transports:
  - REST and MCP (streamable HTTP) now use the same signing mechanism
  - Signature + Signature-Input headers replace the deprecated Request-Signature
  - Covered components: @method, @path, content-digest, ucp-agent, idempotency-key

  Content-Digest (raw bytes) replaces JCS canonicalization:
  - Request signatures hash raw body bytes per RFC 9530
  - Simpler implementation, no JSON canonicalization complexity
  - JCS retained only for AP2 mandates
@github-actions
Copy link
Copy Markdown
Contributor

Super-linter summary

Language Validation result
BIOME_LINT Pass ✅
GIT_MERGE_CONFLICT_MARKERS Pass ✅
JSON Pass ✅
MARKDOWN Pass ✅
OPENAPI Pass ✅
PRE_COMMIT Pass ✅
TRIVY Pass ✅
YAML Pass ✅

All files and directories linted successfully

For more information, see the
GitHub Actions workflow run

Powered by Super-linter

Comment thread docs/specification/signatures.md Outdated
Comment thread docs/specification/signatures.md Outdated
Comment thread docs/specification/signatures.md Outdated
Comment thread docs/specification/signatures.md
Comment thread docs/specification/signatures.md
Comment thread docs/specification/signatures.md
Comment thread docs/specification/signatures.md Outdated
Comment thread docs/specification/checkout-rest.md Outdated
Comment thread source/services/shopping/rest.openapi.json Outdated
  - drop ES512/P-521 (not in IANA HTTP Signature Algorithms registry)
  - clarify algorithm derived from JWK `crv`, not Signature-Input `alg`
  - add ECDSA signature encoding requirement: raw r||s, not ASN.1/DER
  - add `@authority` to required signed components (prevents cross-host replay)
  - add `ucp-agent` to signed components table (binds identity to signature)
  - add identity binding requirement across all auth mechanisms
  - split webhook signing to MUST; keep other responses as RECOMMENDED
  - fix normative language: MAY choose auth mechanism, MUST sign fully once chosen
  - fix OpenAPI schema: signature headers required: false (conditional on auth mechanism)
  RFC 9421 (HTTP Message Signatures) deliberately leaves key discovery to
  the application: "a means of retrieving the key material... is within
  the purview of the application and outside the scope of this
  specification." UCP fills this gap.

  UCP profiles serve dual purpose — they declare capabilities for
  negotiation and publish signing keys for identity verification. This
  means the same fetch that enables capability negotiation also resolves
  the signing keys needed for RFC 9421 signature verification. The new
  Identity & Authentication section in overview.md defines this protocol:

  Key Discovery: profile URL -> fetch -> match keyid to kid in
  signing_keys[] -> verify signature. Both directions use the same
  mechanism (businesses at .well-known/ucp, platforms via UCP-Agent
  header).

  Profile Hosting: HTTPS-only, no redirects, Cache-Control with public
  and max-age >= 60s. Profiles are stable identity documents, not
  per-request configuration.

  Profile Fetching: two-tier trust model — a registry of pre-approved
  platforms with cached identity, and dynamic profile discovery for
  unrecognized platforms bounded by a fixed discovery footprint (LRU
  cache, global rate limit, backoff, async 503+Retry-After).

  Also defines authentication mechanism compatibility (API keys, OAuth,
  mTLS, HTTP Message Signatures) and identity binding requirements
  (authenticated identity must match UCP-Agent header).

  Slims signatures.md to signing/verification mechanics only; removes
  Profile Trust Model section (replaced by normative requirements in
  overview.md). Fixes orphaned validate_profile_url() reference, aligns
  error terminology with registry concept, and scopes the .well-known/ucp
  path constraint to business profiles only.
@github-actions
Copy link
Copy Markdown
Contributor

Super-linter summary

Language Validation result
BIOME_LINT Pass ✅
GIT_MERGE_CONFLICT_MARKERS Pass ✅
JSON Pass ✅
MARKDOWN Pass ✅
OPENAPI Pass ✅
PRE_COMMIT Pass ✅
TRIVY Pass ✅
YAML Pass ✅

All files and directories linted successfully

For more information, see the
GitHub Actions workflow run

Powered by Super-linter

@igrigorik
Copy link
Copy Markdown
Contributor Author

@maximenajim ty sir, sharp eyes! I believe all the flags should be addressed, please take another pass. 🙇🏻

@github-actions
Copy link
Copy Markdown
Contributor

Super-linter summary

Language Validation result
BIOME_LINT Pass ✅
GIT_MERGE_CONFLICT_MARKERS Pass ✅
JSON Pass ✅
MARKDOWN Pass ✅
OPENAPI Pass ✅
PRE_COMMIT Pass ✅
TRIVY Pass ✅
YAML Pass ✅

All files and directories linted successfully

For more information, see the
GitHub Actions workflow run

Powered by Super-linter

@wry-ry wry-ry merged commit 0426800 into main Feb 17, 2026
13 checks passed
@wry-ry wry-ry deleted the feat/signatures branch February 17, 2026 16:58
1. Profiles **MUST** be served over HTTPS.
2. Profile endpoints **MUST NOT** use redirects (3xx).
3. Profile responses **MUST** include a `Cache-Control` header with
`public` and `max-age` of at least 60 seconds. Profiles **MUST NOT**
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How should we handle the profile discovery when we get no-store or no-cache directives ? Reject, default to min TTL ?

alexpark20 added a commit to alexpark20/ucp that referenced this pull request Mar 17, 2026
Add 6 missed post-release changes discovered during full audit of 58 PRs
merged after the 2026-01-23 branch was cut:

  Universal-Commerce-Protocol#16 — Optional request/response signing headers (PR Universal-Commerce-Protocol#156) [Additive]
  Universal-Commerce-Protocol#17 — Eligibility claims & provisional discounts (PR Universal-Commerce-Protocol#250) [High/Breaking]
  Universal-Commerce-Protocol#18 — Optional signals field on cart and checkout (PR Universal-Commerce-Protocol#203) [Additive]
  Universal-Commerce-Protocol#19 — Fulfillment method id/type optional on update (PR Universal-Commerce-Protocol#143/Universal-Commerce-Protocol#196) [Low]
  Universal-Commerce-Protocol#20 — intent field added to context (PR Universal-Commerce-Protocol#95) [Additive]
  Universal-Commerce-Protocol#21 — available_instruments added to payment handler (PR Universal-Commerce-Protocol#187) [Additive]

Also updates PR_URLS map and changes/visible counts from 15 → 21.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
alexpark20 added a commit to alexpark20/ucp that referenced this pull request Mar 17, 2026
Add 6 missed post-release changes discovered during full audit of 58 PRs
merged after the 2026-01-23 branch was cut:

  Universal-Commerce-Protocol#16 — Optional request/response signing headers (PR Universal-Commerce-Protocol#156) [Additive]
  Universal-Commerce-Protocol#17 — Eligibility claims & provisional discounts (PR Universal-Commerce-Protocol#250) [High/Breaking]
  Universal-Commerce-Protocol#18 — Optional signals field on cart and checkout (PR Universal-Commerce-Protocol#203) [Additive]
  Universal-Commerce-Protocol#19 — Fulfillment method id/type optional on update (PR Universal-Commerce-Protocol#143/Universal-Commerce-Protocol#196) [Low]
  Universal-Commerce-Protocol#20 — intent field added to context (PR Universal-Commerce-Protocol#95) [Additive]
  Universal-Commerce-Protocol#21 — available_instruments added to payment handler (PR Universal-Commerce-Protocol#187) [Additive]

Also updates PR_URLS map and changes/visible counts from 15 → 21.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
alexpark20 added a commit to alexpark20/ucp that referenced this pull request Mar 17, 2026
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@wry-ry wry-ry added the enhancement New feature or request label Mar 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request TC review Ready for TC review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Spec] Clarify Request-Signature for Checkout REST binding (define signing/verification; RFC 9421 discussion)

6 participants