Drop misleading "or re-enrol" after permanent revoke; enrol refuses an
existing agent before spending the seed. checkpoint() refuses a currently
revoked/suspended signer. README (and DESIGN) test count updated to 156.
enrol_rekey shared the enrol: bucket and metered before auth, so junk
calls could lock out incident-response rekey. Use a separate rekey:
bucket and charge it only after a bad bootstrap secret, so a valid seed
always proceeds.
Permanent revoke and rekey retired the live pubkey without retaining it,
so historical checkpoints and off-box anchors became permanently
unverifiable. Add append-only agent_keys (public material only); retire
on revoke/rekey; verify against the fingerprint-named historical key.
Document a dedicated checkpoint-signer identity in the runbook.
In-flight verify/issue_code could insert a session or code after revoke
or rekey. sessions.resolve now refuses if the agent is revoked, has no
live key, or (agent sessions) the fingerprint no longer matches.
View codes store the issuing fingerprint and redeem refuses after
rekey/wipe; issue_code re-checks agent state. DESIGN matches the choke
point. Regression tests plant race-surviving sessions and stale codes.
Add requirements.txt (cryptography>=43.0.0,<45) and docs/PROD_RUNBOOK.md
requiring SIGIL_SEAL_PASS and SIGIL_ANCHOR on separate storage. Document
info findings R9/R12/R13 in DESIGN; leave R10 domain separation as a
versioned follow-up (not backward-safe).
claim_code now requires redeem_expires_at > now in the CAS UPDATE.
enrol_rekey shares the enrol: rate-limit bucket. spend_bootstrap and
sibling clear delete rows instead of retaining spent hashes, matching
the schema comment.
Plaintext working copy and export backup were creatable world-readable
under a 0700 temp dir. Write via os.open(mode=0600), fsync, unlink on
failure; pin temp dir 0700; remove export after flush. Document SQLCipher
as deferred — seal-on is not an encrypted DB while the process is open.
checkpoint() only checked that the signer name was enrolled, so a stranger
key could write a polluting checkpoint row. Refuse unless the private key's
fingerprint equals the enrolled fingerprint for that signer.
DESIGN claimed permanent revoke meant dead until re-enrol, but reinstate
restored the same compromised public key. Permanent revoke now wipes
pubkey/fingerprint; reinstate lifts ban flags only. Timed suspend keeps
the key. Compromise recovery may bootstrap+rekey while still revoked,
then reinstate. Regression tests cover reinstate-alone and timed path.
Unspent bootstrap secrets accumulated and remained usable for enrol_rekey
after enrol. put_bootstrap now spends prior unspent seeds for the agent
(one live secret), and successful enrol / enrol_rekey clear any siblings.
Regression tests cover leftover-after-enrol, one-live-secret, and
leftover-after-rekey.
Outstanding view codes survived revoke and rekey, so a pre-issued code
could mint a fresh human_via_agent session after compromise revoke or
key rotation. Invalidate unused codes when sessions are revoked (covers
revoke, suspend, and rekey), and refuse redeem when the agent is
currently revoked. Regression tests cover all three paths.
Tokens/codes/secrets were already digests; a stolen cleartext SQLite file
still leaked the agent roster, revocation state and audit history.
SealedStore / SIGIL_SEAL_PASS wraps on-disk bytes in AES-256-GCM
(PBKDF2-HMAC-SHA256, 480k iters). Working copy is plaintext only while
open (temp file); flush/close re-seals. Incremental path short of
SQLCipher page-level encryption.
11 new tests in test_sealed.py (stolen seal has no SQLite header / no
agent rows; wrong passphrase fails; works with off-box anchor). Suite
130 green.
Part of Quarry 18a5d762. Independent crypto review + platform glue remain.
A signed checkpoint inside the same database is still only as strong as that
file. Store(..., anchor_path=) / SIGIL_ANCHOR now writes the signed tip to a
filesystem path outside the DB (atomic tip file + append-only .log).
verify_external_anchors catches a rewrite even after in-DB checkpoints are
deleted. 8 new tests in test_anchor.py; suite 119 green.
Part of Quarry 18a5d762 (Sigil prod build). Do not merge to main yet:
sealed store and independent crypto review still open.
- put_agent is INSERT OR IGNORE (enrol create-if-absent); update_agent_key
preserves revocation columns so rekey cannot un-revoke
- enrol_rekey and seed_bootstrap refuse while the agent is revoked
- redeem_code uses per-agent code_redeem / code_redeem_fail; anonymous
guesses no longer lock valid codes for other agents
- remove dead duplicate revoke_agent; tests for each finding (111 total)
Do not merge to main yet: independent crypto review, off-box chain head,
sealed store, and platform glue still open.