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

09. Threat model

Draft 0.1. Status vocabulary: built (in the reference implementation and pinned by a trust-gate test where one is named), partial (a control exists but does not close the class), planned (named, not built). Every row cites the implementing file. Red-team history is in TRUST_REPORT.md.

9.1 Assets and adversaries

Assets: a learner's record (private content on device and in the encrypted vault), the public receipts and their log, the keys, and the trust a receiver places in a verify page. Adversaries: an issuer who inflates, a stranger who fabricates witnesses, a forger who mints receipts under someone else's name, the operator itself (or someone with its database access) rewriting history, anyone who steals or loses a key, and the law where it binds a records office holding minors' data. The grading rule the operator uses: impact if breached times whether the door is open now, not current data volume; a records office holding minors' data is reputation-fatal on one breach at any scale.

9.2 Issuer inflation

The family is the issuer. It can title a course anything, grade it anything, and select any tier chip.

controlstatusfile
Tier chips are rendered as the issuer's description; mentor and receiver chips are muted with "issuer-declared" unless the ledger corroborates them (a co-signature on file; a matched institutional domain)builtapi/verify-receipt.js (course_signature_rollup), frontend/src/pages/VerifyReceiptPage.jsx (TranscriptTierBlock)
Course rows are individually addressable (cr_) and can carry an outside witness's signed_grade; disagreement is rendered beside the family's grade, never hiddenbuiltapi/publish-course-receipts.js, api/publish-attestation.js, tests/test-course-attestations.js
"Source-verified" requires a signature from a matched institutional domain; since no mailbox proof exists, domain_matched is never granted, so every receiver chip renders as issuer-declaredpartial (honest by construction, empty in practice)api/publish-attestation.js (signer_verification = 'self_declared'), tests/test-attestation-no-unverified-domain.js
The verify page states that a self-issued transcript's hash does not prove third-party evaluationbuiltVerifyReceiptPage.jsx honesty blocks, chapter 06 section 6.6
Sums, never averages; no GPA rank surfaces that would reward inflationbuilttests/test-sums-never-averages.js, tests/test-roots-mechanisms.js
Witness identity levels 1 to 3 so a co-signature can carry weightbuilt (2026-09-07)shared/witnessIdentity.js, api/publish-attestation.js, api/admin-witness-identity.js, tests/test-witness-identity.js, chapter 03, section 3.9
Organization issuers accountable per learnerplanned (issuer rail)chapter 01, section 1.7

9.3 Fabricated witnesses

Attestations are minted by a public endpoint by design (a witness must not need an account).

