Village
Te reo Māori on this page is a best-effort, machine-assisted translation, not yet reviewed by tangata whenua. We welcome corrections.
Provenance

The Provenance bundle format

A dossier is the file a buyer checks. This page sets out what is in it and how each check is made, so you can read what the checker does, and test another program against the same examples. The checker's fingerprint is here too, so you can see that the copy in your dossier is the one published.

Copyright 2026 My Digital Sovereignty Ltd. All rights reserved. This specification, its test vectors and the checker are published so that anyone may read them and see what the checker checks. Publication is not a licence: no licence to them, or to any other software of My Digital Sovereignty Ltd, is given by publishing them. The terms on which you may run the checker are those of the order under which your dossier was made; anyone holding or running a copy on no such terms is responsible for the terms under which they hold and run it.

The technical specification below is in English.

1. Conventions

Every hash is SHA-256, written as 64 lowercase hex characters. || is byte concatenation. Byte values are written 0x00. A DID is a decentralised identifier, a name a party chose for its key. A record's receipt hash is SHA-256(canonical bytes || 0x00 || the 64 signature bytes). Every limit below is applied before the work it limits. Any text taken from a record or from the file (a provider name, a declared role, a field name) is shown with control and direction characters written as <U+XXXX>, so it can never start a line of the report or pass itself off as a result. The escaped set includes line breaks, direction and zero-width marks, the combining grapheme joiner, variation selectors, Hangul fillers, the braille blank, Mongolian variation selectors and tag characters; the whole result is escaped where it is made, so the page, the printed report, the command line and its --json output carry none of them raw. A literal "<U+" already in the file text is shown as "<<U+", so an escape cannot be forged by typing one.

2. The dossier file

A dossier is delivered as a zip holding README.txt (five languages, one after another), dossier.json, did/<SHA-256 of the DID>.json for each key document, tokens/<digest>.tsr when the file carries time-stamp tokens, verify.html and verify.js (the check page, which runs from the folder with no network), provenance-verifier.js (the checker, byte for byte the published file) and provenance-verifier.js.sha256. A segment pack that had to be cut to fit the size limit adds pack_omissions.json ({ "tokens_omitted": true, "upstream_omitted": [DID...] }, each only when it applies); it is not part of dossier.json.

dossier.json is one JSON object, at most 25 MB and 20000 records, nested at most 64 levels, with no duplicate field name anywhere. A field not listed here is unreadable.

Top level

FieldWhat it holds
dossier_versionWhole number. 1. Any other value is unknown_version.
dossier_familyThe string provenance. Anything else is unreadable (an Email Decision Record dossier is checked on its own page).
subject_lot_refThe lot_ref (64 hex) this dossier is about. It must have records of its own in the file.
assembled_atWhen the file was put together (ISO 8601). Not signed and not read by the check.
segmentsList: one segment per party, each holding that party's records (see "A party segment").
did_documentsList of key documents, one per party DID: id (the DID), verificationMethod (at most 16 entries of { id, type, publicKeyHex }; only the 32-byte Ed25519 key in publicKeyHex is checked) and optionally key_history (entries of { key_fingerprint_sha256, revoked_at? }). Two different documents for one DID are unreadable; an identical copy changes nothing.
estate_rootsList. Carried for a later version; this checker does not yet check them and says so.
disclosuresObject with up to three maps (see "Disclosures"). Optional.
lot_graphThe builder's statement of the lots and handovers in the file: { lots: [lot_ref...], handovers: [{ handover_id, lot_ref, from, to }...], derivations? }. The check recomputes it from the signed records; a difference is changed. Optional.
tokensObject mapping the SHA-256 (64 hex) of each RFC 3161 time-stamp token to the token's DER bytes in base64. A key that is not the hash of its token voids every token in the file.
corroborationObject: the builder's statement of how far other parties corroborate each point of the subject lot. Unsigned; the check works it out again from the signed records and prints its own; a difference is shown, never trusted.
policy_snapshotsObject mapping a party DID to that party's policy as the builder copied it. Unsigned; shown as unsigned and not compared with the records by this checker.
pack_omissions{ tokens_omitted?: true, upstream_omitted?: [DID...] }: what an earlier party left out of the records it passed on, to keep within the size limit. Each makes the result incomplete.
plot_openingsObject mapping a plot commitment to { geometry_sha256, geometry, salt }: the boundary of a plot of land its owner chose to disclose, in decision 204's form. Unsigned; each is checked against the commitment a shown record signed (a mismatch is changed; one for a commitment no shown record carries is listed and not read). At most 256. A segment then also shows the party's disclosure records naming an opened commitment. A checker older than this field refuses it as unreadable; a dossier without openings is unchanged, and every dossier carries its own copy of the checker beside it.

A party segment

