Document

The audit, version 1.1

Version 1.1 — version 1.0 plus the 9 September scope-corrections addendum, appended, nothing deleted.

BB2G Elections System — Full Audit

Scope: 23 repositories, 1,700 files, 144,915 lines of non-vendored code, 15,355 lines of Markdown Read at: commits present 27 August 2026 · Prepared for: Kristen Hall, Born Between 2 Generals, LLC Companion deliverable: BB2G-Master-Specification-v3.md

Detailed reports: audit/01-spec-corpus.md · audit/02-crypto-tokens.md · audit/03-build-consolidation.md · audit/04-identity-runtime.md


The short version

The doctrine is genuinely good. "Verify hard, then sever," the seven principles, the refusal to build a person-finder, the insistence that a flag means please verify and never excluded — that is a better set of commitments than most production election systems operate under, and it is written down clearly enough to hold people to.

The implementation does not yet enforce it. Four things stand out:

Everything below is evidence for those, plus what is genuinely working — which is more than the list above suggests.


Findings by severity

CRITICAL

C1 · The hash-chained audit log can be rewritten end to end, and verify() still returns ok: true. The Ed25519 signing key lives in the same directory as the log it signs (strongroom/src/log/signer.mjs:77, src/vault/custodian.mjs:213), and the only anti-truncation control is a sidecar file sitting beside the log (src/log/chain.mjs:255-263). A working proof of concept — recompute every hash, re-sign every entry, rewrite log.head.json — is at audit/poc/forge.mjs. Before and after both report { ok: true, seq: 2, count: 3 }, with a recorded vote changed from B to A. "Tamper-evident, never immutable" in the code means detectable only by an outsider who independently recorded a head — and nothing in the corpus records one. → v3: LOG-1 through LOG-7, KEY-8.

C2 · Six Merkle implementations, two mutually incompatible tree shapes. MBLTS-MERKLE-V1 duplicates the last node; RFC 6962 promotes it. Same domain separation, different shape, therefore different roots at every non-power-of-two size. ballottrail.bornbetween2generals.com and safe-elections.bornbetween2generals.com cannot verify each other's roots. The only implementation that handles both correctly is custody-layer/src/lib/merkle.mjs:6-28 — in a static explainer site. → v3: LOG-1, ONE-2, ADR-001.

C3 · The registrar, token service and ballot box run in one process. verified-vote/tools/serve_election.py:242-264 collapses all three into a single process over one SQLite file, with an in-memory credential→token map, over cleartext HTTP. That map is the anonymity wall the entire architecture rests on. Whatever this file is called, it is a working artifact that reconnects voter to ballot. → v3: SEP-1 (delete rather than harden), SEP-2.

C4 · The corpus is not buildable as delivered. Zero repositories declare a dependency on another. Coupling is by $HOME sibling-checkout convention — including ~/bb2g-custody and ~/bb2g-house, two required repositories that are not among the 23. Combined with zero CI in any repository, the only gate preventing the JavaScript and Python crypto implementations from silently diverging runs on one laptop, or not at all. → v3: ONE-4, ONE-5.

C5 · Binding online voting is proposed without a threat model that survives it. No normative protocol covers malware-resistant cast-as-intended verification, credential loss, session hijack, availability, or device compromise. verified-vote/docs/VERIFIER-SPEC.md:213-223 states plainly that voter intent cannot be established without voter-verified paper. The prior corpus admitted deepfake and off-camera coercion as residual risks in a footnote and then continued as though it had not. → v3: PRO-1, PRO-2, and §9 in full.

HIGH

H1 · A real Ed25519 private key is committed to git. strongroom/state-dbg/custodian.key, commit 99366f5. Its public half matches custodian.json exactly. .gitignore covers state/ but not state-dbg/. It is the audit-log signing key. This is the only genuine secret in history across all 23 repositories — everything else reads from ~/.secrets or the environment — and election-integrity/engine/demo-key.json is a legitimate build-gated fixture, not a leak. → Rotate now, purge from history, extend .gitignore.

H2 · Math.random mints ballot serials. platforms/safe-elections/index.mjs:48,57. That serial becomes the ballot token and the voter's tracking code. It is the only such use in the engine; other modules argue explicitly against it. → v3: LOG-8.

