ADR-0040 — Signing-context domain separation, and one signature per event¶
- Status: Accepted
- Date: 2026-07-02
- Refines: ADR-0015 (the COSE_Sign1/Ed25519 primitives), ADR-0007 (co-signing as overlay), ADR-0030 (the attestation token this separates from the event)
Context¶
Cairn signs three structurally different artifacts with the same Ed25519 key family and, until
this ADR, the same undifferentiated COSE_Sign1 shape: events (the clinical and node planes,
both an EventBody), attestation tokens (the ADR-0030 responsibility proof), and pairing
bundles (the ADR-0017 §7 out-of-band offer). Nothing in the signed bytes said which kind a
signature was minted for: external_aad was empty and the protected header carried no
content-type. Cross-context replay — presenting a signature minted in one context for
verification in another — failed only because the three payload types happened to have disjoint
required fields. That is structural luck, not a guarantee, and principle 11's additive-only
evolution actively erodes it: parsers ignore unknown fields, so a future payload that grows a
superset of another type's fields becomes cross-parseable, and a signature crosses with it. The
concrete failure is not hypothetical in shape: a signed artifact whose payload parses as an
attestation would confer responsibility (ADR-0007) the signer never intended to confer.
A second, adjacent wire-freeze item (review 2026-07-02, finding B4; issue #95): ADR-0015 §2 cited COSE's "native multi-signer support" as a rationale, but ratified COSE_Sign1 — which RFC 9052 defines as the single-signer variant (multi-signer is COSE_Sign, a different byte layout). Since the stored bytes are sacred (ADR-0015 move 1), drifting from Sign1 to Sign later would be a new event format. The ambiguity had to be resolved before the format freezes, in one direction or the other.
Both fixes invalidate every signature minted before them. There is no production clinical data (the first production surface is weeks old and pre-deployment), so the cost is a dev/test re-initialization — the cheapest this will ever be.
Decision¶
1. One signature per event — COSE_Sign1 is ratified as deliberate¶
An event carries exactly one signature: the authoring actor's, in a COSE_Sign1 envelope. Multi-party accountability is not expressed by multiple envelope signatures:
- the contributor set rides inside the signed body (§3.9) — who took part, in what role;
- responsibility is conferred by a separable attestation token bound to the event's content-address (ADR-0030), never by a second envelope signature;
- co-signing and re-attestation are overlay events referencing the original (ADR-0007 §6, ADR-0015 move 3), exactly like corrections.
ADR-0015's "native multi-signer support" phrase is hereby superseded as a justification (the serialization choice it argued for is unchanged and stays ratified). COSE_Sign — and the multi-signer envelope generally — is dismissed: it would put a mutable-feeling social process (gathering signatures) inside an immutable artifact, where the overlay model already expresses it append-only.
2. Every signature is domain-separated by a signing context¶
A signing context is a short registered string naming the kind of signed artifact. It is bound into every signature twice, deliberately redundantly:
- In the COSE protected header as the content type (RFC 9052 §3.1, label 3) — inside the
signed bytes, self-describing on the wire. A reader decades from now can tell what a blob is
without guessing at payload shapes (principle 11), and a verifier rejects a wrong or missing
context with a legible
ContextMismatchbefore touching the payload. - As the COSE
external_aadin the Sig_structure — so cross-context verification fails cryptographically, in the signature math itself, even against a hypothetical future verifier that forgets the header check. The header check is policy; the aad is physics.
The initial registry:
| Context string | Artifact |
|---|---|
application/cairn-event+cbor |
Events — clinical AND node planes (both sign an EventBody) |
application/cairn-attestation+cbor |
Attestation tokens (ADR-0030) |
application/cairn-pairing+cbor |
Pairing bundles (ADR-0017 §7) |
The registry is closed and additive-only (principle 11 applied to the registry itself): a new
kind of signed artifact gets a new string; an existing string is never repurposed or changed,
because it is load-bearing in every signature ever minted under it. The clinical and node planes
share one context deliberately — they share the EventBody structure and the same verify gate,
and their separation is enforced inside the signed payload by event_type plus each door's
fail-closed classification, which is already the load-bearing plane boundary.
3. Fail closed; no grandfathering¶
Verifiers reject uncontextualized (pre-ADR-0040) blobs. There is no acceptance window for legacy signatures: with zero production data, grandfathering would be pure attack surface (an uncontextualized blob is exactly the cross-context-capable artifact this ADR exists to kill). Dev/test events are re-signed; a dev federation re-initializes.
4. Blast radius and enforcement seam¶
The implementation lives where the §9 rule wants it: one generic sign helper and one generic
verify helper in cairn-event, which every public signing/verification function delegates to —
so threading the context cannot be forgotten by construction. The in-DB floor (cairn_verify /
cairn_attestation_ok via the ADR-0002 pgrx hatch) inherits the enforcement unchanged, because it
already delegates to the same single implementation. No SQL, wire-protocol, or schema change: the
context rides inside signed_bytes, which every door already stores verbatim.
Consequences¶
Easier / now guaranteed: - Cross-context replay is impossible by cryptography, not by the accident of disjoint payload fields; additive payload evolution can no longer create signature-crossing overlap. - Every signed blob is self-describing about its kind — a forensic or archival reader (principle 11) identifies artifacts without payload heuristics. - The Sign1-vs-Sign ambiguity is closed before the freeze: one signature per event, plurality by overlay, consistent with the append-only grain of everything else.
Harder / accepted costs: - Every pre-ADR-0040 signature is invalid. Dev federations re-initialize; the walking-skeleton era's signed fixtures are historical only. (Deliberate — see §3.) - The context string adds ~30 bytes to each signed artifact (well inside the A5 budget; the twin dwarfs it). - A future legitimate need for a second envelope signature (e.g. a hardware token co-signing at write time) must be designed as an overlay or a new context, never by widening Sign1 — that is the point, but it forecloses shortcuts.
How we would know the bet is failing: an artifact kind emerges that genuinely cannot express its multi-party semantics as contributor-set + attestation + overlay (would force revisiting §1); or interop pressure demands standard COSE multi-signature envelopes at the FHIR/interop boundary (handled there by the façade, never by changing the internal format — same answer as RSA/ECDSA in ADR-0015 §6).
Not a new founding principle. This is principle 11 (additive-only, self-describing artifacts) and the §9 small-auditable-surface rule applied to the signature layer, plus the ADR-0015 moves carried to their conclusion.