docs(adr-0012): pin disclosure envelope canonical shape (accepted: HPKE)#459
Conversation
There was a problem hiding this comment.
Pull request overview
Pins the canonical wire shape for the parameters_disclosure envelope (per ADR-0012 amendment), adding a sibling JSON Schema and initial (placeholder) disclosure-envelope test vectors to support cross-SDK interoperability and future hub-side enforcement (ADR-0017 preconditions).
Changes:
- Adds an ADR-0012 amendment specifying v1 envelope fields, canonicalisation (RFC 8785 JCS), encoding rules, and the proposed HPKE ciphersuite.
- Introduces
spec/schema/parameters-disclosure.schema.jsonto validate the envelope’s structural shape. - Adds disclosure-envelope test vectors + documentation under
spec/test-vectors/disclosure-envelope/(with placeholder ciphertext/encapsulation bytes).
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
docs/adr/0012-payload-disclosure-policy.md |
Adds an “Amendments” section pinning the v1 envelope shape and proposing HPKE vs sealed-box. |
spec/schema/parameters-disclosure.schema.json |
New JSON Schema defining the v1 disclosure envelope object structure. |
spec/test-vectors/disclosure-envelope/vectors.json |
New vectors describing canonical JCS outputs and shape invariants (currently with placeholder enc/ct). |
spec/test-vectors/disclosure-envelope/README.md |
Explains the vectors, deterministic ephemeral seeding, and placeholder strategy. |
…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`.
…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`.
378015d to
e2940a1
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
docs/adr/0012-payload-disclosure-policy.md:207
- Same as above: this second ADR-0017 reference also points at a branch-specific URL (
blob/claude/draft-...) and is likely to rot. Prefer a stable issue/PR link, or switch to a relative link when ADR-0017 is merged intodocs/adr/on the main branch.
**Cross-reference to [ADR-0017 (draft)](https://github.com/agent-receipts/ar/blob/claude/draft-adr-0017-Ko7cn/docs/adr/0017-central-receipt-hub.md).** ADR-0017 §6 (precondition check, hub-side rejection of pre-envelope plaintext) MUST consume this schema. The hub rejects with HTTP 422 + diagnostic any `parameters_disclosure` that does not match `parameters-disclosure.schema.json` v1 (or, once shipped, v2). The daemon-attested additive metadata (`peer.*`, `emitter.drop_count`) and the legacy redacted-plaintext map (`input` / `output` under the `--parameter-disclosure` opt-in) are pre-envelope shapes and are rejected by §6 once the gate closes; a separate amendment to ADR-0010 will move the `peer.*` metadata to its dedicated namespace before the hub goes live.
…s, comment accuracy)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
spec/schema/parameters-disclosure.schema.json:42
- The
kiddescription example usesdid:key:z6Mk...#enc-1, which is commonly associated with Ed25519 did:key material. Since this envelope pins X25519 recipients (HPKE DHKEM(X25519)), consider updating the example to an X25519 did:key form (e.g.did:key:z6LS...#enc-1) or making the example algorithm-neutral so readers don’t accidentally treat an Ed25519 did:key as a valid recipient encryption key identifier.
"kid": {
"type": "string",
"minLength": 1,
"description": "Recipient public-key identifier. SHOULD be either an ADR-0007 `did:key` DID URL with a fragment identifying the encryption verification method (e.g. `did:key:z6Mk...#enc-1`) or the `sha256:<hex>` fingerprint convention used elsewhere in the repo (per ADR-0015 \"Fingerprint canonical form\"). The kid is an index — it lets the forensic responder pick the right private key — and is not by itself authenticated; binding to the recipient public key happens through HPKE encapsulation."
Context
This PR pins the
parameters_disclosureenvelope wire shape that ADR-0012 committed to in principle but left underspecified (v,alg,recipients,nonce,ctwith field semantics deferred to a follow-up). The envelope is the user-facing gate for ADR-0017 (central receipt hub): the hub's §6 precondition check rejects any pre-envelopeparameters_disclosureshape (plaintext map, redacted-plaintext map, the daemon's current--parameter-disclosureflag output). Until conformant envelope producers exist across the three SDKs, that rejection is unsafe to enforce, because aggregating pre-envelope plaintext on the hub creates a concentrated-secrets corpus materially worse than the per-node plaintext it replaces. This PR is the spec-side prerequisite for that work landing.This is one of three coordinated spec PRs derisking ADR-0017's preconditions:
claude/spec-did-key-resolution— did:key resolution wire format + test vectors (consumed by the hub's JWS auth).claude/spec-rotation-event-format— rotation event canonical wire format (deferred in ADR-0015).The three PRs do not depend on each other and can land in any order.
The architectural call: HPKE accepted
HPKE (RFC 9180) is the accepted v1 encryption primitive (sign-off recorded in the ADR-0012 amendment). The tradeoff vs libsodium sealed-box is documented in the ADR for the record.
ct.ctproperty Phase C rests on.cloudflare/circl/hpkemature; Pypyhpkeworkable; TS@hpke/coreworkable but younger than libsodium bindings. Gap is real and closing.info, AAD).crypto_box_seal(plaintext, recipient_pk).Call made by this PR: single-shot HPKE, no
noncefieldRFC 9180 single-shot encryption derives the AEAD nonce from the KEM output internally. Each receipt encrypts exactly one parameters object — there is no streaming use case. Surfacing a
noncefield in a single-shot envelope is redundant at best and a footgun at worst (an implementer who supplies a custom nonce against a library that ignores it gets silently wrong behaviour). The schema therefore omitsnonce; the shape-invariants list in the test vectors calls this out so an SDK that adds the field by reflex fails fast. Flagged in the ADR amendment under Single-shot vs streaming HPKE.What's in this PR
docs/adr/0012-payload-disclosure-policy.mdtitled Envelope canonical shape and algorithm choice (accepted: HPKE). Append-only — original decision sections unchanged. Documents the HPKE decision, the sealed-box one-pager, the single-shot call, the v1 locked-in decisions, the Phase C extension story, and cross-references ADR-0017 as the consumer.spec/schema/parameters-disclosure.schema.json— JSON Schema fragment for the envelope. Sibling schema; the main receipt schema is not modified.spec/test-vectors/disclosure-envelope/vectors.json— two static vectors with well-known X25519 test keys (RFC 7748 §6.1, Alice and Bob), deterministicikmEseeds (vector 1 from RFC 9180 §A.1.1; vector 2 from a documented repo-local string), expected canonical-JCS envelope shape, and round-trip plaintext expectations. First revision carries placeholders for the concreteencandctbyte values — the first SDK to ship HPKE base-mode encryption fills them in.spec/test-vectors/disclosure-envelope/README.md— explains the deterministic-ikmEpattern (RFC 9180 §7.1.3DeriveKeyPair), the test-recipient key provenance, why the ciphertexts are placeholders, and what each vector pins.Locked-in decisions (no sign-off required):
vis a JSON string"1", not the integer1(no RFC 8785 number-encoding ambiguity at verifiers).proofValue).recipientsis an array (forward-compat for Phase C multi-recipient),minItems: 1, maxItems: 1in v1 so producers can't accidentally ship a multi-recipient envelope under av: "1"label.ctacross recipients in Phase C (per the original ADR-0012 design); not per-recipient ciphertexts.encto match RFC 9180 §4.1, superseding the original ADR sketch'sencap. One-time spec rename in a not-yet-shipped schema; the benefit is no perpetual translation between spec text and library APIs.What's deliberately NOT in this PR
--parameter-disclosurefrom redacted-plaintext to envelope. Tracked under Promote parameterDisclosure to first-class feature across all SDKs and MCP proxy #280.parameters-disclosure.schema.jsonfrom the mainagent-receipt.schema.json. Lands with the SDK work, not the spec — the wiring needs the cross-SDK harness for confidence.kidregistry mechanism. v1 accepts either adid:keyDID URL with a#-fragment or thesha256:fingerprint form; a registry can be added when there is a real multi-recipient deployment to pin against.alg. v1 pins exactly one ciphersuite. Additional ciphersuites require a new ADR-0012 amendment and a v2 envelope.enc/ctin the test vectors. Placeholders today; the first SDK to ship HPKE base-mode encryption fills them in. Holding placeholders rather than guessing is deliberate — a wrong-looking value that all three SDKs happen to load from the same fixture would silently lock in a bug.Cross-references
claude/spec-did-key-resolutionandclaude/spec-rotation-event-format.Test plan
parameters-disclosure.schema.jsonis a valid JSON Schema 2020-12 document (verified locally withjsonschema).envelope_objectvalidates against the schema. Deferred — placeholderenc/ctstrings deliberately fail the base64url pattern constraints; this check applies oncecomputed_values_statusis"computed"(filled in by the first SDK to ship HPKE).envelope_canonical_jcsandplaintext_canonical_jcsround-trip through a JCS canonicaliser (verified locally with Pythonjson.dumps(sort_keys=True, separators=(',', ':'))as a faithful approximation for these inputs).encandctbytes; cross-SDK harness incross-sdk-tests/is extended to assert byte-equality onenvelope_canonical_jcs.