Skip to content

feat: identity linking — mechanism extensibility with wallet attestation#415

Open
douglasborthwick-crypto wants to merge 1 commit intoUniversal-Commerce-Protocol:mainfrom
douglasborthwick-crypto:feat/identity-linking-wallet-attestation
Open

feat: identity linking — mechanism extensibility with wallet attestation#415
douglasborthwick-crypto wants to merge 1 commit intoUniversal-Commerce-Protocol:mainfrom
douglasborthwick-crypto:feat/identity-linking-wallet-attestation

Conversation

@douglasborthwick-crypto
Copy link
Copy Markdown

Overview

This PR adds wallet_attestation as a non-OAuth identity mechanism for identity
linking, materializing the config.providers extension point reserved in #354
and corresponding to RFC #355 Phase 3 (Mechanism Extensibility).

Context. #354 established the OAuth 2.0 foundation and reserved
config.providers as a single extension point — a map keyed by reverse-domain
provider identifier, with a type discriminator defaulting to oauth2,
explicitly anticipating "future non-OAuth mechanisms such as wallet attestation"
in the schema's $comment. RFC #355's dependency graph cites EP #287 as the
canonical input for this phase. Per RFC #355 lines 285 and 651–655, Phase 3 is
independent of Phase 2 and follows the normal proposal lifecycle against main
(not the v2026-04-08 backport).

This PR is non-breaking. Businesses that do not declare config.providers
continue to use OAuth 2.0 with RFC 8414 discovery on the business domain.

Agent-as-buyer. Wallet attestation also enables flows where OAuth's
browser/consent-screen machinery is structurally unsatisfiable: autonomous
agents have wallets and can present chain-state proofs but cannot complete
OAuth's redirect-based consent flow. The wallet's prior on-chain
acquisitions are durable proof of consent that does not need to be
re-negotiated per request. For agent-as-buyer flows, wallet attestation is
not an alternative to OAuth — it is the only available identity primitive.

Schema-shape note. config.providers is reachable today via the existing
additionalProperties: true rule on config — businesses can declare a
providers map at runtime without any schema-level change, and the
schema-level $comment's normative ignore-and-fall-back rule covers
discovery semantics for unrecognized type values. This PR adds the
wallet_attestation_provider $def to the $defs catalog so spec text and
future structural work have a typed shape to reference. Phase 2 (#330) will
formalize config.providers as a structural typed field with a oneOf
discriminator over the catalog; that formalization is intentionally out of
scope here per RFC #355 line 285.


Why a different mechanism category — not "wallet-flavored OAuth"

OAuth assembles four pieces of infrastructure to manage entitlements:
consent UI, token expiry, revocation lists, and introspection endpoints.

Wallet attestation collapses these into chain state plus a signed boolean:

Operation OAuth wallet_attestation
Issuance Account creation + scope grant Chain mutation (mint, grant, transfer)
Consent Per-request consent screen Prior on-chain act of obtaining the asset
Revocation Revocation list / introspection Chain mutation (burn, transfer-out, downgrade)
Verification Token introspection or JWKS lookup JWKS signature check + freshness check on signed attestation

All four operations are anchored in chain state and verifiable offline against
the provider's published JWKS. The user's prior act of obtaining the asset is
durable proof of consent — not re-negotiated per request. Revocation is native:
the next attestation reads current state and returns false automatically.

This is the framing for why Phase 3 exists as a distinct mechanism class
rather than as a configuration variant of oauth2.


Complementarity with OAuth Identity Linking

This is not a replacement for OAuth Scenario 4 (custom roles via continue_url)
described in the RFC #355 Appendix A walkthrough. The two mechanisms cover
different layers of entitlement representation:

  • OAuth (Scenario 4) — account-based roles where entitlement lives in the
    business's database (e.g., a CRM tier, an employment role, a fraud-risk
    profile). The merchant authoritatively declares the role.

  • wallet_attestation — wallet-bound roles where entitlement lives in chain
    state (e.g., holding a VIP NFT contract, holding a tier token, an allowlist
    membership). Chain state is the source of truth; the verifier signs a boolean
    over a predicate.

A business can legitimately declare both oauth2 and wallet_attestation
provider entries in config.providers for the same identity-linking capability,
covering the full set of entitlements its commerce flow needs.


Pre-auth signal accountability

