The reference verifier
Apache License 2.0. Part of The Record, protocol draft 0.1. Chapter 06. Verification is the normative text; this page is how you run it.
What it does
verify-offline.js is one Node file with no dependencies, no configuration, and no network access. Given a proof file downloaded from a verification page, it answers one question: is this receipt really in the signed, publicly anchored receipt log, and are the bytes the family sealed the bytes you are holding? It answers without asking Eformogi anything, which is the point. If the operator's servers went dark tomorrow, a proof file and this script would still verify.
It checks, in order:
- the artifact hash you computed yourself equals the
sha256recorded in the logged leaf, when you pass--sha256; leaf_hashequals SHA-256 of0x00followed by the leaf payload;- the audit path folds from that leaf, at its index, in a tree of
tree_sizeleaves, up to the root the signed tree head names; - the signed tree head's Ed25519 signature verifies under the context
eformogi-sth-v1:against the published organization key; - the family's own signature over the leaf bytes verifies under the context
eformogi-receipt-sig-v1:against the family public key carried in the proof, when the receipt was signed with a family key; - that family key's
key_idis thedid:keyyou get by deriving one from the public key shipped alongside it, so a proof cannot name one key and hand you another.
Checks 1, 5 and 6 are skipped when their input is absent, and a skipped check is not a pass. Any failure exits non-zero and says which step failed. A proof whose leaf is newer than the latest signed tree head reports pending and exits non-zero rather than guessing.
It also prints one advisory note, which never changes the verdict: whether the family's key was established before or after the receipt was issued. Later is common and legitimate. issued_at is when the family sealed the record, so a record sealed before the family had an account, and every family's first signed receipt, will show a key that came afterwards. Treat it as context, not as a red flag.
What it does not check: whether the family later withdrew the receipt, which is only visible online today, and whether the signed tree head you hold is the one the rest of the world sees. For the second, compare the head against the public anchor repository yourself. That comparison is the whole reason the log is anchored outside our database.
Get a proof and run it
Every verification page at eformogi.com/verify/<receipt id> carries a Download proof (JSON) link under the verdict. Save that file, then:
# the whole check, offline, on the file you downloaded
node verify-offline.js tr_8e59bb9641e8-proof.json
# same thing, with the hash of the document you are actually holding
node verify-offline.js tr_8e59bb9641e8-proof.json --sha256 <64 hex characters>
# pin the organization key yourself instead of trusting the copy in the script
curl -s https://eformogi.com/.well-known/did.json > did.json
node verify-offline.js tr_8e59bb9641e8-proof.json --jwk did.json
You can also fetch a proof directly, without the page:
curl -s "https://eformogi.com/api/receipt-proof?id=tr_8e59bb9641e8" > proof.json
node verify-offline.js proof.json
Exit codes: 0 verified, 1 a step failed or the leaf is not yet covered by a head, 2 bad usage or an unreadable file. Node 18 or newer, because Ed25519 comes from node:crypto.
Run the conformance suite
run-vectors.sh feeds every vector in vectors/ through the verifier and exits non-zero if any computed verdict differs from the verdict the vector expects. Vectors that expect a failure pass when the verifier correctly refuses, which is the half of a conformance suite that is easy to skip and expensive to skip.
sh run-vectors.sh # one line per vector
sh run-vectors.sh -v # every step of every vector
Chapter 10. Conformance says what each vector covers and what passing them entitles you to claim. Short version: say "passes N of the M vectors in version X", name the version, and do not say "certified", because nobody issues that today.
Write your own
The specification, not this file, is normative. If your implementation and this verifier disagree, at least one of them is wrong, and the chapters decide which. Reuse the file under Apache-2.0, port it to your language, or read it as a worked example and throw it away. The vectors are the part that keeps everyone honest.
Files
| File | What it is |
|---|---|
verify-offline.js | The reference verifier. One Node file, no dependencies, no network access. |
run-vectors.sh | Runs every vector below through the verifier and exits non-zero on any mismatch. |
LICENSE-Apache-2.0.txt | Apache License 2.0, the license of the verifier and the vectors. |
LICENSE-CC-BY-4.0.txt | Creative Commons Attribution 4.0, the license of the specification chapters. |
NOTICE.txt | Copyright holder and the two licenses, in a dozen lines. |
Conformance vectors (22)
Every file below is served from this site and is covered by the Apache-2.0 license above. Fetch the files you need, or fetch one vector and run it on its own.
| Vector | Chapter | What it pins | Expected |
|---|---|---|---|
v01-canonical-json.json | 02 | canonical JSON: sorted keys, no whitespace, arrays in order, unicode preserved | must verify |
v02-leaf-payload-exact-bytes.json | 02 | leaf payload normalizes id and sha to lowercase, issued_at to ISO ms, keys sorted | must verify |
v03-leaf-payload-rejects-ecr.json | 02 | leaf payload refuses a non-loggable prefix (ecr_ is institutional-only, never anchored) | must refuse |
v04-cr-derivation.json | 02 | cr_ id derives from {kind:course, student_name, course_title, term, grade, credits as posted string} | must verify |
v05-att-derivation-ws-target.json | 02 | att_ id derives from the public signer payload plus the lowercased email as email_hash_input | must verify |
v06-ws-derivation-v1.json | 02 | ws_ v1 id derives from {entry{kind,title,detail,evidence_url,at}, issuer, person_id, source_entry_id}; issued_at is outside the hash | must verify |
v07-wn-derivation.json | 02 | wn_ id derives from {kind:weekly_note, author:kid, week_of, body}; author is fixed to kid | must verify |
v08-tr-derivation.json | 02 | tr_ id derives from the whole issuance payload {student, courses, issued_at, issuer_user, person_id} | must verify |
v09-kb-binding-frozen.json | 04 | kb_ key binding: did:key derivation, frozen canonical payload, kb_ id and sha256 | must verify |
v10-family-sig-valid.json | 04 | family Ed25519 signature over eformogi-receipt-sig-v1: + anchored leaf bytes verifies against the kb_ public key | must verify |
v11-family-sig-context-stripped.json | 04 | a signature over the raw leaf without the context prefix MUST NOT verify (domain separation is load-bearing) | must refuse |
v12-org-countersig-valid.json | 03 | organization countersignature over eformogi-witness-countersig-v1: + the att_ leaf bytes verifies against the did:web key | must verify |
v13-merkle-roots-frozen.json | 05 | RFC 6962 Merkle roots over 1, 2, 3, 7, 8 and 16 leaves (0x00 leaf domain, 0x01 node domain, split at largest power of two) | must verify |
v14-inclusion-valid.json | 05 | inclusion proof for leaf 3 of a 7-leaf tree folds to the root (RFC 9162 verification) | must verify |
v15-inclusion-wrong-index.json | 05 | the same audit path claimed for a different leaf index MUST NOT verify | must refuse |
v16-sth-valid.json | 05 | signed tree head: Ed25519 over eformogi-sth-v1: + canonical JSON of the seven signed fields | must verify |
v17-sth-tampered-tree-size.json | 05 | a tree head with a tampered tree_size MUST NOT verify | must refuse |
v18-proof-four-steps-valid.json | 06 | the four-step inclusion-proof algorithm: artifact hash, leaf hash, inclusion, STH signature | must verify |
v19-proof-tampered-path.json | 06 | a proof with one flipped audit-path element MUST fail the inclusion step | must refuse |
v20-vc-eddsa-jcs-2022.json | 07 | W3C VC DataIntegrityProof / eddsa-jcs-2022: sha256(JCS(proof options)) || sha256(JCS(document)), signed raw with Ed25519 | must verify |
v21-proof-with-family-signature.json | 06 | a real proof carrying issuer_signature: leaf hash, inclusion, tree head, family signature, and the did:key derivation of the family key id | must verify |
v22-proof-family-signature-tampered.json | 06 | the same proof with one character of the family signature flipped MUST be refused | must refuse |