docs(adr-0015): accept rotation event envelope placement (credentialSubject.keyRotation)#458
Conversation
There was a problem hiding this comment.
Pull request overview
Pins a proposed canonical wire-format placement for ADR-0015 rotation events by adding an amendment that specifies credentialSubject.keyRotation, plus a worked receipt test vector intended to let verifiers/SDKs plan against a stable envelope shape.
Changes:
- Adds a Proposed amendment to ADR-0015 specifying rotation-event placement at
credentialSubject.keyRotationand verifier traversal steps. - Introduces a rotation-event example receipt JSON fixture under
spec/test-vectors/rotation-event/. - Documents the fixture, derived values, and expected canonical-body SHA-256 in a new README.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| docs/adr/0015-key-rotation-byok-anchoring.md | Adds a proposed amendment defining rotation event envelope placement and verification steps. |
| spec/test-vectors/rotation-event/example.json | Adds a worked example receipt containing credentialSubject.keyRotation. |
| spec/test-vectors/rotation-event/README.md | Documents the rotation-event fixture inputs/derived values and intended usage. |
…r hygiene, ADR-0017 links) Four substantive fixes from PR #458 Copilot review pass. Two comments about `||`-prefixed markdown tables were false positives (no such pattern exists in the files; verified with grep) and are not addressed. * **Chain position made replayable.** The fixture was `sequence: 42` with an all-zero `previous_receipt_hash`, which is not a valid chain link — there is no real predecessor whose canonical hash is all zeros. Changed to genesis position (`sequence: 1`, `previous_receipt_hash: null`) so the linkage semantics are sound end-to-end without shipping a separate predecessor receipt. Canonical-bytes SHA-256 and Ed25519 signature recomputed using sdk/go's existing `Canonicalize` helper to keep the README's "recomputable from the keys above" claim accurate: - Old hash `sha256:17e1...b517` → new `sha256:6983c9bd6fb24e844b90f7616315a914fdedc5fef8126e11d46149ba2f320457`. - Signature updated accordingly. * **Secret-scanner hygiene.** Removed the inlined outgoing seed hex (RFC 8032 §7.1 TEST 2). Reviewers reproduce the signature by fetching the seed from the RFC directly; secret scanners no longer trip on a raw 32-byte hex literal that "looks like" a key regardless of provenance. * **ADR-0017 links.** The two `./0017-central-receipt-hub.md` relative links rendered as broken on `main` because ADR-0017 is not yet merged. Switched both to the full GitHub URL on the draft branch (`claude/draft-adr-0017-Ko7cn`) with an inline note that the relative form is restored once ADR-0017 lands.
…er hygiene, table swap, supersession note) Six fixes from PR #459 Copilot review pass. Schema tightening: * Add base64url `pattern` and exact-43-char length constraint to recipient `enc` (the encoded size of a 32-byte X25519 ephemeral public key — load-bearing, not a range). * Add base64url `pattern` and `minLength: 22` to `ct` — accepts any AEAD output but rejects `=` padding, `+`, `/`. minLength floors at the 16-byte GCM tag's encoded size. Test-vector hygiene: * Remove inlined X25519 `private_key_hex` from both `test_recipients` entries and remove `decrypt_with_private_key_hex` from each vector's `round_trip`. Even RFC-published private keys trip secret-scanning tooling and encourage cargo-cult reuse. Reviewers running the decryption round-trip fetch the private keys from RFC 7748 §6.1 directly; the README points there explicitly. Placeholder / shape-invariants reconciliation: * The `<enc-bytes-base64url-unpadded>` placeholders are syntactically invalid base64url by design (so copy-pasting into production fails loudly). Reword `shape_invariants` to scope the rules to SDK-produced envelopes, and add an explicit `computed_values_status` → `"computed"` flip-rule explaining when the rules begin applying to a specific vector. The envelope README's "Reproducibility" section is updated to match. The placeholders themselves stay angle-bracket form rather than syntactically-valid-but-fake base64url, to keep the fail-loud property. Table correction: * The "Phase C migration cost" row in the HPKE-vs-sealed-box comparison had the columns swapped — high migration cost was shown under HPKE; it belongs under sealed-box (since shipping sealed-box for v1 is what creates the cost). Reworded the row label to drop the awkward "if v1 ships sealed-box" conditional and swapped the cells so each column reflects its own choice. Supersession note: * The original ADR-0012 *Forward-compatible envelope shape* illustration uses `v: 1` (integer), includes `nonce`, and names the field `encap`. Added an explicit supersession note in the amendment's *Scope* paragraph so readers don't implement the outdated sketch. ADR-0017 cross-references repointed to the draft branch URL (matches the equivalent fix on PR #458) since ADR-0017 is not yet on `main`.
…entialSubject.keyRotation) Proposes `credentialSubject.keyRotation` as the wire-format placement for the rotation event ADR-0015 itself explicitly deferred. The placement is flagged for human sign-off before merge — the three placement options (a/b/c) are compared in the PR body and (a) is the leading recommendation, not yet locked in. Adds: - ADR-0015 amendment formalising the proposed placement and the verifier-side rotation traversal against it (no change to the original decision sections; appended under a new Amendments heading). - spec/test-vectors/rotation-event/example.json — a worked rotation receipt in canonical form, signed with the RFC 8032 §7.1 TEST 2 key. - spec/test-vectors/rotation-event/README.md — keys, derived values, scope. Deliberately NOT in this PR: - No changes to spec/schema/agent-receipt.schema.json. The current schema already permits the fixture because `credentialSubject` omits `additionalProperties: false` (verified with ajv against the example). Tightening the schema with a normative `keyRotation` `$ref` is a follow-up gated on the placement being accepted. Unblocks ADR-0017 Preconditions item 2 (rotation event canonical wire format).
…r hygiene, ADR-0017 links) Four substantive fixes from PR #458 Copilot review pass. Two comments about `||`-prefixed markdown tables were false positives (no such pattern exists in the files; verified with grep) and are not addressed. * **Chain position made replayable.** The fixture was `sequence: 42` with an all-zero `previous_receipt_hash`, which is not a valid chain link — there is no real predecessor whose canonical hash is all zeros. Changed to genesis position (`sequence: 1`, `previous_receipt_hash: null`) so the linkage semantics are sound end-to-end without shipping a separate predecessor receipt. Canonical-bytes SHA-256 and Ed25519 signature recomputed using sdk/go's existing `Canonicalize` helper to keep the README's "recomputable from the keys above" claim accurate: - Old hash `sha256:17e1...b517` → new `sha256:6983c9bd6fb24e844b90f7616315a914fdedc5fef8126e11d46149ba2f320457`. - Signature updated accordingly. * **Secret-scanner hygiene.** Removed the inlined outgoing seed hex (RFC 8032 §7.1 TEST 2). Reviewers reproduce the signature by fetching the seed from the RFC directly; secret scanners no longer trip on a raw 32-byte hex literal that "looks like" a key regardless of provenance. * **ADR-0017 links.** The two `./0017-central-receipt-hub.md` relative links rendered as broken on `main` because ADR-0017 is not yet merged. Switched both to the full GitHub URL on the draft branch (`claude/draft-adr-0017-Ko7cn`) with an inline note that the relative form is restored once ADR-0017 lands.
…er hygiene, table swap, supersession note) Six fixes from PR #459 Copilot review pass. Schema tightening: * Add base64url `pattern` and exact-43-char length constraint to recipient `enc` (the encoded size of a 32-byte X25519 ephemeral public key — load-bearing, not a range). * Add base64url `pattern` and `minLength: 22` to `ct` — accepts any AEAD output but rejects `=` padding, `+`, `/`. minLength floors at the 16-byte GCM tag's encoded size. Test-vector hygiene: * Remove inlined X25519 `private_key_hex` from both `test_recipients` entries and remove `decrypt_with_private_key_hex` from each vector's `round_trip`. Even RFC-published private keys trip secret-scanning tooling and encourage cargo-cult reuse. Reviewers running the decryption round-trip fetch the private keys from RFC 7748 §6.1 directly; the README points there explicitly. Placeholder / shape-invariants reconciliation: * The `<enc-bytes-base64url-unpadded>` placeholders are syntactically invalid base64url by design (so copy-pasting into production fails loudly). Reword `shape_invariants` to scope the rules to SDK-produced envelopes, and add an explicit `computed_values_status` → `"computed"` flip-rule explaining when the rules begin applying to a specific vector. The envelope README's "Reproducibility" section is updated to match. The placeholders themselves stay angle-bracket form rather than syntactically-valid-but-fake base64url, to keep the fail-loud property. Table correction: * The "Phase C migration cost" row in the HPKE-vs-sealed-box comparison had the columns swapped — high migration cost was shown under HPKE; it belongs under sealed-box (since shipping sealed-box for v1 is what creates the cost). Reworded the row label to drop the awkward "if v1 ships sealed-box" conditional and swapped the cells so each column reflects its own choice. Supersession note: * The original ADR-0012 *Forward-compatible envelope shape* illustration uses `v: 1` (integer), includes `nonce`, and names the field `encap`. Added an explicit supersession note in the amendment's *Scope* paragraph so readers don't implement the outdated sketch. ADR-0017 cross-references repointed to the draft branch URL (matches the equivalent fix on PR #458) since ADR-0017 is not yet on `main`.
e120369 to
9d3adc0
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
docs/adr/0015-key-rotation-byok-anchoring.md:206
- This section is internally inconsistent: it declares the amendment Accepted, but later calls the
keyRotationfield mapping “normative, pending sign-off”. Please make the status and normative language consistent (either remove the “pending sign-off” language, or change the amendment status/wording to Proposed throughout).
**What lives in `keyRotation` (normative, pending sign-off).**
| Field | Type | Description |
|---|---|---|
| `event_type` | string | Constant `"key_rotated"`. |
| `new_public_key` | string | Incoming public key inline, raw bytes per the algorithm's canonical encoding, multibase-`u`-prefixed base64url (per ADR-0001). For Ed25519, the 32 raw bytes per RFC 8032 §5.1.5. |
| `old_key_fingerprint` | string | `sha256:<lowercase hex>` of the outgoing public key (raw bytes — *not* SPKI/PEM, *not* a backend handle). |
| `new_key_fingerprint` | string | `sha256:<lowercase hex>` of the incoming public key (same canonical-bytes rule). |
| `old_algorithm` | string | Algorithm tag of the outgoing key (e.g. `"ed25519"`). |
| `new_algorithm` | string | Algorithm tag of the incoming key. Equal to `old_algorithm` for same-algorithm rotations. |
| `signed_with` | string | Constant `"old"`. The receipt's `proof.proofValue` is computed with the outgoing key. |
`keyRotation` is **optional**. A receipt that is not a rotation event MUST NOT include it. A receipt that is a rotation event MUST include all seven fields.
The receipt's `action.type` SHOULD be `"agent.key.rotate"` (taxonomy entry to be registered alongside the schema-integration follow-up) so that downstream filters can locate rotation receipts without parsing `credentialSubject.keyRotation`. The presence of `keyRotation` is authoritative; `action.type` is the index hint.
docs/adr/0015-key-rotation-byok-anchoring.md:183
- The ADR links to a draft on a named branch (
blob/claude/...). This is likely to rot once the branch is deleted or rebased. Consider linking to a stable reference (issue/PR, or a commit permalink), and/or keep the relative link commented as a TODO once ADR-0017 lands onmain.
**Status of this amendment:** *Accepted* (2026-05-19). Placement option (a) `credentialSubject.keyRotation` is the wire format for rotation events. The fields above (`event_type`, `new_public_key`, `old_key_fingerprint`, `new_key_fingerprint`, `old_algorithm`, `new_algorithm`, `signed_with`) and their semantics are unchanged by this amendment — only their envelope-side carriage is pinned. This amendment closes the gap the original *Rotation event schema* section flagged with "Wire-format placement is out of scope for this ADR." See [ADR-0017 (draft)](https://github.com/agent-receipts/ar/blob/claude/draft-adr-0017-Ko7cn/docs/adr/0017-central-receipt-hub.md) *Preconditions* (rotation-event canonical wire format) for the consumer that requires this pin. The ADR-0017 file is not yet on `main` — the relative link will be repointed at `./0017-central-receipt-hub.md` once ADR-0017 lands.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
spec/test-vectors/rotation-event/README.md:33
- The markdown table here starts each row with
||, which introduces an empty first column and renders the table misaligned on GitHub. Use a single leading|for each row (consistent with other ADR tables in this repo).
| Role | RFC 8032 vector | Public key (hex) |
|---|---|---|
| Outgoing (signs the rotation) | TEST 2 | `3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c` |
| Incoming | TEST 3 | `fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025` |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
docs/adr/0015-key-rotation-byok-anchoring.md:197
- The reference to ADR-0001 here is misleading: ADR-0001 specifies multibase-
ubase64url encoding forproof.proofValue, but it does not currently define an encoding for public keys. Consider either (1) updating the citation to the normative place that definesnew_public_keyencoding, or (2) explicitly stating thatnew_public_keyreuses the same multibase/base64url encoding asproofValueand documenting that rule normatively (in this ADR/spec).
| `event_type` | string | Constant `"key_rotated"`. |
| `new_public_key` | string | Incoming public key inline, raw bytes per the algorithm's canonical encoding, multibase-`u`-prefixed base64url (per ADR-0001). For Ed25519, the 32 raw bytes per RFC 8032 §5.1.5. |
…blic-key citation
…KE) (#459) * docs(adr-0012): pin disclosure envelope canonical shape (proposed: HPKE) * docs(adr-0012): address Copilot review (schema patterns, secret-scanner hygiene, table swap, supersession note) Six fixes from PR #459 Copilot review pass. Schema tightening: * Add base64url `pattern` and exact-43-char length constraint to recipient `enc` (the encoded size of a 32-byte X25519 ephemeral public key — load-bearing, not a range). * Add base64url `pattern` and `minLength: 22` to `ct` — accepts any AEAD output but rejects `=` padding, `+`, `/`. minLength floors at the 16-byte GCM tag's encoded size. Test-vector hygiene: * Remove inlined X25519 `private_key_hex` from both `test_recipients` entries and remove `decrypt_with_private_key_hex` from each vector's `round_trip`. Even RFC-published private keys trip secret-scanning tooling and encourage cargo-cult reuse. Reviewers running the decryption round-trip fetch the private keys from RFC 7748 §6.1 directly; the README points there explicitly. Placeholder / shape-invariants reconciliation: * The `<enc-bytes-base64url-unpadded>` placeholders are syntactically invalid base64url by design (so copy-pasting into production fails loudly). Reword `shape_invariants` to scope the rules to SDK-produced envelopes, and add an explicit `computed_values_status` → `"computed"` flip-rule explaining when the rules begin applying to a specific vector. The envelope README's "Reproducibility" section is updated to match. The placeholders themselves stay angle-bracket form rather than syntactically-valid-but-fake base64url, to keep the fail-loud property. Table correction: * The "Phase C migration cost" row in the HPKE-vs-sealed-box comparison had the columns swapped — high migration cost was shown under HPKE; it belongs under sealed-box (since shipping sealed-box for v1 is what creates the cost). Reworded the row label to drop the awkward "if v1 ships sealed-box" conditional and swapped the cells so each column reflects its own choice. Supersession note: * The original ADR-0012 *Forward-compatible envelope shape* illustration uses `v: 1` (integer), includes `nonce`, and names the field `encap`. Added an explicit supersession note in the amendment's *Scope* paragraph so readers don't implement the outdated sketch. ADR-0017 cross-references repointed to the draft branch URL (matches the equivalent fix on PR #458) since ADR-0017 is not yet on `main`. * docs(adr-0012): accept HPKE as v1 primitive (sign-off received) * docs(adr-0012): replace branch-specific ADR-0017 URLs with stable text references * docs(adr-0012): address third Copilot review (phase label, HPKE status, comment accuracy) * fix(schema): enforce decodable base64url length in ct pattern * fix(schema): raise ct minLength from 22 to 24 (v1 plaintext minimum is {} = 2 bytes)
Context
ADR-0015 (Key Rotation, BYOK, External Anchoring) defines a
key_rotatedevent with seven fields (event_type,new_public_key,old_key_fingerprint,new_key_fingerprint,old_algorithm,new_algorithm,signed_with) but explicitly defers where they live in the verifier-facing AgentReceipt envelope:ADR-0017 names "rotation event canonical wire format" as a precondition for hub implementation. This PR pins that placement so the hub re-verifier and three SDKs can plan against a fixed wire shape.
Placement decision: option (a)
credentialSubject.keyRotation— accepted 2026-05-19.The one-pager: three placement options
The fields and their semantics from ADR-0015 are unchanged. The only question is the envelope-side carriage.
(a)
credentialSubject.keyRotationextension namespace — acceptedRotation events live in a dedicated sub-object on
credentialSubject, additive to today'sprincipal/action/outcome/chainsiblings. Action and outcome semantics stay clean (they continue to describe external tool calls); rotation gets its own vocabulary.Crucially, today's
spec/schema/agent-receipt.schema.jsondeclaresadditionalProperties: falseon the root and on most nested objects, but thecredentialSubject$def(line 372) omits that constraint. ADR-0003 §"Subset compliance" calls this out explicitly: extension fields withincredentialSubjectare permitted by the schema. The fixture in this PR validates against the current schema today — confirmed withajv validate -s spec/schema/agent-receipt.schema.json -d spec/test-vectors/rotation-event/example.json --spec=draft2020 -c ajv-formats→valid. Schema tightening (adding a normativekeyRotation$ref) is a follow-up, not a blocker.(b) Reuse
action.type = "key_rotated"Why not:
actiontoday represents external tool calls — it carriestarget.system,target.resource,parameters_hash,risk_level,timestamp, and (in 0.2.0)outcome.response_hash. Reusing it as a polymorphic envelope erodes the type's clarity —targetandparameters_hashhave no meaning for a rotation. Daemon-internal events and tool calls are different vocabularies; collapsing them is the wrong abstraction.(c) New top-level field on the receipt envelope
Why not: the schema's root does set
additionalProperties: false(line 17). A top-levelkeyRotationfield forces every verifier in the wild to bump its allowed-keys set or fail closed — the biggest possible blast radius for what is conceptually a sub-namespace of the existing credential subject. Reserve top-level extension for cases where a sub-namespace genuinely cannot represent the semantics; rotation events are not that case.What's in this PR
docs/adr/0015-key-rotation-byok-anchoring.md— appended## Amendmentssection with one entry dated 2026-05-18, accepted 2026-05-19. The original decision sections are untouched. The amendment contains: the placement decision and rationale, the seven-field table mapped intokeyRotation, the verifier-side traversal restated as four normative steps, backward-compatibility rules ("absence means no rotation, not malformed"), and the schema-version recommendation.spec/test-vectors/rotation-event/example.json— one worked rotation receipt in pretty-printed form (the README documents the canonical-form bytes alongside). Signed end-to-end with RFC 8032 §7.1 TEST 2 (outgoing) over the canonicalised body. All values are deterministically recomputable from the test keys.spec/test-vectors/rotation-event/README.md— the test-key origin, derived fingerprints, the canonical-bytes SHA-256 (sha256:6983c9bd6fb24e844b90f7616315a914fdedc5fef8126e11d46149ba2f320457), and what is deliberately out of scope.What's deliberately NOT in this PR
spec/schema/agent-receipt.schema.json. Schema integration (adding akeyRotation$refand bumping theversionenum from["0.1.0","0.2.0","0.2.1"]to include"0.3.0") is a follow-up gated on the placement being accepted. The fixture already validates against today's schema unchanged, so this PR commits to nothing schema-side.cross-sdk-tests/waits on SDK implementations.Schema version
Recommend a
0.2.1 → 0.3.0bump when schema integration lands. Rationale matches ADR-0008's0.1.0 → 0.2.0precedent: schema version as a security signal —0.3.0tells a verifier the issuer operates in an environment where rotation events are expressible. Receipts that pre-date the bump are unaffected (their canonical bytes do not change) and remain valid under their original version.Sibling spec PRs
This is one of three coordinated spec PRs derisking ADR-0017's preconditions:
claude/spec-did-key-resolution—did:keyvalue shape and resolution algorithm.claude/spec-disclosure-envelope— ADR-0012 disclosure envelope canonical shape.claude/spec-rotation-event-format— this PR.Each pins a wire shape independently; no cross-PR dependencies.
Test plan
credentialSubject.keyRotationaccepted 2026-05-19.sha256:6983c9bd6fb24e844b90f7616315a914fdedc5fef8126e11d46149ba2f320457.ajv validate -s spec/schema/agent-receipt.schema.json -d spec/test-vectors/rotation-event/example.json --spec=draft2020 -c ajv-formatspasses.$refintegration +versionenum bump to"0.3.0".