FieldWhat it holds
tenant_didThe party's DID. Every number, day root, proof, chain and key fingerprint is looked up under this DID only.
declared_roleText such as grower, as the builder states it. Not signed; shown only as unsigned text.
sequence_disclosurecomplete_range (every record in the party's day roots shown in the file) or selective. Not signed. It can only make a result stricter: under complete_range a missing number is gap_in_sequence. A shortfall in the party leaf (see "Lot commitments") against the count its party signed is incomplete in every segment, whatever the label (party_records_missing): the platform's file shows every standing authorisation, key record, authority confirmation and attestation, so a missing one may have been removed. A file in which a party has issued attestations cannot yet be verified, since the builder does not yet deliver them. A shortfall in the omittable party leaf, under a segment labelled exactly selective, is one informational line, party_records_not_shown: erasure or disclosure records the giver chose not to show, counted from the signed leaf, which cannot be checked from the file; only those numbers are excused from a gap, and nothing is said about completeness. Under complete_range or no label the same shortfall is incomplete or a gap. Whenever any number of a party's range is missing from the file, every record shown needs its own inclusion proof. A checker older than this specification reads a day root whose range holds an erasure or disclosure record, shown in full, as changed; this checker reads a file built before the split as incomplete or changed. Every dossier carries its own copy of the checker.
receiptsList of record entries (see "A record entry"): the party's records of the lots in this file.
day_rootsList of record entries holding the party's day roots (kind provenance_day_root) that cover the records shown.
proofsObject mapping a record's receipt_id to its inclusion proof in its day root: a list of { hash, side }, side "L" or "R", at most 64 steps.
handover_termsObject mapping a handover_id to the terms object both parties signed the digest of (see "Handover terms").
lot_commitsList of lot commitment entries (see "Lot commitments").
lot_commit_unprovenList of { day_root_receipt_id, lot_ref, reason } for pairs the builder could not show whole. Any entry makes the result incomplete.
lot_absencesList of lot absence entries (see "Lot absences"). Present only when not empty.

A record entry

FieldWhat it holds
canonical_bytes_base64The record's canonical bytes (see "Canonical JSON"), standard base64 with padding and nothing else. These are the bytes that were signed.
signature_hexThe Ed25519 signature over those bytes: 64 bytes as 128 hex characters.
json_copyA readable copy of the record. It must equal the record the signed bytes decode to; a copy that differs is changed and is never read in place of the signed bytes.
evidence_strengthOptional, not on a day root: { attestation_source, signing_model, text }, the builder's evidence-strength label. Unsigned. The check works the label out from the signed record: a source or signing model that differs is changed; text that differs is noted and the page prints its own.

Handover terms

handover_terms_sha256 in each handover record is SHA-256(canonical JSON of the terms): { handover_id, lot_ref, giver_did, receiver_did, giver_last_receipt_sha256, product_class, hs_code?, quantity_commitment_sha256?, evidence_refs, proposed_at, expires_at }. The platform sets expires_at between 1 hour and 30 days after proposed_at; the checker does not enforce that range. It recomputes the digest from the terms in the segment and compares expires_at with the time of the reply: a reply after expiry is handover_unmatched. Where the terms are not in the file, it says the expiry was not compared.

Disclosures

MapWhat it holds
lot_saltsMap from lot_ref to { genesis_tenant_did, lot_salt }. The check recomputes lot_ref = SHA-256(canonical JSON of that object).
evidence_saltsMap from an evidence digest to its 32-byte salt (64 hex). A short value read off an object (a seal, certificate or container number) is recorded as SHA-256(UTF-8 of salt hex || ":" || the value with spaces and hyphens removed, upper-cased); the reader types the value and the check recomputes it.
quantity_openingsMap from a quantity_commitment_sha256 to { value, unit, salt }; the check recomputes SHA-256(canonical JSON of that object).

A salt or opening that is absent is listed as not provided and does not lower the headline.

3. Records

Every record carries the common fields, then the fields of its kind. Receipt version: 1. Kinds this checker reads: provenance_step, provenance_handover, provenance_day_root, provenance_mandate, provenance_key_event, provenance_attestation, provenance_tsa_confirmation, provenance_erasure_event, provenance_disclosure_event, provenance_archive_root, provenance_attestation_receipt; a record of another kind is not_understood. One record's canonical bytes may not exceed 16 KiB. No field is free text: every string is a value from a closed list, a digest, a time, a date, a DID or a UUID, so no name, address, note or coordinate can be carried.

Common fields

FieldFormPresence
receipt_idlowercase UUIDrequired
receipt_kindone of a closed list: the kindrequired
receipt_versionwhole number, 1 or morerequired
tenant_dida DID, did:<method>:<identifier> (identifier at most 200 characters)required
signer_didthe party's DID followed by #<key id>required
algorithmone of a closed list: Ed25519required
decided_atISO 8601 UTC with millisecondsrequired
attestation_authorityone of a closed list: platformrequired
time_attestationobjectrequired
tenant_seqwhole number, 1 or moreon every kind but the day root

time_attestation is { grade, unavailable_reason?, authorities: [...], quorum }: grade tsa_rfc3161 with at least one authority entry of status obtained carrying token_digest (the SHA-256 of a token in tokens), or grade operator_attested with an unavailable_reason and no obtained entry. An entry is { provider_id, status, token_digest?, name?, jurisdiction?, imprint_algorithm?, generation_time?, unavailable_reason?, qualified_status, trusted_list_ref? }: qualified_status is the platform's own statement when it asked (qualified_per_trusted_list, not_shown_on_trusted_list or not_assessed), with trusted_list_ref { jurisdiction, sequence, issued } exactly when it says qualified_per_trusted_list; the check never takes it as the label (see "Time stamps"). quorum is { required, configured, obtained, met }: required is 2 on a day root and an archive root and 1 on every other kind, obtained counts the obtained entries, configured counts every authority asked, and met is the string "true" exactly when obtained is at least required. The token's imprint is the anchored digest: SHA-256 of the canonical JSON of the record without signature and without time_attestation.

provenance_step

FieldFormPresence
signing_modelone of a closed list: village_custodied, threshold_hybrid, participant_heldrequired
captured_atISO 8601 UTC with millisecondsrequired
capture_modeone of a closed list: online, queuedrequired
profile_idone of a closed list: generic_v1, honey_nz_v1, beef_nz_v1, dairy_nz_v1, kiwifruit_nz_v1, nz_mpi_animal_products_v1, nz_mpi_plants_v1optional
mandate_ref_sha25664 lowercase hex characters (a SHA-256)by the rules below
lot_ref64 lowercase hex characters (a SHA-256)required
step_typeone of a closed list: harvest, receive, process, pack, inspect, transport_leg, export_declaration, import_clearance, deliveryrequired
occurred_atISO 8601 UTC with millisecondsrequired
product_classone of a closed list: dairy, meat, wine, honey, seafood, horticulture, otherrequired
evidence_refs0 to 16 evidence entriesrequired
attestation_sourceone of a closed list: self, device, third_partyrequired
hs_codesix-digit tariff subheadingoptional
quantity_commitment_sha25664 lowercase hex characters (a SHA-256)optional
plot_commitment_sha25664 lowercase hex characters (a SHA-256)optional
location_classlocation class objectby the rules below
transport_modeone of a closed list: road, rail, sea, air, inland_water, multimodalby the rules below
from_classlocation class objectby the rules below
to_classlocation class objectby the rules below
input_lot_refs1 to 32 lot_refs, sorted and distinctby the rules below
supersedes_receipt_sha25664 lowercase hex characters (a SHA-256)by the rules below
correction_reasonone of a closed list: data_entry_error, evidence_replaced, wrong_lot, wrong_time, withdrawn_by_actorby the rules below
prev_receipt_sha25664 lowercase hex characters (a SHA-256)by the rules below

provenance_handover

FieldFormPresence
signing_modelone of a closed list: village_custodied, threshold_hybrid, participant_heldrequired
captured_atISO 8601 UTC with millisecondsrequired
capture_modeone of a closed list: online, queuedrequired
profile_idone of a closed list: generic_v1, honey_nz_v1, beef_nz_v1, dairy_nz_v1, kiwifruit_nz_v1, nz_mpi_animal_products_v1, nz_mpi_plants_v1optional
mandate_ref_sha25664 lowercase hex characters (a SHA-256)by the rules below
handover_idlowercase UUIDrequired
handover_roleone of a closed listrequired
handover_statusone of a closed listrequired
lot_ref64 lowercase hex characters (a SHA-256)required
counterparty_dida DID, did:<method>:<identifier> (identifier at most 200 characters)required
handover_terms_sha25664 lowercase hex characters (a SHA-256)required
giver_proposal_sha25664 lowercase hex characters (a SHA-256)by the rules below
receiver_reply_sha25664 lowercase hex characters (a SHA-256)by the rules below
reason_codeone of a closed listby the rules below
prev_receipt_sha25664 lowercase hex characters (a SHA-256)by the rules below

provenance_day_root

FieldFormPresence
datedate YYYY-MM-DDrequired
root64 lowercase hex characters (a SHA-256)required
previous_root64 lowercase hex characters (a SHA-256)required
previous_root_record_sha25664 lowercase hex characters (a SHA-256)required
first_tenant_seqwhole number, 1 or morerequired
last_tenant_seqwhole number, 0 or morerequired
countwhole number, 0 or morerequired
lot_commit_root64 lowercase hex characters (a SHA-256)required
lot_commit_leaveswhole number, 0 or morerequired

provenance_mandate

FieldFormPresence
signing_modelone of a closed list: village_custodied, threshold_hybrid, participant_heldrequired
captured_atISO 8601 UTC with millisecondsrequired
capture_modeone of a closed list: online, queuedrequired
profile_idone of a closed list: generic_v1, honey_nz_v1, beef_nz_v1, dairy_nz_v1, kiwifruit_nz_v1, nz_mpi_animal_products_v1, nz_mpi_plants_v1optional
mandate_ref_sha25664 lowercase hex characters (a SHA-256)by the rules below
mandate_idlowercase UUIDrequired
custody_modelone of a closed list: village_custodied, threshold_hybridrequired
key_ida key id (letters, digits, dot, underscore, hyphen; at most 64)required
scopeone or more scopes, sorted and distinct: archive_root, attestation, day_root, disclosure_event, erasure_event, handover, key_event, step, tsa_confirmationrequired
grantor_roleone of a closed list: ownerrequired
grantor_authone of a closed list: passkey, totp, passwordrequired
valid_fromISO 8601 UTC with millisecondsrequired
mandate_statusone of a closed list: granted, withdrawnrequired
valid_untilISO 8601 UTC with millisecondsby the rules below
acknowledgementsthe three acknowledgements, in order: erasure_limits, legal_framing, sealed_permanenceby the rules below
privacy_note_versionwhole number, 1 or moreby the rules below
supersedes_mandate_sha25664 lowercase hex characters (a SHA-256)by the rules below

provenance_key_event

FieldFormPresence
signing_modelone of a closed list: village_custodied, threshold_hybrid, participant_heldrequired
captured_atISO 8601 UTC with millisecondsrequired
capture_modeone of a closed list: online, queuedrequired
profile_idone of a closed list: generic_v1, honey_nz_v1, beef_nz_v1, dairy_nz_v1, kiwifruit_nz_v1, nz_mpi_animal_products_v1, nz_mpi_plants_v1optional
mandate_ref_sha25664 lowercase hex characters (a SHA-256)by the rules below
eventone of a closed list: rotated, withdrawn, compromisedrequired
key_ida key id (letters, digits, dot, underscore, hyphen; at most 64)required
effective_atISO 8601 UTC with millisecondsrequired
successor_key_ida key id (letters, digits, dot, underscore, hyphen; at most 64)by the rules below

provenance_attestation

FieldFormPresence
signing_modelone of a closed list: village_custodied, threshold_hybrid, participant_heldrequired
captured_atISO 8601 UTC with millisecondsrequired
capture_modeone of a closed list: online, queuedrequired
profile_idone of a closed list: generic_v1, honey_nz_v1, beef_nz_v1, dairy_nz_v1, kiwifruit_nz_v1, nz_mpi_animal_products_v1, nz_mpi_plants_v1optional
mandate_ref_sha25664 lowercase hex characters (a SHA-256)by the rules below
lot_ref64 lowercase hex characters (a SHA-256)required
attestation_kindone of a closed list: inspection, certification, auditrequired
pointone of a closed list: origin, handling, quantity, inspectionrequired
outcomeone of a closed list: pass, fail, observedrequired
evidence_refs0 to 16 evidence entriesrequired
occurred_atISO 8601 UTC with millisecondsrequired
location_classlocation class objectrequired
attestation_sourceone of a closed list: self, device, third_partyrequired
attested_receipt_sha25664 lowercase hex characters (a SHA-256)optional
scheme_ref_sha25664 lowercase hex characters (a SHA-256)optional

provenance_tsa_confirmation

FieldFormPresence
signing_modelone of a closed list: village_custodied, threshold_hybrid, participant_heldrequired
captured_atISO 8601 UTC with millisecondsrequired
capture_modeone of a closed list: online, queuedrequired
profile_idone of a closed list: generic_v1, honey_nz_v1, beef_nz_v1, dairy_nz_v1, kiwifruit_nz_v1, nz_mpi_animal_products_v1, nz_mpi_plants_v1optional
mandate_ref_sha25664 lowercase hex characters (a SHA-256)by the rules below
authority_idone of a closed list: certum, aruba, nz_tsa, pt_staterequired
tsu_anchor_sha25664 lowercase hex characters (a SHA-256)required
trusted_list_jurisdictionone of a closed list: AT, BE, BG, CY, CZ, DE, DK, EE, ES, FI, FR, GR, HR, HU, IE, IS, IT, LI, LT, LU, LV, MT, NL, NO, PL, PT, RO, SE, SI, SKrequired
trusted_list_sequencewhole number, 1 or morerequired
trusted_list_issueddate YYYY-MM-DDrequired
statusone of a closed list: confirmed_qualified, confirmed_not_qualified, withdrawnrequired
confirmed_atISO 8601 UTC with millisecondsrequired
confirmer_roleone of a closed list: ownerrequired
supersedes_receipt_sha25664 lowercase hex characters (a SHA-256)by the rules below

provenance_erasure_event

FieldFormPresence
signing_modelone of a closed list: village_custodied, threshold_hybrid, participant_heldrequired
captured_atISO 8601 UTC with millisecondsrequired
capture_modeone of a closed list: online, queuedrequired
profile_idone of a closed list: generic_v1, honey_nz_v1, beef_nz_v1, dairy_nz_v1, kiwifruit_nz_v1, nz_mpi_animal_products_v1, nz_mpi_plants_v1optional
mandate_ref_sha25664 lowercase hex characters (a SHA-256)by the rules below
erased_classone of a closed list: counterparty_label, lot_label, lot_salt, evidence_salt, quantity_opening, plot_geometry, mandate_assertion, allrequired
commitment_sha256_list0 to 256 digests, sorted and distinctrequired
occurred_atISO 8601 UTC with millisecondsrequired
basisone of a closed list: request, leaving, retention_endedrequired

provenance_disclosure_event

FieldFormPresence
signing_modelone of a closed list: village_custodied, threshold_hybrid, participant_heldrequired
captured_atISO 8601 UTC with millisecondsrequired
capture_modeone of a closed list: online, queuedrequired
profile_idone of a closed list: generic_v1, honey_nz_v1, beef_nz_v1, dairy_nz_v1, kiwifruit_nz_v1, nz_mpi_animal_products_v1, nz_mpi_plants_v1optional
mandate_ref_sha25664 lowercase hex characters (a SHA-256)by the rules below
disclosed_classesone or more classes, sorted and distinct: evidence_salt, lot_salt, plot_geometry, quantity_openingrequired
commitment_sha256_list0 to 256 digests, sorted and distinctrequired
recipient_classone of a closed list: buyer, regulator, auditor, certifier, otherrequired
occurred_atISO 8601 UTC with millisecondsrequired

provenance_archive_root

FieldFormPresence
covers_from_datedate YYYY-MM-DDrequired
covers_to_datedate YYYY-MM-DDrequired
day_root_hashes_root64 lowercase hex characters (a SHA-256)required
previous_archive_root_sha25664 lowercase hex characters (a SHA-256)required
hash_algorithmone of a closed list: sha256required

provenance_attestation_receipt

FieldFormPresence
signing_modelone of a closed list: village_custodied, threshold_hybrid, participant_heldrequired
captured_atISO 8601 UTC with millisecondsrequired
capture_modeone of a closed list: online, queuedrequired
profile_idone of a closed list: generic_v1, honey_nz_v1, beef_nz_v1, dairy_nz_v1, kiwifruit_nz_v1, nz_mpi_animal_products_v1, nz_mpi_plants_v1optional
mandate_ref_sha25664 lowercase hex characters (a SHA-256)by the rules below
lot_ref64 lowercase hex characters (a SHA-256)required
attester_dida DID, did:<method>:<identifier> (identifier at most 200 characters)required
attestation_receipt_sha25664 lowercase hex characters (a SHA-256)required
delivery_idlowercase UUIDrequired

The rules a step follows

  • Required: lot_ref, step_type, occurred_at, product_class, evidence_refs (0 to 16 entries).
  • location_class is required on every step type except transport_leg, which carries transport_mode, from_class and to_class instead.
  • input_lot_refs (1 to 32, sorted, distinct) appears only on a process step that begins a new lot.
  • supersedes_receipt_sha256 and correction_reason come as a pair: a correction names the step it replaces; the earlier record stays.
  • prev_receipt_sha256 is absent on the first record of a lot in a party's chain and required on every later one.
  • A location class is { country, region?, facility_type, establishment_ref_sha256? }; facility_type is one of farm, orchard, vineyard, apiary, vessel, aquaculture_site, processing_plant, packhouse, cool_store, winery, warehouse, port, airport, border_inspection_post, distribution_centre, retail_site, other.
  • An evidence entry is { evidence_kind, sha256, salted }; evidence_kind is one of document, lab_result, certificate, seal, photo, sensor_log, weighbridge_ticket, other; salted is "true" or "false".

The rules a handover follows

Role / statusgiver_proposal_sha256receiver_reply_sha256reason_code
giver / proposedabsentabsentabsent
giver / withdrawnrequiredabsentexpired, withdrawn_by_giver, declined_by_receiver
giver / completedrequiredrequiredabsent
receiver / acceptedrequiredabsentabsent
receiver / declinedrequiredabsentnot_expected, quantity_mismatch, evidence_mismatch, damaged, other

A receiver's acceptance begins its chain for the lot and carries no prev_receipt_sha256 unless it takes back a lot it held before. A handover is matched across parties by handover_id and the terms digest; the receiver's lot_ref must equal the giver's.

The chain rules

Per party and lot, a chain begins with exactly one of: a harvest; a process step with input_lot_refs; a receiver's acceptance. Then: receive at most once and first after an acceptance; process, pack, inspect and transport_leg any number of times; export_declaration at most once, after which only inspect, transport_leg, corrections and a proposal; import_clearance at most once and only in a chain begun by an acceptance; delivery closes the chain (corrections only after it); harvest never after the start. A proposal moves the lot to pending, where only inspect, corrections, a withdrawal (back to open) and a completion (handed over) follow. The check re-applies every rule and names the one broken. Two parties each beginning the same lot reference with no handover between them is incomplete (lot_claimed_by_two_parties). A damaged or unreadable entry never explains away a gap: both are reported. A correction inherits the derivation inputs of the step it supersedes, following the chain of corrections back to the original, so correcting a derivation step keeps the lot derived and its mass balance checked.

4. Canonical JSON

  • UTF-8, no whitespace outside strings.
  • Object keys sorted by UTF-16 code unit, compared as unsigned 16-bit numbers; duplicate keys are refused by the parser before canonicalisation.
  • Strings: " and \ escaped; U+0008, U+0009, U+000A, U+000C and U+000D as \b, \t, \n, \f, \r; every other code point below U+0020 as \u followed by four lowercase hex digits; everything else written as itself. A lone surrogate is refused.
  • Numbers: integers only, from -(2^53 - 1) to 2^53 - 1, written in decimal with no sign on zero, no leading zero, no exponent.
  • No null and no boolean: a field is present or absent; a flag is the string "true" or "false".
  • At most 64 levels of nesting.

The test vectors give inputs, their canonical text and its SHA-256, and inputs that must be refused.

5. Hashes and domain bytes

Every tree hash starts with one byte saying what it is, so a value of one kind can never be read as another.

ByteUsed forInput after the byte
0x00day-tree leaf; also the separator in the receipt hashthe 32-byte receipt hash (in the receipt hash: canonical bytes || 0x00 || signature)
0x01node, in the day tree and in the lot treeleft (32 bytes) || right (32 bytes)
0x02lot leafsortkey (32 bytes) || inner (32 bytes): 65 bytes in
0x03lot sortkeylot_ref (32 bytes) || the day root's date, 10 ASCII bytes "YYYY-MM-DD": 43 bytes in
0x04lot innercount (4 bytes, unsigned big-endian) || lot_subroot (32 bytes): 37 bytes in

6. Signatures and keys

  • The signed bytes are the canonical JSON of the record including time_attestation and without signature; these are canonical_bytes_base64.
  • The signature is Ed25519 (RFC 8032) over those bytes, checked against the 32-byte key in the party's key document. The checker uses the browser's or Node's Ed25519, and its own implementation where that is missing; both refuse a non-canonical point or an out-of-range scalar.
  • A key fingerprint is SHA-256 of the 32 key bytes. A reader who gives a party's fingerprint pins that party's key; without one the result is key_not_pinned for that party.
  • A reader pins each party by the fingerprints that party gave them. A party that signs with more than one key, such as a threshold_hybrid party (its split key for the co-signed kinds, the Village key for the rest), is pinned by all of them. The checker takes a list, or one string with the fingerprints separated by commas. Every record's signing key must be one of the fingerprints given, and every fingerprint given must be a key in that party's key document or key history; otherwise the result is key_mismatch. Which key a record's signing model may use is held by the standing authorisation the record names, which states the key.
  • A key is withdrawn by a key_history entry with revoked_at, or by the reader listing its fingerprint. A record whose anchored time is after the withdrawal, or that has no outside time, is unanchored_under_withdrawn_key.
  • A key document may list at most 16 keys. Only the key the record names in its signer_did (the part after #, matched to that key's id in the party's key document) can verify it: a record whose signature checks under another of the party's keys, or that names a key the document does not hold, is changed, whatever is pinned.
  • A reader may give at most 16 fingerprints for one party; any given beyond 16 are not read, and the result names each one dropped. A fingerprint given for a DID that is not a party in the file, or bound with a DID not spelled exactly as the party's (for example with # written as %23), is named on its own line as unused (pin_unused) and changes no verdict. A fingerprint in a party's key history that no record of the file uses is named on a line, and the verdict is unchanged.
  • A fingerprint may be bound to a key id: KEY=FINGERPRINT in a party's box on the page, or --pin DID#KEY=FINGERPRINT on the command line. A bound fingerprint must be the key the document holds under that id: a different key there is key_mismatch (pin_bound_mismatch), and an id the document does not hold is key_mismatch. A bound pin is compared with the key the document gives that id even when no record names it (pin_bound_document_mismatch, key_mismatch).
  • A party with a segment but no record whose signature was checked is still held to its pin: the fingerprint is compared with its key document (pin_no_records); a fingerprint that is not a key there is key_mismatch.
  • A key document must be consistent: one key listed under two ids, or one id listed twice with different keys, is changed, the document named inconsistent, whatever is pinned.
  • A record signed under a joint signing model (threshold_hybrid or participant_held) must verify under a key different from the one the party uses for its village_custodied records; otherwise the record is incomplete (signing_key_not_distinct) and its label does not say it was signed jointly.

7. The day tree and day roots

Each party numbers its records 1, 2, 3 and so on (tenant_seq), across all its lots, and signs one day root per date over a contiguous range of them.

  • Leaf = SHA-256(0x00 || receipt hash). Node = SHA-256(0x01 || left || right). An odd node at the end of a level is promoted unchanged. One leaf: the root is that leaf. No leaves: the root is SHA-256 of no bytes.
  • An inclusion proof lists siblings from the leaf upward as { hash, side }: "L" means the sibling is on the left (node = SHA-256(0x01 || sibling || current)), "R" on the right.
  • A day root carries date, root (the tree over its range in number order), first_tenant_seq, last_tenant_seq, count, previous_root and previous_root_record_sha256 (64 zeros on the first), and the lot commitment lot_commit_root and lot_commit_leaves.
  • Where every record of a day root's range is in the file, the check rebuilds the root from them, so a missing proof changes nothing; otherwise each record shown must carry a proof that reaches the root.

8. Lot commitments

A day root covers all of a party's lots, so a one-lot file shows a selection of its range. Each day root therefore also signs a tree with one leaf per lot carrying that lot's record count and subroot, built so that a neighbouring leaf reveals nothing about another lot.

  • The records of the range are grouped by lot_ref. A sequenced record that is not a step, a handover or an attestation is committed under one of two reserved lot references: erasure and disclosure records under the omittable party leaf, SHA-256 of the ASCII text provenance:party_omittable_records:v1, which is 316f1a126fb367fd21d30a6b3529c29361885d3fc60902e5de2c2dec7aae1876; every other party record (a standing authorisation, key record or authority confirmation, and any kind added later) under the party leaf, SHA-256 of the ASCII text provenance:party_records:v1, which is 1e9111626cca531d5772865a9c298c94b521d33ea632e281e2f41b596d4ab814. No lot may use either reference, as a lot_ref or among input_lot_refs. An attestation is committed under the lot it speaks about, its own lot_ref (its signer holds no lot row for it and it joins no lot chain): a file for that lot that carries the attester's segment shows it against the attester's count for the lot, and one left out reads incomplete. Each included day root with a lot commitment carries an entry or an absence proof for both reserved references and for the lots of the records the segment shows; a segment that shows no step or handover of the file's lots (a party in the file for its attestations) owes this per day root, for the lots of the records it shows in that day root's range, and nothing for the file's other lots, so an attestation pack built for one lot is read in a file for a lot later made from it. A reserved leaf stripped whole, records and entry, is incomplete (lot_absence_missing). What keeps an attestation from being dropped together with its entry is the holder's receipt of it (see "Other party records").
  • count: the lot's records in the range (1 to 232 − 1). lot_subroot: the day-tree root over the lot's receipt hashes in number order.
  • sortkey = SHA-256(0x03 || lot_ref || date). inner = SHA-256(0x04 || count || lot_subroot). leaf = SHA-256(0x02 || sortkey || inner).
  • The lot tree orders leaves by the 32 raw bytes of sortkey, ascending, and combines them with node = SHA-256(0x01 || left || right), an odd node promoted, the leaf hashes used as they are. No lots: lot_commit_root is 64 zeros and lot_commit_leaves is 0.
  • A lot_commits entry carries exactly day_root_receipt_id, lot_ref, count, lot_subroot, index, proof, neighbours; neighbours is { previous, next }, each null or exactly sortkey, inner, proof. It carries no date, sortkey or inner of its own: the check computes them from the signed day root.
  • The path flags of index i in a tree of n leaves are fixed: p = i, m = n; while m > 1: if p is odd the step is "L"; if p is even and p + 1 < m the step is "R"; if p is even and p + 1 = m there is no step; then p = floor(p / 2), m = ceil(m / 2). A proof's sides must equal that list exactly (at most 32 steps), so the index is verified, not trusted.

What the check does with an entry

  1. The day root it names must be in the party's segment and its signature must have verified (an entry naming a day root not in the segment is changed). index is from 0 to lot_commit_leaves − 1, and the leaf walks to the signed lot_commit_root along a proof with the flags of its index.
  2. previous is null exactly at index 0 and next exactly at the last index; each one present walks to the same root with the flags of its own index, and previous.sortkey < own sortkey < next.sortkey, strictly.
  3. More records shown than count is changed; exactly count must rebuild lot_subroot, else changed; fewer is incomplete.
  4. Every shown record inside an included day root needs an entry for (that root, its lot) or the file is incomplete; the counts under one day root may not add up to more than its count; where a party's day root range is wholly in the file, the counts are recomputed from all its records and must equal the signed root.
  5. A lot that matches its signed count under every day root covering it needs no interior-hole rule: numbers not shown are taken as other lots' records, on the signer's own signed word.

What this establishes: the signer signed, under that day root, a leaf binding the lot to a count and a subroot, and the records shown match it or they do not. What it does not: that the signer's tree holds no other leaf with records of the lot. A party holding its own key can place a second leaf for the lot away from the shown one, and the check, which sees the shown leaf and its two neighbours, accepts it. The not-established list says so.

Reasons a builder lists in lot_commit_unproven:

ReasonMeaning
records_not_shownFewer of the lot's records in that day root's range are in the file than its count (a pack cut at a handover).
day_root_without_lot_commitThe day root carries no lot commitment.
lot_commit_not_suppliedA stored pack made before lot commitments existed.

9. Lot absences

For every day root of a party in the file that carries a lot commitment, the file must show, for each lot shown anywhere in the segment, either the lot's leaf or that the signed lot tree holds no leaf for it. A lot_absences entry carries exactly day_root_receipt_id, lot_ref, between, previous_index, next_index; between is { previous, next } in the neighbour form above.

  1. lot_commit_leaves 0: valid only with the zero root, both neighbours null and both indexes null.
  2. Otherwise at least one neighbour; previous null exactly when previous_index is null, and then next_index is 0; next null exactly when next_index is null, and then previous_index is the last index; with both, next_index = previous_index + 1.
  3. Each neighbour walks to the signed root with the flags of its index, and previous.sortkey < the lot's sortkey < next.sortkey, strictly. Two leaves at consecutive indexes of a sorted tree with the sortkey strictly between them leave no place for a leaf with that sortkey.
  4. An absence that does not check is changed; a shown lot with neither an entry nor an absence for a day root that carries a commitment is incomplete.

10. Standing authorisations and signing models

Every sequenced record states its signing_model: one of village_custodied, threshold_hybrid, participant_held. village_custodied: the organisation's key is held by its Village and used on a standing authorisation the organisation's owner gave. threshold_hybrid: the signature needs key shares from more than one holder, one of them the Village, so neither signs alone. participant_held: key shares held by the organisation's own custodians; the Village holds none. The record also states captured_at and capture_mode (online or queued; an online capture is never after decided_at; a queued capture, entered on a device with no connection and sealed when it reconnects, is never more than 5 minutes after it), so a record entered offline and signed later says so. captured_at is the device's clock and is not checked. Where captured_at and decided_at are more than 15 minutes apart, in either mode, the check prints both (record_capture_time, informational): "Time recorded by the device: … Time sealed: … The device's clock was not checked."

A standing authorisation is a provenance_mandate record, signed under the custody model it grants: custody_model (village_custodied or threshold_hybrid), the key_id it covers, a scope (sorted, from archive_root, attestation, day_root, disclosure_event, erasure_event, handover, key_event, step, tsa_confirmation), grantor_role owner, grantor_auth (a grant needs passkey or totp), valid_from and valid_until (at most 12 months apart), the three acknowledgements in order (erasure_limits, legal_framing, sealed_permanence) and privacy_note_version. A grant must be signed with the key it names in key_id: a grant signed by any other key covers nothing, and the records that name it are incomplete, with the reason named. A withdrawal or renewal is a later mandate naming the earlier one in supersedes_mandate_sha256. Every record signed under village_custodied or threshold_hybrid names its mandate in mandate_ref_sha256 (the mandate's receipt hash); a mandate and a participant_held record carry none.

  1. The mandate a record names must be in the file, signed by the same party; otherwise the record was signed outside any standing authorisation shown, and the result is incomplete.
  2. The mandate must cover the record: its kind in scope, the same custody model, decided_at on or after valid_from and before valid_until, no withdrawal or renewal in the file taking effect first, and the key the record names. The first failing reason is named (out of scope, another key arrangement, not yet valid, expired, withdrawn, another key); the result is incomplete.
  3. A key document may say how a key is held (custody on its key entry or key-history entry). Where it disagrees with the signing model of a record signed with that key, the result is incomplete.
  4. A detached day root: a standing authorisation is often granted days before the records that name it. A segment labelled selective may carry, before the party's unbroken run of day roots, the day root that holds that mandate (or a withdrawal or renewal of it) without the day roots between. Such a day root is accepted only before the run, only when it shows no lot record (step or handover), and only when it shows a mandate that the records of the run name; anything else breaks the run as usual. A complete_range segment is never read this way. The check names each detached day root on a line.
  5. What a detached day root cannot show, stated on that line and here: a record the party signed on a day between the detached day root and the run, such as a withdrawal of the mandate or a key record, is not in the file and would not show.

The signing part of every label is printed as the record states it, under the heading "Signing, as the record states it (this page cannot check how the key was held)": a dossier carries no evidence of how a key holder kept its key, and the not-established list says so. The source sentence and the signing sentence of a label are chosen independently, so a holder's own attestation and a signing the page cannot show are both stated, and a label never says "Signed jointly" for a signing the page cannot show.

11. Evidence strength and corroboration

Every record other than a day root is labelled from two of its own signed fields: attestation_source (on a step, self or device; device needs a weighbridge_ticket or sensor_log among its evidence; an attestation is always third_party) and signing_model. The label's text is fixed per value: the source says whose word the record is, and the signing part, printed as the record states it, says who held the key, with the dates of the standing authorisation when it is in the file. The test vectors carry every label and its exact text.

Corroboration is worked out per point of the subject lot (origin, handling, quantity, inspection): contested where another party that did not hold the lot recorded a third_party attestation of fail about that point, corroborated where one recorded pass or observed, otherwise uncorroborated. The parties that held the lot never corroborate one another, and a lot holder's own third_party attestation about that lot is the holder speaking, not corroboration: it is left out before the levels are worked out. Only records whose signature verified, from parties the reader pinned that hold no record of the lot in the file, can corroborate or contest a point; the limit (a party with no record of the lot in this file may hold one elsewhere) is said once. Where a lot holder signs a third_party attestation on its own lot, the label says the file shows that party holds the lot. A superseded step does not count. The file's own corroboration and evidence_strength are unsigned and are checked against what the check works out.

12. Time stamps: the quorum and the qualified label

  1. Each token in tokens must be the SHA-256 of its own bytes, and must check as an RFC 3161 token over the record's anchored digest, issued by an authority whose roots this checker carries. A record whose stated tokens are missing or do not check gives timestamp_missing, never verified.
  2. For each record the check states how many authorities the record says answered against the number required (2 on a day root and an archive root, 1 otherwise) and how many it checked itself, for example "only one authority answered (2 required)".
  3. A quorum counts different authorities, each with its own token: each obtained entry must come from a different authority and carry a different token. The same authority twice, or two authority names over one token, is refused by the platform before signing and by the signing host, and a file carrying such a record is not verified. The check counts only tokens it verified itself, and counts authorities cautiously. An authority is every key on a token's checked path together with the operator of the anchor the path ends at: two paths that share any key, or end at anchors with the same operator, are one authority (the checker's bundled anchors name their operators: both Certum roots are certum, Aruba is arubapec, the Portuguese state root is pt-sce). The count is never more than the number of distinct stamping certificates. Where the two differ for a record whose stated quorum is met, the checker's time line for that record says both, as "(N authorities, M stamping certificates)": that text is in the checker's result and in the command-line report (printed with --authorities), and the result carries both counts as the line's authorities and stamping_certificates. The browser page writes its own time line and shows the number of authorities only. A record whose signed quorum says met but whose checked tokens make fewer authorities than required is incomplete (quorum_not_met_here), whatever authority names the record gives.
  4. A token is labelled qualified only when both hold: a certificate on the path the checker itself verified has the key of a qualified time-stamping service as pinned in the checker's own dated snapshot of the trusted lists (today IT sequence 224, issued 2026-09-24; PL sequence 162, issued 2026-09-18; PT sequence 121, issued 2026-10-07, read 13 October 2026; the lists' own XML signatures are not verified by the checker). The label also needs the list entry to be of type qualified time-stamping service (QTST), with status granted and a status start no later than the token's time; the snapshot holds each service's current status only, so a status that began after the stamp earns no label, nor does a withdrawn status or another type. The Italian entry was re-read on 13 October 2026: sequence 224, type QTST, granted, status start 27 November 2017; and a party in the file has confirmed that authority as qualified in a signed provenance_tsa_confirmation whose tsu_anchor_sha256 matches the token's certificate and which no later record withdraws. Every other token is "not verified as qualified"; the record's own qualified_status is never taken as the label.
  5. A provenance_tsa_confirmation names authority_id (certum, aruba, nz_tsa, pt_state), the fingerprint of the stamping certificate the party matched, the jurisdiction of the trusted list read (an EU or EEA code), its sequence and issue date, a status (confirmed_qualified, confirmed_not_qualified, withdrawn), confirmed_at and confirmer_role owner. It seals no link: the checker prints the official list's address from its own table. A confirmation is superseded or withdrawn only by a later confirmation of the same party; a record of another party naming it changes nothing. While a party's confirmation that an authority is confirmed_not_qualified stands, no token of that authority is labelled qualified, whatever another party confirms. A confirmation that cites an older entry of the trusted list (a lower sequence) than the snapshot the checker carries does not earn the label, and nor does one citing another member state's list or the same sequence with a different issue date: a confirmation earns the label only when it cites exactly the list entry the checker carries; the line says why. A confirmation citing a newer issue of the list than the checker carries does not override the checker's snapshot, which decides; the line says the confirmation cites a newer issue.

13. Key records

A provenance_key_event records that a key was rotated, withdrawn, compromised, with key_id and effective_at. A rotation names a different successor_key_id and is signed by the successor; a withdrawal or compromise is signed by the key it names or by its successor. From effective_at the key signs nothing that counts: a record signed with it whose anchored time is at or after that moment, or that has no outside time, is unanchored_under_withdrawn_key; a record anchored before it still verifies.

A compromise is different: its effective_at is the party's own statement and may be back-dated. The check prints it as a stated compromise time, says it does not verify it, and lists every record signed with that key against it: signed before it by its checked outside time (shown, no finding), signed after it, or not shown either way because no outside time was checked (both unanchored_under_withdrawn_key). No record is quietly counted as good or bad.

14. Other party records

provenance_attestation: a party's signed statement about a lot it does not hold here (inspection, certification, audit; point origin, handling, quantity, inspection; outcome pass, fail, observed). provenance_erasure_event and provenance_disclosure_event list the commitments affected (sorted, distinct) with a closed class, basis or recipient class, and no personal data. provenance_archive_root is dated and unsequenced, like a day root, and covers a run of day roots; this checker checks its signature and its contract only, and says so.

provenance_attestation_receipt: the holder's own signed record that an attestation about its lot was delivered to it through the platform. It names the holder's lot_ref, the attester_did, the attestation's receipt hash attestation_receipt_sha256 and the delivery_id, and nothing of what the attestation says. It is sequenced and committed under the holder's lot like a step, joins no lot chain (no prev_receipt_sha256), and is shown in every file for that lot; one about a lot that is not the file's lot or an input of it is changed (foreign_lot_records). For each such receipt the file must show the attestation it names in the attester's own segment, about the same lot, with an inclusion proof into a signed day root of that party; otherwise the file is incomplete (attestation_receipt_unmet), whether the attestation, the attester's segment or its day root for that day was left out. This holds for the day roots the file includes: a file cut after an earlier day root of the holder or of the certifier, leaving out its trailing day root, can still drop a delivered attestation without trace: the general limit on a party's day roots that are not in the file, which the not-established list states. A receipt that is met gives an informational line (attestation_received); each party in the file for its attestations gives one line stating its signed day roots in the file (attester_roots_shown); an attestation that no receipt in the file names gives an informational line saying it did not reach a holder shown here through the platform (attestation_unreceipted). Such an attestation can be left out of a file together with its entry and nothing in the file says so; the not-established list says this too.

15. Openings, erasures, mass balance and profiles

A commitment is a fingerprint of a value and a random salt: it fixes the value without showing it, and it is opened by disclosing the value and the salt, which anyone can hash again. The word is kept in English on every page and in every language, with this gloss.

  • Plot openings. For each entry of plot_openings the check hashes the geometry and the salt again. A match with the commitment a shown record signed gives a line naming the plot and its number of corner points (plot_opened); a mismatch is changed (plot_opening_mismatch); an entry for a commitment no shown record carries is listed and not read (plot_opening_unused). A plot or quantity opening is said to match only for a record whose signature verified (plot_opened, quantity_opened); for a record that did not verify, the line says the opening was not checked against it (opening_record_unverified). Where the same party signed a disclosure record naming that commitment, verified and pinned, a line says so (plot_disclosure_signed); otherwise a line says the boundary was disclosed without a signed disclosure record in this file (plot_opened_without_disclosure).
  • Erasure. A commitment whose data its party erased is shown as "Commitment unopened (data erased)", informational, never as a change: only when an erasure record of the same party, whose signature verified and whose key the reader pinned, names that digest for that class of data (or for all). The erasure record must also be proved into a day root in the file and must not be signed under a key its party states was compromised before it (not unanchored); the date shown is the date the party states. Disclosure lines follow the same rules. The dossier may include the party's own erasure records for this: at most 64 per segment, earliest first, each naming a digest a shown lot record carries.
  • Mass balance. Over the steps whose signatures verified and the quantities opened in the file, one informational line: balanced (the quantities opened add up: no lot made from others holds more than they did, and no lot's quantity grows along its records; it says only that the figures in the file add up), unbalanced, not compared (the quantities are in different kinds of measure), or not checked (a quantity opening does not match its commitment, or not every quantity needed is opened).
  • Profiles. A record may name a profile: generic_v1, honey_nz_v1, beef_nz_v1, dairy_nz_v1, kiwifruit_nz_v1, nz_mpi_animal_products_v1, nz_mpi_plants_v1. A well-formed profile this copy of the checker does not list gives one informational line per profile, "Profile not known to this checker", naming the parties; those records are checked as any other, and what the profile adds is not checked. An adapter id such as the EU Deforestation Regulation supplier pack is not a profile id.

16. Verdict words, tiers and exit codes

Every finding raises one word. The headline is the word of the lowest tier raised; within a tier the first raised stands. The exit code is the one a command-line run gives.

WordTierExit codeMeaning
changed11A signed record, a stated value, or an entry of the file does not match what was signed.
key_mismatch11A party's key is not the one whose fingerprint the reader gave.
gap_in_sequence11A record that a day root or a chain link says exists is missing from the file.
handover_unmatched11One side of a handover is in the file without the other, or names a record that is not there.
handover_disputed22Both a withdrawal and an acceptance of one handover are in the file; the check states which outside time stamp is earlier and chooses no winner.
unanchored_under_withdrawn_key25A record is signed with a key the file or the reader says was withdrawn before the record's anchored time.
incomplete33Nothing is shown to be wrong, but the file does not hold enough to say it is whole (a record not covered by a day root, an unproven lot commitment, a missing input lot or upstream party).
unknown_version36A version this checker does not read.
unreadable36The file, or part of it, is not in the format: not JSON, a duplicate key, a field the format does not have, a limit exceeded.
not_finished_within_limit36The check was stopped, or neither signature engine could run.
not_understood44A record of a kind this checker does not read.
sandbox_receipt53A record made in a sandbox.
key_not_pinned64The reader gave no fingerprint for a party, so who holds its key is not established.
key_history_missing64No key history for a party, so whether its key was withdrawn is not established.
timestamp_missing64A record's time stamp is absent or does not chain to an authority this checker carries.
verified70Every check passed, and the file is whole for every party in it.

17. The checks, in order

  1. Parse: at most 25 MB; strict JSON with no duplicate field name; nesting at most 64; no unknown top-level, segment or entry field; family provenance; version 1; at most 20000 records.
  2. Each record: the base64 is canonical; the bytes are canonical JSON that re-canonicalise to themselves; the readable copy equals them; version 1; the contract of its kind.
  3. Each signature against the key in that party's key document; the fingerprint the reader gave for that party; key withdrawals.
  4. Time stamps: each token's key is its hash; each token is checked as RFC 3161 against the anchored digest and the authorities this checker carries.
  5. Handovers across parties: proposal and reply over one terms digest, each signed by its own party; the terms digest recomputed; the receiver's lot_ref equal to the giver's.
  6. Per party: the sequence its signed day roots cover, the day-root links, each inclusion proof (at most 64 steps), the lot commitments and absences.
  7. Per party and lot: the chain from its first record to its last, then the chain rules.
  8. Standing authorisations and key records: each record signed on a party's behalf held to the mandate it names; key documents that disagree with a record's signing model; keys stopped by key records.
  9. Time: per record, the authorities that answered against the quorum, and per token the qualified label rule.
  10. Evidence strength and corroboration, worked out from the signed records and compared with any the file states.
  11. Disclosures: lot salts, evidence salts (and a seal number the reader types), quantity openings, evidence files the reader drops.
  12. The lot graph, recomputed and compared with the one stated.

Nothing is fetched: the checker makes no network request. It returns a result for any input and never throws.

18. What the check does not establish

Printed under every result, by the checker itself:

  • Who any party is. A DID is a name a party chose for its key. Whether it belongs to the business on your contract is for you to confirm with that business, for example by comparing the key fingerprint they give you with the one shown here.
  • Whether anything a party declared is true. A grower's harvest record shows what the grower signed and when. It does not show where the produce was grown, what it contains, or that the statement is accurate.
  • What a referenced document says. Where the document is in this file, the page checks only that it matches its fingerprint; where it is not, nothing about it is checked.
  • That nothing was left out. A party can choose not to record a step. For each day root of a party in this file, the records shown for this lot match the count of records that party signed for the lot in that day root, or that party signed that it made none that day, so a record cut from the file or added to it shows. On a day whose day root is not in this file, a record of this lot would not be seen.
  • Whether a party made a correction after this file was put together, or one made after its last day root in this file was left out when the file was put together.
  • Whether the lot meets any law, standard or scheme, including the EU Deforestation Regulation.
  • The time of a record whose time stamp is absent or does not chain to an authority this page carries.
  • Whether a key was withdrawn. The page shows a withdrawal only as this file states it, unless you give it the key history yourself or list the keys you know were withdrawn.
  • Whether a party made later records for this lot that are not in this file. A record made after the last of a party's day roots in this file, such as a later record by the last party to hold the lot, a correction a party made after handing the lot on, or what a party did with an input lot after deriving from it, would not show.
  • Whether a party signed only one history. A party controls its own signing key, so it could sign a day root whose count for a lot leaves out or hides some of that lot's records, or sign a second branch of its chain for this lot, for example one handing the same lot to someone else as well. The page shows that this file agrees with what each party signed, not that a party signed nothing else. A buyer who needs more can ask that party for a dossier showing its records in full. The same holds for a party's records that belong to no lot: a dishonest signer could file a key or authorisation record among its erasure and disclosure records, which a file may leave out and of which this page only counts the ones not shown. Nor does the page show an attestation that another party made and that was never delivered through the platform to a holder in this file: no chain here records receiving it, so a file can leave it out and nothing in the file says so.
  • How a signing key was held. The record states how it was signed; this check cannot see inside a key holder's systems.

19. Test vectors

test-vectors.json (SHA-256 5b17d67e5c4083d88ff1e48ce85df0d3701bacb51f5823de32b77b6fc6850053) holds: canonical JSON inputs, their canonical text and hash, and inputs that must be refused; a record signed with the private key of RFC 8032 section 7.1, test 1 (published in that RFC; it must never sign anything real), with its canonical bytes, signature, receipt hash, anchored digest and day-tree leaf, and the same bytes with one byte changed, whose signature fails; a signed record of every other kind the milestone 1 schema adds (standing authorisation, key record, attestation, authority confirmation, erasure and disclosure events, archive root, and the holder's receipt of an attestation), with the quorum and qualified status in the time block; a step captured offline (queued) whose device time is exactly 5 minutes after its sealing time, which is valid, the same one millisecond later, which is refused, and one captured three hours before it was sealed, which is valid; the party leaf; every evidence-strength label and corroboration vector; day-tree roots of 1, 2, 3 and 5 leaves and every proof of the 5-leaf tree; and the lot commitment and absence vectors: a three-lot tree, a two-lot and a one-lot tree, the path flags up to five leaves, a forged tree with two leaves for one lot, and one absent lot in each gap of the three-lot tree. The published checker meets every vector.

Sample dossiers: samples.json lists 14 sample files (four-party-honey, four-party-honey-altered, honey-chain, honey-chain-altered, honey-chain-incomplete, beef-chain, beef-chain-altered, beef-chain-incomplete, dairy-chain, dairy-chain-altered, dairy-chain-incomplete, attestation-delivered, attestation-delivered-stripped, queued-capture), each with its SHA-256, the key fingerprints to give, what it carries and the result to expect, read from the manifest itself.

20. The checker and its fingerprint

The checker is one hand-written file with no build step: the file served is the source, so its fingerprint is the fingerprint of the source. It runs in a current browser and in Node 20 or later and needs nothing else.

provenance-verifier.js, SHA-256:

eff7425271fd4ff57ac0d27e6e05944de7292c9297edde4396e239f53128618f

The same value is at provenance-verifier.js.sha256, and the export puts a dossier together only when the copy it places inside matches it. To compute it yourself from a copy you hold:

sha256sum provenance-verifier.js                    # Linux
shasum -a 256 provenance-verifier.js                 # macOS
certutil -hashfile provenance-verifier.js SHA256     # Windows
node -e "process.stdout.write(require('crypto').createHash('sha256').update(require('fs').readFileSync('provenance-verifier.js')).digest('hex'))"

Hash the file's bytes as downloaded, without opening and saving it in an editor, which may change line endings.