The two-comment exchange between @jamesandersen and @gsmith85 on RFC #355 (May 4
2026, 4374487442

  • 4374493631)
    articulated a structural OAuth gap: the advertisement of benefits
    (scopes_supported.description: "log in for VIP pricing") and the delivery
    of benefits are two unrelated promises. A user authenticates expecting a
    discount, the business evaluates eligibility post-auth against state the
    platform never sees signed, and the gap is a trust hole.

wallet_attestation collapses this. The signed boolean over the predicate IS
the contract — if the verifier signs holds VIP NFT contract X ≥ 1 with a
JWKS-verifiable signature, eligibility is met at the attested block height,
period. There is no two-step advertisement-then-delivery. The signature is the
trust signal to the platform, the user, and any post-hoc dispute-resolution
layer
that the merchant's claim is verifiable independent of the merchant's
backend.


What changed

New: wallet_attestation_provider definition (source/schemas/common/identity_linking.json)

Single additive change to the schema: a new $defs entry materializing the
wallet_attestation mechanism shape reserved in #354's schema-level $comment.

"wallet_attestation_provider": {
  "type": "object",
  "title": "Wallet Attestation Provider",
  "description": "Trusted third-party verifier that evaluates a business-supplied predicate against a wallet's on-chain state and signs the boolean result. Stateless: each attestation is a self-contained JWS-signed payload, verifiable offline against the provider's published JWKS.",
  "required": ["type", "provider_jwks"],
  "properties": {
    "type": { "const": "wallet_attestation" },
    "provider_jwks": {
      "type": "string",
      "format": "uri",
      "description": "HTTPS URI of the provider's JWKS (RFC 7517). Businesses fetch this URL to obtain the public keys used to verify attestation signatures."
    },
    "attestation_endpoint": {
      "type": "string",
      "format": "uri",
      "description": "Optional HTTPS URI where platforms POST attestation requests. The endpoint accepts a wallet address plus a predicate, evaluates the predicate against current chain state, and returns a JWS-signed payload (ES256 recommended per RFC 7518)."
    }
  },
  "additionalProperties": false
}

Scope discipline: This is the only schema change in Phase 3. The structural
formalization of config.providers as a typed field (oneOf discriminator,
propertyNames pattern, mapping each type value to its $def) is Phase 2's
deliverable per RFC #355 and PR #330 (igrigorik). Phase 3 relies on:

  1. The existing "additionalProperties": true on config — businesses can
    include a providers map runtime without a schema change
  2. The existing schema-level $comment normative rule — "Platforms MUST
    ignore unrecognized fields in config and fall back to this default
    behavior"
    — already covers the discovery-and-fall-back semantics for
    unknown type values
  3. The new $def — catalog-available for Phase 2 to reference when it
    formalizes providers as a structural field

Per RFC #355 line 285, "Phase 3 can land independently of Phase 2 (Phase 2
recommended but not required for mechanism extensibility)."
Keeping Phase 3's
schema delta minimal preserves that independence.

New: ## Wallet Attestation section (docs/specification/identity-linking.md)

Adds a normative section after the OAuth 2.0 sections covering:

  • How it works. The provider reads current chain state, evaluates a
    business-supplied predicate against that state, and returns a JWS-signed
    boolean attestation. The business verifies the signature offline against the
    provider's published JWKS. The attestation is stateless and self-contained
    — no callback, no session, no token exchange.
  • Chain coverage. UCP's schema is chain-agnostic. The predicate carries
    chain context; the set of chains a provider supports is documented at the
    provider's own discovery surface, not in config.providers. Different
    providers may cover different chain sets, and a business may declare
    multiple wallet_attestation provider entries (keyed by reverse-domain) to
    cover the union of chains it accepts.
  • When to use. Wallet-bound entitlements (token-gated catalog access,
    NFT-gated merchandise, allowlist gating, soulbound credentials). Not a
    replacement for OAuth account linking — complementary mechanism.
  • Provider declaration. config.providers entry shape with type: "wallet_attestation", provider_jwks, attestation_endpoint. Reverse-domain
    key naming.
  • Verification procedure (normative MUSTs):
    1. Platforms MUST fetch the provider's JWKS from provider_jwks and select
      the verification key by kid from the attestation header.
    2. Platforms MUST verify the JWS signature over the attestation payload using
      the algorithm declared in the JWKS entry (ES256 recommended).
    3. Platforms MUST check the attestation's freshness window before relying on
      payload.pass (e.g., expires_at claim or attestedAt + max-age).
    4. Platforms MUST treat the signed boolean (payload.pass) as the
      entitlement decision; raw chain state in the payload is informational.
  • Privacy property. The attestation answers "does this wallet meet the
    predicate?"
    rather than "what does this wallet hold?" — a buyer proving
    they hold ≥1 of a token never reveals the actual balance or the rest of
    their portfolio to the business. This is a structural distinction from raw
    on-chain queries (which leak full balances) and from credential-style
    approaches (which require buyer-side wallet UX).
  • Relationship to OAuth scopes. wallet_attestation contributes zero
    scopes. The mechanism is stateless: each attestation is self-contained.
    Operations gated by chain-state predicates do not appear in
    config.scopes; they are evaluated at request time against fresh
    attestations. Operations gated by both account-state AND chain-state
    predicates may appear in config.scopes (for the OAuth side) AND require
    a wallet_attestation provider entry (for the chain-state side).