H3 · Break-glass approvals replay forever. approvalMessage signs only {platform, token, justification} — no nonce, no expiry, no request identifier — and requestedBy is outside the signature (engine/breakglass.mjs:40-52, core.mjs:316). One captured approval works indefinitely. → v3: KEY-7.

H4 · Zero MFA, 2FA, TOTP, WebAuthn or passkey code in 23 repositories. All 20 matches are prose or requirement tables. Two documents say so honestly: bb2g-election-command/docs/security/SECURITY_ARCHITECTURE.md:22 — "There is no login." The only working authentication in the entire portfolio guards marketing PDFs. Delta to the stated goal: 100%. → v3: MFA-1 through MFA-5.

H5 · Government-ID and video verification is a mock. verified-vote/src/blindfold/verification/providers.py:79-81MockRemoteNotary, whose own docstring says it "proves nothing about anyone's identity." No RON vendor, no capture, no liveness detection. election-integrity's "verification sessions" resolve on a coin flip (engine/election.mjs:91). Delta to the goal: roughly 95%. → v3: IDV-1 through IDV-8.

H6 · The four-tier isolation model is not enforced by anything. The systemd unit execs blindfold.registrar.serve, which does not exist. nftables.conf uses undefined variables. The reproducible build references three missing files, so EXPECTED_MEASUREMENT can never be produced. And the attestation gate passes an all-zero report_data while printing "no nonce bound — replayable" next to PASS (verify_attestation.py:122-125). The correct implementation is already written elsewhere in the same repository (engine/attest.mjs:68-75) and was never wired in. → v3: CON-5, CON-6, ATT-3.

H7 · The browser "cast-or-spoil" is not a Benaloh challenge. verified-vote/web/vote.html:374-388 alert()s the JavaScript variable it just encrypted. It proves nothing about what was transmitted, so there is no client-side integrity check and no software independence. → v3: EV-1, EV-2.

H8 · The voting client has weaker security headers than the marketing sites. verified-vote/web/vercel.json has no CSP and no HSTS; purely static explainer repositories have both. No Subresource Integrity anywhere. → v3: EV-4.

H9 · verified-vote signing keys are ephemeral and its ledger is unsigned. Keys are generated per process (token/issuer.py:66) with no persistence path, so a restart invalidates every token ever issued. verify_chain() (token/ledger.py:153-166) accepts a full re-chain, and inclusion_proof() returns the entry rather than a Merkle path. → v3: KEY-1, KEY-2, LOG-9.

H10 · Retiring strongroom-engine deleted the only external-anchor implementation. RecordingAnchor existed at auditlog.mjs:175-217 in the retired repository. No surviving repository publishes a head to a witness; "anchored" everywhere in the live corpus means "a root was computed locally." → v3: ONE-6, LOG-2.

H11 · Under production settings the engine cannot tokenise or rotate at all. Both operations refuse, pending an HSM that was never built. Rotation exists and is correctly versioned, but is file-only. → v3: BB-4, KEY-3.

H12 · The registrar's reissue path documents its own double-token hole. token/registrar.py:119-137 — reachable, and the comment says so.

H13 · Twelve substantive contradictions across the spec corpus. Led by re-vote supersession versus double-vote rejection (full-voting-system-architecture.md:179-184 vs org-election-mvp-architecture.md:51-58), and by the Org MVP asserting in prose that "the Token Authority can identify voters" two paragraphs after its own table says it sees only blinded noise. All twelve are resolved in v3 §2.

MEDIUM

M1 · The one true whole-repo fork diverged bidirectionally. election-integritysafe-elections: 22 of 38 shared paths byte-identical. Coercion-resistant re-vote supersession (election-integrity/engine/election.mjs:238-250 plus test/revote.test.mjs) exists only in the fork its own README marks as a target, not deployed. The best coercion defence in the corpus is in the copy that isn't live.

M2 · Four copies of the governing specification. Two identical at 9,405 bytes, two identical at 9,445 bytes, one unique at 9,631 bytes. The unique copy is the corrected one — it replaces "anchoring" with "witness publishing" and adds the two-line definition of "anchored to." Three copies still carry the withdrawn vocabulary, and two would fail lockchain/build.mjs:1109's own naming gate.