controlstatusfile
A course-row attestation requires a live, family-minted invitation token bound to that exact cr_ (not revoked, under 60 days)built (ruling 2026-09-04)api/publish-attestation.js, api/request-attestation.js, api/resolve-witness-token.js
The target receipt must exist; orphan attestations cannot inflate "witnessed by N"builtapi/publish-attestation.js
No institutional-domain credibility from a claimed email addressbuilt (red-team 2026-08-13, #4)tests/test-attestation-no-unverified-domain.js
Unbound signers render but never move the dissent gauge or wall badges; anonymous signers collapse into one bucketbuilt (red-team 2026-09-02, #1)api/verify-receipt.js (boundSigner), tests/test-attestation-gauge-integrity.js
The issuer of the target may hide a poisoned attestation; the count of hidden rows is disclosedbuilt (red-team 2026-09-02, #1)api/revoke-family-receipt.js, tests/test-attestation-tombstone.js
Self-attestation on a course row is refused when the signer's email hash equals the issuer's; parent-as-witness on a work sample is labeledbuiltapi/publish-attestation.js (pin #9), tests/test-witness-phase-c.js
Free text is firewalled for PII and injection before hashing or storagebuiltshared/firewall.js, policies/firewall.md, tests/test-firewall.js
Rate limits: 10 attestations per minute per IP; disputes 3 per minutebuiltshared/rateLimiterDurable.js, tests/test-ratelimit-coverage.js
Organization countersignature timestamps the ceremony under its own contextbuilt, flag-gated (EFG_SIGNED_RECEIPTS)shared/witnessCountersig.js
Dispute rail with durable record and human review; nothing public changes on filingbuiltapi/dispute-attestation.js, tests/test-attestation-dispute.js
Mailbox proof (level 1), licensed-role lookups, identity verification, reputationplanned (W7)chapter 03, section 3.9
Witness-initiated revocation of their own attestationplannedchapter 03, section 3.7

9.4 Receipt forgery

controlstatusfile
Publishing a transcript requires a signed-in account; issuer_user is pinned from the verified token, never the bodybuiltapi/publish-transcript.js
Course rows claiming a tr_receipt_id must be posted by that transcript's ownerbuilt (red-team 2026-08-13, #3)api/publish-course-receipts.js, tests/test-course-receipt-ownership.js
Every id is re-derived server-side from the canonical hash domain; a client-claimed id that does not match is refusedbuiltapi/publish-course-receipts.js, api/publish-attestation.js, api/publish-wall.js
Same id with a different hash is refused (409); the family must re-issue to change contentbuiltall api/publish-*.js
Canonicalization parity between client and server is gate-testedbuilttests/test-canonicalize-parity.js, tests/test-merkle-log.js
Family Ed25519 signature binds the exact anchored leaf; verified in the receiver's browserbuilt, flag-gated (EFG_SIGNED_RECEIPTS)shared/familySigning.js, frontend/src/utils/receiptSignature.js, tests/test-family-signing.js
Verify payloads never carry identity beyond the printed surface; output keys are snapshot-testedbuiltapi/verify-receipt.js, tests/test-verify-receipt-shape.js
Anti-enumeration: 10 verifies per minute per anonymous IP; keyed callers on a hashed-key bucket with an IP backstopbuiltapi/verify-receipt.js
Security events on denied forgeries feed a daily sentinelbuiltapi/_lib/securityEvents.js, api/cron-security-sentinel.js, tests/test-security-sentinel.js
Guest-issued receipts are unsigned, unrevocable, and never reach the public ledger from publish-transcriptbuilt (disclosed limitation)api/publish-transcript.js (401 for guests)

9.5 Platform capture

The operator, or anyone with its database, is an adversary the design must survive.

controlstatusfile
The log is append-only at the database: UPDATE, DELETE, and TRUNCATE are refused by trigger; tree-head rows are immutable except a single null-to-value anchor writebuiltsupabase/V34_RECEIPT_LOG.sql
Domain-separated hashing (0x00 leaf, 0x01 node, 0x02 chain) defeats second-preimage forgery of inclusion proofsbuiltshared/merkleLog.js, tests/test-merkle-log.js
The daily signer recomputes the root at the latest head and the latest externally anchored head and refuses to sign over divergencebuiltapi/cron-tree-head.js (self-audit), tests/test-tree-head-cron.js
Every newly covered leaf is re-validated (grammar, canonical round trip, hash, chain) before signingbuiltapi/cron-tree-head.js (validateLeaves)
Heads and non-PII leaf payloads are anchored in a third-party git history whose timestamps the operator cannot rewritebuilt (8 of 8 heads anchored)api/cron-tree-head.js (anchorToGitHub, sweepUnanchoredHeads), TRUST_REPORT.md
A receiver verifies inclusion offline against the published key with zero trust in the serverbuiltspec/verifier/verify-offline.js, scripts/verify-receipt-offline.js, tests/test-receipt-proof-endpoint.js
Key establishment is public (kb_); a swapped or backdated family key is visible in the log; keys never self-healbuiltshared/familySigning.js, tests/test-family-signing.js
The published organization key is the same object the signer's drift guard checks; the DID document cannot drift from the signing keybuiltshared/ed25519.js, api/did.js, tests/test-ed25519-issuance.js
A family can export its full record and leavebuiltapi/data-export.js (client-side decrypt), settings export/import round trip
Revocation is complete: purging forwarded institutional copies and storage blobs, not just the rowbuiltshared/forwardingPurge.js, shared/consentStoragePurge.js, tests/test-rule-1-revocation-completeness.js, tests/test-revocation-storage-purge.js
Server-side plaintext readers of the vault ratcheted from seven to one allowed ciphertext passthroughbuilttests/test-vault-plaintext-ratchet.js, docs/H1_STAGE2_ZERO_KNOWLEDGE_ADR.md
Consistency proofs between heads; a second, independent anchor (OpenTimestamps)plannedchapter 05, section 5.6
An organization-key rotation statement format in the anchor repositoryplannedchapter 04, section 4.2
Independent monitors running the daily audit outside the operatorplanned (the anchor files make it possible today; nobody runs one yet)

9.6 Key loss and compromise

keylosscompromisestatus and file
organization keysigning stops; nothing already anchored is affectedforged heads and countersignatures are detectable against third-party anchor timestamps; recovery is rotation plus a signed statementdrift guard built (shared/ed25519.js); rotation planned
family signing keycannot be lost by the family (escrowed); a key that fails to unwrap publishes unsigned and alerts, never re-mintsan attacker with the server KEK could sign under any family; this is the cost of escrow and is disclosed on every verify pagebuilt and disclosed (shared/familySigning.js, custody: escrowed_by_eformogi); family-held signing keys planned
vault DEK (Stage 1)none (server wrap)an attacker with database plus VAULT_KEK reads vaultsbuilt; disclosed as "encrypted at rest" (shared/vaultEnvelope.js)
vault DEK (per-family cutover)lost passkey plus lost recovery code is unrecoverable by anyonethe operator cannot read the vault; a stolen device with an unlocked session canbuilt, flag VITE_ZK_CUSTODY, opt-in ceremony (frontend/src/services/vaultKeyCustody.js, api/vault-key.js, tests/test-zk-key-custody.js)
local device keythe cloud copy is the recovery path (guests keep plaintext for this reason)a non-extractable CryptoKey cannot be dumped from IndexedDB databuilt (frontend/src/services/webCrypto.js, tests/test-vault-local-encryption.js)
witness tokenexpires at 60 days; single use; family can revokea forwarded link is honored and the mismatch is recorded, never blockedbuilt (api/resolve-witness-token.js, api/revoke-witness-request.js)
organization API keyshashed at rest; shown onceheader-only; never in query stringsbuilt (shared/orgApiAuth.js, tests/test-legacy-key-hashing.js, tests/test-no-query-secrets.js)

US-first. GDPR is out of scope for this draft: the erasure and export mechanisms exist, but no EU representative, DPA, or transfer mechanism is claimed.

regimehow the Record meets itstatusfile
FERPAThe family, not a school, is the records office of record, so the operator does not hold "education records" of an educational agency on the family side. Where an institution receives family data, it does so by explicit field-level consent with a receipt, and revocation purges every copy, including forwarded ones.built controls; formal opinion plannedapi/institutional/consent.js, api/consent-receipt.js, shared/forwardingPurge.js, tests/test-rule-1-revocation-completeness.js
COPPAThe account holder is a parent or guardian; children do not hold accounts. Learner-authored surfaces (wn_, wl_) publish only a family-approved excerpt or curated list, carry no view tracking, and are never witnessed. No behavioral advertising exists. Parent-consent mechanics for any direct collection from a child under 13 are flagged for founder and counsel review.partial; attorney review plannedapi/publish-weekly-note.js, api/publish-wall.js, api/counselor/add-student.js (FOUNDER-REVIEW note), api/_lib/send-email.js
NY Education Law 2-dApplies to educational agencies and their third-party contractors. The family-direct Record is neither. The counselor caseload (an institutional surface) is where 2-d obligations could attach if a school district became a contracting party; no district contract exists.partial (surface documented; no contract; counsel review planned)frontend/src/pages/PrivacyPage.jsx, api/counselor/*
Deletion and pseudonymizationAccount deletion hard-deletes vault, links, credentials, and non-consent audit rows; consent artifacts are pseudonymized (user id nulled, salted hash, identifiers stripped) so "did this institution ever have permission?" stays answerable without a person attached. Log leaves are non-PII and survive.builtapi/delete-account.js, supabase/V34_RECEIPT_LOG.sql
Minors' data breach (the binary risk)RLS enabled with owner-scoped or deny-all policies on every family-data table, verified empirically; ownership enforced in code on service-role handlers; a red-team sweep cadence with a permanent antibody test per confirmed findingbuilt (Aug 14 verification); systemic pattern partial (service-role default)shared/redTeamCadence.js (LAST_SWEEP = 2026-09-02), .claude/skills/red-team-sweep/, TRUST_REPORT.md
Honest claims (UDAP exposure)"Encrypted at rest" not "we cannot read it"; escrow stated; per-family custody stated only from server proof; "verifiable" never "accepted"built and gate-pinnedtests/test-zk-key-custody.js (section 9), frontend/src/pages/SovereigntyPage.jsx, frontend/src/pages/SecurityPage.jsx

9.8 Privacy leakage through the protocol itself

vectorcontrolstatus
log leavesfour non-PII fields by construction; the signer refuses any other shapebuilt
witness emailhashed on the attestation; plaintext only on the family's own request rowbuilt and disclosed
verify-view trackingfires on tr_ and ws_ only (the family is notified someone verified); never on wn_, wl_, cr_, kb_; self-views excludedbuilt
wall badgescounts of distinct signers only; never names or hashesbuilt
receipt-proof and tree-headspublic, cached, no PII; rate-limited against enumerationbuilt
anchor repositorynon-PII by construction; receipt rows stay deletable while anchors survivebuilt
document extractiona transient server-side AI processing path for every family, including after custody cutover: the uploaded document is processed for field extraction and not retained; disclosed on the security page and declared as the one exception in the zero-knowledge ADRbuilt and disclosed (docs/H1_STAGE2_ZERO_KNOWLEDGE_ADR.md, frontend/src/pages/SecurityPage.jsx); on-device extraction planned as an opt-in

9.9 What this does not prove

A threat model is a list of doors and locks, not a proof that no door was missed. The two red-team sweeps to date found real holes in the live system (11 of 21 raised on 2026-08-13; 20 of 25 raised on 2026-09-02) and closed them with antibodies; the next sweep is due on or about 2026-10-02. No external firm has reviewed this system yet (TRUST_REPORT.md).