Beta Pre-1.0 — the file-naming convention may still change. v1.0 will freeze it. ZDDC home · All specifications

ZDDC - Custody Extension ~1.0
The Seven Statement Types
Status: Draft

Status: APPROVED 2026-08-12. Transcribes federation-and-custody.md §6.11 (the taxonomy) and the gap-pass designs §6.12–§6.18, plus §4 (the ledger), §5c (mailboxes), §10.11 (consent). Requirement ids [M-S*]. The canonical payload rules [M-C*] (manifest.md §1), the manifest/Merkle rules [M-M*] (manifest.md §2–§4), the envelope rules [M-E*] (envelope.md), and the anchor rules [M-K*] (keys.md) are referenced, never repeated.

Addendum 2026-08-16: §11 adds the one local statement type, record-revision (docs/design/record-signing.md S2; decisions.md D-29). The seven types of §1–§7 are link acts and everything in §2 about the common header speaks of them; §11 is its own closed grammar and says so.

Addendum 2026-08-17: §12 adds the second local type, content-attestation — a person's signed responsibility for the content of a set of files, distinct from the transmittal's issue act (docs/design/transmittal-producer.md; decisions.md D-32). §8 gains [M-S66], the in-band carriage rule that makes person-bound acts offline- verifiable from the container alone (D-33), and §7.1 gains the declared informational confirmed_by op field ([M-S73]).


1. The registry

Seven types, one grammar: a DSSE envelope over a canonical JSON payload = common header + type-specific body. Nothing is ever a second signature on another act's envelope; answering a statement means issuing a NEW statement referencing the old one by id (memo §6.11, §10.10.3).

# Type Trigger Signed by Answered by
1 transmittal issuing a package (incl. re-issuance and supersession) issuer receipt, then optionally response
2 receipt a package arrived and verified recipient —
3 response a review verdict is returned reviewer receipt
4 withdrawal recalling a package with no successor original issuer receipt
5 anchor-update key add / rotate / revoke the party root key receipt
6 consent-withdrawal prospectively un-sharing a scope the party whose data it is receipt
7 succession root-key continuity after rotation or loss the new root (+ old root when it survives) receipt

Three folds the memo made deliberately, recorded so no implementation reinstates them (memo §6.11, §6.12):

  • re-issuance is not a type — it is a transmittal carrying derived_from;
  • supersession is not a type — it is a transmittal carrying supersedes, so issuing and superseding are one atomic act;
  • revocation is not a type — it is an anchor-update op.

[M-S1] A verifier MUST derive the type from payloadType ([M-E5]) and MUST reject any payload whose body does not satisfy that type's rules, even when the body would be valid for a different type.


2. The common header

Every payload of every type carries exactly these header fields (types add their own body fields at the same top level — there is no nested body object; the payload is flat and greppable):

