Skip to content

Fail closed on acceptance keys after #44 REA check (#53) - #54

Draft
FetchDoge wants to merge 22 commits into
mainfrom
launch/53-key-handling
Draft

FetchDoge wants to merge 22 commits into
mainfrom
launch/53-key-handling

Conversation

@FetchDoge

@FetchDoge FetchDoge commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Summary

REA round 5 fixes on launch/53-key-handling (continues from 6bed5ff):

  1. Silent rebind refused — nonempty durable store without commitment refuses startup; only empty stores may first-bind.
  2. Fail-closed default — commitment required unless ledger is explicitly ephemeral; file-backed SqliteLedger cannot opt out.
  3. Atomic SQLite first-boot — try_bind_meta INSERT OR IGNORE + re-read/compare; 20-trial multiprocess test.
  4. Corrupt meta.json refused — never treated as empty; killer test included.
  5. Harness K1 / r2 N-h — --allow-demo-key persists demo key under state dir at mode 0600; subprocess pytest for CI.
  6. One-time rebind — covers 48-byte and token_hex(32) (64-char) old keys; refused before, works after with right old key.
  7. SECURITY.md — keys are UTF-8 encoded bytes; nothing is decoded.

Evidence

  • Tip: see latest commit on branch
  • Suite: 463 passed, 1 skipped (plain and -O)
  • Mutant sweep: 31/31 killed
  • New r5 tests on 6bed5ff: 19 failed, 1 passed (empty-store happy path)

Test plan

  • python -m pytest -rs
  • python -O -m pytest -rs
  • python -m scripts.mutant_sweep_demo_key → 31/31
  • CI + forge on tip
Open in Web Open in Cursor 

cursoragent and others added 22 commits October 19, 2026 15:09
Reject whitespace-only keys; require --allow-demo-key (off by default) on
entry points; raise on invalid configured keys even with the demo flag.
Persist demo keys at <db>.acceptance_key (0600) in --db mode. Fix ledger
docstring and document sign_acceptance. Add subprocess entry-point tests.

Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
Open key files with O_NOFOLLOW; require a regular euid-owned 0600 file;
read exactly 32 bytes with EOF; create via O_CREAT|O_EXCL|O_NOFOLLOW and
fsync file+dir. Refuse to regenerate when the ledger is nonempty and the
key is missing. Log mode only after verification. Run forge on
pull_request with --allow-demo-key; make the durability harness demo-key
flag off by default.

Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
…N-b)

Map every open/read OSError (including EACCES) to ValueError so startup
exits with 'acceptance key error' and no traceback. Open with O_NONBLOCK
so a FIFO at the key path fails fast after the regular-file check.
Load the durable demo key once in server.py and pass the bytes into
ReleaseVerifier — drop the second load that only existed to log the mode.

Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
On first --db use, store HMAC(key, 'permit-acceptance-key-v1') in the
SQLite meta table; refuse to start on a commitment mismatch so a
same-length swapped key cannot silently take over. Separate commit so
the binding can be dropped independently of the OSError/FIFO/once-open
fixes.

Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
Replace skip-unless-root with hard fails: chmod-000 asserts non-root
euid, foreign-owner requires passwordless sudo -n chown nobody and
cleans up with sudo. Both fail on 70b0fc6 and pass on tip.

Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
lstat ownership before O_RDONLY so a real sudo chown nobody 0600
key hits the owner check instead of masking as EACCES. Keeps the
post-open fstat owner check; chmod-000 own-user path still maps
EACCES via _key_oserror.

Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
…eLedger

Regression from 2a1bbfe: --db --allow-demo-key pre-loaded the file key and
ignored PERMIT_ACCEPTANCE_KEY. Env wins when set (refuse empty / whitespace /
short); file is used only when unset. Move ensure_acceptance_key_commitment
into settlement.verifier and call it from ReleaseVerifier for every
SqliteLedger plus server.py --db. Rotation refuses startup (no rebind).

Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
…nk msg)

Add killer tests for each survived mutant and a sweep script that reapplies
them in-process (7/7 killed).

Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
One-flag change on matrix and -O jobs (Marc reviews at merge).

Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
Matrix/ -O stay -q -rs (skip reasons). Add a py3.12 -v step for the
chmod-000 and sudo-chown foreign-owner tests so CI logs show PASSED by name.

Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
- umask 0o277 so dropping fchmod fails mode 0600; sweep still 7/7
- DurableLedger meta.json + requires_acceptance_key_commitment; never
  silently skip; harness key A→B mismatch test
- Acceptance keys must be exactly 32 bytes (match key-file rule)
- README: CI uses -q -rs plus -v for key-file tests only

Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
Refuse silent rebind on nonempty stores without a commitment; require
commitment unless the ledger is explicitly ephemeral; bind atomically
via try_bind_meta (INSERT OR IGNORE + re-read/compare); refuse corrupt
meta.json instead of treating it as empty. Add one-time
rebind_acceptance_key_commitment for pre-exact-32 migrations.

Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
With --allow-demo-key and no env key, durability_harness stores the demo
key under durable_dir/acceptance_key (0600) so kill/reopen survives.

Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
Document one-time rebind for pre-exact-32 keys; SECURITY.md now states
keys are UTF-8 encoded bytes (nothing is decoded). Add
scripts.rebind_acceptance_key CLI.

Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
Silent rebind, ephemeral fail-closed, 20-trial concurrent first boot,
corrupt meta killer, harness K1 demo-key persist, rebind right/wrong
old key. Mutant sweep covers 31 demo-key + commitment mutants.

Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
CI-visible pytest that runs harness/durability_harness.py --kill-point K1
with --allow-demo-key and no PERMIT_ACCEPTANCE_KEY through kill/reopen.
Fails on 6bed5ff (ephemeral demo key); no ci.yml change needed.

Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
Both lengths are refused at startup before rebind (exact-32 / commitment
mismatch) and succeed after rebind with the right old key; wrong old key
still refused for each length.

Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
Co-authored-by: Marcus Richards <contact@Marcusrichards.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants