Skip to content

docs(adr-0012): pin disclosure envelope canonical shape (accepted: HPKE)#459

Merged
ojongerius merged 7 commits into
mainfrom
claude/spec-disclosure-envelope
May 19, 2026
Merged

docs(adr-0012): pin disclosure envelope canonical shape (accepted: HPKE)#459
ojongerius merged 7 commits into
mainfrom
claude/spec-disclosure-envelope

Conversation

@ojongerius
Copy link
Copy Markdown
Contributor

@ojongerius ojongerius commented May 18, 2026

Context

This PR pins the parameters_disclosure envelope wire shape that ADR-0012 committed to in principle but left underspecified (v, alg, recipients, nonce, ct with 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-envelope parameters_disclosure shape (plaintext map, redacted-plaintext map, the daemon's current --parameter-disclosure flag 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).
  • This PR — disclosure envelope canonical shape + test vectors.

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.

Axis HPKE (RFC 9180) libsodium sealed-box
Standardisation IETF RFC, Feb 2022. Wire format and ciphersuite IDs stable across implementations. libsodium convention. No RFC; wire format is "whatever libsodium does."
Multi-recipient (ADR-0012 Phase C) Composable: same content-encryption key, wrapped to N recipients with one HPKE encapsulation each. Single ct. Not native. Multi-recipient means N independent sealed-boxes per receipt. Loses the single-ct property Phase C rests on.
Library maturity (Go / TS / Py) Go cloudflare/circl/hpke mature; Py pyhpke workable; TS @hpke/core workable but younger than libsodium bindings. Gap is real and closing. All three languages have mature libsodium bindings. Maturity gap is in HPKE's favour but small as of 2026.
API surface for v1 single-recipient More moving parts (KEM/KDF/AEAD triple, info, AAD). Simplest possible: crypto_box_seal(plaintext, recipient_pk).
Phase C migration cost if v1 ships sealed-box n/a (HPKE chosen). High — multi-recipient is a wire-format change, not additive. Receipts are permanent once signed; that's exactly the kind of premature commitment we want to avoid.

Call made by this PR: single-shot HPKE, no nonce field

RFC 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 nonce field 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 omits nonce; 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

  1. ADR-0012 amendment at the bottom of docs/adr/0012-payload-disclosure-policy.md titled 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.
  2. spec/schema/parameters-disclosure.schema.json — JSON Schema fragment for the envelope. Sibling schema; the main receipt schema is not modified.
  3. spec/test-vectors/disclosure-envelope/vectors.json — two static vectors with well-known X25519 test keys (RFC 7748 §6.1, Alice and Bob), deterministic ikmE seeds (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 concrete enc and ct byte values — the first SDK to ship HPKE base-mode encryption fills them in.
  4. spec/test-vectors/disclosure-envelope/README.md — explains the deterministic-ikmE pattern (RFC 9180 §7.1.3 DeriveKeyPair), the test-recipient key provenance, why the ciphertexts are placeholders, and what each vector pins.

Locked-in decisions (no sign-off required):

  • v is a JSON string "1", not the integer 1 (no RFC 8785 number-encoding ambiguity at verifiers).
  • All binary fields are unpadded base64url (matches ADR-0009 / spec §4 proofValue).
  • RFC 8785 JCS over both the envelope and the plaintext-before-encryption.
  • recipients is an array (forward-compat for Phase C multi-recipient), minItems: 1, maxItems: 1 in v1 so producers can't accidentally ship a multi-recipient envelope under a v: "1" label.
  • Single shared ct across recipients in Phase C (per the original ADR-0012 design); not per-recipient ciphertexts.
  • AEAD = AES-256-GCM, KEM = X25519, KDF = HKDF-SHA256.
  • Recipient descriptor field is named enc to match RFC 9180 §4.1, superseding the original ADR sketch's encap. 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

  • SDK implementations of envelope encryption (Go / TS / Py). Tracked under Promote parameterDisclosure to first-class feature across all SDKs and MCP proxy #280.
  • Daemon rewire of --parameter-disclosure from redacted-plaintext to envelope. Tracked under Promote parameterDisclosure to first-class feature across all SDKs and MCP proxy #280.
  • Forensic-key CLI (export, import, rotate). Tracked under Promote parameterDisclosure to first-class feature across all SDKs and MCP proxy #280.
  • Reference of parameters-disclosure.schema.json from the main agent-receipt.schema.json. Lands with the SDK work, not the spec — the wiring needs the cross-SDK harness for confidence.
  • A normative kid registry mechanism. v1 accepts either a did:key DID URL with a #-fragment or the sha256: fingerprint form; a registry can be added when there is a real multi-recipient deployment to pin against.
  • Algorithm agility in alg. v1 pins exactly one ciphersuite. Additional ciphersuites require a new ADR-0012 amendment and a v2 envelope.
  • Concrete byte values for enc / ct in 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

  • ADR-0017 §"Preconditions" — names this envelope as the user-facing gate.
  • ADR-0017 §6 — hub-side precondition check that rejects pre-envelope plaintext.
  • Sibling spec PRs: claude/spec-did-key-resolution and claude/spec-rotation-event-format.

Test plan

  • User sign-off on HPKE vs sealed-box for v1.
  • Spec-side validation: parameters-disclosure.schema.json is a valid JSON Schema 2020-12 document (verified locally with jsonschema).
  • Spec-side validation: each vector's envelope_object validates against the schema. Deferred — placeholder enc/ct strings deliberately fail the base64url pattern constraints; this check applies once computed_values_status is "computed" (filled in by the first SDK to ship HPKE).
  • Spec-side validation: each vector's envelope_canonical_jcs and plaintext_canonical_jcs round-trip through a JCS canonicaliser (verified locally with Python json.dumps(sort_keys=True, separators=(',', ':')) as a faithful approximation for these inputs).
  • SDK follow-up (separate PRs): first SDK to land HPKE base-mode encryption with the pinned ciphersuite fills in the placeholder enc and ct bytes; cross-SDK harness in cross-sdk-tests/ is extended to assert byte-equality on envelope_canonical_jcs.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.json to 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.

Comment thread spec/schema/parameters-disclosure.schema.json
Comment thread spec/schema/parameters-disclosure.schema.json
Comment thread spec/test-vectors/disclosure-envelope/vectors.json Outdated
Comment thread spec/test-vectors/disclosure-envelope/vectors.json
Comment thread docs/adr/0012-payload-disclosure-policy.md Outdated
Comment thread docs/adr/0012-payload-disclosure-policy.md
ojongerius pushed a commit that referenced this pull request May 18, 2026
…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`.
@ojongerius ojongerius marked this pull request as ready for review May 19, 2026 09:10
…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`.
@ojongerius ojongerius force-pushed the claude/spec-disclosure-envelope branch from 378015d to e2940a1 Compare May 19, 2026 09:12
@ojongerius ojongerius requested a review from Copilot May 19, 2026 09:13
@ojongerius ojongerius changed the title docs(adr-0012): pin disclosure envelope canonical shape (proposed: HPKE) docs(adr-0012): pin disclosure envelope canonical shape (accepted: HPKE) May 19, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 into docs/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.

Comment thread docs/adr/0012-payload-disclosure-policy.md Outdated
Comment thread spec/test-vectors/disclosure-envelope/vectors.json
@ojongerius ojongerius requested a review from Copilot May 19, 2026 09:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Comment thread spec/schema/parameters-disclosure.schema.json Outdated
Comment thread spec/test-vectors/disclosure-envelope/README.md Outdated
Comment thread spec/test-vectors/disclosure-envelope/vectors.json Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment thread spec/schema/parameters-disclosure.schema.json Outdated
Comment thread docs/adr/0012-payload-disclosure-policy.md
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 kid description example uses did: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."

Comment thread spec/schema/parameters-disclosure.schema.json Outdated
Comment thread spec/test-vectors/disclosure-envelope/vectors.json Outdated
Comment thread spec/test-vectors/disclosure-envelope/README.md Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

@ojongerius ojongerius merged commit 4e1f02e into main May 19, 2026
15 checks passed
@ojongerius ojongerius deleted the claude/spec-disclosure-envelope branch May 19, 2026 10:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants