The verifier
Verify a MINERVA evidence packet
An evidence packet is one folder exported from an institution's record. This page is what a reader needs to check one with nothing but Node.js — the command, what each section recomputes, the shapes it reads — and what a passing check does and does not establish. Nothing of MINERVA's is needed to run it.
The one command
Unzip the packet into a folder and, in that folder, run the script it carries. Node.js 18 or later; nothing installed; no network.
node verify.mjs
It prints one ok: line per section, or names the first row, file, receipt or checkpoint that breaks, and exits with status 1 on any break. When nothing failed it ends with one line a reader can keep and quote:
verified: mnv-verify/1 head <seq> <head_hash> manifest <sha256 of manifest.json> at <ISO UTC> on <hostname>
On any failure the last line is not verified. The packet.exported row that recorded the export carries the same manifest.json hash, so a verification line can be matched to the export it belongs to. The hostname is the machine's own (os.hostname()); no user name is printed.
What the script checks
Seven sections, in this order. Each is a recomputation from the packet's own bytes, never a lookup.
- The ledger. Recomputes every row's hash from
ledger/ledger.jsonand walks the chain — each row'sprev_hashmust name the row before it — then checksledger/head.jsonnames the last row. A row withheld from the exporting reader is carried by hash only and the chain still checks through it. - The files. Recomputes the SHA-256 of every file listed in
SHA256SUMS. - The receipts. For each
anchors/<id>.tsr— an RFC 3161TimeStampResp, DER — reads the hash the authority signed (TSTInfo.messageImprint) and its time out of the bytes, finds the ledger row that carries that hash, checksanchors/index.jsonagrees, and cross-checks theanchors/<id>.receipt.jsonbeside it against the token it names. The token's signature is not checked here — that is OpenSSL's, against the authority's own certificate chain — and the command is printed. - The observed book. Walks
observations/, the connectors' second chain, with the same canonical form, and checksobservations/head.json. - The validation runs. Walks
runs/, the SDK's third chain: each run's results hash is recomputed from the results inside it, then the run's own hash, then the link to the run before; checksruns/head.json. - The checkpoints. For each
checkpoints/<day>.json— a day's ledger head signed with the deployment's Ed25519 key — verifies the signature under the public key the file carries, checks the key id, and finds the ledger row whose hash the checkpoint names. For a checkpoint that was posted to a public log, prints the one command that fetches the log's entry and what to compare; that fetch is the reader's, not the script's. - The verification line. When every section passed, the
verified:line above; otherwisenot verifiedand exit status 1.
The packet's shape
One folder, generated from the workspace's record at the moment of export. The README inside says what is in that particular packet; this is the shape every packet shares.
README.mdWhat is inside, how to verify, what it proves and does not, retention and custody — written for this export.manifest.json·SHA256SUMSEvery file with its size and SHA-256. The verification line quotes the manifest's hash.ledger/ledger.json·ledger/head.jsonEvery ledger row of the workspace as stored, oldest first, each with its own hash and the hash of the row before it; the head's seq and hash and the canonical-form version.anchors/<id>.tsr·anchors/<id>.receipt.json·anchors/index.jsonThe receipts outside time-stamp authorities returned over a ledger head, verbatim; a receipt file beside each (below); an index naming, for each, the row it fixed and the authority that signed it.observations/observations.json·observations/head.jsonThe connectors' observed book, hash-chained. An observation is what an outside system said, never the record: it became a fact only through a decided proposal.runs/runs.json·runs/head.jsonThe validation runs as stored, each with every result it produced inside and hash-chained to the one before.checkpoints/<day>.json·checkpoints/key.pemEach day's ledger head signed with the deployment's own key, the signature and the public key inside; the public key alone, as PEM.printed/Documents printed from the packs and from the deployment's own configuration — among themrecord(how the record is kept) andprocedures-manual— each as.md,.docxand.pdf.register/·dossiers/·binders/Per pack: the gap report, the spreadsheet and the board pack; one dossier per subject on the register; one preparation binder per package. The.mdis the text the hashes quote.verify.mjsThe script described on this page, as it stood when the packet was exported.
The canonical form (mnv-canon/1)
The script's comment states it, and the script is that rule written out in plain JavaScript, so it can be read as well as run. Exactly:
The canonical form (mnv-canon/1), exactly as the software computes it:
object keys sorted by UTF-16 code unit; no whitespace; strings NFC-
normalised then JSON-quoted; finite numbers as JSON numbers, non-finite
as null; null and undefined as null; an object key whose value is
undefined is dropped; arrays in order.
A row's hash is SHA-256 (hex) over the canonical form of
{ id, org_id, at, kind, actor_id, actor_kind, subject_kind, subject_id,
payload, prev_hash, canon_version }.prev_hash is the hash of the row before it in the same workspace; the first row's is null. The observed book uses the same form over { id, org_id, connector_id, node_id, external_id, title, observed_at, facts, prev_hash, canon_version }. A validation run's results_hash is the form over its results in order, each as { id, run_id, org_id, node_id, of, metric, value, threshold, direction, verdict, meets, method, segment, detail }, and the run's own hash the form over { id, org_id, node_id, suite, started_at, finished_at, by_id, by_name, method, sample, data_hash, code_version, env, status, counts, tables, note, results_hash, prev_hash, canon_version }.
Without the script, sha256sum -c SHA256SUMS (macOS: shasum -a 256 -c SHA256SUMS) checks every file against its recorded checksum, and sha256sum printed/<document>.md reproduces the hash the README prints for it.
The receipt
An anchor is a ledger head fixed by a party outside the deployment: the software sends the head's SHA-256 and a random nonce to an RFC 3161 time-stamp authority, and the authority signs the hash together with its own clock. Only a hash and a nonce leave the building. The authority's answer is kept verbatim as anchors/<id>.tsr; beside it the packet writes a receipt file:
{
"format": "mnv-receipt/1",
"issuer": "<the authority's host>",
"subject": { "org_id": "…", "head_seq": 3060, "head_hash": "<64 hex>" },
"statement_sha256": "<the imprint the authority signed>",
"registered_at": "<the authority's genTime, verbatim>",
"nonce": "<hex>",
"tsr": "<id>.tsr",
"note": "SCITT-shaped (RFC 9943 vocabulary); the signed artefact is the RFC 3161 token beside it, not a COSE receipt"
}The receipt is SCITT-shaped — it uses that vocabulary for the issuer, the subject and the statement — and the signed artefact is the RFC 3161 token beside it, not a COSE receipt. The script checks that the receipt file and the token agree; the token's signature is checked with OpenSSL against the authority's own certificate chain:
openssl ts -verify -digest <head hash, hex> -in anchors/<id>.tsr -CAfile <the authority's certificate chain, PEM>
openssl ts -reply -in anchors/<id>.tsr -text prints what the token says. The script prints the verify command with the imprint filled in, once per receipt. Whether an authority is a qualified one under any regime is the operator's statement, never the script's.
The checkpoint and the public log
A checkpoint is one day's ledger head signed with the deployment's own Ed25519 key: the signature is over the UTF-8 bytes of three lines joined by LF with no trailing newline — the workspace id, the head's seq in decimal, the head's hash — under the public key in checkpoints/key.pem, whose key_id is the first 16 hex characters of SHA-256 over the key's DER. By hand, with Node.js alone:
node -e "const c=require('crypto');const m=Buffer.from('<org_id>\n<head_seq>\n<head_hash>');console.log(c.verify(null,m,require('fs').readFileSync('checkpoints/key.pem','utf8'),Buffer.from('<signature>','base64')))"A copy of each checkpoint leaves the deployment the day it is signed — to the owner's mail, to the institution's bucket and to a public transparency log — whichever the operator configured; the README's table says which actually left. When the log is Rekor (https://rekor.sigstore.dev), the checkpoint file carries the entry the log answered with (uuid, logIndex, logID, integratedTime), and the entry is fetched by anyone:
curl -s https://rekor.sigstore.dev/api/v1/log/entries/<uuid>
Compare the body's hash and signature against the checkpoint file's. What a checkpoint proves: that the software holding this deployment's key signed this head on this day — the service's attestation, never a person's signature. A rewrite inside the deployment would disagree with a copy the deployment does not control.
The verifier, to read before you run it
The packet's verify.mjs is the one to run; this copy is for reading it first, and for checking that a packet's copy is the same bytes. It is generated from the same text the software writes into every packet, so a packet exported after 26 October 2026 carries this file; an earlier packet carries the version of its day — read the one inside the packet.
- File
- verify.mjs
- Size
- 27,228 bytes
- SHA-256
- e80e8e87ebb75aab1fe7af5e1ac65bb97f3f0834bda706183f2e9e0c621bc5aa
- Check
- shasum -a 256 verify.mjs
The script imports node:crypto, node:fs, node:path and node:url and nothing else; it opens no connection.
What a passing check does not prove
In the packet README's own words, kept here so nobody has to open a packet to read them.
- Append-only is an application-layer property. The software only ever adds rows. The hash chain lets a reader catch a row edited or removed in place — the recomputed hash no longer matches, or the next row no longer names the one before. It does not, by itself, catch a chain rewritten wholesale from some point on by someone holding the store and this same arithmetic. That is what the anchors and the checkpoints are for: a chain rewritten after a receipt's time cannot reproduce the receipt, and a rewrite inside the deployment disagrees with a copy the deployment does not control.
- Rows before the first witness. A receipt or a checkpoint proves nothing about the period before the first one was recorded; rows written before it could have been rewritten before it, and nothing outside the file says otherwise. A packet with no anchor stands on its own arithmetic and on the software's append-only behaviour — stated in its README, not assumed.
- What was never recorded. Every figure inside is recomputed from the record; where the record is silent the document says so. A field nobody recorded is a gap and a field recorded as UNSPECIFIED is a stated gap — two different findings — and neither is a number invented to fill it. The packet shows what the record holds; it cannot show what was never written into it, nor who wrote a row beyond the account the row names.
- The authority's honesty, and its clock. A receipt proves that the head's hash existed by the time on the receipt as witnessed by that authority. The check rests on the authority's key and clock, which is why a deployment may name more than one authority and may post the head to a public log; the README's tables say which did, and the script never calls any of them qualified.
- The export is the next row. The
packet.exportedrow that records an export lands after the head the packet quotes, so the head inside a packet is one row behind the store the moment the export finishes — by design. - A reader's reach may be partial. Rows about sheets behind a gate the exporter may not pass are carried by hash only, marked
withheld; the chain still checks end to end, and the registers and binders show only what that reader may open. - No posture. No document in a packet asserts a posture against any standard, and no figure in it is a coverage percentage.
Published 26 October 2026 by Minerva Software Inc. The script's text is the specification; where this page and the script in a packet differ, the script in the packet is what was run. The policy key's format is published beside this page at /policy-key. Questions about either go to minerva@minerva.studio.