M3 · "StrongRoom" names four different things. "LockChain" is a name three repositories re-implement independently (709 / 163 / 65 lines, zero shared bytes). ProofCore does not exist anywhere in the corpus. The genuine shared spine is Blindfold (Python) plus strongroom/engine; the actual de-facto spine is a copy-pasted static-site generator.

M4 · Two election-command implementations, 68 modules versus 70. Three MBLTS custody implementations, one a byte-for-byte lift. Seventeen distinct site build scripts.

M5 · Every repository has exactly one commit. No branch history, no review trail, no CI history anywhere in 23 repositories.

M6 · cryptography>=42 unpinned with no lockfile in the one repository that does real cryptography.

M7 · vault.bypass.test.mjs tests only a body edit (src/vault/index.mjs:78), not the realistic bypass path.

OBSERVATIONS


The three tensions worth naming

These are not defects. They are places where two of the stated goals pull against each other, and pretending otherwise produces a system that fails quietly.

Ballot secrecy versus end-to-end verifiability. Resolved the same way the corpus already resolved it — blind signatures and a homomorphic tally, so the voter verifies inclusion without anyone being able to open their ballot. This is the part of the design that is genuinely right.

Continuous key rotation versus permanent audit verifiability. A signature that must verify in 2036 needs its 2026 key discoverable. Resolved by epoch identifiers plus a witness-cosigned Key Transition Log: private halves are destroyed, public halves live forever.

Online voting versus software independence. Not resolvable with current technology, by anyone. Resolved by scope: no binding public online voting, ever, under this architecture. Online for enrollment, verification, custody, publication and audit; paper for the ballot of record in statutory elections.


This week

Next

Then


Addendum, 9 September 2026 — scope corrections

The audit covered 23 repositories selected as elections-related. A second pass across all 415 repositories found relevant work outside that set, and two statements above need correcting.

Correction 1 — "ProofCore does not exist anywhere in the corpus" (M3). True within the 23; wrong as stated. kristenslab/proofcore exists, with a coherent design: assertion classes carrying hard confidence ceilings, provenance on every claim, and the rule that agreement cannot promote an inference into a fact. It is documentation only — no implementation beyond the README — so the substance of M3 stands, but the name is not vapor.

Correction 2 — the post-quantum work was never absent, only absent from elections. quantum-safe, quantum, quantum-suite, quantum-lab and halo sit outside the 23. halo/lib/pqc.ts implements a genuine hybrid KEM using the IETF PQ/T combiner QSF_ml_kem768_p256 over ML-KEM-768 and ECDH P-256, taken from @noble/post-quantum rather than hand-rolled — the strongest single piece of cryptography in the portfolio. quantum-safe/HYBRID-PQC-PLAN.md correctly refuses to claim what isn't built, and its four posture states are the right model. What remains accurate: no elections component uses any post-quantum primitive, and the elections apps do not appear in the plan's migration table at all. Addressed by PQ-1 through PQ-9 in v3.1.

Addition — sentinel-id already solves problems this audit raised. It holds a protected-identity architecture with 93 passing tests: keyed one-way tokens with per-context key derivation, a reverse-map vault sealed to a public key so the minting service holds no reversal secret, 3-of-5 Shamir custody, break-glass requiring at least two distinct role classes, hash-chained audit on every operation, and k-anonymity with OpenDP accounting. That is a materially stronger custody and break-glass design than the elections corpus has, and its docs/CRYPTO_REVIEW.md — which states plainly that a brief written by the code's authors is not the audit — is the right template for GATE-1. Recommendation: derive packages/keyvault from it rather than writing it fresh.

What this changes about the "one build" finding. It sharpens it rather than softening it. The real cost of scattered builds was never disk space; it was that v3.0 specified a post-quantum posture and a custody model from scratch while working implementations of both sat in sibling repositories, invisible because they were filed under a different subject. Appendix B of v3.1 now names the adjacent systems explicitly so the next revision cannot repeat the omission.


Audit against the code as committed, not against intent. Every finding cites a file and line; the four detailed reports carry the quoted evidence. This is an engineering audit, not legal or security certification — recommendations GATE-1 and GATE-2 of the v3 specification remain necessary.