From b8cdc717f856cf1fe2583dedb7734758daf2506e Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Thu, 27 Aug 2026 11:01:03 -0400 Subject: [PATCH 01/13] docs(rfc): propose ACK v2 ACK-ID: a core RFC (identities spelled did:web, one-fetch key resolution, grants, RFC 9421 signed requests, verification checklist, revocation levers, crit-based extension mechanism) plus six extensions, with ext-controller drafted in full. ACK-Pay: a design language plus one normative profile adopting the x402 offer-receipt artifacts and binding receipts to the authorizing grant. The README records settled decisions and the document map. --- docs/ack-id/rfc/README.md | 184 ++++++++++ docs/ack-id/rfc/core.md | 473 ++++++++++++++++++++++++++ docs/ack-id/rfc/ext-attestations.md | 67 ++++ docs/ack-id/rfc/ext-audit.md | 37 ++ docs/ack-id/rfc/ext-controller.md | 314 +++++++++++++++++ docs/ack-id/rfc/ext-delegation.md | 94 +++++ docs/ack-id/rfc/ext-revocation.md | 41 +++ docs/ack-id/rfc/ext-web.md | 52 +++ docs/ack-pay/receipt-verification.mdx | 5 +- docs/ack-pay/rfc/README.md | 50 +++ docs/ack-pay/rfc/core.md | 177 ++++++++++ 11 files changed, 1492 insertions(+), 2 deletions(-) create mode 100644 docs/ack-id/rfc/README.md create mode 100644 docs/ack-id/rfc/core.md create mode 100644 docs/ack-id/rfc/ext-attestations.md create mode 100644 docs/ack-id/rfc/ext-audit.md create mode 100644 docs/ack-id/rfc/ext-controller.md create mode 100644 docs/ack-id/rfc/ext-delegation.md create mode 100644 docs/ack-id/rfc/ext-revocation.md create mode 100644 docs/ack-id/rfc/ext-web.md create mode 100644 docs/ack-pay/rfc/README.md create mode 100644 docs/ack-pay/rfc/core.md diff --git a/docs/ack-id/rfc/README.md b/docs/ack-id/rfc/README.md new file mode 100644 index 00000000..c8d17cb9 --- /dev/null +++ b/docs/ack-id/rfc/README.md @@ -0,0 +1,184 @@ +# ACK-ID Core RFC (proposal) + +Draft restructuring of the ACK-ID v2 working draft into a small core RFC plus +named extensions. Not wired into the docs site; discussion material only. + +The v2 working draft referenced throughout (`docs/ack-id/specification.mdx` +and its section numbers) lives on the `ack-id-v2` branch +([PR #7](https://github.com/catena-labs/ack-private/pull/7)), not on `main`. + +## Motivation + +The v2 spec's L1-L4 conformance levels are an admission that it is four specs +stapled together. This proposal makes that structure literal: the core RFC is +roughly today's L1, and everything above it becomes an extension document that +can be adopted, versioned, and argued about independently. + +Design guidelines for the core: + +1. Use grants. +2. Work with Web Bot Auth as deployed today. +3. Plain JOSE; no VC machinery. +4. "It works with what you have", and a missing document is a failure, + never a fallback. Core resolves keys from one fixed path per identity, + the did:web resolution rule (`/did.json`; + `/.well-known/did.json` for a bare domain), and reads only the + `publicKeyJwk` entries. Orgs whose keys live behind an OIDC `jwks_uri` + reuse them via the discovery profile in ext-web. That profile is opt-in + rather than core, for two reasons: a fallback triggered by a 404 turns + key removal (core's revocation lever) into a silent widening of the + trusted key set, and the discovery locations real OIDC providers use do + not match path identities anyway. + +The target: implementable with a stock JOSE library and an HTTP client in an +afternoon. + +The through-line of the split: the ideas DIDs and VCs were for survive in +core (self-issued identifiers with no central registry, key rotation that +survives identity, signed portable claims about who may do what, +callback-free verification), while their encodings (JSON-LD, the VC data +model, presentation exchange, generalized DID resolution) move out. +Semantically a grant is a verifiable credential; ext-attestations keeps a +mechanical, non-normative mapping to the VC data model, so the VC and eIDAS +world re-enters as an adapter, never as a core dependency. Core dependencies +can never be removed; adapters can be added whenever demand shows up. + +Core is the foundation. The pairing that carries the original ACK-ID +promise, a verifiable line from a request back to the accountable entity +behind an agent, is **core + ext-controller**, which is why +ext-controller is drafted as a full RFC while the other extensions remain +stubs. The other flagship pairing is core + ACK-Pay: payment receipts that +walk back to an owner. + +## Document map + +| Current v2 spec section | Lands in | +| ------------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| 3.2 documents (keys only), 4 keys, 4.1 thumbprints | [core](./core.md) | +| 6 grant format, 6.1 leaf rules | [core](./core.md) | +| 8.1 minimal verification, 8.2 signed requests | [core](./core.md) | +| 9 revocation levers (jti single-use, short exp, key removal, owner unpinning) | [core](./core.md) | +| 3.1 controllers; 5 ownership proofs; 6.3 control grants; 4.2 key purpose | [ext-controller](./ext-controller.md) | +| 3.1 did:key; 6.2 chains and attenuation; 6.4 issuance service; 6.5 registration | [ext-delegation](./ext-delegation.md) | +| 9.1 signed revocation list; 9.2 status list | [ext-revocation](./ext-revocation.md) | +| 8.2 OAuth carriage; CIMD; DPoP; keyless clients; Web Bot Auth directory; RP discovery | [ext-web](./ext-web.md) | +| 7 attestations | [ext-attestations](./ext-attestations.md) | +| 8.3 full verification | [ext-controller](./ext-controller.md) | +| 8.4 evidence bundles; 10 event log | [ext-audit](./ext-audit.md) | +| 11 artifact type discipline | core Section 9 (extensions append rows) | +| 12 security considerations | core Section 10 (extensions carry their own) | +| 1.2 test vectors | core deliverable, tracked alongside it | +| 4.3 signer backend interface | cut; what survives is core Section 9's signing-tooling note | +| 13 hosted directory profile | stays a non-normative profile | + +[ext-controller](./ext-controller.md) is drafted as a full RFC; it carries +the ownership pitch and deserves the same scrutiny as core. The remaining +extension stubs summarize scope and defer normative text to the v2 working +draft sections they extract. + +## What stays in core even though it looks like extra + +- `cnf` possession binding. Without it grants are bearer tokens. +- The `crit` rule and rejected unknown `constraints`: a verifier rejects + any grant whose `crit` names a claim it does not implement, and any grant + whose `constraints` carry a member it does not understand. These two + rules are the extension mechanism; a meaning-changing extension claim + (`chain`, `status`) rides under `crit` and can never downgrade a + core-only verifier. +- A complete minimal revocation story (`jti` single-use, short `exp`, agent + key removal, owner-key unpinning). Revocable is half the point of grants; + the list mechanisms move out, the levers stay. + +## Settled decisions + +1. **Identifier spelling in `iss`/`sub`: did:web stays canonical, presented + URL-first.** Earlier drafts of this proposal spelled identities as bare + URLs in core and moved DIDs to ext-controller. Two things settled it the + other way: the x402 offer-receipt extension identifies JWS signers as + did:web and resolves their keys from the same `did.json` core reads, so a + URL-only core would reintroduce a second spelling the moment payments + enter; and ext-controller's controller derivation operates on the DID + path. The Cloudflare-shaped objection (their Wallets product + deliberately avoids DID-like schemes) changed the presentation and left + the wire format alone: core Section 3 now leads with the identity URL, + treats the DID spelling as a serialization requiring no DID library, + and uses the URL form wherever an external spec requires a URL + (`Signature-Agent`, per Web Bot Auth), with a bijective mapping and no + aliasing. +2. **Key location: the did:web resolution rule.** `/did.json` + for path identities, `/.well-known/did.json` for a bare domain; core reads + only the `publicKeyJwk` entries and ignores the rest of the document. One + hosted document then serves ACK-ID, ACK-Pay receipts, and x402 + offer-receipt verification. (Earlier drafts used separate `jwks.json` + paths; that meant a second document for no added capability.) Fetches + follow redirects, with the SSRF checks applied to every hop: the fixed + path is where resolution starts, not a constraint on serving topology. +3. **Control grants live in ext-controller.** Core verifiers pin owner + keys at onboarding and never consume a control grant, so moving them + into core would add surface with no core consumer. The counterargument + (they are the ownership story, and burying them weakens the pitch) is + answered by framing: ext-controller is drafted as a full RFC, and + core + ext-controller is presented as the flagship pairing throughout. +4. **did:key lives in ext-delegation, not ext-controller.** The ephemeral + sub-agent case is delegation-shaped (owner signs an intermediate, an + orchestrator mints a chained leaf for a did:key worker), so the whole + story reads in one document. ext-controller is about ownership of + consequential identities, which did:key can never anchor. +5. **The authority artifact is a grant, not a mandate.** Earlier drafts + called it a mandate. That word now carries at least three meanings in + agent commerce: AP2's Intent and Cart Mandates (a human approving a + purchase), a policy-shaped usage in finance ("keep the portfolio + balanced"), and this artifact. The first two are not ours to rename, and + an AP2 mandate can ride in the same request as this artifact, so one word + for both is unworkable. Qualifiers ("agent mandate") do not survive into + `typ` values or HTTP field names. "Grant" is the verb core already used + to define the artifact, it names a signed instrument rather than a rule + (which "permission" does not), and `Grant` is free as an HTTP field name + where `Authorization` and `Authority` are taken. The layer boundary this + makes explicit: an AP2 mandate says a human approved a transaction; an + ACK-ID grant says an owner authorized an agent to act. ACK documents use + "mandate" only when citing AP2. +6. **Controller derivation never crosses DNS labels.** A subdomain-hosted + agent (`agent.company.service.com`) is a bare origin, legal everywhere in + core, but in ext-controller it is an apex with no derived controller. A + label-based rule would need the Public Suffix List as its stop boundary + (an external, continuously edited dependency on which two verifiers + could disagree), and DNS delegation does not track organizational + control. + Platforms that want the ownership chain name agents as path identities + under the company host (`did:web:company.service.com:agent-name`); + identities name where keys live rather than where traffic is served, so + per-agent subdomains can stay for routing. +7. **`chain` carries one hash: the immediate parent.** The working draft + (6.2) had the leaf enumerate its full ancestry plus a path-consistency + rule; ext-delegation now specifies pairwise linkage: each artifact hashes + only its immediate parent, and ancestry is pinned transitively because a + parent's hash covers the parent's own `chain`. The commitment is + equivalent; the single entry is structurally decoy-proof where the flat + list needed the consistency rule to be enforced correctly; delegation is + local (a holder extends the chain knowing only its parent); and pairwise + hashing is the AP2 mandate-chain convention, so grants slot into AP2/UCP + flows without redesign. Field-tested in the id.sh reference + implementation, whose panel regressions include the decoy-hash rejection. + Known limit, true of any hash-linked format: an ancestor can never be + inserted above an issued root without reissuing everything below it. +8. **Unknown grant claims are ignored; `crit` marks the exceptions.** + Earlier drafts closed the claim table: a core verifier rejected any + top-level claim it did not recognize. PR #9 review argued that JWT + implementers expect additive claims to be ignored, and the closed table + made every extension claim a breaking change even when ignoring it was + safe. Core now follows the RFC 7515 `crit` pattern on payload claims: + verifiers ignore unrecognized claims, and a claim that changes the + grant's meaning MUST be named in `crit`, which verifiers without the + extension reject. The cost, stated openly: the safety of the rule moved + from the verifier to the issuer. Extensions therefore also give their + artifacts a second rejection surface (a distinct `typ`, a reserved + scope token, an `aud` shape core rejects) where ignoring a claim would + widen authority. +9. **Wildcard scope tokens are not reserved.** Earlier drafts reserved any + scope token containing `*` for ext-delegation. Scope meaning is + audience-owned, and the blanket reservation blocked RPs from their own + wildcard grammar (PR #9 review). Only `control` and `register` stay + reserved; ext-delegation's attenuation wildcards apply to + parent-to-child containment inside chains, which core-only verifiers + never accept anyway. diff --git a/docs/ack-id/rfc/core.md b/docs/ack-id/rfc/core.md new file mode 100644 index 00000000..5ce4089a --- /dev/null +++ b/docs/ack-id/rfc/core.md @@ -0,0 +1,473 @@ +# ACK-ID Core (draft RFC) + +**Status: proposal draft.** The key words MUST, MUST NOT, SHOULD, SHOULD NOT, +and MAY are to be interpreted as described in RFC 2119. + +## 1. Scope + +ACK-ID Core defines how an agent proves who it is and what it may do. The +proof is cryptographic and needs no callbacks. In core, the relying party +already knows and trusts the owner that authorized the agent. Core defines: + +- identities and key resolution, +- keys and thumbprints, +- the grant artifact, +- the signed-request carriage (Web Bot Auth compatible), +- the verification checklist, +- the revocation levers, +- the extension mechanism. + +All other topics live in extension documents layered on this one: ownership +discovery for unknown counterparties, delegation chains, revocation lists, +the OAuth surface, attestations, and audit logs. A core-only deployment is +complete and useful on its own. Example: an organization that authenticates +its own agents to its own services. To serve unknown counterparties, and to +trace a request back to an accountable entity, pair core with +ext-controller. + +### 1.1 How it works + +Three parties. An **owner** (Acme) is accountable for an **agent** (Acme's +invoice bot). A **relying party** (the API the bot calls) accepts the +agent's requests because the owner authorized them. + +Two keypairs: + +- The agent holds a keypair and publishes the public key in one JSON + document at its identity URL (Section 3). +- The owner holds a keypair and gives the public key to the relying party + once, at onboarding (Section 2). + +The flow: + +1. The owner signs a **grant**: a short-lived JWT saying this agent may + perform these actions at this relying party (Section 5). +2. The agent signs each HTTP request with its own key (RFC 9421) and + attaches the grant in the `Grant` field (Section 6). +3. The relying party verifies the request signature against the agent's + published keys (one HTTPS fetch). It verifies the grant against the + owner key it pinned at onboarding. It then checks that the grant's scope + covers the request (Section 7). + +Nobody calls anybody back. The owner can be offline. The agent proves +possession of its key on every request. The relying party verifies +everything locally except the one fetch of the agent's published keys. + +Every artifact verifies with a stock JOSE library. Conversion between an +identity's two spellings (Section 3.1) is string manipulation. + +## 2. Terminology + +- **Relying party (RP)**: the service an agent calls. It verifies what the + agent presents. This document says **verifier** when a rule also applies + to parties that are not the target of a request (for example, an auditor + who walks an ACK-Pay receipt trail). +- **Owner**: the accountable party behind an agent. In core, the RP knows + the owner out of band and pins its keys at onboarding. +- **Agent**: software acting under an owner's authority. +- **Identity**: an owner or agent, named by its HTTPS **identity URL** and + spelled as a did:web DID in artifacts (Section 3). +- **Grant**: a JWT in which an issuer gives a subject bounded authority to + act (Section 5). +- **Artifact**: any signed object this protocol defines. In core: grants + and signed requests. +- **Onboarding**: the out-of-band step in which an RP learns an owner's + grant-signing public keys and records (**pins**) them against the + owner's identity. The channel belongs to the RP: a business agreement, a + dashboard, a config file. Core requires only that the channel exists and + supports unpinning (Section 8). + +## 3. Identity and key resolution + +### 3.1 Identities + +An identity has two equivalent spellings: + +- the **identity URL**: `https://acme.com` for an owner, + `https://acme.com/invoice-bot` for an agent. The URL is where the + identity's keys are published (Section 3.2). +- the **did:web DID**: `did:web:acme.com`, `did:web:acme.com:invoice-bot`. + This spelling appears inside grants, as `iss` and `sub`. + +Conversion between the two is string manipulation: take the host, then the +path segments, and join them with `:` in the DID spelling. The mapping +works in both directions and gives exactly one result each way. One +identity, two spellings, each used only where this document says so. The +DID spelling exists for compatibility with adjacent layers that name +signers as DIDs; the x402 offer-receipt extension resolves its signers' +keys from the same document Section 3.2 defines. The URL spelling appears +where an external spec requires a URL (`Signature-Agent`, per Web Bot +Auth, Section 6). + +A host may carry any number of DNS labels: +`https://agent.company.service.com` is a legal root identity, spelled +`did:web:agent.company.service.com`, with keys resolved per Section 3.2 +rule 2. Host labels carry no relationship: core treats identities on +different hosts as unrelated, whatever labels they share. The +agent-to-owner binding is the grant (Section 5), and its `iss` may name an +owner on any host the RP has onboarded. Controller derivation +(ext-controller) operates on path segments only, never on host labels. A +deployment that must choose between subdomain-per-agent and path-per-agent +layouts should read that document's Section 2. + +Before a verifier uses an identity for comparison or URL construction, it +MUST reject the identity if its URL form contains any of: a userinfo +component, a port, a query, a fragment, a trailing slash, a dot-segment +(`.` or `..`), an empty path segment, percent-encoding, or `.well-known` +as its first path segment. The `.well-known` rule closes an aliasing hole. +The path identity `https://acme.com/.well-known` would publish its keys at +`https://acme.com/.well-known/did.json`. That is the same location that +serves the root identity `https://acme.com`, so one document would answer +for two identities. To compare two identities, a verifier MUST map both to +one spelling, lowercase the host (and the scheme, in the URL form), and +compare byte-for-byte. No other normalization is defined. + +### 3.2 Key resolution + +Given an identity, a verifier resolves its current public keys from its +**DID document**, fetched from exactly one location (the did:web resolution +rule): + +1. **Path identities** (URL form has a path): `/did.json`. +2. **Root identities** (bare origin): `https:///.well-known/did.json`. + +A verifier reads exactly one thing from the document: the key set. A key is +a `verificationMethod` entry whose `publicKeyJwk` member carries a JWK (RFC +7517). Core ignores every other member (`controller`, service endpoints, +non-JWK key encodings); ext-controller assigns them meaning. A document +with no parseable `publicKeyJwk` entries is malformed. + +A missing or malformed document ends resolution with failure. There is no +fallback location. That strictness is what makes key removal a revocation +lever (Section 8): a resolver that tried another location on 404 would +quietly widen a key set its publisher deleted. Orgs whose keys live behind +an OIDC `jwks_uri` use the opt-in discovery profile in ext-web. + +Non-normative: the same document serves the x402 offer-receipt extension's +JWS key discovery, which resolves did:web signers from `did.json`. One +hosted document covers ACK-ID verification and ACK-Pay artifact +verification. + +Resolution rules: + +- All fetches MUST use HTTPS, MUST time out, and MUST cap response size. + Fetchers MAY follow redirects to a bounded depth (RECOMMENDED limit 3). + Every hop MUST use HTTPS and MUST pass the address checks below. +- The identity URL is attacker-influenced until a signature verifies. + Fetchers MUST NOT connect to loopback, private (RFC 1918), link-local, + or unique-local addresses, on any hop. Deployments that face hostile + input SHOULD resolve DNS once and pin the address for the connection. +- Key currency is judged against a fresh read of the document. A verifier + MUST NOT use a read older than 300 seconds and SHOULD cache no longer + than 60. Publishers MUST serve the document with cache lifetimes of 60 + seconds or less. Removal of an agent key from the DID document then + invalidates everything that key signs within the cache bound: at most + 300 seconds. (Owner keys are pinned, not resolved; their lever is + unpinning, Section 8.) + +## 4. Keys + +- **Algorithms.** A verifier MUST accept both `EdDSA` (Ed25519, + `kty: "OKP"`, `crv: "Ed25519"`) and `ES256` (P-256, `kty: "EC"`, + `crv: "P-256"`), and MUST reject any other `alg`. A verifier MUST select + the verification key by `kid` and MUST confirm the selected key's + `kty`/`crv` matches the asserted `alg`. Keys outside this set MUST be + ignored: a co-hosted DID document may carry RSA keys for other + protocols, and they play no part here. Issuers SHOULD issue Ed25519 by + default; P-256 exists for hardware-backed and FIPS-constrained keys. +- **Custody.** Private keys are generated by the holder and MUST NOT be + transmitted to any server. The owner's key signs grants; each agent's key + signs that agent's requests. Owner keys SHOULD NOT live on the machines + running the agents they authorize. +- **Rotation.** An identity holds one or more keys. Rotation is + add-then-remove and MUST NOT change the identity. + +### 4.1 Thumbprints + +Keys are named by their RFC 7638 JWK thumbprint, base64url encoded +(computed per RFC 8037 for Ed25519). The thumbprint is the published key's +name everywhere: the JWK `kid`, the `verificationMethod` fragment, the +HTTP signature `keyid`, and the grant `cnf.jkt`. Lookup is by recomputed +thumbprint. Given a `kid` or `keyid`, the verifier selects the resolved +entry whose recomputed RFC 7638 thumbprint equals it. A stated JWK `kid` +or fragment is a label, never the selector, so a published entry cannot +claim another key's name. Exactly one entry may match. Zero matches, or +more than one, is a rejection. + +## 5. Grants + +A grant is a JWT: header `alg` per Section 4, `typ: "grant+jwt"`, `kid` the +issuer key's thumbprint. Verifiers MUST reject a grant whose `typ` differs. + +```json +{ + "iss": "did:web:acme.com", + "sub": "did:web:acme.com:invoice-bot", + "aud": "https://api.examplebank.com", + "scope": "invoices:read", + "constraints": { "region": "us" }, + "iat": 1781035200, + "exp": 1781121600, + "jti": "grn_4kq8", + "cnf": { "jkt": "tH5Qw9..." } +} +``` + +The example is non-normative; the recognized claims and their requiredness +are: + +| claim | requiredness | rule | +| ------------- | ------------ | --------------------------------- | +| `iss` | REQUIRED | DID of an onboarded owner | +| `sub` | REQUIRED | DID of the agent | +| `aud` | REQUIRED | single string, exact match | +| `scope` | REQUIRED | space-delimited action scopes | +| `iat`, `exp` | REQUIRED | numeric; time rules below | +| `jti` | REQUIRED | unique per issuer | +| `cnf` | REQUIRED | `jkt` possession pin | +| `constraints` | OPTIONAL | open object; members only narrow | +| `nbf` | OPTIONAL | numeric; time rules below | +| `crit` | OPTIONAL | critical claim names; rules below | + +**Unknown claims are ignored; `crit` names the ones that must not be.** A +verifier ignores any top-level claim it does not recognize. A claim that +changes what the grant means MUST be named in `crit` by its issuer. A +verifier MUST reject a grant whose `crit` names a claim the verifier does +not implement and evaluate. This is the RFC 7515 `crit` pattern, applied +to payload claims, and it is the extension mechanism. A claim like `chain` +(ext-delegation) or `status` (ext-revocation) rides under `crit`, so a +verifier that cannot evaluate it rejects the grant. `crit`, when present, +MUST be a non-empty array of unique strings. It MUST NOT name any claim in +the table above, and MUST NOT name a claim absent from the payload. A +violation of any of these rules is a rejection. + +- `iss` and `sub` are DIDs (Section 3.1). `iss` MUST name an owner the RP + has onboarded (Section 2). The grant MUST verify against a key pinned + for exactly that owner (Section 7). +- `aud` MUST be a single string exactly matching an identifier the RP + recognizes as its own. (Extensions define artifacts with other `aud` + shapes; core verifiers never accept those artifacts, by the rules above.) +- `scope` is space-delimited. Every token is an action scope whose meaning + is owned by the audience, wildcard grammars included. The tokens + `control` and `register` are reserved for extensions (ext-controller and + ext-delegation define them). A core verifier MUST reject a grant that + carries either. +- `constraints` is an object of audience-defined members. A member only + ever narrows what the grant allows: `{"region": "us"}` grants less than + no constraint at all. Because members only narrow, a verifier MUST + reject a grant that carries a member the verifier does not understand; + to skip one would widen the grant. This document assigns no meaning to + any member. Data that does not narrow authority does not belong in a + grant. +- Time rules, with a clock-skew allowance `skew` (RECOMMENDED 60 seconds) + applied uniformly: `iat <= now + skew`; `nbf <= now + skew` when + present; `exp > now - skew`; `exp > iat`; and `exp > nbf` when `nbf` is + present. The last two compare issuer-set values against each other, so + no skew applies: an empty validity interval makes the grant malformed. + A malformed or non-numeric time claim is a rejection. (`nbf` is + recognized because some JOSE libraries add it by default.) +- `jti` MUST be unique per issuer, with enough entropy that revocation and + single-use tracking by `jti` are well defined. +- `cnf.jkt` pins the key that must prove possession at presentation. A + grant without it would be a bearer artifact, and core has no bearer + artifacts. Verifiers MUST reject a grant that lacks `cnf.jkt`. Verifiers + MUST match `cnf.jkt` against the possession key exactly, never against + "any current key of the subject." + +## 6. Signed requests + +Agents sign requests with RFC 9421 HTTP message signatures. The profile +stays within what Web Bot Auth infrastructure verifies as deployed today, +so one signature serves ACK-ID verifiers and WBA edges alike. The parts +have three sources. RFC 9421 defines the signature itself and carries no +credential. `Signature-Agent` comes from Web Bot Auth. The `Grant` field +is defined by this document. + +- The signature MUST cover `@method`, `@target-uri`, `content-digest` when a + body is present, the `Signature-Agent` field, and the `Grant` field with + the `sf` parameter when grants are presented. +- `Signature-Agent` carries the agent's identity URL (the URL spelling of + its DID, Section 3.1; Web Bot Auth requires a URL here). It is an + untrusted hint until the signature verifies. Because it is under the + signature, a middlebox cannot swap it. +- `keyid` is the signing key's thumbprint. `created` (and `expires` where + used) bound replay. Verifiers MUST enforce a maximum `created` age + (RECOMMENDED default 300 seconds); the maximum is the RP's + replay-exposure ceiling, a policy choice like the Section 8 lifetime + maximum. When `expires` is present, it MUST be covered by the signature, + and a verifier MUST reject a request past it (the Section 5 `skew` + applies). `expires` only ever narrows the window: an `expires` beyond + the maximum `created` age does not extend acceptance. A captured request + replays verbatim within that window. RPs that accept non-idempotent + requests MUST therefore enforce replay protection: a cache over the + signature value, or the RFC 9421 `nonce` parameter. +- `Grant` is a structured-field List of Tokens (RFC 9651), one token per + compact JWT. (An RFC 9651 Token must begin with an alphabetic character + or `*`. A compact JWT's first segment is base64url of a JSON header and + always begins `eyJ`, so grants satisfy the grammar.) A verifier MUST + reject a request whose `Grant` field fails structured-field parsing; it + MUST NOT salvage tokens from it. An unsigned `Grant` field is open to + middlebox changes. Coverage is what binds the grants to the request, and + a verifier MUST check that coverage, never assume it. +- Each presented grant is evaluated independently; the request is authorized + only by grants that individually pass. + +Non-normative: a Web Bot Auth edge (for example, a CDN verifying crawler +signatures) discovers keys per its own draft, at +`/.well-known/http-message-signatures-directory` on the `Signature-Agent` +origin, not via Section 3.2. An agent that wants those edges to recognize +it also hosts that directory; see ext-web. The signature itself is the +same either way. + +## 7. Verification + +The RP holds the owner's grant-signing public keys, pinned at onboarding +(Section 2). The pinned set MUST contain grant-signing keys only; never, +for example, an issuer's continuously online revocation key +(ext-revocation). Three checks authorize a request. The only document +fetch is the resolution of the agent's keys per Section 3.2. + +1. **Possession**: verify the request signature (Section 6) against the + agent's keys, resolved fresh per Section 3.2 from the `Signature-Agent` + identity URL, with the Section 4.1 thumbprint check against `keyid`. +2. **Grant**, all of: + - `typ` is `grant+jwt`. + - The signature verifies against a key pinned for the owner named in + `iss`. Never merely "some pinned key": an RP with more than one + onboarded owner binds `iss` to the pinning entry, which stops one + owner minting grants attributed to another. + - `sub` equals the requesting agent's DID, mapped from the + `Signature-Agent` URL per Section 3.1. + - `aud` matches the RP exactly. + - The Section 5 claim table, `crit` rule, and time rules pass. + - The lifetime `exp - iat` does not exceed the RP's maximum (Section 8). + - No reserved scope tokens are present. + - `cnf.jkt` equals the possession key thumbprint. + - Every `constraints` member is understood and satisfied. + - The RP's Section 8 revocation posture holds. +3. **Scope**: the grant's scope covers the requested action. + +Pinning is core's stance, and it is also core's boundary. An RP that must +verify owners it never onboarded resolves issuer keys instead of pinning +them; that is ext-controller's Section 6. Nothing else in the checklist +changes. + +A passing verification proves continuity of key control and authorization. +It never proves human presence, consent, or the legal identity behind a +domain. + +## 8. Revocation + +Core's revocation story is complete against agent-key and grant +compromise. It consists of these levers; the list mechanisms live in +ext-revocation. + +- **Short `exp` is the primary mechanism.** Grant lifetimes SHOULD be + short (minutes to days). An `exp` on the order of minutes bounds + exposure with no revocation check at all. +- **Single-use grants.** For one-shot authority, the RP records the `jti` + at first acceptance and rejects reuse; the grant is spent when used. + Which actions need single-use authority is the RP's call (a payment + authorization, a one-shot registration); core supplies the mechanism. + Redemption MUST be atomic: a check-and-set keyed by (`iss`, `jti`). A + separate read-then-write lets two concurrent presentations both pass. + The RP keeps the record until the grant's `exp` has passed. +- **Key removal.** Removing an agent's keys from its DID document + invalidates everything the agent signs within the cache bound (Section + 3.2). +- **Owner-key unpinning.** Pinned owner keys are established out of band + at onboarding, and they are revoked the same way. RPs MUST support + unpinning a compromised owner key through the onboarding channel. + Unpinning invalidates every grant that key signed. + +A core RP MUST define a maximum acceptable grant lifetime, as a duration. +It MUST reject any grant whose lifetime `exp - iat` exceeds it; the +Section 7 checklist enforces this. Because acceptance also requires +`iat <= now + skew`, the same bound caps remaining validity at acceptance. +The maximum is the RP's exposure ceiling. An RP that needs to accept +longer lifetimes implements ext-revocation and learns the issuer's +mechanism at onboarding. There is no middle state in which a revoked, +unexpired grant is knowingly accepted. + +One consequence to weigh openly: core has no delegation, so an owner key +signs every grant, and minute-scale lifetimes keep that key signing +continuously. A core-only deployment therefore chooses between an online +owner key and longer grant lifetimes. ext-delegation resolves the tension: +the owner signs one medium-lived intermediate offline, and a hot service +mints the short-lived leaves. + +## 9. Extension mechanism + +Extensions add capability without weakening a core-only verifier, through +three surfaces: + +- **Critical claims.** An extension that changes what a grant means + defines a claim (`chain`, `status`) and requires issuers to name it in + `crit` (Section 5). Verifiers that implement the extension evaluate the + claim; every other verifier rejects the grant. A claim that is safe to + ignore is carried without a `crit` entry, and verifiers ignore it. +- **Constraints and scopes.** Narrowing semantics ship as `constraints` + members: an unrecognized member is a rejection (Section 5), so a + restriction is never skipped. Granting semantics ship as scope tokens: + an unrecognized token authorizes nothing, because no requested action + matches it. An extension MUST NOT ship narrowing semantics as scope + tokens. The reserved tokens `control` and `register` are rejected by + core verifiers until the defining extension is implemented. +- **Artifact types.** Every protocol artifact carries a distinct JOSE + `typ`. Verifiers MUST reject an artifact whose `typ` does not match its + context. The registry is this table plus each extension's "Registers" + section, governed with this specification. No external registry is + consulted at verification time. (Attestations are third-party artifacts + outside this registry by design; see ext-attestations.) + + | `typ` | artifact | defined by | + | ----------- | ------------------ | ---------- | + | `grant+jwt` | grants (Section 5) | core | + + Non-normative: signing tooling should not expose generic sign-anything + operations for protocol `typ` values. Mint a `typ` only through tooling + that enforces its artifact rules; that is what keeps one keypair safe + across every use. + +An RP MAY advertise accepted extensions, audience identifier, and required +scopes in a well-known discovery document (ext-web); core assigns it no +location or schema. An implementation states which extensions it supports. +There are no numbered conformance levels. + +## 10. Security considerations + +- **Key custody.** The machine holding a key is the trust boundary. A + pinned owner key co-located with its agents reduces every grant to + self-issuance. +- **Replay.** Signed requests carry bounded `created` windows, and + non-idempotent requests require a replay cache or nonce (Section 6). + Grants carry `exp` and `jti`; single-use redemption is `jti` + replay-checking. +- **Header integrity.** Unsigned `Grant` and `Signature-Agent` fields are + strippable and substitutable. Coverage (Section 6) is mandatory. A + verifier MUST verify the coverage, never assume it. +- **Key resolution SSRF.** Section 3.2 fetches an attacker-influenced URL. + Apply the stated protections to every fetch and every redirect hop. +- **Critical claims.** The `crit` rule rests on issuers naming every claim + that changes a grant's meaning. A meaning-changing claim omitted from + `crit` is ignored by verifiers without the extension. Extensions + therefore also give their artifacts a second rejection surface where + ignoring a claim would widen authority: a distinct `typ`, a reserved + scope token, or an `aud` shape core rejects. +- **Redirected key fetches.** Key resolution follows redirects (Section + 3.2), so whoever can set a redirect at the identity's origin chooses + where keys are read from. Publishers SHOULD serve `did.json` directly. + An RP MAY refuse redirected resolution where its policy needs the + stronger property. +- **Shared key sets.** A key set shared across identities (a root + identity's `/.well-known/did.json` serving a whole org) weakens what a + request signature proves. A passing signature proves possession of some + key in that set, never which party holds it. Any key-holder in the set + can pass Section 7 step 1 for any identity the set serves. + `Signature-Agent` MUST NOT be treated as authenticated identity on its + own; the `cnf`-bound grant identifies the agent. Publishers SHOULD serve + per-agent key sets, at path identities or per-agent hosts, where + isolation matters. +- **Privacy.** Verification is callback-free: credential use does not + reveal itself to issuers. Published DID documents are public; put no + personal data in identities or key metadata. diff --git a/docs/ack-id/rfc/ext-attestations.md b/docs/ack-id/rfc/ext-attestations.md new file mode 100644 index 00000000..23dc5d76 --- /dev/null +++ b/docs/ack-id/rfc/ext-attestations.md @@ -0,0 +1,67 @@ +# Extension: Attestations (stub) + +**Extracts v2 working draft section 7.** + +Third-party claims about an identity (KYB status, compliance level, audit +results), distinct from grants: an attestation asserts, a grant authorizes. + +## Adds + +- **Carriage.** JWTs (`typ: "jwt"`) or SD-JWTs (`typ: "dc+sd-jwt"`), issued by + any party under its own keys, typed by issuer-controlled `vct` per SD-JWT + VC. No central attestation-type registry. +- **Subject binding** (the one normative rule): the attestation issuer MUST + verify, within its own session with the subject, that the subject controls + the identity being named (a session-bound challenge signed by a current key + of that identity). RPs SHOULD require issuers to document their binding + procedure. +- **Selective disclosure.** SD-JWT only, and only here. Guidance: assert + results ("verified at level 2") rather than raw attributes (a date of + birth). +- **Freshness.** Attestations SHOULD include `cnf` (an RFC 7638 thumbprint + of the subject key the issuer verified at binding time) or a log-head + reference (ext-audit), so post-issuance key-set divergence is detectable. + A verifier compares `cnf` against the key the subject proves possession + of in the current interaction. It compares a log-head reference against + the subject's current published head. A mismatch fails the attestation, + never the session. Whether absent evidence downgrades or rejects is RP + policy, declared in advance. Exact binding and failure semantics are a + drafting requirement for this extension's normative text. + +## VC bridge (non-normative) + +Semantically a grant is a verifiable credential: an issuer, a subject, +claims, validity, and proof. The mapping to the W3C VC data model is +mechanical and value-preserving: the table round-trips the data. It does +not carry core's enforcement semantics (exact-`aud` matching, `cnf` +possession, the `crit` rule, rejected unknown constraints), which is why it +is an adapter, never an equivalence: + +| grant | VC data model | +| ----------------------------- | --------------------------- | +| `iss` | `issuer` | +| `sub` | `credentialSubject.id` | +| `aud`, `scope`, `constraints` | `credentialSubject` members | +| `iat` / `exp` | `validFrom` / `validUntil` | +| `jti` | `id` | +| JWS signature | `proof` | + +A gateway can wrap a grant as a `vc+jwt` for VC-consuming systems (or +translate a VC-shaped credential back into a grant) without losing data. This +extension is also where VC-formatted evidence enters ACK: an attestation MAY +be presented as a W3C VC and verified under this extension's rules. Wallet +and eIDAS ecosystems integrate here while core keeps zero VC dependencies. +The ideas survive in core; the encodings are adapters. A +normative adapter is a drafting requirement before any W3C VC is accepted +under this extension: claim-by-claim rules, rejection of unmappable claims, +and the accepted serializations (`vc+jwt`, `dc+sd-jwt`; no Data Integrity +proofs, matching core's JOSE-only posture). + +## Registers + +Nothing in the protocol `typ` registry, deliberately. Attestations are not +protocol artifacts: they ride the SD-JWT VC ecosystem's types (`typ: "jwt"`, +`typ: "dc+sd-jwt"`) and are typed by issuer-controlled `vct`, so existing +wallets and verifiers consume them unchanged. Core's distinct-`typ` rule +governs protocol artifacts only; an attestation can never be confused with a +grant because a grant MUST carry `typ: "grant+jwt"`. diff --git a/docs/ack-id/rfc/ext-audit.md b/docs/ack-id/rfc/ext-audit.md new file mode 100644 index 00000000..d69c8ad7 --- /dev/null +++ b/docs/ack-id/rfc/ext-audit.md @@ -0,0 +1,37 @@ +# Extension: Audit (stub) + +**Extracts v2 working draft sections 8.4 and 10.** + +Two audit mechanisms with very different costs: evidence bundles work today +with zero ecosystem; the event log adds detectable-history guarantees for +identities that need them. + +## Adds + +- **Evidence bundles.** RPs accepting high-stakes grants retain: the + presented JWTs; the exact fetched bytes of the key documents and + anchors, with retrieval times and what was validated against them; the + revocation artifact actually checked; and any signed log heads. The + result is a self-contained, independently re-verifiable dispute record + that survives any host disappearing or republishing. Re-fetches never + repair a bundle; the record is what was seen. Works with core alone; + SHOULD-level guidance. +- **Event log.** An append-only, hash-chained log of lifecycle events (key + add/remove, proof add/retract, revocations, deactivation): JCS-hashed + entries, `prev` linkage, per-entry JWS by the authorizing key + (`typ: "log-entry+jws"`), signed heads (`typ: "log-head+jwt"`), optional + host receipts (`typ: "log-receipt+jws"`). Replaying the log MUST reproduce + the identity's published documents exactly; divergence or conflicting + signed entries is portable proof of misbehavior. +- **Witnessing.** Heads published to at least one independent append-only + location, named in the identity's documents, so a host cannot present a + fabricated history to a first-time verifier. Witness trust belongs to the + verifier, never the host: a location the host nominates adds nothing + against that host fabricating history. RPs accept only witnesses they + trust independently: preconfigured, or a quorum across operators. + Revocation lists from log-backed issuers SHOULD commit to the log head + they reflect. + +## Registers + +- Artifact types `log-entry+jws`, `log-head+jwt`, `log-receipt+jws`. diff --git a/docs/ack-id/rfc/ext-controller.md b/docs/ack-id/rfc/ext-controller.md new file mode 100644 index 00000000..54e15529 --- /dev/null +++ b/docs/ack-id/rfc/ext-controller.md @@ -0,0 +1,314 @@ +# ACK-ID Extension: Controllers and Ownership (draft RFC) + +**Status: proposal draft.** The key words MUST, MUST NOT, SHOULD, SHOULD NOT, +and MAY are to be interpreted as described in RFC 2119. Extracts v2 working +draft sections 3.1 (controllers), 4.2, 5, 6.3, and 8.3 steps 2, 4, 5. + +## 1. Scope + +Core authorizes agents whose owner the RP onboarded out of band. Identity +there is pinned keys plus a claims check. This extension serves the other +case: an agent presents, and the RP has never met its owner. The RP needs +to establish, offline and without a callback, who stands behind the agent. +This extension defines: + +- the controller relationship, derived mechanically from the identity name, +- key purpose segregation within resolved DID documents, +- control grants (the issuer asserts it controls the subject), +- ownership proofs (binding an owner to an external anchor), +- the full verification checklist. + +Together with core, this is the line back to the accountable entity: request +to agent key, agent to owner through the grant and the control grant, +owner to anchor through an ownership proof, anchor to legal entity through +the RP's own records. Core + this extension is the flagship pairing; a +deployment that never verifies unknown counterparties does not need it. + +This document layers on core. Identities and key resolution (core Sections +3.1-3.2), keys and thumbprints (core Section 4), grants (core Section 5), +and signed requests (core Section 6) apply here without redefinition. One +core premise is replaced: core verifies grants against pinned owner keys, +while this extension verifies them against assertion keys resolved from the +issuer's DID document (Section 6). That substitution is what removes the +onboarding requirement. + +## 2. Controllers + +Each agent identity has exactly one **controller**, derived by an exact rule: +**the controller is the DID with the final path segment removed.** The +recursion grounds at the bare origin. Multi-level paths are legal and derive +recursively (`did:web:acme.com:teams:invoice-bot` is controlled by +`did:web:acme.com:teams`, which is controlled by `did:web:acme.com`); +one-segment identities derive the bare origin directly. + +A bare origin can be an owner identity (a self-hosted deployment, +`did:web:acme.com`) or a hosted directory's apex serving many tenants. A +verifier never classifies it in advance; what verifies decides it. The +derivation names exactly one candidate controller per level. When a control +grant from the candidate verifies (Section 4), the candidate is the verified +controller. When none does (the directory-apex case, where the host never +asserts control of its tenants), the identity is a top-level owner: it has +no controller, and for consequential decisions it anchors directly through +ownership proofs (Section 5). + +The agent's DID document `controller` property MUST equal the derived +controller; verifiers MUST reject a document that disagrees with the +derivation. Because the rule is mechanical and there is exactly one +legitimate control-grant issuer per agent (Section 4), two verifiers can +never disagree about who the controller is. Verifiers MUST key policy +decisions on the canonical DID, never on an origin or display string. + +Derivation operates on path segments only, never on DNS labels. Every bare +origin, including a multi-label host such as +`did:web:agent.company.service.com`, is an apex: it has no derived +controller, and a control grant asserting control of a bare origin has no +legitimate issuer (Section 4) and MUST be rejected. A label-based rule is +deliberately not defined. It would need a stop boundary between +organizational labels and registry labels, which is the Public Suffix List. +That list is an external, continuously edited dependency: two verifiers +with different copies would derive different controllers, and the +never-disagree property above would be lost. DNS delegation also does not +reliably track organizational control: the platform operating `service.com` +controls every label beneath it, whoever a subdomain nominally belongs to. + +A deployment that gives each agent its own subdomain therefore chooses +between two shapes. Treat each agent subdomain as a top-level owner in its +own right, pinned at onboarding (core) or anchored directly by ownership +proofs (Section 5), with no derived chain. Or name agents as path +identities under the company's host +(`did:web:company.service.com:agent-name`), which restores the mechanical +chain: the agent's controller is `did:web:company.service.com`, itself +anchorable to the company's own domain by an ownership proof. The identity +names where keys live (core Section 3.2), never where the agent serves +traffic. A platform can keep per-agent subdomains for routing while naming +identities as paths. + +Core reads only `publicKeyJwk` entries from the DID document (core Section +3.2); this extension is what assigns the document's `controller` and +verification-relationship members their meaning and enforcement. + +## 3. Key purpose + +Keys are segregated by purpose through the DID document's verification +relationships, and verifiers implementing this extension MUST enforce the +segregation: + +- **Assertion keys** appear in `assertionMethod`. Only these keys sign + grants and proof claims. +- **Request keys** appear in `authentication`. They sign HTTP requests + (core Section 6) and DPoP proofs (ext-web); typically the agent's key. +- **Revocation keys** (ext-revocation) appear in the DID document but MUST + NOT appear in `assertionMethod`. They sign revocation artifacts and are + expected to be online continuously; the exclusion is what keeps a hot key + from minting grants. + +Enforcement, from published documents: a verifier MUST reject a grant or +proof claim signed by a key outside the issuer's `assertionMethod`. It MUST +verify request signatures only against keys in the subject's +`authentication`. It MUST reject a revocation artifact whose signing key is +absent from the issuer's DID document or present in `assertionMethod`. In +core, where the RP pins exact keys and never reads verification +relationships, segregation reduces to core's existing rules: the pinned set +contains grant-signing keys only (core Section 7), and possession matches +`cnf.jkt` exactly. + +Relationship entries resolve under a closed profile: `assertionMethod`, +`authentication`, and the revocation relationship carry fragment references +to the document's own top-level `verificationMethod` entries (the +`publicKeyJwk` entries core reads). Embedded verification methods inside a +relationship array, and references to anything outside the document, are +outside this profile and MUST be ignored. A reference with no matching +top-level entry is ignored the same way: the key is unavailable for that +purpose, and a dangling reference never widens what a key may do. Duplicate +`verificationMethod` ids make the document malformed (core Section 3.2). + +## 4. Control grants + +A control grant asserts the controller relationship as a verifiable +artifact: the issuer asserts it controls the subject. + +Shape: a grant (core Section 5, `typ: "grant+jwt"`) with these +deviations from core's claim table, which this extension defines for the +`control` scope only: + +| claim | rule | +| ------- | -------------------------------------------------------------------------------------- | +| `scope` | exactly the single token `control`; mixing with action scopes is rejected | +| `iss` | MUST equal the controller derived from `sub` (Section 2); any other issuer is rejected | +| `sub` | the agent DID | +| `aud` | MUST be absent | +| `cnf` | OPTIONAL | +| `exp` | REQUIRED; SHOULD be at most 90 days, reissued before expiry | + +It binds DIDs rather than keys, so key rotation invalidates nothing. The signature +MUST verify against a current assertion key of the derived controller +(Section 3). Because `iss` is derivable from `sub` by any verifier, there is +exactly one legitimate issuer, and a control grant from anyone else is +rejected regardless of signature validity. + +The table is the complete set of overrides. A verifier recognizing the +`control` scope evaluates the artifact under core Section 5 with exactly +these deviations and no others: `typ`, the claim table, the `crit` rule, +time rules, lifetime bound, and `jti` uniqueness stand unchanged. Core's +owner-onboarding premise is replaced per Section 6: the signature verifies +against resolved assertion keys, never a pinned set. Possession is carried +by the leaf grants presented alongside, whose `cnf.jkt` core already +requires. The control grant's `sub` MUST equal the presenting agent's DID +(the `Signature-Agent` mapping, core Section 3.1), or the grant is ignored +for this presentation. + +A verifier obtains the control grant from the presentation itself. An +agent operating under this extension presents it in the `Grant` field +alongside its leaf grants (core Section 6; each presented grant is +evaluated independently). It MAY additionally be published with the +subject's documents; publication location is deployment-defined. A lapsed +control grant means the ownership relationship no longer verifies +offline, and that state SHOULD be publicly legible wherever the identity is +rendered. + +Control grants need no distinct `typ`, unlike ext-delegation's +intermediates: the `control` scope token is mandatory and reserved, so every +core-only verifier already rejects them unconditionally (core Section 5). + +## 5. Ownership proofs + +A proof binds an identity to an external anchor. It has two halves, and a +verifier checks both directly, without reference to any directory: + +1. **The claim**: a JWS signed by a current assertion key of the identity, + header `typ: "proof+jwt"`, payload naming the anchor: + + ```json + { + "iss": "did:web:acme.com", + "type": "domain", + "anchor": "acme.com", + "iat": 1781035200, + "jti": "prf_8fk2" + } + ``` + +2. **The anchor**: a record placed where only the anchor's owner could put + it. + +### 5.1 Domain anchor + +Either of: + +- DNS TXT at `_ack-id.`: + + ``` + _ack-id.acme.com. TXT "ack-id=did:web:acme.com;jkt=kWmvje3K..." + ``` + + The `jkt` pins a key, and the pinned key MUST be a current **assertion + key** of the named identity; a pin naming a key outside `assertionMethod` + makes the proof fail. A verifier that reads a pinned assertion key from + DNS the counterparty controls, then verifies grants against that key, + has verified the chain with no reference to any key directory. `jkt` MAY + be omitted, but verifiers SHOULD prefer pinned anchors. Multiple `_ack-id` + records are evaluated independently; a record naming a different identity + MUST be surfaced, not silently ignored. + +- HTTPS file `https:///.well-known/ack-id.json`, embedding the claim + so the file is self-contained: + + ```json + { "claims": [""] } + ``` + +### 5.2 Code-host anchor + +A file committed where only the org could commit it. One exact location per +host, so two verifiers can never fetch different records. For GitHub: the +repository named `.ack-id` under the owner, file `ack-id.json` at the +repository root, read from the default branch through the host's raw +endpoint (`https://raw.githubusercontent.com//.ack-id/HEAD/ack-id.json`). +Profiles for other hosts pin their equivalents. The shape is the same as +the well-known file, with `anchor` = `github:` or the host's +equivalent. The proof record MUST store the host's stable numeric owner ID +at creation. Verification MUST NOT follow rename or transfer redirects and +MUST treat an owner-ID mismatch as revoked. Verifiers SHOULD weight domain +anchors above code-host anchors: anyone with repository-creation rights in +an org can mint the latter. + +### 5.3 Semantics + +A proof asserts control of that anchor and nothing more. RP policy MUST +compare proven anchors against out-of-band expectations of the counterparty +(a contract, a merchant record, an allowlist) and MUST NOT treat the +identity's name string as evidence of anything. + +Anchors MUST be re-checked periodically by whoever publishes proof status, +and a claim whose signing key is removed is dropped with it. Core's fetch +rules (SSRF checks on every hop, timeouts, size caps; core Section 3.2) +apply to every anchor fetch. Code-host anchor verification MUST NOT follow +rename or transfer redirects (5.2); that rule is this extension's, not +core's. + +## 6. Full verification + +In order, for consequential decisions or unknown counterparties: + +1. **Possession**: as core Section 7 step 1, with request keys drawn from + `authentication` only (Section 3). +2. **Canonical identity**: map the `Signature-Agent` URL to the agent's DID + (core Section 3.1) and resolve its DID document; policy keys on the DID. +3. **Each leaf grant**: core Section 7 step 2, with one substitution: the + signature verifies against a current assertion key of the issuer, + resolved per core Section 3.2 and segregated per Section 3, rather than a + pinned key. Where the issuer advertises a revocation mechanism + (ext-revocation), its status for the `jti` MUST be checked; otherwise + core's Section 8 posture (maximum acceptable lifetime) applies. +4. **Controller**: derive the controller (Section 2) and verify the + subject's control grant (Section 4). If it is absent or expired there + is no offline-verifiable owner; the RP fails or knowingly falls back to + host-asserted data at its own risk. +5. **Proofs and policy**: verify the controller's ownership proofs + (Section 5) and compare proven anchors against out-of-band expectations + (5.3). The chain holds only when the leaf grant's issuer is the + verified controller (or chains to it, ext-delegation); an action grant + not linked to the controller is third-party authorization the RP's policy + must independently accept. + +What a passing verification proves: continuity of key control, the control +relationship, and control of the proven anchors. The step from anchor to +legal entity belongs to the RP's records (the contract or merchant record +that names the domain), never to the protocol. It still never proves human +presence or consent. + +## 7. Interaction with core + +A core-only verifier rejects every artifact this extension defines: control +grants fall to core's reserved scope token, and a proof claim presented as +a grant falls to core's `typ` check (`proof+jwt` is not `grant+jwt`). +There is no downgrade path. The two verification modes compose: an RP MAY +run core verification for onboarded owners and full verification for +unknown ones; nothing in this extension changes the meaning of a core +verification that passes. + +## 8. Security considerations + +- **Name-derived control.** The controller relationship is derived from the + name, and whoever controls the origin serving the documents can publish + keys for every identity under it. The derivation rule makes this legible; + it does not remove it. Where that concentration matters, pin keys instead + of locations: a DNS `jkt` pin (5.1) verifies the chain with no reference + to the serving origin's key documents. +- **Display strings.** Names prove nothing (5.3). Any UI that renders an + identity SHOULD use its proven anchors as the trust signal, never its + name string. Lookalike names over unproven anchors are the expected + phishing shape. +- **Anchor fetching.** Server-side anchor fetching is an SSRF surface; core + Section 3.2 rules apply. DNS SHOULD be queried from multiple vantage + points and DNSSEC validated where present. +- **Code-host anchors.** Weight below domain anchors (5.2); repository + creation is a much weaker capability than DNS control. +- **Lapsed control grants.** A lapsed control grant is a silent + downgrade from verified ownership to host-asserted data. RPs SHOULD treat + it as a state change rather than a steady state (Section 4). + +## Registers + +- Scope token `control`, with the Section 4 claim table. +- Artifact type `proof+jwt`. diff --git a/docs/ack-id/rfc/ext-delegation.md b/docs/ack-id/rfc/ext-delegation.md new file mode 100644 index 00000000..619c3b56 --- /dev/null +++ b/docs/ack-id/rfc/ext-delegation.md @@ -0,0 +1,94 @@ +# Extension: Delegation Chains (stub) + +**Extracts v2 working draft sections 6.2, 6.4, and 6.5, plus the did:key +portion of 3.1.** + +Core grants are direct: owner signs, agent presents. This extension adds +re-delegation with normative attenuation. That is what lets owner keys stay +offline while short-lived grants are minted continuously. It is also where +the ephemeral sub-agent story lives: a chained leaf whose subject is a +did:key needs no hosted document at all. + +## Adds + +- **The `chain` claim** (reserved in core): exactly one entry, the SHA-256 + hash (over the ASCII bytes of the compact serialization) of the immediate + parent. Each parent carries its own `chain`, so the ancestry is a walked + linked list, and the full ancestry is pinned transitively: a parent's hash + covers the parent's own `chain` claim. Presenters carry every ancestor, + and every chained artifact names `chain` in `crit` (core Section 5), so + a verifier without this extension rejects it. Verifiers MUST reject a + `chain` with more than one entry: a longer list carries decoy hashes that + ride along unverified. Verifiers MUST detect cycles. They MUST check + issuer-subject linkage (`child.iss == parent.sub`), each ancestor's + signature, expiry, and revocation, and the root's `iss` against the RP's + trust anchor. This supersedes working draft 6.2's format (full-ancestry + list plus a path-consistency rule). The commitment is cryptographically + equivalent. The single entry is decoy-proof structurally rather than by + an extra rule, and pairwise hash linkage is the AP2 mandate-chain + convention, so a grant can slot into AP2/UCP flows without redesign. +- **Key binding through the chain.** A parent's `cnf.jkt` pins the exact key + that must sign its child. +- **Attenuation rules.** A child never exceeds its parent: `exp` never later, + `aud` never wider, every scope token authorized verbatim or by a parent + wildcard (`*`, or prefix form `invoices:*`), constraints provably within the + parent's under the audience's semantics, else reject. +- **Intermediates.** Delegation envelopes, never accepted as direct grants. + Attenuation wildcards and array/absent `aud` are legal only here, and an + intermediate MUST be structurally marked (a distinct `typ`, + `grant-int+jwt`, registered by this extension) so that every verifier + rejects it as a direct grant unconditionally, even when it carries a + plain shape. Without the marker, a plain-shaped intermediate + presented by a compromised issuance service would satisfy every core rule + at its full unattenuated breadth. +- **The issuance-service pattern.** Owner signs one medium-lived intermediate + offline; a hot service mints short-lived leaves; compromise is bounded by + the intermediate and revocable by its `jti`. The minting interface is + outside verification, and this extension will profile one for interop: an + OAuth 2.0 Token Exchange (RFC 8693) request whose `subject_token` is the + intermediate, whose `requested_token_type` names `grant+jwt`, and whose + response carries the leaf. Token-exchange-capable issuers (a Keycloak + realm, a custody provider's signing API) can expose it directly. + Verification of the resulting artifacts never involves the exchange + endpoint; every leaf still verifies under this extension's chain rules + alone, so the profile adds a mint surface and no trust surface. +- **did:key identities for ephemeral sub-agents.** A did:key carries its + public key inside the identifier, so it resolves with no fetch; it has no + hosted document, controller, rotation, or revocation, so it can never + anchor ownership. It exists to be the short-lived `sub` of a chained leaf + (owner signs the intermediate; an orchestrator mints a leaf whose subject + is the worker's did:key), and the worker dies with its grant. Encodes an + Ed25519 or P-256 key (core Section 4). Open item: signed-request carriage. + `Signature-Agent` requires a URL (core Section 6) and a did:key has none, + so the presentation profile for did:key subjects (key resolution directly + from the identifier, `Signature-Agent` absent or repurposed) needs + definition here. +- **Registration grants.** `scope: "register"` (reserved in core): + single-use, key-pinned, one-hour authority to create a named identity at a + hosted directory, with namespace-derivation checks so an owner's signature + never authorizes a name in someone else's namespace. Drafting requirements + for the normative text: `aud` names the directory, and the requested name + is carried explicitly in the grant, never inferred. Namespace containment + is byte-exact against the owner's identity after core Section 3.1 + validation (which already rejects names that would need escaping). + Redemption at the directory is an atomic check-and-set keyed by + (`iss`, `jti`), retained through `exp`. + +## Registers + +- Claim `chain`. +- Artifact type `grant-int+jwt` (intermediates). +- Scope token `register`, and the wildcard attenuation grammar evaluated + inside chains (`*`, prefix form). The grammar governs parent-to-child + containment only; what a scope token means to an audience stays the + audience's (core Section 5). + +## Interaction with core + +A core-only verifier rejects every artifact this extension defines: chained +leaves fall to core's `crit` rule (`chain` is always critical), +intermediates to core's `typ` check (`grant-int+jwt`), registration grants +to the reserved `register` scope token. There is no downgrade path. One consequence worth +naming: without this extension, every grant is signed directly by an owner +key, so short grant lifetimes keep that key hot (core Section 8 states the +tradeoff). diff --git a/docs/ack-id/rfc/ext-revocation.md b/docs/ack-id/rfc/ext-revocation.md new file mode 100644 index 00000000..91900556 --- /dev/null +++ b/docs/ack-id/rfc/ext-revocation.md @@ -0,0 +1,41 @@ +# Extension: Revocation Mechanisms (stub) + +**Extracts v2 working draft sections 9.1 and 9.2.** + +Core's revocation levers are short `exp`, single-use `jti`, agent key +removal, and owner-key unpinning. This extension defines the two +discoverable mechanisms for pulling back a still-valid grant by `jti`. It +also carries the issuer-side obligation: an issuer of grants that outlive its +RPs' acceptable exposure window MUST expose one of these mechanisms, and a +verifier implementing this extension MUST check it. + +## Adds + +- **Signed revocation list** (baseline): a short-lived JWT + (`typ: "revocation-list+jwt"`) listing revoked `jti` values, `exp` on the + order of minutes so staleness is legible, entries retained until the revoked + artifact's own `exp` passes. Fully publicly auditable; no holder privacy. +- **Status list** (optional profile): the IETF OAuth Token Status List, JWT + form only. Covered grants carry `status.status_list.uri` and `idx`, and + name `status` in `crit` (core Section 5); the + verifier fetches the whole list and reads the bit locally, so the issuer + never learns which credential was checked. For issuers with many holders to + protect; requires scale, randomized indexes, and decoys to deliver herd + privacy. The W3C BitstringStatusList (JSON-LD) MUST NOT be used. +- **Revocation keys.** Both artifacts are signed by a dedicated + revocation-purpose key, published in the DID document but excluded from + assertion use, so a continuously online key can never mint grants. + ext-controller's key-purpose segregation enforces the exclusion and is a + declared dependency of that guarantee. A deployment running this + extension without ext-controller enforces it at onboarding instead: a + core RP MUST NOT pin an issuer's revocation key among that issuer's + grant-signing keys. +- **Discovery.** Advertised via the `RevocationList` service entry in the DID + document (ext-controller) or learned at onboarding (core). + +## Registers + +- Artifact type `revocation-list+jwt`. +- Grant claim `status` (per the IETF status list draft), always named in + `crit`, so a status-bound grant is rejected by any verifier that cannot + check its status (core Section 5). diff --git a/docs/ack-id/rfc/ext-web.md b/docs/ack-id/rfc/ext-web.md new file mode 100644 index 00000000..58b85a00 --- /dev/null +++ b/docs/ack-id/rfc/ext-web.md @@ -0,0 +1,52 @@ +# Extension: Web Surface (stub) + +**Extracts v2 working draft section 8.2 (OAuth carriage, discovery, keyless +clients) and the Web Bot Auth directory (3.2, L3).** + +Core's signed-request carriage already uses the Web Bot Auth wire format. This +extension adds the surfaces that make an agent legible to infrastructure that +is not an ACK verifier: WBA edges, OAuth authorization servers, MCP servers. + +## Adds + +- **OIDC discovery key resolution** (opt-in profile). For orgs whose JWKS + lives behind an OAuth2/OIDC `jwks_uri` (a Keycloak realm, a hosted IdP) + rather than at core's fixed paths. + Explicitly configured per identity by the verifier, never triggered by a + 404 on the fixed path (core resolution has no fallback). For a configured + identity, the discovered `jwks_uri` is the sole key location and replaces + core's fixed path. Key sets from the two locations MUST NOT be unioned; a + union would stop key removal at either location from revoking. + Requirements: the RFC 8414 insertion form is fetched first and, when it + yields a valid document, is authoritative. The OIDC path-append form is + consulted only when the insertion form returns no document. When both + were fetched and disagree on `issuer` or `jwks_uri`, the verifier MUST + reject rather than choose. The discovery document's `issuer` MUST exactly + equal the identity URL. The `jwks_uri` MUST be same-origin with the + identity URL. Core's SSRF, redirect, timeout, and size rules apply to + every fetch, including the `jwks_uri` target. +- **Web Bot Auth directory.** Hosting + `/.well-known/http-message-signatures-directory` at the agent's signing + origin (a dedicated subdomain), per the HTTP message signatures directory + draft, so CDN-style edges that speak that draft recognize the agent. Same + keys as the core DID document, republished at the location their spec + requires. +- **OAuth via CIMD.** The identity URL doubles as an OAuth Client ID Metadata + Document `client_id`: registration by URL alone, `private_key_jwt` client + authentication, no client secret. Hardware-backed P-256 keys plug in here. +- **DPoP-bound tokens** (RFC 9449), with the `mh` claim binding the `Grant` + field on the OAuth carriage: base64url SHA-256 over the re-serialized + (RFC 9651) structured-field form, never raw wire bytes. +- **RP discovery.** The well-known document advertising audience identifier, + required scopes, accepted carriages, and supported extensions; 401 + challenges naming the same parameters; RFC 9728 protected-resource metadata + for MCP servers. +- **Keyless clients.** MCP assistant clients with + `token_endpoint_auth_method: none` and PKCE: bearer sessions that satisfy + no `cnf`-bound grant and are never an ACK-verified chain; ACK guarantees + come from a key-holding gateway signing upstream. + +## Registers + +- Artifact type `dpop+jwt` usage profile and the `mh` DPoP claim. +- The RP discovery document location and schema. diff --git a/docs/ack-pay/receipt-verification.mdx b/docs/ack-pay/receipt-verification.mdx index ec449e4c..d4183ca2 100644 --- a/docs/ack-pay/receipt-verification.mdx +++ b/docs/ack-pay/receipt-verification.mdx @@ -66,8 +66,9 @@ _Example ACK Receipt Verifiable Credential:_ `credentialSubject.metadata` is optional and should be treated as an extension point for verifier-specific payment evidence. Keep the core receipt fields -stable, and put references or hashes to external policy, mandate, execution, -and settlement records in metadata when a deployment needs a richer audit trail. +stable, and put references or hashes to external policy, AP2 mandate, +execution, and settlement records in metadata when a deployment needs a richer +audit trail. Metadata fields are non-normative: ACK-Pay verifies the receipt signature and the bound payment request token, but applications decide which metadata fields diff --git a/docs/ack-pay/rfc/README.md b/docs/ack-pay/rfc/README.md new file mode 100644 index 00000000..f258c084 --- /dev/null +++ b/docs/ack-pay/rfc/README.md @@ -0,0 +1,50 @@ +# ACK-Pay Core RFC (proposal) + +Draft restructuring of ACK-Pay into a design language plus one small +normative profile. Not wired into the docs site; discussion material only. + +Depends on the [ACK-ID core RFC](../../ack-id/rfc/README.md) for identities, +key resolution, grants, and signed requests. Read that first. + +## Motivation + +The payments wire layer is already won: x402, l402, h402, and the card +networks' agent programs define how money moves. A competing ACK wire +protocol would add confusion and get no adoption. None of them provides +accountability: a quote a seller cannot later deny, a proof of payment a +buyer can show to a third party, and a trail from that payment back to the +owner behind the paying agent. + +x402 now ships signed offers and receipts as an extension +([offer-receipt](https://docs.x402.org/extensions/offer-receipt)), with JWS +signing and did:web key discovery from `did.json`. That is the artifact layer +ACK-Pay would otherwise have had to invent, discoverable from the same DID +document ACK-ID core already reads. So ACK-Pay stops being a protocol and +becomes: + +1. **A design language** (non-normative): the roles, the client- and + server-initiated flows, human oversight points, and how the offer/receipt + pattern maps onto any rail. +2. **One normative profile** ([core](./core.md)): adopt the x402 + offer-receipt artifacts (pinned at a named version, core Section 1), sign + them with ACK-ID identities, and bind receipts to the grant that + authorized the payment. That binding is the trail: receipt to agent, + agent to grant, grant to owner, owner to legal entity (ACK-ID + ext-controller). Core + this profile is one of the two flagship pairings + named in the ACK-ID RFC; the accountability trail is the gap the wire + protocols leave. + +## Document map + +| Current ACK-Pay material | Lands in | +| -------------------------------------------------------- | ----------------------------------------------------- | +| introduction, components-roles, use-cases | design language (non-normative) | +| client-initiated-sequence, server-initiated-sequence | design language (non-normative) | +| core-payment-sequences, hitl, operational-considerations | design language (non-normative) | +| payment-request-payload, payment-service | superseded by signed offers ([core](./core.md)) | +| receipt-verification (VC receipts) | superseded by the receipt profile ([core](./core.md)) | +| `packages/ack-pay` (JWT request tokens, VC receipts) | recut against the profile after RFC decisions | + +## Open decisions + +See [core Section 8](./core.md#8-open-decisions). diff --git a/docs/ack-pay/rfc/core.md b/docs/ack-pay/rfc/core.md new file mode 100644 index 00000000..d73d31b7 --- /dev/null +++ b/docs/ack-pay/rfc/core.md @@ -0,0 +1,177 @@ +# ACK-Pay Core (draft RFC) + +**Status: proposal draft.** The key words MUST, MUST NOT, SHOULD, SHOULD NOT, +and MAY are to be interpreted as described in RFC 2119. + +## 1. Scope + +ACK-Pay Core defines how a payment leaves verifiable evidence: a **signed +offer** the seller cannot later deny quoting, a **signed receipt** the buyer +can present to third parties, and a binding from that receipt to the ACK-ID +grant that authorized the payment. It defines no wire protocol, no +settlement mechanism, and no new artifact formats. The artifacts are the +x402 [offer-receipt extension](https://docs.x402.org/extensions/offer-receipt), +adopted as published and profiled here. This profile pins the extension as +shipped in `@x402/extensions` 2.22.0 (x402 repo, offer-receipt source at +commit `59ac597`, 2026-06-17). Upstream changes flow into this profile only +by re-pinning here, never implicitly. Everything else ACK-Pay describes +(roles, payment flows, human oversight, rails other than x402) is design +language: non-normative patterns that define no conformance. + +This document layers on ACK-ID core: identities and key resolution (ACK-ID +Sections 3.1-3.2), keys and thumbprints (Section 4), grants (Section 5), +and signed requests (Section 6) are used here without redefinition. + +## 2. Terminology + +- **Seller**: the party serving the paid resource and signing offers and + receipts. The x402 resource server. +- **Buyer**: the agent paying for the resource, acting under an ACK-ID + grant from its owner. +- **Offer**: the seller-signed quote attached to a `402 Payment Required` + response. +- **Receipt**: the seller-signed acknowledgment attached to the `200 OK` + response after payment. +- **Rail**: the mechanism that moves the money. Core profiles x402; other + rails are design language (Section 6). + +## 3. Artifacts + +The offer and receipt payloads, their placement (offers in the 402 response's +`extensions` member; receipts in the `PAYMENT-RESPONSE` `extensions`), their +field sets (`resourceUrl`, `offerType`, `network`, `amount`, `payTo`, +`validUntil`; `resourceUrl`, `payer`, `network`, `issuedAt`, optional +`txHash`), and their signature encodings are as defined by the x402 +offer-receipt extension. This profile constrains them: + +- **Signature scheme.** ACK-Pay conformance requires the JWS scheme. An + EIP-712/did:pkh signature MAY additionally be present; it carries no + ACK-Pay semantics and is passed through unevaluated. +- **Signer identity.** The JWS signer MUST be a did:web identity per ACK-ID + core Section 3.1, and verifiers MUST resolve its keys per ACK-ID core + Section 3.2. This is the same `did.json` the x402 extension documents for + key discovery: one hosted document serves both protocols. ACK-ID core's + resolution rules (one fixed location, SSRF protections, freshness) + apply. +- **Algorithms.** EdDSA and ES256, per ACK-ID core Section 4. Whether ACK + verifiers also accept ES256K for wallet-adjacent deployments is an open + decision (Section 8). +- **Key separation.** The offer/receipt signing key MUST NOT be the `payTo` + account and SHOULD NOT be an owner grant-signing key. A dedicated + signing key bounds what a compromise can mint. +- **Offer expiry.** Offers MUST carry `validUntil`, and verifiers MUST + reject an offer past it. Sellers SHOULD keep offer validity short (the + x402 default of 300 seconds is a reasonable ceiling). + +## 4. Identity binding + +A receipt's `payer` is a settlement address, which moves money and names no +accountable party. The binding that makes a receipt an accountability +artifact: + +- When the paid request was ACK-verified (signed request plus grant, ACK-ID + core Sections 6-7), the seller SHOULD include in the receipt payload a + single profile-owned member, `ack`, carrying the agent's DID and the `jti` + of the grant presented: + + ```json + "ack": { "agent": "did:web:acme.com:shopper", "grantId": "grn_4kq8" } + ``` + +- The `ack` member rides inside the extension's JWS receipt payload and is + covered by its signature. It has no EIP-712 representation: that schema + is fixed-typed and cannot carry it, which is one more reason conformance + requires the JWS scheme (Section 3). A canonical bound-receipt fixture + ships with the test vectors the ACK-ID RFC tracks as a core deliverable. + Namespacing the binding under one member keeps + the profile's footprint in the upstream payload to a single name, so + upstream evolution cannot collide with it member-by-member. A verifier + that does not recognize it ignores it: the binding only ever adds + evidence, never authority, so it is exactly the kind of member that is + never named `crit` (ACK-ID core Section 5). +- A receipt without the binding is still a valid x402 receipt: it proves + payment and attributes it to no one. RPs that need the trail reject + unbound receipts as a policy choice. + +The resulting chain, each link independently verifiable: receipt names the +agent (`ack.agent`, signed by the seller); the grant named by +`ack.grantId` binds that agent to its owner (`iss`, signed by the owner); +the owner's legal identity is anchored per ACK-ID ext-controller (ownership +proofs). This is the full trail from a payment event to the legal entity +behind the paying agent, walkable by a third party with no callback to any +participant. + +## 5. Third-party verification + +A third party verifies a receipt (with its offer, when presented together) +as follows. Checks 1-4 restate the x402 extension's verification in ACK-ID +terms; check 5 is this profile's addition. + +1. **Keys**: resolve the signer's keys per ACK-ID core Section 3.2 from the + signer DID; thumbprint rules per ACK-ID core Section 4.1. +2. **Signatures**: verify the JWS on the receipt, and on the offer when + present. +3. **Matching**: receipt and offer agree on `resourceUrl` and `network`; the + offer's `validUntil` had not passed at `issuedAt`. +4. **Freshness**: `issuedAt` is sane for the claimed transaction; where + `txHash` is present, it MAY be checked against the named network. +5. **The trail**: when the `ack` member is present and the named grant is + presented alongside the receipt (presenters retain and supply it; + ext-audit's evidence bundles are the retention shape), verify the grant + per ACK-ID core Section 7 rules: signature against the owner's keys, + `sub` equals `ack.agent`, `jti` equals `ack.grantId`. Where + legal-entity assurance is required, verify the owner's anchors per + ext-controller. A receipt whose `ack` member arrives without the grant + attributes the payment but proves no authorization; RPs that need the + trail treat it as unbound. + +What a passing verification proves: the named seller quoted these terms, +acknowledged payment for this resource, and attributed the payment to this +agent under this owner's grant. The receipt carries no amount or `payTo`. +The offer's signature proves the terms. The seller's signed acknowledgment +attests their satisfaction, and `txHash` (check 4) is the on-chain +corroboration where present. It does not prove the resource was +delivered or fit for purpose, that settlement is final on any particular +rail, or that a human approved the payment. + +## 6. Other rails (design language) + +The offer/receipt pattern is rail-independent: an offer is a signed quote +before payment, a receipt a signed acknowledgment after, whatever moved the +money between them. Mappings for l402, card-network agent programs +(Mastercard Agent Pay and its peers), and bank transfers follow the same +shape and are non-normative until a deployment needs one written down. The +card networks are converging on the same primitives: Mastercard's +Verifiable Intent pilots carry signed, key-bound SD-JWT mandates, so a +mapping there is translation rather than invention. This section is the design-language boundary: flows, +oversight points, and rail mappings live in the ACK-Pay pattern docs and +never define conformance. + +## 7. Relationship to the existing ACK-Pay spec + +- The payment-request token and payment-service flows are superseded by + signed offers: the seller signs the quote at the 402, and no intermediary + mints it. +- VC receipts are superseded by this profile's receipts: plain JOSE, same + no-VC posture as ACK-ID core. +- The flow and role documents remain as the design language, marked + non-normative. +- `packages/ack-pay` gets recut against this profile after the RFC + decisions land, in the same sequence as the ACK-ID SDK. + +## 8. Open decisions + +1. **Upstreaming the binding member.** `ack` is a profile-defined member of + the extension's receipt payload; the single namespaced name confines + collision risk with upstream evolution, but upstreaming is still the + durable answer. Propose to the x402 extension either the `ack` member as + an extension point or native attribution fields, so non-ACK verifiers + learn to display the binding. This should be pursued now, while the + extension is young; the answer decides whether the member stays + namespaced or migrates to upstream-native fields. +2. **ES256K.** The x402 JWS scheme allows secp256k1. Accepting it widens + wallet-key reuse; rejecting it keeps ACK-ID core's two-algorithm + discipline. Current draft: reject, revisit on deployment evidence. +3. **Offer presentation.** Receipts verify without their offer; disputes + want both. Should the profile require buyers to retain offers + (evidence-bundle style, ACK-ID ext-audit) or leave retention to policy? From cc639609ba747f217780b97d16c2df6d2a46e1b6 Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Mon, 14 Sep 2026 12:49:20 -0400 Subject: [PATCH 02/13] docs(rfc): pin ACK-Pay to the x402 offer-receipt code The profile named @x402/extensions 2.22.0 next to commit 59ac597, but that commit ships 2.15.0, and the field names were copied from the extension's documentation page, which disagrees with types.ts. Pin the source at the commit (the last change to offer-receipt, unchanged through 2.25.0), make the code the source of truth, and use `scheme` and `transaction`. Co-Authored-By: Claude Fable 5.1 --- docs/ack-pay/rfc/core.md | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/docs/ack-pay/rfc/core.md b/docs/ack-pay/rfc/core.md index d73d31b7..08e66b91 100644 --- a/docs/ack-pay/rfc/core.md +++ b/docs/ack-pay/rfc/core.md @@ -11,10 +11,16 @@ can present to third parties, and a binding from that receipt to the ACK-ID grant that authorized the payment. It defines no wire protocol, no settlement mechanism, and no new artifact formats. The artifacts are the x402 [offer-receipt extension](https://docs.x402.org/extensions/offer-receipt), -adopted as published and profiled here. This profile pins the extension as -shipped in `@x402/extensions` 2.22.0 (x402 repo, offer-receipt source at -commit `59ac597`, 2026-06-17). Upstream changes flow into this profile only -by re-pinning here, never implicitly. Everything else ACK-Pay describes +adopted as published and profiled here. This profile pins the extension's +source as it stands at `x402-foundation/x402` commit `59ac597` +(2026-06-17), the last change to +`typescript/packages/extensions/src/offer-receipt`; that source first +shipped in `@x402/extensions` 2.15.0 and is unchanged through 2.25.0. The +code (`types.ts` at that commit) is the source of truth for field names. +The extension's documentation page disagrees with it in two places +(`offerType` for `scheme`, `txHash` for `transaction`); this profile follows +the code. Upstream changes flow into this profile only by re-pinning here, +never implicitly. Everything else ACK-Pay describes (roles, payment flows, human oversight, rails other than x402) is design language: non-normative patterns that define no conformance. @@ -39,10 +45,12 @@ and signed requests (Section 6) are used here without redefinition. The offer and receipt payloads, their placement (offers in the 402 response's `extensions` member; receipts in the `PAYMENT-RESPONSE` `extensions`), their -field sets (`resourceUrl`, `offerType`, `network`, `amount`, `payTo`, -`validUntil`; `resourceUrl`, `payer`, `network`, `issuedAt`, optional -`txHash`), and their signature encodings are as defined by the x402 -offer-receipt extension. This profile constrains them: +field sets (offer: `version`, `resourceUrl`, `scheme`, `network`, `asset`, +`payTo`, `amount`, `validUntil`; receipt: `version`, `resourceUrl`, `payer`, +`network`, `issuedAt`, optional `transaction`), and their signature +encodings are as defined by the x402 offer-receipt extension. This profile +constrains them and deviates from none of them; a future deviation is +stated in this section explicitly: - **Signature scheme.** ACK-Pay conformance requires the JWS scheme. An EIP-712/did:pkh signature MAY additionally be present; it carries no @@ -114,7 +122,7 @@ terms; check 5 is this profile's addition. 3. **Matching**: receipt and offer agree on `resourceUrl` and `network`; the offer's `validUntil` had not passed at `issuedAt`. 4. **Freshness**: `issuedAt` is sane for the claimed transaction; where - `txHash` is present, it MAY be checked against the named network. + `transaction` is present, it MAY be checked against the named network. 5. **The trail**: when the `ack` member is present and the named grant is presented alongside the receipt (presenters retain and supply it; ext-audit's evidence bundles are the retention shape), verify the grant @@ -129,7 +137,7 @@ What a passing verification proves: the named seller quoted these terms, acknowledged payment for this resource, and attributed the payment to this agent under this owner's grant. The receipt carries no amount or `payTo`. The offer's signature proves the terms. The seller's signed acknowledgment -attests their satisfaction, and `txHash` (check 4) is the on-chain +attests their satisfaction, and `transaction` (check 4) is the on-chain corroboration where present. It does not prove the resource was delivered or fit for purpose, that settlement is final on any particular rail, or that a human approved the payment. From fc61a718d6047fabe9c09cdbe077b610f370f7f7 Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Mon, 14 Sep 2026 12:49:20 -0400 Subject: [PATCH 03/13] docs(rfc): refuse redirects during key resolution by default Platform HTTP clients follow redirects without exposing the hops, so per-hop SSRF checks never run unless the fetcher walks the chain itself. Make refusal the default and following an explicit opt-in for fetchers that implement the checks, matching the SDK resolver (#133). Co-Authored-By: Claude Fable 5.1 --- docs/ack-id/rfc/README.md | 13 +++++++------ docs/ack-id/rfc/core.md | 19 ++++++++++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/docs/ack-id/rfc/README.md b/docs/ack-id/rfc/README.md index c8d17cb9..f49e681b 100644 --- a/docs/ack-id/rfc/README.md +++ b/docs/ack-id/rfc/README.md @@ -111,8 +111,10 @@ draft sections they extract. hosted document then serves ACK-ID, ACK-Pay receipts, and x402 offer-receipt verification. (Earlier drafts used separate `jwks.json` paths; that meant a second document for no added capability.) Fetches - follow redirects, with the SSRF checks applied to every hop: the fixed - path is where resolution starts, not a constraint on serving topology. + refuse redirects by default. A fetcher may opt in, and then it walks + every hop itself with the SSRF checks applied to each; platform HTTP + clients hide the hops, so following on their default would skip the + checks. The SDK resolver made the same change in #133. 3. **Control grants live in ext-controller.** Core verifiers pin owner keys at onboarding and never consume a control grant, so moving them into core would add surface with no core consumer. The counterargument @@ -158,10 +160,9 @@ draft sections they extract. list needed the consistency rule to be enforced correctly; delegation is local (a holder extends the chain knowing only its parent); and pairwise hashing is the AP2 mandate-chain convention, so grants slot into AP2/UCP - flows without redesign. Field-tested in the id.sh reference - implementation, whose panel regressions include the decoy-hash rejection. - Known limit, true of any hash-linked format: an ancestor can never be - inserted above an issued root without reissuing everything below it. + flows without redesign. Known limit, true of any hash-linked format: an + ancestor can never be inserted above an issued root without reissuing + everything below it. 8. **Unknown grant claims are ignored; `crit` marks the exceptions.** Earlier drafts closed the claim table: a core verifier rejected any top-level claim it did not recognize. PR #9 review argued that JWT diff --git a/docs/ack-id/rfc/core.md b/docs/ack-id/rfc/core.md index 5ce4089a..f5a2edd9 100644 --- a/docs/ack-id/rfc/core.md +++ b/docs/ack-id/rfc/core.md @@ -151,8 +151,13 @@ verification. Resolution rules: - All fetches MUST use HTTPS, MUST time out, and MUST cap response size. - Fetchers MAY follow redirects to a bounded depth (RECOMMENDED limit 3). - Every hop MUST use HTTPS and MUST pass the address checks below. + Fetchers MUST NOT follow redirects by default: a redirect response ends + resolution with failure. A fetcher MAY opt in to following redirects to a + bounded depth (RECOMMENDED limit 3) only if it handles each hop itself, so + that every hop uses HTTPS and passes the address checks below. Platform + HTTP clients (browser `fetch`, undici, Workers) follow redirects without + exposing the hops, so a fetcher on their default never sees the hop it + would have to check. - The identity URL is attacker-influenced until a signature verifies. Fetchers MUST NOT connect to loopback, private (RFC 1918), link-local, or unique-local addresses, on any hop. Deployments that face hostile @@ -454,11 +459,11 @@ There are no numbered conformance levels. therefore also give their artifacts a second rejection surface where ignoring a claim would widen authority: a distinct `typ`, a reserved scope token, or an `aud` shape core rejects. -- **Redirected key fetches.** Key resolution follows redirects (Section - 3.2), so whoever can set a redirect at the identity's origin chooses - where keys are read from. Publishers SHOULD serve `did.json` directly. - An RP MAY refuse redirected resolution where its policy needs the - stronger property. +- **Redirected key fetches.** Key resolution refuses redirects by default + (Section 3.2). A fetcher that opts in accepts that whoever can set a + redirect at the identity's origin chooses where keys are read from, and + it takes on checking every hop itself. Publishers SHOULD serve `did.json` + directly at the fixed path. - **Shared key sets.** A key set shared across identities (a root identity's `/.well-known/did.json` serving a whole org) weakens what a request signature proves. A passing signature proves possession of some From 2000280241e0d4a8b93407049d55589f2b844a7f Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Mon, 14 Sep 2026 12:49:20 -0400 Subject: [PATCH 04/13] docs(rfc): add carriage, chain subjects, and status to ext-attestations Three drafting requirements from review: a request field for attestations since Grant rejects non-grant typs, attestation subjects that name a verified chain ancestor so did:key leaves can inherit them, and the SD-JWT VC status claim as the revocation lever. Co-Authored-By: Claude Fable 5.1 --- docs/ack-id/rfc/ext-attestations.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/ack-id/rfc/ext-attestations.md b/docs/ack-id/rfc/ext-attestations.md index 23dc5d76..6028531e 100644 --- a/docs/ack-id/rfc/ext-attestations.md +++ b/docs/ack-id/rfc/ext-attestations.md @@ -27,6 +27,29 @@ results), distinct from grants: an attestation asserts, a grant authorizes. never the session. Whether absent evidence downgrades or rejects is RP policy, declared in advance. Exact binding and failure semantics are a drafting requirement for this extension's normative text. +- **Request carriage.** The `Grant` field carries grants only: core + Section 7 rejects any token in it whose `typ` is not `grant+jwt`, so an + attestation has no slot in a signed request, and an RP that wants one at + decision time would have to fetch it out of band. Drafting requirement: + a request field for attestations with the same RFC 9651 List-of-Tokens + grammar as `Grant`, covered by the request signature under the same + coverage rule, each token evaluated independently under this extension. + Core-only verifiers ignore the field. It carries evidence, never + authority, so it needs no `crit` entry and opens no downgrade. +- **Subjects in delegation chains.** A behavioral attestation (a rating + built from decision and settlement history) can never attach to an + ephemeral did:key leaf (ext-delegation): the leaf has no history and is + gone before it earns any. Drafting requirement: an attestation MAY name + as `sub` any verified ancestor in the presented chain, typically the + durable agent or owner behind the leaf. A verifier that has walked the + chain accepts an attestation whose `sub` equals an ancestor's `sub`, with + `cnf` compared against that ancestor's key rather than the leaf's. +- **Revocation.** An attestation is the artifact most likely to need + pulling back inside its lifetime (a rating downgraded after an incident, + a compliance status withdrawn). SD-JWT VC defines a `status` claim over + the IETF Token Status List, the same list ext-revocation profiles for + grants. A verifier implementing this extension MUST check `status` when + present, mirroring ext-revocation's obligation. ## VC bridge (non-normative) From 0bd473f0bc7aa3b1fc3892e114cce6d9a0107d89 Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Mon, 14 Sep 2026 12:49:20 -0400 Subject: [PATCH 05/13] docs(rfc): state the did:key signed-request shape in ext-delegation Replace the open item with the intended shape: Signature-Agent absent, identity from the leaf sub, key decoded from the identifier, keyid equal to its thumbprint and to cnf.jkt. Co-Authored-By: Claude Fable 5.1 --- docs/ack-id/rfc/ext-delegation.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/ack-id/rfc/ext-delegation.md b/docs/ack-id/rfc/ext-delegation.md index 619c3b56..2994aa7f 100644 --- a/docs/ack-id/rfc/ext-delegation.md +++ b/docs/ack-id/rfc/ext-delegation.md @@ -58,11 +58,15 @@ did:key needs no hosted document at all. anchor ownership. It exists to be the short-lived `sub` of a chained leaf (owner signs the intermediate; an orchestrator mints a leaf whose subject is the worker's did:key), and the worker dies with its grant. Encodes an - Ed25519 or P-256 key (core Section 4). Open item: signed-request carriage. - `Signature-Agent` requires a URL (core Section 6) and a did:key has none, - so the presentation profile for did:key subjects (key resolution directly - from the identifier, `Signature-Agent` absent or repurposed) needs - definition here. + Ed25519 or P-256 key (core Section 4). Signed-request carriage for did:key + subjects, the intended shape (normative text is a drafting requirement): + `Signature-Agent` is absent, because Web Bot Auth requires a URL there + (core Section 6) and a did:key has none. The agent's identity is the + leaf's `sub`. The verifier decodes the public key from that identifier, + so core Section 7 step 1 runs with no fetch, and `keyid` MUST equal the + thumbprint of the decoded key, which MUST equal the leaf's `cnf.jkt`. The + `sub` check in step 2 compares against the leaf's own `sub`; possession + of the pinned key is what ties the request to it. - **Registration grants.** `scope: "register"` (reserved in core): single-use, key-pinned, one-hour authority to create a named identity at a hosted directory, with namespace-derivation checks so an owner's signature From 28afe7f29503f54fa80858751b58423440102564 Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Mon, 14 Sep 2026 12:49:46 -0400 Subject: [PATCH 06/13] docs(rfc): restrict identity path segments to the did:web alphabet A colon inside a path segment made the URL-to-DID mapping lossy: https://acme.com/a:b and https://acme.com/a/b both spelled did:web:acme.com:a:b. Reject any segment character outside the did:web idchar set (percent-encoding is already rejected), which closes that hole and any other separator collision. Co-Authored-By: Claude Fable 5.1 --- docs/ack-id/rfc/core.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/ack-id/rfc/core.md b/docs/ack-id/rfc/core.md index f5a2edd9..d7cc4239 100644 --- a/docs/ack-id/rfc/core.md +++ b/docs/ack-id/rfc/core.md @@ -113,9 +113,15 @@ layouts should read that document's Section 2. Before a verifier uses an identity for comparison or URL construction, it MUST reject the identity if its URL form contains any of: a userinfo component, a port, a query, a fragment, a trailing slash, a dot-segment -(`.` or `..`), an empty path segment, percent-encoding, or `.well-known` -as its first path segment. The `.well-known` rule closes an aliasing hole. -The path identity `https://acme.com/.well-known` would publish its keys at +(`.` or `..`), an empty path segment, percent-encoding, a path segment +with any character outside `A-Z`, `a-z`, `0-9`, `.`, `-`, and `_`, or +`.well-known` as its first path segment. The segment alphabet is the +did:web `idchar` set with percent-encoding removed, and it is what keeps +the mapping bijective: `:` is the DID spelling's segment separator, so +without the rule `https://acme.com/a:b` and `https://acme.com/a/b` would +both spell `did:web:acme.com:a:b`, and one key document would answer for +two identities. The `.well-known` rule closes an aliasing hole of the same +kind. The path identity `https://acme.com/.well-known` would publish its keys at `https://acme.com/.well-known/did.json`. That is the same location that serves the root identity `https://acme.com`, so one document would answer for two identities. To compare two identities, a verifier MUST map both to From 0a95426f4303bb3d118b0699bcb358658d476099 Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Mon, 14 Sep 2026 12:49:46 -0400 Subject: [PATCH 07/13] docs(rfc): reject ES256K in the ACK-Pay profile Close open decision 2 as reject. WebCrypto has no secp256k1, so an ES256K receipt would not verify with native crypto on browser and edge runtimes, and key separation already forbids the signing key being payTo, so accepting it would buy no wallet-key reuse. Co-Authored-By: Claude Fable 5.1 --- docs/ack-pay/rfc/core.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/ack-pay/rfc/core.md b/docs/ack-pay/rfc/core.md index 08e66b91..415006c6 100644 --- a/docs/ack-pay/rfc/core.md +++ b/docs/ack-pay/rfc/core.md @@ -61,9 +61,13 @@ stated in this section explicitly: key discovery: one hosted document serves both protocols. ACK-ID core's resolution rules (one fixed location, SSRF protections, freshness) apply. -- **Algorithms.** EdDSA and ES256, per ACK-ID core Section 4. Whether ACK - verifiers also accept ES256K for wallet-adjacent deployments is an open - decision (Section 8). +- **Algorithms.** EdDSA and ES256, per ACK-ID core Section 4. The x402 JWS + scheme also allows ES256K; this profile rejects it. WebCrypto has no + secp256k1, so an ES256K receipt would not verify with native crypto on + browser and edge runtimes. The key-separation rule below already forbids + the signing key being the `payTo` account, so accepting ES256K would buy + no wallet-key reuse anyway. The cost, stated openly: an EVM seller + provisions one non-wallet signing key. - **Key separation.** The offer/receipt signing key MUST NOT be the `payTo` account and SHOULD NOT be an owner grant-signing key. A dedicated signing key bounds what a compromise can mint. @@ -177,9 +181,6 @@ never define conformance. learn to display the binding. This should be pursued now, while the extension is young; the answer decides whether the member stays namespaced or migrates to upstream-native fields. -2. **ES256K.** The x402 JWS scheme allows secp256k1. Accepting it widens - wallet-key reuse; rejecting it keeps ACK-ID core's two-algorithm - discipline. Current draft: reject, revisit on deployment evidence. -3. **Offer presentation.** Receipts verify without their offer; disputes +2. **Offer presentation.** Receipts verify without their offer; disputes want both. Should the profile require buyers to retain offers (evidence-bundle style, ACK-ID ext-audit) or leave retention to policy? From 3c06bdf2ec620968e49519decd8fe44de0f5ce13 Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Mon, 14 Sep 2026 12:49:46 -0400 Subject: [PATCH 08/13] docs(rfc): record did:jwks as superseded by ext-web The SDK resolver's jwks.json-then-OIDC fallback is the chain core forbids. Name the disposition in the settled decisions so the RFC answers what happens to the method. Co-Authored-By: Claude Fable 5.1 --- docs/ack-id/rfc/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/ack-id/rfc/README.md b/docs/ack-id/rfc/README.md index f49e681b..24564767 100644 --- a/docs/ack-id/rfc/README.md +++ b/docs/ack-id/rfc/README.md @@ -183,3 +183,12 @@ draft sections they extract. reserved; ext-delegation's attenuation wildcards apply to parent-to-child containment inside chains, which core-only verifiers never accept anyway. +10. **did:jwks is superseded by ext-web's discovery profile.** The SDK's + `packages/did` resolves `did:jwks` through `jwks-did-resolver`, whose + algorithm tries `/.well-known/jwks.json` and then falls back to OIDC + discovery. That is the fallback chain core forbids: a miss at one + location must never widen the key set (core Section 3.2). The need it + served, keys that live behind an OIDC `jwks_uri`, is met by ext-web's + opt-in profile, which a verifier configures per identity and which no + 404 ever triggers. The method is not carried into v2; the resolver + retires when the packages are recut against this RFC. From 264f72affbcea534ea7ef91704661589f1ccee53 Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Mon, 14 Sep 2026 12:50:08 -0400 Subject: [PATCH 09/13] docs(rfc): recompute content-digest and bound created in signed requests Covering content-digest binds the header value only; a verifier must recompute the digest over the received body or a middlebox can swap the body under a valid signature. created had a maximum age but no future bound; apply the Section 5 skew, the same bound iat carries. Co-Authored-By: Claude Fable 5.1 --- docs/ack-id/rfc/core.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/ack-id/rfc/core.md b/docs/ack-id/rfc/core.md index d7cc4239..1d19c066 100644 --- a/docs/ack-id/rfc/core.md +++ b/docs/ack-id/rfc/core.md @@ -296,7 +296,10 @@ is defined by this document. - The signature MUST cover `@method`, `@target-uri`, `content-digest` when a body is present, the `Signature-Agent` field, and the `Grant` field with - the `sf` parameter when grants are presented. + the `sf` parameter when grants are presented. Coverage binds the + `content-digest` header value only. A verifier MUST recompute the digest + over the body it received and MUST reject the request on mismatch; + without that step a middlebox can swap the body under a valid signature. - `Signature-Agent` carries the agent's identity URL (the URL spelling of its DID, Section 3.1; Web Bot Auth requires a URL here). It is an untrusted hint until the signature verifies. Because it is under the @@ -305,7 +308,9 @@ is defined by this document. used) bound replay. Verifiers MUST enforce a maximum `created` age (RECOMMENDED default 300 seconds); the maximum is the RP's replay-exposure ceiling, a policy choice like the Section 8 lifetime - maximum. When `expires` is present, it MUST be covered by the signature, + maximum. Verifiers MUST also reject a `created` in the future beyond the + Section 5 `skew`, the same bound `iat` carries. When `expires` is + present, it MUST be covered by the signature, and a verifier MUST reject a request past it (the Section 5 `skew` applies). `expires` only ever narrows the window: an `expires` beyond the maximum `created` age does not extend acceptance. A captured request From 66e32b0d0b416b746cf7af7d68364c539536e45c Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Mon, 14 Sep 2026 12:50:08 -0400 Subject: [PATCH 10/13] docs(rfc): state the fixed-price limit and the receipt recovery gap The receipt carries no amount, so the profile covers fixed-price flows only; say so in Section 5 and fold a settled-amount field into open decision 1 so attribution and outcome upstream together. Fold buyer side receipt recovery (pay, lose the 200, hold no proof) into open decision 2, since idempotent re-issue needs a request surface the profile does not define. Co-Authored-By: Claude Fable 5.1 --- docs/ack-pay/rfc/core.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/docs/ack-pay/rfc/core.md b/docs/ack-pay/rfc/core.md index 415006c6..1f66093c 100644 --- a/docs/ack-pay/rfc/core.md +++ b/docs/ack-pay/rfc/core.md @@ -142,7 +142,11 @@ acknowledged payment for this resource, and attributed the payment to this agent under this owner's grant. The receipt carries no amount or `payTo`. The offer's signature proves the terms. The seller's signed acknowledgment attests their satisfaction, and `transaction` (check 4) is the on-chain -corroboration where present. It does not prove the resource was +corroboration where present. The profile therefore covers fixed-price +flows: the receipt acknowledges payment of exactly the offered terms. +Where the settled amount can differ from the offered one (x402's `upto` +scheme, metered and session flows), the receipt has no slot for what was +settled; open decision 1 tracks it. It does not prove the resource was delivered or fit for purpose, that settlement is final on any particular rail, or that a human approved the payment. @@ -178,9 +182,17 @@ never define conformance. collision risk with upstream evolution, but upstreaming is still the durable answer. Propose to the x402 extension either the `ack` member as an extension point or native attribution fields, so non-ACK verifiers - learn to display the binding. This should be pursued now, while the - extension is young; the answer decides whether the member stays - namespaced or migrates to upstream-native fields. -2. **Offer presentation.** Receipts verify without their offer; disputes - want both. Should the profile require buyers to retain offers - (evidence-bundle style, ACK-ID ext-audit) or leave retention to policy? + learn to display the binding. Propose a settled-amount field alongside + it, so attribution and outcome land upstream together and `upto` and + metered receipts can state what was paid (Section 5). This should be + pursued now, while the extension is young; the answer decides whether + the member stays namespaced or migrates to upstream-native fields. +2. **Offer presentation and receipt recovery.** Receipts verify without + their offer; disputes want both. Should the profile require buyers to + retain offers (evidence-bundle style, ACK-ID ext-audit) or leave + retention to policy? The buyer side has a mirror gap: an agent that + pays and loses the `200` before storing it holds no proof, and + re-issuance is undefined here and upstream. A seller that re-issues + idempotently for an already-settled payment (same content, `issuedAt` + preserved, keyed by a stable settlement reference) would close it, but + that needs a request surface this profile does not define. From a6f6214de10ef0666ad7d97b0bfefb71673aa250 Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Mon, 14 Sep 2026 12:50:29 -0400 Subject: [PATCH 11/13] docs(rfc): keep redemption records through exp plus skew Acceptance runs to exp + skew but the single-use record was dropped at exp, leaving a skew-sized window in which a spent grant passes again. Retain through exp + skew in core Section 8 and for registration grants in ext-delegation. Co-Authored-By: Claude Fable 5.1 --- docs/ack-id/rfc/core.md | 4 +++- docs/ack-id/rfc/ext-delegation.md | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/ack-id/rfc/core.md b/docs/ack-id/rfc/core.md index 1d19c066..9e1f4811 100644 --- a/docs/ack-id/rfc/core.md +++ b/docs/ack-id/rfc/core.md @@ -387,7 +387,9 @@ ext-revocation. authorization, a one-shot registration); core supplies the mechanism. Redemption MUST be atomic: a check-and-set keyed by (`iss`, `jti`). A separate read-then-write lets two concurrent presentations both pass. - The RP keeps the record until the grant's `exp` has passed. + The RP keeps the record until `exp + skew` has passed: acceptance runs + to `exp + skew` (Section 5), so a record dropped at `exp` leaves a + skew-sized window in which a spent grant passes again. - **Key removal.** Removing an agent's keys from its DID document invalidates everything the agent signs within the cache bound (Section 3.2). diff --git a/docs/ack-id/rfc/ext-delegation.md b/docs/ack-id/rfc/ext-delegation.md index 2994aa7f..35e29a1c 100644 --- a/docs/ack-id/rfc/ext-delegation.md +++ b/docs/ack-id/rfc/ext-delegation.md @@ -76,7 +76,7 @@ did:key needs no hosted document at all. is byte-exact against the owner's identity after core Section 3.1 validation (which already rejects names that would need escaping). Redemption at the directory is an atomic check-and-set keyed by - (`iss`, `jti`), retained through `exp`. + (`iss`, `jti`), retained through `exp + skew` (core Section 8). ## Registers From 9ade08083bd224845f93355087b2b4996631cb72 Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Mon, 14 Sep 2026 12:50:29 -0400 Subject: [PATCH 12/13] docs(rfc): add rail-neutral design-language rules to ACK-Pay Name the receipt binding as a standalone attribution shape (agent, grant reference, settlement reference) so other rail mappings carry the same members; state that translation into rail-native authority never widens a grant; and say the profile's one-grant, one-request, one-receipt, one-settlement shape is not a permanent relationship. Co-Authored-By: Claude Fable 5.1 --- docs/ack-pay/rfc/core.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/ack-pay/rfc/core.md b/docs/ack-pay/rfc/core.md index 1f66093c..dc894baf 100644 --- a/docs/ack-pay/rfc/core.md +++ b/docs/ack-pay/rfc/core.md @@ -24,6 +24,11 @@ never implicitly. Everything else ACK-Pay describes (roles, payment flows, human oversight, rails other than x402) is design language: non-normative patterns that define no conformance. +The profile describes one shape: one grant, one request, one receipt, one +settlement. It does not fix that relationship. Subscriptions, sessions, +`upto`, and batch settlement may relate the four differently; they are +tracked as follow-ups, and nothing here rules them out. + This document layers on ACK-ID core: identities and key resolution (ACK-ID Sections 3.1-3.2), keys and thumbprints (Section 4), grants (Section 5), and signed requests (Section 6) are used here without redefinition. @@ -163,6 +168,19 @@ mapping there is translation rather than invention. This section is the design-l oversight points, and rail mappings live in the ACK-Pay pattern docs and never define conformance. +The binding Section 4 defines is rail-independent in shape too. Named on +its own it is an attribution: the agent's DID, the artifact reference of +the authorizing grant, and the rail's settlement reference, signed by +whoever issues that rail's receipt. The x402 profile is the case where the +attribution embeds in the receipt payload as `ack`, with the receipt's own +`transaction` member as the settlement reference. A mapping for another +rail carries the same members, so a verifier that walks the trail consumes +one shape whatever moved the money. + +One rule survives every translation: converting a grant into a wallet's or +a rail's native authority never widens it. A grant constraint the rail +cannot enforce refuses the payment; it is never dropped. + ## 7. Relationship to the existing ACK-Pay spec - The payment-request token and payment-service flows are superseded by From 76d5ff5038d4f5b88039d6b0f191305bd75f4c6f Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Mon, 14 Sep 2026 12:50:58 -0400 Subject: [PATCH 13/13] docs(rfc): define artifact references once and bind ack by content jti is unique per issuer only and the ack member names no issuer, so a second owner could mint a grant with the same sub and jti and walk the trail to the wrong party. Define the artifact reference (base64url SHA-256 over the compact serialization) once in core Section 4.2, have chain reuse it, and replace ack.grantId with ack.grant carrying the reference of the presented grant. Co-Authored-By: Claude Fable 5.1 --- docs/ack-id/rfc/core.md | 11 +++++++++++ docs/ack-id/rfc/ext-delegation.md | 6 +++--- docs/ack-pay/rfc/core.md | 24 +++++++++++++++--------- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/docs/ack-id/rfc/core.md b/docs/ack-id/rfc/core.md index 9e1f4811..6038b90f 100644 --- a/docs/ack-id/rfc/core.md +++ b/docs/ack-id/rfc/core.md @@ -205,6 +205,17 @@ or fragment is a label, never the selector, so a published entry cannot claim another key's name. Exactly one entry may match. Zero matches, or more than one, is a rejection. +### 4.2 Artifact references + +An artifact that refers to another artifact commits to it by content. The +reference is the base64url encoding (no padding) of the SHA-256 digest +over the ASCII bytes of the referenced artifact's compact serialization. +This is the one reference construction ACK documents use (ext-delegation's +`chain`, ACK-Pay's `ack.grant`), so one test vector covers every link in +a trail. A reference names exactly one set of bytes and nothing else: it +carries no issuer, so a verifier that needs the referenced artifact's +meaning obtains the artifact itself and checks that its reference matches. + ## 5. Grants A grant is a JWT: header `alg` per Section 4, `typ: "grant+jwt"`, `kid` the diff --git a/docs/ack-id/rfc/ext-delegation.md b/docs/ack-id/rfc/ext-delegation.md index 35e29a1c..7622fdbe 100644 --- a/docs/ack-id/rfc/ext-delegation.md +++ b/docs/ack-id/rfc/ext-delegation.md @@ -11,9 +11,9 @@ did:key needs no hosted document at all. ## Adds -- **The `chain` claim** (reserved in core): exactly one entry, the SHA-256 - hash (over the ASCII bytes of the compact serialization) of the immediate - parent. Each parent carries its own `chain`, so the ancestry is a walked +- **The `chain` claim** (reserved in core): exactly one entry, the artifact + reference (core Section 4.2) of the immediate parent. Each parent carries + its own `chain`, so the ancestry is a walked linked list, and the full ancestry is pinned transitively: a parent's hash covers the parent's own `chain` claim. Presenters carry every ancestor, and every chained artifact names `chain` in `crit` (core Section 5), so diff --git a/docs/ack-pay/rfc/core.md b/docs/ack-pay/rfc/core.md index dc894baf..1a16b593 100644 --- a/docs/ack-pay/rfc/core.md +++ b/docs/ack-pay/rfc/core.md @@ -88,13 +88,18 @@ artifact: - When the paid request was ACK-verified (signed request plus grant, ACK-ID core Sections 6-7), the seller SHOULD include in the receipt payload a - single profile-owned member, `ack`, carrying the agent's DID and the `jti` - of the grant presented: + single profile-owned member, `ack`, carrying the agent's DID and the + artifact reference (ACK-ID core Section 4.2) of the grant presented: ```json - "ack": { "agent": "did:web:acme.com:shopper", "grantId": "grn_4kq8" } + "ack": { "agent": "did:web:acme.com:shopper", "grant": "kQ3v8Zt..." } ``` + The reference binds the grant by content. A `jti` would not: `jti` is + unique per issuer only, and `ack` names no issuer, so a second owner + could mint a grant with the same `sub` and `jti` and the trail would walk + to the wrong party. The reference names the exact bytes the seller saw. + - The `ack` member rides inside the extension's JWS receipt payload and is covered by its signature. It has no EIP-712 representation: that schema is fixed-typed and cannot carry it, which is one more reason conformance @@ -111,8 +116,8 @@ artifact: unbound receipts as a policy choice. The resulting chain, each link independently verifiable: receipt names the -agent (`ack.agent`, signed by the seller); the grant named by -`ack.grantId` binds that agent to its owner (`iss`, signed by the owner); +agent (`ack.agent`, signed by the seller); the grant referenced by +`ack.grant` binds that agent to its owner (`iss`, signed by the owner); the owner's legal identity is anchored per ACK-ID ext-controller (ownership proofs). This is the full trail from a payment event to the legal entity behind the paying agent, walkable by a third party with no callback to any @@ -132,11 +137,12 @@ terms; check 5 is this profile's addition. offer's `validUntil` had not passed at `issuedAt`. 4. **Freshness**: `issuedAt` is sane for the claimed transaction; where `transaction` is present, it MAY be checked against the named network. -5. **The trail**: when the `ack` member is present and the named grant is - presented alongside the receipt (presenters retain and supply it; - ext-audit's evidence bundles are the retention shape), verify the grant +5. **The trail**: when the `ack` member is present and the referenced grant + is presented alongside the receipt (presenters retain and supply it; + ext-audit's evidence bundles are the retention shape), check that the + grant's artifact reference equals `ack.grant`, then verify the grant per ACK-ID core Section 7 rules: signature against the owner's keys, - `sub` equals `ack.agent`, `jti` equals `ack.grantId`. Where + `sub` equals `ack.agent`. Where legal-entity assurance is required, verify the owner's anchors per ext-controller. A receipt whose `ack` member arrives without the grant attributes the payment but proves no authorization; RPs that need the