From 2e87c90dc52efff475344b293330887b73373d70 Mon Sep 17 00:00:00 2001 From: Douglas Borthwick <256362537+douglasborthwick-crypto@users.noreply.github.com> Date: Thu, 19 Mar 2026 03:49:05 -0400 Subject: [PATCH 1/2] feat: add wallet_attestation mechanism type to identity linking registry --- .cspell/custom-words.txt | 3 + docs/specification/identity-linking.md | 221 +++++++++++++++++++- source/schemas/common/identity_linking.json | 26 ++- 3 files changed, 247 insertions(+), 3 deletions(-) diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index 9490b8f0..6e18cc45 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -107,3 +107,6 @@ zapatillas yml jwks keyid +ATST +attestedAt +expiresAt diff --git a/docs/specification/identity-linking.md b/docs/specification/identity-linking.md index 3cf85784..7c743b8a 100644 --- a/docs/specification/identity-linking.md +++ b/docs/specification/identity-linking.md @@ -45,8 +45,9 @@ negotiate the mechanism exactly like other UCP capabilities. Businesses **MUST** declare the supported mechanisms in the capability `config` using the `supported_mechanisms` array. Each mechanism must dictate its `type` -using an open string vocabulary (e.g., `oauth2`, `verifiable_credential`) and -provide the necessary resolution endpoints (like `issuer`). +using an open string vocabulary (e.g., `oauth2`, `wallet_attestation`, +`verifiable_credential`) and provide the necessary resolution endpoints (like +`issuer` for OAuth or `provider_jwks` for wallet attestation). ```json { @@ -276,6 +277,109 @@ Example metadata retrieved via RFC 8414: [OpenID RISC Profile 1.0](https://openid.net/specs/openid-risc-1_0-final.html) to enable Cross-Account Protection. +### Wallet Attestation (`"type": "wallet_attestation"`) + +The wallet attestation mechanism enables commerce flows where the user's +blockchain wallet address serves as the identity. Instead of redirecting to an +authorization server and establishing an account link, the platform obtains a +cryptographically signed attestation of on-chain state (token holdings, +credentials, membership) from a third-party verification provider. The business +verifies this attestation offline using the provider's published JWKS. + +This mechanism is appropriate when: + +- The commerce action depends on **what the user holds**, not who they are + (e.g., token-gated merchandise, holder-exclusive discounts, membership + verification). +- No persistent account link is needed — each transaction is independently + verifiable. +- The business wants to verify eligibility without requiring the user to create + an account or complete an OAuth redirect flow. + +#### JWKS Resolution + +When a platform encounters `"type": "wallet_attestation"`, it **MUST** use the +`provider_jwks` URI from the mechanism configuration as the JWKS endpoint for +signature verification. + +Platforms **MUST** fetch the JWKS document from `provider_jwks` and cache it +according to standard HTTP caching headers. If the fetch fails (non-2xx HTTP +response or connection timeout), the platform **MUST** abort the identity +linking process. + +The JWKS document **MUST** conform to +[RFC 7517 (JSON Web Key)](https://datatracker.ietf.org/doc/html/rfc7517). Each +key in the `keys` array **MUST** include a `kid` (Key ID) field. Platforms +select the verification key by matching the `kid` from the attestation response +against the `kid` values in the JWKS. + +#### Attestation Flow + +The wallet attestation flow is stateless — no redirect, no token exchange, no +account creation: + +1. The platform determines the user's wallet address (e.g., via a connected + wallet or user input). +2. The platform sends a verification request to the `attestation_endpoint` + (if provided in the mechanism configuration) with the wallet address and the + conditions to verify. +3. The provider evaluates the conditions against on-chain state and returns a + signed attestation containing at minimum: + - `pass` (boolean) — whether all conditions were met. + - `sig` — cryptographic signature over the attestation payload. + - `kid` — identifier of the signing key, resolvable via the provider's JWKS. +4. The platform attaches the attestation to the cart or checkout object. If a + dedicated attestation extension is available (e.g., a sibling map on the + checkout object keyed by reverse-domain eligibility claims), the platform + **SHOULD** use that extension. Otherwise, the platform **MAY** attach the + attestation as an opaque extension property. +5. The business verifies the attestation offline: fetch JWKS from + `provider_jwks`, select the key matching `kid`, verify `sig` over the + canonical serialization of the attestation payload. + +#### For platforms + +- **MUST** obtain the user's wallet address before initiating the attestation + flow. +- **MUST** send verification requests to the `attestation_endpoint` declared in + the mechanism configuration. If `attestation_endpoint` is not provided, the + platform **MUST** determine the provider's endpoint through out-of-band + configuration. +- **MUST** verify that the attestation response contains `sig`, `kid`, and a + payload with a `pass` boolean before relaying it to the business. +- **MUST** check `expiresAt` (if present) and discard expired attestations + before attaching them to cart or checkout objects. +- **SHOULD** cache JWKS responses according to HTTP caching headers to avoid + redundant fetches. + +#### For businesses + +- **MUST** declare `provider_jwks` in the mechanism configuration pointing to + the JWKS endpoint of each attestation provider they trust. +- **MUST** verify attestation signatures offline using the published JWKS. The + business fetches the JWKS, selects the key whose `kid` matches the + attestation's `kid`, and verifies `sig` over the canonical JSON serialization + of the attestation payload. +- **MUST** reject attestations where `kid` does not match any key in the + declared JWKS. +- **MUST** check `expiresAt` and reject expired attestations. +- **MUST NOT** trust attestation results without signature verification — the + `pass` boolean alone is not sufficient. +- **MAY** accept attestations from multiple providers by listing multiple + `wallet_attestation` entries in `supported_mechanisms`, each with a different + `provider_jwks`. + +#### Scope Considerations + +The wallet attestation mechanism does not require OAuth 2.0 scopes. +Verification is stateless and self-contained — there is no authorization grant, +no access token, and no persistent session. Capabilities that use wallet +attestation exclusively (without also requiring OAuth) contribute zero scopes to +the derived scope set. The pruning algorithm in the +[overview specification](overview.md) already handles this correctly: +capabilities without `identity_scopes` are retained in the intersection without +affecting the authorization scope union. + ## End-to-End Workflow & Example ### Scenario: An AI Shopping Agent (Platform) and a Shopping Merchant (Business) @@ -362,3 +466,116 @@ The AI Shopping Agent only knows how to perform checkouts. It does NOT yet know authorization code for an `access_token` bound only to checkout and successfully utilizes the UCP REST APIs via `Authorization: Bearer `. + +### Scenario: Token-Gated Commerce with Wallet Attestation + +#### 1. The Merchant's Profile (`/.well-known/ucp`) + +The Merchant sells token-gated merchandise. Holders of a specific token receive +exclusive discounts. The merchant supports both OAuth (for account-linked +experiences) and wallet attestation (for token-gated eligibility). + +```json +{ + "dev.ucp.shopping.checkout": [{ "version": "2026-03-14", "config": {} }], + "dev.ucp.common.identity_linking": [{ + "version": "2026-03-14", + "config": { + "supported_mechanisms": [ + { + "type": "wallet_attestation", + "provider_jwks": "https://verifier.example.com/.well-known/jwks.json", + "attestation_endpoint": "https://verifier.example.com/v1/attest" + }, + { + "type": "oauth2", + "issuer": "https://auth.merchant.example.com" + } + ] + } + }] +} +``` + +#### 2. The AI Agent's Profile + +The AI Shopping Agent supports both wallet attestation and OAuth. + +```json +{ + "dev.ucp.shopping.checkout": [{ "version": "2026-03-14" }], + "dev.ucp.common.identity_linking": [{ "version": "2026-03-14" }] +} +``` + +#### 3. Execution Steps + +1. **Capability Discovery & Intersection**: The agent intersects profiles and + negotiates `dev.ucp.shopping.checkout` and + `dev.ucp.common.identity_linking`. +2. **Identity Mechanism Selection**: The agent applies the Mechanism Selection + Algorithm. The first entry is `wallet_attestation`, which the agent supports, + so it is selected (business preference takes priority). +3. **Wallet Address Resolution**: The agent determines the user's wallet address + — for example, by prompting the user to connect their wallet or reading a + previously stored address. +4. **Attestation Request**: The agent sends a verification request to the + `attestation_endpoint`: + + ```http + POST https://verifier.example.com/v1/attest + Content-Type: application/json + + { + "wallet": "0x1234...abcd", + "conditions": [ + { + "type": "token_balance", + "contractAddress": "0xabcd...1234", + "chainId": 1, + "threshold": 1, + "decimals": 18 + } + ] + } + ``` + +5. **Attestation Response**: The provider returns a signed attestation: + + ```json + { + "attestation": { + "id": "ATST-3F7A2B1C9D4E8F06", + "pass": true, + "results": [{ "condition": 0, "met": true }], + "attestedAt": "2026-03-18T12:00:00Z", + "expiresAt": "2026-03-18T12:30:00Z" + }, + "sig": "MEUCIQDx...base64...==", + "kid": "verifier-key-v1" + } + ``` + +6. **Attach to Checkout**: The agent attaches the attestation to the checkout + object as an extension property, keyed by the eligibility claim: + + ```json + { + "extensions": { + "com.merchant.token_holder": { + "provider_jwks": "https://verifier.example.com/.well-known/jwks.json", + "kid": "verifier-key-v1", + "attestation": { "id": "ATST-3F7A2B1C9D4E8F06", "pass": true, "results": [{ "condition": 0, "met": true }], "attestedAt": "2026-03-18T12:00:00Z", "expiresAt": "2026-03-18T12:30:00Z" }, + "sig": "MEUCIQDx...base64...==" + } + } + } + ``` + +7. **Business Verification (Offline)**: The merchant fetches the JWKS from + `https://verifier.example.com/.well-known/jwks.json`, selects the key with + `kid: "verifier-key-v1"`, verifies the signature over the canonical JSON + serialization of the `attestation` object, checks that `expiresAt` has not + passed, and reads `attestation.pass` to determine eligibility. If verified, + the token-gated discount is applied. No network call to the attestation + provider is needed at verification time. diff --git a/source/schemas/common/identity_linking.json b/source/schemas/common/identity_linking.json index d58007da..39dfa9ad 100644 --- a/source/schemas/common/identity_linking.json +++ b/source/schemas/common/identity_linking.json @@ -54,7 +54,7 @@ "properties": { "type": { "type": "string", - "description": "The mechanism type discriminator. Known values: 'oauth2'. Specific mechanism schemas constrain this to a constant value." + "description": "The mechanism type discriminator. Known values: 'oauth2', 'wallet_attestation'. Specific mechanism schemas constrain this to a constant value." } }, "additionalProperties": true @@ -81,6 +81,30 @@ } }, "additionalProperties": true + }, + + "wallet_attestation": { + "type": "object", + "title": "Wallet Attestation Mechanism", + "description": "Stateless identity mechanism where the user's blockchain wallet address serves as the identity. A third-party attestation provider verifies on-chain state (token holdings, credentials, membership) and returns a cryptographically signed result that the business verifies offline via JWKS. No redirect flow, token exchange, or account creation is required.", + "required": ["type", "provider_jwks"], + "properties": { + "type": { + "const": "wallet_attestation", + "description": "Wallet attestation identity mechanism." + }, + "provider_jwks": { + "type": "string", + "format": "uri", + "description": "URI of the JWKS endpoint containing the public key(s) used to verify attestation signatures. The business fetches this endpoint, selects the key matching the `kid` in the attestation, and verifies the signature. This is the trust anchor — the business declares which signing keys it accepts." + }, + "attestation_endpoint": { + "type": "string", + "format": "uri", + "description": "URI of the attestation endpoint that accepts wallet verification requests and returns signed attestation results. If omitted, the platform is responsible for determining the provider's attestation endpoint independently." + } + }, + "additionalProperties": true } } } From 83c859b51f877703301804d8a617cfe909c19e3b Mon Sep 17 00:00:00 2001 From: Douglas Borthwick <256362537+douglasborthwick-crypto@users.noreply.github.com> Date: Thu, 19 Mar 2026 04:12:50 -0400 Subject: [PATCH 2/2] chore: retrigger CLA check