diff --git a/.agent/plans/2026-07-14-aauth-events/implementation-log.md b/.agent/plans/2026-07-14-aauth-events/implementation-log.md new file mode 100644 index 0000000..f0f7708 --- /dev/null +++ b/.agent/plans/2026-07-14-aauth-events/implementation-log.md @@ -0,0 +1,564 @@ +# AAuth Events - Implementation Log + +Append-only record for decisions, deviations, and open questions made while +implementing the approved AAuth Events research and plan. + +## Decisions Taken + +### [2026-07-15] [Phase 0] Owner implementation approval - RESOLVED + +The owner approved implementation on branch +`feat/aauth-events-implementation`. Child-branch commits are allowed after this +approval; integration remains subject to the phase gates and owner review. + +### [2026-07-15] [Phase 0] C1 role coverage - RESOLVED + +The package covers AP issuance/inbox, resource registration/delivery, and agent +verification. Standard AP-to-agent transport remains out of scope. + +### [2026-07-15] [Phase 0] C2 package dependency - RESOLVED + +`AAuth.Events` depends only on `AAuth`. It does not depend on or modify +`AAuth.R3`; R3 is referenced only by a metadata-composition test and Bookings. + +### [2026-07-15] [Phase 0] C3 agent deduplication - RESOLVED + +Agent deduplication uses SHA-256 of the exact compact event token instead of the +draft's `{iss,eid}` key, which would discard later events on one subscription. + +### [2026-07-15] [Phase 0] C4 event body shape - RESOLVED + +The wire body is the direct AsyncAPI-defined JSON payload with +`application/json`, matching the delivery example rather than a wrapper or JWT +body. + +### [2026-07-15] [Phase 0] C5 covered components - RESOLVED + +Bodyless requests cover the four base components. Registration JSON also +covers `content-type`; event JSON covers `content-type` and `content-digest`. + +### [2026-07-15] [Phase 0] C6 AP durability - RESOLVED + +Production AP hosts must provide a durable store implementation. The package +does not register an in-memory production default. + +### [2026-07-15] [Phase 0] C7 sample topology - RESOLVED + +The runnable sample extends Bookings and MockAgentProvider and adds a focused +EventAgent console application. + +### [2026-07-15] [Phase 0] C8 subscription lifetime - RESOLVED + +Subscription lifetime is application policy represented by stored `ExpiresAt`. +No non-standard lifetime wire field is added. + +### [2026-07-15] [Phase 0] C9 package versioning - RESOLVED + +`AAuth.Events` tracks the `AAuth` version and is packed by the same release +workflow. + +### [2026-07-15] [Phase 0] C10 EventAgent sample - RESOLVED + +The focused event flow is implemented in `samples/EventAgent`; GuidedTour is +not widened. + +### [2026-07-15] [Phase 0] C11 registration API layers - RESOLVED + +Resource registration exposes both a low-level typed verifier and an +opinionated ASP.NET endpoint mapper with an application policy callback. + +### [2026-07-15] [Phase 0] C12 registration response mapping - RESOLVED + +The default mapper uses 200 success, 400 malformed, 401 signature/JWT failure, +403 audience or agent-ticket mismatch, 404 unknown/expired ticket, and 409 +duplicate `eid` or reused ticket. + +### [2026-07-15] [Phase 0] C13 AP failure mapping - RESOLVED + +Expired or invalid event tokens map to 401; wrong event-token audience maps to +403. + +### [2026-07-15] [Phase 0] C14 AP retry idempotency - RESOLVED + +SHA-256 of the exact compact event token is the AP idempotency key. Exact +retries return the prior 202 outcome without another inbox write or use. + +### [2026-07-15] [Phase 0] C15 outbound URL policy - RESOLVED + +Events network calls require HTTPS except loopback HTTP, disable redirects, +reject private/link-local IP literals except loopback, and invoke a pluggable +trust policy. + +### [2026-07-15] [Phase 0] C16 event ID generation - RESOLVED + +Generated `eid` values contain at least 128 CSPRNG bits, are base64url encoded, +and are never reused by the AP. + +### [2026-07-15] [Phase 0] C17 body limit - RESOLVED + +Events endpoints buffer at most 1 MiB by default before verification. Hosts can +configure a different limit. + +### [2026-07-15] [Phase 0] C18 AsyncAPI scope - RESOLVED + +The first package supplies AAuth vocabulary/security constants, metadata +composition, and AAuth declaration validation. Applications own complete +AsyncAPI documents and schemas. + +### [2026-07-15] [Phase 0] C19 sample AP-to-agent transport - RESOLVED + +The sample uses agent-authenticated polling and explicitly labels it +non-normative. + +### [2026-07-15] [Phase 0] C20 event issued-at validation - RESOLVED + +Event `iat` is required and future-issued events are rejected using configured +clock skew at AP and agent verification. + +### [2026-07-15] [Phase 0] C21 signing algorithms - RESOLVED + +Events supports EdDSA and ES256 through `IAAuthKey`, emits the supplied key's +algorithm, and rejects `none` and unsupported algorithms. + +### [2026-07-15] [Phase 0] C22 cross-origin event endpoint - RESOLVED + +An AP `event_endpoint` may be on another HTTPS origin after the configured URL +trust policy accepts it. + +### [2026-07-15] [Phase 0] C23 event token identity - RESOLVED + +Every event token includes a required fresh random `jti` so legitimate +same-time events cannot collapse into the compact-token retry key. This is a +deliberate draft extension. + +### [2026-07-15] [Phase 0] RF1 event-token none asymmetry - RESOLVED + +The event-header omission does not permit unsigned tokens. Both token types +reject `none`. + +### [2026-07-15] [Phase 0] RF2 agent payload trust - RESOLVED + +Agent-facing APIs expose payloads only as unauthenticated data. Consequential +details are re-fetched through the application's normal AAuth client; no generic +Events re-fetch API is added. + +### [2026-07-15] [Phase 0] RF3 registration body integrity - RESOLVED + +The draft registration profile is retained for interoperability. Body data is +named and documented as signature-unbound and cannot widen the mapper's +configured channel authorization. + +### [2026-07-15] [Phase 0] RF4 AsyncAPI operation direction - RESOLVED + +Samples follow the draft's `action: receive`. The validator ignores operation +direction rather than treating `send` or `receive` as an AAuth validity rule. + +### [2026-07-15] [Phase 0] RF5 vocabulary composition - RESOLVED + +OpenAPI and AsyncAPI entries are composed in one caller-owned map before a +single `r3_vocabularies` assignment. Existing entries are preserved and +conflicts fail. + +### [2026-07-15] [Phase 0] D1 package deliverable - RESOLVED + +Ship one preview `AAuth.Events` package for all protocol-defined roles, excluding +standard AP-to-agent transport. + +### [2026-07-15] [Phase 0] D2 production dependency boundary - RESOLVED + +The production Events project references only `AAuth`; a test-only R3 reference +is permitted for RF5. + +### [2026-07-15] [Phase 0] D3 idempotency surface - RESOLVED + +Compact-token SHA-256 is the default key and agent deduplication remains +pluggable. + +### [2026-07-15] [Phase 0] D4 payload API - RESOLVED + +Direct JSON bytes are preserved and exposed to agents only through +`UnauthenticatedEventPayload`. + +### [2026-07-15] [Phase 0] D5 exact HTTP profiles - RESOLVED + +Standardized Events requests reject unexpected covered components. +Registration body authorization is explicitly out of scope. + +### [2026-07-15] [Phase 0] D6 store requirement - RESOLVED + +AP DI fails when no application store is registered; in-memory implementations +are test/sample-only. + +### [2026-07-15] [Phase 0] D7 stored expiry - RESOLVED + +The SDK models application-supplied `ExpiresAt` without adding a wire claim. + +### [2026-07-15] [Phase 0] D8 random eid format - RESOLVED + +Generated event IDs use at least 128 random bits and base64url encoding. + +### [2026-07-15] [Phase 0] D9 registration defaults - RESOLVED + +The endpoint mapper applies the C12 status defaults while low-level users retain +typed failures. + +### [2026-07-15] [Phase 0] D10 AP delivery defaults - RESOLVED + +AP delivery adds the C13 mappings and exact-token idempotent 202 behavior. + +### [2026-07-15] [Phase 0] D11 Events network transport - RESOLVED + +All Events metadata, JWKS, and delivery calls use an Events-owned no-redirect +client and URL policy. + +### [2026-07-15] [Phase 0] D12 bounded buffering - RESOLVED + +The default body limit is 1 MiB and configurable. + +### [2026-07-15] [Phase 0] D13 algorithms and time - RESOLVED + +Both token types support EdDSA/ES256, reject `none`, and require valid `iat`. + +### [2026-07-15] [Phase 0] D14 AsyncAPI integration boundary - RESOLVED + +The validator checks AAuth declarations only and does not validate operation +direction. + +### [2026-07-15] [Phase 0] D15 sample scope - RESOLVED + +The sample extends Bookings and MockAgentProvider, adds EventAgent, and uses +authenticated non-normative polling. + +### [2026-07-15] [Phase 0] D16 one vocabulary map - RESOLVED + +AsyncAPI is added to one completed vocabulary map before metadata assignment, +with no production R3 dependency. + +### [2026-07-15] [Phase 0] D17 required event jti - RESOLVED + +Every event token requires a fresh random `jti`; missing or empty values fail. + +### [2026-07-15] [Phase 0] Execution model - RESOLVED + +Implementation uses balanced parallel waves, isolated worktrees, disjoint file +ownership, child-branch commits, and non-fast-forward coordinator merges. + +### [2026-07-15] [Phase 0] Payload re-fetch API - RESOLVED + +Payload re-fetch remains application-owned and documentation-driven; the SDK +adds no generic resource re-fetch abstraction. + +### [2026-07-15] [Phase 0] Sample polling acknowledgment - RESOLVED + +Signed polling returns a non-destructive batch. A separate signed ACK removes +each processed event after verification and display. + +### [2026-07-15] [Phase 0] Resource subscription factory - RESOLVED + +`ResourceSubscription.FromRegistration(...)` maps verified registration facts +while the application supplies `ExpiresAt`. + +### [2026-07-15] [Phase 0] Protected waitlist ticket endpoint - RESOLVED + +Bookings adds `POST /waitlist/request`, protected by the existing +`searchAvailability` grant, to issue the sample ticket URL. + +### [2026-07-15] [Phase 0] Pre-change validation baseline - RESOLVED + +`dotnet test AAuth.slnx` passed before implementation changes: AAuth.Tests 517, +AAuth.Conformance 573, and AAuth.R3.Tests 39, with no failures or skips. + +### [2026-07-15] [Phase 1] Package and token foundation - RESOLVED + +Added the `AAuth.Events` preview project and tests, package-local compact JWS +writer, complete Events constants, strict subscribe/event builders and claim +readers, required agent confirmation-key binding, AgentId validation, EdDSA and +ES256 support, 128-bit random `eid`/`jti`, and the required event `jti` +extension. Seventeen token tests pass, the full solution builds, and the +production assets contain no `AAuth.R3` dependency. + +### [2026-07-15] [Phase 2] Events HTTP security layer - RESOLVED + +Added exact bodyless, registration, and event RFC 9421 profiles; bounded raw +body and RFC 9530 digest verification; typed errors; EdDSA/ES256 subscribe and +event key resolution; signature-only silent re-key retry; no-redirect +policy-checked transport; and URL trust rules. Review fixes prevent +deterministic claim failures from triggering JWKS refreshes and reject malformed +Authorization headers on inbound verification. Fifty-two token/HTTP tests pass +and the full solution builds without warnings. + +### [2026-07-15] [Phase 3] Metadata, discovery, and AsyncAPI - RESOLVED + +Added collision-safe AP metadata composition, immutable OpenAPI/AsyncAPI +vocabulary composition, policy-checked cached event-endpoint resolution, and a +focused AsyncAPI AAuth validator that deliberately ignores operation direction. +Cross-package R3 composition, issuer binding, unresolved channel references, +cache invalidation, endpoint policy, and public/protected declarations are +covered by discovery tests. + +### [2026-07-15] [Phase 4] Agent Provider role contracts - RESOLVED + +Added collision-retrying subscribe-token issuance, required durable store +contracts, defensive subscription/incoming-event models, atomic acceptance +outcomes, endpoint verification/status mapping, and required-store DI. Review +removed unbounded in-process ID retention and fixed content-header preservation. +Twenty-five AP tests cover issuance, mappings, durability failures, +EdDSA/ES256, cancellation, and concurrent final use. + +### [2026-07-15] [Phase 5] Resource registration role - RESOLVED + +Added explicit channel/context boundaries, low-level subscribe-token and HTTP +verification, signature-unbound body projection, public/protected endpoint +mapping, selected-event subset enforcement, signed registration client, and DI. +Protected paths preserve escaped PathBase values and reject missing tickets +before invoking application policy. Fourteen registration tests cover both +algorithms, binding, tickets, mappings, bodies, cancellation, and DI. + +### [2026-07-15] [Phase 7] Agent verification and deduplication - RESOLVED + +Added typed event verification outcomes, exact-token SHA-256 idempotency, +pluggable and bounded/expiring deduplication, defensive unauthenticated payload +projection, context lookup, and validated DI registration. Sixteen agent tests +cover both algorithms, context, replay, concurrency, payload substitution, +typed failures, expiry/capacity, and cancellation. + +### [2026-07-15] [Phase 6] Resource event delivery - RESOLVED + +Added immutable resource subscription state, `FromRegistration`, defensive +prepared deliveries, once-only event `jti`, current AP endpoint resolution, +exact-token/body retries with fresh HTTP signatures, and typed AP response +parsing. Review removed an artificial signature-time sequence, restored +metadata-cache semantics, and corrected C8 so application subscription lifetime +may extend beyond the subscribe-token registration window. Twenty-four delivery +tests cover both algorithms, retries, payload immutability, endpoint changes, +all response variants, malformed responses, transport failures, timeout, and +cancellation. + +### [2026-07-15] [Phase 8] Cross-role conformance and API freeze - RESOLVED + +Added four role-specific conformance matrices, a true in-process AP/resource/ +agent flow, and an executable spec-coverage matrix for every in-scope +MUST/MUST NOT in Events L190-L617. Review corrected stored-resource/audience +validation order and made protected-ticket matching and consumption atomic. +The API-freeze gate passes: Events 278, AAuth.Tests 517, +AAuth.Conformance 573, and AAuth.R3.Tests 39, with no failures or skips; the +full solution builds with zero warnings. + +### [2026-07-15] [Phase 9] Runnable Bookings/AP/EventAgent sample - RESOLVED + +Extended Bookings with merged OpenAPI/AsyncAPI metadata, protected waitlist +tickets, registration, and an authenticated deterministic trigger. Extended +MockAgentProvider with the normative event endpoint and explicitly +non-normative in-memory token acquisition, polling, and ACK routes. Added +EventAgent with durable enrollment, generic challenge handling, registration, +polling, verification, deduplication, unauthenticated payload display, and ACK. +The integrated focused stack completed the full flow successfully. Review +corrected the Bookings resource URL, trigger grant ordering/lifetime, revoked +subscription state, and bodyless/empty payload handling. + +### [2026-07-15] [Phase 10] Documentation and release dry run - RESOLVED + +Added the Events workflow, final package/sample READMEs, root documentation and +sample indexes, focused Makefile targets, and shared release packing. The +workflow-equivalent Release gate restored, built, and ran 1,407 tests, then +packed `AAuth`, `AAuth.R3`, and `AAuth.Events` at one version. The Events nupkg +contains its README and only the matching `AAuth` dependency. Final docs review +confirmed frozen API names, routes, security disclosures, retention/durability +requirements, and the non-normative polling/ACK boundary. + +### [2026-07-15] [Phase 11] Independent internal review - RESOLVED + +Three disjoint read-only reviewers covered protocol/crypto, state/concurrency, +and package/sample/docs. All reported HIGH and MEDIUM findings were fixed: +bodyless AP delivery (including `Content-Length: 0`), conventional/transient +agent DI, bounded sample idempotency retention, non-empty selected event types, +and bounded outbound response reads. Follow-up reviewers reported no remaining +high-confidence findings. + +The definitive Release gate restored, built, ran 1,413 tests +(AAuth.Tests 517, AAuth.Conformance 573, AAuth.R3.Tests 39, and +AAuth.Events.Tests 284), and packed all three packages at one version. The +Events package includes its README, depends only on matching-version `AAuth`, +and has no `AAuth.R3` dependency. No core/R3 production file or generated +artifact is in the implementation diff. + +## Deviations from Plan + +None. + +### [2026-07-15] [Phase 1] AP-wide eid uniqueness moved to Phase 4 - PROCEEDED + +Phase 1 proves 128-bit CSPRNG/base64url generation but cannot prove that an AP +never returns a previously stored ID without the durable subscription store. +The collision-retry requirement is therefore tested at `SubscribeTokenIssuer` +in Phase 4, where store insertion is authoritative. This changes no wire or +public token behavior. + +### [2026-07-15] [Phase 6] Registration expiry is not subscription expiry - RESOLVED + +The first delivery implementation incorrectly limited +`ResourceSubscription.ExpiresAt` to the subscribe token's `exp`. C8 and the +draft rationale define that `exp` only as the registration credential window. +The guard was removed and a regression test now permits the application policy +lifetime to extend beyond the token window. + +### [2026-07-15] [Phase 11] Owner review gate follows child-branch commits - RESOLVED + +The execution plan originally phrased owner review as preceding any commit. +The owner explicitly approved implementation-time commits on isolated child +branches. Final owner review therefore gates push/final acceptance rather than +the already-approved rollback-unit commits. + +## Open Questions / Inputs Needed + +None. + +## Post-Code-Review Decisions + +> **Appended after code review (2026-07-16):** The entries below evaluate +> findings 1-3 and deviations A-B in +> [implementation-review-report.md](implementation-review-report.md). +> `RESOLVED` means the decision is settled; pending implementation is marked +> `TODO`. No clarification was required. + +### [2026-07-16] [Phase 5] Review gap 1 - resource audience must fail closed - RESOLVED + +**Decision: agree.** The draft requires the resource to match the subscribe +token `aud` to its own URL +([Subscribe Token](../../../aauth-spec/v09/draft-hardt-aauth-events.md#subscribe-token), +L221 and L275). A nullable `ResourceAudience` silently disables that check, so +this is a security and conformance gap. The canonical resource identifier must +be explicit; deriving it from the request host is unsafe behind proxies or +aliases. + +**TODO (code):** Make a non-empty resource audience mandatory for channel +descriptors and low-level registration verification. Fail before discovery or +handler invocation when it is absent. Add tests proving missing configuration +fails closed and a token for another resource never reaches the handler. + +### [2026-07-16] [Phase 4] Review gap 2 - separate issuer lifetimes - RESOLVED + +**Decision: agree.** The draft explicitly separates the subscribe-token +registration window from subscription lifetime +([Design Rationale](../../../aauth-spec/v09/draft-hardt-aauth-events.md#why-exp-is-the-jwt-validity-period-not-the-subscription-lifetime), +L681-L687). Reusing one `Lifetime` for JWT `exp` and AP +`AgentProviderSubscription.ExpiresAt` contradicts C8/D7 and forces an avoidable +security-versus-usability tradeoff. + +**TODO (code):** Replace the ambiguous issuer lifetime with separate positive +token and subscription lifetime settings. Use the former for JWT `exp` and the +latter for AP `ExpiresAt`; add a short-token/long-subscription regression test +and update sample configuration accordingly. Add no wire lifetime claim. + +### [2026-07-16] [Phase 9] Review gap 3 - event expiry must not expire a subscription - RESOLVED + +**Decision: agree with the gap, but not with duplicating event-expiry rejection +inside the store.** `claims.ExpiresAt` is the event response window, not +subscription state ([AP Validation](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-delivery), +L411). The endpoint resolver already validates it using configured clock skew; +a second strict store check produces inconsistent status codes and must never +transition the subscription to `Expired`. + +**TODO (code):** Remove event-token expiry from the sample store's subscription +expiry branch. Only stored subscription status and +`subscription.ExpiresAt` may expire the subscription. Add a regression proving +an event admitted within verifier skew cannot invalidate the subscription and +a later valid event remains acceptable; events outside skew must still fail +with 401 before the store call. + +### [2026-07-16] [Phase 0] Review deviation A - required event jti - RESOLVED + +**Decision: retain the strict `jti` requirement and accept the reported +interoperability consequence.** The claim is a deliberate draft extension, but +without a per-event identity two legitimate same-second events can produce the +same compact token and become indistinguishable from a retry. Accepting missing +`jti` would reintroduce that ambiguity, so no code fallback will be added. + +**TODO (documentation/upstream):** Before release, state prominently that peers +must include `jti` and propose the claim for the upstream draft. Do not modify +the vendored specification as part of the SDK fix. + +### [2026-07-16] [Phase 7] Review deviation B - exact-token agent deduplication - RESOLVED + +**Decision: agree with the reviewer; no action.** Draft deduplication on +`{iss,eid}` ([Agent Verification](../../../aauth-spec/v09/draft-hardt-aauth-events.md#ap-to-agent), +L445) would discard every event after the first on an unlimited subscription. +Hashing the exact compact token, together with the required fresh `jti`, +distinguishes retries from later events and remains the selected C3/C14 +behavior. + +### [2026-07-16] [Phase 0] Post-review implementation clarifications - RESOLVED + +The owner resolved the implementation-planning questions as follows: + +1. **Question:** How should the `jti` upstream-spec TODO be handled? + **Response:** Update the SDK README and draft an upstream issue/proposal for + owner review. Do not post it during implementation. +2. **Question:** What public API shape should separate issuer lifetimes use? + **Response:** Replace `Lifetime` with `TokenLifetime` and + `SubscriptionLifetime`. +3. **Question:** What defaults should the new lifetime options have? + **Response:** Keep the one-hour default for `TokenLifetime`; require callers + to set a positive `SubscriptionLifetime`. + +### [2026-07-16] [Phase 5] Post-review gap 1 implementation - RESOLVED + +The pending audience action above is complete. Channel descriptors, low-level +registration verification, and subscribe-token resolution now require a +non-empty canonical resource audience. Missing or mismatched `aud` fails before +metadata/JWKS discovery; no request-host inference or compatibility overload +was added. Regression tests cover invalid configuration, zero-request +pre-discovery rejection, wrong-resource 403 mapping, and handler non-invocation. + +### [2026-07-16] [Phase 4] Post-review gap 2 implementation - RESOLVED + +The pending lifetime action above is complete. +`SubscribeTokenIssuerOptions.Lifetime` was replaced by a one-hour-defaulted +`TokenLifetime` and a required positive `SubscriptionLifetime`. JWT `exp` and +AP `AgentProviderSubscription.ExpiresAt` now use those values independently. +MockAgentProvider demonstrates a 300-second registration token and a +3,600-second subscription without adding a wire claim. + +### [2026-07-16] [Phase 9] Post-review gap 3 implementation - RESOLVED + +The pending sample action above is complete. Event-token `exp` no longer +changes subscription state; the endpoint resolver remains the sole event-expiry +validator with configured clock skew. Accepted receipts use a separate, +positive, one-hour-defaulted retention window, so a skew-accepted event remains +pollable while sample replay state stays bounded. Endpoint-level EdDSA/ES256 +tests cover within-skew acceptance, later events, outside-skew 401, and +retention expiry. + +### [2026-07-16] [Phase 10] Post-review jti interoperability action - RESOLVED + +The pending documentation/upstream action above is complete. The package README +prominently states that v09 omits event `jti`, this SDK requires it, peers +without it are rejected, and no fallback is provided. A review-only upstream +proposal is saved as `files/aauth-events-jti-upstream-proposal.md` in the +session folder. It was not posted, and the vendored specification was not +modified. + +### [2026-07-16] [Phase 11] Follow-up review decisions - RESOLVED + +A fresh read-only review found and resolved two coupled issues: wrong-audience +tokens now fail before outbound discovery, and skew-accepted sample receipts +are retained independently of event-token `exp`. A subsequent review reported +no remaining high-confidence findings. + +The suggestion to bump the package version or add compatibility shims was not +adopted. This is an intentionally breaking alpha SDK, compatibility shims are +prohibited by the approved plan, and the publish workflow supplies the next +release version explicitly. + +### [2026-07-16] [Phase 11] Post-review validation - RESOLVED + +The Release build passed. The full test gate passed 1,432 tests: +AAuth.Events.Tests 303, AAuth.Tests 517, AAuth.Conformance 573, and +AAuth.R3.Tests 39. The Events package packed as +`9.9.9-events-postreview`; its README contains the new warnings and lifetime +API, and its only package dependency is matching-version `AAuth`. + +The focused MockAgentProvider/Person Server/R3 AS/Bookings/EventAgent stack +completed acquisition, protected registration, event delivery, polling, +verification, and ACK. The upstream proposal remains unposted. No commit was +created. diff --git a/.agent/plans/2026-07-14-aauth-events/implementation-plan.md b/.agent/plans/2026-07-14-aauth-events/implementation-plan.md new file mode 100644 index 0000000..cde256c --- /dev/null +++ b/.agent/plans/2026-07-14-aauth-events/implementation-plan.md @@ -0,0 +1,639 @@ +# AAuth Events - Implementation Plan + +Phased plan for a complementary `AAuth.Events` preview package implementing +[AAuth Events](../../../aauth-spec/v09/draft-hardt-aauth-events.md). + +- Research: [research.md](research.md) +- Created: 2026-07-14 +- Updated: 2026-07-15 (review comments addressed) +- Package: `src/AAuth.Events/` +- Review gate: no implementation or commit until the repository owner approves + this plan + +## Guiding principles + +- **Spec conformance is paramount; backwards compatibility is not a goal.** + This is a spec-accurate alpha SDK. Use one wire format, do not add legacy + shims, and record every deliberate draft interpretation in + `implementation-log.md`. +- **Keep Events outside core.** `AAuth.Events` depends only on `AAuth`. + `src/AAuth/` and `src/AAuth.R3/` are not modified. +- **Reuse public primitives, own Events semantics.** Reuse core keys, JWT + validation, discovery caches, headers, and outbound signer. Keep the + no-`cnf` event-token path, body-bound verifier, stores, and endpoints in the + Events package. +- **Spec owns the wire; applications own policy.** Ticket formats, channel + parameters, subscription lifetime, payload schemas, retention, and durable + storage remain explicit application seams. +- **Durability and atomicity are API contracts.** The AP cannot return `202` + until one store operation has enforced subscription state and durably + recorded the event + ([Event Delivery](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-delivery), + L402-L428). +- **Security invariants are test targets.** Key binding, covered components, + raw-body digest, audience/resource binding, expiry, replay, `max_uses`, + ticket consumption, URL policy, and concurrency each get negative tests. +- **A verified event envelope does not authenticate its payload to the agent.** + Agent-facing APIs label payload bytes unauthenticated and direct consequential + actions to re-fetch details from the verified resource. + +## Cross-cutting decisions + +| ID | Decision | +|---|---| +| D1 | Ship `AAuth.Events` as a preview NuGet package covering AP, resource, and agent roles; exclude standardized AP-to-agent transport | +| D2 | The production `AAuth.Events` project depends only on `AAuth`; do not modify `AAuth.R3`; track the `AAuth` package version and release. A test-only R3 reference is allowed for RF5 | +| D3 | Use SHA-256 of the compact event token as the default event idempotency key; expose a pluggable deduplicator (C3/C14) | +| D4 | Send the AsyncAPI-defined payload directly as `application/json`; preserve raw bytes, but expose them to agents only as `UnauthenticatedEventPayload` because the event token does not bind them (RF2) | +| D5 | Follow the draft's exact body profiles: registration signs `content-type` but not `content-digest`; event delivery signs both. Registration parameters are signature-unbound, and application contracts must not use them to grant or widen authorization (C5/RF3) | +| D6 | Require an application-provided durable AP store; in-memory stores exist only in tests/samples | +| D7 | Subscription lifetime is application policy recorded as `ExpiresAt`; add no wire claim | +| D8 | Generate never-reused `eid` values from at least 128 cryptographically random bits, base64url encoded | +| D9 | Registration mapper defaults: 200/400/401/403/404/409 as recorded in research C12 | +| D10 | AP delivery adds 401 for expired/invalid event JWT and 403 for wrong `aud`; exact event-token retries return idempotent 202 without another write/use | +| D11 | Outbound Events URLs use HTTPS except loopback HTTP, disable redirects, reject private/link-local IP literals except loopback, and pass a pluggable trust policy; cross-origin `event_endpoint` is allowed after policy approval | +| D12 | Buffer at most 1 MiB by default for digest verification; make the limit configurable | +| D13 | Support core algorithms EdDSA and ES256; reject `none` and unsupported algorithms for both token types per RF1; require and validate `iat` with clock skew | +| D14 | AsyncAPI support is limited to AAuth constants, metadata composition, and declaration validation; follow the draft's `action: receive` examples but do not validate operation direction (RF4) | +| D15 | Samples extend Bookings and MockAgentProvider, add `samples/EventAgent`, and use agent-authenticated non-normative polling | +| D16 | Compose OpenAPI and AsyncAPI in one caller-owned vocabulary map before a single `r3_vocabularies` assignment; preserve entries and reject malformed/conflicting mappings without a production `AAuth.R3` dependency (RF5) | +| D17 | Require a fresh random `jti` in every event token so compact-token hashing distinguishes legitimate same-time events from exact retries; reject missing/empty `jti` (C23, deliberate draft extension) | + +Full questions and owner responses are retained in +[research.md](research.md#clarification-record). Reviewer follow-up decisions +RF1-RF5 are retained in +[Reviewer-comment resolutions](research.md#reviewer-comment-resolutions). + +## Core SDK change assessment + +No core change is planned or required. + +| Core file(s) considered | Potential change | Decision | +|---|---|---| +| `src/AAuth/Server/Metadata/AAuthAgentMetadataOptions.cs`, `src/AAuth/Server/Metadata/WellKnownEndpoints.cs` | Generic agent `AdditionalMetadata` seam | Rejected as convenient, not necessary; Events composes metadata and MockAgentProvider already owns its document | +| `src/AAuth/HttpSig/SignatureKeyParser.cs`, `src/AAuth/HttpSig/AAuthVerifier.cs` | General no-`cnf` JWT parsing and arbitrary covered-field verification | Rejected because it broadens core security behavior; Events uses a package-local exact verifier | +| `src/AAuth/Tokens/JwtWriter.cs` | Make compact signing public | Rejected; keep one internal writer in `AAuth.Events` | + +If implementation discovers a genuine blocker, work stops before touching core. +The proposed change must be added here with exact files, affected APIs, rejected +package-only alternatives, and a correctness argument for owner approval. + +## Phase 0 - Decision and deviation gate + +Create the append-only implementation record before code. Seed it with C1-C23 +and RF1-RF5 from research, including every draft discrepancy in +[Specification issues](research.md#specification-issues-to-retain-in-the-implementation-log). +The local rulings are explicit draft interpretations, not silent compatibility +behavior. + +### Implementation decisions + +- Package and namespace: `AAuth.Events`. +- No core or `AAuth.R3` changes. +- No commit until owner approval of this plan. + +### Definition of Done + +- [x] `implementation-log.md` exists with every C1-C23 and RF1-RF5 ruling + recorded as `[YYYY-MM-DD] [Phase 0] ... - RESOLVED`. +- [x] Every draft discrepancy identifies the selected behavior and the + spec lines it interprets. +- [x] Package public API names and role boundaries are confirmed without adding + Events behavior to core or a production `AAuth.R3` dependency. +- [x] The owner has approved implementation to begin. + +## Phase 1 - Package foundation and token primitives + +**Goal:** establish the optional package and byte-accurate subscribe/event token +surface before adding network or server behavior. + +**Spec:** [Subscribe Token](../../../aauth-spec/v09/draft-hardt-aauth-events.md#subscribe-token) +L204-L244; +[Event Token](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-token) +L340-L374. + +### Files + +| File | Action and responsibility | +|---|---| +| `src/AAuth.Events/AAuth.Events.csproj` | New packable `net10.0` preview project; project-reference `AAuth`; ASP.NET framework reference; package README; same version/repository metadata as `AAuth.R3` | +| `src/AAuth.Events/README.md` | New package status, role matrix, storage requirements, and preview warnings | +| `src/AAuth.Events/AAuthEventsConstants.cs` | New token types, claim names, DWK names, header components, AsyncAPI vocabulary, and security-scheme constants | +| `src/AAuth.Events/Internal/EventsJwtWriter.cs` | New shared compact-JWS writer using `IAAuthKey`; no duplicated token signing | +| `src/AAuth.Events/Tokens/SubscribeTokenBuilder.cs` | New required AP/resource/agent/key/time inputs; optional positive `max_uses`; secure `eid` generation | +| `src/AAuth.Events/Tokens/EventTokenBuilder.cs` | New required resource/agent/`eid`/key/time inputs and a fresh random `jti` per build | +| `src/AAuth.Events/Tokens/SubscribeTokenClaims.cs` | New typed verified subscribe-token projection | +| `src/AAuth.Events/Tokens/EventTokenClaims.cs` | New typed verified event-token projection including required `jti` | +| `tests/AAuth.Events.Tests/AAuth.Events.Tests.csproj` | New optional-package test project using shared xUnit props and TestHost | +| `tests/AAuth.Events.Tests/Tokens/*Tests.cs` | New token structure, validation, algorithm, time, claim, and tamper tests | +| `AAuth.slnx` | Add package and package-test projects | + +### Implementation decisions + +- Builders accept `IAAuthKey`, emit its `Algorithm`, and require a private key. +- `exp > iat`; `max_uses > 0`; `eid`, `iss`, `sub`, and `aud` are non-empty + and typed where core types exist. +- Event `jti` uses at least 128 cryptographically random bits, base64url + encoded. AP and agent readers require a non-empty value. +- Token readers require every draft-required claim; they do not silently + default missing values. +- Core `TokenVerifier.Verify` checks `iat` only when present. The typed + subscribe/event claim readers must therefore require `iat` explicitly before + returning a verified projection. + +### Definition of Done + +- [x] Subscribe tokens contain exactly the required header/payload claims and + optional `max_uses`. +- [x] Event tokens contain no `cnf` and no event payload, but include the + required local `jti` extension. +- [x] EdDSA and ES256 round-trip through the builders and `TokenVerifier`; + both subscribe and event token readers reject `none`, unsupported + algorithms, missing `kid`, missing claims, invalid times, empty `eid`, + missing/empty event `jti`, and non-positive `max_uses`. +- [x] Two event tokens built with the same resource, agent, `eid`, `iat`, and + `exp` still have different `jti` values and compact serializations. +- [x] Generated `eid` values are base64url and contain at least 128 random bits. +- [x] The production package references `AAuth` only; `AAuth.R3` is absent from + `src/AAuth.Events/obj/project.assets.json`. +- [x] `dotnet test tests/AAuth.Events.Tests/AAuth.Events.Tests.csproj` passes + for token tests. + +## Phase 2 - Events HTTP signing and verification + +**Goal:** implement the draft's two signed POST profiles without widening the +core verifier. + +**Spec:** subscribe presentation and verification +([Subscribe Token](../../../aauth-spec/v09/draft-hardt-aauth-events.md#subscribe-token), +L247-L280); event delivery and `dwk`-without-`cnf` +([Event Delivery](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-delivery), +L376-L413). + +### Files + +| File | Action and responsibility | +|---|---| +| `src/AAuth.Events/Http/EventsHttpMessageVerifier.cs` | New RFC 9421 verifier restricted to the base fields plus the D5 Events profiles; freshness and exact field-order checks | +| `src/AAuth.Events/Http/EventsRequestBody.cs` | New bounded raw-body reader and RFC 9530 SHA-256 `Content-Digest` parser/verifier | +| `src/AAuth.Events/Http/EventsJwtKeyResolver.cs` | New AP/resource metadata + `kid` resolver for subscribe and no-`cnf` event JWTs; returns the same key for JWT and HTTP verification | +| `src/AAuth.Events/Http/EventsRequestSigner.cs` | New thin adapter over `AAuthSigningHandler.AdditionalComponentsKey` for registration and event profiles | +| `src/AAuth.Events/Http/EventsVerificationError.cs` | New typed failure categories used by both endpoint mappers | +| `src/AAuth.Events/Discovery/IEventsUrlPolicy.cs` | New pluggable outbound trust decision used before metadata, JWKS, or delivery network access | +| `src/AAuth.Events/Discovery/DefaultEventsUrlPolicy.cs` | New D11 scheme/IP/cross-origin rules | +| `src/AAuth.Events/Discovery/EventsHttpClientFactory.cs` | New no-redirect handler used by Events metadata, JWKS, and delivery calls | +| `tests/AAuth.Events.Tests/Http/*Tests.cs` | New covered-field, raw-body, digest, timestamp, key-binding, malformed-header, and algorithm tests | + +### Implementation decisions + +- Do not call `SignatureKeyParser.ParseAny` for event JWTs because core requires + `cnf.jwk`; extract with public `SignatureKeyHeader.GetJwt`. Parse subscribe + `cnf.jwk` through `KeyFactory` so EdDSA and ES256 are both supported. +- Verify cheap syntax/type/DWK/URL policy before network fetch, then JWT + signature, then HTTP signature and body digest. +- `EventsJwtKeyResolver` receives the Events URL policy and hardened no-redirect + client through constructor injection; it never falls back to core's default + discovery transport. +- After core JWT verification, typed claim readers explicitly reject missing + `iat` before returning success. +- The verifier accepts no arbitrary extension components. + +### Definition of Done + +- [x] Bodyless requests verify only the four base components. +- [x] Registration JSON requires signed `content-type` and deliberately does + not claim signature-bound body integrity; `content-digest` is absent from + this exact draft profile. +- [x] Event JSON requires signed `content-type` and `content-digest`; digest is + compared with the exact bounded bytes later passed to storage. +- [x] Missing, reordered, duplicated, or unexpected covered components fail. +- [x] Event JWT and HTTP signature must verify with the same resource `kid`. +- [x] Wrong `cnf.jwk` binding fails registration. +- [x] Requests over the configured body limit fail before durable storage or + application callbacks. +- [x] Redirects, disallowed schemes, non-loopback private/link-local IP + literals, cross-origin policy rejection, and loopback exceptions are + covered before a discovery request is sent. +- [x] Verification tests include tampered event body/header/path/authority/token, + stale/future signature time, unknown `kid`, silent key rotation, and + algorithm mismatch. + +## Phase 3 - Metadata, discovery, AsyncAPI, and URL policy + +**Goal:** provide Events discovery without an `AAuth.R3` dependency, using the +hardened outbound transport established in Phase 2. + +**Spec:** AP metadata +([AP Metadata](../../../aauth-spec/v09/draft-hardt-aauth-events.md#ap-metadata), +L190-L202); AsyncAPI vocabulary and security declarations +([Event Discovery](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-discovery), +L449-L505). + +### Files + +| File | Action and responsibility | +|---|---| +| `src/AAuth.Events/Discovery/AAuthEventsMetadata.cs` | New `event_endpoint`, `WithAsyncApiVocabulary`, and vocabulary-value helpers; compose a caller-owned map before one metadata assignment | +| `src/AAuth.Events/Discovery/EventEndpointResolver.cs` | New cached AP metadata resolver that reads the current `event_endpoint` at delivery time | +| `src/AAuth.Events/Discovery/AsyncApiAAuthValidator.cs` | New validation for AsyncAPI 3.0, `aauth_subscribe`, public-operation security, and protected-channel annotation | +| `tests/AAuth.Events.Tests/AAuth.Events.Tests.csproj` | Add an `AAuth.R3` project reference for cross-package tests only; the production `AAuth.Events` project remains independent | +| `tests/AAuth.Events.Tests/Discovery/*Tests.cs` | New metadata, cross-package composition, cache/update, endpoint-policy integration, and AsyncAPI tests | + +### Implementation decisions + +- `event_endpoint` must be absolute HTTPS except loopback HTTP. +- Cross-origin endpoints are allowed only after URL-policy approval. +- AsyncAPI helpers validate AAuth declarations, not arbitrary AsyncAPI schemas. +- `WithAsyncApiVocabulary` returns a validated map that preserves every existing + entry, is idempotent for the same endpoint, and throws for malformed values + or a conflicting AsyncAPI endpoint. A separate helper serializes that map as + the one `r3_vocabularies` value. +- Operation `action` is not an AAuth validity criterion. Samples use the draft's + `receive`; the validator accepts either direction rather than asserting which + application the document describes. + +### Definition of Done + +- [x] AP metadata composition emits one valid `event_endpoint` without + overriding typed base metadata. +- [x] Resource metadata can advertise both existing OpenAPI and Events AsyncAPI + vocabulary entries in one `r3_vocabularies` object. +- [x] A cross-package test starts with a caller-owned OpenAPI map, applies the + Events helper, passes the completed map once to + `R3Metadata.AddVocabularies`, and proves both entries survive; identical + reapplication is stable and malformed/conflicting values fail. +- [x] Tests and sample code never call a whole-object R3 composer after Events + metadata has already been assigned; composition precedes serialization. +- [x] Delivery resolves the endpoint from AP metadata at send time and honors + the configured metadata cache; it never persists an endpoint copied from + a subscribe token. +- [x] Metadata and endpoint resolution use the Phase 2 hardened client and URL + policy; no default-redirect path is reachable. +- [x] Required AsyncAPI AAuth declarations are accepted; missing/wrong scheme + and incorrectly secured public/protected operations are reported. +- [x] AsyncAPI tests pin that operation direction is outside validator scope and + that the draft's `action: receive` example is accepted. + +## Phase 4 - Agent Provider issuance and durable event endpoint + +**Goal:** issue subscribe tokens against AP subscription state and accept event +deliveries only through one atomic durable store operation. + +**Spec:** AP setup and `max_uses` +([Subscribe Token](../../../aauth-spec/v09/draft-hardt-aauth-events.md#subscribe-token), +L218-L229); AP validation and responses +([Event Delivery](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-delivery), +L402-L428). + +### Files + +| File | Action and responsibility | +|---|---| +| `src/AAuth.Events/AgentProvider/IAAuthAgentProviderEventStore.cs` | New durable contract for collision-safe subscription creation and atomic/idempotent event acceptance | +| `src/AAuth.Events/AgentProvider/AgentProviderSubscription.cs` | New stored `eid`, agent, resource, max-use, use-count, lifetime, and status model | +| `src/AAuth.Events/AgentProvider/IncomingEvent.cs` | New compact token, token hash, required `jti`, verified claims, raw payload, content type/digest, and receipt metadata | +| `src/AAuth.Events/AgentProvider/EventAcceptanceResult.cs` | New accepted/idempotent/unknown/expired/forbidden/exhausted outcomes and remaining uses | +| `src/AAuth.Events/AgentProvider/SubscribeTokenIssuer.cs` | New generate-sign-store service; returns a token only after subscription creation succeeds | +| `src/AAuth.Events/AgentProvider/EventEndpointExtensions.cs` | New `MapAAuthEventEndpoint` with ordered verification and exact response mapping | +| `src/AAuth.Events/DependencyInjection/AAuthEventsAgentProviderExtensions.cs` | New one-call AP registration; requires an application store | +| `tests/AAuth.Events.Tests/AgentProvider/*Tests.cs` | New issuance, endpoint, durability, response, replay, and concurrency tests | + +### Implementation decisions + +- The package throws during AP DI/startup when no durable store is registered. +- The store, not the endpoint, owns the transaction that checks state, writes + the inbox event, and updates uses. +- Exact token-hash retries return the original successful outcome and + `remaining_uses` without another write or increment. +- A missing/empty event `jti` fails before the store. A distinct token with a + fresh `jti` is never collapsed into an earlier event merely because the other + claims and timestamps match. + +### Definition of Done + +- [x] Subscribe-token issuance stores the AP-side resource `aud`, agent `sub`, + `eid`, optional `max_uses`, and application `ExpiresAt`. +- [x] Subscribe-token issuance retries store collisions with a fresh random + `eid` and never returns an identifier already held by the AP. +- [x] Unknown/expired subscription returns 404; resource mismatch and wrong + agent `aud` return 403; invalid/expired JWT or signature returns 401; + malformed input returns 400; exhausted uses return 429. +- [x] `202` occurs only after the store reports durable acceptance. +- [x] Limited subscriptions return exact `remaining_uses`; unlimited + subscriptions accept either no response body or `{}` through the client. +- [x] Concurrent final-use deliveries produce one durable event and one + successful use; later distinct events return 429. +- [x] A delivery rejected at step 8 for wrong agent `aud` returns 403 without + incrementing the use count or writing an inbox event. +- [x] Exact retries are idempotent under concurrency and do not consume uses. +- [x] Two same-time events with identical non-`jti` claims are both durably + accepted; retrying either exact compact token is idempotent. +- [x] No package-provided in-memory AP store is registered. + +## Phase 5 - Resource subscription registration + +**Goal:** support public and protected registration with layered verifier and +endpoint APIs while leaving channel/ticket policy application-owned. + +**Spec:** public and protected registration +([Subscription Registration](../../../aauth-spec/v09/draft-hardt-aauth-events.md#subscription-registration), +L285-L339); registration replay +([Security Considerations](../../../aauth-spec/v09/draft-hardt-aauth-events.md#subscribe-token-replay-at-registration), +L588-L599). + +### Files + +| File | Action and responsibility | +|---|---| +| `src/AAuth.Events/Resource/SubscriptionRegistrationVerifier.cs` | New low-level subscribe JWT + HTTP request verifier | +| `src/AAuth.Events/Resource/SignatureUnboundRegistrationBody.cs` | New bounded raw-body projection whose name and API documentation state that the registration signature does not cover its content | +| `src/AAuth.Events/Resource/VerifiedSubscriptionRegistration.cs` | New typed AP issuer, agent, resource, `eid`, max-use, key data, and optional `SignatureUnboundRegistrationBody` kept separate from verified authorization facts | +| `src/AAuth.Events/Resource/IAAuthSubscriptionRegistrationHandler.cs` | New application callback for public policy or atomic protected-ticket consume + subscription persistence; contract separates verified authorization facts from signature-unbound preferences | +| `src/AAuth.Events/Resource/SubscriptionRegistrationResult.cs` | New accepted/malformed/unauthorized/forbidden/not-found/conflict outcomes | +| `src/AAuth.Events/Resource/SubscriptionEndpointExtensions.cs` | New public/protected endpoint mapper with D9 defaults | +| `src/AAuth.Events/Agent/SubscriptionRegistrationClient.cs` | New signed POST client using subscribe token as the sole credential | +| `src/AAuth.Events/DependencyInjection/AAuthEventsResourceExtensions.cs` | New one-call resource registration | +| `tests/AAuth.Events.Tests/Resource/Subscription*Tests.cs` | New public/protected, binding, ticket, duplicate, body, and status tests | + +### Implementation decisions + +- The package does not define ticket syntax or a ticket response property. +- A protected handler receives the verified subscribe `sub` and ticket path + value and must consume/register atomically; failed validation does not burn a + ticket. +- Authorization and channel scope come only from the verified subscribe claims, + endpoint, and protected-ticket state. The handler contract requires + signature-unbound body parameters to be treated only as preferences within + that boundary. Because channel schemas are application-defined, the package + cannot cryptographically enforce an arbitrary callback's policy. +- Success defaults to 200 as shown in the overview + ([L163](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L163)). + +### Definition of Done + +- [x] Public registration needs no credential beyond the subscribe token. +- [x] Protected registration rejects expired, unknown, reused, wrong-context, + and wrong-agent tickets. +- [x] The subscribe token's `aud`, AP signature, times, `cnf`/HTTP key, and + `eid` are enforced before the application handler. +- [x] Duplicate `eid` registration returns 409. +- [x] Application-supplied `ExpiresAt` is persisted without a new wire field. +- [x] Optional direct JSON registration parameters are size-bounded, covered + only by signed `content-type`, and exposed under the signature-unbound type. +- [x] Package API documentation, endpoint integration tests, and Bookings prove + the first-party path never uses altered body parameters to widen the event + type, channel, agent, or resource authorization held by the ticket and + verified subscribe token; low-level callback users receive the same warning. +- [x] Default 200/400/401/403/404/409 mapping has integration tests; low-level + verifier users receive typed failures without forced HTTP responses. + +## Phase 6 - Resource event delivery + +**Goal:** turn a stored resource subscription into a signed event delivery and +maintain resource-side remaining-use state. + +**Spec:** event construction and request +([Event Token](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-token), +L340-L374; +[Event Delivery](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-delivery), +L376-L400); remaining-use handling L415-L426. + +### Files + +| File | Action and responsibility | +|---|---| +| `src/AAuth.Events/Resource/ResourceSubscription.cs` | New `eid`, AP issuer, agent, resource, lifetime, and remaining-use model | +| `src/AAuth.Events/Resource/PreparedEventDelivery.cs` | New immutable logical-delivery artifact containing the once-generated `jti`, compact token, raw body, and content metadata; exact retries reuse it | +| `src/AAuth.Events/Resource/EventDeliveryClient.cs` | New endpoint resolution, delivery preparation/send APIs, raw JSON POST signing, response parsing, and typed failure handling | +| `src/AAuth.Events/Resource/EventDeliveryResult.cs` | New accepted/idempotent/exhausted/error outcome with optional remaining uses | +| `tests/AAuth.Events.Tests/Resource/EventDelivery*Tests.cs` | New endpoint refresh, token/body signing, response, retry, and cancellation tests | + +### Implementation decisions + +- The client accepts raw UTF-8 JSON bytes or no payload; it does not + parse/reserialize application data. +- A caller supplies event `exp`; the package does not infer business response + windows. +- A fresh `jti` is generated once when a logical event is prepared. Retrying an + ambiguous or failed transport attempt reuses the same + `PreparedEventDelivery`; preparing again means a distinct event. + +### Definition of Done + +- [x] Every event token copies the subscription `eid`, targets stored agent + `sub`, uses resource `iss`/DWK/key, and carries a fresh random `jti`. +- [x] Separate preparations with otherwise identical same-time inputs produce + different compact tokens; retrying one preparation sends the byte-identical + compact token and payload. +- [x] AP metadata is resolved at delivery time through the hardened resolver. +- [x] Payload bytes produce and are bound to the transmitted `Content-Digest`. +- [x] 202 with `remaining_uses`, 202 with no body, and 202 with `{}` parse + correctly. +- [x] Resource callers can remove exhausted subscriptions on `remaining_uses: + 0`; 429 is a typed exhausted result. +- [x] Transport, metadata, verification, timeout, and cancellation failures are + surfaced, not converted to success. + +## Phase 7 - Agent event verification and deduplication + +**Goal:** verify AP-delivered event artifacts without defining the AP-to-agent +transport. + +**Spec:** [Agent Verification](../../../aauth-spec/v09/draft-hardt-aauth-events.md#ap-to-agent) +L436-L447 and local interpretation C3/C14. + +### Files + +| File | Action and responsibility | +|---|---| +| `src/AAuth.Events/Agent/EventTokenVerifier.cs` | New resource discovery/JWT verification, agent audience, `iat`/`exp`, required `jti`, and typed claims | +| `src/AAuth.Events/Agent/UnauthenticatedEventPayload.cs` | New raw bytes and content type with API documentation that no agent-verifiable artifact binds them | +| `src/AAuth.Events/Agent/VerifiedAgentEvent.cs` | New verified token and SHA-256 idempotency key with an optional, separately named `UnauthenticatedEventPayload` | +| `src/AAuth.Events/Agent/IEventDeduplicator.cs` | New pluggable processed-event contract | +| `src/AAuth.Events/Agent/InMemoryEventDeduplicator.cs` | New bounded/expiring convenience implementation for agents and samples | +| `src/AAuth.Events/DependencyInjection/AAuthEventsAgentExtensions.cs` | New agent verifier/deduplicator registration | +| `tests/AAuth.Events.Tests/Agent/*Tests.cs` | New issuer, audience, time, context, payload, and dedup tests | + +### Implementation decisions + +- Default dedup keys SHA-256 over the exact compact event token, not + `{iss,eid}`; record this deliberate draft deviation in the log. +- Unknown local `eid` context is returned as a typed result for application + policy; it is never treated as a verified actionable event. +- Event-token verification authenticates only the resource, audience, timing, + and subscription context. Payloads may inform display or relevance, but + consequential actions re-fetch details from verified `iss` with a current + auth token. + +### Definition of Done + +- [x] Wrong type/DWK/signature/issuer/audience, missing/future `iat`, expired + `exp`, and missing/empty `jti` fail. +- [x] Multiple distinct event tokens for one unlimited `eid`, including tokens + with otherwise identical same-time claims, can be processed. +- [x] An exact compact-token replay is ignored by the default deduplicator. +- [x] Payload bytes remain unchanged but are surfaced only as + `UnauthenticatedEventPayload`; payload business/schema validation remains + application-owned. +- [x] A test substitutes the payload while retaining the event token and proves + token verification cannot detect the change and never labels the payload + authenticated. +- [x] No polling, SSE, WebSocket, push, or callback transport enters the package + API. + +## Phase 8 - Cross-role conformance and adversarial tests + +**Goal:** prove the complete package behavior against the draft and owner +rulings, including state races that isolated unit tests cannot prove. + +### Files + +| File | Action and responsibility | +|---|---| +| `tests/AAuth.Events.Tests/Conformance/SubscribeTokenConformanceTests.cs` | Full positive/negative subscribe matrix | +| `tests/AAuth.Events.Tests/Conformance/RegistrationConformanceTests.cs` | Public/protected registration and error matrix | +| `tests/AAuth.Events.Tests/Conformance/EventDeliveryConformanceTests.cs` | Resource-to-AP request, status, durability, uses, and replay matrix | +| `tests/AAuth.Events.Tests/Conformance/AgentVerificationConformanceTests.cs` | Agent validation and multi-event/dedup interpretation | +| `tests/AAuth.Events.Tests/Conformance/EventsEndToEndTests.cs` | In-process AP/resource/agent flow with controllable clocks and stores | + +### Definition of Done + +- [x] Every MUST/MUST NOT in spec L190-L617 that falls within package scope has + at least one positive or negative test. +- [x] Public and protected flows pass end to end with EdDSA and ES256. +- [x] Key rotation, cached metadata, changed AP endpoint, redirect attempts, and + URL-policy rejections are covered. +- [x] Concurrent ticket use, duplicate registration, event retry, final + `max_uses`, and durable-store failure are deterministic. +- [x] Conformance pairs two same-time events that differ only by `jti` with an + exact retry of each, proving distinct acceptance and retry idempotency. +- [x] Adversarial coverage includes registration-body parameter substitution + and AP-side event-payload substitution, asserting the documented + authorization and trust boundaries rather than false cryptographic + detection. +- [x] The AP never returns success after a failed/cancelled durable operation. +- [x] A coverage-to-spec table in test names or test documentation maps each + scenario to its section and line range. + +## Phase 9 - Runnable Bookings/AP/EventAgent sample + +**Goal:** demonstrate the draft's protected appointment waitlist flow while +clearly separating sample-only AP-to-agent polling and in-memory persistence +from package guarantees. + +### Files + +| File | Action and responsibility | +|---|---| +| `samples/MockResourceServers/Bookings/Bookings.csproj` | Reference `AAuth.Events` alongside existing `AAuth.R3` | +| `samples/MockResourceServers/Bookings/Program.cs` | Wire Events DI, one merged OpenAPI+AsyncAPI vocabulary map, protected waitlist registration, and deterministic event trigger | +| `samples/MockResourceServers/Bookings/Events/BookingsEventSubscriptions.cs` | Sample-only ticket/subscription persistence and policy | +| `samples/MockResourceServers/Bookings/asyncapi.json` | AsyncAPI 3.0 protected waitlist channel, AAuth annotation, and direct JSON payload schema | +| `samples/MockAgentProvider/MockAgentProvider.csproj` | Reference `AAuth.Events` | +| `samples/MockAgentProvider/Program.cs` | Publish `event_endpoint`; wire issuer and event endpoint; add authenticated sample acquisition/polling routes | +| `samples/MockAgentProvider/Events/SampleAgentProviderEventStore.cs` | Clearly labelled in-memory, non-durable sample implementation | +| `samples/MockAgentProvider/Events/SampleAgentEventEndpoints.cs` | Non-normative agent-signed subscribe-token acquisition and pending-event polling | +| `samples/EventAgent/EventAgent.csproj` | New console project referencing `AAuth` and `AAuth.Events` | +| `samples/EventAgent/Program.cs` | Enrol, obtain protected ticket, acquire subscribe token, register, poll, verify, deduplicate, and display the explicitly unauthenticated payload without acting on it | +| `samples/EventAgent/README.md` | Commands and normative/non-normative boundary | +| `Makefile` | Add focused AP/Bookings/EventAgent launch targets | +| `AAuth.slnx` | Add EventAgent | + +### Implementation decisions + +- The sample uses a protected Bookings waitlist; public registration remains in + conformance tests. +- Polling endpoints require the enrolled agent's signed identity. They are + sample-only and are not exported by `AAuth.Events`. +- A deterministic sample trigger replaces timing-dependent background jobs. + +### Definition of Done + +- [x] Bookings publishes both OpenAPI and AsyncAPI R3 vocabulary entries. +- [x] Bookings emits exactly one `r3_vocabularies` object; composing Events + metadata does not replace the OpenAPI entry supplied through the R3 path. +- [x] Its AsyncAPI document validates through `AsyncApiAAuthValidator`. +- [x] The initial authenticated Bookings response returns a short-lived, + single-use, agent-bound ticket URL. +- [x] EventAgent registers using only the subscribe token at the ticket URL. +- [x] Bookings sends a body-bound event to the AP; AP records it; EventAgent + polls, verifies, resolves context, and prints the direct JSON payload with + an unauthenticated-data warning. +- [x] Reusing a ticket or `eid`, changing agent/resource/audience, replaying the + same event token, and exceeding `max_uses` are demonstrated or covered by + adjacent integration tests. +- [x] Sample output labels AP acquisition/polling and in-memory storage as + non-normative/non-production. + +## Phase 10 - Release, samples, snippets, and docs sweep + +**Goal:** publish the optional package with core/R3 and update all non-compiled +surfaces after APIs are frozen. + +### Files + +| File | Action and responsibility | +|---|---| +| `.github/workflows/publish.yml` | Pack `src/AAuth.Events/AAuth.Events.csproj` with the shared version | +| `README.md` | Add optional package, EventAgent, and Events workflow links | +| `docs/workflows/aauth-events.md` | Role diagram, public/protected flows, storage contracts, security, and draft deviations | +| `src/AAuth.Events/README.md` | Final public API and setup snippets | +| `samples/MockResourceServers/Bookings/README.md` | Waitlist and event-trigger instructions | +| `samples/MockAgentProvider/README.md` | Event endpoint and non-normative polling notes | +| `Makefile`, sample configs, inline snippets | Final command/port/name consistency sweep | + +### Definition of Done + +- [x] Release dry-run builds, tests, and produces `AAuth`, `AAuth.R3`, and + `AAuth.Events` packages at the same requested version. +- [x] Packed `AAuth.Events` has only the intended `AAuth` dependency and includes + its README. +- [x] Every README/snippet uses the frozen API names and direct JSON payload. +- [x] Docs state that production APs must supply durable storage and document + retention; sample storage is not conformant durability. +- [x] Docs disclose C3/C4/C5/C8/C12/C13/C14/C20/C23 and RF1-RF4, including + event-token `none`/`jti`, registration-body integrity, agent payload + trust, and AsyncAPI operation perspective. +- [x] No docs imply that the sample polling endpoints are standardized. + +## Phase 11 - Internal review + +**Goal:** use a fresh read-only reviewer after implementation to find +spec/security/logic defects before owner review or commit. + +### Review inputs + +- vendored Events draft, especially L190-L617; +- [research.md](research.md), including all clarification responses; +- this plan and `implementation-log.md`; +- package, tests, samples, docs, solution, and release diff. + +### Definition of Done + +- [x] A fresh subagent reports severity-graded findings for spec conformance, + crypto/key binding, body integrity, SSRF/redirects, atomicity/durability, + replay/idempotency, metadata composition, error mapping, package + boundaries, agent payload trust, and sample claims. +- [x] Every CRITICAL/HIGH finding is fixed; MEDIUM/LOW findings are fixed or + explicitly ruled in `implementation-log.md`. +- [x] The reviewer confirms no `src/AAuth/` or `src/AAuth.R3/` production file + changed. +- [x] Targeted Events tests, full solution build/test, and package dry-run pass. +- [x] The final diff contains no generated `bin/` or `obj/` artifacts. +- [x] Work is flagged ready for owner review before push/final acceptance. + +## Out of scope + +| Item | Reason | +|---|---| +| Standard AP-to-agent subscribe-token acquisition or delivery protocol | Explicitly out of scope in Events L182-L188 and L430-L434; samples use non-normative polling | +| Production durable store provider | Persistence technology is host-specific; the package defines the atomic contract | +| Full AsyncAPI object model, generation, or payload-schema engine | First release provides AAuth integration declarations only | +| Changes to `AAuth` core or `AAuth.R3` | Public core primitives and package-local Events behavior are sufficient | +| GuidedTour or SampleApp Events mode | Dedicated EventAgent is the approved focused sample | +| SSE, WebSocket, mobile push, webhook, or queue adapters | AP-to-agent transport is platform-specific | +| Payload encryption | The draft defines signed transport and privacy guidance, not encryption | +| End-to-end event payload authentication | The draft defines no agent-verifiable payload signature or digest claim (RF2); the package labels payloads unauthenticated and directs consequential actions to a resource re-fetch instead of inventing a wire extension | +| Non-JSON event payloads | The selected first-release wire interpretation is direct `application/json` | +| Subscription lifetime negotiation claim/body field | The draft defines no such wire field; applications set `ExpiresAt` | +| Upstream specification edits or IANA registration | Draft maintenance is outside this repository implementation | diff --git a/.agent/plans/2026-07-14-aauth-events/research.md b/.agent/plans/2026-07-14-aauth-events/research.md new file mode 100644 index 0000000..bc71a7d --- /dev/null +++ b/.agent/plans/2026-07-14-aauth-events/research.md @@ -0,0 +1,422 @@ +# AAuth Events - Research + +- Created: 2026-07-14 +- Specification: + [aauth-spec/v09/draft-hardt-aauth-events.md](../../../aauth-spec/v09/draft-hardt-aauth-events.md) + (2026-06-24 draft) +- Precedent: + [src/AAuth.R3](../../../src/AAuth.R3) and + [.agent/plans/2026-07-02-r3-rich-resource-requests](../2026-07-02-r3-rich-resource-requests) +- Updated: 2026-07-15 (review follow-up) +- Status: ready for owner review; no implementation or commit has been made + +## Problem and scope + +AAuth Events adds asynchronous subscription and delivery to AAuth. An Agent +Provider (AP) issues a resource-scoped subscribe token, a resource registers the +subscription, and the resource later sends a signed event token and optional +payload to the AP. The AP is the durable inbox because agents need not expose a +public endpoint. Agent-to-AP token acquisition and AP-to-agent transport are +explicitly outside the protocol; only token issuance, receipt verification, and +agent-side event verification are in SDK scope +([protocol overview](../../../aauth-spec/v09/draft-hardt-aauth-events.md#fig-overview), +L143-L188; [AP-to-agent](../../../aauth-spec/v09/draft-hardt-aauth-events.md#ap-to-agent), +L430-L447). + +The selected deliverable is a complementary `AAuth.Events` NuGet package that +depends on `AAuth`, not a feature folded into the core SDK. It covers all +protocol-defined AP, resource, and agent roles. The non-normative sample uses +agent-authenticated polling to make the full flow runnable. + +## Research method + +Three read-only research threads were collated: + +1. The complete Events draft, split by protocol role, wire artifact, security + requirement, and conformance scenario. +2. The `AAuth.R3` package and its research, implementation plan, and log as the + packaging and planning precedent. +3. The core SDK's public crypto, JWT, HTTP-signature, discovery, metadata, DI, + endpoint, test, sample, and release seams. + +> **Update (2026-07-15):** The four missed findings in +> [spec-inconsistency-review.md](spec-inconsistency-review.md) and its +> cross-package metadata risk were re-verified against the vendored draft, +> `R3Metadata`, the Bookings metadata setup, and the +> [AsyncAPI 3.0.0 Operation Object](https://www.asyncapi.com/docs/reference/specification/v3.0.0#operationObject). +> The resulting decisions are recorded as RF1-RF5 below. A subsequent +> consistency pass exposed the C14 token-identity ambiguity, resolved by C23. + +The highest-risk findings were then re-verified directly against the vendored +specification and current source. In particular: + +- token claims and validation sequences were checked at + [Subscribe Token](../../../aauth-spec/v09/draft-hardt-aauth-events.md#subscribe-token) + L204-L280 and + [Event Token](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-token) + L340-L374; +- AP delivery ordering, atomic use counts, durability, and status codes were + checked at + [Event Delivery](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-delivery) + L376-L428; +- discovery and AsyncAPI requirements were checked at + [Event Discovery](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-discovery) + L449-L573; +- core signer/verifier asymmetry was checked in + [AAuthSigningHandler.cs](../../../src/AAuth/HttpSig/AAuthSigningHandler.cs#L38-L52), + [AAuthVerifier.cs](../../../src/AAuth/HttpSig/AAuthVerifier.cs#L20-L23), and + [SignatureKeyParser.cs](../../../src/AAuth/HttpSig/SignatureKeyParser.cs#L89-L119); +- the AP metadata limitation was checked in + [AAuthAgentMetadataOptions.cs](../../../src/AAuth/Server/Metadata/AAuthAgentMetadataOptions.cs) + and + [WellKnownEndpoints.cs](../../../src/AAuth/Server/Metadata/WellKnownEndpoints.cs#L46-L58). + +Finer sample file placement reported by the repository research thread was +re-checked only where it affects the planned Bookings, MockAgentProvider, and +EventAgent flow. + +## Protocol findings + +### Roles and boundaries + +| Role | Protocol responsibility | SDK boundary | +|---|---|---| +| AP | Publish `event_endpoint`; issue subscribe tokens; retain AP subscription state; authenticate, authorize, atomically account for, and durably record delivered events | Token issuer, AP store contract, delivery endpoint, metadata composition | +| Agent | Obtain and present a subscribe token; retain `eid` context; verify resource event tokens; deduplicate events | Registration client, event verifier, context/dedup seams | +| Resource | Verify subscription requests; enforce public or protected channel policy; store subscriptions; discover the current AP endpoint; issue and deliver event tokens | Registration verifier/mapper, resource store/handler seams, delivery client | +| Application | Define channel parameters, ticket format, subscription lifetime, event payload schema, durable persistence, and AP-to-agent transport | Explicit policy and storage interfaces; no invented wire fields | + +The AP must advertise an HTTPS `event_endpoint`, may change it, and the resource +must resolve it from current AP metadata rather than from the subscribe token +([AP Metadata](../../../aauth-spec/v09/draft-hardt-aauth-events.md#ap-metadata), +L190-L202). + +### Subscribe token + +The AP-signed JWT uses `typ: aa-subscribe+jwt`. Its header requires `alg`, `typ`, +and `kid`; `none` must not be accepted and EdDSA is recommended. Required payload +claims are `iss`, fixed `dwk: aauth-agent.json`, `sub`, resource `aud`, +`cnf.jwk`, opaque `eid`, `iat`, and `exp`. Optional `max_uses` is a positive +integer enforced by the AP; omission means unlimited +([Subscribe Token](../../../aauth-spec/v09/draft-hardt-aauth-events.md#subscribe-token), +L204-L244). + +The resource validation order is: + +1. require `typ: aa-subscribe+jwt`; +2. require `dwk: aauth-agent.json`, resolve the AP key by `kid`, and verify the + JWT signature; +3. validate `exp` and `iat`; +4. bind `aud` to the resource; +5. bind `cnf.jwk` to the HTTP-signature key; +6. require a non-empty `eid`. + +The resource then stores enough state to deliver events and resolves the AP +metadata at delivery time +([Subscribe Token verification](../../../aauth-spec/v09/draft-hardt-aauth-events.md#subscribe-token), +L268-L280). + +### Registration + +Public channels accept the subscribe token as the sole `Signature-Key` JWT on a +signed POST. Protected channels first return an opaque, HTTPS, short-lived, +single-use ticket URL from an authenticated interaction. Registration at that +URL carries no additional auth token; the resource must atomically enforce the +ticket and bind subscribe-token `sub` to the previously authenticated agent +([Subscription Registration](../../../aauth-spec/v09/draft-hardt-aauth-events.md#subscription-registration), +L285-L290; +[Protected Subscriptions](../../../aauth-spec/v09/draft-hardt-aauth-events.md#protected-subscriptions), +L291-L339). + +The ticket format and response field are application-defined. The resource +should reject a second registration for an existing `eid` +([Subscribe Token Replay](../../../aauth-spec/v09/draft-hardt-aauth-events.md#subscribe-token-replay-at-registration), +L588-L590). + +### Event token and delivery + +The resource-signed JWT uses `typ: aa-event+jwt`. Its header requires `alg`, +`typ`, and `kid`; its required payload is resource `iss`, fixed +`dwk: aauth-resource.json`, agent `aud`, subscription `eid`, `iat`, and `exp`. +It deliberately has no `cnf` and no event-specific data +([Event Token](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-token), +L340-L374). + +Unlike the subscribe-token header, which explicitly forbids `alg: none` at +[L212](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L212), the +event-token header only recommends EdDSA at +[L348](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L348). This is a +draft omission, not permission to accept unsigned event tokens. C21 applies to +both token types: `none` and unsupported algorithms are rejected. + +The draft defines no per-event identifier in its event-token claims +([L351-L359](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L351-L359)). +Hashing the compact token therefore cannot distinguish a retry from two +legitimate events whose other claims and whole-second timestamps are equal; +deterministic signatures can make those compact tokens identical. C23 adds a +required, fresh, cryptographically random `jti` to each event token and requires +AP and agent verifiers to reject a missing or empty value. This is a deliberate +wire extension that makes C14's compact-token hash safe as an idempotency key +and follows the core builders' fresh-token-ID convention +([ResourceTokenBuilder.cs](../../../src/AAuth/Tokens/ResourceTokenBuilder.cs#L102-L122)). + +The event token is the `Signature-Key` JWT on a POST to the current AP +`event_endpoint`. The same resource key identified by `kid` verifies the JWT +and HTTP signature. A payload is the direct, optional AsyncAPI-defined request +body +([Event Delivery](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-delivery), +L376-L400). + +The AP validation order is: + +1. parse the event JWT and validate `typ`; +2. resolve the resource key and verify the JWT; +3. verify the HTTP signature with that same key; +4. find an active `eid`; +5. bind resource `iss` to the resource authorized by the subscribe token; +6. validate event expiry; +7. enforce and atomically increment `max_uses`; +8. bind event `aud` to the stored agent. + +Only after a durable inbox write may the AP return `202`. A limited subscription +returns `remaining_uses`; exhausted subscriptions return `429` on later +deliveries +([AP Validation](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-delivery), +L402-L428). + +### Agent verification + +The agent verifies event `typ`, resource JWKS signature, its own `aud`, `exp`, +and local `eid` context, then applies deduplication. Payload interpretation is +defined by the resource's AsyncAPI schema, not by the event JWT +([Agent Verification](../../../aauth-spec/v09/draft-hardt-aauth-events.md#ap-to-agent), +L436-L447). + +That verification authenticates the event envelope, not the payload. The event +token contains no event data +([L361](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L361)), and the +resource-to-AP `Content-Digest` +([L387](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L387)) is consumed +at the AP rather than conveyed in an agent-verifiable artifact. An AP can +therefore replace or inject a payload without invalidating the event token, +despite the draft saying the agent may use the payload directly +([L447](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L447)). Agent APIs +must label the payload unauthenticated; consequential or sensitive details +should be fetched from the resource API with a current auth token, consistent +with [Event Content](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-content) +L614-L616. + +### Discovery + +The R3 vocabulary identifier is `urn:aauth:vocabulary:asyncapi`; supporting +resources should publish it in `r3_vocabularies`. AsyncAPI documents describe +channels, receive operations, direct payload schemas, and the +`aauth_subscribe` security scheme. Public operations declare the scheme; +protected ticket operations omit it and explain the prior authenticated call +([Event Discovery](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-discovery), +L449-L505). + +The package only needs constants, metadata composition, and validation of these +AAuth declarations. Applications remain responsible for full AsyncAPI document +generation and payload schema validation. + +The examples use `action: receive` +([L537](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L537), +[L544](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L544)). AsyncAPI 3.0 +defines `action` from the perspective of the application described by the +document, so a resource-owned producer document would ordinarily use `send`. +The draft instead frames the document for the agent reader +([L721](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L721)), making its +intended application perspective ambiguous. The SDK follows the draft examples +but does not treat operation direction as an AAuth validity rule. + +### Security and privacy invariants + +- A subscribe token is bound to one resource by `aud`; AP delivery rechecks the + recorded resource + ([Subscribe Token Scope](../../../aauth-spec/v09/draft-hardt-aauth-events.md#subscribe-token-scope), + L574-L579). +- Protected tickets are short-lived, single-use, and agent-bound + ([Pre-Authorized Subscription URL Security](../../../aauth-spec/v09/draft-hardt-aauth-events.md#pre-authorized-subscription-url-security), + L592-L599). +- The AP sees event tokens and payloads and should document retention + ([AP as Delivery Intermediary](../../../aauth-spec/v09/draft-hardt-aauth-events.md#ap-as-delivery-intermediary), + L600-L603). +- Stable agent identifiers are visible to resources, and payloads should exclude + unnecessary sensitive data + ([Privacy Considerations](../../../aauth-spec/v09/draft-hardt-aauth-events.md#privacy-considerations), + L608-L617). +- Body-bearing event deliveries bind the raw payload through + `Content-Digest`; the AP stores and forwards the verified raw bytes, avoiding a + parse/reserialize gap on the resource-to-AP hop. This does not provide + end-to-end payload authentication to the agent. +- Issuer metadata, JWKS, and delivery destinations are outbound trust inputs. + The selected policy is HTTPS except loopback HTTP, no redirects, private or + link-local IP-literal rejection except loopback, and an application trust + callback. + +## Repository findings + +### `AAuth.R3` precedent + +`AAuth.R3` is a packable preview project that references `AAuth`, includes its +own README and ASP.NET framework reference, and tracks the core version +([AAuth.R3.csproj](../../../src/AAuth.R3/AAuth.R3.csproj#L8-L30)). It centralizes +wire names and exposes typed helpers such as `R3AuthClaims`; consumers pass the +result through generic core seams rather than teaching core about R3 +([R3AuthClaims.cs](../../../src/AAuth.R3/R3AuthClaims.cs#L7-L44), +[R3AccessTokenEndpoint.cs](../../../src/AAuth.R3/R3AccessTokenEndpoint.cs#L48-L59)). + +R3 also uses JSON composition for optional metadata +([R3Metadata.cs](../../../src/AAuth.R3/R3Metadata.cs#L7-L31)). Its project and +test project are explicit solution members, and the shared release workflow +packs it with the same version as core +([AAuth.slnx](../../../AAuth.slnx#L24-L31), +[publish.yml](../../../.github/workflows/publish.yml#L35-L41)). + +`R3Metadata.AddVocabularies` creates and assigns the complete +`r3_vocabularies` object rather than merging a nested entry +([R3Metadata.cs](../../../src/AAuth.R3/R3Metadata.cs#L11-L29)). Bookings +currently assigns one OpenAPI-only object through `AdditionalMetadata` +([Program.cs](../../../samples/MockResourceServers/Bookings/Program.cs#L50-L55)). +When Events is added, both package contributions must be collected in one +caller-owned vocabulary map before that map is assigned to metadata. The Events +helper operates on that map rather than an already serialized top-level +property; `R3Metadata` or core `AdditionalMetadata` performs the single final +assignment. This avoids order-dependent overwrites while preserving package +independence. The helper is idempotent for the same AsyncAPI mapping and rejects +malformed or conflicting mappings. + +The Events package should copy these boundary and release patterns, not R3's +protocol-specific models. + +### Reusable public core surface + +| Need | Existing public surface | Finding | +|---|---|---| +| Token crypto | `IAAuthKey`, `AAuthKey`, `EcdsaAAuthKey`, `KeyFactory` | Sufficient for EdDSA and ES256 signing and verification | +| Generic JWT validation | `TokenVerifier.Verify` and `VerifyWithJwksAsync` | Accept caller-supplied `typ`, `dwk`, audience, clocks, and keys ([TokenVerifier.cs](../../../src/AAuth/Tokens/TokenVerifier.cs#L49-L151), [TokenVerifier.cs](../../../src/AAuth/Tokens/TokenVerifier.cs#L387-L475)) | +| Outbound HTTP signatures | `AAuthSigningHandler`, `JwtSignatureKeyProvider` | Supports per-request `content-type` and `content-digest`; computes SHA-256 digest when requested ([AAuthSigningHandler.cs](../../../src/AAuth/HttpSig/AAuthSigningHandler.cs#L43-L52), [AAuthSigningHandler.cs](../../../src/AAuth/HttpSig/AAuthSigningHandler.cs#L114-L165)) | +| Header parsing | `SignatureKeyHeader.GetJwt` | Can extract a compact JWT without requiring `cnf` ([SignatureKeyHeader.cs](../../../src/AAuth/HttpSig/SignatureKeyHeader.cs#L90-L104)) | +| Metadata and JWKS | `MetadataClient`, `JwksClient` | Cached metadata with issuer binding and rate-limited JWKS refresh ([MetadataClient.cs](../../../src/AAuth/Discovery/MetadataClient.cs#L23-L104), [JwksClient.cs](../../../src/AAuth/Discovery/JwksClient.cs#L23-L109)) | +| ASP.NET/test stack | `Microsoft.AspNetCore.App`, TestHost, shared xUnit props | Matches the R3 package and test project ([tests/Directory.Build.props](../../../tests/Directory.Build.props#L10-L25), [AAuth.R3.Tests.csproj](../../../tests/AAuth.R3.Tests/AAuth.R3.Tests.csproj#L5-L11)) | + +### Events-owned gaps + +| Gap | Evidence | No-core resolution | +|---|---|---| +| Core JWT `Signature-Key` parsing requires `cnf.jwk`; event tokens intentionally omit it. It also parses `cnf.jwk` with Ed25519-only `AAuthKey.FromJwk`, so it cannot handle an ES256 subscribe-token confirmation key | [SignatureKeyParser.cs](../../../src/AAuth/HttpSig/SignatureKeyParser.cs#L104-L127); Events [L394-L398](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L394-L398) | Extract with `SignatureKeyHeader.GetJwt`, parse confirmation keys with `KeyFactory`, perform cheap structural checks, resolve the issuer key, then use `TokenVerifier` | +| Core inbound verifier rejects `content-type` and `content-digest` extension components | [AAuthVerifier.cs](../../../src/AAuth/HttpSig/AAuthVerifier.cs#L20-L23), [AAuthVerifier.cs](../../../src/AAuth/HttpSig/AAuthVerifier.cs#L93-L113) | Package-owned Events HTTP-message verifier with an exact allowlist and raw-body digest verification | +| Core agent metadata options have no extension bag and its JSON builder is private | [AAuthAgentMetadataOptions.cs](../../../src/AAuth/Server/Metadata/AAuthAgentMetadataOptions.cs), [WellKnownEndpoints.cs](../../../src/AAuth/Server/Metadata/WellKnownEndpoints.cs#L208-L225) | Metadata composer that adds `event_endpoint`; samples already own AP metadata/JWKS JSON ([MockAgentProvider/Program.cs](../../../samples/MockAgentProvider/Program.cs#L32-L58)) | +| Core discovery client permits default redirects and has no Events trust callback | [AAuthDiscoveryServiceCollectionExtensions.cs](../../../src/AAuth/DependencyInjection/AAuthDiscoveryServiceCollectionExtensions.cs#L38-L41) | Events-owned hardened HTTP handlers and pluggable URL policy for Events-initiated network calls | +| Core compact JWT writer is internal | `src/AAuth/Tokens/JwtWriter.cs` | One internal shared writer inside `AAuth.Events`; no public core API expansion | + +### Core change assessment + +No core SDK change is required. + +Two generic core changes would be convenient but fail the user's +"absolutely necessary" threshold: + +| Candidate core change | Files affected | Why it is not planned | +|---|---|---| +| Add `AdditionalMetadata` to agent metadata | `src/AAuth/Server/Metadata/AAuthAgentMetadataOptions.cs`, `src/AAuth/Server/Metadata/WellKnownEndpoints.cs`, discovery tests | A package metadata composer and the existing hand-built MockAgentProvider document can emit `event_endpoint`; convenience does not justify a core edit | +| Generalize JWT parsing and HTTP verification for no-`cnf` tokens and arbitrary covered fields | `src/AAuth/HttpSig/SignatureKeyParser.cs`, `src/AAuth/HttpSig/AAuthVerifier.cs`, middleware and conformance tests | This would widen the core security surface. An Events-specific verifier can enforce the narrower draft exactly without changing existing behavior | + +`AAuth.R3` also remains unchanged. `AAuth.Events` owns the AsyncAPI vocabulary +constant and only the Bookings sample references both optional packages. + +## Selected package boundary + +The selected dependency graph is: + +```text +AAuth.Events -> AAuth +AAuth.R3 -> AAuth +Bookings -> AAuth + AAuth.R3 + AAuth.Events +``` + +The package surface divides into: + +- token builders/readers and typed verified claims; +- Events-specific HTTP signing requirements and inbound verification; +- AP subscription issuance, durable inbox contracts, and event endpoint; +- resource registration validation/mapping and event delivery; +- agent verification and pluggable deduplication; +- event endpoint and AsyncAPI metadata/discovery helpers; +- one-call DI registration for AP, resource, and agent roles. + +The AP store contract must make subscription creation collision-safe and event +acceptance atomic: active-subscription lookup, resource and agent binding, +expiry, required event `jti`, event-token hash idempotency, `max_uses`, use-count +update, and durable inbox write are one operation. No in-memory implementation +is registered by the package; test and sample implementations are explicitly +non-production. + +## Clarification record + +Every implementation-shaping clarification asked during research is captured +below, normalized to ASCII for this repository. + +| ID | Question | Response | +|---|---|---| +| C1 | Which roles should the first `AAuth.Events` package plan cover? | All protocol-defined roles (Recommended): AP token issuance/event inbox, resource registration/event delivery, and agent verification; exclude only the spec-defined AP<->agent transport | +| C2 | Should `AAuth.Events` depend directly on the experimental `AAuth.R3` package for AsyncAPI discovery integration? | No (Recommended): depend only on `AAuth`; keep the AsyncAPI vocabulary constant/helper in `AAuth.Events`, with optional sample integration | +| C3 | The draft uses `eid` as a subscription ID and permits unlimited multi-event subscriptions, but Agent Verification says to deduplicate every event by `{iss, eid}`, which would discard all events after the first. How should the plan handle this contradiction? | Expose pluggable deduplication and default to exact event-token hash (Recommended); log the deliberate deviation pending a spec correction | +| C4 | The draft's HTTP example sends the AsyncAPI payload as the direct `application/json` body, while the discovery text calls it a `payload` field and the AsyncAPI example says `contentType: application/jwt`. Which wire shape should the plan target? | Direct AsyncAPI-defined JSON body with `application/json` (Recommended), matching the delivery example | +| C5 | The draft shows extra signed components only in examples, without normative prose. Should the plan require `content-type` for JSON subscription bodies and both `content-type` plus `content-digest` for event payload bodies? | Yes (Recommended): enforce the shown components when a body is present; use base components only when absent | +| C6 | For the AP's durability MUST, should the package require an application-supplied durable inbox implementation rather than register an in-memory production default? | Require a durable store interface; keep in-memory implementations test/sample-only (Recommended) | +| C7 | Which sample topology should the implementation plan use for the end-to-end flow? | Extend existing Bookings + MockAgentProvider and add an event-focused agent flow (Recommended), matching the draft's waitlist example | +| C8 | The draft references subscription expiry but defines no lifetime negotiation field. Should the package model lifetime as application policy recorded at registration, without adding a non-standard wire claim or body field? | Yes (Recommended): application policy supplies `ExpiresAt`; no new wire field | +| C9 | Should the preview `AAuth.Events` package track the core `AAuth` version and be packed by the same release workflow, as `AAuth.R3` does? | Yes (Recommended): same version and release workflow | +| C10 | For the event-focused agent flow, should the plan add a small dedicated console sample or extend the existing GuidedTour? | Add `samples/EventAgent` (Recommended): focused subscribe/receive/verify flow without widening GuidedTour | +| C11 | For resource registration, should the package include an opinionated ASP.NET endpoint mapper in addition to low-level verification primitives, despite the draft not defining registration error bodies/statuses? | Provide both (Recommended): typed verifier plus mapper with documented defaults and an application callback for channel/ticket policy | +| C12 | Which default registration response mapping should the plan record for the mapper where the draft is silent? | 200 success; 400 malformed; 401 signature/JWT failure; 403 audience or agent-ticket mismatch; 404 unknown/expired ticket; 409 duplicate `eid` or reused ticket (Recommended) | +| C13 | For AP delivery failures the draft omits mappings for an expired event token and a wrong event-token `aud`. Which defaults should the plan use? | 401 for expired/invalid event token; 403 for wrong `aud` (Recommended) | +| C14 | How should the AP handle a retry carrying an already durably recorded event token, given the draft has no per-event `jti`? | Treat the compact event-token hash as idempotency key; return 202 without another inbox record or use-count increment (Recommended) | +| C15 | What outbound URL policy should the plan require for issuer metadata, JWKS, and `event_endpoint` fetch/delivery targets? | HTTPS except loopback HTTP, no automatic redirects, reject private/link-local IP literals except loopback, plus a pluggable trust policy (Recommended) | +| C16 | What `eid` generation rule should the plan use where the draft says only "opaque" and "unique to the AP"? | At least 128 bits from a cryptographic RNG, base64url, never reused by the AP (Recommended) | +| C17 | Should the Events endpoint helpers impose a configurable payload-size limit before buffering the body for `Content-Digest` verification? | Yes; default 1 MiB and allow applications to lower/raise it (Recommended) | +| C18 | How much AsyncAPI support should the first package include? | Integration helpers only (Recommended): vocabulary/security constants, metadata composition, and validation of required AAuth declarations; applications own AsyncAPI documents/schemas | +| C19 | Which explicitly non-normative AP<->agent mechanism should the samples use to complete the runnable flow? | Authenticated polling for pending events (Recommended), matching the workload-agent example | +| C20 | The event token lists `iat` as required, but AP/agent validation steps omit an `iat` check. Should the package require it and reject future-issued event tokens using configured clock skew? | Yes (Recommended), consistent with subscribe-token and core JWT validation | +| C21 | Should AAuth Events accept both signing algorithms already supported by core (`EdDSA` and `ES256`), while emitting whichever algorithm the supplied `IAAuthKey` uses? | Yes (Recommended); reject `none` and all unsupported algorithms | +| C22 | May an AP's `event_endpoint` be on a different HTTPS origin from its `iss`, as the draft does not require same-origin? | Yes (Recommended): allow cross-origin after the configured URL trust policy accepts it | +| C23 | A consistency pass found one unresolved C14 edge case: event tokens have no `jti`, and `iat`/`exp` use whole seconds, so two legitimate EdDSA events for the same subscription in one second can produce the same compact token and be mistaken for a retry. Which ruling should the plan record? | Add a random `jti` to each event token (Recommended; deliberate draft extension) | + +No clarification questions were required for RF1-RF5. C23 was asked during the +post-edit consistency pass and its response is recorded verbatim above. + +## Reviewer-comment resolutions + +| ID | Finding | Decision | +|---|---|---| +| RF1 | Event-token header omits the subscribe-token `none` prohibition | **Fix:** record the draft asymmetry; C21 still rejects `none` for every Events token | +| RF2 | Event payload lacks end-to-end integrity at the agent | **Fix:** expose it only as an unauthenticated payload and direct consequential actions to a resource-API re-fetch; do not invent a signed payload wire format | +| RF3 | Registration JSON is not covered by `Content-Digest` | **Push back on changing the wire profile:** retain C5 and the draft example for interoperability, label the body signature-unbound, and make non-reliance for authorization an explicit application contract rather than claim cryptographic enforcement | +| RF4 | AsyncAPI examples use `receive` where a resource-owned producer document would use `send` | **Push back on validator enforcement:** follow the draft examples because its agent-reader perspective is ambiguous; record the issue and leave operation direction outside AAuth declaration validation | +| RF5 | `AAuth.R3` and `AAuth.Events` can overwrite each other's `r3_vocabularies` contribution | **Fix:** compose one caller-owned map before a single metadata assignment, preserve existing entries, reject conflicts, and add a cross-package test | + +## Specification issues to retain in the implementation log + +The owner rulings remove implementation blockers but do not erase upstream draft +issues: + +| Issue | Draft evidence | Local ruling | +|---|---|---| +| `eid` is a subscription identifier but agent deduplication treats it as an event identifier | Unlimited subscriptions at [L229](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L229); dedup at [L445](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L445) | C3 and C14: SHA-256 of the compact event token is the event idempotency key | +| Payload wording and media type conflict | Direct body at [L380](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L380); AsyncAPI `application/jwt` at [L551](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L551) | C4: direct `application/json` body | +| Extra covered components appear in examples, not normative prose | Registration example [L251-L266](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L251-L266); delivery example [L382-L399](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L382-L399) | C5: require the shown fields when a body exists | +| Subscription lifetime is described but has no negotiation field | [Design rationale](../../../aauth-spec/v09/draft-hardt-aauth-events.md#design-rationale) L681-L687 | C8: application policy records `ExpiresAt`; no wire extension | +| Registration statuses and two AP failure mappings are unspecified | Overview `200` at [L163](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L163); AP status list at [L428](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L428) | C12 and C13 | +| Required event `iat` is omitted from AP and agent validation lists | Claim at [L358](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L358); validation at [L402-L413](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L402-L413) and [L438-L445](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L438-L445) | C20: require and validate with clock skew | +| AP validation increments `max_uses` at step 7 before step 8 checks agent `aud` | [L412-L413](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L412-L413) | The durable store commits no use increment or inbox write unless all eight checks pass | +| The resource's stated minimum record `{eid, iss}` omits the agent identifier needed as event-token `aud` | Registration state at [L279](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L279); event `aud` at [L352](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L352) | Persist subscribe-token `sub` in resource subscription state; no wire change | +| Event-token `alg` omits the subscribe-token prohibition on `none` | Subscribe header at [L212](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L212); event header at [L348](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L348) | RF1/C21: reject `none` for both token types | +| The event payload is authenticated to the AP but not end to end to the agent | Token excludes event data at [L361](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L361); AP-hop digest at [L387](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L387); direct agent use at [L447](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L447) | RF2: surface an unauthenticated payload and re-fetch consequential data from the resource | +| Registration JSON is not bound by `Content-Digest` | Registration profile at [L251-L266](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L251-L266); delivery profile at [L382-L399](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L382-L399) | RF3/C5: retain the draft profile, label the body signature-unbound, and document that applications must not rely on it to grant or widen authorization | +| AsyncAPI operation perspective is ambiguous | Draft `receive` examples at [L537](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L537) and [L544](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L544); agent-reader rationale at [L721](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L721) | RF4: follow the draft examples; do not validate `send` versus `receive` | +| Event tokens have no per-event identity, so exact-token retry detection can collide with a legitimate event | Event claims omit `jti` at [L351-L359](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L351-L359); AP validation has no idempotency step at [L406-L413](../../../aauth-spec/v09/draft-hardt-aauth-events.md#L406-L413) | C23: require a fresh random `jti`; retain C14's compact-token hash as the AP and agent idempotency key | + +These rulings must be copied into `implementation-log.md` as Phase 0 +`RESOLVED` decisions before implementation begins. diff --git a/.agent/plans/2026-07-14-aauth-events/spec-review.md b/.agent/plans/2026-07-14-aauth-events/spec-review.md new file mode 100644 index 0000000..55448eb --- /dev/null +++ b/.agent/plans/2026-07-14-aauth-events/spec-review.md @@ -0,0 +1,254 @@ +# AAuth Events Specification Review + +**Spec:** `draft-hardt-aauth-events-00`, 2026-06-24. + +**Method:** Compiled from [research.md](research.md), +[implementation-plan.md](implementation-plan.md), and +[implementation-log.md](implementation-log.md), then checked against the +vendored draft, `src/AAuth.Events`, and its conformance tests. These are +upstream specification issues, not unresolved SDK defects. + +## 1. `eid` cannot be both a subscription ID and an event idempotency key + +- **Nature:** Internal contradiction. `eid` identifies a subscription, which + may accept unlimited events, but agent verification deduplicates every event + by `{iss, eid}` ([Terminology](../../../aauth-spec/v09/draft-hardt-aauth-events.md#terminology), + L138; [Subscribe Token](../../../aauth-spec/v09/draft-hardt-aauth-events.md#subscribe-token), + L229; [Agent Verification](../../../aauth-spec/v09/draft-hardt-aauth-events.md#ap-to-agent), + L445). +- **Concern:** All events in one subscription share the same key. +- **If unchanged:** An agent following the draft processes the first event and + discards every later event in an ongoing subscription. +- **SDK assumption/trade-off:** Deduplicate the exact compact token hash, not + `{iss, eid}`, and rely on a fresh `jti` to distinguish later events + ([EventTokenVerifier.cs](../../../src/AAuth.Events/Agent/EventTokenVerifier.cs#L155-L211), + [EventTokenBuilder.cs](../../../src/AAuth.Events/Tokens/EventTokenBuilder.cs#L30-L87)). + This deliberately deviates from the draft; a newly signed token is a new + event even if its business payload is equivalent. + +## 2. Event tokens do not explicitly forbid `alg: none` + +- **Nature:** Missing security requirement. Subscribe tokens say + implementations `MUST NOT` accept `none`; the equivalent event-token rule is + absent ([Subscribe Token](../../../aauth-spec/v09/draft-hardt-aauth-events.md#subscribe-token), + L212; [Event Token](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-token), + L348). +- **Concern:** The asymmetry permits an event-only implementation to interpret + unsigned JWTs as allowed. +- **If unchanged:** A permissive verifier can accept forged events without a + resource private key. +- **SDK assumption/trade-off:** Apply one strict algorithm allowlist to both + token types: EdDSA or ES256 only + ([EventsJwtKeyResolver.cs](../../../src/AAuth.Events/Http/EventsJwtKeyResolver.cs#L122-L124)). + This closes the gap but requires an SDK update before any future algorithm + can interoperate. + +## 3. The minimum resource record cannot populate event `aud` + +- **Nature:** Internal contradiction. The stated minimum resource state is + `{eid, iss}`, while event issuance and AP validation require the agent + identifier in `aud` + ([Subscription Verification](../../../aauth-spec/v09/draft-hardt-aauth-events.md#subscribe-token), + L279; [Event Token](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-token), + L356; [AP Validation](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-delivery), + L413). +- **Concern:** Neither `eid` nor the AP issuer identifies the subscribed agent. +- **If unchanged:** A resource storing only the documented minimum cannot issue + an acceptable event token; delivery fails the AP audience check. +- **SDK assumption/trade-off:** Persist subscribe-token `sub` as + `AgentSubject`, in addition to the stated minimum, and use it as event `aud` + ([ResourceSubscription.cs](../../../src/AAuth.Events/Resource/ResourceSubscription.cs#L45-L63), + [EventDeliveryClient.cs](../../../src/AAuth.Events/Resource/EventDeliveryClient.cs#L190-L203)). + This adds required durable state without changing the wire format. + +## 4. Event payload integrity ends at the AP + +- **Nature:** End-to-end integrity gap. The event token excludes event data; + `Content-Digest` protects the resource-to-AP hop, but the agent may use the + forwarded payload directly + ([Event Token](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-token), + L361; [Event Delivery](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-delivery), + L387; [Agent Verification](../../../aauth-spec/v09/draft-hardt-aauth-events.md#ap-to-agent), + L447). +- **Concern:** The agent has no resource-signed digest with which to authenticate + payload bytes supplied by the AP. +- **If unchanged:** A compromised or malicious AP can substitute actionable + payload content while the event token still verifies. +- **SDK assumption/trade-off:** Expose payload bytes only as + `UnauthenticatedEventPayload`; consequential details must be re-fetched from + the verified resource + ([UnauthenticatedEventPayload.cs](../../../src/AAuth.Events/Agent/UnauthenticatedEventPayload.cs#L9-L46)). + This avoids a private wire extension but adds application work and another + authenticated request. + +## 5. Registration JSON is not integrity-bound + +- **Nature:** Signature-profile gap. Registration signs `content-type` but not + `content-digest`, unlike event delivery + ([Registration Presentation](../../../aauth-spec/v09/draft-hardt-aauth-events.md#subscribe-token), + L251-L266; [Event Delivery](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-delivery), + L382-L399). +- **Concern:** The HTTP signature authenticates the agent and path, not the + registration body bytes. +- **If unchanged:** A TLS-terminating intermediary or compromised hop can alter + requested event types or other body preferences without invalidating the + HTTP signature. +- **SDK assumption/trade-off:** Retain the draft profile for interoperability, + label the body `SignatureUnboundRegistrationBody`, and prohibit body + preferences from widening authorization + ([SignatureUnboundRegistrationBody.cs](../../../src/AAuth.Events/Resource/SignatureUnboundRegistrationBody.cs#L9-L35)). + This contains the authorization impact but does not provide body integrity. + +## 6. Event payload wire shape and media type conflict + +- **Nature:** Conflicting wire definitions. The HTTP example sends a direct + JSON body, discovery prose calls it a `payload` field, and the AsyncAPI + example declares `application/jwt` + ([Event Delivery](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-delivery), + L380-L395; [AsyncAPI Document](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-discovery), + L479; [Example AsyncAPI Document](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-discovery), + L551). +- **Concern:** The alternatives describe different HTTP bodies and parsers. +- **If unchanged:** Implementations may send a JSON object, a wrapper object, or + a JWT and fail to interoperate. +- **SDK assumption/trade-off:** Follow the concrete delivery example: preserve + the direct payload bytes and require `application/json`, with no wrapper + ([PreparedEventDelivery.cs](../../../src/AAuth.Events/Resource/PreparedEventDelivery.cs#L28-L49)). + Peers choosing either other interpretation are incompatible. + +## 7. Required signed components exist only in examples + +- **Nature:** Normative omission. Examples cover `content-type` for + registration and `content-type` plus `content-digest` for event delivery, but + the verification algorithms do not require those components + ([Registration Verification](../../../aauth-spec/v09/draft-hardt-aauth-events.md#subscribe-token), + L265-L279; [AP Validation](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-delivery), + L404-L413). +- **Concern:** Examples and normative steps define different signature + profiles. +- **If unchanged:** Strict implementations can reject each other, while lenient + implementations may leave event bodies unprotected. +- **SDK assumption/trade-off:** Treat the examples as normative and require the + exact component sequence: base components only when bodyless, + `content-type` for registration JSON, and `content-type` plus + `content-digest` for event JSON + ([AAuthEventsConstants.cs](../../../src/AAuth.Events/AAuthEventsConstants.cs#L63-L75), + [EventsHttpMessageVerifier.cs](../../../src/AAuth.Events/Http/EventsHttpMessageVerifier.cs#L42-L93)). + This is secure and deterministic but may reject a peer following only the + prose. + +## 8. `max_uses` is incremented before audience validation + +- **Nature:** Invalid validation order. AP step 7 mutates use state before step + 8 checks the event audience + ([AP Validation](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-delivery), + L412-L413). +- **Concern:** A request can fail authorization after consuming a finite use. +- **If unchanged:** Wrong-audience deliveries can exhaust a subscription + without delivering any event. +- **SDK assumption/trade-off:** Require one durable store transaction to check + subscription, resource, audience, expiry, replay, and limits before committing + use count and inbox state + ([IAAuthAgentProviderEventStore.cs](../../../src/AAuth.Events/AgentProvider/IAAuthAgentProviderEventStore.cs#L5-L29), + [EventEndpointExtensions.cs](../../../src/AAuth.Events/AgentProvider/EventEndpointExtensions.cs#L129-L132)). + Correctness depends on the application-provided store honoring this contract. + +## 9. The protocol has no per-event identity or AP retry rule + +- **Nature:** Missing replay/idempotency model. Event claims omit `jti`, and AP + validation has no exact-retry check + ([Event Token](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-token), + L351-L359; [AP Validation](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-delivery), + L404-L413). +- **Concern:** A retry is indistinguishable from another use; token-hash + deduplication alone also collides when two same-second events serialize + identically. +- **If unchanged:** Network retries can double-deliver and double-count + `max_uses`; attempts to add local deduplication can suppress legitimate + same-second events. +- **SDK assumption/trade-off:** Add a required, fresh 128-bit `jti`; use the + compact-token SHA-256 hash as the AP and agent idempotency key; return the + prior `202` result for an exact retry without another use + ([EventTokenBuilder.cs](../../../src/AAuth.Events/Tokens/EventTokenBuilder.cs#L30-L87), + [IncomingEvent.cs](../../../src/AAuth.Events/AgentProvider/IncomingEvent.cs#L24-L42), + [EventAcceptanceResult.cs](../../../src/AAuth.Events/AgentProvider/EventAcceptanceResult.cs#L3-L38)). + This is a deliberate wire extension: otherwise spec-conforming event tokens + that omit `jti` are rejected. + +## 10. Subscription expiry has no wire representation + +- **Nature:** Missing lifecycle contract. The AP can return `404` for an expired + subscription, but subscribe-token `exp` is explicitly only the registration + window + ([AP Validation](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-delivery), + L409-L428; [Design Rationale](../../../aauth-spec/v09/draft-hardt-aauth-events.md#design-rationale), + L681-L687). +- **Concern:** No field communicates or negotiates subscription lifetime + between AP and resource. +- **If unchanged:** Implementations apply unrelated local expiry policies and + can disagree about whether a subscription is active. +- **SDK assumption/trade-off:** Keep `TokenLifetime` and AP-side + `SubscriptionLifetime` separate, and require resource applications to supply + their own stored `ExpiresAt` + ([SubscribeTokenIssuer.cs](../../../src/AAuth.Events/AgentProvider/SubscribeTokenIssuer.cs#L22-L24), + [ResourceSubscription.cs](../../../src/AAuth.Events/Resource/ResourceSubscription.cs#L74-L92)). + No non-standard claim is added, so cross-party lifetime synchronization + remains unsolved. + +## 11. Required event `iat` is never listed as validated + +- **Nature:** Missing validation requirement. `iat` is required, but neither + the AP nor agent validation sequence says to reject a future value + ([Event Token](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-token), + L358; [AP Validation](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-delivery), + L404-L413; [Agent Verification](../../../aauth-spec/v09/draft-hardt-aauth-events.md#ap-to-agent), + L438-L445). +- **Concern:** Requiring a claim without defining its validation gives it no + consistent security meaning. +- **If unchanged:** Future-issued events may be accepted by some + implementations and rejected by others. +- **SDK assumption/trade-off:** Require `iat`, require `exp > iat`, and reject + future issuance using configured clock skew + ([EventTokenClaims.cs](../../../src/AAuth.Events/Tokens/EventTokenClaims.cs#L30-L42), + [TokenVerifier.cs](../../../src/AAuth/Tokens/TokenVerifier.cs#L107-L130)). + This is stricter than the event-specific validation lists. + +## 12. Failure status codes are incomplete + +- **Nature:** Interoperability omission. Registration defines no failure map; + AP delivery omits outcomes for an expired event token and wrong event + audience + ([Protocol Overview](../../../aauth-spec/v09/draft-hardt-aauth-events.md#protocol-overview), + L163; [AP Validation](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-delivery), + L404-L428). +- **Concern:** Callers cannot implement deterministic retry, re-registration, + or terminal-failure policy. +- **If unchanged:** Servers return different statuses for the same failure and + clients handle them inconsistently. +- **SDK assumption/trade-off:** Registration maps to + `200/400/401/403/404/409`; expired or invalid event tokens map to `401`; wrong + event audience maps to `403` + ([SubscriptionRegistrationResult.cs](../../../src/AAuth.Events/Resource/SubscriptionRegistrationResult.cs#L3-L11), + [SubscriptionEndpointExtensions.cs](../../../src/AAuth.Events/Resource/SubscriptionEndpointExtensions.cs#L183-L190), + [EventEndpointExtensions.cs](../../../src/AAuth.Events/AgentProvider/EventEndpointExtensions.cs#L157-L191)). + These are opinionated defaults, not guaranteed peer behavior. + +## 13. AsyncAPI operation direction is reversed or perspective-dependent + +- **Nature:** Vocabulary ambiguity. A resource-owned document uses + `action: receive` for messages emitted by the resource, while the rationale + frames the document for an agent reader + ([AsyncAPI Document](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-discovery), + L478; [Example AsyncAPI Document](../../../aauth-spec/v09/draft-hardt-aauth-events.md#event-discovery), + L537 and L544; [Design Rationale](../../../aauth-spec/v09/draft-hardt-aauth-events.md#design-rationale), + L721). +- **Concern:** AsyncAPI normally describes `send`/`receive` from the document + owner's perspective; the draft mixes resource ownership with agent + perspective. +- **If unchanged:** Generic tooling can model the resource as a consumer and + generate the wrong integration shape. +- **SDK assumption/trade-off:** Do not validate operation direction; validate + only AAuth-specific declarations + ([AsyncApiAAuthValidator.cs](../../../src/AAuth.Events/Discovery/AsyncApiAAuthValidator.cs#L53-L103), + [DiscoveryTests.cs](../../../tests/AAuth.Events.Tests/Discovery/DiscoveryTests.cs#L118-L136)). + This accepts both interpretations but cannot diagnose a direction error. diff --git a/.agent/plans/2026-07-29-aauth-v09-sdk-conformance/research.md b/.agent/plans/2026-07-29-aauth-v09-sdk-conformance/research.md new file mode 100644 index 0000000..1eeb287 --- /dev/null +++ b/.agent/plans/2026-07-29-aauth-v09-sdk-conformance/research.md @@ -0,0 +1,769 @@ +# AAuth draft-09 SDK Migration - Conformance Research + +- Created: 2026-07-29 +- Specification: + [aauth-spec/v09/draft-hardt-oauth-aauth-protocol.md](../../../aauth-spec/v09/draft-hardt-oauth-aauth-protocol.md) + (draft-09, published 2026-07-04), with companions + [draft-hardt-aauth-r3.md](../../../aauth-spec/v09/draft-hardt-aauth-r3.md) and + [draft-hardt-httpbis-signature-key-06.txt](../../../aauth-spec/v09/draft-hardt-httpbis-signature-key-06.txt) +- Baseline: **draft-08** ([aauth-spec/v08/](../../../aauth-spec/v08/)), migrated 2026-06-25 +- Scope: `src/AAuth/` and `src/AAuth.R3/`, plus the test and documentation surfaces + that carry the wire contract +- Provenance: [issue #44](https://github.com/aauth-dev/dotnet-samples/issues/44) + and the research and plan on `origin/v09-spec-migration` were incorporated and + independently checked on 2026-07-29 +- Status: research complete; no implementation started + +## Problem and scope + +This is conformance research for migrating the SDK to draft-09. It deliberately +covers two classes of gap, because a migration that fixes only the first leaves the +SDK non-conformant against the very spec it claims to target: + +- **DELTA** — the requirement changed between draft-08 and draft-09. The SDK was + conformant when written and is now behind. +- **PRE-EXISTING** — the requirement is unchanged from draft-08 or older and the SDK + is already non-conformant. A delta-scoped sweep will not surface these, because + there is no diff to follow. + +A third category emerged during research and is tracked separately: + +- **CONTRACT DEBT** — tests and documentation encoding the draft-08 wire shape. + These do not affect the shipped product directly, but they will let a broken + migration pass CI and will actively mislead integrators. + +`src/AAuth.Events/` is excluded — under separate review in +[PR #45](https://github.com/aauth-dev/dotnet-samples/pull/45). `samples/` is +excluded because the samples on the current branch are being reverted. + +## Source-of-truth status + +This document supersedes issue #44 and +`.agent/plans/2026-07-19-aauth-v09-spec-migration/research.md` as the technical +source of truth for the migration. Issue #44 may remain as a work tracker, but no +technical finding or scope boundary depends on it. The earlier branch artifacts +contributed five change sets, seven scope rulings, validation requirements, and a +surface inventory; all are incorporated below with this audit's pre-existing gaps +and contract debt. + +The complete migration evidence set is: + +| Change set | Findings | +|---|---| +| RFC 9457 error responses | A1-A3, C1-C3 | +| Clarification `action` | A4, B10, C1, C3 | +| AAuth Events and protocol integration | A5 | +| Revised R3 AsyncAPI vocabulary | A6 and B3 | +| Signature Keys `self-jwt` | A7 | +| Pre-existing conformance remediation | B1-B8 and B11 | + +### Imported migration rulings + +These rulings came from the earlier migration plan and are retained here so that +removing or closing issue #44 does not lose implementation-shaping context: + +| ID | Ruling | +|---|---| +| D1 | Rename public `ErrorDescription` APIs to `Detail`; use only the draft-09 `detail` wire member, with no compatibility alias or dual-wire fallback | +| D2 | Include existing R3 token, document, challenge, and enforcement error bodies in the RFC 9457 cutover | +| D3 | Do not treat MockAgentProvider/Bootstrap-family error normalization as core SDK work | +| D4 | Migrate the implemented PS clarification flow; research the pre-existing missing AS clarification receiver separately | +| D5 | Keep the complete Events protocol in its dedicated package initiative; core exposes no partial Events support beyond an explicitly approved metadata seam | +| D6 | Coordinate revised R3 AsyncAPI support with Events because the handoff requires both a multi-member operation model and subscription semantics | +| D7 | Implement Signature Keys `self-jwt` only through a coordinated dependency/use-case initiative; do not conflate it with Events `sig=jwt` tokens | + +### Sequencing consequence + +The pre-existing findings are not uniformly separate work. Several touch **exactly +the same code** the delta already forces open, and are far cheaper done together +than scheduled independently: + +| Pre-existing finding | Shares code with | Recommendation | +|---|---|---| +| B8 missing polling outcomes | A1/A2 error cutover (same response helpers) | Ride along where the endpoint state exists | +| B9 endpoint-specific extension codes | A1/A2 (same response helpers) | Preserve and document deliberately | +| B10 updated-request token identity | A4 clarification handler | Ride along | +| B1 auth token `exp` ceiling | nothing | Separate, and urgent | +| B2, B4, B5, B6, B7, B11 | nothing | Separate initiatives | + +## Research method + +Two passes. + +**Pass 1** partitioned draft-09 by its own section structure — nine read-only +subagents, one per area, weighted by normative density (~200 MUST/SHOULD statements, +82 of them in `# Protocol Primitives` alone). This was deliberately *not* partitioned +by the known draft-08 → draft-09 delta, so unchanged requirements were audited with +equal weight. That choice is what surfaced the pre-existing set. + +**Pass 2** was designed against pass 1's demonstrated blind spots rather than against +the spec: six sweeps targeting error emission by pattern, wire-name-to-.NET API +mirroring, negative/prohibition requirements, test and documentation surfaces, +cross-cutting synthesis, and client-side conformance. + +### Verification posture + +Subagent output was treated as a lead, not a finding. Every claim recorded here as +CRITICAL or HIGH was re-verified by hand against source. That pass changed the result +materially — see [Corrections to subagent findings](#corrections-to-subagent-findings), +which records **seven** wrong claims including two false "conformant" verdicts on +requirements that are in fact unimplemented. + +Line citations were re-derived by grepping the requirement text and then printing +each cited line to confirm it matches the quotation. Seven of the line numbers +originally reported were wrong. + +**Recorded as reported, not individually re-verified:** the per-document metadata +field tables, the CONFORMANT determinations in agent identity, delegation, missions, +and federation, and the full 45-row negative-requirement inventory. + +### Final independent verification + +After consolidation, six fresh read-only verifiers checked every finding and every +confirmed-conformant claim against the named spec area and current source: + +| Verification area | Items | Outcome | +|---|---|---| +| Error responses and clarification | A1-A4, B8-B10, C1-C4 | Verified; error-body inventory fixed at 12 files; B9 reframed as legal endpoint extensions | +| AAuth Events | A5, D5-D7, companion evidence | Verified against the complete Events draft and current package boundary | +| R3 | A6, B3, D2, D6, R3 evidence | Verified; capability wording narrowed to conditional multi-member rejection | +| Signature Keys and discovery | A7, B4, B5, D7 | Verified; B4 raised to HIGH and B5 default/enforcement distinction clarified | +| Tokens, scopes, missions, federation | B1, B2, B10, B11 and corresponding conformant claims | Verified; B10 confirmed PRE-EXISTING from draft-08 L1071 | +| Interaction, metadata, replay, IANA | B6-B8 and corresponding conformant claims | Verified; optional metadata omissions retained as capability gaps | + +The final pass did not accept verifier conclusions blindly. Conflicts were resolved +against source: B10 is not a draft-09 delta; B6 remains an SDK conformance gap despite +one verifier calling it a deployment obligation, because L2076 places both MUSTs on +"the server"; and the test file locations in C2 were confirmed directly. + +#### Finding verification ledger + +| Item | Governing spec area | Final verdict | +|---|---|---| +| A1 | Protocol `#error-responses`, `#error-response-format`, L2230-L2243 | VERIFIED | +| A2 | Protocol `#error-response-format`, L2238-L2243; draft-08 baseline L2242-L2247 | VERIFIED | +| A3 | Protocol `#error-response-format`, especially `error`/`detail` at L2240-L2241 | VERIFIED | +| A4 | Protocol `#agent-response-to-clarification`, L1012-L1021 | VERIFIED DELTA | +| A5 | Protocol agent metadata and JWT registrations; complete Events draft L190-L632 | VERIFIED, separate initiative | +| A6 | R3 `#asyncapi-vocabulary`, v09 L186-L200 versus v08 L180-L188 | VERIFIED DELTA | +| A7 | Signature Keys draft-06 §3.7, L757-L830; protocol `#keying-material` | VERIFIED DELTA, separate initiative | +| B1 | Protocol `## Re-authorization`, L1304-L1310 | VERIFIED PRE-EXISTING; resource-side clause needs upstream clarification | +| B2 | Protocol `#scopes`, L1980-L1994 | VERIFIED PRE-EXISTING | +| B3 | R3 standard vocabularies L110-L243 | VERIFIED capability limitation with narrowed wording | +| B4 | Protocol `#jwks-discovery`, L2409; Signature Keys §6.3 | VERIFIED PRE-EXISTING | +| B5 | Protocol `#jwks-discovery`, L2405-L2407 | VERIFIED PRE-EXISTING with safe-default caveat | +| B6 | Protocol `#interaction-code-format`, L2064-L2080; security rationale L2729 | VERIFIED PRE-EXISTING | +| B7 | Protocol metadata documents L2452-L2638 | VERIFIED capability/SHOULD gaps, not one blanket violation | +| B8 | Protocol Polling Error Codes L2270-L2280 plus each producing state machine | PARTLY VERIFIED; `invalid_code` is a definite B6 gap, other outcomes need reachability evidence | +| B9 | Protocol common error format L2238-L2243 plus endpoint-specific definitions | VERIFIED inventory requirement; custom codes are not inherently non-conformant | +| B10 | Protocol clarification updated request, v09 L1065 and v08 L1071 | VERIFIED PRE-EXISTING | +| B11 | Protocol Agent Token `exp`, L560 | VERIFIED PRE-EXISTING SHOULD-grade gap | +| C1 | Derivative evidence for `#error-response-format` and `#agent-response-to-clarification` | VERIFIED contract debt | +| C2 | Derivative evidence for the RFC 9457 wire/public-API cutover | VERIFIED contract debt | +| C3 | Derivative documentation evidence for A1-A4 | VERIFIED contract debt | +| C4 | End-to-end evidence for coordinated producer/consumer cutover | VERIFIED coverage gap; no existing assertion needs a mechanical rename | + +## Why the first pass missed findings + +The earlier migration research found four things pass 1 did not. Rather than treat +that as bad luck, each was traced to a mechanical cause and confirmed. These causes +shaped pass 2. + +### 1. Case and separator blindness + +Pass 1 grepped the wire name `error_description` and reported 5 files. The .NET +mirror is `ErrorDescription` — PascalCase, no underscore — so +[TokenError.cs](../../../src/AAuth/Errors/TokenError.cs) was invisible. Confirmed: a +case-insensitive search returns that file, the exact-case search does not. The +consequence was missing a **public API breaking change** entirely. + +*Countermeasure:* pass 2 searched case-insensitively and with separators stripped. + +### 2. File-role intuition instead of pattern sweeps + +Pass 1 looked for error emission in files it judged to be "endpoints". Error bodies +also come from helpers and policy types. A pattern-first sweep finds **12 files that +emit JSON AAuth error bodies**; pass 1's "exhaustive" inventory listed 9. It missed +[AAuthHttpContextExtensions.cs](../../../src/AAuth/Server/Verification/AAuthHttpContextExtensions.cs) +(a challenge helper) and +[R3Enforcement.cs](../../../src/AAuth.R3/R3Enforcement.cs) (a decision type emitting +`r3_denied` and `r3_approval_required`), among others. + +*Countermeasure:* pass 2's error sweep was forbidden from filtering by file role and +was given the specific misses as examples. + +### 3. Scope exclusion of tests and documentation + +Pass 1 excluded `tests/` and `docs/` as "not the SDK". For a wire-format cutover that +was the wrong call: fixtures encoding the old shape keep passing while the product +breaks. This exclusion hid the most operationally dangerous category in this +document — see [C1](#c1-the-conformance-suite-cannot-detect-a-broken-migration). + +*Countermeasure:* pass 2 swept them as first-class surfaces. + +### 4. Negative requirements are invisible to "find the implementation" + +Pass 1's prompts asked subagents to locate the code implementing each requirement. +That method cannot evaluate requirements phrased as MUST NOT / MUST reject / MUST +ignore, because for those the *absence* of code is ambiguous — it may mean the SDK +correctly never does the forbidden thing, or that it fails to reject input a +conformant implementation must refuse. Those are opposite outcomes. + +*Countermeasure:* pass 2 ran a dedicated sweep classifying every negative requirement +as ENFORCED / VACUOUS / UNENFORCED / UNENFORCEABLE. That sweep proved the least +reliable of the six (corrections X4-X5) — the framework was right, its verdicts +needed heavy checking. + +### 5. Partitioning prevents synthesis + +Nine area-scoped reviewers cannot see relationships spanning areas. The earlier +migration research noted that Signature Keys `self-jwt` and the Events +`sig=jwt`-with-`dwk` +presentation are structurally similar but separate wire schemes that must not be +conflated. No area-scoped reviewer held both. + +*Countermeasure:* pass 2 included a synthesis sweep given the whole picture. + +## Findings: draft-09 deltas + +### A1. No JSON AAuth error body uses RFC 9457 problem details — CRITICAL + +> Error response bodies use the HTTP problem details format ([@!RFC9457]) with +> `Content-Type: application/problem+json`. +> — [L2238](../../../aauth-spec/v09/draft-hardt-oauth-aauth-protocol.md) + +`application/problem+json` appears **zero times** in `src/AAuth/` and +`src/AAuth.R3/`. Every error body goes out via `Results.Json` / `Results.NotFound` +and serialises as `application/json`. + +The verified producer surface is 12 files: + +| Package | Error-body producer | +|---|---| +| Core | `AAuthAccessServerEndpoints.cs` | +| Core | `AAuthApplicationBuilderExtensions.cs` | +| Core | `AAuthGovernanceApplicationBuilderExtensions.cs` | +| Core | `AAuthPersonServerEndpoints.cs` | +| Core | `GovernanceEndpoints.cs` | +| Core | `AAuthInteractionEndpointExtensions.cs` | +| Core | `RevocationEndpoint.cs` | +| Core | `AAuthHttpContextExtensions.cs` | +| R3 | `R3AccessTokenEndpoint.cs` | +| R3 | `R3Challenge.cs` | +| R3 | `R3DocumentEndpoint.cs` | +| R3 | `R3Enforcement.cs` | + +[R3Enforcement.cs](../../../src/AAuth.R3/R3Enforcement.cs) emits `r3_denied` and +`r3_approval_required`; the helper +[AAuthHttpContextExtensions.cs](../../../src/AAuth/Server/Verification/AAuthHttpContextExtensions.cs) +emits `auth_token_required`. These non-endpoint-shaped files are why pattern-first +enumeration is required. + +Three emitters already use what become RFC 9457 **extension members** — +`{error, mission_status}` in `GovernanceEndpoints`, `{error, id}` in the governance +pending handler, and `{error, r3_uri, r3_s256}` in `R3Enforcement`. Legal under +RFC 9457, but they should be declared deliberately rather than inherited by accident. + +Correctly excluded from the cutover: +[AAuthVerificationMiddleware.cs](../../../src/AAuth/Server/Verification/AAuthVerificationMiddleware.cs) +and `AAuthChallengeMiddleware.cs` emit status plus header and no body — +authentication failures use the `Signature-Error` header per +[L2232-L2234](../../../aauth-spec/v09/draft-hardt-oauth-aauth-protocol.md). The spec +does not forbid an accompanying problem body, but the migration should preserve the +current bodyless behavior rather than invent one without a use case; any future body +would still be governed by A1. + +### A2. Six emission sites still use the removed `error_description` — HIGH + +draft-09 replaced `error_description` with `detail` +([L2241](../../../aauth-spec/v09/draft-hardt-oauth-aauth-protocol.md); `error` becomes +a REQUIRED extension member at +[L2240](../../../aauth-spec/v09/draft-hardt-oauth-aauth-protocol.md)). Six sites +remain: [RevocationEndpoint.cs](../../../src/AAuth/Server/RevocationEndpoint.cs) ×3 and +[AAuthApplicationBuilderExtensions.cs](../../../src/AAuth/DependencyInjection/AAuthApplicationBuilderExtensions.cs) ×3. + +This is a **breaking public .NET API change**, not only a wire change. Two public +members mirror the removed wire member: + +| Public member | Location | +|---|---| +| `TokenErrorResponse.ErrorDescription` | [TokenError.cs:50](../../../src/AAuth/Errors/TokenError.cs#L50) | +| `AAuthTokenExchangeException.ErrorDescription` | [AAuthTokenExchangeException.cs:27](../../../src/AAuth/Errors/AAuthTokenExchangeException.cs#L27) | + +A dedicated sweep confirmed these are the **only** public members mirroring a removed +or renamed draft-09 wire member. The SDK constructs wire names explicitly in code and +uses exactly one `[JsonPropertyName]` attribute, so there is no hidden serialisation +drift to chase. D1 rules that both become `Detail` without aliases. + +### A3. SDK clients cannot read draft-09 error text — HIGH + +The client parsers read **only** `error_description`, with no `detail` fallback: +[TokenExchangeClient.cs:247](../../../src/AAuth/Agent/TokenExchangeClient.cs#L247) and +[AccessServerClient.cs:514](../../../src/AAuth/Access/AccessServerClient.cs#L514). +Against a conformant draft-09 server the explanation is silently discarded — no +exception, no log, just a null diagnostic exactly when one is wanted. + +A dedicated client-side sweep found this is the SDK's **only** silent failure of this +shape; request construction, polling, challenge handling, and header emission and +parsing were all confirmed conformant. + +### A4. Clarification `action` discriminator absent on both sides — HIGH + +> A POST body MUST include an `action` member identifying the response type. A server +> MUST reject a POST with a missing or unrecognized `action` value with +> `400 Bad Request`. +> — [L1020](../../../aauth-spec/v09/draft-hardt-oauth-aauth-protocol.md) + +`"action"` appears in neither +[ClarificationExchange.cs](../../../src/AAuth/Agent/ClarificationExchange.cs) nor the +pending-URL POST handler in +[AAuthPersonServerEndpoints.cs](../../../src/AAuth/Person/AAuthPersonServerEndpoints.cs). +Both directions break. + +See [C1](#c1-the-conformance-suite-cannot-detect-a-broken-migration): seven +conformance test locations pass whether or not this is fixed correctly. + +### A5. AAuth Events is a complete companion protocol — SEPARATE INITIATIVE + +draft-09 adds the optional `event_endpoint` to Agent Provider metadata, required +when the AP supports Events (protocol L2482-L2511), and references the companion's +`aa-subscribe+jwt` and `aa-event+jwt` types (L2899-L2915). Core +`AAuthAgentMetadataOptions`, `BuildAgentMetadata`, and `ServerMetadata` cannot emit +or parse `event_endpoint`. + +The companion itself defines complete AP, resource, and agent roles: + +| Feature group | Spec evidence | +|---|---| +| AP metadata | Events `#ap-metadata`, L190-L202 | +| Subscribe-token claims and verification | Events `#subscribe-token`, L204-L283 | +| Public and protected registration | Events L285-L339 | +| Event-token claims | Events `#event-token`, L340-L374 | +| Resource-to-AP delivery and atomic acceptance | Events `#event-delivery`, L376-L429 | +| AP-to-agent routing and agent verification | Events `#ap-to-agent`, L430-L447 | +| AsyncAPI discovery | Events `#event-discovery`, L449-L573 | +| Replay, privacy, and retention | Events L574-L616 | + +The current branch contains a dedicated `AAuth.Events` implementation under review, +so this migration must not duplicate it. D5 retains Events as a coordinated package +initiative. Core metadata support is added only if that package boundary cannot emit +and consume the required field without implying partial core support. + +The critical shared primitive is issuer-controlled key discovery. Events delivery +uses `sig=jwt` with `dwk` and no `cnf`; the AP discovers the resource key by +`iss`/`dwk`/`kid`, verifies the JWT, then verifies the HTTP signature with the same +key. That is **not** Signature Keys `self-jwt`, despite the structural similarity. + +### A6. Revised R3 AsyncAPI operations exceed the current model — SEPARATE INITIATIVE + +The revised R3 draft keeps `operationId` REQUIRED, makes `action` OPTIONAL, and hands +an R3-granted subscription to AAuth Events through a ticket and subscribe token +(`#asyncapi-vocabulary`, R3 L186-L200). + +[R3Operation.cs](../../../src/AAuth.R3/Model/R3Operation.cs) deliberately accepts +exactly one string member. A valid AsyncAPI entry can contain both `operationId` and +`action`, so deserialization rejects it. Adding only a vocabulary constant is +therefore incomplete; D6 coordinates the model change and subscription handoff with +Events. + +This limitation also affects GraphQL, WSDL, and OData multi-member operations. gRPC +is different: its one-member `{method: ...}` operation is representable through the +generic `Field`/`Id` model even though no convenience constant or factory exists. + +### A7. Signature Keys `self-jwt` is absent — SEPARATE INITIATIVE + +Signature Keys draft-06 adds `sig=self-jwt` (§3.7, L757-L830). The JWT MUST contain +HTTPS `iss` and `dwk`, MUST have `kid`, MUST NOT contain `cnf`, and SHOULD contain +standard claims. Verification must reject malformed or unexpected input cheaply, +discover `{iss}/.well-known/{dwk}` and `jwks_uri`, verify the JWT, and verify the HTTP +signature with the same discovered key. + +[AAuthConstants.cs](../../../src/AAuth/AAuthConstants.cs) declares only `jwt`, `hwk`, +`jkt-jwt`, and `jwks_uri`; there is no `self-jwt` formatter, parser branch, provider, +resolver, test, or documentation. A presented `self-jwt` is rejected as an unsupported +scheme. + +D7 keeps this as a dependency/use-case initiative because the main AAuth protocol +still requires `scheme=jwt` for resource, PS, and AS requests and attaches no current +core flow to `self-jwt`. It must share one admitted `iss`/`dwk` JWKS resolver with +Events while preserving the distinct wire schemes and the mandatory `cnf` rejection. +The draft's illustrative `typ` and `dwk` values also differ from this repository's +AAuth values, so a concrete use case must be selected before implementation. + +## Findings: pre-existing non-conformance + +None of these appeared in the earlier delta-scoped migration research. + +### B1. Auth token `exp` is not bounded by the agent token `exp` — CRITICAL + +> Auth tokens MUST NOT have an `exp` value that exceeds the `exp` of the agent token +> used to obtain them — a resource MUST reject an auth token whose associated agent +> token has expired. +> — [L1310](../../../aauth-spec/v09/draft-hardt-oauth-aauth-protocol.md) + +[AuthTokenBuilder.cs](../../../src/AAuth/Tokens/AuthTokenBuilder.cs) caps `Lifetime` at +one hour (L141-L143) and computes `exp = iat + Lifetime` (L152). It never reads the +agent token's `exp`, and no file in the SDK compares the two. An agent token with five +minutes remaining yields an auth token valid for a further hour. + +Identical text in draft-08, so this is a pre-existing defect, not migration debt. + +The second clause deserves an upstream question rather than an implementation: when an +agent presents an auth token, that token *is* the `Signature-Key` JWT, so the resource +never sees the agent token and cannot observe its expiry. As written the resource-side +obligation appears unenforceable. + +### B2. Resource-token scopes are not validated against published metadata — HIGH + +[L1987](../../../aauth-spec/v09/draft-hardt-oauth-aauth-protocol.md) carries two +requirements; the SDK satisfies one: + +| Requirement | Status | +|---|---| +| Auth token scope MUST NOT be broader than resource token scope | **Conformant** — [TokenVerifier.cs:166-167, 276](../../../src/AAuth/Tokens/TokenVerifier.cs#L166) | +| Resource token MUST only include scopes defined in `scope_descriptions`, and identity scopes declared in `scopes_supported` | **Missing** | + +`scope_descriptions` and `scopes_supported` are emitted and parsed as metadata but +never consulted when issuing or verifying a token, so a resource can mint scopes it +never published. + +### B3. Multi-member forms of four R3 vocabularies are unsupported — MEDIUM + +[Vocabulary.cs](../../../src/AAuth.R3/Model/Vocabulary.cs) declares `mcp` and +`openapi`. R3 defines seven vocabulary identifiers, but missing convenience constants +do not alone establish wire nonconformance. `grpc` uses a one-member `{method: ...}` +shape and is representable through the generic `Field`/`Id` API. + +The structural gap is the multi-member form of four vocabularies. GraphQL always +requires `{operation, type}` and is therefore always rejected. AsyncAPI, WSDL, and +OData have one required member plus an optional second member; the one-member form is +representable, but the converter rejects a conformant entry when that optional member +is present. [R3Operation.cs](../../../src/AAuth.R3/Model/R3Operation.cs) rejects a +second member and its own API documentation names those four as unsupported. A6 covers +the draft-09 AsyncAPI delta; the other three are pre-existing capability limitations +in a preview package, not proof that every R3 use is non-conformant. + +### B4. No egress admission on metadata and JWKS fetches — HIGH + +[L2409](../../../aauth-spec/v09/draft-hardt-oauth-aauth-protocol.md) requires egress +admission per the signature-key spec before fetching issuer metadata or `jwks_uri`; +signature-key §6.3 details the checklist. Neither +[MetadataClient.cs](../../../src/AAuth/Discovery/MetadataClient.cs) nor +[JwksClient.cs](../../../src/AAuth/Discovery/JwksClient.cs) applies any of it. + +Notably [R3FetchClient.cs](../../../src/AAuth.R3/R3FetchClient.cs) *does* enforce +HTTPS, private-IP rejection, and no-redirect — the control exists in the codebase and +simply was not applied to the core discovery path. This is HIGH because L2409 is a +MUST and the missing controls sit on an issuer-controlled network-fetch boundary. + +### B5. JWKS cache ignores HTTP cache headers and the 24-hour cap — MEDIUM + +[L2405](../../../aauth-spec/v09/draft-hardt-oauth-aauth-protocol.md) requires caching +JWKS, says implementations SHOULD respect `Cache-Control` and `Expires`, and says +cached entries SHOULD be discarded after a maximum of 24 hours regardless of cache +headers. `JwksClient` never reads response headers. Its default one-hour TTL is safely +inside the 24-hour recommendation, but the constructor accepts any caller-supplied TTL +and enforces no maximum, so a 30-day cache is possible. The once-per-minute floor and +the same-`kid` refresh at +[L2407](../../../aauth-spec/v09/draft-hardt-oauth-aauth-protocol.md) *are* implemented +correctly. + +### B6. Interaction-code rate limiting is delegated, not enforced — HIGH + +Two normative MUSTs, neither met: + +> the server MUST rate-limit code-validation attempts at the interaction URL. After a +> small number of failed attempts the server MUST treat the pending interaction as +> terminally failed and return `invalid_code`. +> — [L2076](../../../aauth-spec/v09/draft-hardt-oauth-aauth-protocol.md) + +[L2729](../../../aauth-spec/v09/draft-hardt-oauth-aauth-protocol.md) then names entropy +and rate limiting together as *the* brute-force defence. The SDK implements entropy +and single-use; +[IInteractionPendingStore.cs](../../../src/AAuth/Server/ResourceManaged/IInteractionPendingStore.cs) +documents rate limiting as a deployment control and provides no seam, and nothing +implements the terminal-failure transition. + +### B7. Metadata capability gaps — LOW to MEDIUM + +Absent from options and emission: `mission_control_endpoint` (PS), +`additional_signature_components` (resource), `claims_supported` (PS), +`localhost_callback_allowed` (agent). `additional_signature_components` is the +consequential one — without it a resource cannot advertise extra covered components, +so agents cannot know to sign them. + +These are not one class of normative violation: + +| Field | Spec status | Assessment | +|---|---|---| +| `mission_control_endpoint` | OPTIONAL | Capability omission, not nonconformance | +| `localhost_callback_allowed` | OPTIONAL, default `false` | Capability omission; omission safely preserves the default | +| `additional_signature_components` | OPTIONAL | Capability omission that prevents resources from using the extension through typed SDK metadata | +| `claims_supported` | RECOMMENDED | SHOULD-grade gap; the PS cannot advertise supported identity claims through typed metadata | + +### B8. Required polling outcomes are incomplete — MEDIUM + +`abandoned`, `expired`, `invalid_code`, and `slow_down` exist in +[PollingError.cs](../../../src/AAuth/Errors/PollingError.cs) and are normative in the +Polling Error Codes table at +[L2270-L2280](../../../aauth-spec/v09/draft-hardt-oauth-aauth-protocol.md), but direct +search finds no endpoint emitting those four strings. `user_unreachable` is likewise +defined but never emitted. + +The table defines wire meanings; it does not require every implementation to produce +every outcome unconditionally. The concrete conformance gap is narrower: + +- B6 requires failed interaction-code attempts to become terminal `invalid_code`, but + no such transition or body exists. +- Expired pending state and polling throttling should use `expired` and `slow_down` + when those states occur; current endpoints use other codes or bare status handling. +- `abandoned` and `user_unreachable` need flow-by-flow evidence before being called + defects, rather than mere currently unattached outcomes. + +### B9. Endpoint-specific error extensions need an explicit inventory — LOW + +The SDK emits additional codes, including: +`policy_unavailable`, `policy_error`, `payment_required`, `untrusted_person_server`, +`untrusted_access_server`, `invalid_carrier`, `invalid_carrier_token`, +`unknown_interaction`, `unknown_pending`, `request_withdrawn`, +`invalid_upstream_token`, `untrusted_fetcher`, `invalid_signature`, +`r3_evaluation_failed`, `r3_denied`, `r3_approval_required`, `auth_token_required`, +`mission_terminated`, and dynamic pending-entry errors. + +These are **not automatically out of spec**. The common format says `error` is a code +"as defined by the endpoint returning the error," and several values come from +endpoint-specific or companion behavior rather than the token/polling tables. The +migration requirement is to inventory them, preserve their statuses and extension +members deliberately, and document which are SDK extensions. Upstream proposals are +appropriate only where interoperability requires a common value. + +### B10. Updated-request resource token identity unvalidated — MEDIUM + +[L1065](../../../aauth-spec/v09/draft-hardt-oauth-aauth-protocol.md) requires a +replacement resource token in a clarification exchange to carry the same `iss`, +`agent`, and `agent_jkt` as the original. The pending-URL POST handler accepts the new +token without comparing any of the three. Shares code with A4. + +### B11. Agent token lifetime is unbounded — MEDIUM + +[L560](../../../aauth-spec/v09/draft-hardt-oauth-aauth-protocol.md) says agent tokens +SHOULD NOT exceed 24 hours. +[AgentTokenBuilder.cs:66](../../../src/AAuth/Tokens/AgentTokenBuilder.cs#L66) defaults +`Lifetime` to one hour but enforces no maximum, so a caller can set 30 days and the +builder emits it. + +Most persuasive as an **internal inconsistency**: the SDK's other two token builders +both enforce their ceilings — +[AuthTokenBuilder.cs:143](../../../src/AAuth/Tokens/AuthTokenBuilder.cs#L143) throws +above 1 hour and +[ResourceTokenBuilder.cs:99](../../../src/AAuth/Tokens/ResourceTokenBuilder.cs#L99) +throws above 5 minutes. Agent tokens are the only one without a guard. + +## Findings: test and documentation contract debt + +New in pass 2, and the category most likely to let an apparently successful migration +ship broken. + +### C1. The conformance suite cannot detect a broken migration + +`tests/AAuth.Conformance/` asserts `application/problem+json` nowhere, and asserts +the clarification `action` discriminator nowhere. (`["action"]` does occur 11 times +in the suite, but every occurrence is the unrelated **governance** `action` field +of permission and audit requests — `"SendEmail"`, `"WebSearch"` — in +`Missions/Governance*Tests.cs`. None is in a clarification test.) + +Multiple tests exercise the clarification POST by checking which *keys are present* — +exactly the inference draft-09 removed: + +| Test | Lines | +|---|---| +| `Missions/ClarificationChatTests.cs` | L81, L201, L207 | +| `Missions/ChallengeClarificationSeamTests.cs` | L201 | +| `Missions/GovernanceClientTests.cs` | L299 | +| `Person/PersonServerMapperTests.cs` | L479, L568, L575 | + +Every cited location passes whether or not `action` is implemented. A migration could be declared +complete with `make test-conformance` green while clarification flows fail against +every conformant peer. + +### C2. Error-shape fixtures encode the removed member + +`Agent/ChallengeHandlerTests.cs` L328/L336 and `AccessServerClientTests.cs` L311 mock +`{"error": ..., "error_description": ...}` and assert on `ErrorDescription`. +`Errors/TokenErrorTests.cs` L37 asserts the property directly — that one at least +fails at compile time under a rename, which is the desirable behaviour. +`Errors/PollingErrorTests.cs` L29/L65/L89 assert error codes over `application/json` +and never check the content type. + +### C3. Documentation teaches the draft-08 shape + +[docs/advanced/error-handling.md](../../../docs/advanced/error-handling.md) shows the +old wire member and public API at L97, L106, L114, L149, and L323. +[docs/advanced/clarification-chat.md](../../../docs/advanced/clarification-chat.md) +describes the agent responses at L43 and L161 without mentioning the required `action` +discriminator. [docs/README.md](../../../docs/README.md) L273 lists the old type names. +No document anywhere shows `application/problem+json` or `detail`. Integrators copying +these snippets will build draft-08 requests that draft-09 servers reject. + +### C4. Existing e2e assertions do not inspect the new wire members + +`tests/e2e/` asserts at the UI level and does not depend on the error wire shape or the +clarification body directly. Existing assertions therefore need no mechanical rename, +but that does **not** make e2e unaffected: the live clarification flow should gain an +assertion or captured-wire check proving `action=clarification_response`, and a full +e2e run remains a migration gate because the producer and consumer cut over together. + +## Migration acceptance evidence + +These are validation requirements, not implementation steps. They are incorporated +from the earlier migration plan so this document remains sufficient to derive a new +implementation plan. + +### RFC 9457 cutover evidence + +- Representative PS, AS, authorization, interaction, governance, challenge, + revocation, and R3 tests assert status, `application/problem+json`, required + `error`, and optional `detail`. +- `TokenExchangeClient` and `AccessServerClient` parse `detail` and continue to branch + only on `error`; optional RFC 9457 `type`, `title`, `status`, and `instance` do not + affect classification. +- Public `ErrorDescription` APIs and compiled consumers move to `Detail` without an + alias or `error_description` fallback, per D1. +- Header-only authentication failures retain `Signature-Error` behavior and do not + acquire an invented JSON body. +- R3 extension members (`r3_uri`, `r3_s256`) and core extension members + (`mission_status`, `id`) survive the media-type cutover. +- A post-change search finds no `error_description` in the migrated SDK, tests, or + documentation. + +### Clarification cutover evidence + +- Captured agent JSON proves both exact shapes: + `action=clarification_response` with `clarification_response`, and + `action=updated_request` with `resource_token`. +- The PS accepts both recognized actions and rejects missing or unknown actions with + `400` problem details. +- A recognized action without its required payload fails defensively as + `invalid_request`; cancellation remains a bodyless `DELETE`. +- Updated requests enforce B10's unchanged `iss`, `agent`, and `agent_jkt` binding. +- Unit and conformance fixtures stop inferring the operation from member presence. +- The live clarification flow exposes the action member in captured wire evidence. + +### Companion initiative evidence + +- Events validation covers every feature group listed in A5, including token + positives and negatives, protected-ticket single use, atomic `max_uses`, durable + acceptance before `202`, replay, expiry, resource/AP/agent integration, and + AsyncAPI discovery. +- R3 AsyncAPI validation proves multi-member operation round trips and the handoff to + Events registration; adding only a vocabulary constant is insufficient. +- `self-jwt` validation covers cheap structural rejection, required `iss`/`dwk`/`kid`, + mandatory `cnf` absence, JWT and HTTP verification with the same key, cache expiry, + SSRF admission, and unknown-key behavior. +- Events `sig=jwt` and Signature Keys `sig=self-jwt` share one admitted discovery + primitive but remain separate parser and verification branches. + +### Repository validation matrix + +Every implementation phase must pass the narrow tests for its touched surface and the +repository gates: + +```bash +make build +make test-unit +make test-conformance +make e2e +``` + +An unavailable e2e environment is a blocker requiring an explicit ruling, not a +silent skip. + +## Corrections to subagent findings + +Recorded because they calibrate how much weight the un-re-verified findings deserve. + +| # | Claim | Correction | +|---|---|---| +| X1 | Scope narrowing is MISSING | **Wrong.** Implemented at [TokenVerifier.cs:166-167, 276](../../../src/AAuth/Tokens/TokenVerifier.cs#L166). Two other subagents independently reported it conformant. The real gap is the other half of L1987 — B2. | +| X2 | Unemitted polling error codes are a DELTA | **Wrong.** The polling table is unchanged from draft-08; reclassified PRE-EXISTING (B8). | +| X3 | Error-format gap is "6 sites" | **Understated.** Six sites use the removed member, but every JSON AAuth error-body producer emits the wrong content type, across 12 verified files (A1). | +| X4 | Clarification `action` rejection is ENFORCED | **Wrong.** Verified: `action` appears nowhere in the PS endpoint or the agent client. Reported ENFORCED on the strength of nearby unrelated code. | +| X5 | Auth token `exp` ceiling is ENFORCED at `AAuthVerificationMiddleware.cs:195` | **Wrong.** That line is a jkt-jwt naming-JWT expiry check. No file compares auth token `exp` to agent token `exp` (B1). | +| X6 | Verified claims are re-read from an unverified payload (trust-boundary defect) | **Overstated — investigated and cleared.** The cited lines are OpenTelemetry tagging. Signature resolution, `_verifier.Verify`, and issuer verification all occur before `AAuthVerificationResult` is populated. Not a defect. | +| X7 | Assorted spec line numbers | **Drifted.** Seven citations were wrong by 5-50 lines. All citations here were re-derived from the requirement text and validated by printing the cited line. | + +X4 and X5 came from the same sweep — the negative-requirements pass. Its inventory of +*which* requirements exist is useful; its ENFORCED verdicts are not reliable without +checking, because the method rewards finding any nearby plausible code. + +## Confirmed conformant + +Recorded so coverage is auditable, and because these were checked with the same rigour +as the failures. + +- **Agent identity and delegation** — all 21 sampled requirements, including identifier + syntax and case-sensitive comparison, the `+` sub-agent delimiter, single-level depth + enforced on both AP and PS sides, and `act.agent` naming the immediate delegator. +- **Call chaining** — upstream token `aud` verified equal to the intermediary agent + token's `iss`. +- **Missions** — `s256` over exact received bytes with no re-serialisation; `approver` + validated as a server identifier; reference copied unchanged into resource and auth + tokens. +- **AS federation** — all seven auth-token delivery verification steps. +- **R3 content addressing** — `R3Hash` computes `r3_s256` over exact bytes and the + document endpoint serves the stored bytes without canonicalisation or + re-serialisation. +- **R3 protected fetch** — `R3DocumentEndpoint` requires a verified AS HTTP signature + before returning an R3 document; unauthenticated agent fetches are rejected. +- **R3 claim pairing** — `r3_uri` and `r3_s256` are required together and preserved in + the conditional-approval flow; `r3_uri` and `r3_s256` on + `r3_approval_required` remain intentional RFC 9457 extension members after A1. +- **HTTP message signatures** — the four mandated covered components in order; agent + keying material restricted to `scheme=jwt`, with `hwk` and `jkt-jwt` rejected for + agent and auth tokens. +- **Client behaviour** — request construction, `Retry-After` handling with the + 5-second default, linear 429 backoff, unrecognised `status` treated as pending, + challenge handling, and tolerant header parsing. The identified client defects are + A3's stale error-member parser and A4's stale clarification serializer. +- **Token discipline** — `none` rejected; `typ` and `dwk` verified on every token type; + auth tokens capped at 1 hour and resource tokens at 5 minutes; `cnf` + proof-of-possession enforced. (Agent token ceiling is the exception — B11.) +- **Metadata issuer binding** — a document whose `issuer` does not match its fetch URL + is rejected. +- **Replay** — keyed on the verified signature within the freshness window rather than + token `jti`, which is the correct scoping. +- **Interaction codes** — Crockford base32, ≥40 bits entropy, hyphen stripping, glyph + folding, case-insensitive comparison, single-use consume, and documented as a + correlation identifier rather than an authorization credential. +- **IANA registrations** — all three `typ` values and all six registered claim names + match the spec strings exactly. +- **Verification ordering** — claims exposed on `AAuthVerificationResult` are read + after signature resolution and JWT verification (see X6). + +## Gaps and open questions + +1. **Is the resource-side half of B1 implementable as written?** The resource never + sees the agent token when an auth token is presented. Either the SDK lacks a + mechanism or the spec clause needs an upstream question. +2. **Which B8 outcomes are reachable in the current SDK?** `invalid_code` is required + by B6; `expired`, `slow_down`, `abandoned`, and `user_unreachable` need state-machine + evidence before their absence is classified as a defect. +3. **Which B9 codes need standardization?** Preserve endpoint-specific values during + the media-type cutover, then propose only values whose cross-implementation + interpretation is necessary. +4. **How far should B3 go?** Supporting the multi-member forms of GraphQL, AsyncAPI, + WSDL, and OData means reshaping `R3Operation` — a public API change in a preview + package. gRPC already fits the generic one-member model. +5. **Does B6 warrant an SDK seam** (an `IInteractionAttemptLimiter`-style contract), or + is documenting the deployment obligation sufficient given it is two MUSTs? +6. **Should C1 be fixed before the migration rather than with it?** Tests that cannot + fail are worse than absent tests, because they are read as assurance. + +## Out of scope + +| Item | Reason | +|---|---| +| `src/AAuth.Events/` | Under separate review in PR #45 | +| `samples/` | Being reverted on the current branch | +| Vendoring a newer spec revision | draft-09 is latest; signature-key draft-07 exists but draft-09 cites draft-06 | +| Upstream specification edits | Draft maintenance is outside this repository | diff --git a/.github/instructions/update-spec.instructions.md b/.github/instructions/update-spec.instructions.md index 8b416cb..88be73a 100644 --- a/.github/instructions/update-spec.instructions.md +++ b/.github/instructions/update-spec.instructions.md @@ -42,6 +42,7 @@ Each `v/` snapshot contains: | `draft-hardt-oauth-aauth-protocol.md` | AAuth repo | The protocol spec; names the snapshot. | | `draft-hardt-aauth-bootstrap.md` | AAuth repo | Informational; often unchanged across drafts. | | `draft-hardt-aauth-r3.md` | AAuth repo | Rich Resource Requests; has its own version. | +| `draft-hardt-aauth-events.md` | AAuth repo | AAuth Events; standalone draft. Include when present upstream. | | `interop-demo-profile.md` | AAuth repo | Informational; new since draft-06. Include when present upstream. | | `draft-hardt-httpbis-signature-key-.txt` | IETF archive | Lives in the separate `dickhardt/signature-key` repo; vendor the IETF `.txt` at the version the protocol references. | @@ -63,8 +64,21 @@ curl -s "https://api.github.com/repos/dickhardt/AAuth/git/ref/tags/$TAG" | grep curl -s "https://api.github.com/repos/dickhardt/AAuth/contents?ref=$TAG" | grep '"name"' ``` -Record the commit SHA, commit date, document date (the `date = ` field in the -spec frontmatter), and the tag name — they all go into `SPEC-VERSION.md`. +Record the commit SHA, commit date, document date, and the tag name — they all go +into `SPEC-VERSION.md`. + +> **Take the document date from the published Internet-Draft, not the kramdown +> `date = ` frontmatter.** Upstream routinely leaves the frontmatter stale (both +> draft-08 and draft-09 ship with `date = 2026-06-17` while the published +> revisions are dated 2026-06-24 and 2026-07-04). Read it off the `.txt`: + +```bash +curl -s "https://www.ietf.org/archive/id/draft-hardt-oauth-aauth-protocol-$NN.txt" \ + | sed -n '1,12p' | grep -oE '[0-9]{1,2} [A-Z][a-z]+ 20[0-9]{2}' | head -1 +``` + +For companion drafts not yet on the Datatracker (currently R3 and AAuth Events), +the frontmatter is the only available source — use it, and say so in the entry. ### 2. Download the snapshot @@ -75,9 +89,19 @@ TAG=draft-hardt-oauth-aauth-protocol-$NN BASE="https://raw.githubusercontent.com/dickhardt/AAuth/$TAG" mkdir -p "v$NN" for f in draft-hardt-oauth-aauth-protocol.md draft-hardt-aauth-bootstrap.md \ - draft-hardt-aauth-r3.md interop-demo-profile.md; do + draft-hardt-aauth-r3.md; do curl -fsSL "$BASE/$f" -o "v$NN/$f" done + +# Optional standalone/informational docs; include when present upstream. +for f in draft-hardt-aauth-events.md interop-demo-profile.md; do + if curl -fsSL "$BASE/$f" -o "v$NN/$f"; then + echo "Downloaded optional $f" + else + rm -f "v$NN/$f" + echo "Optional $f not present upstream; record as absent." + fi +done ``` For the HTTP Signature Keys dependency, vendor the IETF `.txt` at the version the @@ -98,6 +122,9 @@ Record "unchanged" docs explicitly so each snapshot stays auditable. ```bash diff -q v02/draft-hardt-aauth-bootstrap.md v08/draft-hardt-aauth-bootstrap.md diff -q v02/draft-hardt-aauth-r3.md v08/draft-hardt-aauth-r3.md +if [ -f v02/draft-hardt-aauth-events.md ] && [ -f v08/draft-hardt-aauth-events.md ]; then + diff -q v02/draft-hardt-aauth-events.md v08/draft-hardt-aauth-events.md +fi # Protocol structural delta (added/removed sections): diff <(grep -nE '^#{1,2} ' v02/draft-hardt-oauth-aauth-protocol.md | sed 's/^[0-9]*://') \ <(grep -nE '^#{1,2} ' v08/draft-hardt-oauth-aauth-protocol.md | sed 's/^[0-9]*://') @@ -110,7 +137,8 @@ diff <(grep -nE '^#{1,2} ' v02/draft-hardt-oauth-aauth-protocol.md | sed 's/^[0- - Append a `## \`v/\` — protocol draft-NN` section (sections are in **ascending** order). Include a metadata table (source repo, commit SHA, commit date, tagged version, document date, IETF draft URL, copied-on date) and a per-file list - noting which docs are unchanged from the prior snapshot. + noting which docs are unchanged from the prior snapshot, including AAuth Events + when present. - Add a short "Notable changes since draft-XX" highlight list. ### 5. Update `CHANGELOG.md` @@ -120,7 +148,7 @@ diff <(grep -nE '^#{1,2} ' v02/draft-hardt-oauth-aauth-protocol.md | sed 's/^[0- per-snapshot sections (newest first). - Summarize the delta by theme, then reproduce the **author's verbatim per-draft changelog** from the protocol's `# Document History` section for every draft - spanned. Note unchanged R3/bootstrap, any new documents, and the + spanned. Note unchanged R3/bootstrap/AAuth Events, any new documents, and the signature-key version bump. - Reference the spec's own kramdown anchors (e.g. `#sub-agents`) rather than line numbers where possible — anchors are stable across line shifts. @@ -142,6 +170,7 @@ permanent home and survives the repo being moved, renamed, or deprecated: - Document page (all revisions) — - Per-revision text — `https://www.ietf.org/archive/id/draft-hardt-oauth-aauth-protocol-.txt` - Per-revision HTML — `https://datatracker.ietf.org/doc/html/draft-hardt-oauth-aauth-protocol-` +- AAuth Events editor's copy — If the GitHub repo is unavailable, vendor the Datatracker `.txt` rendering for that revision instead of the kramdown `.md`, and note the substitution in the snapshot's diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a439077..ea327e7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -39,6 +39,7 @@ jobs: run: | dotnet pack src/AAuth/AAuth.csproj -c Release -p:PackageVersion=${{ inputs.version }} -o ./nupkg dotnet pack src/AAuth.R3/AAuth.R3.csproj -c Release -p:PackageVersion=${{ inputs.version }} -o ./nupkg + dotnet pack src/AAuth.Events/AAuth.Events.csproj -c Release -p:PackageVersion=${{ inputs.version }} -o ./nupkg - name: NuGet login (OIDC trusted publishing) if: ${{ inputs.dry-run == false }} diff --git a/AAuth.slnx b/AAuth.slnx index d0b0e44..71efbe3 100644 --- a/AAuth.slnx +++ b/AAuth.slnx @@ -2,6 +2,7 @@ + @@ -23,10 +24,12 @@ + + diff --git a/Makefile b/Makefile index b83d827..c7a824e 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ PS_PROJECT := samples/MockPersonServer/MockPersonServer.csproj AP_PROJECT := samples/MockAgentProvider/MockAgentProvider.csproj TOUR_PROJECT := samples/GuidedTour/GuidedTour.csproj AGENT_PROJECT := samples/AgentConsole/AgentConsole.csproj +EVENT_AGENT_PROJECT := samples/EventAgent/EventAgent.csproj SAMPLE_PROJECT := samples/SampleApp/SampleApp.csproj CONCIERGE_PROJECT := samples/Concierge/Concierge.csproj LIVE_PROJECT := samples/LiveWhoAmITest/LiveWhoAmITest.csproj @@ -43,6 +44,7 @@ KEYCLOAK_REALM := samples/MockAccessServers/Federated/keycloak # AgentConsole persists its enrollment under $LocalApplicationData; the MockAgentProvider # keeps its agent registry in memory, so the cache goes stale whenever the AP restarts. AGENT_CACHE_DIR := $(or $(XDG_DATA_HOME),$(HOME)/.local/share)/aauth-agent-console +EVENT_AGENT_CACHE_DIR := $(or $(XDG_DATA_HOME),$(HOME)/.local/share)/aauth-event-agent E2E_DIR := tests/e2e # Environment that points the MockAccessServer at the live Keycloak policy engine. @@ -71,6 +73,7 @@ endef .PHONY: help build restore test test-unit test-conformance format clean \ resources ps ps-consent ap concierge tour sampleapp agent live \ + events-stack agent-events event-agent-reset \ demo demo-mission agent-mission \ keycloak access-server demo-keycloak \ agent-federated agent-reset \ @@ -144,6 +147,24 @@ sampleapp: ## Run the SampleApp Blazor app (port 5240) agent: ## Run AgentConsole against the Profile server (override URL=… for a different target) $(DOTNET) run --project $(AGENT_PROJECT) -- $(or $(URL),$(PROFILE_URL)) +events-stack: ## Run the focused AAuth Events stack (AP + PS + R3 AS + Bookings) + @echo "Building the Events stack..." + @$(DOTNET) build $(SOLUTION) -v q + @echo "Run 'make agent-events' in another terminal." + @trap 'trap - INT TERM; echo; echo "Stopping..."; kill 0' INT TERM; \ + MockPersonServer__TrustedAccessServers__0=$(R3AS_URL) $(DOTNET) run --no-build --project $(PS_PROJECT) & \ + $(DOTNET) run --no-build --project $(AP_PROJECT) & \ + $(DOTNET) run --no-build --project $(R3AS_PROJECT) & \ + $(DOTNET) run --no-build --project $(BOOKINGS_PROJECT) & \ + wait + +agent-events: ## Run EventAgent through the protected Bookings waitlist flow + $(DOTNET) run --project $(EVENT_AGENT_PROJECT) -- \ + --ap $(AP_URL) --bookings $(BOOKINGS_URL) --ps $(PS_URL) + +event-agent-reset: ## Clear EventAgent enrollment metadata after an AP restart + @rm -rf "$(EVENT_AGENT_CACHE_DIR)" && echo "Cleared EventAgent cache ($(EVENT_AGENT_CACHE_DIR))." + live: ## Run LiveWhoAmITest against whoami.aauth.dev (needs cloudflared + network) $(DOTNET) run --project $(LIVE_PROJECT) @@ -171,6 +192,7 @@ demo: ## Start the full stack + stub Access Server + both UIs (all flows incl. f @echo " Open in your browser:" @echo " GuidedTour: $(TOUR_URL) (step-by-step walkthrough of every flow)" @echo " SampleApp: $(SAMPLE_URL) (minimal app: /federated, /deferred, /callchain)" + @echo " EventAgent CLI: run 'make agent-events' in another terminal" @echo "------------------------------------------------------------------" @echo "" @echo "Building services (once) before launch..." diff --git a/README.md b/README.md index bfc0332..586809b 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ The four parties are: The SDK supports all four signing modes (`hwk`, `jwks_uri`, `jwt`, `jkt-jwt`), the full three-party challenge/exchange flow (autonomous and deferred user-consent), signature verification middleware, resource & auth token builders, JWKS / metadata discovery, and a Blazor `GuidedTour` walk-through. See the [SDK documentation](docs/) for complete usage guides. +Experimental draft extensions ship as complementary preview packages rather +than expanding core: [`AAuth.R3`](src/AAuth.R3/) for Rich Resource Requests and +[`AAuth.Events`](src/AAuth.Events/) for asynchronous subscription and delivery. + ## Access Modes AAuth supports four resource access modes. Each adds parties and capabilities, and they build on one another — adoption is incremental. Run `make demo` (no Docker) to start every service plus both UIs, then follow the demo column below. For the live-Keycloak federated experience, use `make demo-keycloak`. @@ -66,6 +70,11 @@ For the live-Keycloak federated experience, run `make demo-keycloak` instead. Se [samples/README.md](samples/README.md) for the full list of sample projects and configuration options. +For the AAuth Events waitlist flow, run `make events-stack`, then +`make agent-events` in another terminal. EventAgent demonstrates protected +registration, AP inbox delivery, verification, deduplication, and explicitly +non-normative polling/ACK. + ### Dev container (recommended) Open this repo in VS Code → **Reopen in Container**. The container @@ -85,6 +94,13 @@ dotnet build AAuth.slnx dotnet add package AAuth --prerelease ``` +Optional experimental extensions are installed separately: + +```bash +dotnet add package AAuth.R3 --prerelease +dotnet add package AAuth.Events --prerelease +``` + The simplest mode is **pseudonymous (HWK)** — the agent signs every request with an inline public key. No Agent Provider, no Person Server, no registration. The resource sees a stable key thumbprint it can use for rate-limiting or access control, but doesn't know the agent's identity. ```csharp @@ -247,6 +263,8 @@ Full SDK documentation lives in [`docs/`](docs/): - [Glossary & Acronyms](docs/glossary.md) — every acronym and short protocol term used across the repo - [Signing Modes](docs/signing-modes/overview.md) — hwk, jwks_uri, jwt, jkt-jwt - [Workflows](docs/workflows/identity-based-access.md) — identity-based, PS-asserted, federated +- [Rich Resource Requests](docs/workflows/rich-resource-requests.md) — optional `AAuth.R3` preview +- [AAuth Events](docs/workflows/aauth-events.md) — optional `AAuth.Events` preview - [Server Guide](docs/server/verification-middleware.md) — verification middleware, token issuance - [Configuration Reference](docs/reference/configuration.md) @@ -256,6 +274,7 @@ Full SDK documentation lives in [`docs/`](docs/): dotnet test AAuth.slnx # full suite (unit + conformance) dotnet test tests/AAuth.Tests # SDK unit + integration tests only dotnet test tests/AAuth.Conformance # spec conformance suite only +dotnet test tests/AAuth.Events.Tests # AAuth Events unit + conformance tests ``` ## Repository Layout @@ -263,8 +282,10 @@ dotnet test tests/AAuth.Conformance # spec conformance suite only | Path | Description | |------|-------------| | [src/AAuth/](src/AAuth/) | AAuth SDK library (the NuGet package) | +| [src/AAuth.R3/](src/AAuth.R3/) | Optional Rich Resource Requests preview package | +| [src/AAuth.Events/](src/AAuth.Events/) | Optional AAuth Events preview package | | [docs/](docs/) | SDK documentation — signing modes, workflows, server guides | -| [samples/](samples/) | Sample applications — Profile, Calendar, Trips, Wallet, Inbox resource servers, Concierge, AgentConsole, MockPersonServer, MockAgentProvider, GuidedTour, SampleApp | +| [samples/](samples/) | Sample applications — resource servers, AgentConsole, EventAgent, MockPersonServer, MockAgentProvider, GuidedTour, and SampleApp | | [tests/](tests/) | Unit, integration, and spec-conformance tests | | [aauth-spec/](aauth-spec/) | Protocol specifications (drafts 01, 02, and 08) from [dickhardt/AAuth](https://github.com/dickhardt/AAuth) | @@ -277,6 +298,7 @@ This SDK targets **draft-08** of the AAuth protocol specification: | [draft-hardt-oauth-aauth-protocol](aauth-spec/v08/draft-hardt-oauth-aauth-protocol.md) | 08 | | [draft-hardt-aauth-bootstrap](aauth-spec/v08/draft-hardt-aauth-bootstrap.md) | 01 | | [draft-hardt-aauth-r3](aauth-spec/v08/draft-hardt-aauth-r3.md) | 00 | +| [draft-hardt-aauth-events](aauth-spec/v09/draft-hardt-aauth-events.md) | 00 (experimental extension) | The protocol tracks IETF **draft-08** ([`aauth-spec/v08/`](aauth-spec/v08/), source commit [`dd2b852`](https://github.com/dickhardt/AAuth/commit/dd2b8524eb8a6beb1a6cd922f285cc8bd0464cd8), 2026-06-25). Earlier draft-02 ([`aauth-spec/v02/`](aauth-spec/v02/)) and draft-01 ([`aauth-spec/v01/`](aauth-spec/v01/)) snapshots are retained for reference. All four resource access modes — including the `AAuth-Access` opaque-token flow (resource-managed, two-party access) — are implemented. See [SPEC-VERSION.md](aauth-spec/SPEC-VERSION.md) and [aauth-spec/CHANGELOG.md](aauth-spec/CHANGELOG.md) for details. diff --git a/aauth-spec/CHANGELOG.md b/aauth-spec/CHANGELOG.md index a751108..4388c33 100644 --- a/aauth-spec/CHANGELOG.md +++ b/aauth-spec/CHANGELOG.md @@ -7,28 +7,39 @@ snapshots in this repository. Each snapshot is a self-contained folder; see [`SPEC-VERSION.md`](SPEC-VERSION.md) for source commits and metadata. Entries are grouped by snapshot folder, so every document in a release (protocol, -R3, bootstrap) is listed together and a change to R3 or bootstrap travels with the -protocol version it shipped in. +R3, bootstrap, AAuth Events, and informational profiles) is listed together and a +change to a companion document travels with the protocol version it shipped in. Section and line references in the **`v02/`** entry point into the draft-02 files under [`v02/`](v02/) (commit `feda56b`); references in the **`v08/`** entry point -into the draft-08 files under [`v08/`](v08/) (commit `dd2b852`). Anchors in -parentheses (e.g. `#sub-agents`) are the spec's own kramdown anchors and are stable -across line shifts. - -| Snapshot | Protocol | Bootstrap | R3 | Source commit | -|---|---|---|---|---| -| [`v01/`](v01/) | draft-01 | draft-01 | draft-00 | `c090879` (2026-05-11) | -| [`v02/`](v02/) | draft-02 | draft-01 (unchanged) | draft-00 (revised) | `feda56b` (2026-06-09) | -| [`v08/`](v08/) | draft-08 | draft-01 (unchanged) | draft-00 (unchanged) | `dd2b852` (2026-06-25) | - -> **The SDK code targets `v02/` (draft-02).** The `v08/` snapshot was vendored -> 2026-06-25 as the latest upstream reference; migrating the SDK to draft-08 is -> tracked separately and has not started. See +into the draft-08 files under [`v08/`](v08/) (commit `dd2b852`); references in the +**`v09/`** entry point into the draft-09 files under [`v09/`](v09/) (commit +`90089f8`). Anchors in parentheses (e.g. `#sub-agents`) are the spec's own +kramdown anchors and are stable across line shifts. + +| Snapshot | Protocol | Bootstrap | R3 | Events | Source commit | +|---|---|---|---|---|---| +| [`v01/`](v01/) | draft-01 | draft-01 | draft-00 | — | `c090879` (2026-05-11) | +| [`v02/`](v02/) | draft-02 | draft-01 (unchanged) | draft-00 (revised) | — | `feda56b` (2026-06-09) | +| [`v08/`](v08/) | draft-08 | draft-01 (unchanged) | draft-00 (unchanged) | — | `dd2b852` (2026-06-25) | +| [`v09/`](v09/) | draft-09 | draft-01 (unchanged) | draft-00 (revised) | draft-00 (new) | `90089f8` (2026-07-05) | + +> **The SDK code targets `v08/` (draft-08).** The `v09/` snapshot was vendored +> 2026-07-14 as the latest upstream reference; it does not change SDK behavior. See > [`SPEC-VERSION.md`](SPEC-VERSION.md). ## Contents +- [`v09/` — AAuth draft-09 snapshot](#v09--aauth-draft-09-snapshot) + - [Protocol (draft-09)](#protocol-draft-09) + - [1. Clarification response discriminator](#1-clarification-response-discriminator) + - [2. RFC 9457 problem details](#2-rfc-9457-problem-details) + - [3. AAuth Events integration](#3-aauth-events-integration) + - [AAuth Events (draft-00, new)](#aauth-events-draft-00-new) + - [R3 (draft-00, revised in place)](#r3-draft-00-revised-in-place) + - [Bootstrap and Interoperability Demo Profile (unchanged)](#bootstrap-and-interoperability-demo-profile-unchanged) + - [HTTP Signature Keys (draft-06)](#http-signature-keys-draft-06) + - [Author's verbatim changelog (draft-09)](#authors-verbatim-changelog-draft-09) - [`v08/` — AAuth draft-08 snapshot](#v08--aauth-draft-08-snapshot) - [Protocol (drafts 03–08)](#protocol-drafts-0308) - [1. Agent-delegation restructure](#1-agent-delegation-restructure) @@ -56,9 +67,133 @@ across line shifts. --- +## `v09/` — AAuth draft-09 snapshot + +The latest upstream snapshot, vendored 2026-07-14. It bundles protocol +**draft-09** with the newly added standalone AAuth Events **draft-00**, the +revised-in-place R3 **draft-00**, unchanged bootstrap **draft-01**, the unchanged +Interoperability Demo Profile, and HTTP Signature Keys **draft-06**. + +> **The SDK remains on draft-08.** The entries below measure draft-09 against the +> prior [`v08/`](v08/) snapshot for reference; no SDK migration is included. + +### Protocol (draft-09) + +Published as IETF +[draft-hardt-oauth-aauth-protocol-09](https://datatracker.ietf.org/doc/draft-hardt-oauth-aauth-protocol/09/) +(commit `90089f8`, document date 2026-07-04). The delta from draft-08 has three +themes; the author's verbatim draft-09 changelog is reproduced below. + +#### 1. Clarification response discriminator + +POST responses to a clarification at the pending URL are now self-describing: + +- Every POST body MUST contain an **`action`** member. +- `action: "clarification_response"` accompanies `clarification_response`; + `action: "updated_request"` accompanies a replacement `resource_token`. +- A server MUST reject a missing or unrecognized `action` with `400 Bad Request`. +- The clarification and updated-request examples now include the discriminator. + +#### 2. RFC 9457 problem details + +AAuth error bodies now use the common error response format at +`#error-response-format`: + +- `Content-Type` changed from `application/json` to + **`application/problem+json`** under RFC 9457. +- The AAuth **`error`** code is a REQUIRED problem-details extension member; + receivers use it to decide how to proceed. +- RFC 9457 **`detail`** replaces `error_description`; standard problem members + (`type`, `title`, `status`, `instance`) MAY also be present. +- Token-endpoint and polling examples were added, and affected endpoint text and + the mission-termination example now use the common format. + +#### 3. AAuth Events integration + +The protocol now links the newly standalone Events draft into the AAuth family: + +- Asynchronous event delivery is listed as a protocol capability, with the AP + acting as the agent's event router. +- AP metadata adds the optional **`event_endpoint`** field, required when the AP + supports AAuth Events. +- The IANA discussion references the Events-defined **`aa-subscribe+jwt`** and + **`aa-event+jwt`** type values. + +### AAuth Events (draft-00, new) + +[`draft-hardt-aauth-events.md`](v09/draft-hardt-aauth-events.md) is a new +standalone editor's draft (document date 2026-06-24). Its Document History labels +it `draft-hardt-aauth-events-00` and records “Initial draft.” No Datatracker +revision had been published as of the 2026-07-14 snapshot, so the file is pinned +to the protocol tag's source commit. + +The draft defines: + +- A four-phase **AP-as-inbox** flow: subscribe-token acquisition, subscription + registration, resource-to-AP event delivery, and platform-dependent AP-to-agent + delivery. +- AP-issued **subscribe tokens** (`typ: aa-subscribe+jwt`) carrying agent + identity, resource audience, confirmation key, event ID (`eid`), expiry, and + optional `max_uses`; agents present them as the `Signature-Key` JWT. +- Public subscriptions and protected subscriptions using a short-lived, + single-use, agent-bound **subscription ticket URL** from an earlier authorized + resource interaction. +- Resource-issued **event tokens** (`typ: aa-event+jwt`) carrying resource + identity, agent audience, `eid`, and response-window expiry. Resources send the + token and optional AsyncAPI-defined payload to the AP, which validates and + durably records delivery before returning `202 Accepted`. +- Event discovery through the R3 + **`urn:aauth:vocabulary:asyncapi`** vocabulary and an `aauth-subscribe` + AsyncAPI security scheme. +- Replay, token-scope, ticket, intermediary, enumeration, and payload-privacy + considerations, plus JWT type and R3 vocabulary registration requests. + +### R3 (draft-00, revised in place) + +R3 retains its draft-00 label and 2026-03-24 document date but differs from +`v08/`: + +- It adds a reference to AAuth Events. +- The AsyncAPI vocabulary now describes resources that **emit events**, makes + its `action` member optional, and notes that subscribing agents use `receive`. +- An R3 grant for an AsyncAPI subscription operation is connected explicitly to + the AAuth Events subscription-ticket and subscribe-token flow. + +### Bootstrap and Interoperability Demo Profile (unchanged) + +- **Bootstrap** remains draft-01 and is byte-identical to `v08/`. +- **Interoperability Demo Profile** is byte-identical to `v08/`. + +### HTTP Signature Keys (draft-06) + +Bumped **draft-05 → draft-06** +([`draft-hardt-httpbis-signature-key-06.txt`](v09/draft-hardt-httpbis-signature-key-06.txt)), +the revision cited by the published protocol draft-09. Draft-06 adds the +`self-jwt` standalone scheme: the self-issued JWT's signing key, discovered via +`{iss}/.well-known/{dwk}`, is reused as the HTTP signing key without a `cnf` +claim. + +### Author's verbatim changelog (draft-09) + +Reproduced from the Document History section of +[`v09/draft-hardt-oauth-aauth-protocol.md`](v09/draft-hardt-oauth-aauth-protocol.md): + +> **draft-hardt-oauth-aauth-protocol-09** +> +> - Clarification chat: added a required `action` discriminator +> (`clarification_response` / `updated_request`) to the agent's POST responses +> on the pending URL, so the response type is explicit rather than inferred +> from key presence. +> - Error responses: adopted RFC 9457 problem details — error bodies use +> `Content-Type: application/problem+json` with the AAuth error code as a +> required `error` extension member; `error_description` replaced by the RFC +> 9457 `detail` member; added token endpoint and polling error examples. + +--- + ## `v08/` — AAuth draft-08 snapshot -The latest upstream snapshot, vendored 2026-06-25 for reference. It bundles +The prior upstream snapshot, vendored 2026-06-25 for reference. It bundles protocol **draft-08** with the unchanged R3 (**draft-00**) and bootstrap (**draft-01**), adds the new informational **Interoperability Demo Profile**, and bumps the HTTP Signature Keys draft to **draft-05**. @@ -73,7 +208,7 @@ bumps the HTTP Signature Keys draft to **draft-05**. Published as IETF [draft-hardt-oauth-aauth-protocol-08](https://datatracker.ietf.org/doc/draft-hardt-oauth-aauth-protocol/08/) -(commit `dd2b852`, document date 2026-06-17). draft-08 is the cumulative result of +(commit `dd2b852`, document date 2026-06-24). draft-08 is the cumulative result of six published drafts (03 → 08). Grouped below by theme; the author's verbatim per-draft changelog is reproduced at the end. Anchors in parentheses are the spec's own kramdown anchors. diff --git a/aauth-spec/SPEC-VERSION.md b/aauth-spec/SPEC-VERSION.md index 4046a27..d6ef6da 100644 --- a/aauth-spec/SPEC-VERSION.md +++ b/aauth-spec/SPEC-VERSION.md @@ -2,10 +2,10 @@ These spec files were copied from the [AAuth](https://github.com/dickhardt/AAuth) repository for reference while building the .NET samples. They are grouped by the -AAuth protocol draft version under [`v01/`](v01/), [`v02/`](v02/), and -[`v08/`](v08/). Each folder is a self-contained snapshot, so each carries its own -copy of the HTTP Signature Keys draft at the version that snapshot's protocol -references. +AAuth protocol draft version under [`v01/`](v01/), [`v02/`](v02/), +[`v08/`](v08/), and [`v09/`](v09/). Each folder is a self-contained snapshot, so +each carries its own copy of the HTTP Signature Keys draft at the version that +snapshot's protocol references. The GitHub repository is the working source we vendor from. The canonical, permanent home is the **IETF Datatracker**, which retains every published revision @@ -13,12 +13,18 @@ permanent home is the **IETF Datatracker**, which retains every published revisi deprecated. Use it as the source of record and fallback: - Datatracker document — -- Per-revision text (example) — +- Per-revision text (example) — The vendored `.md` files are the upstream kramdown source; if the GitHub repo is unavailable, the Datatracker `.txt`/`.html` renderings are the authoritative substitute. +**Document dates below come from the published Internet-Draft, not the vendored +kramdown frontmatter.** Upstream leaves the frontmatter `date` field stale — both +the draft-08 and draft-09 sources still read `2026-06-17`, while the published +revisions are dated 2026-06-24 and 2026-07-04 respectively. Read the date off the +Datatracker `.txt` rendering when recording a new snapshot. + The SDK code now targets **draft-08** ([`v08/`](v08/)) — migrated from draft-02 in the 2026-06-25 migration (see `.agent/plans/2026-06-25-aauth-v08-spec-migration/`). All four resource access modes are implemented, including the `AAuth-Access` @@ -27,8 +33,9 @@ opaque-token flow (resource-managed, two-party access), added under sub-agent (S5) interop demo is deferred, though the parent-mediated code path is implemented and conformance-tested. -`v08/` is therefore both the version the SDK conforms to and the latest upstream -snapshot, vendored 2026-06-25. The earlier draft-02 ([`v02/`](v02/)) and draft-01 +`v08/` remains the version the SDK conforms to. The latest upstream reference is +draft-09 ([`v09/`](v09/)), vendored 2026-07-14; it includes the newly added AAuth +Events draft. The earlier draft-02 ([`v02/`](v02/)) and draft-01 ([`v01/`](v01/)) snapshots are retained for reference. For a high-fidelity record of what changed between snapshots, see @@ -96,7 +103,7 @@ For a high-fidelity record of what changed between snapshots, see | Commit | `dd2b8524eb8a6beb1a6cd922f285cc8bd0464cd8` | | Commit date | 2026-06-25 | | Tagged version | `draft-hardt-oauth-aauth-protocol-08` | -| Document date | 2026-06-17 | +| Document date | 2026-06-24 (per the published Internet-Draft) | | IETF draft | | | Copied on | 2026-06-25 | @@ -138,3 +145,55 @@ draft-08 bundles six published protocol drafts (03 → 08). The headline deltas: - **New `## PS Approval Endpoint Authentication`** section and an implementation-clarity pass (draft-06): `AAuth-Requirement`/`AAuth-Access`/`AAuth-Capabilities` grammar, JWKS same-`kid` refresh, and structured `cnf.jwk` verification ordering. + +## `v09/` — protocol draft-09 + +> This is the latest upstream reference snapshot. The SDK still targets draft-08; +> vendoring draft-09 does not migrate SDK behavior. + +| Field | Value | +|---|---| +| Source repository | | +| Commit | `90089f80eaccccbd22e32e06946e2aa08f7d67fe` | +| Commit date | 2026-07-05 | +| Tagged version | `draft-hardt-oauth-aauth-protocol-09` | +| Protocol document date | 2026-07-04 (per the published Internet-Draft) | +| AAuth Events document date | 2026-06-24 (per the draft's kramdown frontmatter — not yet published as an Internet-Draft) | +| IETF draft | | +| Copied on | 2026-07-14 | + +- `draft-hardt-oauth-aauth-protocol.md` — Main AAuth protocol specification + (draft-09) +- `draft-hardt-aauth-bootstrap.md` — Agent bootstrap guidance (draft-01, + byte-identical to `v08/`) +- `draft-hardt-aauth-r3.md` — Rich Resource Requests (R3) specification (draft-00, + revised from `v08/` to connect its AsyncAPI vocabulary to AAuth Events) +- `draft-hardt-aauth-events.md` — AAuth Events specification (draft-00; **new** in + this snapshot). The tagged editor's copy is pinned by the snapshot commit; no + IETF Datatracker revision had been published as of the copied-on date. +- `interop-demo-profile.md` — Interoperability Demo Profile (informational, + byte-identical to `v08/`) +- `draft-hardt-httpbis-signature-key-06.txt` — HTTP Signature Keys + (Internet-Draft, draft-06; bumped from draft-05 in `v08/`). Downloaded + 2026-07-14 from + + (Internet-Draft, 2 July 2026 revision). The published protocol draft-09 cites + this revision. + +### Notable changes since draft-08 + +- **AAuth Events draft-00** adds AP-routed asynchronous events, subscribe and + event tokens, public and protected subscription registration, and AsyncAPI/R3 + discovery. +- **Protocol integration for Events** adds the AP `event_endpoint` metadata field, + describes the AP's event-router role, and references the `aa-subscribe+jwt` and + `aa-event+jwt` types. +- **Clarification responses** now require an explicit `action` discriminator with + `clarification_response` or `updated_request`. +- **Error responses** now use RFC 9457 problem details with + `Content-Type: application/problem+json`, the AAuth `error` extension member, + and `detail` instead of `error_description`. +- **R3 AsyncAPI vocabulary** now links granted event operations to the AAuth + Events subscription-ticket and subscribe-token flow. +- **HTTP Signature Keys draft-06** adds the `self-jwt` scheme for a self-issued + JWT whose discovered signing key also verifies the HTTP message signature. diff --git a/aauth-spec/v09/draft-hardt-aauth-bootstrap.md b/aauth-spec/v09/draft-hardt-aauth-bootstrap.md new file mode 100644 index 0000000..39b27f8 --- /dev/null +++ b/aauth-spec/v09/draft-hardt-aauth-bootstrap.md @@ -0,0 +1,411 @@ +%%% +title = "AAuth Bootstrap Guidance" +abbrev = "AAuth-Bootstrap" +ipr = "trust200902" +area = "Security" +workgroup = "TBD" +keyword = ["agent", "authorization", "bootstrap", "http", "identity"] +category = "info" + +[seriesInfo] +status = "informational" +name = "Internet-Draft" +value = "draft-hardt-aauth-bootstrap-latest" +stream = "IETF" + +date = 2026-05-06T00:00:00Z + +[[author]] +initials = "D." +surname = "Hardt" +fullname = "Dick Hardt" +organization = "Hellō" + [author.address] + email = "dick.hardt@gmail.com" + +%%% + + + + AAuth Protocol + + Hellō + + + + + + + + HTTP Signature Keys + + Hellō + + + + + + + + Web Authentication: An API for accessing Public Key Credentials - Level 3 + + W3C + + + + + + + + Establishing your app's integrity (App Attest) + + Apple + + + + + + + + Play Integrity API + + Google + + + + + + + + Web Cryptography API + + W3C + + + + + + +.# Abstract + +This document provides informational guidance for agent providers (APs) on enrolling agents and issuing AAuth agent tokens defined in [@!I-D.hardt-oauth-aauth-protocol]. It covers per-platform key handling, optional platform attestation, agent identifier strategies, and refresh patterns. The mechanisms described here are not normative protocol — they are common patterns that interoperable AP implementations can adopt or adapt. + +.# Discussion Venues + +*Note: This section is to be removed before publishing as an RFC.* + +Discussion of this document takes place on GitHub at https://github.com/dickhardt/AAuth. Issues, comments, and pull requests are welcome there. Source for this draft is in the same repository. + +{mainmatter} + +# Introduction + +The AAuth Protocol [@!I-D.hardt-oauth-aauth-protocol] establishes that every agent has its own cryptographic identity — an agent identifier of the form `aauth:local@domain`, bound to a signing key, and attested by an agent token issued by an agent provider (AP). The protocol defines the agent token format and how agents present that identity to person servers (PSes), resources, and access servers (ASes). It does not specify how an agent comes to hold an agent token in the first place. That step is **bootstrap**, and it is the subject of this document. + +## What Bootstrapping Is + +Bootstrapping is the AP-side ceremony by which an instance of an agent acquires an agent token. The agent generates a signing key on the device or in the browser where it will run, presents whatever evidence the AP requires (a signed-in account, an attested device, a published JWKS, etc.), and receives an agent token whose `cnf.jwk` is bound to that key and whose `sub` is an `aauth:local@domain` identifier the AP has chosen. + +After bootstrap the agent can participate in AAuth: it can sign HTTP messages per [@!I-D.hardt-httpbis-signature-key], identify itself at resources, and present its agent token to a PS so the user can bind the agent to themselves on first interaction. + +## What Bootstrapping Is Not + +- **Not normative protocol.** This document is informational. The AAuth Protocol does not mandate a specific bootstrap ceremony, and conformance does not depend on the patterns described here. APs are free to use other approaches that produce a valid agent token. +- **Not the user-to-agent binding.** Binding an agent to a person is performed by the PS, lazily, on the agent's first interaction with the PS per the AAuth Protocol. Bootstrap produces an agent identity; the PS attaches that identity to a user. +- **Not authorization.** Bootstrap conveys no scope, no resource permission, and no user identity claims. Those are obtained through the flows defined in the AAuth Protocol after bootstrap. +- **Not one-size-fits-all.** Web, mobile, and self-hosted agents have different threat models and different platform primitives available to them. This document offers patterns appropriate to each, not a single prescribed ceremony. + +## Patterns Covered + +- **Per-platform key handling** — where the agent's signing key lives and how strongly it is protected on web, mobile, and self-hosted deployments; desktop and workload coverage is TBD (#per-platform-keys). +- **Optional platform attestation** — when and why to require WebAuthn, App Attest, or Play Integrity (#optional-attestation). +- **Agent identifier strategies** — how to construct the `sub` claim's local part (#identifier-strategies). +- **Refresh patterns** — issuing fresh agent tokens for renewal (#refresh-patterns). + +Throughout, when this document refers to "the durable key" and "the ephemeral key" it means the keys defined in (#per-platform-keys). The ephemeral key's public part appears in `agent_token.cnf.jwk` and signs HTTP messages from the agent per [@!I-D.hardt-httpbis-signature-key]; the durable key serves as the AP's stable enrollment anchor and signs only at refresh. APs that use a single durable key for all signatures (#per-platform-keys) can read references to "the ephemeral key" as referring to that same durable key. + +# Conventions and Definitions + +{::boilerplate bcp14-tagged} + +This document is informational guidance and does not itself impose normative requirements. Normative requirements relevant to bootstrap are defined in [@!I-D.hardt-oauth-aauth-protocol]; this document references them where helpful but uses lowercase "should" / "must" in its own descriptive prose. + +# Terminology + +Terms defined in [@!I-D.hardt-oauth-aauth-protocol] are used here with the same meaning. In particular: + +- **Agent Provider (AP)** — issues agent tokens. +- **Agent token** — JWT signed by the AP, carrying `iss`, `sub`, `cnf.jwk`, optionally `ps`, and other claims. +- **Person Server (PS)** — represents the person; binds agents to a person on first interaction. + +This document additionally uses: + +- **Durable key** — a signing key whose lifetime is intended to span the agent install (typically the lifetime of an install or browser-storage entry). The durable key is the AP's stable enrollment anchor; it is presented only to the AP at refresh and is not used to sign requests to PSes, resources, or ASes. +- **Ephemeral key** — a signing key generated fresh per agent-token issuance. Its public part appears in `agent_token.cnf.jwk`. The agent uses it to sign HTTP messages for the agent token's lifetime, then discards it on the next refresh. +- **Platform attestation** — a mechanism by which the runtime platform attests to properties of the agent or its key (WebAuthn, Apple App Attest, Google Play Integrity, etc.). + +# Per-Platform Key Handling {#per-platform-keys} + +On web, mobile, and desktop, APs should use a two-key pattern: a **durable key** that serves as the AP's stable enrollment anchor and is presented only to the AP at refresh, plus an **ephemeral key** generated fresh per agent-token issuance whose public part appears in `agent_token.cnf.jwk` and which signs HTTP messages for the agent token's lifetime. Refresh chains the new ephemeral key to the durable key via the `jkt-jwt` scheme [@!I-D.hardt-httpbis-signature-key]; see (#refresh-patterns). + +This pattern bounds the blast radius of an ephemeral-key leak to one agent token's lifetime, narrows the durable key's attack surface to the AP refresh path (it never signs requests to PSes, resources, or ASes), and accommodates hardware-backed durable keys on platforms that have them today and on platforms that may expose them in the future without protocol change. APs may use a single durable key for all signatures where simplicity outweighs these properties — receivers cannot distinguish the two patterns, since they only verify `cnf.jwk` against the HTTP signature. + +Self-hosted agents (#self-hosted-agents) use a single key — the JWKS-published key serves as both the AP signing key and the agent's signing key, since there is no separate AP to refresh against. + +## Web Apps + +The durable key is a non-extractable [@WebCryptoAPI] key generated with `extractable: false` and stored in IndexedDB scoped to the AP's origin. The ephemeral key is also a non-extractable WebCrypto key, regenerated on each refresh and discarded when the next refresh produces its replacement. + +Properties of the durable key: + +- The private key cannot be read or exported by JavaScript, including by code injected via XSS or malicious browser extensions. JS can only ask the browser to sign with it. +- The key is bound to the origin's IndexedDB storage. Clearing site data destroys it; a new enrollment is required. +- No user-verification gesture is required to sign — operations are fast enough for routine signature use. + +Both keys are software-protected (browser sandbox) rather than hardware-protected. The two-key pattern still applies: the durable key signs only the periodic refresh (once per agent-token lifetime), while the ephemeral key signs every HTTP request and rotates on each refresh. APs that want the additional assurance of a WebAuthn ceremony at enrollment time can layer one on top; see (#optional-attestation). If browsers later expose hardware-backed credentials suitable for use as the durable key, the pattern accommodates them with no protocol change. + +## Mobile (iOS and Android) + +The durable key is generated and stored in the platform's hardware-backed keystore: the Secure Enclave on iOS or StrongBox on Android (or the Android Keystore where StrongBox is unavailable). The ephemeral key is a software key in app memory, regenerated on each refresh. + +Properties of the durable key: + +- The private key cannot be exported from the keystore. Cryptographic operations are performed by the keystore on the application's behalf. +- The key is bound to the application install. Reinstalling the app generates a new key and requires re-enrollment. +- The keystore can additionally enforce user authentication (biometric or device passcode) before sign operations, if the AP wants user-presence on each refresh. + +The AP typically also requires an attestation ceremony at enrollment to confirm the durable key is real keystore-bound material rather than software-generated. See (#optional-attestation). Because the durable key signs only at refresh, any per-op cost (keystore round-trip, optional user verification) is incurred at most once per agent-token lifetime, not on every request. + +## Self-Hosted Agents {#self-hosted-agents} + +A self-hosted agent runs under a domain the user controls. The agent publishes its AP metadata document at `/.well-known/aauth-agent.json` per [@!I-D.hardt-oauth-aauth-protocol]; the JWKS itself is hosted at any HTTPS URL referenced by the metadata's `jwks_uri`. The corresponding private key should be hardware-bound where the platform supports it: macOS Keychain (Secure Enclave on supported hardware), Windows TPM, or Linux Secret Service. + +Self-hosted agents act as their own AP — they self-issue agent tokens signed by the JWKS-published key. There is no separate AP to refresh against, so the two-key pattern does not apply: the JWKS-published key serves both as the AP signing key (signing self-issued agent tokens) and as the key whose public part appears in `agent_token.cnf.jwk` (signing HTTP messages). Because the trust anchor is a key the user controls and publishes, no platform attestation step exists. Other parties verify the agent token signature against the published JWKS, exactly as they would for any other AP. + +## Desktop Apps + +TBD. Future revisions will cover key handling for native desktop applications, where the durable key would live in a hardware-backed store (macOS Keychain with Secure Enclave on supported hardware, Windows TPM via CNG, Linux Secret Service / TPM2) and the ephemeral key in process memory, following the same pattern as mobile. + +## Workload + +TBD. Future revisions will cover headless workload identity (e.g., SPIFFE/SPIRE SVIDs, WIMSE workload identity, cloud-platform IMDS attestation), where the trust anchor is platform attestation rather than user interaction. + +# Optional Platform Attestation {#optional-attestation} + +Platform attestation gives the AP cryptographic evidence about the runtime context in which the durable key was generated. It is optional — the AAuth Protocol does not require it. APs choose whether to require attestation based on their threat model. + +Common reasons an AP might require attestation: + +- **Anti-fraud at enrollment.** Distinguishing real user devices from server-side automation. +- **Hardware-binding evidence.** Confirming the durable key is in a Secure Enclave or StrongBox rather than software. +- **App-integrity evidence.** Confirming the agent is the AP's published app, not a modified or repackaged binary. +- **User-presence evidence.** Confirming a real user gesture authorized this enrollment. + +Common reasons an AP might not require attestation: + +- The AP serves a trust posture where AP-side fraud detection is not signal-driven (e.g., paid accounts, invitation-only enrollment). +- The AP wants the broadest possible reach, including environments where attestation is unavailable. +- The deployment is self-hosted, where the user is the trust anchor. + +## WebAuthn (Web Apps) + +[@WebAuthn] provides user-verification and a hardware-rooted credential on web. APs that require it perform a registration ceremony at enrollment and an assertion ceremony at later sensitive operations. The credential is stored in the user's authenticator (TPM, Secure Enclave, security key, or platform syncing fabric). + +Tradeoffs: + +- Provides user-verification (touch, face, PIN) the WebCrypto-only approach lacks. +- Provides hardware-protected key material that cannot be lifted even by a fully compromised browser process. +- UX failure modes exist around cross-Chrome-profile use, embedded webviews, syncing-fabric inconsistencies, and cross-device QR ceremonies. APs that adopt WebAuthn should plan for fallback paths when these fail. + +## App Attest (iOS) + +[@AppAttest] attests to two things: that the durable key was generated in the device's Secure Enclave, and that the request originates from the AP's published app on a genuine Apple device. The AP receives an attestation object at enrollment which it verifies against Apple's attestation root, then accepts subsequent assertions signed by the same enclave key. + +## Play Integrity (Android) + +[@PlayIntegrity] provides a device, app, and account integrity verdict signed by Google. The AP nominates a nonce, the agent invokes the Play Integrity API, and the AP verifies the resulting integrity token. Subsequent assertions can be signed by an Android Keystore (or StrongBox) key that the AP enrolled at the same time. + +## When to Require Attestation + +A practical rule of thumb: + +- **Consumer-grade APs**: optional. Most consumer agent providers do not require attestation; the protocol-level binding at the PS is the security gate. +- **Regulated or high-assurance APs**: usually required. Financial services, healthcare, enterprise SSO providers benefit from attestation as part of their AML/KYC or device-management posture. +- **Multi-tenant APs**: often required at higher tiers. An AP may issue weak agent tokens to free tier users and require attestation for paid or enterprise tier users, surfacing the difference to receivers via AP-defined claims in the agent token. + +# Agent Identifier Strategies {#identifier-strategies} + +The agent token's `sub` is an `aauth:local@domain` identifier. The `domain` part is the AP. The `local` part identifies the agent install at the AP — it must be stable for the lifetime of the install so PSes and other parties can recognize a returning agent. + +APs are free to choose any opaque scheme for the local part: a random string assigned at enrollment, a deterministic derivation from the durable key's thumbprint, a sequential identifier, or a human-readable handle. When deriving from a thumbprint, use the durable key's thumbprint — the ephemeral key rotates on each refresh and is not a stable identifier. Receivers treat the identifier as opaque. + +## Per-Install Identity {#per-install-identity} + +Each install's durable key is the basis for one agent identity. A returning user on a new device is a new agent. This keeps the AP minimal — it has no user-account system, no `(user, durable_jkt)` mappings, and no ability to correlate a single user's activity across their devices. + +Multi-device users will see multiple agent entries in their PS dashboard. Grouping or merging those entries belongs at the PS, which already authenticates the user and is the correct layer for cross-device correlation. Rotation of the durable key produces a new agent identity; rotation of the ephemeral key (on every refresh) does not — the agent's `sub` is stable across ephemeral rotations. PS-side regrouping is the recovery path for durable-key changes. + +# Example Agent Token Claims + +A typical agent token issued by an AP, illustrating the claims described in [@!I-D.hardt-oauth-aauth-protocol] and the identifier strategies in (#identifier-strategies). + +JWT header: + +```json +{ "alg": "EdDSA", "typ": "aa-agent+jwt", "kid": "..." } +``` + +JWT payload: + +```json +{ + "iss": "https://ap.example", + "dwk": "aauth-agent.json", + "sub": "aauth:k7q3p9n2@ap.example", + "ps": "https://ps.example", + "cnf": { "jwk": { "kty": "OKP", "crv": "Ed25519", "x": "..." } }, + "iat": 1746316800, + "exp": 1746320400, + "jti": "..." +} +``` + +# Refresh Patterns {#refresh-patterns} + +Agent token lifetime is the AP's policy re-evaluation cadence — every refresh is the AP's chance to re-check device posture, attestation freshness, and account status before issuing a new token. A typical lifetime is **1 hour**, matching common practice for proof-of-possession-bound access tokens. APs may use shorter lifetimes (e.g., 5–15 minutes) for higher-assurance deployments where attestation must be refreshed often, or longer lifetimes up to the AAuth Protocol's 24-hour ceiling for low-policy-churn deployments where refresh chattiness is undesirable. + +## Two-Key Refresh + +On web, mobile, and desktop, refresh chains the new ephemeral key to the durable key via the `jkt-jwt` scheme [@!I-D.hardt-httpbis-signature-key]: + +1. The agent generates a fresh ephemeral key pair. +2. The agent constructs a JWT signed by the **durable key**, naming the new ephemeral public key. This is the "naming JWT" carried in the `Signature-Key` header under `scheme=jkt-jwt`. +3. The agent signs the refresh request with the **ephemeral key** under [@!RFC9421] HTTP Message Signatures. +4. The AP verifies the durable-key signature on the naming JWT, looks up the enrollment by the durable key's thumbprint, verifies the HTTP signature against the ephemeral public key, applies its policy (device posture, attestation freshness, account status), and returns a new agent token whose `cnf.jwk` is the ephemeral public key. +5. The agent uses the new agent token and ephemeral key for the agent token's lifetime, then discards the ephemeral key on the next refresh. + +Example refresh request: + +```http +POST /refresh HTTP/1.1 +Host: ap.example +Content-Type: application/json +Signature-Input: sig=("@method" "@authority" + "@path" "signature-key");created=1746316800 +Signature: sig=:...ephemeral-key signature bytes...: +Signature-Key: sig=jkt-jwt;jwt="eyJhbGc..." + +{} +``` + +The `jwt` parameter value is a JWT signed by the durable key with payload including the ephemeral public key (typically as `cnf.jwk`) and a `jti` for replay protection. The HTTP signature is produced by the ephemeral key. The AP correlates the two keys via the naming JWT's payload. + +## Single-Key Refresh + +APs that opt for the single-durable pattern (#per-platform-keys) sign the refresh request directly with the durable key under the `hwk` scheme [@!I-D.hardt-httpbis-signature-key]. The AP verifies the signature, looks up the enrollment by the key's thumbprint, and issues a fresh agent token with a new `exp`. The same `cnf.jwk` is carried through; the agent's key is unchanged. + +## Mobile Refresh Specifics + +APs that required platform attestation at enrollment typically do not re-attest on every refresh — the durable-key signature on the naming JWT (or the durable-key HTTP signature in the single-key pattern) is sufficient proof that the same enclave-resident key is making the request. APs that want periodic re-attestation can require a fresh App Attest assertion or Play Integrity verdict on a schedule (e.g., every 30 days) by including a server nonce in the refresh challenge. + +## Self-Hosted Refresh + +Self-hosted agents self-issue agent tokens. There is no separate refresh ceremony — the agent generates a new agent token signed by its JWKS-published key whenever needed. The two-key pattern does not apply (#self-hosted-agents). + +## Key Rotation vs Token Refresh + +Refresh issues a new agent token bound to a fresh ephemeral key (or, in the single-key pattern, to the same durable key). **Durable key rotation** generates a new durable key and is a separate, rare event. Under the per-install identity model (#per-install-identity), a new durable key is a new agent — the PS treats it as new on first interaction, and any cross-device or cross-rotation continuity is handled at the PS by the user. + +# Per-Platform Enrollment Sketches + +This section sketches a typical end-to-end enrollment for each platform. The sketches are illustrative; APs are free to vary them. + +## Web App Enrollment + +1. User logs into the AP through the AP's normal login. +2. Agent (running in the AP's web origin) generates a non-extractable WebCrypto Ed25519 **durable** key and stores its handle in IndexedDB. +3. Agent posts the durable public key to an AP-internal enrollment endpoint, signed by the new key (`hwk` scheme). +4. AP optionally performs a WebAuthn registration and verifies it. +5. AP records `(ap_user, durable_jkt)` and is now ready to issue agent tokens. +6. When the agent needs an agent token directed at PS_X, it generates a fresh **ephemeral** WebCrypto key and calls an AP-internal token-issuance endpoint indicating `ps=PS_X`, signed via `jkt-jwt` chaining the durable key to the ephemeral key (#refresh-patterns). The AP returns an agent token with `sub` derived per the AP's identifier strategy (#identifier-strategies) (using the durable key's thumbprint when derivation is used), `ps = PS_X`, `cnf.jwk` = the ephemeral public key, and any AP-attested claims. + +## Mobile App Enrollment + +1. User signs into the AP through the app's normal login. +2. App generates a **durable** key in the Secure Enclave (iOS) or StrongBox (Android). +3. App initiates platform attestation: App Attest on iOS, Play Integrity on Android. The AP nominates a nonce. +4. App posts the durable public key, the attestation result, and the nonce to an AP-internal enrollment endpoint. +5. AP verifies the attestation against the platform's trust root. +6. AP records `(ap_user, durable_jkt, attestation)` and is ready to issue agent tokens. +7. Token issuance proceeds as in the web app sketch — app generates a fresh ephemeral key per agent token and chains it to the durable key via `jkt-jwt`. + +## Self-Hosted Enrollment + +1. User generates a hardware-bound key on their machine. +2. User publishes an AP metadata document at `/.well-known/aauth-agent.json` per [@!I-D.hardt-oauth-aauth-protocol], with `jwks_uri` pointing to a JWKS containing the public part of that key. +3. The agent self-issues an agent token signed by that key as needed. + +There is no separate enrollment step — publication of the JWKS is the enrollment. + +# Security Considerations + +## Trust in the AP + +Every AP-attested claim in the agent token is only as trustworthy as the AP that signed the token. Receivers should apply policy proportional to their trust in the AP. An unfamiliar AP making strong attestation claims may warrant additional caution at the PS consent screen. + +## Ephemeral Key Compromise + +An ephemeral-key leak — via memory disclosure, in-page attacker, side channel, or similar — exposes only the signatures the agent makes during the current agent token's lifetime. At the recommended 1-hour lifetime, the blast radius is bounded to roughly that window before natural expiry forces replacement. Agents that detect compromise can decline to refresh, aging out the ephemeral key without explicit revocation. This bounding is the primary security argument for the two-key pattern (#per-platform-keys). + +## Durable Key Compromise + +Compromise of the durable key compromises the install's agent identity for the durable key's lifetime. The durable key signs only at refresh and is presented only to the AP, so its attack surface is much narrower than the ephemeral key's — but a successful compromise lets the attacker mint refresh requests indefinitely until the AP revokes the enrollment. APs should detect anomalous refresh patterns and provide a way for users to revoke a durable enrollment. + +A non-extractable WebCrypto durable key cannot be exfiltrated by page-level attackers, but it can be used by them while they hold execution in the page. APs should pair WebCrypto-only enrollment with normal web hygiene (CSP, subresource integrity, dependency review) and should not treat the non-extractable property as a substitute for keeping the page's JavaScript clean. A hardware-backed durable key (Secure Enclave, StrongBox, TPM) cannot be exfiltrated at all, only used in-place — narrowing the threat to malicious code running in the agent application itself. + +## Attestation Replay + +Platform attestation results (App Attest, Play Integrity, WebAuthn ceremonies) should be bound to a server-nominated nonce that is single-use and short-lived (5 minutes is a reasonable upper bound). The AP should verify that the attestation includes the nonce it issued; without this binding, a captured attestation can be replayed across enrollments. + +## Self-Hosted JWKS Key Compromise + +Compromise of the self-hosted JWKS key allows the attacker to mint agent tokens for that user's domain. Users running self-hosted agents should use hardware-backed keys (Secure Enclave / TPM / StrongBox) and rotate the published JWKS if compromise is suspected. + +# Privacy Considerations + +## Identifier Stability and User Tracking + +An agent's `sub` is the same value at every PS the agent contacts, not a per-PS pairwise identifier. A stable `sub` lets each PS reliably re-identify the agent across sessions — that is the intended property — but it also means colluding PSes (or any party with cross-PS telemetry) can correlate the agent's activity across them. Under per-install identity (#per-install-identity), durable-key rotation produces a new `sub`, giving users a natural "fresh start" capability. + +## AP Visibility Into Agent Activity + +The AP that issued an agent token does not see the agent's subsequent traffic to PSes, resources, or ASes (they verify against the AP's published JWKS, not by calling the AP). The AP's view is limited to enrollment and refresh requests. APs should document their data retention practices for those events. + +# IANA Considerations + +This document is informational and registers no new media types, JWT claim names, or metadata fields. + +# Implementation Status + +*Note: This section is to be removed before publishing as an RFC.* + +This section records the status of known implementations of the patterns described in this document at the time of posting of this Internet-Draft, and is based on a proposal described in [@?RFC7942]. The description of implementations in this section is intended to assist the IETF in its decision processes in progressing drafts to RFCs. + +TBD + +# Document History + +*Note: This section is to be removed before publishing as an RFC.* + +- draft-hardt-aauth-bootstrap-01 + - Major rewrite. The document is now informational guidance for AP implementers. The previously-normative PS bootstrap protocol (PS `/bootstrap` endpoint, `bootstrap_token`, bootstrap announcement, agent server [now Agent Provider] `bootstrap_endpoint` / `refresh_endpoint` / `webauthn_endpoint`) has been removed. PS-side binding to a person now happens lazily on the agent's first interaction with the PS per the AAuth Protocol; the bootstrap document covers AP-side enrollment patterns only. + - Removed Agent-Attested Display Values section; the `platform` and `device` parameters are defined and described in the AAuth Protocol. + +- draft-hardt-aauth-bootstrap-00 + - Initial draft. + +# Acknowledgments + +TBD. + +{backmatter} diff --git a/aauth-spec/v09/draft-hardt-aauth-events.md b/aauth-spec/v09/draft-hardt-aauth-events.md new file mode 100644 index 0000000..15abfd9 --- /dev/null +++ b/aauth-spec/v09/draft-hardt-aauth-events.md @@ -0,0 +1,758 @@ +%%% +title = "AAuth Events" +abbrev = "AAuth-Events" +ipr = "trust200902" +area = "Security" +workgroup = "TBD" +keyword = ["agent", "events", "webhooks", "async", "subscribe", "http", "identity"] +category = "standard" + +[seriesInfo] +status = "standard" +name = "Internet-Draft" +value = "draft-hardt-aauth-events-latest" +stream = "IETF" + +date = 2026-06-24T00:00:00Z + +[[author]] +initials = "D." +surname = "Hardt" +fullname = "Dick Hardt" +organization = "Hellō" + [author.address] + email = "dick.hardt@gmail.com" + +%%% + + + + AAuth Protocol + + Hellō + + + + + + + + HTTP Signature Keys + + Hellō + + + Cloudflare + + + + + + + + AAuth Rich Resource Requests (R3) + + Hellō + + + + + + + + AAuth Bootstrap Guidance + + Hellō + + + + + + + + AsyncAPI Specification 3.0.0 + + AsyncAPI Initiative + + + + + + +.# Abstract + +This document defines AAuth Events — an event subscription and delivery mechanism for agents operating under the AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]). It specifies the subscribe token that agents use to register callbacks with resources, the event token that resources deliver when events fire, and the delivery path through the Agent Provider (AP). AAuth Events enables agents to receive asynchronous notifications without requiring a public endpoint, using the cryptographic identity established by the AAuth Protocol. + +.# Discussion Venues + +*Note: This section is to be removed before publishing as an RFC.* + +Discussion of this document takes place on GitHub at https://github.com/dickhardt/AAuth. Issues, comments, and pull requests are welcome there. Source for this draft is in the same repository. + +{mainmatter} + +# Introduction + +## Agents Cannot Receive Webhooks + +Agents are often not servers with a routable endpoint. Whether running as a workload, a mobile app, or a single-page application, agents typically cannot receive inbound HTTP connections. Existing event delivery mechanisms — webhooks, WebSub, callback URLs — all assume the receiver is always-on and reachable. This assumption fails for agents that run intermittently, execute behind NAT, or live inside a platform that does not expose inbound HTTP. + +At the same time, many interactions agents initiate are inherently asynchronous. An agent books a medical appointment and needs to know if an earlier slot opens. An agent monitors inventory and needs to know when a product becomes available. An agent submits an order and needs confirmation when it ships. In each case, the agent initiates a synchronous request, the resource accepts it, and then the resource needs to reach back to the agent when something changes — potentially hours or days later. + +Existing approaches each fall short: + +- **Webhooks** require the agent to have a public URL. Agents do not. +- **Polling** is wasteful and imprecise. For time-sensitive events like waitlist slots, polling is too slow and too expensive. +- **Server-Sent Events / WebSocket** require a persistent outbound connection, which conflicts with intermittent agent workloads. +- **Message queues** (SQS, Kafka, RabbitMQ) require shared infrastructure, are not web-standard, and have no standardized subscription protocol across trust domains. + +## The Agent Provider as Inbox + +The AAuth Protocol establishes that every agent has an Agent Provider (AP) — a stable, always-on server that issues the agent's identity token. The AP is already a first-class principal in the AAuth ecosystem: it has its own cryptographic identity, publishes metadata at a well-known URL, and is trusted by all parties that interact with the agent. + +AAuth Events uses the AP as the agent's permanent event inbox. A resource that needs to notify an agent does not need to reach the agent directly — it posts the event to the AP's event endpoint. The AP delivers the event to the agent through whatever mechanism the AP and agent have established. The agent does not need a public URL. The AP is the public URL. + +## What AAuth Events Provides + +- **No public endpoint required**: The AP receives events on the agent's behalf. AP-to-agent delivery is platform-dependent and out of scope for this specification. +- **Cryptographic authorization**: The subscribe token is AP-signed and restricts event delivery to a specific resource. No shared secrets. +- **Agent identity at subscription time**: The resource knows cryptographically which agent subscribed, via the `sub` claim in the subscribe token. +- **Protected and public subscriptions**: Public event channels require only a subscribe token. Protected channels use a pre-authorized subscription URL issued by the resource during a prior authenticated interaction. +- **Event discovery via AsyncAPI**: Resources describe their event channels using AsyncAPI ([@AsyncAPI]) as an AAuth R3 vocabulary ([@!I-D.hardt-aauth-r3]). + +## Relationship to Existing Standards + +AAuth Events builds on the AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]) and HTTP Signature Keys ([@!I-D.hardt-httpbis-signature-key]). It provides the transport and subscription mechanisms that AsyncAPI ([@AsyncAPI]) describes: resources use AsyncAPI to document their event channels and payload schemas, while AAuth Events defines how agents subscribe and how events are delivered. + +# Conventions and Definitions + +{::boilerplate bcp14-tagged} + +# Terminology + +Terms defined in [@!I-D.hardt-oauth-aauth-protocol] are used here with the same meaning. In particular: Agent, Agent Provider (AP), Agent Token, Resource, Resource Token, Auth Token, Person Server (PS), Access Server (AS), and HTTP Sig. + +This document additionally uses: + +- **Subscribe Token**: A JWT issued by the AP to the agent, authorizing a specific resource to deliver events to the AP on the agent's behalf. Contains the Event ID and the agent's current signing key. +- **Event ID (eid)**: An opaque, AP-generated identifier that uniquely identifies a subscription at the AP. The agent maps the `eid` to its own context. The `eid` is the correlation key between the subscribe token, the AP's subscription record, and the event token. +- **Event Token**: A JWT issued and signed by the resource when an event fires, addressed to the agent (`aud` = agent identifier), and delivered to the AP's event endpoint. +- **Event Endpoint**: An endpoint published by the AP in its metadata at which resources deliver event tokens. +- **Subscription Ticket**: An opaque, short-lived value returned by a resource in response to an authenticated interaction, pre-authorizing a subsequent subscription registration call. Used when subscription to a protected channel requires prior authenticated context. + +# Protocol Overview + +AAuth Events involves four phases: setup, subscription registration, event delivery from resource to AP, and event delivery from AP to agent. + +~~~~ ascii-art +Agent AP Resource + | | | + | (1) request | | + | subscribe | | + | token | | + |------------>| | + | | | + | subscribe | | + | token | | + |<------------| | + | | | + | (2) signed request | + | w/ subscribe token | + |------------------------------------->| + | | | + | 200 OK | + |<-------------------------------------| + | | | + | | ... time passes ... | + | | | + | | (3) POST event token. | + | | (+ optional payload) | + | |<-----------------------| + | | | + | | 202 Accepted | + | |----------------------->| + | | | + | (4) event | | + | token + | | + | payload | | + |<------------| | +~~~~ +Figure: AAuth Events Protocol Overview {#fig-overview} + +1. **Subscribe token acquisition (non-normative)**: The agent requests a subscribe token from its AP. The AP generates an `eid`, creates a subscription record, and issues a subscribe token. This interaction is AP-internal and out of scope for this specification. See (#non-normative-ap-agent) for examples. + +2. **Subscription registration**: The agent presents the subscribe token to the resource as the `Signature-Key` JWT on a signed HTTP request to the resource's subscription endpoint. The resource validates the subscribe token, stores the `eid` and the AP's `event_endpoint` (resolved from the AP's metadata), and registers the subscription. + +3. **Event delivery — resource to AP**: When an event fires, the resource issues an event token (a JWT signed by the resource) and POSTs it to the AP's `event_endpoint`, presenting the event token as the `Signature-Key` JWT. The optional request body carries the AsyncAPI-defined payload for the event type. + +4. **Event delivery — AP to agent (non-normative)**: The AP validates the event, looks up the subscription by `eid`, and delivers the event token and any payload to the agent. This step is platform-dependent and out of scope for this specification. See (#non-normative-ap-agent) for examples. + +# AP Metadata {#ap-metadata} + +The AP MUST publish an `event_endpoint` claim in its metadata at `/.well-known/aauth-agent.json` if it supports AAuth Events. The `event_endpoint` is an HTTPS URL at which the AP receives event tokens from resources. + +```json +{ + "issuer": "https://ap.example", + "jwks_uri": "https://ap.example/.well-known/jwks.json", + "event_endpoint": "https://ap.example/events" +} +``` + +The AP MAY update the `event_endpoint` URL at any time. Resources resolve the AP's `event_endpoint` from the AP's metadata (using the `iss` claim in the subscribe token to locate the AP's well-known document) rather than caching it from the subscribe token. + +# Subscribe Token {#subscribe-token} + +## Structure + +A subscribe token is a JWT with `typ: aa-subscribe+jwt`, issued and signed by the AP, with the following claims: + +Header: + +- `alg`: Signing algorithm. EdDSA is RECOMMENDED. Implementations MUST NOT accept `none`. +- `typ`: `aa-subscribe+jwt` +- `kid`: Key identifier (AP's signing key) + +Required payload claims: + +- `iss`: Agent Provider URL. Used by the resource to locate the AP's metadata and `event_endpoint`. +- `dwk`: `aauth-agent.json` — the well-known metadata document name for key discovery ([@!I-D.hardt-httpbis-signature-key]). +- `sub`: Agent identifier. The AAuth agent identifier (`aauth:local@domain`) of the subscribing agent. +- `aud`: Resource URL. The resource that is authorized to deliver events for this subscription. The resource MUST verify that its own URL matches this claim. +- `cnf`: Confirmation claim ([@!RFC7800]) with `jwk` containing the agent's current public signing key. The resource uses this key to verify the HTTP signature on the subscription registration request. +- `eid`: Event ID. An opaque string generated by the AP, unique to the AP. The agent maps the `eid` to its own context (see (#agent-context-mapping)). The resource includes the `eid` in every event token it issues for this subscription. +- `iat`: Issued-at timestamp. +- `exp`: Expiration timestamp. The resource MUST reject subscribe tokens with `exp` in the past. + +Optional payload claims: + +- `max_uses`: A positive integer. If present, the AP MUST NOT accept more than this many event tokens for this `eid`. If absent, the subscription is unlimited. Enforcement is the AP's responsibility; the AP informs the resource of remaining uses in its `202 Accepted` response (see (#event-delivery)). The resource SHOULD track `remaining_uses` to manage subscription state — for example, prompting the agent to re-subscribe when the subscription is exhausted. + +Example subscribe token payload: + +```json +{ + "iss": "https://ap.example", + "dwk": "aauth-agent.json", + "sub": "aauth:k7q3p9n2@ap.example", + "aud": "https://resource.example", + "cnf": { "jwk": { "kty": "OKP", "crv": "Ed25519", "x": "..." } }, + "eid": "evt_8f3k2n9p", + "iat": 1750000000, + "exp": 1750086400, + "max_uses": 1 +} +``` + +## Presentation + +The agent presents the subscribe token as the `Signature-Key` JWT on the subscription registration request, using `scheme=jwt`: + +```http +POST /appointments/waitlist HTTP/1.1 +Host: resource.example +Content-Type: application/json +Signature-Input: sig=("@method" "@authority" + "@path" "signature-key" "content-type");created=1750000000 +Signature: sig=:...signature bytes...: +Signature-Key: sig=jwt; + jwt="eyJhbGciOiJFZERTQSIsInR5cCI6ImFhLXN1Yitqd3QiLCJraWQiOiIuLi4ifQ..." + +{ + "event_types": ["slot.available"] +} +``` + +The subscribe token replaces the agent token as the `Signature-Key` JWT for subscription registration requests. The `cnf.jwk` in the subscribe token provides the key the resource uses to verify the HTTP signature. The subscribe token is structurally analogous to the agent token — both are AP-signed JWTs carrying `cnf.jwk` — distinguished by `typ`. + +## Verification + +The resource MUST verify the subscribe token as follows: + +1. Decode the JWT header. Verify `typ` is `aa-subscribe+jwt`. +2. Verify `dwk` is `aauth-agent.json`. Discover the AP's JWKS via `{iss}/.well-known/{dwk}` per ([@!I-D.hardt-httpbis-signature-key]). Locate the key matching `kid` and verify the JWT signature. +3. Verify `exp` is in the future and `iat` is not in the future. +4. Verify `aud` matches the resource's own URL. +5. Verify `cnf.jwk` matches the key used to sign the HTTP request. +6. Verify `eid` is present and non-empty. + +After verification, the resource stores the subscription record with sufficient information to deliver events — at minimum `{eid, iss}` (the Event ID and the AP's issuer URL). When an event fires, the resource resolves the AP's `event_endpoint` from `{iss}/.well-known/aauth-agent.json` at delivery time, using standard HTTP caching for the well-known document. + +## Agent Context Mapping {#agent-context-mapping} + +The `eid` is the agent's correlation key. The agent maintains a local mapping of `eid` values to internal context — for example, "eid `evt_8f3k2n9p` corresponds to the appointment waitlist for Dr. Smith opened as part of mission `mission_xyz`". This mapping is the agent's own concern and is not defined by this specification. + +# Subscription Registration {#subscription-registration} + +## Public Subscriptions + +For event channels that do not require prior authorization, the agent presents the subscribe token (as the `Signature-Key` JWT) on a signed POST to the resource's subscription endpoint. No additional credential is required. The resource validates the subscribe token per (#subscribe-token) and registers the subscription. + +## Protected Subscriptions {#protected-subscriptions} + +Some event channels require the agent to be authorized before it can register a subscription — for example, subscribing to events for a specific patient's appointments, or events associated with a particular account. In these cases, the resource does not accept subscription registrations from arbitrary agents; only agents that have already been authorized in an earlier interaction may register. + +This specification defines a **pre-authorized subscription URL** pattern for protected subscriptions: + +1. The agent makes an authenticated request to the resource (using an auth token obtained through one of the AAuth Protocol access modes). +2. The resource, if subscription to events is available for the context established by this interaction, returns a **subscription ticket URL** — an HTTPS URL that encodes a short-lived, single-use authorization to register a subscription. The ticket URL is opaque and is valid only for the specific context (agent, operation, and resource state) established in step 1. +3. The agent obtains a subscribe token from its AP. +4. The agent presents the subscribe token (as the `Signature-Key` JWT) on a signed POST to the subscription ticket URL. No additional auth token is required at this step; the authorization is embedded in the URL. The request body MAY include additional parameters as defined by the resource's AsyncAPI channel schema. +5. The resource validates the subscribe token, verifies the ticket in the URL is valid for the calling agent (by checking `sub` in the subscribe token matches the agent that triggered step 1) and has not been used before, and registers the subscription. + +The subscription ticket URL is resource-controlled: the resource issues it, defines its scope and expiry, and enforces its single-use constraint. The ticket is not defined by this specification beyond the pattern above. + +Example response from step 2: + +```json +{ + "status": "unavailable", + "next_available": "2026-08-24", + "waitlist": { + "subscribe_url": "https://resource.example/waitlist/st_9k2m_abc123", + "event_types": ["slot.available"], + "offer_window_seconds": 300 + } +} +``` + +Example subscription registration from step 4: + +```http +POST /waitlist/st_9k2m_abc123 HTTP/1.1 +Host: resource.example +Content-Type: application/json +Signature-Input: sig=("@method" "@authority" + "@path" "signature-key" "content-type");created=1750000000 +Signature: sig=:...signature bytes...: +Signature-Key: sig=jwt; + jwt="eyJhbGciOiJFZERTQSIsInR5cCI6ImFhLXN1Yitqd3QiLCJraWQiOiIuLi4ifQ..." + +{ + "event_types": ["slot.available"] +} +``` + +The HTTP signature covers the request path (including the ticket), cryptographically binding the subscribe token's identity to this specific ticket URL. + +The resource SHOULD include the subscription ticket URL in an AsyncAPI channel parameter ([@AsyncAPI]) so that agents that discover the resource's event capabilities through its AsyncAPI document know to obtain the URL from a prior API response. + +# Event Token {#event-token} + +## Structure + +When an event fires, the resource issues an event token: a JWT signed by the resource with the following claims: + +Header: + +- `alg`: Signing algorithm. EdDSA is RECOMMENDED. +- `typ`: `aa-event+jwt` +- `kid`: Key identifier (resource's signing key) + +Required payload claims: + +- `iss`: Resource URL. +- `dwk`: `aauth-resource.json` — the well-known metadata document name for key discovery ([@!I-D.hardt-httpbis-signature-key]). +- `aud`: Agent identifier (`aauth:local@domain`). The agent MUST verify this matches its own identifier. +- `eid`: Event ID. MUST match the `eid` from the subscribe token for this subscription. The AP uses the `eid` to look up the subscription record and route to the agent. The agent uses the `eid` to look up its local context mapping. +- `iat`: Issued-at timestamp. +- `exp`: Expiration timestamp. The agent MUST NOT act on an event token with `exp` in the past. The meaning of `exp` is event-specific — for time-sensitive events, it encodes the deadline by which the agent must act. + +The event token is the transport and security layer. It carries no event-specific data. Event-specific content is delivered as the POST body alongside the event token (see (#event-delivery)). + +Example event token payload: + +```json +{ + "iss": "https://resource.example", + "dwk": "aauth-resource.json", + "aud": "aauth:k7q3p9n2@ap.example", + "eid": "evt_8f3k2n9p", + "iat": 1750200000, + "exp": 1750200300 +} +``` + +# Event Delivery: Resource to AP {#event-delivery} + +## Request + +When an event fires for an active subscription, the resource posts to the AP's `event_endpoint`, presenting the event token as the `Signature-Key` JWT. The POST body is the AsyncAPI-defined payload for the event type (OPTIONAL — omitted if the event carries no payload): + +```http +POST /events HTTP/1.1 +Host: ap.example +Content-Type: application/json +Signature-Input: sig=("@method" "@authority" + "@path" "signature-key" "content-type" "content-digest");created=1750200000 +Signature: sig=:...resource signing key signature bytes...: +Signature-Key: sig=jwt; + jwt="eyJhbGciOiJFZERTQSIsInR5cCI6ImFhLWV2ZW50K2p3dCIsImtpZCI6Ii4uLiJ9..." + +{ + "event_type": "slot.available", + "slot_time": "2026-07-15T10:00:00Z" +} +``` + +The event token in `Signature-Key` provides the resource's identity (`iss`) and routing and authorization claims (`eid`, `aud`, `exp`). Unlike agent tokens and subscribe tokens, no `cnf.jwk` is needed: the resource has a stable JWKS discoverable from `{iss}/.well-known/{dwk}`, and the AP uses the same key (identified by `kid` in the JWT header) to verify both the JWT signature and the HTTP signature. This is an extension to the `Signature-Key` JWT scheme: when a JWT has `dwk` but no `cnf`, the verifier resolves the HTTP signing key from `{iss}/.well-known/{dwk}` using `kid` rather than from an inline `cnf.jwk`. The request body structure is defined by the resource's AsyncAPI message schema for the event type (see (#event-discovery)). The AP forwards both the event token and the payload body to the agent. + +The resource resolves the AP's `event_endpoint` from `{iss}/.well-known/aauth-agent.json` at delivery time, using standard HTTP caching for the AP's well-known document. + +## AP Validation + +The AP MUST validate the event delivery request as follows: + +1. Extract the event token JWT from the `Signature-Key` header. Verify `typ` is `aa-event+jwt`. +2. Discover the resource's JWKS via `{iss}/.well-known/{dwk}`. Locate the key matching `kid` and verify the JWT signature. +3. Verify the HTTP signature using the same key (matched by `kid`). This applies the `dwk`-without-`cnf` extension to the Signature-Key JWT scheme: the JWT signing key and the HTTP signing key are the same key, discoverable from the resource's well-known document. +4. Look up the subscription record by `eid`. If no active subscription exists for this `eid`, return `404`. +5. Verify `iss` matches the resource recorded at subscription time (the `aud` of the subscribe token for this `eid`). +6. Verify the event token `exp` is in the future. +7. If `max_uses` is set in the subscribe token, verify the use count has not been exceeded. Increment the use count atomically. If the use limit is reached, the AP MAY mark the subscription as complete after delivery. +8. Verify the event token `aud` matches the agent identifier in the subscription record. + +If all checks pass, the AP returns `202 Accepted` and proceeds with delivery to the agent. The AP MUST NOT return `202` before the event has been durably recorded for delivery. If `max_uses` was set in the subscribe token, the AP MUST include a JSON response body with a `remaining_uses` field indicating how many more event tokens the AP will accept for this `eid`: + +```http +HTTP/1.1 202 Accepted +Content-Type: application/json + +{ + "remaining_uses": 0 +} +``` + +When `remaining_uses` is `0`, the subscription is exhausted. The resource SHOULD clean up its subscription record and MAY prompt the agent to re-subscribe on the next interaction. When `max_uses` was not set, the AP returns `202 Accepted` with no body (or an empty JSON object). + +The AP returns `400` for malformed requests, `401` if the resource's HTTP signature cannot be verified, `403` if the resource does not match the subscription's authorized resource, `404` if the `eid` is unknown or the subscription has expired, and `429` if `max_uses` has been exceeded. + +# Event Delivery: AP to Agent {#ap-to-agent} + +How the AP delivers the event token to the agent is platform-dependent and outside the scope of this specification. The AP is the agent's inbox; the internal mechanism is an implementation choice for the AP and agent. + +See (#non-normative-ap-agent) for non-normative examples of AP-to-agent delivery for different platforms. + +## Agent Verification + +Upon receiving an event token (and optional payload) from the AP, the agent MUST: + +1. Decode the JWT header. Verify `typ` is `aa-event+jwt`. +2. Discover the resource's JWKS via `{iss}/.well-known/{dwk}` per ([@!I-D.hardt-httpbis-signature-key]). Verify the JWT signature. +3. Verify `aud` matches the agent's own identifier. +4. Verify `exp` is in the future. If `exp` has passed, the agent SHOULD NOT act on the event (the response window has closed). +5. Look up `eid` in the agent's local context mapping to recover the context associated with this subscription. +6. Deduplicate: if the agent has already processed an event with this `eid` from this `iss`, it SHOULD ignore the duplicate. The `eid` is a natural idempotency key. + +If a payload was included, the agent MAY use it directly. The payload structure is defined by the resource's AsyncAPI message schema for the event type. + +# Event Discovery {#event-discovery} + +Resources describe their event capabilities using AsyncAPI ([@AsyncAPI]) as an AAuth R3 vocabulary ([@!I-D.hardt-aauth-r3]). + +## R3 Vocabulary Identifier + +The vocabulary identifier for AAuth Events is: + +``` +urn:aauth:vocabulary:asyncapi +``` + +Resources that support AAuth Events SHOULD declare this vocabulary in their AAuth resource metadata: + +```json +{ + "issuer": "https://resource.example", + "r3_vocabularies": { + "urn:aauth:vocabulary:openapi": "/openapi.json", + "urn:aauth:vocabulary:asyncapi": "/asyncapi.json" + } +} +``` + +## AsyncAPI Document + +The resource's AsyncAPI document describes: + +- **Channels**: Event streams the agent may subscribe to. Channels MAY use parameterized addresses (e.g., `/waitlist/{subscriptionTicket}`) when the subscription endpoint URL is dynamic (see (#protected-subscriptions)). +- **Operations**: `receive` operations on channels, with the security requirement and message schema. +- **Messages**: The payload schema for each event type. The AsyncAPI payload schema describes the `payload` field in the event delivery POST body (see (#event-delivery)). The AAuth event token envelope (`iss`, `aud`, `eid`, `exp`) is implicit and not part of the AsyncAPI schema. +- **Security schemes**: The AAuth subscribe token security scheme. + +## Security Scheme + +Resources MUST declare the AAuth subscribe token security scheme as follows: + +```yaml +securitySchemes: + aauth_subscribe: + type: http + scheme: aauth-subscribe + description: > + AAuth Subscribe Token (typ: aa-subscribe+jwt), issued by the agent's Agent + Provider, presented as the Signature-Key JWT with HTTP Message Signatures. + See draft-hardt-aauth-events. +``` + +Operations that require only a subscribe token declare: + +```yaml +security: + - aauth_subscribe: [] +``` + +Operations that require a pre-authorized subscription URL (see (#protected-subscriptions)) have no security scheme on the subscription endpoint itself — the subscription ticket in the URL carries the authorization. The resource SHOULD annotate such channels with a description noting that the subscription URL is obtained from a prior authenticated API call. + +## Example AsyncAPI Document + +```yaml +asyncapi: 3.0.0 +info: + title: Appointments Events + version: 1.0.0 + +channels: + waitlistPublic: + address: /appointments/waitlist/public + messages: + slotAvailable: + $ref: '#/components/messages/SlotAvailable' + + waitlistProtected: + address: /appointments/waitlist/{subscriptionTicket} + description: > + Subscription URL returned by POST /appointments when no slot is + immediately available and the calling agent is authorized for + waitlist access. The subscriptionTicket is embedded in the URL + and carries the authorization context. + parameters: + subscriptionTicket: + description: Single-use ticket from the POST /appointments response. + messages: + slotAvailable: + $ref: '#/components/messages/SlotAvailable' + +operations: + subscribePublicWaitlist: + action: receive + channel: + $ref: '#/channels/waitlistPublic' + security: + - aauth_subscribe: [] + + subscribeProtectedWaitlist: + action: receive + channel: + $ref: '#/channels/waitlistProtected' + +components: + messages: + SlotAvailable: + contentType: application/jwt + payload: + type: object + properties: + event_type: + type: string + const: slot.available + slot_time: + type: string + format: date-time + doctor_id: + type: string + required: + - event_type + - slot_time + + securitySchemes: + aauth_subscribe: + type: http + scheme: aauth-subscribe + description: AAuth Subscribe Token as Signature-Key JWT +``` + +# Security Considerations + +## Subscribe Token Scope + +The `aud` claim in the subscribe token restricts which resource may deliver events to the AP for this `eid`. If a resource attempts to deliver events for an `eid` issued to a different resource, the AP MUST reject the request (#event-delivery). This prevents a compromised resource from hijacking another resource's subscription channel. + +## Event Token Forgery + +Event tokens are signed by the resource using the resource's own signing key. The agent verifies the event token against the resource's JWKS ([@!I-D.hardt-httpbis-signature-key]). A party without the resource's private key cannot forge a valid event token. There are no shared secrets in AAuth Events. + +## Replay Prevention + +The AP enforces `max_uses` per `eid` and rejects event tokens with `exp` in the past. The agent additionally deduplicates on `eid` from the same `iss` (#agent-verification). These two layers prevent replay: a captured event token cannot be re-delivered once the AP has tracked its delivery and the agent has processed it. + +## Subscribe Token Replay at Registration + +A subscribe token with a valid `exp` could in principle be presented to the resource's subscription endpoint more than once. The `eid` is the deduplication key: the resource SHOULD reject subscription registration requests for an `eid` it has already registered. Single-use enforcement of the subscription ticket URL (in protected subscriptions) provides an additional constraint. + +## Pre-Authorized Subscription URL Security + +The subscription ticket URL (see (#protected-subscriptions)) encodes authorization from a prior authenticated context. Resources MUST ensure that subscription tickets are: + +- Short-lived (expiry appropriate to the expected delay between issuing and using the ticket). +- Single-use (the resource invalidates the ticket on first successful subscription registration). +- Bound to the agent that triggered the prior interaction (the resource MUST verify that `sub` in the subscribe token matches the agent that established the ticket). + +## AP as Delivery Intermediary + +The AP sees every event token delivered to an agent. The AP validates the event token's `iss`, `aud`, and `eid` claims but does not need to inspect resource-specific payload claims. APs SHOULD document their data retention policies for event tokens. + +## Resource Enumeration + +A resource that exposes its AsyncAPI document publicly reveals what event types it emits. This may be intentional (public API). Resources that wish to restrict event type discovery MAY gate their AsyncAPI document with AAuth authentication. + +# Privacy Considerations + +## Agent Identifier Stability + +The `sub` claim in the subscribe token carries the agent's stable identifier. Resources that receive subscribe tokens can correlate an agent's subscription activity over time. This is the intended property — the resource needs to know which agent subscribed. Agents and APs should be aware that subscription registrations leave a record at the resource. + +## Event Content + +The event token carries no event-specific data — it is the security and routing envelope only. Event-specific content travels in the `payload` field of the POST body (see (#event-delivery)), which is also visible to the AP during routing. Resources SHOULD NOT include sensitive personal data in the payload beyond what is necessary for the agent to evaluate relevance. Sensitive details SHOULD be fetched by the agent from the resource's data API using a current auth token. + +# IANA Considerations + +## JWT Type Values + +This specification defines the following JWT `typ` header parameter values, to be registered in the IANA "JSON Web Token Types" registry: + +- `aa-subscribe+jwt`: AAuth Subscribe Token. +- `aa-event+jwt`: AAuth Event Token. + +## AAuth R3 Vocabulary Identifiers + +This specification defines the following R3 vocabulary identifier: + +- `urn:aauth:vocabulary:asyncapi`: AAuth AsyncAPI event vocabulary. + +# Implementation Status + +*Note: This section is to be removed before publishing as an RFC.* + +TBD + +# Document History + +*Note: This section is to be removed before publishing as an RFC.* + +- draft-hardt-aauth-events-00 + - Initial draft. + +# Acknowledgments + +TBD. + +{backmatter} + +# Design Rationale {#design-rationale} + +This appendix explains the key design decisions in AAuth Events and the alternatives considered. + +## Why the AP Is the Delivery Intermediary + +Agents are workloads, not servers. They spin up, execute, and terminate. They run behind NAT, inside containers, or on mobile devices. They have no stable public endpoint. + +Every existing push delivery mechanism (webhooks, WebSub, CIBA ping/push mode, W3C Web Push) assumes the subscriber has a stable HTTP endpoint. W3C Web Push is the closest analog to what AAuth Events does — it uses a browser push service (Google/Apple/Mozilla) as the subscriber's stable address. AAuth Events uses the AP in this role, with two improvements: the AP already has a trust relationship with the agent (it issued the agent's identity token), and the subscriber's identity is cryptographic (not just an opaque push service subscription). + +The AP-as-inbox pattern mirrors how email works: you do not need to be online when someone sends you mail. The mail server is the stable address. AAuth Events gives agents the same property for event delivery. + +## Why the Subscribe Token Is the Signature-Key JWT + +The subscribe token simultaneously serves two functions: it proves the agent's identity (via `cnf.jwk` + HTTP signature) and registers the subscription (via `eid`, `aud`, `exp`). Presenting it as the `Signature-Key` JWT means a single signed HTTP request to the subscription endpoint accomplishes both without a separate credential or header. + +This is structurally analogous to the agent token — both are AP-signed JWTs with `cnf.jwk`, distinguished by `typ`. The resource's verification path is the same whether it is processing an identity-based request with an agent token or a subscription registration with a subscribe token. + +The alternative — a separate header or body parameter carrying the subscribe token alongside the normal agent token — was rejected because it requires two credentials where the subscribe token alone is sufficient. + +## Why `aud` in the Subscribe Token Is the Resource + +The `aud` claim restricts event delivery authorization to a specific resource. Only the resource named in `aud` may deliver events for this `eid` to the AP. This prevents: + +- A compromised resource from injecting events into another agent's subscription channels. +- The AP from accepting events from unexpected callers. + +The AP enforces this by matching the calling resource (identified by its HTTP signature) against the `aud` in the subscribe token stored in the subscription record. + +## Why `exp` Is the JWT Validity Period, Not the Subscription Lifetime + +Subscription lifetime is a negotiation between the agent and the resource at registration time. The resource has its own policy on maximum subscription duration. These durations can be days or months and are resource-specific. + +The subscribe token's `exp` is the standard JWT validity window — how long the resource may accept this token for registration. Conflating JWT validity with subscription lifetime would either force a very long-lived token (security concern: replay window) or a very short subscription (UX concern: subscriptions expire before they're useful). + +The resource stores the subscription record with whatever lifetime the agent and resource negotiate at registration. The subscribe token is a registration credential, not a subscription policy document. + +## Why `max_uses` Is in the Subscribe Token + +`max_uses` is the AP's throttle on how many event tokens it will accept for a given `eid`. It is AP-enforced, not resource-enforced. Placing it in the subscribe token — which the AP issued and controls — makes it AP-policy without requiring a separate AP configuration step. + +For single-shot events (confirm this reservation), `max_uses: 1` ensures the AP accepts exactly one event. For ongoing subscriptions, `max_uses` is omitted (unlimited). When `max_uses` is absent, there is no sentinel value — absence means unlimited, avoiding any need for a special value such as -1. + +The AP informs the resource of remaining uses in the `202 Accepted` response body after each delivery. The resource SHOULD use `remaining_uses: 0` as the signal to clean up its subscription record and prompt the agent to re-subscribe. This keeps the AP as the enforcement point while giving the resource the state it needs to manage the subscription lifecycle. + +## Why the Event Token Is the Transport Layer, Not the Data Layer + +The event token carries only what is needed for security, routing, and correlation: `iss`, `aud`, `eid`, `exp`. It is the cryptographic layer — the AP uses it to authenticate the resource, look up the subscription, and verify the delivery is authorized. The agent uses it to verify authenticity and look up its context via `eid`. + +Event-specific data travels as a separate `payload` in the same POST body. The AsyncAPI message schema for the event type defines the payload structure. This separation keeps the JWT minimal and avoids embedding event data in a signed-but-not-encrypted envelope. For events where the agent needs full details beyond the payload, it fetches them from the resource's data API using a current auth token. + +## Why `exp` in the Event Token Is the Response Window + +The `exp` claim in the event token defines how long the agent has to respond to the event. Its meaning is event-specific: for a waitlist slot, it is the deadline by which the agent must claim the slot; for a shipping confirmation, it may be a much longer acknowledgment window. + +The AP delivers events in near real-time. If the AP cannot deliver an event before its `exp`, the agent should not act on it (the response window has closed). The agent verifies `exp` before acting. + +## Why Protected Subscriptions Use a Pre-Authorized URL + +For protected event channels, the resource needs to verify that the subscribing agent has been authorized in a prior interaction before accepting the subscription. The naive approach — requiring both an auth token and a subscribe token on the subscription call — is awkward because the two tokens serve different purposes and the auth token conveyance alongside a `Signature-Key` subscribe token has no established AAuth pattern. + +The pre-authorized subscription URL pattern solves this cleanly: authorization is captured in the prior authenticated interaction, and the resource returns a ticket URL that encodes this context. The agent then presents only the subscribe token at the ticket URL. The HTTP signature covers the URL path (including the ticket), binding the subscribe token's identity to this specific authorization context. + +This mirrors established patterns (OAuth authorization codes, S3 presigned URLs) while preserving the AAuth Events invariant: all subscription endpoints accept only the subscribe token as the `Signature-Key` JWT. + +## Why AsyncAPI Is the Discovery Vocabulary + +AsyncAPI is the de facto standard for describing event-driven APIs. It describes channels, message schemas, security requirements, and (via parameterized addresses) dynamic subscription endpoints. The AAuth R3 vocabulary framework already accommodates multiple vocabularies per resource — AsyncAPI sits naturally alongside OpenAPI for synchronous operations. + +The target reader of an AAuth resource's AsyncAPI document is an AAuth-capable agent, not generic AsyncAPI tooling. This means the `aauth_subscribe` security scheme (type: `http`, scheme: `aauth-subscribe`) does not need to be understood by Swagger UI or code generators — it is a declaration for the agent's benefit, interpreted per this specification. + +## Comparison to Existing Patterns + +| | AAuth Events | Webhooks | WebSub | CIBA (ping) | Web Push | +|---|---|---|---|---|---| +| Receiver needs public URL | No | Yes | Yes | Yes | No | +| Caller identity | Cryptographic (resource key) | HMAC shared secret | None | Client creds | Push service | +| Subscriber identity at resource | Agent identifier (`sub`) | None | None | Client ID | Subscription ID | +| Per-operation subscription | Yes | No (account-level) | No | Yes | No | +| General event types | Yes | Yes | Yes | No (auth only) | Yes | +| Standard description format | AsyncAPI (R3) | Proprietary | Atom/RSS | N/A | None | + +# Non-Normative AP-to-Agent Delivery Examples {#non-normative-ap-agent} + +How the AP delivers an event token to an agent is platform-dependent and not specified by this document. The following examples illustrate common patterns, paralleling the approach taken in [@?I-D.hardt-aauth-bootstrap] for agent token acquisition. + +## Workload Agents + +A workload agent (running in a cloud function, container, or batch job) may poll the AP for pending event tokens on startup, using an AP-internal endpoint. The AP acts as a durable inbox — storing event tokens until the workload polls. The workload validates and processes pending events before beginning its primary task. + +## Mobile Agents + +A mobile agent may receive events via the platform's native push notification infrastructure (APNs on iOS, FCM on Android). The AP holds a push token registered by the agent at enrollment time and delivers event tokens to the agent via push notification. The agent wakes on receipt, fetches the full event token from the AP if needed, and processes it. + +## Web Agents + +A web agent with a persistent session may receive events via a server-sent event (SSE) or WebSocket connection that the agent maintains to the AP. The AP streams event tokens over this connection as they arrive. + +## Self-Hosted Agents + +A self-hosted agent may receive events in two ways depending on whether it manages its own AP or delegates to an external one. + +If the agent acts as its own AP ([@?I-D.hardt-aauth-bootstrap]), it may expose an internal event endpoint. Events are delivered directly to this endpoint by the resource — the AP and agent are collocated. + +If the agent uses an external AP service, it maintains an outbound persistent connection (SSE, WebSocket, or a similar mechanism) to the AP's inbox service. The AP delivers event tokens and payloads over this connection as they arrive. The self-hosted agent does not need a public inbound endpoint — the outbound connection to the AP is sufficient. + + diff --git a/aauth-spec/v09/draft-hardt-aauth-r3.md b/aauth-spec/v09/draft-hardt-aauth-r3.md new file mode 100644 index 0000000..7252098 --- /dev/null +++ b/aauth-spec/v09/draft-hardt-aauth-r3.md @@ -0,0 +1,665 @@ +%%% +title = "AAuth Rich Resource Requests (R3)" +abbrev = "AAuth-R3" +ipr = "trust200902" +area = "Security" +workgroup = "TBD" +keyword = ["agent", "authorization", "http", "resource"] + +[seriesInfo] +status = "standard" +name = "Internet-Draft" +value = "draft-hardt-aauth-r3-latest" +stream = "IETF" + +date = 2026-03-24T00:00:00Z + +[[author]] +initials = "D." +surname = "Hardt" +fullname = "Dick Hardt" +organization = "Hellō" + [author.address] + email = "dick.hardt@gmail.com" + +%%% + + + + AAuth Protocol + + Hellō + + + + + + + + AAuth Events + + Hellō + + + + + + +.# Abstract + +This document defines AAuth Rich Resource Requests (R3), an extension to the AAuth Protocol ([@!I-D.hardt-aauth-protocol]) that enables structured, vocabulary-based authorization for resource access. Resources publish R3 documents (content-addressed authorization definitions) and advertise vocabularies describing their operations. Agents request access using those vocabularies. Auth tokens carry granted operations in the same vocabulary format, enabling resources to enforce authorization directly from the token. R3 provides human-displayable context for consent decisions and content-addressed audit provenance via the `r3_s256` hash in auth tokens. + +.# Discussion Venues + +*Note: This section is to be removed before publishing as an RFC.* + +This document is part of the AAuth specification family. Source for this draft and an issue tracker can be found at https://github.com/dickhardt/AAuth. + +{mainmatter} + +# Introduction + +**Status: Exploratory Draft** + +The AAuth Protocol ([@!I-D.hardt-aauth-protocol]) defines resource tokens as the mechanism by which resources declare what authorization is needed to access them, and scope strings as the primary way to express what operations are available. Scopes are sufficient for simple, well-known access patterns but are limited in three respects: + +1. **Human comprehension.** Scope strings like `calendar:write` are not self-describing to users or auth agents making approval decisions. + +2. **Machine precision.** Scopes do not express which specific operations are being authorized or distinguish operations that need per-call approval. + +3. **Audit completeness.** Scopes do not identify which specific version of an authorization definition was in effect at the time of approval. + +R3 addresses these by introducing: + +- **Vocabularies** that describe a resource's operations in terms the agent already understands (MCP tools, OpenAPI operations, gRPC methods, etc.) +- **R3 documents**: structured, content-addressed authorization definitions published by the resource and fetched by the AS +- **Vocabulary-based grants** in auth tokens, so resources can enforce authorization directly from claims they understand + +# Conventions and Definitions + +{::boilerplate bcp14-tagged} + +# Terminology + +- **Vocabulary**: A defined scheme for expressing resource operations. Each vocabulary corresponds to an API description format (MCP, OpenAPI, gRPC, GraphQL, AsyncAPI, WSDL, OData). Resources advertise which vocabularies they support; agents use them to request access. +- **R3 Document**: A JSON document published by a resource, describing the operations it provides and the consequences of granting access. Identified by the SHA-256 hash of its content. Fetched by both the PS (for user consent using `display` fields) and the AS (for policy evaluation using `operations`); not accessible to agents. +- **R3 URI (`r3_uri`)**: A URI identifying an R3 document. Included in a resource token. +- **R3 Hash (`r3_s256`)**: A SHA-256 hash of the R3 document, base64url-encoded without padding. Included alongside `r3_uri` in the resource token and the auth token. + +# Vocabularies + +Resources advertise their supported vocabularies in well-known metadata. Each vocabulary maps to an API description format that agents already know how to discover and parse. + +## Resource Metadata Extensions + +R3 extends the `/.well-known/aauth-resource.json` document defined in AAuth Protocol ([@!I-D.hardt-aauth-protocol]): + +```json +{ + "resource": "https://calendar.example.com", + "r3_vocabularies": { + "urn:aauth:vocabulary:mcp": "https://calendar.example.com/mcp", + "urn:aauth:vocabulary:openapi": "https://calendar.example.com/openapi.json" + } +} +``` + +**`r3_vocabularies`** (OPTIONAL). A JSON object mapping vocabulary URIs to their discovery endpoints. Keys MUST be vocabulary URIs from the `urn:aauth:vocabulary:` namespace for standard vocabularies defined in this document, or third-party URI namespaces for proprietary vocabularies. Values are vocabulary-specific discovery endpoints (the MCP server URL, the OpenAPI spec URL, the gRPC reflection endpoint, etc.). A resource MAY advertise multiple vocabularies simultaneously. + +## Standard Vocabularies + +This document defines seven standard vocabularies. Third parties MAY define additional vocabularies using their own URI namespaces. Each vocabulary defines: the vocabulary URI, the structure of operation requests, how the resource maps operations to R3 documents, and the discovery endpoint. + +Standard vocabularies use the `urn:aauth:vocabulary:` namespace. + +### MCP Vocabulary (`urn:aauth:vocabulary:mcp`) {#mcp-vocabulary} + +For resources that expose an MCP server. The discovery endpoint is the MCP server URL. Agents discover available tool names via MCP tool discovery. + +Each operation entry contains: + +- **`tool`** (REQUIRED). The MCP tool name as advertised by the MCP server's tool discovery. + +```json +{ + "vocabulary": "urn:aauth:vocabulary:mcp", + "operations": [ + { "tool": "create_calendar_event" }, + { "tool": "modify_calendar_event" } + ] +} +``` + +### OpenAPI Vocabulary (`urn:aauth:vocabulary:openapi`) {#openapi-vocabulary} + +For resources that expose an OpenAPI-described HTTP API. The discovery endpoint is the OpenAPI specification URL. Agents discover available operations by fetching and parsing the spec. + +Each operation entry contains: + +- **`operationId`** (REQUIRED). The `operationId` as defined in the OpenAPI specification. + +```json +{ + "vocabulary": "urn:aauth:vocabulary:openapi", + "operations": [ + { "operationId": "createEvent" }, + { "operationId": "updateEvent" } + ] +} +``` + +### gRPC Vocabulary (`urn:aauth:vocabulary:grpc`) {#grpc-vocabulary} + +For resources that expose a gRPC server. The discovery endpoint is the gRPC server reflection endpoint (supporting `grpc.reflection.v1.ServerReflection`) or a hosted `.proto` file URL. + +Each operation entry contains: + +- **`method`** (REQUIRED). The fully qualified gRPC method name in the form `package.ServiceName/MethodName`. + +```json +{ + "vocabulary": "urn:aauth:vocabulary:grpc", + "operations": [ + { "method": "calendar.CalendarService/CreateEvent" }, + { "method": "calendar.CalendarService/UpdateEvent" } + ] +} +``` + +### GraphQL Vocabulary (`urn:aauth:vocabulary:graphql`) {#graphql-vocabulary} + +For resources that expose a GraphQL API. The discovery endpoint is the GraphQL endpoint. Agents discover available operations via GraphQL introspection (`__schema` query). + +Each operation entry contains: + +- **`operation`** (REQUIRED). The GraphQL operation name. MUST be a named query, mutation, or subscription. +- **`type`** (REQUIRED). One of `query`, `mutation`, or `subscription`. + +```json +{ + "vocabulary": "urn:aauth:vocabulary:graphql", + "operations": [ + { "operation": "CreateCalendarEvent", "type": "mutation" }, + { "operation": "GetCalendarEvents", "type": "query" } + ] +} +``` + +### AsyncAPI Vocabulary (`urn:aauth:vocabulary:asyncapi`) {#asyncapi-vocabulary} + +For resources that emit events described by AsyncAPI. The discovery endpoint is the AsyncAPI specification URL. + +- **`operationId`** (REQUIRED). The `operationId` as defined in the AsyncAPI specification. +- **`action`** (OPTIONAL). The AsyncAPI action type: `send` or `receive`. Agents subscribing to events use `receive`. + +When a resource grants an agent an AsyncAPI subscription operation via R3, the actual subscription registration and event delivery use the AAuth Events protocol ([@?I-D.hardt-aauth-events]). The resource issues a subscription ticket URL in response to the authenticated request; the agent then completes subscription registration using a subscribe token per AAuth Events. + +```json +{ + "vocabulary": "urn:aauth:vocabulary:asyncapi", + "operations": [ + { "operationId": "publishCalendarUpdate", "action": "send" }, + { "operationId": "receiveCalendarEvent", "action": "receive" } + ] +} +``` + +### WSDL Vocabulary (`urn:aauth:vocabulary:wsdl`) {#wsdl-vocabulary} + +For resources that expose a SOAP/WSDL-described web service. The discovery endpoint is the WSDL document URL. + +Each operation entry contains: + +- **`operation`** (REQUIRED). The operation name as defined in the WSDL `portType` or `binding`. +- **`service`** (OPTIONAL). The WSDL service name, for disambiguation when multiple services expose the same operation name. + +```json +{ + "vocabulary": "urn:aauth:vocabulary:wsdl", + "operations": [ + { "operation": "CreateCalendarEvent", "service": "CalendarService" } + ] +} +``` + +### OData Vocabulary (`urn:aauth:vocabulary:odata`) {#odata-vocabulary} + +For resources that expose an OData service. The discovery endpoint is the OData service root URL. Agents discover entity sets, functions, and actions via the `$metadata` document. + +Each operation entry contains: + +- **`operation`** (REQUIRED). An entity set name, a bound function (`EntitySet/FunctionName`), or a bound action (`EntitySet/ActionName`). +- **`methods`** (OPTIONAL). An array of HTTP methods for entity set CRUD (e.g., `["GET", "POST"]`). Omitted for bound functions and actions. + +```json +{ + "vocabulary": "urn:aauth:vocabulary:odata", + "operations": [ + { "operation": "Events", "methods": ["GET", "POST", "PATCH"] }, + { "operation": "Events/SendCancellation" } + ] +} +``` + +# Authorization Endpoint Extensions {#authorization-endpoint-extensions} + +R3 extends the authorization endpoint defined in AAuth Protocol ([@!I-D.hardt-aauth-protocol]) with an `r3_operations` request parameter. When an agent wants to declare intended operations using a resource's vocabulary, it includes `r3_operations` in the authorization endpoint request body. + +## Request + +The agent sends `r3_operations` in the authorization endpoint request: + +```http +POST /authorize HTTP/1.1 +Host: calendar.example.com +Content-Type: application/json +Signature-Input: sig=("@method" "@authority" "@path" "signature-key");created=1741824000 +Signature: sig=:...signature bytes...: +Signature-Key: sig=jwt;jwt="eyJhbGc..." + +{ + "r3_operations": { + "vocabulary": "urn:aauth:vocabulary:openapi", + "operations": [ + { "operationId": "createEvent" }, + { "operationId": "updateEvent" } + ] + } +} +``` + +**`r3_operations`** (OPTIONAL). An object containing: + +- **`vocabulary`** (REQUIRED). A URI identifying the vocabulary. MUST be supported by the resource as advertised in `r3_vocabularies`. +- **`operations`** (REQUIRED). An array of operation requests. Structure is vocabulary-specific; see {{mcp-vocabulary}} through {{odata-vocabulary}}. + +When `r3_operations` is present, the resource maps the declared operations to an appropriate R3 document and includes `r3_uri` and `r3_s256` in the resource token. When `r3_operations` is absent, the resource MAY still include R3 claims in the resource token based on its own policy. + +## Response + +The resource returns a resource token as defined in AAuth Protocol ([@!I-D.hardt-aauth-protocol]), extended with R3 claims: + +```json +{ + "resource_token": "eyJhbGciOiJFUzI1NiJ9..." +} +``` + +The resource token contains `r3_uri` and `r3_s256` identifying the R3 document that covers the requested operations. The resource's internal mapping from operations to R3 documents is opaque to the agent. + +## Operations Spanning Multiple Definitions {#operations-spanning-definitions} + +A resource MAY organize its authorization definitions into multiple R3 documents internally — for example, one document per scope or operation group. A single resource token carries exactly one `r3_uri`/`r3_s256` pair, and the resulting auth token therefore pins exactly one R3 document. + +When an agent's `r3_operations` request includes operations that the resource maps to more than one of its internal definitions, the resource MUST compose a single R3 document that covers all of the requested operations and reference that composed document in the resource token: + +- **`operations`** is the union of the requested operations, expressed in the request's vocabulary. +- **`display`** describes the combined access. The resource MAY merge the `display` sections of the underlying definitions (for example, concatenating `implications` and unioning `data_accessed`) or author a purpose-built summary for the combination. + +The composed document is served at a fresh content-addressed `r3_uri` exactly like any other R3 document (see {{content-addressing}}): the resource builds it on the fly and persists the serialized bytes so the hash remains stable across the AS and PS fetches. Because R3 documents are content-addressed, an identical combination of operations reduces to the same hash and MAY be cached and reused across requests. + +This composition is opaque to the agent. The agent sees one `r3_uri`/`r3_s256` in the resource token regardless of how many internal definitions the requested operations were drawn from, and the auth token's `r3_granted` and `r3_conditional` claims express the granted operations against that single composed document. + +# R3 Document {#r3-document} + +An R3 document is a JSON object published by the resource at a URI. It describes the authorization semantics for a class of access: what operations are covered (in vocabulary format), what the access means in human terms, and what consequences it carries. + +The document MUST be served over HTTPS. The resource MUST require a valid HTTP Message Signature on requests to R3 document URIs, and MUST reject requests that are not signed by the resource's AS. Agents cannot fetch R3 documents. + +```json +{ + "version": "2", + "vocabulary": "urn:aauth:vocabulary:mcp", + "operations": [ + { "tool": "create_calendar_event" }, + { "tool": "modify_calendar_event" } + ], + "display": { + "summary": "Create and modify events on your work calendar", + "implications": "Meetings can be scheduled or rescheduled. Existing events can be modified.", + "data_accessed": "Event titles, times, attendees, and descriptions in the work calendar", + "irreversible": "Sent meeting invitations cannot be unsent" + } +} +``` + +## Fields + +**`version`** (RECOMMENDED). A string identifying the version of this R3 document. The combination of `r3_uri` + SHA-256 hash provides content-addressing independent of this field; `version` is for human readability. + +**`vocabulary`** (REQUIRED). The vocabulary URI identifying how operations are expressed. MUST match one of the vocabularies the resource advertises in `r3_vocabularies`. + +**`operations`** (REQUIRED). An array of operations covered by this R3 document, using the vocabulary-specific structure defined in {{mcp-vocabulary}} through {{odata-vocabulary}}. This is the same format used in the agent's `r3_operations` request and in the auth token's `r3_granted` and `r3_conditional` claims. + +**`display`** (RECOMMENDED). Human-readable descriptions of the consequences of granting this access. The resource describes what *it* does, not what the agent intends: + +- `summary` (REQUIRED if `display` present). A short plain-language description suitable for a consent UI or auth agent. +- `implications` (OPTIONAL). Side effects of granting this access: emails sent, records modified, costs incurred. +- `data_accessed` (OPTIONAL). What data becomes visible to the caller. +- `irreversible` (OPTIONAL). Plain-language description of actions that cannot be undone. + +## Content Addressing {#content-addressing} + +The R3 hash (`r3_s256`) is computed as the SHA-256 hash of the bytes of the R3 document as served by the resource, base64url-encoded without padding. + +The resource's serialization is the document. There is no canonicalization step — verifiers hash the bytes received over the wire, not a normalized form. Resources MUST serialize the R3 document once and serve those exact bytes verbatim on every request for the same `r3_uri`. Re-serialization between hash computation and serving (e.g. middleware that parses and re-stringifies JSON, CDN minification, response framework helpers that reorder keys) will produce different bytes and break hash verification. Resources that build R3 documents on the fly SHOULD persist the serialized bytes (e.g. in a key-value store keyed by `r3_uri` or `r3_s256`) rather than re-build the document per request. + +The `r3_s256` hash is the document's identity, not the URI. The AS caches documents by hash. If a resource updates the document at the same URI, existing auth tokens still reference the previous hash (which the AS has cached). New resource tokens reference the new hash. This enables: + +- **Infinite caching by the AS.** A document that verifies against its hash need never be re-fetched. +- **Permanent audit records.** An auth token carrying `r3_s256` identifies the exact authorization semantics that were approved, regardless of subsequent changes at the same URI. + +## Resource Token Extensions + +R3 extends the resource token defined in AAuth Protocol ([@!I-D.hardt-aauth-protocol]) (a JWT with `typ: resource+jwt`) with two additional payload claims. When a resource includes R3 information, it MUST include both. + +Base claims (from AAuth Protocol): +- `iss`: Resource URL +- `dwk`: `aauth-resource.json` +- `aud`: Auth server URL +- `jti`: Unique token identifier +- `agent`: Agent identifier +- `agent_jkt`: JWK Thumbprint of the agent's signing key +- `iat`: Issued at timestamp +- `exp`: Expiration timestamp +- `scope`: Requested scopes (optional) + +R3 extension claims: +- **`r3_uri`** (REQUIRED for R3): The URI where the AS can fetch the R3 document. The AS authenticates itself using an HTTP Message Signature. +- **`r3_s256`** (REQUIRED for R3): The SHA-256 hash of the R3 document at `r3_uri`, base64url-encoded without padding. + +```json +{ + "typ": "resource+jwt", + "alg": "EdDSA", + "kid": "resource-key-1" +} +``` + +```json +{ + "iss": "https://calendar.example.com", + "dwk": "aauth-resource.json", + "aud": "https://as.example.com", + "jti": "rt-8f3a2b", + "agent": "assistant@agent.example", + "agent_jkt": "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs", + "r3_uri": "https://calendar.example.com/r3/a1b2c3d4", + "r3_s256": "aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789abcd", + "iat": 1741824000, + "exp": 1741824300 +} +``` + +Resource tokens MAY include both `scope` (as defined in AAuth Protocol ([@!I-D.hardt-aauth-protocol])) and R3 claims. When both are present, the AS MUST enforce both independently. + +# R3 Processing + +Both the PS and the AS fetch R3 documents, but for different purposes: + +- **The PS** fetches R3 to present the `display` section to the user during consent — summary, implications, data accessed, irreversibility. The PS uses this information to determine whether the request fits the mission scope and to obtain informed user consent. +- **The AS** fetches R3 to evaluate `operations` for policy decisions and to populate `r3_granted` and `r3_conditional` in the auth token. + +Both independently verify `r3_s256` against the fetched document. Because R3 documents are content-addressed, both can cache aggressively by hash. + +## AS Processing + +When the AS receives a resource token containing `r3_uri` and `r3_s256`, it MUST: + +1. Validate the resource token signature per AAuth Protocol ([@!I-D.hardt-aauth-protocol]). +2. Fetch the R3 document at `r3_uri`. The AS MAY use a cached copy if the cache entry was stored with the same `r3_s256` value. +3. Compute the SHA-256 hash of the bytes received and compare it to `r3_s256`. If the hashes do not match, the AS MUST reject the resource token. +4. Record `r3_uri` and `r3_s256` in its audit log alongside the token issuance event, the agent identifier, and the timestamp. +5. Use the `operations` section for policy evaluation. +6. Include `r3_uri`, `r3_s256`, `r3_granted`, and (if applicable) `r3_conditional` in the issued auth token. + +## Caching + +Because R3 documents are content-addressed, the AS MAY cache them by `r3_s256` to avoid redundant fetches. When serving a cached entry, the AS MUST verify that the stored document produces the expected hash. The AS is not required to retain R3 documents beyond their immediate use in token issuance — the AS's audit log records `r3_uri` and `r3_s256`, which is sufficient for later verification by re-fetching. + + +# Auth Token Extensions + +R3 extends the auth token defined in AAuth Protocol ([@!I-D.hardt-aauth-protocol]) (a JWT with `typ: auth+jwt`) with claims for audit provenance and vocabulary-based grants. The resource can enforce authorization directly from these claims. + +Base claims (from AAuth Protocol): +- `iss`: Auth server URL +- `dwk`: `aauth-issuer.json` +- `aud`: Resource URL +- `jti`: Unique token identifier +- `agent`: Agent identifier +- `cnf`: Confirmation claim with `jwk` containing the agent's public key +- `iat`: Issued at timestamp +- `exp`: Expiration timestamp +- `sub`: User identifier (conditional) +- `scope`: Authorized scopes (conditional) + +R3 extension claims: +- **`r3_uri`** (REQUIRED for R3): The URI of the R3 document that was in effect at approval time. +- **`r3_s256`** (REQUIRED for R3): The SHA-256 hash of that R3 document. +- **`r3_granted`** (REQUIRED for R3): Operations the AS fully authorized. The resource serves these immediately. +- **`r3_conditional`** (OPTIONAL): Operations authorized in principle but requiring per-call approval based on the specific parameters the agent provides. + +```json +{ + "typ": "auth+jwt", + "alg": "EdDSA", + "kid": "as-key-1" +} +``` + +```json +{ + "iss": "https://as.example.com", + "dwk": "aauth-issuer.json", + "aud": "https://calendar.example.com", + "jti": "at-9d4c1e", + "agent": "assistant@agent.example", + "sub": "user:alice@example.com", + "cnf": { "jwk": { "kty": "OKP", "crv": "Ed25519", "x": "NzbLsXh8uDCcd..." } }, + "r3_uri": "https://calendar.example.com/r3/a1b2c3d4", + "r3_s256": "aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789abcd", + "r3_granted": { + "vocabulary": "urn:aauth:vocabulary:mcp", + "operations": [ + { "tool": "list_calendar_events" }, + { "tool": "modify_calendar_event" } + ] + }, + "r3_conditional": { + "vocabulary": "urn:aauth:vocabulary:mcp", + "operations": [ + { "tool": "create_calendar_event" } + ] + }, + "iat": 1741824000, + "exp": 1741824900 +} +``` + +**`r3_uri`** and **`r3_s256`** provide audit provenance: a permanent, verifiable record of which R3 document was in effect at approval time. The AS can verify the R3 document by fetching `r3_uri` and checking `r3_s256`. + +**`r3_granted`** and **`r3_conditional`** use the same vocabulary-specific operation format as the R3 document's `operations` field and the agent's `r3_operations` request: + +- **`vocabulary`** (REQUIRED). The vocabulary URI. +- **`operations`** (REQUIRED). An array of operations using the vocabulary-specific structure. The AS MAY narrow the grant to fewer operations than defined in the R3 document. + +The distinction: `r3_granted` operations are fully authorized and the resource serves them. `r3_conditional` operations require the resource to challenge when the agent actually calls. On the challenge the resource builds a **per-call proposal** ({{per-call-proposals}}) — a content-addressed document carrying the specific parameters of the call — and the AS evaluates those concrete parameters before issuing a per-call auth token. + +## Resource Enforcement + +The resource matches each incoming API call against the auth token claims: + +1. **Match in `r3_granted`**: serve the request. +2. **Match in `r3_conditional`**: build a per-call proposal ({{per-call-proposals}}) and return `AAuth-Requirement` with a resource token referencing it. The AS evaluates the specific call against the proposed parameters. +3. **No match**: reject the request. + +No token introspection or R3 document fetch is needed at enforcement time. The resource uses the vocabulary it already understands. + +When `r3_operations` was not used (the agent received the resource token via a 401 rather than the authorization endpoint), the AS populates `r3_granted` and `r3_conditional` based on the operations defined in the R3 document and its own policy. The AS decides which operations to grant outright and which to make conditional. + +# Per-Call Proposals {#per-call-proposals} + +An `r3_conditional` operation is authorized in principle but not for any specific call: the consequences depend on the concrete parameters the agent supplies (who the email is addressed to, how large the payment is, which record is deleted). When the agent invokes such an operation, the resource challenges the call and the AS re-evaluates it against those parameters before issuing a per-call auth token. + +A **per-call proposal** is the document that carries the specifics of that one pending call. It is an R3 document scoped to a single invocation: same structure, same content-addressing ({{content-addressing}}), and the same AS/PS-only fetch restriction as a class R3 document. Reusing content-addressing keeps tokens small — they carry only the `r3_uri`/`r3_s256` reference, never the parameters — and binds the eventual approval to the exact call that was proposed. + +## Proposal Document + +In addition to the R3 document fields ({{r3-document}}), a per-call proposal carries: + +**`operations`** (REQUIRED). The single conditional operation being invoked, in the resource's vocabulary. + +**`parameters`** (REQUIRED). The concrete parameters of the call, machine-readable, for the AS to evaluate and the resource to bind. A large or sensitive value MAY be represented by a digest object in place of the inline value: + +- `s256` (REQUIRED). `BASE64URL(SHA-256(value-bytes))` of the parameter value as it will be presented at call time. +- `excerpt` (OPTIONAL). A short, human-readable excerpt of the value for display. +- `media_type` (OPTIONAL). The media type of the value. + +**`display`** (RECOMMENDED). Per-call human context for the user's approval decision. In addition to the structured fields defined in {{r3-document}}, a proposal's `display` MAY include a `detail` Markdown string. The following sections are RECOMMENDED as a convention (not normative requirements): `## Action`, `## To` / `## Recipient`, `## Details`, `## Content` (excerpt), `## Irreversible`. + +```json +{ + "vocabulary": "urn:aauth:vocabulary:mcp", + "operations": [ { "tool": "send_email" } ], + "parameters": { + "to": "mom@example.com", + "subject": "Dinner Sunday?", + "body": { "s256": "aBcD…", "excerpt": "Hi Mom, are you free…", "media_type": "text/plain" } + }, + "display": { + "summary": "Send an email as you", + "detail": "## Action\nSend an email\n\n## To\nmom@example.com\n\n## Details\nSubject: Dinner Sunday?\n\n## Content\nHi Mom, are you free…" + } +} +``` + +## Flow + +1. **Conditional challenge.** The agent invokes an `r3_conditional` operation. The resource builds the proposal, persists it keyed by its `r3_s256`, and returns `AAuth-Requirement` with a resource token whose `r3_uri`/`r3_s256` reference the proposal. The token carries only the reference, not the parameters. +2. **Approval.** The AS fetches the proposal and evaluates `parameters` per policy; the PS renders `display` for user consent. On approval, the AS issues a per-call auth token that echoes the proposal's `r3_uri`/`r3_s256` and lists the now-approved operation in `r3_granted`. +3. **Enforced retry.** The agent retries the actual call with the per-call auth token. The resource recovers the proposal from its store via `r3_s256` and MUST verify that the agent's actual parameters match the approved proposal. For any parameter represented as a digest, the resource MUST verify that `BASE64URL(SHA-256(presented-value))` equals the stored `s256`. If anything differs, the resource MUST reject the call. An approval to email one recipient cannot be replayed against another. + +## Large and Sensitive Payloads + +Representing a parameter as a digest keeps large or sensitive payloads out of every token and away from the PS: only the `s256` and a short `excerpt` appear in the proposal. The full bytes travel directly from the agent to the resource at call time, where the resource verifies them against the digest. This is also a privacy control — the resource chooses what the PS (and through it, the user-facing surface) sees versus what stays between the agent and the resource. + +Whether the AS or PS additionally *machine-evaluates* `parameters` (for example, auto-denying a payment over a threshold) or treats the proposal as display-for-human-consent only is deployment policy. The parameters are present in the proposal either way, so machine policy can be layered on without a format change. + +# Security Considerations + +## R3 Document Access Restriction + +The AS MUST authenticate itself when fetching `r3_uri` using an HTTP Message Signature as defined in the AAuth Protocol ([@!I-D.hardt-aauth-protocol]). The resource MUST reject requests not signed by its AS. + +This prevents agents from fetching R3 documents by following the `r3_uri` they carry in the resource token. Since a resource has exactly one AS, the resource only needs to recognize signatures from that AS. The agent opacity property (agents carry the hash of a document they cannot read) depends on this restriction. + +## R3 Endpoint Access Control + +The agent opacity property — agents carry the hash of a document they cannot read — depends entirely on the resource correctly restricting access to R3 document endpoints. If the resource fails to require a valid HTTP Message Signature from its AS on R3 document requests, or accepts signatures from keys other than its AS's, agents can fetch and read R3 documents, breaking the opacity guarantee. Implementations MUST treat R3 endpoint access control as a critical security requirement and SHOULD verify this restriction during deployment testing. + +## Hash Verification + +The AS MUST verify `r3_s256` against the fetched document before using it. Failure to verify allows a resource to serve different content than what was hashed in the resource token. + +## Audit Log Integrity + +The AS MUST write audit log entries atomically with token issuance. An auth token issued without a corresponding audit log entry creates an undetectable gap in the observability record. Implementations SHOULD use transactional writes or equivalent mechanisms. + +## Operation Validation + +For all vocabularies, the resource MUST validate declared operations against its authoritative definition (MCP tool list, OpenAPI spec, `.proto` file, GraphQL schema, AsyncAPI spec, WSDL document, or OData `$metadata`) before issuing a resource token. + +## Grant Enforcement + +Resources MUST enforce `r3_granted` and `r3_conditional` claims in auth tokens. Operations in `r3_granted` define fully authorized access. Operations in `r3_conditional` MUST trigger an `AAuth-Requirement` before being served. The resource MUST reject API calls that do not match an operation in either claim. + +# IANA Considerations + +## JWT Claims Registration + +This document requests registration of the following JWT claims in the IANA JSON Web Token Claims registry: + +| Claim | Description | Reference | +|-------|-------------|-----------| +| `r3_uri` | R3 document URI | This document | +| `r3_s256` | R3 document SHA-256 hash | This document | +| `r3_granted` | Fully authorized operations in vocabulary format | This document | +| `r3_conditional` | Conditionally authorized operations requiring per-call approval | This document | + +## R3 Vocabulary Registry + +This specification establishes the AAuth R3 Vocabulary Registry. The initial contents are: + +| Vocabulary URI | Interface Type | Reference | +|----------------|---------------|-----------| +| `urn:aauth:vocabulary:mcp` | MCP server | This document | +| `urn:aauth:vocabulary:openapi` | HTTP/REST | This document | +| `urn:aauth:vocabulary:grpc` | gRPC | This document | +| `urn:aauth:vocabulary:graphql` | GraphQL | This document | +| `urn:aauth:vocabulary:asyncapi` | Event-driven | This document | +| `urn:aauth:vocabulary:wsdl` | SOAP/WSDL | This document | +| `urn:aauth:vocabulary:odata` | OData | This document | + +New values may be registered following the Specification Required policy ([@!RFC8126]). + +# Implementation Status + +*Note: This section is to be removed before publishing as an RFC.* + +This section records the status of known implementations of the protocol defined by this specification at the time of posting of this Internet-Draft, and is based on a proposal described in [@RFC7942]. The description of implementations in this section is intended to assist the IETF in its decision processes in progressing drafts to RFCs. + +There are currently no known implementations. + +# Document History + +*Note: This section is to be removed before publishing as an RFC.* + +- draft-hardt-aauth-r3-00 + - Initial submission + +# Acknowledgments + +The author would like to thank reviewers for their feedback. + +{backmatter} + +# Design Rationale + +## Why Not RAR + +OAuth 2.0 Rich Authorization Requests ([@RFC9396]) defines `authorization_details` as a structured extension to authorization requests. RAR is a natural reference point for this work. R3 deliberately does not use or profile RAR for the following reasons: + +**Directionality.** RAR is client-declared: the agent constructs `authorization_details` and sends it to the AS. The agent defines what it wants. R3 is resource-declared: the resource defines what access it provides and signs that definition. The agent cannot modify or reframe it. This is the opposite directionality, and the security properties depend on it. + +**Agent opacity.** In R3, the R3 document is fetched by the AS directly from the resource, restricted to AS-only access. The agent carries a hash of a document it cannot read. RAR has no equivalent because the client constructs the authorization details and necessarily knows their content. + +**Content addressing.** R3 uses a content-addressed URI plus SHA-256 hash to pin the exact authorization semantics in effect at approval time. An auth token carrying `r3_uri` and `r3_s256` is a permanent, verifiable record. RAR carries no equivalent versioning or integrity guarantee. + +**Audit trail.** The AS records `r3_uri` in its audit log, creating a durable reference to the exact R3 document version. This is not possible with RAR's inline `authorization_details` structure. + +RAR and R3 are complementary. RAR remains appropriate for client-declared authorization detail. R3 addresses the resource-declared case that RAR was not designed for. + +# Vocabulary Summary + +| Vocabulary URI | Interface Type | Operation Identifier | Discovery Mechanism | +|----------------|---------------|---------------------|---------------------| +| `urn:aauth:vocabulary:mcp` | MCP server | Tool name | MCP tool discovery | +| `urn:aauth:vocabulary:openapi` | HTTP/REST | `operationId` | OpenAPI spec URL | +| `urn:aauth:vocabulary:grpc` | gRPC | `package.Service/Method` | Server reflection or `.proto` URL | +| `urn:aauth:vocabulary:graphql` | GraphQL | Operation name | GraphQL introspection | +| `urn:aauth:vocabulary:asyncapi` | Event-driven | `operationId` | AsyncAPI spec URL | +| `urn:aauth:vocabulary:wsdl` | SOAP/WSDL | Operation name | WSDL document URL | +| `urn:aauth:vocabulary:odata` | OData | Entity set or bound operation | `$metadata` URL | + +# Comparison with RAR + +| Property | RAR ([@RFC9396]) | R3 | +|----------|---------------|----| +| Who declares | Client (agent) | Resource | +| Direction | Client -> AS | Resource -> AS (via agent carrier) | +| Agent visibility | Agent constructs the detail | Agent carries opaque token | +| Versioning | None | Content-addressed URI + hash | +| Audit trail | Inline in request | `r3_uri` recorded by AS | +| Human display | Not specified | `display` section in R3 document | +| Irreversibility signal | Not specified | `display.irreversible` field | diff --git a/aauth-spec/v09/draft-hardt-httpbis-signature-key-06.txt b/aauth-spec/v09/draft-hardt-httpbis-signature-key-06.txt new file mode 100644 index 0000000..028f8f2 --- /dev/null +++ b/aauth-spec/v09/draft-hardt-httpbis-signature-key-06.txt @@ -0,0 +1,2184 @@ + + + + +HTTP D. Hardt +Internet-Draft Hellō +Intended status: Standards Track T. Meunier +Expires: 3 January 2027 Cloudflare + 2 July 2026 + + + HTTP Signature Keys + draft-hardt-httpbis-signature-key-06 + +Abstract + + This document defines two HTTP header fields and one Accept-Signature + parameter for use with HTTP Message Signatures as defined in RFC + 9421. The Signature-Key request header distributes public keys used + to verify signatures, with six initial key distribution schemes: + pseudonymous inline keys (hwk), self-issued key delegation via JWK + Thumbprint JWTs (jkt-jwt), identified signers with JWKS URI discovery + (jwks_uri), JWT-based delegation (jwt), self-issued JWTs (self-jwt), + and X.509 certificate chains (x509). The sigkey parameter extends + Accept-Signature (RFC 9421 Section 5) to indicate the type of + Signature-Key the server requires. The Signature-Error response + header provides structured error information when signature + verification fails. Together, these mechanisms enable flexible trust + models ranging from privacy-preserving pseudonymous verification to + horizontally-scalable delegated authentication and PKI-based identity + chains. + +Discussion Venues + + _Note: This section is to be removed before publishing as an RFC._ + + Source for this draft and an issue tracker can be found at + https://github.com/dickhardt/signature-key + (https://github.com/dickhardt/signature-key). + +Status of This Memo + + This Internet-Draft is submitted in full conformance with the + provisions of BCP 78 and BCP 79. + + Internet-Drafts are working documents of the Internet Engineering + Task Force (IETF). Note that other groups may also distribute + working documents as Internet-Drafts. The list of current Internet- + Drafts is at https://datatracker.ietf.org/drafts/current/. + + + + + + +Hardt & Meunier Expires 3 January 2027 [Page 1] + +Internet-Draft Signature-Keys July 2026 + + + Internet-Drafts are draft documents valid for a maximum of six months + and may be updated, replaced, or obsoleted by other documents at any + time. It is inappropriate to use Internet-Drafts as reference + material or to cite them other than as "work in progress." + + This Internet-Draft will expire on 3 January 2027. + +Copyright Notice + + Copyright (c) 2026 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents (https://trustee.ietf.org/ + license-info) in effect on the date of publication of this document. + Please review these documents carefully, as they describe your rights + and restrictions with respect to this document. Code Components + extracted from this document must include Revised BSD License text as + described in Section 4.e of the Trust Legal Provisions and are + provided without warranty as described in the Revised BSD License. + +Table of Contents + + 1. Conventions and Definitions . . . . . . . . . . . . . . . . . 3 + 2. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 4 + 3. Signature-Key HTTP Request Header . . . . . . . . . . . . . . 5 + 3.1. Label Consistency . . . . . . . . . . . . . . . . . . . . 6 + 3.2. Multiple Signatures . . . . . . . . . . . . . . . . . . . 6 + 3.3. Header Web Key (hwk) . . . . . . . . . . . . . . . . . . 6 + 3.4. JKT JWT Self-Issued Key Delegation (jkt-jwt) . . . . . . 8 + 3.5. JWKS URI Discovery (jwks_uri) . . . . . . . . . . . . . . 11 + 3.6. JWT Confirmation Key (jwt) . . . . . . . . . . . . . . . 12 + 3.7. Self-Issued JWT (self-jwt) . . . . . . . . . . . . . . . 14 + 3.8. X.509 Certificates (x509) . . . . . . . . . . . . . . . . 16 + 4. Accept-Signature sigkey Parameter . . . . . . . . . . . . . . 17 + 4.1. Parameter Definition . . . . . . . . . . . . . . . . . . 17 + 4.2. Label Binding . . . . . . . . . . . . . . . . . . . . . . 18 + 4.3. Response Status Codes . . . . . . . . . . . . . . . . . . 19 + 4.4. sigkey Semantics . . . . . . . . . . . . . . . . . . . . 19 + 4.4.1. jkt . . . . . . . . . . . . . . . . . . . . . . . . . 19 + 4.4.2. uri . . . . . . . . . . . . . . . . . . . . . . . . . 20 + 4.4.3. x509 . . . . . . . . . . . . . . . . . . . . . . . . 20 + 4.5. Incremental Adoption . . . . . . . . . . . . . . . . . . 20 + 4.6. Coexistence with WWW-Authenticate . . . . . . . . . . . . 21 + 4.7. Examples . . . . . . . . . . . . . . . . . . . . . . . . 21 + 4.8. Client Processing . . . . . . . . . . . . . . . . . . . . 22 + 5. Signature-Error HTTP Response Header . . . . . . . . . . . . 22 + 5.1. Header Structure . . . . . . . . . . . . . . . . . . . . 22 + + + +Hardt & Meunier Expires 3 January 2027 [Page 2] + +Internet-Draft Signature-Keys July 2026 + + + 5.2. Response Body . . . . . . . . . . . . . . . . . . . . . . 23 + 5.3. Access Denied . . . . . . . . . . . . . . . . . . . . . . 23 + 5.4. Error Codes . . . . . . . . . . . . . . . . . . . . . . . 23 + 5.4.1. unsupported_algorithm . . . . . . . . . . . . . . . . 23 + 5.4.2. invalid_signature . . . . . . . . . . . . . . . . . . 24 + 5.4.3. invalid_input . . . . . . . . . . . . . . . . . . . . 24 + 5.4.4. invalid_request . . . . . . . . . . . . . . . . . . . 24 + 5.4.5. invalid_key . . . . . . . . . . . . . . . . . . . . . 24 + 5.4.6. unknown_key . . . . . . . . . . . . . . . . . . . . . 25 + 5.4.7. invalid_jwt . . . . . . . . . . . . . . . . . . . . . 25 + 5.4.8. expired_jwt . . . . . . . . . . . . . . . . . . . . . 25 + 6. Security Considerations . . . . . . . . . . . . . . . . . . . 25 + 6.1. Key Validation . . . . . . . . . . . . . . . . . . . . . 25 + 6.2. Caching and Performance . . . . . . . . . . . . . . . . . 26 + 6.3. Scheme-Specific Risks . . . . . . . . . . . . . . . . . . 26 + 6.4. Algorithm Selection . . . . . . . . . . . . . . . . . . . 28 + 6.5. Signature-Key Integrity . . . . . . . . . . . . . . . . . 28 + 7. Privacy Considerations . . . . . . . . . . . . . . . . . . . 28 + 7.1. Pseudonymity vs. Identity . . . . . . . . . . . . . . . . 29 + 7.2. Key Discovery Tracking . . . . . . . . . . . . . . . . . 29 + 7.3. JWT Contents . . . . . . . . . . . . . . . . . . . . . . 29 + 8. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 30 + 8.1. HTTP Field Name Registration . . . . . . . . . . . . . . 30 + 8.2. Signature-Key Scheme Registry . . . . . . . . . . . . . . 30 + 8.2.1. Registration Procedure . . . . . . . . . . . . . . . 30 + 8.2.2. Initial Registry Contents . . . . . . . . . . . . . . 31 + 8.2.3. Registration Template . . . . . . . . . . . . . . . . 31 + 8.3. HTTP Signature Metadata Parameters . . . . . . . . . . . 31 + 8.4. URN Sub-namespace Registration . . . . . . . . . . . . . 32 + 8.5. Signature Error Code Registry . . . . . . . . . . . . . . 32 + 8.5.1. Initial Registry Contents . . . . . . . . . . . . . . 32 + 9. Document History . . . . . . . . . . . . . . . . . . . . . . 33 + 10. Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . 34 + 11. References . . . . . . . . . . . . . . . . . . . . . . . . . 34 + 11.1. Normative References . . . . . . . . . . . . . . . . . . 34 + 11.2. Informative References . . . . . . . . . . . . . . . . . 36 + Appendix A. Design Rationale . . . . . . . . . . . . . . . . . . 36 + A.1. Why jwks_uri Instead of Inline JWKS? . . . . . . . . . . 36 + A.2. Why a Separate Header? . . . . . . . . . . . . . . . . . 37 + A.3. Why Schemes Instead of Just a Key and Key ID? . . . . . . 38 + A.4. Why Strings Instead of Byte Sequences for hwk? . . . . . 38 + Authors' Addresses . . . . . . . . . . . . . . . . . . . . . . . 38 + +1. Conventions and Definitions + + {::boilerplate bcp14-tagged} + + + + + +Hardt & Meunier Expires 3 January 2027 [Page 3] + +Internet-Draft Signature-Keys July 2026 + + +2. Introduction + + HTTP Message Signatures [RFC9421] provides a powerful mechanism for + creating and verifying digital signatures over HTTP messages. To + verify a signature, the verifier needs the signer's public key. + While RFC 9421 defines signature creation and verification + procedures, it intentionally leaves key distribution to application + protocols, recognizing that different deployments have different + trust requirements. + + This document defines: + + * *Signature-Key* (Signature-Key HTTP Request Header (#signature- + key-http-request-header)) — a request header that distributes + public keys for HTTP Message Signature verification. The header + supports six schemes, each designed for different trust models and + operational requirements: + + 1. *Header Web Key (hwk)* - Self-contained public keys for + pseudonymous verification + 2. *JKT JWT (jkt-jwt)* - Self-issued key delegation via JWK + Thumbprint JWTs ("jacket jot") + 3. *JWKS URI (jwks_uri)* - Identified signers with key discovery + via metadata + 4. *JWT (jwt)* - Delegated keys embedded in signed JWTs for + horizontal scale + 5. *Self-Issued JWT (self-jwt)* - Self-signed JWTs where the + signer and issuer are the same party + 6. *X.509 (x509)* - Certificate-based verification with PKI trust + chains + + Additional schemes may be defined through the IANA registry + established by this document. + + * *sigkey* (Accept-Signature sigkey Parameter (#accept-signature- + sigkey-parameter)) — a parameter for the Accept-Signature header + ([RFC9421], Section 5) that indicates the type of Signature-Key + the server requires. This extends RFC 9421's existing mechanism + for requesting signatures rather than defining a new header. + + * *Signature-Error* (Signature-Error HTTP Response Header + (#signature-error-http-response-header)) — a response header that + provides structured error information when signature verification + fails, enabling clients to diagnose and correct signing issues. + + The Signature-Key header works in conjunction with the Signature- + Input and Signature headers defined in RFC 9421, using matching + labels to correlate signature metadata with keying material. + + + +Hardt & Meunier Expires 3 January 2027 [Page 4] + +Internet-Draft Signature-Keys July 2026 + + +3. Signature-Key HTTP Request Header + + The Signature-Key header provides the public key or key reference + needed to verify an HTTP Message Signature. It is a Structured Field + Dictionary [RFC8941] keyed by signature label, where each member + describes how to obtain the verification key for the corresponding + signature. + + *Format:* + + Signature-Key: