Part I — Foundations · Chapter 11
The Canon Standard
The Canon Standard
"Canon specifies the what (artifact shape, protocol, hash algorithm, signature algorithm); it does not specify the how (internal implementation of the four stages, which challenge types must be applied in any given domain, user-interface affordances)." — Meridian-Canon v0.2.0 §4.1.1
This is the chapter that earns the rest of the book.
Canon is the formal contract every attestation produced by every implementation must satisfy. The parent in case 2024JC000099 has a right to verify any exhibit the state offers against them. So does opposing counsel. So does the judge. Canon makes that verification structural — independent of the issuer's cooperation, independent of the system that produced the artifact. By the end of this chapter, you will be able to describe exactly how that works: down to the hash function, the canonicalization algorithm, and each of the seven steps a recipient runs to reach a definitive verdict.
If you read no other chapter in Part I, read this one.
At a glance
- The Canon specification (v0.2.0) is implementation-neutral and source-available; conformant attestations from any implementation are byte-for-byte verifiable by any other.
- An attestation has four blocks — Witness, Findings, Refutation, Seal — and must satisfy nine numbered requirements (R1–R9). All-or-nothing: a non-conformant artifact is not a Canon attestation.
- A recipient verifies an attestation by running seven deterministic steps; steps 1–6 yield pass/fail; step 7 surfaces the declined- challenges inventory for substantive review. No issuer cooperation required.
Learning objectives
After reading this chapter you should be able to:
- State what Canon specifies and what it explicitly leaves to implementation, and explain why the boundary is drawn there.
- Describe the four blocks (Witness, Findings, Refutation, Seal) and the semantic dependency that makes their ordering mandatory.
- Map each of the nine requirements (R1–R9) to the block or field that satisfies it.
- Execute the seven-step falsification protocol by hand against a provided attestation JSON, and identify, for any failure, which step produced it and what the failure implies.
- Construct the chain hash — serialize without the seal field, hash, sign the hash — and explain why RFC 8785 canonicalization is required rather than standard JSON serialization.
- Distinguish the four attestation kinds (ObservationAttestation, SearchAttestation, BriefAttestation, AuditAttestation), state when each is emitted, and describe the supports-graph path from a BriefAttestation back to its originating ObservationAttestations.
- Explain why the declined-challenges inventory (R6) is the element that distinguishes Canon from Sigstore, in-toto, C2PA, and W3C VC.
What Canon is, and is not
Canon is a protocol specification. It tells you the structure and the rules a JSON document must satisfy to be a Canon Attestation, and the protocol a recipient runs to verify one. It does not tell you which language to write your implementation in, which database to back it with, which language model to use for enrichment, or which corpus to ingest from. Those are all implementation choices.
Implementation neutrality buys interoperability. An attestation produced by a Python implementation, signed under one issuer's Ed25519 key, is verifiable by a Rust verifier, a Go verifier, or a JavaScript verifier against the same public key URL, with no coordination between implementations. The recipient does not need to know what produced the artifact; they run the seven-step protocol.
Canon is not a software library. It is not a database schema. It is not an ML pipeline. The book you are reading describes one such library, schema, and pipeline (Meridian-Cannon), but Canon itself is an order of magnitude smaller in scope: it is the structural and cryptographic contract the library, schema, and pipeline must satisfy at their output boundary.
▼ Why It Matters. Implementation-neutrality is what lets a court in 2030 verify an attestation produced in 2026 by a system that no longer exists. The bytes are self-describing; the protocol is fixed; the recipient runs the seven steps and either succeeds or pinpoints the failure. Long-archive verifiability is the property Canon's design choices serve. When you wonder why R7 mandates RFC 8785 instead of letting each implementation pick its own canonicalization, the answer is: a verifier in 2030 cannot phone the issuer.
The four blocks
Every Canon attestation contains exactly four blocks, in this order:
- Witness — what was observed.
- Findings — what was inferred.
- Refutation — what challenges were applied and declined.
- Seal — the cryptographic binding over all of the above.
The ordering is semantic, not merely sequential. Each stage presupposes the prior, and a valid attestation includes all four. Removing any one — or reordering them — produces an artifact that is not a Canon attestation, full stop.
Witness
The Witness block is the function of observing. It produces records of what was seen, by whom, when, and in what state.
"witness": [{
"observation_id": "obs-01JABC...",
"source": "imessage://chat.db/handle:+17155559876/...",
"received_at": "2026-04-30T18:00:00.000000Z",
"custody_chain": [
{"custodian": "jpwhite", "received_at": "2026-04-30T18:00:00.000000Z"}
],
"content_hash": "sha256:f4c9...a2b3",
"content_ref": "file:///vault/2026/04/30/obs-01JABC.txt"
}]
A WitnessEntry must include:
- A stable, opaque
observation_idof the formobs-<id>. - AsourceURI identifying where the bytes came from. - Areceived_attimestamp in RFC 3339 with microsecond precision, in UTC. - Acustody_chainlisting each custodian transition with its ownreceived_at. - Acontent_hashof the formsha256:<64 hex>computed over the raw bytes. - Either acontent_ref(URI to retrievable bytes) or acontent_inline(base64 of the bytes themselves) — at least one must be present. The Witness block is plural: an attestation can witness many observations. The block is a list, not a single entry. The hash is the load-bearing element. The recipient of the attestation, performing step 4 of the verification protocol, retrieves the bytes — either inline or via the URI — and recomputes the SHA-256. If the recomputed hash matchescontent_hash, the bytes have not been altered. If not, the artifact has been tampered with at the witness layer.
Findings
The Findings block is the function of reasoning. It produces claims derived from observations and prior claims.
"findings": {
"method": "imessage_extractor.py v0.4.2",
"claims": [
{
"claim_id": "claim-01",
"statement": "The message rescheduled the visit from 2024-03-14 to 2024-03-21.",
"supports": ["obs-01JABC..."],
"inference_type": "deduction",
"gaps": ["Sender identity verified only by phone number."]
}
]
}
A Claim must include:
- A
claim_idunique within this attestation. - Astatement— the natural-language proposition. - Asupportsarray of length ≥ 1: each entry must resolve to either anobservation_idin this attestation's Witness block or aclaim_iddefined earlier in this Findings block. Forward references are prohibited; circular references are prohibited. - Aninference_typefrom the closed vocabulary:observation,deduction,induction,abduction,compound. - Agapsarray. May be empty only ifinference_typeisobservationand the claim asserts something directly present in the source bytes. Otherwise the array must contain at least one entry. Theinference_typeis not decorative. It is a formal commitment by the issuer to the epistemic status of the claim. | Type | Epistemic status | |---|---| |observation| Directly present in the source. ("The headerFrom:containedmwebb@…") | |deduction| Logically valid inference. ("Sender is caseworker-042, because the address matches.") | |induction| Generalization from examples. ("This sender writes in formal register, across N messages.") | |abduction| Best explanation for an observation. ("This message references the Smith motion, suggested by keywords.") | |compound| A claim combining multiple inference types. | > ◆ Going Deeper — Why R5 forbids "no gaps come to mind." > > The temptation in any extractor pipeline is to emit a non-empty >gapsarray only when something feels uncertain, and an empty one > otherwise. The Canon spec forbids this for a reason rooted in > Popper (Chapter 2): every inferred claim has at least one gap. > The statistical nature of the inference. The scope limit of the > training data. The acknowledged non-exhaustiveness of the sources > consulted. None of these is "no gap." > > Meridian-Cannon's runner enforces this in code: if the LLM returns > a non-observational claim with an emptygapsarray, the runner > inserts a generic scope-limit gap before sealing. The gap is real; > the runner only forces the issuer to admit it. > > A claim with a generic gap is more honest than a claim with no gap. > A claim with no gap on a non-trivial inference is, almost by > definition, sycophantic — the issuer's system has decided what the > issuer wants to hear and stripped the qualifications. R5 is the > structural defense. > > In the Pydantic enforcement layer (meridian/canon/schema.py), the >_gap_disclosuremodel validator raisesValueErrorat construction > time ifinference_type != InferenceType.OBSERVATIONandgapsis
empty. The constraint is not aspirational; it is a hard type error.
Refutation
The Refutation block is the function of challenging. It applies adversarial tests to the claims.
"refutation": {
"challenges": [
{
"challenge_id": "chal-01",
"type": "adversarial_prompt",
"targets": ["claim-01"],
"input": "Could this message have been about something other than rescheduling?",
"outcome": "survived",
"model_outcomes": {"M1": "survived", "M2": "survived", "M3": "revised"},
"consensus_outcome": "survived"
}
],
"coverage": {
"applied": ["adversarial_prompt", "consistency_check", "replay"],
"declined": [
{"type": "counter_evidence",
"reason": "no_negation_search_implemented_for_imessage_extractor"},
{"type": "coverage_audit", "reason": "deferred_to_batch_pass"}
]
}
}
A Challenge must include:
- A
challenge_idunique within this attestation. - Atypefrom the closed vocabulary:replay,adversarial_prompt,consistency_check,coverage_audit,counter_evidence. - Atargetsarray of one or moreclaim_ids defined in this attestation's Findings. - Aninput(the challenge content). - Anoutcomefrom the closed vocabulary:survived,failed,revised,contested. - Optionalmodel_outcomesandconsensus_outcomefor Tri-Model Consensus runs (Chapter 19). The Refutation block must include at least one challenge — R6 forbids empty refutation. It must also include acoverageobject with both anappliedlist (which challenge types ran) and adeclinedlist (which challenge types did not run, with machine-readable reasons). The declined inventory is Canon's load-bearing innovation. It is what distinguishes a self-critical artifact from a self-congratulatory one. A well-formeddeclinedentry names the challenge type and a short, machine-readable reason:"no_negation_search_implemented","deferred_to_batch_pass","language_not_supported". Human-readable prose alone is not sufficient; the reason field must be a token a downstream tool can match on. The five challenge types are detailed in Chapter 19. For now the point is structural: every attestation declares both what it tested and what it could have tested but didn't, and why. > ☉ In the Wild — Why "machine-readable reason" matters. > > The SolarWinds breach (December 2020) exposed how supply-chain > compromise hides in the gap between applied controls and > declared controls. A vendor's audit may report compliance with > 200 controls; an attacker hides in the 10 that the audit declared > out of scope in human-prose footnotes that no automated tool ever > read. > > Canon's R6 reflex is to require declines in machine-readable > tokens. A downstream tool consuming a corpus of attestations can > tally how many declinedcounter_evidencefor which reasons; a > tool can flag a sudden surge inlanguage_not_supporteddeclines
as evidence of a pipeline regression. The decline inventory is queryable. A human-prose-only decline disclosure is, for the same purpose, opaque.
SLSA, in-toto, and C2PA all eventually converged on similar structured-decline disciplines after early human-prose schemes proved unauditable at scale. Canon is starting where they ended.
Seal
The Seal block is the function of binding. It commits the issuer to everything above.
"seal": {
"payload_type": "application/vnd.nora.canon.attestation+json; version=0.2.0",
"payload": "<base64url-encoded JCS bytes of the attestation without the seal field>",
"signatures": [{
"keyid": "sha256:7c11...8901",
"sig": "<base64url-encoded Ed25519 signature>",
"public_key_url": "https://keys.example.org/meridian-pipeline/ed25519.pem"
}],
"chain_hash": "sha256:09a1...d2ef"
}
This is the DSSE envelope (Dead Simple Signing Envelope), the signing format introduced in Canon v0.2.0. It is the same envelope format used by SLSA and in-toto, defined in the IETF/CNCF DSSE spec.
The Seal must include:
- A
payload_typestring:"application/vnd.nora.canon.attestation+json; version=0.2.0". This string is itself a falsifiable claim — a verifier who does not know DSSE can hash thepayload_typeand confirm it matches the expected format version. Substituting a differentpayload_typeinvalidates the signature. - Apayload— the RFC 8785 canonical serialization of the attestation excluding thesealfield, base64url-encoded. These are the bytes over which the chain hash is computed. - Asignaturesarray of one or more{keyid, sig, public_key_url}entries: -keyid— the SHA-256 fingerprint of the issuer's public key. -sig— the Ed25519 signature, base64url-encoded. -public_key_url— a stable HTTPS URL hosting the public key in PEM format (RFC 8410), maintained for the archival lifetime. - Achain_hashof the formsha256:<64 hex>— the SHA-256 of the JCS bytes stored inpayload. This is a convenience field: a recipient who has already decodedpayloadcan verify it without recomputing; a recipient who has not can use it as a pre-check before fetching keys. > ◆ Going Deeper — The DSSE signing construction, step by step. > > Canon v0.2.0 uses the DSSE (Dead Simple Signing Envelope) signing > format. Here is the construction precisely: > > 1. Take the full attestation JSON object. > 2. Remove thesealkey entirely (not null-set it; remove it). > 3. Serialize the remaining object using RFC 8785 (JSON Canonicalization > Scheme). RFC 8785 sorts object keys lexicographically, removes > insignificant whitespace, and normalizes Unicode escapes. The result > is a deterministic byte sequence regardless of the serializer that > produced the original document. Call these bytescanonical_bytes. > 4. Compute SHA-256 overcanonical_bytes. Prependsha256:to the > 64-character hex digest. This ischain_hash. > 5. Base64url-encodecanonical_bytes. This ispayload.
Construct the PAE (Pre-Authentication Encoding) over the payload type and payload:
PAE = "DSSEv1\n" + len_le8(payload_type) + "\n" + payload_type + "\n" + len_le8(payload) + "\n" + payloadwhere
len_le8(s)is the 8-byte little-endian encoding oflen(s)> (length in bytes), andpayload_typeis the string >"application/vnd.nora.canon.attestation+json; version=0.2.0". > > 7. SignPAEwith the issuer's Ed25519 private key. Base64url-encode the > 64-byte signature. This issigin thesignaturesarray.In code:
from meridian.canon.signing import sign_dsse, CANON_PAYLOAD_TYPE sig = sign_dsse(private_key, canonical_bytes) # Equivalent to: Ed25519.sign(PAE(CANON_PAYLOAD_TYPE, canonical_bytes))The implementation is in
meridian/canon/canonicalize.py> (canonicalize_for_seal),meridian/canon/hashing.py(sha256_hex), > andmeridian/canon/signing.py(sign_dsse,CANON_PAYLOAD_TYPE). > > Why PAE rather than signing the hash string directly (as in v0.1.1)? > PAE binds thepayload_typeinto the signed material. An attacker who > replaces thepayload_typefield in the envelope — attempting to make a > v0.2.0 attestation appear to be a different format — must also forge the > signature. Without the issuer's private key, they cannot. The version > string is now cryptographically pinned, not just advisory. > ◆ Going Deeper — The algorithm-substitution defense. > > In Canon v0.2.0 the DSSE envelope binds thepayload_typestring — > which carries the format version — into the PAE before signing. An > attacker who substitutes a differentpayload_type, a different > canonicalizer, or a different signature algorithm at verification time > must also forge the signature; without the issuer's private key, they > cannot. > > XML-DSig (Chapter 7) put the canonicalization outside the signed > envelope and produced two decades of attacks. JWS-family schemes > added similar defenses retroactively. Canon has carried the > canonicalization-inside-signed-material defense since v0.1.0; DSSE > extends it to the format version string in v0.2.0. The choice is not > aesthetic. ## The nine requirements (R1–R9) A conformant implementation satisfies all nine, for every artifact emitted, with no partial credit: | R# | Requirement | What it forbids | |---|---|---| | R1 | Structural validity againstcanon.schema.jsonv0.2.0. | Any field deviation, missing field, type mismatch. | | R2 | Content integrity: SHA-256 over raw bytes, retrievable inline or viacontent_ref, re-hashable. | Hash drift between observation and seal. | | R3 | Supports closure: every claim'ssupportsresolves; no forward references; no cycles. | Claims floating without a chain to a witness; circular reasoning. | | R4 | Inference typing from the closed vocabulary. | Free-text inference labels; unlabeled claims. | | R5 | Gap disclosure: empty gaps only for observation-type claims over verified hashes. | "No gaps come to mind" as a reason for an empty array. | | R6 | Refutation completeness: at least one challenge; coverage with bothappliedanddeclined; declined entries have machine-readable reasons. | Reporting only successes; declining without explaining; human-prose-only reasons. | | R7 | DSSE sealing: RFC 8785 canonical form of attestation excludingseal→chain_hash; Ed25519 overPAE(payload_type, JCS_bytes); signature inseal.signatures[*].sig. | Hashing the wire format; signing without PAE; omittingpayload_typefrom signed material; algorithm substitution. | | R8 | Key discoverability:public_key_urlis reachable in PEM (RFC 8410); maintained for the archival lifetime. | Inline keys; ephemeral URLs; broken hosting. | | R9 | Falsifiability: a recipient with ordinary network access can reach a definitive verdict via the seven-step protocol without issuer cooperation. | Verification that requires the issuer to log in, look up records, or otherwise mediate. | | R10 (optional) | Transparency log: sealed attestations MAY be submitted to a Rekor/Sigstore log;seal.transparency_log_urlcarries the inclusion proof. Controlled byMERIDIAN_REKOR_ENABLED. Install:pip install "meridian-canon[transparency]". | Not conformance-breaking if absent; adds third-party witnessing and long-archive discoverability. | R1–R7 are mechanical. R8 is operational. R9 is the master requirement: it is the property the other eight requirements exist to support. A change to any of R1–R8 that breaks R9 (e.g., requiring the recipient to fetch a non-public log) is a non-conformant change. A useful calibration exercise: take any signed artifact you have — a signed Git commit, a Sigstore-signed binary, a PDF with a digital signature, a JWT from a recent login — and ask, for each of R1 through R9, whether the artifact carries something analogous. Most artifacts have R1, R2, R7, and R8 in some form. Few have R5 (gap disclosure) or R6 (refutation completeness). The gap between "signed" and "Canon-conformant" is precisely the gap between "somebody signed this" and "a recipient can audit what the issuer tried and did not try." ## The seven-step falsification protocol (§8.3) The falsification protocol is how a recipient reaches a verdict without trusting the issuer. It is the operational form of R9. Steps 1–6 are deterministic: each returnspassorfailwith a specific error. Step 7 is informational: it surfaces the declined-challenges inventory for the recipient's substantive judgment. The recipient decides whether the declines are acceptable; the protocol does not decide for them. The reference implementation ismeridian/canon/walk.py.
%%| label: fig-canon-verify
%%| fig-cap: "Canon v0.2.0 seven-step verification protocol"
flowchart TD
A([Receive DSSEEnvelope]) --> B[1. Decode payload\nbase64url → canonical_bytes]
B --> C{2. chain_hash\nmatch?}
C -- No --> FAIL1([❌ Field tampered])
C -- Yes --> D[3. Reconstruct PAE\nDSSEv1 + payload_type + canonical_bytes]
D --> E{4. Ed25519 verify\nsig over PAE}
E -- Invalid --> FAIL2([❌ Signature invalid])
E -- Valid --> F[5. Validate schema\nAgainst Canon v0.2.0]
F --> G{6. All challenges\nhave outcomes?}
G -- Missing --> WARN([⚠ Incomplete refutation])
G -- Yes --> H{7. Any declined\nchallenges?}
H -- Yes --> I([⚠ Accepted with\ndeclines noted])
H -- No --> OK([✅ Fully verified])
style OK fill:#10b981,color:#fff
style FAIL1 fill:#ef4444,color:#fff
style FAIL2 fill:#ef4444,color:#fff
style WARN fill:#f59e0b,color:#fff
style I fill:#f59e0b,color:#fff
Step 1 — Fetch the issuer's public key.
Retrieve the PEM document at seal.public_key_url. Compute its SHA-256 fingerprint. Compare to seal.public_key_fingerprint. If they do not match, the key hosting has been tampered with or substituted. fail: fingerprint mismatch. Step 2 — Verify the DSSE signature. Base64url-decode seal.payload to obtain canonical_bytes. Construct
the PAE:
PAE = "DSSEv1\n"
+ len_le8(seal.payload_type) + "\n" + seal.payload_type + "\n"
+ len_le8(canonical_bytes) + "\n" + canonical_bytes
Using the fetched public key, verify seal.signatures[0].sig (base64url- decoded) against PAE. If verification fails, either the attestation was altered after sealing, the payload_type was substituted, or the key is wrong. fail: DSSE signature does not verify. Step 3 — Verify the chain hash. SHA-256 the canonical_bytes from step 2 (the decoded seal.payload). Prepend sha256:. Compare to seal.chain_hash. If they do not match, the payload was tampered with after the envelope was assembled. fail: chain_hash mismatch. Note: if step 3 passes but step 2 failed, the PAE verification is wrong for the claimed payload — the seal was replaced without the signing key. If step 2 passes but step 3 fails, chain_hash was updated without re-signing — an inconsistent forge. As a secondary check: also deserialize canonical_bytes via RFC 8785 and confirm the reconstruction matches the outer attestation structure. Step 4 — Re-hash each Witness entry's content. For each WitnessEntry with a content_ref, fetch the bytes at that URI. (For content_inline, base64-decode.) Compute SHA-256 and compare to content_hash. Record verified and failed counts. A failed count above zero means at least one source artifact has been altered since the attestation was sealed. Step 5 — Resolve supports. For each Claim, verify that every entry in supports is either (a) an observation_id present in the Witness block, or (b) a claim_id defined earlier in the Findings list. A forward reference or a dangling reference is fail: unresolved support. This step confirms that the reasoning chain is grounded and acyclic (R3). Step 6 — Verify refutation targets. For each Challenge in the Refutation block, verify that every entry in targets is a claim_id present in the Findings block. A dangling target means a challenge was applied to a claim that does not exist in this attestation. fail: challenge targets unresolved. Step 7 — Assess the declined-challenges inventory. Retrieve refutation.coverage.declined. Count the entries and read the machine-readable reasons. This step is informational only — no pass/fail verdict is emitted. The recipient must decide: are the declines acceptable for the context? A counter_evidence challenge declined for no_negation_search_implemented is different from one declined for negation_search_found_no_conflicts. The first is a gap; the second is substantive coverage. That distinction is the recipient's judgment to make, not the protocol's. > § For the Record — Canon §8.3, the falsification protocol. > > "A Recipient in possession of an Attestation and ordinary network > access MUST be able to reach a definitive, binary verdict on the > Attestation's integrity and internal consistency by executing the > following steps without any cooperation from the Issuer or any > other party. Steps 1–6 are deterministic; each returns pass or > fail. Step 7 is informational; it surfaces the declined-challenge > inventory for the Recipient's substantive judgment." > — Meridian-Canon v0.2.0 §8.3. > ✻ Try This. The reference verifier ships as meridian-canon walk. > If you have cloned this repository and run pip install -e ".[test]",
you can walk any attestation JSON:
meridian-canon walk appendices/B_worked_attestation.jsonThe output is a JSON verdict dict with a
verdictkey (validor >invalid) and astepsobject showing each step's result. Run it > against the worked attestation in Appendix B. Then corrupt one byte > of thecontent_reftarget and run it again. Observe which step > catches the corruption and what the error message says. Try corrupting > thechain_hashfield directly. Then try corrupting thesignature. > Each corruption is caught by a different step, for a different reason. ## What Canon does not specify | Specified | Not specified | |---|---| | JSON shape of an Attestation. | Storage format after issuance. | | Required fields of each block. | Transport protocol. | | Hash algorithm (SHA-256) and canonicalization (RFC 8785). | User interfaces for walking the chain. | | Signature algorithm (Ed25519). | Internal implementation of any stage. | | Seven-step falsification protocol. | Which challenge types are domain-required. | | Inference-type vocabulary. | Choice of adversary models. | | Versioning discipline (semver). | Key-storage mechanisms (Keychain vs HSM vs file). | The unspecified column is where the design choices in Meridian-Cannon (and in your eventual capstone) live. Canon makes those choices interoperable; it does not make them for you. ## Versioning discipline Canon follows semver. The current version is v0.2.0; the schema URL embeds the major.minor pair. The version a recipient is verifying against appears in thecanon_versionfield of the attestation envelope, and is also embedded inseal.payload_type— the string"application/vnd.nora.canon.attestation+json; version=0.2.0". Versioning is conservative. R1–R9 are guaranteed not to weaken across minor versions. New attestation kinds, new inference types, new challenge types may be added in minor versions; existing fields are not removed except across major versions. A v0.2.0 verifier reading a v0.1.x attestation should treat the absent DSSE envelope gracefully: fall back to the v0.1.x verification path ifseal.payload_typeis absent. Across major versions, all bets are off. ## The four attestation kinds Canon itself does not require any particular kind of attestation. Meridian-Cannon emits four kinds at distinct layer boundaries plus an audit kind. Each kind is a full Canon attestation — four blocks, all nine requirements — distinguished by what the Witness and Findings blocks contain. ### ObservationAttestation (kind =observation) Emitted at L1, the moment an item enters the system. The Witness block contains the raw item — the iMessage extract, the email MIME blob, the audio file bytes, the PDF page. The Findings block contains a single observation-typed claim asserting the item's existence and hash. The Refutation block contains a singlereplaychallenge confirming hash stability: the extraction pipeline reruns and produces the same hash. Thecoverage.declinedblock lists all other challenge types with reasonnot_applicable_at_observation_layer. This is the root of the attestation graph. Every EnrichmentAttestation, SearchAttestation, and BriefAttestation traces its supports path back to one or more ObservationAttestations. A recipient who wants to verify the entire chain starts here. In the running case: when the iMessage extract for the March 14, 2024 visit-rescheduling message enters the system, the ingest worker emits an ObservationAttestation. Theobservation_idisobs-01JABC. That ID becomes the root node of every downstream claim about that message. ### SearchAttestation (kind =search) Emitted at L6 in response to a retrieval query. The Witness block lists the query text and theobservation_ids of the top-K retrieved items. The Findings block contains claims about the ranking — why these items ranked above others, what the retrieval scores were, what the embedding distance was. The Refutation block recordsconsistency_checkandcoverage_auditchallenges applied to the result set: are the results internally consistent? Does the query match the corpus slice that was searched? A SearchAttestation answers the question: when the attorney searched for "rescheduled visit March 2024," why did these ten messages come back? The answer is in the Findings and Refutation blocks, not in the attorney's memory of what they clicked. ### BriefAttestation (kind =brief) Emitted as a composite document, synthesizing multiple SearchAttestations and EnrichmentAttestations. A BriefAttestation is what happens when the system assembles a narrative: "here are the ten messages relevant to the visit rescheduling, in chronological order, with extraction claims and confidence levels." Its Witness block references the upstream attestations by theirattestation_ids. Its Findings block contains higher-order claims that aggregate across the upstream claims. Its Refutation block challenges the synthesis itself — are the higher-order claims consistent with the lower-order ones? A BriefAttestation cannot be stronger than its weakest supporting attestation. The supports-graph is acyclic and traces back to ObservationAttestations at the leaves. A recipient who walks the graph fully has verified the entire evidentiary chain. ### AuditAttestation (kind =audit) Produced by the Admissibility Auditor (Chapter 26). It is a Canon-conformant artifact that records the readiness of another attestation for evidentiary review. The Witness block references the target attestation by ID and includes a hash of it. The Findings block contains claims about the target's conformance: R1 satisfied, R2 satisfied, R6 gap analysis. The Refutation block applies the five Daubert factors as challenge types and records the outcomes. An AuditAttestation is the mechanized form of the expert disclosure a court asks for under FRE 702. It does not replace counsel's review; it structures it. The supports-graph traversal means the parent's attorney in case 2024JC000099 can start from a BriefAttestation — a narrative summary of ten relevant messages — and walk back, step by step, to the original bytes of each message. At no point does the attorney have to take the issuer's word for anything. The ObservationAttestation at the root of each path holds the content hash; the attorney reruns the hash against the original file and either matches or doesn't. This is what it means for verifiability to be an artifact property rather than an issuer property. ## The seven-step protocol as a table | Step | What it checks | Pass condition | Failure implication | |---|---|---|---| | 1 | Public key fetch + fingerprint | Fetched PEM hashes toseal.signatures[0].keyid| Key was substituted or hosting was tampered with | | 2 | DSSE signature over PAE |Ed25519.verify(sig, PAE(payload_type, canonical_bytes))passes | Payload, payload_type, or key was substituted | | 3 | Chain hash match |SHA-256(canonical_bytes)==seal.chain_hash| Payload was tampered with; seal replaced without re-signing | | 4 | Witness content hashes | Allcontent_refbytes hash tocontent_hash| One or more source artifacts were altered | | 5 | Supports resolution | Allsupportsentries resolve without forward or dangling refs | Reasoning chain is broken or fabricated | | 6 | Refutation targets | All challenge targets resolve to claims in Findings | Challenge was applied to non-existent claim | | 7 | Declined inventory | (Informational) | Recipient judgment: are the declines acceptable? | --- > ▼ Why It Matters — What the seven steps prove and what they don't. > > Passing all seven steps establishes one thing precisely: the attestation's bytes have not been altered since the private key signed them. It establishes nothing about what happened before the key was applied. Specifically, the seven steps do not prove that: (1) the key belongs to a person with authority to acquire the underlying data; (2) thereceived_attimestamp accurately reflects when the source was actually acquired; (3) the source bytes genuinely came from the claimed device or account; or (4) no fabrication occurred before hashing. Authentication under FRE 901 requires a foundation — testimony or certification establishing these facts — that the seven-step protocol does not provide by itself. The protocol is a necessary condition for relying on the attestation; it is not a sufficient condition for admissibility. Independent corroboration of the acquisition circumstances is still required. --- The table is not just a summary. It is the decision matrix a forensic expert uses when testifying about a Canon attestation in court. For each step, the expert can point to a specific computation with a specific binary result. That is the specification's contribution to the Daubert gatekeeping analysis: the methodology is testable, described, and deterministic. ## Reading the spec yourself Two artifacts you should read in their entirety before continuing: 1.canon.schema.jsonv0.2.0 — the JSON Schema that R1 enforces. It is short. Reading it once gives you a map of the entire data model, including the DSSE envelope fields. 2. The Meridian-Canon v0.2.0 specification, §4 (Conformance Requirements) and §10 (Cryptographic Protocol). Sections 4 and 10 are the formal core. Everything else is commentary. The rest of this book is engineering commentary on the spec. The spec is the authority. Where this book is unclear or wrong, the spec governs. > § For the Record — Canon's purpose statement. > > "Canon is published as a source-available, implementation-neutral > standard under the NORA Canon Evaluation & Commentary License v1.0. > Its stated purpose is to define the structure and protocol of NORA > Attestations — artifacts produced when a claim has passed through the > Witness → Findings → Refutation → Seal chain with its evidentiary > record intact. Canon specifies the what (artifact shape, protocol, > hash algorithm, signature algorithm, DSSE signing envelope); it does > not specify the how (internal implementation of the four stages, > which challenge types must be applied in any given domain, > user-interface affordances)." > — Meridian-Canon v0.2.0 §1. ## How Canon compares to its neighbors The full comparative table lives in Appendix E. The headline: | System | What it signs | What it adds beyond signing | |---|---|---| | Sigstore Rekor | Software artifacts | Identity-bound short-lived certs; transparency log. | | in-toto / SLSA | Build provenance | Predicate vocabulary for build steps; policy via Rego. | | C2PA Content Credentials | Media files | Capture-device assertions; edit chain; embedded JUMBF. | | W3C VC 2.0 | Issued credentials | Selective disclosure; revocation lists; DID-anchored issuers. | | Canon | Reasoning over personal-data evidence | Inference types; declared gaps; applied-and-declined challenges with reasons. | The element none of the others have is the declined-challenges inventory. Sigstore tells you a binary was signed; in-toto tells you which build steps it passed; C2PA tells you what edits were applied; VC tells you what an issuer asserts. Canon tells you all of the above, and what the issuer tried to test, and what they couldn't or didn't, and why. That last column is the surface a court reviews when admissibility is contested. It is the surface this book is in service of. ## Worked example, walked Returning to the running case: the parent's attorney needs the March 14, 2024 iMessage rescheduling the supervised visit. The system has emitted an attestation. Walk it block by block. - Witness. One observation: the iMessage with hashf4c9...a2b3, retrievable at the file URL. Thesourceisimessage://chat.db/handle:+17155559876/.... The custody chain records thatjpwhitereceived the bytes at 2026-04-30T18:00:00Z. R2 satisfied. - Findings. One claim: the message rescheduled the visit. Inference typededuction. Supports[obs-01JABC...]resolves to the Witness entry. Gaps array:"Sender identity verified only by phone number; could be a pool number reassigned."R3, R4, R5 satisfied. - Refutation. One challenge:adversarial_prompt, target[claim-01], outcomesurvivedafter Tri-Model Consensus (M1: survived, M2: survived, M3: revised; consensus: survived because two of three agree). Coverage applied:[adversarial_prompt, consistency_check, replay]. Coverage declined:counter_evidence("no_negation_search_implemented_for_imessage_extractor"),coverage_audit("deferred_to_batch_pass"). R6 satisfied. - Seal. DSSE envelope:payload_type="application/vnd.nora.canon.attestation+json; version=0.2.0",payload(base64url JCS bytes),signatures[{keyid, sig, public_key_url}],chain_hash. R7, R8 satisfied. Optionally R10 (Rekor inclusion proof). - Falsifiability. The attorney's paralegal runsmeridian-canon walk attestation.json. Steps 1–6 all returnpass. Step 7 surfaces two declined challenges. The attorney reviews them:counter_evidencedeclined is a gap — no negation search ran. The attorney can note this in the Admissibility Auditor report and disclose it to the court. R9 satisfied. The artifact in front of the attorney is not just an evidence record. It is an evidence record plus the falsification harness for itself, plus the disclosure of what the harness did not do. The four blocks together transform the question a recipient asks. Without Canon: can I trust this issuer? With Canon: can I run the seven steps without trusting them? The first question has no structural answer. The second has a deterministic one. The cryptography, the schema, the inference typing, the gap discipline, the declined-coverage discipline — all of it serves that move.## Exercises ### Warm-up 1. Take the attestation in💡Key Takeaways- The seven-step verification protocol (fetch key → verify DSSE signature → verify chain hash → re-hash Witness content → resolve Claim supports → resolve Challenge targets → assess declined inventory) lets any recipient reach a binary verdict on steps 1–6 without the issuer's cooperation. - The DSSE envelope contains four load-bearing fields —payload_type,payload,signatures, andchain_hash— wherepayload_typeis itself a falsifiable claim because substituting it invalidates the Ed25519 signature over the PAE bytes. -chain_hashis the SHA-256 of the JCS-canonical attestation bytes (stored as a convenience field in the DSSE envelope), while the actual Ed25519 signature is overPAE(payload_type, canonical_bytes)— confusing the two breaks verification. - Requirements R1–R8 are mechanical (structural validity, content integrity, supports closure, inference typing, gap disclosure, refutation completeness, DSSE sealing, key discoverability); R9 is the master requirement — any change to R1–R8 that breaks independent verification is non-conformant. - A receiver verifies without trusting the custodian because the public key is at a stable URL committed inside the signed payload, the canonical bytes are in the envelope, and every reasoning step traces to a hashed source — the issuer is never in the verification loop.appendices/B_worked_attestation.md(or the one in Chapter 1 if Appendix B isn't yet drafted) and identify, for each of R1 through R9, the field or property of the artifact that satisfies that requirement. 2. Why iscanonicalization: "rfc8785"declared inside the seal? What kind of attack does its absence enable? Name the XML-DSig vulnerability this mirrors. 3. The seven-step protocol ends at step 7 with an informational result, not a pass/fail. Why? What would go wrong if the protocol tried to enforce an acceptable set of declines automatically? ### Core 4. Construct a minimal attestation — one Witness entry, one observation-type claim with empty gaps, one replay challenge, full coverage block, valid seal structure (no cryptographic fields yet). Write it as JSON. Verify it satisfies R1 through R6 structurally. (You will sign one in Lab 6.) 5. The spec's R5 says non-observational claims must have at least one gap. Construct three example gap entries that would not satisfy the spirit of R5 even though they are syntactically valid (they are evasions or boilerplate). Then construct three gap entries that would. What is the distinguishing criterion? 6. Walk the chain hash construction by hand for a three-field JSON object{"a": 1, "b": 2, "c": 3}using RFC 8785 ordering rules. What is the RFC 8785 canonical form of this object? How would you compute the chain hash ifcwere the "seal" field? ### Stretch 7. Suppose Canon v0.3.0 added a new challenge type,explainability_audit. What would have to change in your implementation? In a v0.2.0 verifier consuming a v0.3.0 attestation that includes anexplainability_auditchallenge, what should happen? What would a conformant v0.2.0 verifier do with the unknown challenge type? 8. Readmeridian/canon/schema.pyin this repo. Compare the Pydantic models there to the JSON Schema you would expect from the spec text. For each of R3 (supports closure) and R5 (gap disclosure), trace exactly where the enforcement happens: in the Pydantic model validator, inemit.py, or in the seven-step walker? What is the consequence of each enforcement point being where it is? 9. A BriefAttestation has supports that include three SearchAttestations, each of which has supports pointing to ObservationAttestations. Sketch the full acyclic directed graph. How many total attestations does a complete verification require walking? For a corpus of 10,000 ObservationAttestations, what is the practical limit on how many a BriefAttestation can include before the verification is no longer "ordinary network access"? ## Build-your-own prompt Sketch the four blocks of one attestation your capstone system would emit for a single document in your corpus. You do not have to fill in the cryptographic fields; just state what each block contains for one specific output of your system. Then: state which of the five challenge types (replay, adversarial_ prompt, consistency_check, coverage_audit, counter_evidence) your current implementation can apply, and which it must decline. Write adeclinedentry — with a machine-readable reason — for each type you cannot apply. This declined inventory is the first draft of your Admissibility Profile, which you will refine at Chapter 26. ## Further reading - The Canon spec v0.2.0, §4 (Canon Conformance Requirements) and §10 (Cryptographic Protocol). Read both in full. -canon.schema.jsonv0.2.0 (the schema R1 enforces). -meridian/canon/schema.pyin this repository — the Pydantic implementation of the schema. -meridian/canon/walk.py— the reference implementation of the seven-step falsification protocol. -meridian/canon/signing.py—sign_dsse,verify_dsse,CANON_PAYLOAD_TYPE; the DSSE PAE construction. -meridian/canon/emit.py— the build → canonicalize → hash → sign pipeline; the source of truth for R7 enforcement. - DSSE specification (github.com/secure-systems-lab/dsse) — the envelope format Canon v0.2.0 adopts for R7. - Rekor/Sigstore documentation (docs.sigstore.dev) — the transparency log Canon v0.2.0 supports as optional R10. - Appendix B of Meridian-Canon-Revised (Example Attestation, color-coded by block). - Appendix E of this textbook (Comparative architectures: Sigstore, in-toto, C2PA, W3C VC vs Canon). - RFC 8785 (JSON Canonicalization Scheme). Read §3 (the algorithm). It is 12 pages. - RFC 8410 (Ed25519 keys in PEM). Needed for R8 key hosting. - The dossiersresearch/01_cryptography_pedagogy.mdandresearch/02_verifiable_evidence_ecosystem.mdfor context on the choices Canon makes and the alternatives it doesn't.
Next: Chapter 5 — Cryptographic Hashing. Where the engineering begins.