Eformogi
The Record: protocol draft 0.1 (CC BY 4.0; verifier Apache-2.0)

06. Verification

Draft 0.1. Reference implementation: api/verify-receipt.js, api/receipt-proof.js, scripts/verify-receipt-offline.js (copied as spec/verifier/verify-offline.js), frontend/src/utils/receiptSignature.js, frontend/src/pages/VerifyReceiptPage.jsx. Vectors v18, v19.

6.1 Two paths, one algorithm

A receiver holds an artifact (a printed transcript, a PDF, a work sample, a course row) and a receipt id. Verification answers, in order: is this id a real receipt; do the bytes I hold match what was sealed; who signed; was it publicly logged; what tier of claim is this; has it been withdrawn.

pathneedsproves
onlinethe id, a browser, no accountsteps 1 to 6 with the operator's server answering steps 2 and 5
offlinea proof file downloaded earlier (or handed over with the artifact), the published organization key, the reference verifiersteps 1, 2 (artifact hash), 4 (signatures, with the leaf bytes from the proof), 5 (inclusion), without any network and without trusting the operator's server

The proof file is what the Download proof (JSON) link under the verdict on /verify/<id> saves, and what GET /api/receipt-proof?id=<id> returns. The reference verifier and the conformance vectors are published at /spec/verifier under Apache-2.0, so obtaining the offline path takes no account, no clone, and no permission.

A receiver who does not trust the online answer MUST be able to reproduce it offline from public inputs. That is the design test for every field on the verify payload.

6.2 The algorithm, step by step

Step 0. Classify the id. Match ^(tr|cr|ws|wn|wl|att|kb)_[a-f0-9]{12}$ (case-insensitive). An id with no known prefix falls through to the legacy lookups (event receipts by uuid, consent receipts, plan receipts) and otherwise 404s. ecr_ and tl_ never resolve here (chapter 02).