New: wallet_state_required and wallet_state_optional info codes

Defined normatively in spec text (in the new ## Wallet Attestation section of
docs/specification/identity-linking.md), mirroring how identity_required
and insufficient_scope shipped in #354 (normative definitions in spec
sections, not in the schema files). The two strings are also added to the
examples array in source/schemas/shopping/types/info_code.json as
documentary entries (mirroring how identity_optional is listed there). The
info_code.json schema is type: string — freeform-permitted, no enforced
enum; the additions are documentary only and do not change validation
behavior.

Spec semantics:

  • wallet_state_required — operation requires a valid wallet_attestation; the
    business has declared the predicate the attestation must satisfy. Platforms
    MUST emit a WWW-Authenticate: WalletAttestation challenge on the 401
    response when this code is set (see "WalletAttestation HTTP authentication
    challenge scheme" below).
  • wallet_state_optional — wallet attestation unlocks specific benefits; the
    business advertises a chain-state-bound benefit (e.g. "verify your wallet
    for VIP pricing"
    ) and the platform may surface the option to the user.

These codes compose with identity_* rather than replacing them — a merchant
can legitimately emit BOTH identity_optional AND wallet_state_optional on
the same operation, letting the agent choose which path to offer the user.

New: WalletAttestation HTTP authentication challenge scheme [Provisional — open for TC review]

#354 adopted RFC 6750 §3 WWW-Authenticate: Bearer challenges for OAuth
identity linking. Wallet attestation is not OAuth and cannot use the
Bearer scheme verbatim — the challenge advertises a different proof
mechanism.

Why a new scheme rather than extending Bearer with a parameter? Bearer
semantics imply a token-issuance flow: the platform exchanges the challenge
for a token via an authorization endpoint, then presents that token on
subsequent requests. wallet_attestation issues no token. The challenge
advertises a predicate-evaluation request: the platform calls the provider's
attestation_endpoint, receives a JWS-signed payload, and presents the
payload directly. A new scheme name keeps Bearer semantics intact for OAuth
and avoids overloading the registered scheme with an alternative flow.

Phase 3 defines:

WWW-Authenticate: WalletAttestation
                  realm="https://merchant.example.com/",
                  predicate="<URL-safe-encoded predicate>",
                  expected_kid="<provider-kid>",
                  resource_metadata="https://merchant.example.com/.well-known/ucp-protected-resource"
  • realm (MUST) — issuer URI per RFC 6750 §3 conventions
  • predicate (MUST) — flat URL-safe encoded representation of the predicate
    the attestation must satisfy (e.g., chain=base&erc20=0xABC&op=gte&value=1).
    Suitable for single-condition predicates; complex composition (multi-clause
    AND/OR) is out of scope for this PR — see "Predicate encoding follow-up"
    below.
  • expected_kid (SHOULD) — JWKS key identifier (the kid value the business
    expects to find in the attestation header). Allows platforms to validate
    provider identity before issuing the request.
  • resource_metadata (SHOULD) — URI of the business's ucp-protected-resource
    metadata document, parallel to the Bearer challenge convention from feat!: identity linking OAuth 2.0 foundation with capability-driven scopes #354

Scheme name registration. Scheme names are governed by IANA's HTTP
Authentication Scheme Registry (RFC 7235). UCP defining the scheme inline
here, with intent to register if Phase 3 ratifies, mirrors how Bearer was
specified in OAuth 2.0 before its later IANA registration. Open to
alternative naming proposals during review.

Predicate encoding follow-up. The flat query-string encoding above is
sufficient for single-clause predicates (a balance check, an NFT holding,
an allowlist membership). Complex composition (multi-clause AND/OR, nested
conditions, schema-versioned predicate dialects) will need either a richer
encoding or a discovery-URI pattern (e.g., predicate_uri="https://merchant.example.com/.well-known/predicate/<id>")
where the predicate is published at a stable URL the platform fetches and
caches. Resolution of the encoding format is deferred to a follow-up Phase 3.x
PR so this PR's review surface stays focused on the mechanism itself.


Forward compatibility

This is a non-breaking addition.

  • Businesses without config.providers continue to use OAuth 2.0 with RFC 8414
    discovery on the business domain.
  • Platforms that do not implement wallet_attestation MUST ignore entries with
    unrecognized type values per the schema-level $comment rule, falling
    back to the OAuth 2.0 default.
  • The wallet_attestation_provider $def is catalog-available for Phase 2
    (delegated IdP, feat!: delegated IdP, identity chaining, and capability-driven scope model #330) to reference when it formalizes config.providers as
    a structural field with a typed oneOf discriminator. Phase 3 leaves that
    formalization to Phase 2 by design.

Out of scope

  • Issuance. Phase 3 defines a verification mechanism — given a wallet and a
    condition, return a signed boolean. How the wallet's chain state got into
    that condition is the issuance layer (ERC-721, ERC-1155, ERC-5114 soulbound,
    ERC-4671 non-transferable badges, allowlist mints, paid mints, snapshot
    drops). Issuance is vendor- and chain-specific by design; the verifier reads
    chain state regardless. This cut is consistent with [RFC] Proposal: Identity Linking, Identity Management, and Loyalty #355's Layer 2
    separation.
  • Wallet connection / signing UX. Platform-side concern, outside UCP scope.
  • Attestation provider selection. UCP does not endorse specific verifiers.
    Businesses choose which provider_jwks URLs they trust, the same way they
    choose which OAuth issuers they accept.
  • Phase 2 (delegated IdP). Out of scope for this PR; tracked separately
    per RFC [RFC] Proposal: Identity Linking, Identity Management, and Loyalty #355 dependency graph.
  • Cart/Checkout attestations eligibility extension. EP Wallet Attestation — Identity Mechanism + Eligibility Extension #287 covers two
    surfaces: (1) the wallet_attestation mechanism in the identity-linking
    registry (Layer 2 — this PR), and (2) an attestations map on Cart/Checkout
    for attaching signed proofs alongside context.eligibility claims (Layer 3,
    prior implementation attempt feat: attestation extension for eligibility claims #264). The eligibility-extension surface is a
    separate workstream and out of scope for this PR. The mechanism defined
    here can be used independently of any Cart/Checkout extension; the two
    compose cleanly when both are present, but neither depends on the other.

Type of change

  • New feature (non-breaking addition of wallet_attestation provider type)
  • Documentation update

References


Checklist

  • 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

Adds wallet_attestation as a non-OAuth identity mechanism for identity
linking, materializing the config.providers extension point reserved
in Universal-Commerce-Protocol#354 and corresponding to RFC Universal-Commerce-Protocol#355 Phase 3 (Mechanism Extensibility).

Schema:
- New wallet_attestation_provider $def under $defs (type discriminator,
  provider_jwks URI, optional attestation_endpoint URI;
  additionalProperties: false)

Spec text (docs/specification/identity-linking.md):
- New ## Wallet Attestation section with provider declaration,
  verification procedure, privacy property, scope relationship,
  WalletAttestation HTTP authentication challenge scheme [Provisional],
  wallet_state_required and wallet_state_optional info codes
- Future Extensibility updated to reflect wallet_attestation as
  defined in this version

Info codes:
- wallet_state_required and wallet_state_optional appended to
  source/schemas/shopping/types/info_code.json examples (documentary;
  schema permits freeform codes)

Non-breaking. Businesses without config.providers continue to use
OAuth 2.0 with RFC 8414 discovery on the business domain.

Phase 3 is independent of Phase 2 (Universal-Commerce-Protocol#330) per RFC Universal-Commerce-Protocol#355. Supersedes
prior implementation attempt Universal-Commerce-Protocol#280. Cart/Checkout attestations
extension (Universal-Commerce-Protocol#264) is out of scope.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants