MINERVA™
Verify a packetThe policy keySecurity

The verifierpublished 2026-10-26

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.

01The 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.

02What the script checks

Seven sections, in this order. Each is a recomputation from the packet's own bytes, never a lookup.

  1. The ledger. Recomputes every row's hash from ledger/ledger.json and walks the chain — each row's prev_hash must name the row before it — then checks ledger/head.json names the last row. A row withheld from the exporting reader is carried by hash only and the chain still checks through it.
  2. The files. Recomputes the SHA-256 of every file listed in SHA256SUMS.
  3. The receipts. For each anchors/<id>.tsr — an RFC 3161 TimeStampResp, DER — reads the hash the authority signed (TSTInfo.messageImprint) and its time out of the bytes, finds the ledger row that carries that hash, checks anchors/index.json agrees, and cross-checks the anchors/<id>.receipt.json beside 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.
  4. The observed book. Walks observations/, the connectors' second chain, with the same canonical form, and checks observations/head.json.
  5. 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; checks runs/head.json.
  6. 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.
  7. The verification line. When every section passed, the verified: line above; otherwise not verified and exit status 1.

03The 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 them record (how the record is kept) and procedures-manual — each as .md, .docx and .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 .md is the text the hashes quote.
  • verify.mjsThe script described on this page, as it stood when the packet was exported.

04The 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.

05The 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.

06The 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.

07The 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.

08What 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.exported row 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.

MINERVA issues no certification, attestation or opinion. Management still writes its own assertion; your auditor still selects, tests and concludes. What is shown is evidence about this workspace only.

© 2026 Minerva Software Inc. · minerva@minerva.studio