Step 1. Hash integrity (the receiver's own step). Compute SHA-256 over the artifact you hold, exactly as the issuing surface computed it (the whole transcript issuance payload for tr_; the canonical hash-domain object per chapter 02 for the others). Compare with the receipt's hash. A mismatch means the bytes you hold are not the bytes that were sealed. No server can do this step for you; the verifier's --sha256 flag performs the comparison against the logged leaf.

Step 2. Existence and state. Online: GET /api/verify-receipt?id=. verified: true means the id resolves in the operator's ledger; it does not mean the claims are true. If the payload carries revoked: true, the family withdrew it: stop, and treat the artifact as withdrawn (the id, kind, dates, and hash remain so a receiver holding the link learns the truth rather than hitting a memory hole).

Step 3. Identity of content. The receipt id MUST equal the prefix plus the first 12 hex of the receipt's sha256. A receiver holding the artifact MAY re-derive both from the canonical hash domain (vectors v04 to v08) and MUST treat a mismatch as a forged or corrupted receipt.

Step 4. Signatures.

A verifier MUST NOT fail a receipt because key_established_at is later than issued_at. issued_at is the moment the family sealed the record, and it legitimately precedes the key in at least two ordinary cases: a guest issues a record before having an account and republishes it later with the original issued_at, and a family's first signed receipt is stamped before its signing key is minted. The ordering is worth showing a receiver and worth nothing as a verdict, so an implementation SHOULD report it as an advisory note. A tolerance window is the wrong fix; the gap is weeks. The reference client (frontend/src/utils/receiptSignature.js) verifies against the served public_jwk only; the kb_ join is done server-side when the payload is built.

Step 5. Log inclusion. Fetch GET /api/receipt-proof?id= (or use the proof file you hold), then run the four checks the reference verifier runs, in order, all of which MUST pass:

  1. (optional, with --sha256) the artifact hash you computed equals sha256 inside leaf_payload;
  2. leaf_hash == SHA256(0x00 || leaf_payload);
  3. the audit path folds from leaf_hash at leaf_index in a tree of sth.tree_size leaves to sth.root_hash (chapter 05, section 5.4);
  4. sth.signature verifies under 'eformogi-sth-v1:' against the organization key.

Two further checks run when the proof carries issuer_signature: the family signature of step 4 over the same leaf bytes, and the did:key derivation of key_id from public_jwk. Both are skipped, not failed, when the receipt was not signed with a family key. The key-establishment ordering prints as an advisory note and never changes the verdict. Vectors v21 (a real proof with a valid family signature: all five checks pass) and v22 (the same proof with one character of that signature flipped: refused) pin this.

status: "pending" means no head covers the leaf yet: the verifier exits 1 and says so; try again after the next daily head. Vector v18 passes all four; v19 (one flipped path element) fails at check 3.

Step 6. Tier display. Render the family's tier_counts chips with the labels of chapter 03, section 3.6, muting any mentor or receiver chip the ledger does not corroborate and suffixing it "issuer-declared". Render every attestation with its self-declared fields, its ceremony observations, and its dispute door. Where a witness's signed_grade differs from family_stated_grade, render both.

Step 7. Say what was proved. The tr_, cr_, and ws_ verify surfaces MUST carry a "what this verification means" paragraph and a "what this verification does NOT mean" paragraph (section 6.6). Gaps recorded against the reference implementation: the kb_, wn_, and wl_ pages do not carry the paired paragraphs, and a witnessed ws_ page replaces the "does NOT mean" heading with "Trust ladder: rung 2 reached." followed by the caveat text; extending the pair to every surface and keeping the heading stable when witnessed is planned.

6.3 Verify payloads by kind

Common to every found receipt: verified: true, kind, id, issued_by: "Eformogi". Never returned: student or family identity beyond what the family printed, org or counselor names or emails, narrative text, file paths, or raw metadata blobs.

kind (id prefix)fields
family_transcript (tr_)issued_at, hash, student_name, issuer, program, grad_year, course_count, total_credits, tier_counts or null, courses[] or null (cr_receipt_id, course_title, term, family_stated_grade, credits, signed_count, dissenting_count, source_signed_count, co_signatures[] (always present; [] for a course with no outside co-signer) of {identity_level, identity_source, identity_verified_at, witness_history_count}, newest first, carrying no signer name, email, or hash), course_signature_rollup or null (total, co_signed, with_dissent, source_backed), issuer_signature or null
course_receipt (cr_)issued_at, hash, student_name, course_title, term, family_stated_grade, credits, tr_receipt_id, transcript_eligible: true, attestations[], hidden_attestation_count, issuer_signature
work_sample (ws_)issued_at, hash, title, entry_kind, issuer, occurred_at, has_evidence, attestations[], hidden_attestation_count, issuer_signature
weekly_note (wn_)hash, author: "kid", week_of, issued_at, excerpt, transcript_eligible: false, issuer_signature; no attestations field at all
wall (wl_)curator, pinned_ids[], pinned_count, attestation_counts {ws_id: distinct signers}, snapshot_sha256, created_at, updated_at, transcript_eligible: false, issuer_signature
attestation (att_)hash, ws_receipt_id or cr_receipt_id, ws_title or course_context {course_title, term, family_stated_grade, student_name}, target_revoked, signed_at, signer {name, role, organization, relationship, verification, verified_domain}, signed_grade, statement, witness_binding, signed_from_invited_address, signer_is_issuer, org_countersig, org_key_id
key_binding (kb_)hash, key_id, public_jwk, established_at, custody: "escrowed_by_eformogi"
revoked (any family prefix)revoked: true, original_kind, hash, issued_at, revoked_at
event_receipt (uuid)event_type, event_category, occurred_at, surfaced_to_family
consent_receipt (uuid)type, categories[], status, occurred_at, plus allowlisted non-identifying facts per type
plan_receipt (legacy)grad_rules_version, generated_at, depth_label, status, ai_used

Attestation entries inside attestations[] carry the same signer and ceremony fields as the attestation kind, minus the target context.

6.4 Exact error semantics

GET /api/verify-receipt:

statusbodywhen
200{verified: true, kind, ...}found (including kind: "revoked")
400{error: "id required"} or {error: "id too long"}missing id or longer than 200 characters
404{verified: false, id, message} with a per-prefix message (No Eformogi transcript receipt with this id., ... work-sample receipt ..., ... Sunday Note ..., ... wall ..., ... key-binding receipt ..., ... course receipt ..., ... attestation ..., or the generic No Eformogi receipt with this id.)unknown id; also when a prefix's table is missing in a fresh environment
402{error: "Monthly verification quota exceeded", plan, included, used, hint, upgrade_url}keyed organization over quota, only when enforcement is on (shadow mode today)
405{error: "Method not allowed"}non-GET
429rate-limit responseanonymous: 10 per minute per IP (anti-enumeration); keyed organization: 60 per minute per key plus a 120 per minute per IP backstop
500{error: "Lookup failed"}every lookup path errored

Headers on keyed calls: X-Verify-Quota-Plan, X-Verify-Quota-Limit, X-Verify-Quota-Used. A present-but-invalid organization key is never an error: it falls back to public attribution so a bad key can never break a free verify. self=1 marks the issuing device viewing its own receipt and suppresses view counting and notification; it can only undercount.

GET /api/receipt-proof:

statusbodywhen
200the proof object (section 6.4.1)covered leaf
200{status: "pending", leaf_index, next_sth, leaf_payload, leaf_hash, issuer_signature}leaf newer than the latest head
400{error: "id required"} or {error: "id too long"}
404{error: "no log entry for this id"}no leaf, or the log table absent
500{error: "log inconsistent with published tree head"}leaf count under the head does not equal tree_size (refuse rather than fabricate)
500{error: "proof lookup failed"}other

Reference verifier exit codes: 0 verified or verdict matched; 1 a step failed, the proof is pending, or a vector's verdict mismatched; 2 usage error or unreadable input.

6.4.1 The proof file

This is the whole offline input. It contains no personal data by construction: the leaf grammar is four fields (chapter 02), and the signature block is the public half of a key whose establishment is already a public kb_ receipt.

fieldtyperequiredmeaning
receipt_idstringyesthe receipt this proof is for, lowercased
leaf_indexintegeryesposition of the leaf in the log, zero-based
tree_sizeintegeryesleaf count of the tree the head covers; equals sth.tree_size
leaf_payloadstringyesthe exact canonical bytes that were hashed into the log, verbatim, not re-serialized
leaf_hashstringyes64 hex; MUST equal SHA-256 of 0x00 followed by leaf_payload
audit_pathstring[]yesRFC 6962 audit path from the leaf to sth.root_hash, each 64 hex
sthobjectyesthe signed tree head, verbatim as signed, plus signature: version, log_id, tree_size, root_hash, chain_hash, timestamp, key_id, signature
issuer_signatureobjectnopresent only when the receipt was signed with a family key: sig (multibase base58btc), key_id (did:key), alg (Ed25519), context (eformogi-receipt-sig-v1:), public_jwk, kb_receipt_id, key_established_at, custody
did_documentstringyeswhere to fetch the organization key for step 4 of the inclusion checks

A pending response carries status: "pending", leaf_index, next_sth, leaf_payload, leaf_hash, and issuer_signature when present. It is enough to check a family signature immediately; it is not enough to prove inclusion, and the verifier refuses rather than implying otherwise.

The sth object MUST be the bytes that were signed, with the signature appended. An implementation that rebuilds it from typed database columns will re-serialize timestamps and break every offline check, which is why the reference implementation stores and returns the signed body verbatim.

6.5 Online versus offline capabilities

checkonlineoffline
artifact hash equals sealed hashyesyes (--sha256)
id is a real receiptyesinferred from inclusion
withdrawn stateyesno (planned: a revocation leaf or a signed revocation list; today revocation is only visible online)
family signatureyes, in the browser, against the served public_jwkyes: the proof carries issuer_signature, and the reference verifier checks the signature and the did:key derivation of key_id (resolving the kb_ receipt offline needs its own proof file and is planned)
organization countersignatureyesyes
log inclusionyesyes
head is anchored externallyanchor_github on the headcheck the anchor repository's history yourself
tier corroboration, dissent, hidden countsyesno (these are ledger folds served online)

6.6 What this does not prove (the verify page's own words)

The transcript page: "The family issued this transcript through Eformogi. The SHA-256 above was computed at issuance over the entire transcript content. If the printed document you are reading matches the hash, no field has been altered since the family sealed it. The family, not the school, is the records office of record." And: "This is a self-issued, family-attested transcript. The hash proves the bytes haven't changed since issuance; it does not, by itself, prove the underlying claims were evaluated by a third party. Receivers who need third-party-evaluated or issuer-of-record records should ask the family for the linked evidence. The per-course signature chips show who put their name on each row rather than promoting any row to a rank Eformogi cannot award."

The work-sample page, unwitnessed: "This is a self-attested work sample. The hash proves the entry hasn't changed since sealing; it does not, by itself, prove the work was evaluated by a third party." Witnessed: "Each signature is hashed against this same receipt id; signatures cannot be silently altered. Receivers should verify each signer's identity independently; Eformogi does not gatekeep evaluator legitimacy."

The course page: "The grade above is what the family wrote down. Each co-signature below is a separate signed claim from a third party who reviewed the course; where their attested grade differs, both are rendered openly."

An implementation of this specification MUST carry equivalent text on every verify surface and MUST NOT describe a Record as "verified" without the qualifier of what was verified.