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

03. Witness attestation

Draft 0.1. Reference implementation: api/request-attestation.js, api/resolve-witness-token.js, api/revoke-witness-request.js, api/witness-requests.js, api/publish-attestation.js, shared/witnessCountersig.js, shared/witnessIdentity.js, api/admin-witness-identity.js, api/_lib/witness-history.js, api/verify-receipt.js (attestation folds, rollup, identity levels), api/revoke-family-receipt.js, api/dispute-attestation.js, frontend/src/pages/VerifyReceiptPage.jsx (tier labels, identity chips, honesty blocks). Vectors v05, v12.

3.1 What a witness is

A witness is an adult outside the family who puts their name on one specific receipt: a work sample (ws_) or a single course row (cr_). The witness produces an attestation (att_), which is itself a public, content-addressed, logged receipt. A witness never edits the family's record; disagreement is recorded beside the family's claim and rendered openly (pin #6: disagreement is rendered, never hidden). The operator never validates the role or the relationship a witness claims (pin #2: roles and relationships are self-declared); the operator records and discloses facts about the ceremony, states how far the signer's identity was checked (section 3.9), and, when enabled, countersigns that the ceremony happened.

3.2 Request

A family asks a specific person to witness a specific receipt.

POST /api/request-attestation (authenticated as the family account)

fieldtyperequiredmeaning
ws_receipt_id or cr_receipt_idreceipt idexactly onethe target; it MUST already exist in the public ledger
witness_emailstringyeslowercased; stored in plaintext on the request row so the invitation can be sent; never copied to the public att_ row
notestring, at most 500nothe family's message to the witness
competencyobjectno; cr_ targets onlya competency-evidence claim (chapter 08, section 8.7): {jurisdiction, taxonomy_version, attribute_id, evidence_kind, evidence_ref, rubric_level: null}. Normalized by the taxonomy module and refused when it names anything the jurisdiction has not defined (an unknown attribute, kind, jurisdiction, or version; a non-null rubric_level; an empty or over-long evidence_ref); evidence_ref passes the firewall like a statement. Stored on the request row, outside every hash domain; refused with 503 until its column exists. The same claim is accepted from an issuer on a course row it declared (api/issuer/declare-competency.js).

Rules (MUST):

3.3 Landing and lifecycle

The witness opens /witness/<token>. GET /api/resolve-witness-token?token= (public) resolves the token to the minimum context needed to sign: target_kind (work_sample or course) and the surface fields of the target (for a course: course_title, term, the family-stated grade, credits). When the invitation carries a competency claim (section 3.2), the response also carries competency (re-normalized through the taxonomy on the way out; a stored claim the taxonomy no longer recognizes resolves as an ordinary course ask), competency_attribute_name, competency_evidence_kind_label, portrait_source_line, and grade_optional: true; otherwise competency is null and grade_optional is false. No write happens on resolve.

statusconditionHTTP on resolve
pendingnot fulfilled, not revoked, younger than 60 days200
signedfulfilled_at set200 with fulfilled: true and fulfilled_at; the page shows the request as already signed. Single use is enforced at publish time, not at resolve.
withdrawnfamily revoked the link (revoked = true)410
expiredolder than 60 daysrefused

The family lists its own requests (GET /api/witness-requests) and may revoke a pending one by token or by target id (POST /api/revoke-witness-request); ownership is enforced against requester_user from the verified session.

The signing page never pressures the person it invited (pin #4). No scheduled job writes to an attestation request or contacts the person it invited, so an unsigned invitation is never chased; the only scheduled read is a count of pending invitations for the operator's own queue. A request that is ignored simply expires at 60 days. The operator MUST NOT add a reminder sequence, a countdown, a completion meter, or a score of the signer to this surface.

3.4 Signing

POST /api/publish-attestation (public; no account for the witness)

fieldtyperequiredin att_ hashmeaning
id, sha256stringsyes(derived)the client's derivation; the server re-derives and refuses a mismatch
ws_receipt_id or cr_receipt_idreceipt idexactly oneyestarget
signer_namestring, at most 120yesyesself-declared
signer_rolestring, at most 120yesyesself-declared
signer_organizationstring, at most 200noyesself-declared
signer_relationshipstring, at most 200cr_ only, requiredyesfree text such as tutor weekly 18mo; never fact-checked
signed_gradestring, at most 32cr_ only; required unless the presented invitation carries a competency claim for this exact row, in which case it MAY be absentyes (null when absent)the grade the witness attests to; MAY differ from the family-stated grade. A null grade is a Portrait evidence signature: the witness signed what they saw, not the grade, and it does not count as a grade co-signature (section 3.6)
statementstring, at most 1000noyesfree text
signer_emailstringnoas email_hash_input (lowercased)stored only as signer_email_hash = sha256(lowercase)
signed_atISO 8601no (server default now)nothe leaf timestamp
request_token64 hexrequired for cr_ targets; optional for ws_nobinds the signing to an invitation

Server rules (MUST):

  1. Firewall first. statement and signer_relationship are scanned by the deterministic firewall (shared/firewall.js, policies/firewall.md) before anything is hashed or stored; a statement carrying an SSN, a card number, or an injection payload is refused with a receipt of what was blocked. This validates the text's safety, never the claim's truth.
  2. Target must exist in the public ledger. Attestations on unknown ids are refused so nobody can inflate "witnessed by N" against a receipt that was never issued.
  3. Course rows require an invitation. A cr_-targeted attestation MUST present a live token minted by the issuing family for that exact cr_ (not revoked, not older than 60 days). A walk-up signature on a transcript-grade row is refused (ruling of 2026-09-04). Work samples accept walk-up signatures from /verify/ws_<id>.
  4. No self-attestation on course rows (pin #9). When both the signer's email hash and the transcript issuer's email hash are known and equal, the cr_ attestation is refused with self-attestation refused. On a work sample the same condition is allowed and labeled (signer_is_issuer: true): a homeschool parent witnessing their own child's project is legitimate life, co-signing their own grade assignment is not.
  5. Domain credibility is not granted from a claimed address. signer_verification is the field about the address the signer typed, and it is self_declared for every signer; domain_matched is reserved for a future mailbox-proof flow and MUST NOT be set from an unverified email (red-team finding, 2026-08-13). The identity level of section 3.9 is a different field asking a different question, and it reads the address the family invited, proven by the single-use emailed token, never the typed one.
  6. Idempotent. Re-posting the same attestation returns existed: true; the same id with a different hash is a 409.
  7. The identity level is recorded, not claimed. The server writes witness_identity_level and witness_identity_source from the invited address alone, under the conditions in section 3.9. A level supplied in the request body is ignored.
  8. A token is single use, and a signature is never relabeled. A presented token whose invitation is already fulfilled is refused unless it was fulfilled by this very att_ (an idempotent re-post). When the posted att_ already exists and the presented token names an unfulfilled invitation for the same target, that invitation is marked fulfilled by the existing att_ only if the existing signature is younger than ten minutes and has not yet answered any other invitation (the case of a first invitation left open because the signer typed a different address); otherwise the invitation stays open. A signature that already answers one invitation is never claimed by a later token, so two attributes on one course take two signatures: the signing page pre-fills the statement of a competency ask with the attribute name and the evidence, editable, so a witness answering two asks does not produce identical words by default. Whether the grade is optional is read from the presented invitation alone (rule 7's posture): nothing in the request body can make it optional.

Ceremony observations recorded outside the hash, disclosed on every verify surface, never used to block:

fieldvaluesmeaning
witness_bindingemailed_link, directhow the witness reached the signing page
signed_from_invited_addresstrue, false, nullwhether the signer's email hash equals the invited address (null when unknowable)
signer_is_issuertrue, false, nullparent-as-witness label on ws_
signer_verification, signer_email_domainself_declared, nullsee rule 5
witness_identity_level0, 1, 2how far the signer's identity was checked; 3 is never stored, see section 3.9
witness_identity_sourcedomain, staff free text, nullhow the level was reached; the free text stays internal and only domain or manual is published
witness_identity_verified_attimestamp or nullserver clock at the moment of the check, never the client's signed_at
org_countersig, org_key_idmultibase signature, did:web:eformogi.com#key-1section 3.5
hidden_by_issuer_attimestamp or nullsection 3.7

After a successful insert the attestation is appended to the transparency log with issued_at = signed_at, and, if a token was used, the request is marked fulfilled and the family is notified.

3.5 Organization countersignature

When family signing is enabled (EFG_SIGNED_RECEIPTS), the operator countersigns the ceremony: Ed25519 with the published organization key over 'eformogi-witness-countersig-v1:' + <the att_ leaf payload> (chapter 02, section 2.5). This asserts "the operator's infrastructure processed this attestation at this time and its bytes have not changed." It asserts nothing about who the witness is. The context prefix is distinct from the family receipt context and the tree-head context so no signature can be replayed as another kind (vector v12; a context-stripped signature MUST fail).

3.6 Tiers as displayed

Each course row carries a tier chosen by the family in the builder. Verify surfaces render the tier as a chip with these labels, and MUST describe them as the issuer's description of the source, never as a rank the operator awards:

tier valuelabelmeaning as rendered
selfFamily-statedthe family wrote it down
familyFamily-gradedthe family graded against a rubric
mentorCo-signedan outside teacher, coach, mentor, or supervisor put their name on it
receiverSource-verifiedthe family states the claim came directly from the source (a registrar record, a score report)

Ledger corroboration rules (api/verify-receipt.js, lookupTranscript):

3.7 Revocation, hiding, and the withdrawn state

actoractmechanismpublic effect
familyrevoke a receipt it issued (tr_, cr_, ws_, wn_, wl_)POST /api/revoke-family-receipt {receipt_id}; sets revoked_at; tr_ cascades to its cr_ rowsverify returns 200 {verified: true, revoked: true, kind: "revoked", original_kind, id, hash, issued_at, revoked_at}; content withheld; never a 404 (a 404 would be a memory hole)
family (issuer of the target)hide an attestation on its receiptsame endpoint with an att_ id; sets hidden_by_issuer_atthe row stops rendering and counting; the verify payload discloses hidden_attestation_count
familywithdraw a witness linkPOST /api/revoke-witness-requestthe token resolves 410; a signing attempt with it is refused
witnessrevoke their own attestationplanned (gap: no witness-initiated revocation path exists; a witness who disputes their own signature uses the dispute rail)
anyonedispute an attestationsection 3.8nothing public changes
nobodyrevoke a kb_key establishment is a historical fact; rotation is the honest operation (chapter 04)

Guest-issued receipts (no bound account) cannot be revoked because no caller can prove issuance. An attestation whose target was revoked still verifies as the signer's own claim, with target_revoked: true and the target's content withheld.

3.8 Dispute

POST /api/dispute-attestation {receipt_id: "att_...", statement, email?} (public, 3 per minute per IP). A dispute is an allegation, not a verdict: it is written durably to the audit log and notifies a human; nothing on the public page changes until a person reviews it. The operator does not gatekeep evaluator legitimacy and does not auto-moderate.

3.9 Identity levels (0 to 3)

Shipped 2026-09-07. shared/witnessIdentity.js holds the single definition of the four levels and their names; api/publish-attestation.js earns the automatic step, api/admin-witness-identity.js is the human path, api/_lib/witness-history.js counts the history, and api/verify-receipt.js publishes the result on the att_, ws_, cr_, and tr_ verify surfaces.

A level says how far the signer's identity was checked, and nothing else. It is a fact about the check the operator performed. It is not a rank of one signer against another, not a score of the work, and not a claim that the role the signer typed is real: names, roles, and relationships stay self-declared at every level, including level 2 (pin #2). Every surface MUST describe a level in those terms.

levelnamehow it is reachedthe sentence a receiver reads
0emailthe default for every signature; nothing beyond an addressEmail address only. No further identity check on file.
1verified domain or licenseautomatically at signing time when the address the family invited belongs to a school, district, government, or military domain (levelForEmail, the one rule in shared/institutionalDomains.js); or by an Eformogi admin who looked up a professional license or registry entry and recorded where and whenInvited at a school or organization address and signed through the link sent there. When the source is a lookup: a professional license or registry lookup was recorded by Eformogi staff.
2verified identityonly by an Eformogi admin who checked an identity document, for counselors of record. The assertion is stored, never the document: the endpoint accepts an attestation id, a level, and a source string, and refuses a body carrying any other keyIdentity checked against an identity document by Eformogi staff. Only the record of the check is kept, never the document.
3established historyderived at read time, never stored. effectiveLevel(stored_level, history_count) returns 3 once the signer's signer_email_hash has co-signed HISTORY_LEVEL_THRESHOLD (5) or more other receipts from the family-invited addressEstablished history: a count of prior co-signatures on file over time.

Rules (MUST):

  1. Only levels 1 and 2 are settable (ADMIN_SETTABLE_LEVELS), by POST /api/admin-witness-identity under the admin secret, with a required free-text source naming where and when the check happened. Every successful call is logged to audit_log as witness_identity_set, and the response reports whether the row landed. Level 0 is the default and is never set. Asking for level 3 is a 400.
  2. Nothing about level 3 is stored. It is computed on every read from ledger rows the receiver could count themselves. The stored level is left alone, so an admin-set 1 or 2 survives a signer whose history has not reached the threshold, and a signer who passes the threshold reads as 3 without any row being rewritten.
  3. The level sits outside the att_ hash, like the V31 and Phase C observations, in witness_identity_level, witness_identity_source, and witness_identity_verified_at. A missing column reads as 0 and null rather than failing a verify.
  4. The automatic step reads the invited address only, and only when the token's target is this exact receipt, the invitation is not revoked, it is younger than 60 days, and the signer did not type a different address (signed_from_invited_address is not false). The signing endpoint is public, so an address the signer merely claims can never raise a level; the antibody is tests/test-attestation-no-unverified-domain.js.
  5. The staff source string is internal. The public payload carries exactly one of three fixed words, domain (earned from the invited address), proven_address (the signer's own address, proven once; rule 7), or manual (recorded by staff). The free text stays in the row and in audit_log, and nothing else ever reaches /verify.
  6. The history count is a count, not a ranking. It counts distinct other receipts co-signed from the family-invited address, capped at 99. A walk-up signature never increments anyone's history and reads 0 for itself, even under a real teacher's address hash.
  7. Signed from a proven address (ratified by the founder on October 22, 2026, named by him). A signer who clicks the emailed claim link (POST /api/signer-bind-claim) proves that one mailbox once; the device that clicked holds the signer page's token. A later signature made from that device presents the token (signer_page_token on POST /api/publish-attestation), and the server records witness_identity_source = proven_address and signer_verification = proven_address, uses the page's signer_hash as the signature's identity when no address was typed, and sets witness_identity_verified_at to its own clock. The level is 1 only when the proven address's domain passes the same levelForEmail rule the invited address does, and only then is the domain published (signer_email_domain, the mailbox-proof the V31 posture waited for); otherwise the level stays 0 with this source. A typed address that differs from the page's identity means the token is ignored. A strong invitation (rule 4) wins over this path. The signature accrues to the signer's page the way a strong invitation's does. The claim link records the proven address's domain on the page (signer_pages.proven_domain, migration 2026-10-22-proven-address.sql); the address itself never travels or is stored. Possession of the token is the proof, so the signing page's done screen lets the signer stop a device from signing from the address whenever the device holds a token (a shared tablet, a borrowed phone); the signatures already made are unchanged. Stopping also retires the token on the server (POST /api/signer-page-forget gives the page a fresh token and keeps everything accrued to it; added October 23, 2026), so a copy taken from the device stops working everywhere; the signer's next claim link, or next signature from an emailed invitation, carries the new token. The signer's hand-off code is keyed on the identity, never on the token, so a printed link survives the rotation. The sentence a receiver reads: "Signed from a proven address. The signer proved this address by email once; no further identity check on file." at level 0, and "Signed from a proven school or organization address. The signer proved this address by email once." at level 1.

What api/verify-receipt.js publishes for each signature:

fieldvalues
identity_level0 to 3, after the level-3 derivation
identity_sourcedomain, proven_address, manual, or null
identity_verified_attimestamp or null
witness_history_count0 to 99

The four fields sit inside the signer object on the att_, ws_, and cr_ payloads, beside the self-declared name and role. On a tr_ payload they are one entry of the course's co_signatures[] with no signer wrapper, because a transcript row publishes how far each signer was checked without republishing who the signer said they were: the cr_ page holds that, and the transcript links to it. The one exception is the transcript's Portrait section (chapter 08, section 8.7): each signed evidence row there carries the signer's self-declared name, role, and organization beside the four fields, because a row of evidence without the adult who saw it is hollow. That publishes nothing the att_ page does not already publish; the course rollup itself still carries no name, email, or hash.

The stored column is witness_identity_level; the published field inside the signer object is identity_level. They are the same fact under two names. signer_verification (section 3.4, rule 5) is neither an alias nor a synonym: it reports the typed address, it stays self_declared, and it never moves because of a level.

The ladder adds no personal data to the public record. It publishes the level, the two-word source, the timestamp of the check, and the count. It never publishes the address, the address hash, the document, or the staff note.

Display (frontend/src/pages/VerifyReceiptPage.jsx): each co-signature renders a chip reading Identity · <the level's name>, with the source line ("Source: the address the family invited." or "Source: recorded by Eformogi staff.") and, at two or more, the sentence "This signer has co-signed N other records." A transcript prints the plain-sentence legend once for every level that appears on it rather than repeating a sentence on each row, and identical chips on one course collapse into a single chip carrying a count.

3.10 Worked example

The public demo attestation on /verify/ws_demo_robotics (a static fixture; no real student) has the shape a real one has:

{
  "id": "att_demo_coach",
  "kind": "attestation",
  "ws_receipt_id": "ws_demo_robotics",
  "signed_at": "2026-04-05T18:30:00Z",
  "signer": { "name": "Sarah Kim", "role": "Lead robotics coach", "organization": "Lincoln High FRC Team 4012" },
  "statement": "Maya was our drive team lead for the 2026 season. ..."
}

A real derivation (vector v05): signer Sarah Kim, role Lead robotics coach, organization Lincoln High FRC Team 4012, target ws_abc123def456, email S.Kim@LincolnHS.org (hashed as s.kim@lincolnhs.org) produces att_5d9b296a66cb.

3.11 What this does not prove

An attestation proves that someone who typed this name, role, and statement signed this exact receipt at this time, and, when countersigned, that the operator processed that signing then. It does not prove the signer is who they say they are, holds the role they claim, or evaluated the work competently. The verify page says exactly this: receivers should verify each signer's identity independently; the operator does not gatekeep evaluator legitimacy. An identity level (section 3.9) narrows that gap without closing it. It reports how far the address behind the signature, or the identity behind the address, was checked, and it says nothing about whether the claimed role is real or the judgment sound. A "Co-signed" chip still means a named person signed.