Skip to content

Latest commit

 

History

History
69 lines (49 loc) · 11.4 KB

File metadata and controls

69 lines (49 loc) · 11.4 KB

ADR-0002 — Reference-Implementation Code Layer for privacy-architecture

Status: Accepted · Date: 2026-07-24 · Grilled: 2026-07-24 (grill-with-threat-model, 9 findings folded below) Supersedes: — · Superseded-by:

Context

Subrosa's thesis is privacy must be structural, not procedural, and its README closes with Hughes' "Cypherpunks write code. This repo is that code, with citations." Today that is rhetorically true and literally false: the repo is prose (taxonomy, skill instructions, a dissolution map) plus one stdlib validator. The suite is moving into commercial use (privacy-intermediary consulting, Canada + global); the artifact a client engagement deploys — and the differentiator against paper-privacy consultants — is the build layer. The privacy-architecture skill has a complete design workflow but nothing runnable: every ARCH-DISSOLVES / ARCH-SATISFIES / ARCH-MANDATES claim is currently discharged by citation, never by test.

Forces: ADR-0001 (dependency diet; no unjustified third-party code); the suite's auditability ethos (Zimmermann 1991 — "the only way to be sure is to have the source code reviewed"); operator stack is Rust-first for security tooling; CI already enforces repo invariants (tools/validate.py) and should extend to code, not fork into a second quality regime; reference code that readers copy into production is a foreseeable misuse channel.

Decision