{
  "from":   {"party": "<64-hex>", "name": "acme", "person": "alice@example.com"},
  "to":     [{"party": "<64-hex>", "name": "owner"}],
  "ts":     "2026-01-15T09:00:00Z",
  "prev":   "<64-hex statement id>",
  "re":     "<64-hex statement id>",
  "thread": "<64-hex statement id>"
}
  • [M-S2] from — REQUIRED object. from.party is the SHA-256 fingerprint of the party's anchored root key, lowercase hex ([M-E11]) — not a name. Names are local: each side names the other in its own registry and the two never need to agree (shipped Stage 1 rule; memo §2b). from.name is OPTIONAL, informational, and a verifier MUST NOT use it in any decision. from.person is OPTIONAL — the acting person's email, evidentiary depth, never access control (memo §6.18). When present it MUST match the certification of the signing key and the party row's emails ([M-K17]); when absent, the chain MUST be length-1 (self-certified), because a length-2 chain's whole point is binding an act to a person. (decisions.md D-15.)

    Conversely — the case the sentence above leaves silent — when the signing chain IS length-1, from.person MUST be absent. A self-certified root certifies no person, so there is nothing for a present person to match, and accepting it would let a signer attach any name to their own act with no binding at all. Violation: unknown_key (the signer could not be resolved as that person).

    [M-S51] Which class a malformed party identifier gets. A from.party or to[].party that is not 64 lowercase hex characters MUST be rejected bad_link, not malformed — even though every other bad digest in this profile is malformed ([M-M4], [M-E11]). The distinction is what the field is for: party identifiers are link plumbing, and a value that cannot name a party cannot name an end of the link, which is the same failure as naming the wrong one ([M-S9]). Digest fields inside a body — manifest[].root, manifest_root, supersedes[], derived_from[].manifest — are content, and a bad one there is malformed. Stated because a verifier that classified by "it's a hex field" would report malformed for a party named rather than fingerprinted, and the class is part of the conformance contract.

  • [M-S3] to — REQUIRED array of objects with the same party/name shape. v1 semantics: exactly one element, permanently (memo §6.14 — fan-out is N per-link statements, and the array shape is kept only because it is already in the grammar). Zero or ≥ 2 elements MUST be rejected (bad_link).

  • [M-S4] ts — REQUIRED, per [M-C6]. Asserted, informational only; never used to judge key validity ([M-K19]) and never used to order statements ([M-S6]).

    Its class is malformed, absent or present — a missing ts, a non-string ts, and a ts in the wrong form are one answer. ts is the time vocabulary, whose class [M-C6] fixes outright, and it is not link plumbing: [M-S51] reserves bad_link for values that name a party, and a timestamp names nobody. Stated because the neighbouring header fields do answer bad_link, which is how one implementation came to give ts their class ([M-C11]'s table is the whole line).

  • [M-S5] prev — REQUIRED, null on this signer's first statement on this link, else the id of that signer's immediately preceding statement on this link. This is the per-link hash chain: dropping a middle statement breaks it, so middle-completeness is free and only the tip is unprotected (memo §6.11, §10.12.6). prev is scoped to (signer party, link) — an issuer's chain to Acme is independent of its chain to Owner.

  • [M-S6] Monotonic acceptance. A receiver MUST accept an inbound statement only if it verifies ([M-E*]) and its prev equals that signer's current chain tip on the link. Out-of-order or gapped → refuse and surface; do not queue, do not accept-and-flag (memo §6.12, fail closed). The chain order IS the order; ts never overrides it.

  • [M-S7] re — the id of the statement this one answers. Absent on thread roots. MUST be present on receipt and response; MUST be absent on transmittal ([M-S13]).

  • [M-S8] thread — the id of the thread root. Absent on roots, and REQUIRED whenever re is present. When present it MUST equal the terminus of the re-chain — deliberately redundant and cheaply checkable, kept so offline tools group statements without walking the whole ledger (memo §6.11). Violation: bad_link.

    "Absent on roots" was written as a statement about roots and read by one implementation as permission for non-roots to omit it too, which is a real gap rather than a bug: a re with no thread was accepted by one verifier and refused by the other (decisions.md, D-S7). It is now a MUST in the direction the redundancy needs. The redundancy only buys anything if it is always there — an offline tool that must fall back to walking the chain whenever a producer felt like omitting thread has gained nothing, and the fields' agreement is a check a verifier can make from the statement's own bytes ([M-S8]'s terminus rule below). Omitting it is not a cheaper honest statement; it is one fewer thing that can be checked.

    The terminus is computed by walking the re-chain as far as the verifier can resolve it, and when the next hop is unresolvable the walk stops there. Concretely: with no ledger at all, the only chain in evidence is the single hop this statement makes, so the terminus is re itself and the rule reduces to thread MUST equal re. A verifier MUST NOT wave the check through for want of a ledger — a statement whose own two link fields disagree is refusable from its own bytes, and refusing it is what keeps [M-S6]'s gap-free chain property true rather than aspirational. (Currency is a separate axis and stays unknown in isolation, [M-S48]; this is the crypto/link axis and it is decidable here.)

    The cost, stated because it is real and both reference implementations flagged it: a legitimately mid-thread statement verified in isolation is rejected. A receipt for a response carries re = the response and thread = the original transmittal, which is correct — and a verifier handed that one file with no ledger cannot tell it from a fabricated mismatch, so it refuses. That is the fail-closed side of the trade, and the fix is evidence, not leniency: verify a mid-thread statement with its thread, which is what _ledger/ travelling with the container is for ([M-S43]).

  • [M-S9] Link binding. from.party and to[0].party MUST be the two ends of the link the statement arrived on, and MUST differ from each other. A statement replayed onto a different link fails here — the cross-link half of domain separation, the type string being the other half (memo §6.11). Violation: bad_link.

    This requirement used to say "in that direction". It cannot: a link's two ends are an unordered set of party fingerprints ([M-K16]), so there is nothing in the link context that says which end is "us". Direction is established where it actually lives — the signer is whoever's anchored key verified the signature, checked against from.party at step 7 — and a verifier MUST NOT try to reconstruct it from the parties list. The check here is membership plus distinctness, which is what both reference implementations do and all that this rule can honestly ask.

    What "the link the statement arrived on" is, when the verifier was told something odd. The link context carries a set of party fingerprints, and three cases are distinct:

    • no ends at all (the key absent, or an empty list) — the verifier was not told, which is the ordinary offline case: a .zip handed over out of band arrives on no link. The rule is NOT EVALUATED, and a verifier MUST NOT report that it passed.
    • exactly two distinct fingerprints — the rule is enforced as above.
    • anything else — one end, three ends, or the same end twice. A ZDDC link is bilateral by construction ([M-S3] makes that permanent), so this is not a link, and a verifier MUST refuse every statement offered on it (bad_link) rather than skipping the check. Fail closed: silently accepting everything is what a skip amounts to, and "the context was malformed" is not a reason to admit an unbound statement. (Both reference implementations skipped. Neither was wrong about the other, which is why this was invisible to a behaviour diff and had to be found by reading — decisions.md.)
  • [M-S10] Unknown header or body fields are ignored and remain signature-covered ([M-C7]), except as governed by critical ([M-C8]) and [M-C11]: a field this spec names is not an unknown field, and one carrying the wrong JSON type is a refusal rather than a shrug.


3. transmittal — the issue act

Trigger. A document controller commits a package to a counterparty. On disk this is the existing staging→archive commit (X-ZDDC-Op: commit); the statement is what makes it a custody act rather than a file move.

Signed by the issuer (an operational key of from.party). Countersigned by nobody — the recipient answers with a separate receipt ([M-E13]).

Body:

{
  "name": "TR-0007",
  "mailbox": "default",
  "manifest": [{"path": "…", "size": 0, "root": "<64-hex>"}],
  "supersedes": ["<64-hex>"],
  "derived_from": [{"manifest": "<64-hex>", "file": "…", "verbatim": true}]
}
  • [M-S11] name and mailbox are REQUIRED single path segments per [M-M12]. The mailbox travels in the signed payload so the issuer's issued/<M>/<T> mirrors the receiver's received/<M>/<T> and the two ledgers stay reconcilable on the path (memo §5c). The <party> segment and the issued/received slot are each side's own frame and MUST NOT appear in the payload ([M-M9] context).
  • [M-S12] manifest is REQUIRED and non-empty, per [M-M6]–[M-M11].
  • [M-S13] A transmittal is always a thread root: re and thread MUST be absent (bad_link otherwise). Its own id is the thread id that every answering statement carries.
  • [M-S14] supersedes — OPTIONAL array of ids of prior transmittals on this link that this issue replaces (memo §6.12; almost always one, the array covers consolidation reissues). Ids MUST be unique. Naming a statement the verifier holds and that is not a transmittal on this link MUST be rejected (bad_link). Supersession is atomic with the issue act: there is no way to issue B without superseding A, and no way to supersede A while pointing at nothing.
  • [M-S15] derived_from — OPTIONAL array; this is re-issuance (memo §6.11.1). Each entry: manifest (the id of the inbound transmittal this content came from, on a link the recipient is not party to), file (the in-container path, per [M-M9]), verbatim (boolean — was the file passed through untouched). It exists so two bilateral ledgers join deterministically in discovery, and because content-addressing already makes verbatim files hash-equal, derived_from adds the pin the hashes cannot: which inbound original the re-issuer held. It is opt-out — it reveals the sub-link transmittal's id upstream (memo §10.10).
  • [M-S16] A verifier MUST NOT interpret revision labels (A, B, 0) or status codes. Ordering is explicit signed acts, never inference from labels (memo §6.12); labels are UI vocabulary that never enters the ledger.

On disk. <transmittal container>/_ledger/<seq>-transmittal-<id8>.dsse.json (§8).

4. receipt — the bilateral timestamp

Trigger. A package arrived and verified at the recipient. This is the trusted-time primitive of the whole system (Haber–Stornetta; memo §6.5): with no TSA, no log, and no network, a counterparty's signature over your manifest root is what proves when — relative to their chain — you sent it.

Signed by the recipient. re = the transmittal id (REQUIRED).

Body:

{"manifest_root": "<64-hex>"}
  • [M-S17] manifest_root is REQUIRED: the statement id of the received transmittal, echoed verbatim. Because a transmittal's id is the SHA-256 of its canonical payload ([M-C5]) and that payload contains every file's Merkle root, echoing the id binds the receipt to exactly the bytes received — a receipt can never be replayed against a different manifest (memo §6.11.2).
  • [M-S18] manifest_root MUST equal re. The redundancy is deliberate and load-bearing in the same way [M-S8] is: re is link plumbing that a tool may rewrite while grouping; manifest_root is the substance of what was acknowledged. A verifier holding both MUST reject a mismatch (bad_link).
  • [M-S19] A receipt acknowledges receipt, never agreement, acceptance, or approval. Implementations MUST NOT present a receipt as a verdict; the verdict is a response.
  • [M-S20] A receipt is issued for every statement type, not only transmittals (memo §6.11: anchor-updates, withdrawals, consent-withdrawals and successions are "receipted like any statement"). The body shape is identical; manifest_root echoes whatever statement id re names. (The field name is transmittal-flavoured for a general echo — decisions.md D-16.)

[M-S21] Honest limit — a recipient who refuses to countersign after receiving is the classic fair-exchange problem, unsolvable without a trusted third party (memo §6.10). "You never sent it" is answered by the issuer's retained signed transmittal; "I never received it" is answered by the receipt; refusal is a contractual matter. A conforming implementation states this rather than implying a receipt is guaranteed.

On disk. In the recipient's copy of the transmittal container's _ledger/, and — because it is the issuer's evidence — copied back into the issuer's container _ledger/ when it arrives.

5. response — the review verdict

Trigger. The core document-control loop: submittal in, verdict + marked-up copies back (memo §6.13). Drafted in the physical reviewing/<party>/ workflow folders (which already carry received_path back to the canonical submittal) and issued through the ordinary stage→commit handoff — no new doors.

Signed by the reviewer. re = the reviewed transmittal (REQUIRED — a response answers something, and that is exactly why it earns a distinct payloadType: a verdict must never replay as a fresh issuance).

Body:

{
  "status": "RSA",
  "documents": [{"path": "…", "status": "RSC", "remarks": "…"}],
  "manifest": [{"path": "…", "size": 0, "root": "<64-hex>"}]
}
  • [M-S22] status is REQUIRED: the package-level verdict code, carried verbatim. Status codes are the deployment's own vocabulary — config, not protocol. A verifier MUST NOT interpret, validate, or enumerate status codes; what RSA/RSC mean is contractual and bundle-defined (memo §6.13, the kernel rule that nothing domain-specific is hardcoded). The only constraint is [M-C1]: it is a string.

  • [M-S23] documents — OPTIONAL per-document verdicts (a package can return mixed results). Each entry REQUIRES path ([M-M9]–[M-M11]) and MAY carry status and remarks, both strings ([M-C11]). Paths MUST be unique and MUST refer to paths present in the reviewed transmittal's manifest when the verifier holds it (bad_path otherwise).

    An entry that is not an object, or whose path is absent or not a string, is malformed — [M-E23] rule 2's shape-before-content line, which is written for a manifest entry and which documents[] therefore inherits nothing from unless it is said here. An entry that is not an entry has no path to judge, so bad_path would be a rejection for a reason the verifier never reached (decisions.md, D-S6). bad_path stays for a path that is a string and not a legal path.

  • [M-S24] manifest — OPTIONAL returned files (markups, comment sheets), exactly the [M-M6]–[M-M11] shape. When present it MUST be non-empty; a response with no returned files simply omits the key.

  • [M-S25] The response is receipted by the original issuer like any statement, which is what closes the round-trip with signatures on both halves.

On disk. The returned package lands like any transmittal — the reviewer's issued/<mailbox>/… mirroring the original issuer's received/<mailbox>/… — with the response statement in its _ledger/.

6. withdrawal — recall with no successor

Trigger. Pulling a package back when nothing replaces it. It remains its own type precisely because it is the no-successor case and must never look like an issue act (memo §6.12).

Signed by the original issuer. re = the withdrawn transmittal (REQUIRED).

Body:

{"reason": "issued in error"}
  • [M-S26] reason is OPTIONAL free text.
  • [M-S27] manifest MUST be absent — a withdrawal delivers nothing (malformed).
  • [M-S28] Withdrawal is PROSPECTIVE. It marks the thread withdrawn for future reliance; it neither requires nor requests destruction of delivered copies (retention duties usually forbid that — memo §6.12, §6.18). Receipting a withdrawal acknowledges notice, not agreement.

7. anchor-update, consent-withdrawal, succession — the link-level acts

These three are about the relationship rather than a document, so they live in the party compartment's _ledger/, not in a transmittal container (§8).

Trigger. Adding, rotating, or revoking a key. This is the whole of key lifecycle: no CRL, no OCSP, no key server ([M-K27]).

Signed by the party root key only ([M-K18]). re/thread absent (each anchor-update is a root of its own single-statement thread); prev still chains ([M-S5]).

Body:

{"ops": [
  {"op": "add", "key": "<b64url raw public key>", "alg": "ed25519",
   "kind": "sign", "email": "alice@example.com",
   "not_before": "2026-01-15T00:00:00Z", "not_after": "2026-07-15T00:00:00Z"},
  {"op": "revoke", "key": "<b64url raw public key>",
   "compromise_date": "2026-03-02"}
]}
  • [M-S29] ops is REQUIRED, a non-empty array. Each op REQUIRES op ∈ {add, rotate, revoke} and key.

  • [M-S30] add and rotate REQUIRE alg ([M-K7]), kind (sign|kem), not_before and not_after ([M-C6], not_before < not_after), and — for kind: sign — email, which is what makes the allowlist cryptographically real (memo §10.3). rotate differs from add only in intent and MAY carry replaces; a verifier treats both identically. (The memo writes this as validity; expanding it to an explicit window is decisions.md D-17.)

    replaces, defined — the token appeared here and nowhere else in the spec set. It is OPTIONAL and it is the raw public key of the key this op supersedes, in the same encoding as the op's own key (base64url unpadded, [M-K2]) — not a statement id and not a fingerprint, which is what "the id-less raw key it succeeds" was reaching for. It is informational only: it records which key gave way to which, so a human reading the ledger can see the rotation. A verifier MUST NOT require it, MUST NOT require the key it names to be anchored, and MUST NOT treat its presence as revoking anything — revocation is a revoke op ([M-S31]) and nothing else. Being an ordinary optional field, it is signature-covered like any other ([M-C7]).

  • [M-S73] confirmed_by (addendum 2026-08-17, D-33) — each op MAY carry confirmed_by, a string: the email of the person whose deliberate act triggered this certification (in the shipped producer, the document controller whose commit gesture confirmed a first-use key binding — docs/design/transmittal-producer.md §4). It is informational only, the [M-S2] from.name posture verbatim: a verifier MUST NOT use it in any decision. It exists because the statement itself cannot carry the confirmer honestly — an anchor-update is root-signed, a root is length-1, and [M-S2] therefore forbids from.person on it. Being a declared field, [M-C11] applies: present with a non-string value is a refusal (malformed), never a shrug.

  • [M-S31] revoke REQUIRES key and MAY carry compromise_date ([M-C6] date form). With a compromise date, signatures by that key at or after the date are invalid even if timestamped ([M-K24]). Without one, the key simply stops being valid from this statement forward and prior signatures remain valid — the ordinary end-of-life case (memo §6.15: "otherwise the key stays historically valid so the ledger keeps verifying").

    "From this statement forward" needs an instant, and the anchor state has no field for one. The rule is a comparison — signatures before the revocation stay valid, signatures after it do not — but the revoking op carries no time of its own (deliberately: ts is asserted and MUST NOT be used, [M-K19]), and the derived anchor state ([M-K16]) records that a key is revoked, not when. So the anchor state carries the instant explicitly: a revoked entry with no compromise_date MAY carry revoked_at, an instant ([M-C6]) that the verifier records when it applies the op — in practice the receipt timestamp of the revoking anchor-update, else the instant at which it was folded into the state. It is a field of the derived anchor state, never of the wire payload; nothing signs it, and it is exactly as trustworthy as the local state it lives in.

    A verifier holding a revoked entry with neither a compromise_date nor a revoked_at MUST fail closed — every signature by that key is unknown_key, including ones that predate the revocation. It cannot do otherwise honestly: with no instant, "before the revocation" is not a question it can answer, and answering it permissively would keep a key usable precisely because the verifier lost track of when it died.

  • [M-S32] An anchor-update MUST NOT change the root key. Root change is succession ([M-S37]); accepting a root swap inside a routine key update would let one compromised operational context silently re-root the party. Violation: bad_link.

  • [M-S33] Ops apply in array order, and array order is the ledger's order. A later verified statement's op supersedes an earlier one for the same key.

Trigger. The personal archetype's core act, and the link-termination mechanism (memo §6.15, §10.11): un-sharing a scope (a path), not one transmittal.

Signed by the party whose data it is. re absent (it is not an answer); prev chains.

Body:

{"scope": "records/labs/", "effective": "2026-04-01", "reason": "…"}
  • [M-S34] scope is REQUIRED: a path prefix in the same encoding as [M-M9]–[M-M11], with a trailing / when it names a directory. The link itself is scope / — that is how termination is expressed (memo §6.15).

    How that squares with [M-M9], which forbids a leading OR trailing /: a scope is a prefix, not a path, so a trailing separator is part of its grammar rather than a violation of the path grammar. The rule, spelled out because "the same encoding as [M-M9]" read literally forbids the very form the sentence before it requires:

    • the single literal "/" is the whole-link scope and is legal as-is;
    • otherwise, remove one trailing / if present and apply [M-M9]/[M-M10] to the remainder. Everything else still refuses (bad_path): ./.. segments, backslashes, control bytes, empty segments, the reserved enclave, over-length — and a leading /, since a wire scope is relative to the party compartment, exactly as a manifest path is relative to its container.

    A trailing / is not decoration: drawings/ un-shares a directory, drawings un-shares that directory and a sibling file named drawings.pdf. Producers SHOULD write the slash when they mean a directory, and a verifier MUST NOT add or remove one on the producer's behalf.

    The empty string is bad_path, not malformed. Absence, and presence with a non-string type, are structural and answer malformed ([M-C11]); once the field is a string, everything about its value is judged by the path rules, and "" violates them exactly as /a/ or ../x does ([M-M9] refuses an empty path outright). The alternative reading — "empty means it isn't really there, so it's the missing-field answer" — is the one that has to invent a rule, and it would put one illegal spelling of a scope in a different class from all the others (decisions.md, D-S9).

  • [M-S35] effective is REQUIRED, a calendar date ([M-C6]).

  • reason is OPTIONAL free text, exactly as [M-S26] makes it on a withdrawal. Declared here so the type's field list is closed and [M-C11] bites on it: it appeared in the §7.2 body example from the first draft with no requirement naming it, which is how one implementation came to type-check it and the other to treat it as an unknown field.

  • [M-S36] It is PROSPECTIVE-ONLY and implementations MUST say so. It cannot claw back delivered plaintext, and the recipient is often legally required to retain their copy (memo §10.11, §6.18). What it governs is future flow: federated reads, new issues, grant and anchor removal. Presenting it as deletion or as a right-to-erasure guarantee is non-conforming.

7.3 succession — root continuity

Trigger. Root-key rotation, acquisition where the key survives, or key loss (memo §6.15).

Signed by the new root; countersigned in the same envelope by the old root when it survives ([M-E14]).

Body:

{"new_root": "<b64url raw public key>", "alg": "ed25519",
 "linked": true, "name": "acme-holdings", "reason": "rebrand"}
  • [M-S37] new_root is REQUIRED and MUST equal the raw public key whose fingerprint is signatures[0].keyid ([M-E11], [M-E15]) — the statement names the key it is signed by, so a verifier cannot be pointed at a third key.

  • [M-S50] new_root MUST NOT name a key that is already anchored to any party in the verifier's anchor state — neither as a party's root_key nor as an operational key it has learned from a verified anchor-update. Violation: malformed. (DECIDED 2026-08-13. The id is out of sequence because requirement ids are stable and append-only; the rule belongs here, beside the other new_root rule.)

    The reason is identity, not hygiene. from.party is the fingerprint of a party's anchored root ([M-S2]), so two parties anchored to one key are one identity on the wire: every from/to field, every link binding ([M-S9]) and every signature would stop distinguishing them, and a statement signed by either would verify as the other. Succeeding to a key already anchored elsewhere is therefore not a rotation at all — it is a merge of two identities into one, which no counterparty agreed to. The two degenerate cases fall out of the same rule: naming your own current root succeeds nothing, and naming one of your own operational keys promotes a certified leaf into the root that certified it, which is [M-K18] run backwards.

    A verifier holding no anchor state cannot evaluate this and MUST NOT report that it passed; with an envelope in hand, [M-E15]'s positional check remains the rule that binds new_root to the signer.

  • [M-S52] alg is REQUIRED and MUST name an algorithm this version implements — in v1 that is ed25519 and nothing else ([M-K7]). An absent alg is malformed; a present one that this version does not implement — whether unknown, or reserved-but-unimplemented like ecdsa-p256 — is unsupported_type, exactly as [M-E9] requires of any anchor declaring one. The two classes are deliberately different answers to two different questions: "this is not a succession payload" versus "this is a succession I cannot evaluate". (DECIDED 2026-08-13. The id is out of sequence because requirement ids are stable and append-only; the field appeared in the §7.3 example from the first draft with no requirement naming it, which is how one implementation came to check it and the other to ignore it.)

    Why this field exists at all, when [M-E8] says the algorithm rides the anchor and never the artifact. Everywhere else in the profile that rule holds without exception: a verifier learns an algorithm from the anchor entry that declares the key it is about to use, and refuses any hint the envelope or payload offers. A succession is the one statement that declares a key instead of looking one up — new_root is by construction not anchored anywhere yet ([M-E12]'s single exception, envelope.md §6 step 7) — so there is no anchor entry to read the algorithm from. A succession with no stated algorithm is precisely the gap [M-E8] leaves, and closing it here keeps the rule intact rather than carving a second exception into it: the algorithm still rides the anchor declaration, and this payload is the anchor declaration the counterparty will pin.

    Two consequences follow, and both are the point rather than side effects. The field is not an algorithm hint a verifier may act on — it is checked against what v1 implements and never used to select a verification routine, so a succession naming ecdsa-p256 is refused rather than attempted, and no downgrade surface is created. And because index 0 is verified as Ed25519 ([M-E10]) whatever the payload says, the check is the only thing that stops a succession from claiming to introduce a key of one kind while carrying a signature of another.

  • [M-S38] linked is REQUIRED boolean and MUST agree with the envelope: true ⇒ exactly two signatures, index 1 being the party's currently anchored old root (cryptographic continuity — verifiers accept the new root with no ceremony); false ⇒ exactly one signature, the break succession of key loss. Disagreement between linked and the signature count MUST be rejected (malformed). The full cardinality, ordering, and signer-identity rules are envelope.md §5.

  • [M-S39] There is NO old_sig payload field, and a producer MUST NOT emit one. The old root's proof of continuity is signatures[1] of the same envelope ([M-E14]) — memo §6.15 describes it as a payload field, which cannot be produced without circularity (the old key would have to sign a payload containing its own signature) and would give the payload two canonical forms, breaking [M-C2] and the content-addressed id. RESOLVED 2026-08-12; decisions.md D-10. A payload carrying old_sig is an unknown field and is therefore ignored under [M-C7] — it grants nothing, and a verifier MUST NOT consult it for continuity.

  • [M-S40] A break succession MUST NOT be auto-accepted. The counterparty receipts it only after out-of-band re-verification (memo §6.15 — "your doctor knows you"), and the ledger shows an honest break rather than a spliced chain. An implementation that silently re-anchors on an unlinked succession is non-conforming — it converts key loss into key theft. (Not fully vector-testable: "a human re-verified out-of-band" is not a property of bytes. The vector asserts the format and that a conforming verifier surfaces break; the ceremony is conformance.md §5.)

    Why this ceremony is the whole of the assurance, not an extra layer on top of one: an unlinked succession's single signature is by the key the payload itself names, verified against nothing the counterparty already trusts (envelope.md §6 step 7 — the one exception to [M-E12]). Anyone can mint one naming any party. A verifier that accepts it has established that the bytes are self-consistent and nothing else, so a surface presenting a break succession must present it as a claim awaiting out-of-band re-verification, never as an authenticated act.

  • [M-S41] name is OPTIONAL (a rename rides the same event), and a string ([M-C11]). History is NEVER rewritten: the registry row appends the event and thread-joining across it is done by the row, not by editing statements ([M-K25]). reason is likewise OPTIONAL free text ([M-S26]), declared for the same reason it is declared on a consent-withdrawal: the §7.3 example carries it, so the field list is closed only if a requirement names it.

  • [M-S42] A novation — a different legal entity assuming the contract — is NOT a succession. It is a new link (new keys, new TOFU), the old link terminated by consent-withdrawal, and current-state records re-issued on the new link with derived_from ([M-S15]). Nothing pretends the new entity signed the old history (memo §6.15).


8. Where statements live

[M-S43] Statements are ordinary files that travel with the artifact (constraint 1: the .zip self-verifies), in a reserved _ledger/ directory — underscore is the existing reserved-name convention, and unlike .zddc.d/ it mirrors and ships (memo §6.11):

Scope Location
per-transmittal acts (transmittal, receipt, response, withdrawal) _ledger/ inside the transmittal container
link-level acts (anchor-update, consent-withdrawal, succession) _ledger/ at the party compartment root, archive/<party>/ — where the WORM cascade already makes the local ledger append-only for free

[M-S44] Filename: <seq>-<type>-<id8>.dsse.json where <seq> is a 4-digit zero-padded counter, monotonically increasing within that _ledger/ directory in chain order, and <id8> is the first 8 characters of the statement id. The filename is presentation, never authority — a verifier MUST derive type from payloadType ([M-E5]) and identity from the payload hash ([M-C5]), and MUST tolerate any filename. The convention exists so a directory listing reads in order to a human, which is the whole ZDDC premise (properly named files in properly named folders, shown). (The memo calls placement a "leaning, confirm in the lifecycle pass" and the lifecycle pass did not revisit it — decisions.md D-18.)

[M-S45] _ledger/ MUST be skipped by nothing: it is deliberately not enclave material. Public halves and signatures must travel or offline verification is impossible ([M-K12]).

[M-S66] In-band carriage of link-level acts (addendum 2026-08-17, D-33). A container's _ledger/ MAY additionally hold copies of link-level statements — anchor-update and succession envelopes — that a verifier needs to resolve the signers of the statements in that ledger. This is what makes a shipped container self-verifying for person-bound acts: the recipient pins only the party root ([M-K11]) and learns the certified keys from the container itself, with no prior distribution and no network ([M-K27]). The rules:

  • A copy is evidence, never a chain insertion. Each carried envelope verifies on its own terms (the [M-E*] procedure, [M-S32], signed by the pinned root or proven continuous from it by a carried succession). Folding carried copies is a local evidence fold, not door acceptance: [M-S6] speaks of a link's live door and its chain tip, and a verifier working from carried copies MUST NOT report [M-S6] as satisfied — the [M-S9] "not told" posture, one rule over.
  • The fold is conservative, because carried copies carry no total order. prev chains per (signer party, link) across all statement types, so two carried anchor-updates are generally not adjacent in the chain and their order is not establishable from the copies alone. When every carried op set touches each key at most once, the fold is unambiguous and the verifier folds directly. When multiple carried ops touch the same key and their order cannot be established from carried prev links, that key is unresolved — signatures under it are unknown_key, fail closed. A verifier MUST NOT break the tie by filename <seq> ([M-S44]: presentation, never authority), by ts ([M-K19]), or by leniency.
  • Producers keep the ambiguity from arising. A producer packing a container MUST carry the current certifying op for each signer it packs for — not the key's whole history; the container needs the state that verifies its own statements, and history lives in the party-root ledger ([M-S43] table, unchanged).

9. The two flows, as sequences

9.1 Issue → receipt (the custody handoff)

  1. A (issuer) assembles the package in staging/<B>/issued/<M>/<T>/, computes each file's root ([M-M1]–[M-M4]), builds the transmittal payload, canonicalizes ([M-C1]), signs the PAE ([M-E6]) with an operational key, and writes the envelope to the container's _ledger/. The signing gesture shows what is being signed — type, counterparty, file count, manifest root fingerprint — before confirm (memo §6.16).
  2. A commits: the tail moves same-path into archive/<B>/issued/<M>/<T>/ (WORM). The statement rides along; it was already inside the container.
  3. The package reaches B — over the link, or on a USB stick. Transport is an optimization for freshness, never a security dependency.
  4. B verifies: envelope ([M-E*] §6), header ([M-S2]–[M-S10]), each file's bytes against its root, and prev against A's chain tip ([M-S6]). Any failure → refuse at the door, surface it, do not provisionally accept ([M-E21]).
  5. B issues a receipt echoing the transmittal id ([M-S17]), signs, files it in B's container _ledger/, and returns it to A, who files it in theirs. Both sides now hold signed evidence of both halves — bilateral witnessing, which is why no global chain is needed (memo §4).
  6. A's later claim is bounded and honest: "B holds my signed transmittal and I hold B's signed receipt over exactly these bytes, as of my anchor state of <as-of>" ([M-K21]).

Equivocation remains possible and undetectable: A can sign two conflicting "rev A" manifests to two different counterparties (memo §10.6, §11.5). Resolution is discovery, not machinery. A conforming implementation does not claim otherwise.

9.2 Submittal → response → receipt (the review round-trip)

  1. A issues a transmittal (a submittal) to B; B receipts it.
  2. B drafts in reviewing/<A>/, whose .zddc carries received_path back to the canonical submittal.
  3. B issues a response: re = the submittal id, status = the deployment's verdict code verbatim ([M-S22]), optional per-document verdicts, optional manifest of returned markups — staged and committed through the ordinary handoff, landing in B's issued/<M>/… mirroring A's received/<M>/….
  4. A verifies and issues a receipt for the response, closing the round-trip with signatures on both halves ([M-S25]).
  5. A issues rev B as a new transmittal carrying supersedes: [<rev-A id>] ([M-S14]) — a new thread root, not a reply. The UI's revision/status progression (issued A (IFR) → returned 0 (RSA)) is the view; these statements are the substance behind it.

10. Currency — the second verdict axis

[M-S46] Verification has two orthogonal axes and every surface MUST present both (memo §6.12):

  • crypto tri-state — valid / unknown-because-stale / invalid ([M-K20]);
  • currency — current / superseded / withdrawn / unknown.

[M-S47] Currency is derived, never stored: per document thread, current = the latest verified issue act on the thread that no verified supersedes ([M-S14]) or withdrawal ([M-S26]) names. Any tool recomputes it by scanning the thread's _ledger/ (the server/browser split rule — nothing derived is stored).

"Names", pinned — because the word was carrying two readings. A statement names another exactly through the field that points at it:

  • a transmittal names what it supersedes through supersedes[];
  • a withdrawal names what it withdraws through re — §6 makes re the withdrawn transmittal, and it is REQUIRED there.

thread names nothing for this purpose. It is a grouping aid, explicitly redundant ([M-S8]), and a verifier MUST NOT treat a withdrawal whose thread matches the subject — but whose re does not — as withdrawing it. The two readings differ in kind, not in an edge case: matching on thread retires every statement in a thread as soon as any one of them is withdrawn, which is a much larger act than the one the signer performed. A field a tool may rewrite while grouping ([M-S18]'s words) must not be able to retire records (decisions.md, D-S10).

A statement never names itself. A supersedes[] entry, or a withdrawal's re, equal to the subject's own id is IGNORED. It cannot occur in an honest statement at all — an id is the SHA-256 of the payload, so a payload naming its own id would have to contain its own hash — so a ledger projection asserting one is corrupt, and the fail-closed answer to corrupt derived state is to let it retire nothing rather than to let it retire the record it is attached to (decisions.md, D-S11).

[M-S48] A rev-A artifact verified in isolation — a zip handed over out-of-band, with no link ledger — MUST report currency unknown, never current. "Valid signature, superseded content" is the everyday true answer about an old drawing, and "valid signature, currency unknown" is the honest answer about a loose zip. Reporting only the crypto axis is the failure mode this requirement exists to prevent.

[M-S49] Re-presenting a statement already in the ledger is a no-op, identified by its content-addressed id — not a new act, not an error (memo §6.12).


11. The local statement: record-revision

Added 2026-08-16 (docs/design/record-signing.md S2, DECIDED 2026-08-16; decisions.md D-29). The seven types above are link acts: two parties, a per-link prev chain, receipts. This section adds the one local statement — a server's own signed assertion that it committed one revision of one record. It reuses the whole envelope profile ([M-E1]–[M-E12], the PAE, Ed25519, keyid) and the canonical form ([M-C*]); what it deliberately does not reuse is the link grammar. There is no counterparty, so there is no from/to, no link binding, no prev, no re/thread, and no receipt — the record's own hash chain (previous_sha, docs/design/record-signing.md D3) is its ordering, and dropping a middle revision breaks that chain exactly as dropping a middle statement breaks [M-S5]'s.

Trigger. The server's record-write chokepoint (writeWithHistory) commits a revision: audit fields stamped, prior bytes snapshotted to history, new bytes live. The statement is the D4 routine tier — the server operational key vouching "the server, having authenticated principal, recorded these bytes as revision N" — never the person's own intent, which only a personal signature carries (record-signing memo §5).

Signed by a record signer ([M-S61]). In v1 that is the server operational key and the payload says so (key_kind: "server-operational"); the personal kinds are reserved ([M-S59]).

Body — which is the whole payload; there is no common header:

{
  "path": "100004/mdl/ACM/ACM-PRJ-SPC-0001.yaml",
  "revision": 4,
  "sha256": "<64-hex>",
  "previous_sha": "<64-hex, or null on the first tracked revision>",
  "principal": "alice@example.com",
  "elevated": false,
  "key_kind": "server-operational",
  "ts": "2026-08-16T12:00:00Z"
}
  • [M-S53] §2 does not apply to this type, and the field list above is the complete declared list. from, to, ts aside (declared here in its own right), prev, re and thread are not declared fields of this type: a payload carrying one has it ignored as an unknown field ([M-C7], signature-covered, granting nothing). This is stated because the obvious shortcut — running the §2 header checks on every type — would refuse every honest record-revision for having no from. The [M-C11] typing rule applies to each declared field below, class malformed (nothing in this payload is link plumbing, so [M-S51]'s bad_link carve-out never applies).
  • [M-S54] path is REQUIRED: the record's logical path in the signing server's own tree, relative to its serving root — the same frame as [M-K14]'s scope/container — in the [M-M9]/[M-M10] encoding (no leading /, no ./.., no .zddc.d segment, ≤ 1024 bytes). Violation: bad_path. The frame is stated honestly: two servers holding mirrors of one tree may address the same record differently, and the statement is the signing server's local evidence, meaningful in its frame.
  • [M-S55] revision is REQUIRED: an integer ([M-C3]) ≥ 1 — the record's server-stamped revision audit field. Violation: malformed.
  • [M-S56] sha256 is REQUIRED: the lowercase-hex SHA-256 of this revision's stored canonical bytes — the server-normalized YAML that writeWithHistory wrote, which are the only canonical bytes a record has (record-signing memo D3). This is a plain SHA-256, deliberately not zddc-merkle-v1: it MUST be the same value the successor revision's previous_sha audit field will carry, so the chain and the signatures speak one hash vocabulary and every link is checkable against every envelope with no second construction. Not 64 lowercase hex: malformed.
  • [M-S57] previous_sha is REQUIRED: null on the first tracked revision, else the full SHA-256 of the prior revision's stored bytes — verbatim the record's own previous_sha audit field. It is deliberately not coupled to revision == 1: a record whose early history predates audit stamping can carry revision: 1 with a predecessor (the lazy- migration path in writeWithHistory), and a rule inventing a coupling the data does not have would refuse honest statements about real records. Wrong type or a non-null value that is not 64 lowercase hex: malformed. Absent is not null ([M-S5]'s reasoning verbatim: an omitted chain link is how a middle revision gets dropped without breaking anything visible).
  • [M-S58] principal is REQUIRED, a string: the acting identity exactly as the server authenticated and stamped it (the record's updated_by) — which may be "anonymous" on an unauthenticated deployment, carried verbatim. elevated is REQUIRED, a boolean, and is always written explicitly — omitting it when false would give one act two byte forms (D-7's rule). Both are evidentiary depth, never access control ([M-K29]'s posture): a verifier MUST NOT treat principal as a verified attribute of a person — it is the server's authentication claim, exactly as trustworthy as the server that made it.
  • [M-S59] key_kind is REQUIRED and names what kind of key vouches (record-signing memo D5 — signature records carry their kind, and no UI may render an operational signature as a personal attestation). v1 implements all three D5 kinds: server-operational, personal-software (the S3 addendum — a person's own registered key file signing a deliberate act; the binding is [M-S64]), and — since the S4 addendum, 2026-08-17 — personal-webauthn (a hardware authenticator's assertion; the signature encoding is envelope.md §8 and the kind↔︎encoding coupling is [M-S65]). The [M-S52] split applies verbatim to anything else: absent is malformed (this is not a record-revision payload); present and not implemented is unsupported_type (a statement this verifier cannot evaluate) — the branch every reserved kind exited through, and the answer any future kind gets until its own addendum lands.
  • [M-S60] ts is REQUIRED, an instant per [M-C6] (the field is already in the instant family). Asserted and informational, exactly as [M-S4]: the record's own stamped updated_at lives inside the bytes sha256 pins, so the payload does not duplicate it and ts is merely when the signer says it signed.
  • [M-S61] Signer resolution. The verifier resolves signatures[0].keyid against its record-signer set — the keys it has been told, out of band, may sign this server's record revisions (keys.md §8, [M-K31]; in the conformance context this is record_signers). A keyid matching no entry is unknown_key; an empty or absent set is never a pass ([M-E21]'s rule) — a verifier with no record-signer knowledge refuses, it does not TOFU. The matched entry's declared kind MUST equal the payload's key_kind (unknown_key otherwise): the kind travels in the signed payload and rides the trust entry, and binding them is what makes D5 checkable rather than a UI courtesy. [M-E13] applies as written — exactly one signature. In v1 the result has no staleness axis: a record-signer set carries no revocation machinery ([M-K31] states the limit), so the tri-state collapses to valid/invalid.
  • [M-S64] The personal binding (S3 addendum, 2026-08-17; extended to personal-webauthn by the S4 addendum). When the payload's key_kind is a personal kind — personal-software or personal-webauthn — the matched signer entry MUST carry the identity the key is registered to (email, keys.md §9/§10), and it MUST equal the payload's principal, compared case-insensitively — the [M-K13] match posture, one registry over. Violation, and an entry carrying no identity at all: unknown_key — a key registered to nobody can vouch personally for nobody, and a key registered to Alice signing "Sam signed off" is the exact laundering this rule exists to refuse. The operational kind deliberately has NO such rule: the server's key signs for every authenticated principal, and principal there is the server's authentication claim, not the signer's identity ([M-S58]).
  • [M-S65] The kind↔︎encoding coupling (S4 addendum, 2026-08-17; widened to content-attestation by the §12 addendum). A payload whose key_kind is personal-webauthn MUST carry the webauthn extension on its one signature entry, and its signature verifies ONLY under the assertion profile (envelope.md §8, [M-E25]–[M-E27]) — never the Ed25519 equation. Conversely, a webauthn extension under any other key_kind, or on a payloadType that declares no key_kind at all (every link type), is malformed. Both directions are the same D5 honesty one layer down: there is no Ed25519 spelling of a hardware assertion, no assertion spelling of a software signature, and no envelope that leaves a verifier to guess which equation applies.
  • [M-S62] Bytes-held agreement. When the verifier holds the revision's stored bytes (the live record, or the history snapshot this revision became — supplied as record_bytes_b64 in the conformance input), it MUST check that sha256 equals the SHA-256 of those bytes; disagreement is malformed. A verifier not holding the bytes MUST NOT report the check as satisfied — [M-M6]'s rule, one door over. The check is deliberately a hash equality and nothing more: agreement between the statement and the bytes' own audit fields is the record-chain walk's job (it re-hashes the actual lineage, which is stronger than field comparison), and putting a YAML parse inside this profile would make the conformance layer depend on a format it otherwise never reads.
  • [M-S63] Placement, travel, and the two honest limits. On disk: <dir>/.zddc.d/sigs/<stem>/<revision>-<sha8>.dsse.json, beside the record's history sidecar and inside any folder-shaped record's container — where <sha8> is the first 8 hex characters of sha256, and the filename is presentation, never authority ([M-S44] verbatim: identity is the payload hash, type is payloadType, and a verifier tolerates any name). Unlike _ledger/ ([M-S43]), these envelopes live in the reserved enclave and deliberately do NOT travel: they are the server's own evidence about its records, verified in place (the record-chain check) or served through the server's mediated surfaces exactly as history is — never packaged into a transmittal, whose custody evidence is its own transmittal statement. The limits, stated because implementations must not over-claim: (1) an operational-key signature attests the server's authentication of principal, never the person's intent (D4); (2) the currency vocabulary of §10 does not apply — a record-revision is not an issue act, and the revision ordering axis is the record's own hash chain, walked by re-hashing, not derived from statements.

12. The local statement: content-attestation

Added 2026-08-17 (docs/design/transmittal-producer.md, DECIDED 2026-08-17; decisions.md D-32). The second local statement (registry entry #9): a person's deliberate, signed responsibility for the content of a set of files. It exists because the transmittal deliberately cannot say this: a transmittal is the issue act — "I issue exactly these bytes to this recipient" — signed by whoever runs the handoff, and a document controller cannot honestly vouch for engineering content they did not produce. The person who can, signs this statement instead; the two acts join by content (below), and several people may each attest their own files, coverage being the union.

Like §11 it reuses the envelope profile ([M-E1]–[M-E12]) and the canonical form ([M-C*]) and none of the link grammar: no counterparty, so no from/to, no link binding, no prev, no re/thread, and no receipt. Unlike §11 it is signed by a person, never a server — and unlike §11's enclave placement it travels ([M-S72]), because its whole point is that the recipient of a transmittal sees who took responsibility for what.

Trigger. An in-staging gesture on an assembled transmittal container (staging/<party>/<slot>/<mailbox>/<T>/), moving nothing — performed once the files are in staging, where the project team can no longer modify them (the cr ratchet), so the signer attests bytes their colleagues cannot silently change afterward.

Body — which is the whole payload; there is no common header:

{
  "manifest": [{"path": "dwg/ACM-PRJ-DWG-0001_B.pdf", "size": 481203,
                "root": "<64-hex>"}],
  "principal": "alice@example.com",
  "key_kind": "personal-software",
  "ts": "2026-08-20T09:00:00Z"
}
  • [M-S67] §2 does not apply to this type, and the field list above is the complete declared list — [M-S53] verbatim, one type over: from, to, prev, re and thread are not declared fields; a payload carrying one has it ignored as an unknown field ([M-C7], signature-covered, granting nothing). [M-C11] applies to each declared field, class malformed (nothing here is link plumbing, so [M-S51]'s bad_link carve-out never applies).
  • [M-S68] manifest is REQUIRED and non-empty, per [M-M6]–[M-M11] — the same entry law as a transmittal's: path in the [M-M9]/[M-M10] in-container encoding, size, root a zddc-merkle-v1 digest. The entries are the substance: what is attested is these bytes at these paths. Nothing about any shipment, recipient, or container name appears in the payload — responsibility for content is recipient-independent, and a container rename between attestation and issue changes nothing the signer said.
  • [M-S69] principal and key_kind are REQUIRED strings. key_kind MUST be a personal kind — personal-software or personal-webauthn ([M-S59]'s vocabulary). server-operational is declared-and-forbidden: a server authenticates people, it does not take content responsibility, and a payload claiming so is malformed — not unsupported_type, which is reserved by the [M-S52] split for kinds this spec has not defined (those still exit through unsupported_type). The [M-S65] kind↔︎encoding coupling applies verbatim: personal-webauthn verifies only under the assertion profile and MUST carry the webauthn extension; personal-software never carries one.
  • [M-S70] ts is REQUIRED, an instant per [M-C6] — asserted and informational, [M-S4]'s posture; never used to judge key validity ([M-K19]).
  • [M-S71] Signer resolution — one binding rule, vantage-dependent registries. The verifier resolves signatures[0].keyid against every signer registry its context supplies: the record-signer set ([M-S61]'s record_signers — the local vantage, fed by the users registry, [M-K32]/[M-K33]) and each anchored party's derived operational fold (the counterparty vantage, [M-K16] — where the WHOLE [M-K17] chain runs as written with principal standing as the acting person: certifying op kind sign, validity window and revocation at the [M-K20] instant, the op's email AND the party row's emails allowlist ([M-K13]) — and a length-1 match, the self-certified root, certifies no person and therefore refuses). Every matched entry MUST carry a registered identity (email) and it MUST equal the payload's principal, compared case-insensitively — the [M-S64] binding, both registries, no exception for any match. Zero matches anywhere, a match carrying no identity, or any match whose identity disagrees: unknown_key. An empty or absent context is never a pass ([M-E21] — no TOFU). [M-E13] applies as written — exactly one signature. A verifier on no link and with no record-signer knowledge therefore cannot resolve this statement, which is the bilateral limit stated honestly, not a defect: in-band carriage ([M-S66]) exists so the recipient always can.
  • [M-S72] Placement, travel, and the join. On disk: <container>/_ledger/<seq>-content-attestation-<id8>.dsse.json — [M-S44] naming (filename is presentation, never authority), [M-S45] travel (it mirrors and ships; this type is the reason the rule matters). The attestation joins a transmittal by content, never by reference: an attested entry covers a transmittal manifest entry when path AND root are equal. There is no linking field in either payload, deliberately — content-addressing already joins them, an edited file stops matching and shows as uncovered, and an unchanged file's attestation survives re-issuance for free. The join is reporting, never a verification precondition: an attestation verifies standalone, and coverage ("N of M entries attested") is a statement a surface makes about a transmittal, with the DC's commit gesture surfacing — never gating — it (rung 1, F-3). The honest limits, [M-S63]'s posture: (1) an attestation is evidence of a deliberate act, not an approval workflow — no status, no gate lives in this spec; (2) it attests exactly the entries it lists — silence about a file is silence, not refusal; (3) the §10 currency vocabulary does not apply — a content-attestation is not an issue act and supersedes nothing.
Rendered from “The Seven Statement Types” ZDDC