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
transmittalcarryingderived_from; - supersession is not a type — it is a
transmittalcarryingsupersedes, so issuing and superseding are one atomic act; - revocation is not a type — it is an
anchor-updateop.
[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.partyis 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.nameis OPTIONAL, informational, and a verifier MUST NOT use it in any decision.from.personis 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'semails([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.mdD-15.)Conversely — the case the sentence above leaves silent — when the signing chain IS length-1,
from.personMUST be absent. A self-certified root certifies no person, so there is nothing for a presentpersonto 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.partyorto[].partythat is not 64 lowercase hex characters MUST be rejectedbad_link, notmalformed— even though every other bad digest in this profile ismalformed([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 ismalformed. Stated because a verifier that classified by "it's a hex field" would reportmalformedfor a party named rather than fingerprinted, and the class is part of the conformance contract.[M-S3]
to— REQUIRED array of objects with the sameparty/nameshape. 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 missingts, a non-stringts, and atsin the wrong form are one answer.tsis the time vocabulary, whose class [M-C6] fixes outright, and it is not link plumbing: [M-S51] reservesbad_linkfor values that name a party, and a timestamp names nobody. Stated because the neighbouring header fields do answerbad_link, which is how one implementation came to givetstheir class ([M-C11]'s table is the whole line).[M-S5]
prev— REQUIRED,nullon 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).previs 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
prevequals 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;tsnever overrides it.[M-S7]
re— the id of the statement this one answers. Absent on thread roots. MUST be present onreceiptandresponse; MUST be absent ontransmittal([M-S13]).[M-S8]
thread— the id of the thread root. Absent on roots, and REQUIRED wheneverreis present. When present it MUST equal the terminus of there-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
rewith nothreadwas 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 omittingthreadhas 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 isreitself and the rule reduces tothreadMUST equalre. 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 staysunknownin 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 andthread= 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.partyandto[0].partyMUST 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.partyat 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
.ziphanded 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.)
- no ends at all (the key absent, or an
empty list) — the verifier was not told, which is the ordinary
offline case: a
[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]
nameandmailboxare REQUIRED single path segments per [M-M12]. The mailbox travels in the signed payload so the issuer'sissued/<M>/<T>mirrors the receiver'sreceived/<M>/<T>and the two ledgers stay reconcilable on the path (memo §5c). The<party>segment and theissued/receivedslot are each side's own frame and MUST NOT appear in the payload ([M-M9] context). - [M-S12]
manifestis REQUIRED and non-empty, per [M-M6]–[M-M11]. - [M-S13] A
transmittalis always a thread root:reandthreadMUST be absent (bad_linkotherwise). 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 atransmittalon 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_fromadds 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_rootis 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_rootMUST equalre. The redundancy is deliberate and load-bearing in the same way [M-S8] is:reis link plumbing that a tool may rewrite while grouping;manifest_rootis 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_rootechoes whatever statement idrenames. (The field name is transmittal-flavoured for a general echo —decisions.mdD-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]
statusis 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; whatRSA/RSCmean 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 REQUIRESpath([M-M9]–[M-M11]) and MAY carrystatusandremarks, 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_pathotherwise).An entry that is not an object, or whose
pathis absent or not a string, ismalformed— [M-E23] rule 2's shape-before-content line, which is written for a manifest entry and whichdocuments[]therefore inherits nothing from unless it is said here. An entry that is not an entry has no path to judge, sobad_pathwould be a rejection for a reason the verifier never reached (decisions.md, D-S6).bad_pathstays for apaththat 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; aresponsewith 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]
reasonis OPTIONAL free text. - [M-S27]
manifestMUST 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).
7.1
anchor-update — key management on the link
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]
opsis REQUIRED, a non-empty array. Each op REQUIRESop∈ {add,rotate,revoke} andkey.[M-S30]
addandrotateREQUIREalg([M-K7]),kind(sign|kem),not_beforeandnot_after([M-C6],not_before<not_after), and — forkind: sign—email, which is what makes the allowlist cryptographically real (memo §10.3).rotatediffers fromaddonly in intent and MAY carryreplaces; a verifier treats both identically. (The memo writes this asvalidity; expanding it to an explicit window isdecisions.mdD-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 ownkey(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 arevokeop ([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 carryconfirmed_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.nameposture verbatim: a verifier MUST NOT use it in any decision. It exists because the statement itself cannot carry the confirmer honestly — ananchor-updateis root-signed, a root is length-1, and [M-S2] therefore forbidsfrom.personon it. Being a declared field, [M-C11] applies: present with a non-string value is a refusal (malformed), never a shrug.[M-S31]
revokeREQUIRESkeyand MAY carrycompromise_date([M-C6] date form). With a compromise date, signatures by that key at or after the date areinvalideven 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:
tsis 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 nocompromise_dateMAY carryrevoked_at, an instant ([M-C6]) that the verifier records when it applies the op — in practice the receipt timestamp of the revokinganchor-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_datenor arevoked_atMUST fail closed — every signature by that key isunknown_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-updateMUST NOT change the root key. Root change issuccession([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.
7.2
consent-withdrawal — prospectively un-sharing a
scope
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]
scopeis 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,drawingsun-shares that directory and a sibling file nameddrawings.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, notmalformed. Absence, and presence with a non-string type, are structural and answermalformed([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../xdoes ([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).- the single literal
[M-S35]
effectiveis REQUIRED, a calendar date ([M-C6]).reasonis OPTIONAL free text, exactly as [M-S26] makes it on awithdrawal. 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_rootis REQUIRED and MUST equal the raw public key whose fingerprint issignatures[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_rootMUST NOT name a key that is already anchored to any party in the verifier's anchor state — neither as a party'sroot_keynor as an operational key it has learned from a verifiedanchor-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 othernew_rootrule.)The reason is identity, not hygiene.
from.partyis the fingerprint of a party's anchored root ([M-S2]), so two parties anchored to one key are one identity on the wire: everyfrom/tofield, 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_rootto the signer.[M-S52]
algis REQUIRED and MUST name an algorithm this version implements — in v1 that ised25519and nothing else ([M-K7]). An absentalgismalformed; a present one that this version does not implement — whether unknown, or reserved-but-unimplemented likeecdsa-p256— isunsupported_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
successionis the one statement that declares a key instead of looking one up —new_rootis 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
successionnamingecdsa-p256is 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]
linkedis 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 betweenlinkedand the signature count MUST be rejected (malformed). The full cardinality, ordering, and signer-identity rules areenvelope.md§5.[M-S39] There is NO
old_sigpayload field, and a producer MUST NOT emit one. The old root's proof of continuity issignatures[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.mdD-10. A payload carryingold_sigis 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 isconformance.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]
nameis 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]).reasonis likewise OPTIONAL free text ([M-S26]), declared for the same reason it is declared on aconsent-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 withderived_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.
prevchains per (signer party, link) across all statement types, so two carriedanchor-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 carriedprevlinks, that key is unresolved — signatures under it areunknown_key, fail closed. A verifier MUST NOT break the tie by filename<seq>([M-S44]: presentation, never authority), byts([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)
- A (issuer) assembles the package in
staging/<B>/issued/<M>/<T>/, computes each file'sroot([M-M1]–[M-M4]), builds thetransmittalpayload, 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). - A commits: the tail moves same-path into
archive/<B>/issued/<M>/<T>/(WORM). The statement rides along; it was already inside the container. - The package reaches B — over the link, or on a USB stick. Transport is an optimization for freshness, never a security dependency.
- B verifies: envelope ([M-E*] §6), header
([M-S2]–[M-S10]), each file's bytes against its
root, andprevagainst A's chain tip ([M-S6]). Any failure → refuse at the door, surface it, do not provisionally accept ([M-E21]). - B issues a
receiptechoing 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). - 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)
- A issues a
transmittal(a submittal) to B; B receipts it. - B drafts in
reviewing/<A>/, whose.zddccarriesreceived_pathback to the canonical submittal. - B issues a
response:re= the submittal id,status= the deployment's verdict code verbatim ([M-S22]), optional per-document verdicts, optionalmanifestof returned markups — staged and committed through the ordinary handoff, landing in B'sissued/<M>/…mirroring A'sreceived/<M>/…. - A verifies and issues a
receiptfor the response, closing the round-trip with signatures on both halves ([M-S25]). - A issues rev B as a new
transmittalcarryingsupersedes: [<rev-A id>]([M-S14]) — a new thread root, not a reply. The UI's revision/status progression (issuedA (IFR)→ returned0 (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
transmittalnames what it supersedes throughsupersedes[]; - a
withdrawalnames what it withdraws throughre— §6 makesrethe 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,tsaside (declared here in its own right),prev,reandthreadare 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 honestrecord-revisionfor having nofrom. The [M-C11] typing rule applies to each declared field below, classmalformed(nothing in this payload is link plumbing, so [M-S51]'sbad_linkcarve-out never applies). - [M-S54]
pathis REQUIRED: the record's logical path in the signing server's own tree, relative to its serving root — the same frame as [M-K14]'sscope/container— in the [M-M9]/[M-M10] encoding (no leading/, no./.., no.zddc.dsegment, ≤ 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]
revisionis REQUIRED: an integer ([M-C3]) ≥ 1 — the record's server-stampedrevisionaudit field. Violation:malformed. - [M-S56]
sha256is REQUIRED: the lowercase-hex SHA-256 of this revision's stored canonical bytes — the server-normalized YAML thatwriteWithHistorywrote, which are the only canonical bytes a record has (record-signing memo D3). This is a plain SHA-256, deliberately notzddc-merkle-v1: it MUST be the same value the successor revision'sprevious_shaaudit 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_shais REQUIRED:nullon the first tracked revision, else the full SHA-256 of the prior revision's stored bytes — verbatim the record's ownprevious_shaaudit field. It is deliberately not coupled torevision == 1: a record whose early history predates audit stamping can carryrevision: 1with a predecessor (the lazy- migration path inwriteWithHistory), 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]
principalis REQUIRED, a string: the acting identity exactly as the server authenticated and stamped it (the record'supdated_by) — which may be"anonymous"on an unauthenticated deployment, carried verbatim.elevatedis 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 treatprincipalas 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_kindis 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 isenvelope.md§8 and the kind↔︎encoding coupling is [M-S65]). The [M-S52] split applies verbatim to anything else: absent ismalformed(this is not a record-revision payload); present and not implemented isunsupported_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]
tsis 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 stampedupdated_atlives inside the bytessha256pins, so the payload does not duplicate it andtsis merely when the signer says it signed. - [M-S61] Signer resolution. The verifier
resolves
signatures[0].keyidagainst 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 isrecord_signers). Akeyidmatching no entry isunknown_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 declaredkindMUST equal the payload'skey_kind(unknown_keyotherwise): 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 tovalid/invalid. - [M-S64] The personal binding (S3
addendum, 2026-08-17; extended to
personal-webauthnby the S4 addendum). When the payload'skey_kindis a personal kind —personal-softwareorpersonal-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'sprincipal, 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, andprincipalthere 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-attestationby the §12 addendum). A payload whosekey_kindispersonal-webauthnMUST carry thewebauthnextension 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, awebauthnextension under any otherkey_kind, or on apayloadTypethat declares nokey_kindat all (every link type), ismalformed. 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_b64in the conformance input), it MUST check thatsha256equals the SHA-256 of those bytes; disagreement ismalformed. 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 ofsha256, and the filename is presentation, never authority ([M-S44] verbatim: identity is the payload hash, type ispayloadType, 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 owntransmittalstatement. The limits, stated because implementations must not over-claim: (1) an operational-key signature attests the server's authentication ofprincipal, never the person's intent (D4); (2) the currency vocabulary of §10 does not apply — arecord-revisionis 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,reandthreadare 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, classmalformed(nothing here is link plumbing, so [M-S51]'sbad_linkcarve-out never applies). - [M-S68]
manifestis REQUIRED and non-empty, per [M-M6]–[M-M11] — the same entry law as a transmittal's:pathin the [M-M9]/[M-M10] in-container encoding,size,rootazddc-merkle-v1digest. 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]
principalandkey_kindare REQUIRED strings.key_kindMUST be a personal kind —personal-softwareorpersonal-webauthn([M-S59]'s vocabulary).server-operationalis declared-and-forbidden: a server authenticates people, it does not take content responsibility, and a payload claiming so ismalformed— notunsupported_type, which is reserved by the [M-S52] split for kinds this spec has not defined (those still exit throughunsupported_type). The [M-S65] kind↔︎encoding coupling applies verbatim:personal-webauthnverifies only under the assertion profile and MUST carry thewebauthnextension;personal-softwarenever carries one. - [M-S70]
tsis 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].keyidagainst every signer registry its context supplies: the record-signer set ([M-S61]'srecord_signers— the local vantage, fed by the users registry, [M-K32]/[M-K33]) and each anchored party's derivedoperationalfold (the counterparty vantage, [M-K16] — where the WHOLE [M-K17] chain runs as written withprincipalstanding as the acting person: certifying opkindsign, validity window and revocation at the [M-K20] instant, the op'semailAND the party row'semailsallowlist ([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'sprincipal, 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 atransmittalby content, never by reference: an attested entry covers a transmittal manifest entry whenpathANDrootare 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 — acontent-attestationis not an issue act and supersedes nothing.