Add a code/ Rust workspace of small, single-purpose reference crates ("primitives"), governed by these rules. The grill (below) forced the central reframe: a pure library is not a system, and does not flip a taxonomy tag. A crate provides a primitive that a compliant system must correctly wrap; the ARCH-* tag is discharged by the deployed system, never by the crate. Crate docs state the system preconditions the primitive depends on and does not itself guarantee.

  1. v1 scope = the four primitives that are honestly deliverable as pure libraries (library-pure — no networking, no storage, no async). The grill (findings 2, 3, 6, 7) showed the other three are system properties a library cannot hold; they are deferred to ADR-0003, not half-built:
    • subrosa-tokenize — per-context HMAC-SHA256 tokenization with typed domain separation (P5; A10 pre-export pseudonymization). Clean library scope.
    • subrosa-crypto-erase — envelope encryption where key destruction renders ciphertext unrecoverable in this process. Precondition it cannot guarantee: no plaintext/key copy escaped to swap, snapshot, or a caller buffer (finding 2). Provides the primitive for A7/Art. 17; the system must establish memory hygiene.
    • subrosa-commithiding commitments: H(domain ‖ value ‖ blinding) where a high-entropy blinding lives only in the opening and is never stored with the commitment. Constructor refuses a short blinding (finding 8 — a salted hash over an enumerable domain with the salt alongside is reversible; that construction is explicitly rejected).
    • subrosa-aggregate — k-threshold suppression; constructor enforces a k-floor and refuses to build below it (finding 5's floor made structural, not tested-after-the-fact).
    • Deferred to ADR-0003 (system-scoped, with their preconditions named): lockbox, retain, auditlog. These require a mediated single read path / atomic store scheduling / an out-of-band anchor respectively — none expressible in a pure library. Shipping them as libraries would manufacture the "false assurance with an audit trail" this suite exists to condemn.
  2. Every crate cites its statute, and states its system preconditions. The crate-level doc names the taxonomy record(s) and axis cell(s) its primitive serves (same taxonomy/regulatory-taxonomy--*.md reference format the validator checks) and a # Preconditions section listing what the surrounding system must guarantee for the ARCH-* claim to hold.
  3. Every crate ships positive tests and a real negative test — the negative test asserts a specific rejection (an Err variant or a #[should_panic]), never a tautology. deny_* naming is necessary but the validator only counts presence; test substance is a human review-gate, explicitly (finding 5).
  4. Dependency allowlist checked against the lockfile, not direct deps (finding 4). code/ALLOWED-DEPS.md enumerates every permitted crate — direct and transitive — with justification per ADR-0001; the validator parses Cargo.lock and fails if any package (transitive included) is absent. Initial direct set: sha2, hmac, zeroize, subtle, getrandom — RustCrypto-family / minimal, exact-pinned; transitive closure enumerated in ALLOWED-DEPS.md at lock time.
  5. Secrets are a typed capability, not a naming convention (finding 1). Key material is held in a Secret type: no Debug/Display/Clone, bytes reachable only through an explicit expose() that returns a short-lived borrow, zeroize-on-drop. Public APIs accept/return Secret, never bare [u8; N]/Vec<u8> for key material, so a raw nameable key binding does not arise in ordinary use. Semgrep denial of logging on exposed bytes is defense-in-depth, not the primary control.
  6. Reference, not product. Each crate's docs open with: unaudited reference implementation for teaching and engagement scaffolding; not published to crates.io in v1; production use requires an audit.
  7. ZK is explicitly out of v1. Circuits (age predicates, proof-of-innocence) enter via a future ADR that picks the proving stack on its own supply-chain merits.
  8. Enforcement files are not self-certifying (finding 9). tools/validate.py warns when a crate-code change and an enforcement-file change (ALLOWED-DEPS.md, .semgrep.yml, the workspace Cargo.lock) land in the same commit, and requires each ALLOWED-DEPS entry to carry a non-empty justification. A solo repo cannot fully separate author from gate-keeper; this makes the coupling visible rather than silent, and CODEOWNERS marks the enforcement files.

Considered Options

  • Rust workspace of pure library crates — chosen. Matches operator stack (Solana/security tooling), gives real zeroization semantics, keeps the dependency surface auditable, compiles in CI in seconds at this scale.
  • TypeScript first — rejected for core: no reliable secret-wiping in a GC runtime, weaker misuse-resistance story for crypto hygiene; may later be added as bindings/ports, not the reference.
  • Full ZK stack first (noir/arkworks) — rejected for v1: heaviest dependency and audit burden in the whole design space, violates the ADR-0001 diet before the layer has proven its shape; deferred to its own ADR.
  • Stay prose-only (status quo) — rejected: leaves the suite's central claim untestable and the consulting differentiator unbuilt.

Consequences

Easier: engagements get deployable scaffolds tied to statutory cells; the dissolution map's tags gain executable evidence; CI's "the repo enforces its own invariants" story extends from cross-references to cryptographic properties; the portfolio shows engineering, not only research. Harder: a Rust toolchain enters CI; the workspace must be maintained through dependency advisories; the "reference, not product" boundary must be policed forever (a reader will paste this into production).

Security Considerations & Mitigations

Output of grill-with-threat-model (2026-07-24), 9 findings, each with its resolution. Findings 2/3/6/7 were category errors and reshaped the Decision (v1 dropped from seven crates to four; three deferred to ADR-0003).

  1. Secret defined by name → rename to evade (F1). Resolution: secrets are a typed capability (Secret type, rule 5), not a regex over field names. Bare key bytes do not arise as a nameable binding in normal use; Semgrep on exposed bytes is defense-in-depth only.
  2. crypto-erase claims erasure it cannot own — swap/snapshot/caller-buffer copies survive (F2). Resolution: crate no longer discharges Art. 17. It provides in-process key destruction; docs' # Preconditions name the memory-hygiene guarantees the system must supply. Tag reframed to "serves A7/Art. 17," not "dissolves."
  3. lockbox can't enforce a query layer it doesn't own (F3). Resolution: deferred to ADR-0003. A pure library cannot hold a system-mediation property; shipping it as a library would be false assurance.
  4. Direct-dep allowlist ignores the transitive closure (F4). Resolution: validator checks Cargo.lock membership (rule 4), transitive packages enumerated in ALLOWED-DEPS.md; --locked in CI.
  5. deny_* naming is self-graded (F5). Resolution: negative tests must assert a specific Err/should_panic; validator counts presence only and the ADR states test substance is a review-gate, not a lint-gate. k-floor is enforced in the constructor (structural), not merely tested.
  6. retain two-clock TOCTOU vs the store (F6). Resolution: deferred to ADR-0003; atomicity is a store property, not a library one.
  7. auditlog tamper-evident ≠ tamper-proof; write-capable insider rewrites the chain (F7). Resolution: deferred to ADR-0003; requires an out-of-band anchor. The Decision-298 insider is inside a pure library's trust boundary — stating otherwise would misrepresent the control against its own motivating threat.
  8. commit over low-entropy domains is a lookup table (F8). Resolution: subrosa-commit is a hiding commitment with high-entropy blinding kept only in the opening and never stored beside the commitment; constructor rejects short blinding. Salt-stored-alongside is explicitly out.
  9. Enforcement is in-repo and self-modifiable in the same PR (F9). Resolution: validator warns on same-commit code+enforcement changes and requires non-empty ALLOWED-DEPS justifications; CODEOWNERS marks enforcement files. Residual risk acknowledged for a solo repo — made visible, not eliminated.

Standing mitigations: reference-not-product disclaimers (rule 6, no crates.io); internally-generated nonces and typed contexts so domain-separation misuse is unrepresentable; constructor-enforced floors so toy parameters fail to build.

Enforcement

  1. CI: new code job in .github/workflows/validate.ymlcargo test --workspace --locked + cargo clippy --workspace -- -D warnings.
  2. tools/validate.py extensions (stdlib-only, as today): (a) every code/crates/*/src/lib.rs cites ≥1 taxonomy/regulatory-taxonomy--*.md record whose file resolves and contains a # Preconditions doc section; (b) every package in code/Cargo.lock (direct + transitive) appears in code/ALLOWED-DEPS.md, and every ALLOWED-DEPS entry carries a non-empty justification; (c) every crate contains ≥1 deny_* negative test (presence only — substance is a review-gate, per F5); (d) warn when a commit touches both crate source and an enforcement file.
  3. Semgrep: code/.semgrep.yml — deny Debug/Display/Clone derive on the Secret type and deny println!|dbg!|eprintln!|format! receiving expose()d bytes; run in CI (defense-in-depth per F1, not the primary secret control).
  4. CODEOWNERS: ALLOWED-DEPS.md, .semgrep.yml, Cargo.lock, and tools/validate.py are marked as enforcement files (F9).

Dogfood (ADR-0010)

Accepted only after cargo test --workspace and cargo clippy -D warnings pass locally and tools/validate.py is green with the new checks — run this session, not asserted.

Output Schema Impact

Schema Change Type: none