feat: identity linking — mechanism extensibility with wallet attestation#415
Open
douglasborthwick-crypto wants to merge 1 commit intoUniversal-Commerce-Protocol:mainfrom
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
This PR adds
wallet_attestationas a non-OAuth identity mechanism for identitylinking, materializing the
config.providersextension point reserved in #354and corresponding to RFC #355 Phase 3 (Mechanism Extensibility).
Context. #354 established the OAuth 2.0 foundation and reserved
config.providersas a single extension point — a map keyed by reverse-domainprovider identifier, with a
typediscriminator defaulting tooauth2,explicitly anticipating "future non-OAuth mechanisms such as wallet attestation"
in the schema's
$comment. RFC #355's dependency graph cites EP #287 as thecanonical 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-08backport).This PR is non-breaking. Businesses that do not declare
config.providerscontinue 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.providersis reachable today via the existingadditionalProperties: truerule onconfig— businesses can declare aprovidersmap at runtime without any schema-level change, and theschema-level
$comment's normative ignore-and-fall-back rule coversdiscovery semantics for unrecognized
typevalues. This PR adds thewallet_attestation_provider$defto the$defscatalog so spec text andfuture structural work have a typed shape to reference. Phase 2 (#330) will
formalize
config.providersas a structural typed field with aoneOfdiscriminator 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:
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
falseautomatically.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
oauth2andwallet_attestationprovider entries in
config.providersfor 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
articulated a structural OAuth gap: the advertisement of benefits
(
scopes_supported.description: "log in for VIP pricing") and the deliveryof 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_attestationcollapses this. The signed boolean over the predicate ISthe contract — if the verifier signs
holds VIP NFT contract X ≥ 1with aJWKS-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_providerdefinition (source/schemas/common/identity_linking.json)Single additive change to the schema: a new
$defsentry materializing thewallet_attestationmechanism shape reserved in #354's schema-level$comment.Scope discipline: This is the only schema change in Phase 3. The structural
formalization of
config.providersas a typed field (oneOfdiscriminator,propertyNamespattern, mapping eachtypevalue to its$def) is Phase 2'sdeliverable per RFC #355 and PR #330 (igrigorik). Phase 3 relies on:
"additionalProperties": trueonconfig— businesses caninclude a
providersmap runtime without a schema change$commentnormative rule — "Platforms MUSTignore unrecognized fields in
configand fall back to this defaultbehavior" — already covers the discovery-and-fall-back semantics for
unknown
typevalues$def— catalog-available for Phase 2 to reference when itformalizes
providersas a structural fieldPer 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 Attestationsection (docs/specification/identity-linking.md)Adds a normative section after the OAuth 2.0 sections covering:
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 context; the set of chains a provider supports is documented at the
provider's own discovery surface, not in
config.providers. Differentproviders may cover different chain sets, and a business may declare
multiple
wallet_attestationprovider entries (keyed by reverse-domain) tocover the union of chains it accepts.
NFT-gated merchandise, allowlist gating, soulbound credentials). Not a
replacement for OAuth account linking — complementary mechanism.
config.providersentry shape withtype: "wallet_attestation",provider_jwks,attestation_endpoint. Reverse-domainkey naming.
provider_jwksand selectthe verification key by
kidfrom the attestation header.the algorithm declared in the JWKS entry (
ES256recommended).payload.pass(e.g.,expires_atclaim orattestedAt+ max-age).payload.pass) as theentitlement decision; raw chain state in the payload is informational.
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).
wallet_attestationcontributes zeroscopes. 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 freshattestations. Operations gated by both account-state AND chain-state
predicates may appear in
config.scopes(for the OAuth side) AND requirea
wallet_attestationprovider entry (for the chain-state side).New:
wallet_state_requiredandwallet_state_optionalinfo codesDefined normatively in spec text (in the new
## Wallet Attestationsection ofdocs/specification/identity-linking.md), mirroring howidentity_requiredand
insufficient_scopeshipped in #354 (normative definitions in specsections, not in the schema files). The two strings are also added to the
examplesarray insource/schemas/shopping/types/info_code.jsonasdocumentary entries (mirroring how
identity_optionalis listed there). Theinfo_code.jsonschema istype: string— freeform-permitted, no enforcedenum; the additions are documentary only and do not change validation
behavior.
Spec semantics:
wallet_state_required— operation requires a valid wallet_attestation; thebusiness has declared the predicate the attestation must satisfy. Platforms
MUST emit a
WWW-Authenticate: WalletAttestationchallenge on the 401response when this code is set (see "WalletAttestation HTTP authentication
challenge scheme" below).
wallet_state_optional— wallet attestation unlocks specific benefits; thebusiness 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 merchantcan legitimately emit BOTH
identity_optionalANDwallet_state_optionalonthe same operation, letting the agent choose which path to offer the user.
New:
WalletAttestationHTTP authentication challenge scheme [Provisional — open for TC review]#354 adopted RFC 6750 §3
WWW-Authenticate: Bearerchallenges for OAuthidentity linking. Wallet attestation is not OAuth and cannot use the
Bearerscheme verbatim — the challenge advertises a different proofmechanism.
Why a new scheme rather than extending
Bearerwith a parameter?Bearersemantics 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_attestationissues no token. The challengeadvertises a predicate-evaluation request: the platform calls the provider's
attestation_endpoint, receives a JWS-signed payload, and presents thepayload directly. A new scheme name keeps
Bearersemantics intact for OAuthand avoids overloading the registered scheme with an alternative flow.
Phase 3 defines:
realm(MUST) — issuer URI per RFC 6750 §3 conventionspredicate(MUST) — flat URL-safe encoded representation of the predicatethe 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 (thekidvalue the businessexpects to find in the attestation header). Allows platforms to validate
provider identity before issuing the request.
resource_metadata(SHOULD) — URI of the business'sucp-protected-resourcemetadata document, parallel to the
Bearerchallenge convention from feat!: identity linking OAuth 2.0 foundation with capability-driven scopes #354Scheme 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
Bearerwasspecified 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.
config.providerscontinue to use OAuth 2.0 with RFC 8414discovery on the business domain.
wallet_attestationMUST ignore entries withunrecognized
typevalues per the schema-level$commentrule, fallingback to the OAuth 2.0 default.
wallet_attestation_provider$defis catalog-available for Phase 2(delegated IdP, feat!: delegated IdP, identity chaining, and capability-driven scope model #330) to reference when it formalizes
config.providersasa structural field with a typed
oneOfdiscriminator. Phase 3 leaves thatformalization to Phase 2 by design.
Out of scope
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.
Businesses choose which
provider_jwksURLs they trust, the same way theychoose which OAuth issuers they accept.
per RFC [RFC] Proposal: Identity Linking, Identity Management, and Loyalty #355 dependency graph.
attestationseligibility extension. EP Wallet Attestation — Identity Mechanism + Eligibility Extension #287 covers twosurfaces: (1) the
wallet_attestationmechanism in the identity-linkingregistry (Layer 2 — this PR), and (2) an
attestationsmap on Cart/Checkoutfor attaching signed proofs alongside
context.eligibilityclaims (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
References
— Phase 3 (Mechanism Extensibility) per the dependency graph
— canonical schema input for this PR
— Phase 1 foundation; reserved the
config.providersextension point thisPR materializes
— Phase 2 (separate workstream; this PR is independent per RFC [RFC] Proposal: Identity Linking, Identity Management, and Loyalty #355 line 285)
— prior implementation attempt for the wallet_attestation mechanism, opened
Mar 19 2026, predating feat!: identity linking OAuth 2.0 foundation with capability-driven scopes #354's
config.providersextension point. This PRsupersedes feat: add wallet_attestation mechanism type to identity linking registry #280 by re-cutting the mechanism against the merged Phase 1
schema and the
config.providersmap shape ratified in feat!: identity linking OAuth 2.0 foundation with capability-driven scopes #354's$comment. Recommend closing feat: add wallet_attestation mechanism type to identity linking registry #280 in favor of this PR.— prior implementation attempt for the Cart/Checkout
attestationseligibility-extension surface. Out of scope for this PR (see Out of scope
above). feat: attestation extension for eligibility claims #264 remains the open workstream for that surface.
https://api.insumermodel.com/.well-known/jwks.json(ES256, P-256, kid
insumer-attest-v1) — InsumerAPI's published verificationkey set, demonstrating the verifier shape this PR specifies
Checklist