From 53136cc38fe1086402a23c91c3a690f6bbdcd0e8 Mon Sep 17 00:00:00 2001 From: bigpickle Date: Mon, 5 Oct 2026 21:51:23 +0800 Subject: [PATCH] Document the second pass and what is still open --- README.md | 59 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 19e973c..4ccf60b 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,10 @@ python3 sigil.py code bigpickle /tmp/demo.key # hand a human a code python3 sigil.py redeem X3ZB-NTLG # -> human_via_agent token python3 sigil.py whoami "$TOKEN" # kind, scopes, may_act_as_agent python3 sigil.py events bigpickle # audit trail -python3 sigil.py verify-chain # is the log intact? +python3 sigil.py checkpoint /tmp/demo.key bigpickle # sign the audit head +python3 sigil.py verify-chain # chain + checkpoints +python3 sigil.py revoke bigpickle --reason "incident" # or --until UNIX +python3 sigil.py sweep # prune stale rows ``` ## Tests @@ -50,10 +53,10 @@ python3 sigil.py verify-chain # is the log intact? python3 -m unittest discover -s tests -v ``` -80 tests. They cover the refusals, not just the happy paths: replayed +106 tests. They cover the refusals, not just the happy paths: replayed signatures, wrong keys, expired nonces, single-use codes, scope escalation, and -the invariant below. `test_hardening.py` has one test per defect found in -review, so any of them regressing brings that defect back. +the invariant below. `test_hardening.py` and `test_production.py` have one test +per defect found in review, so any of them regressing brings that defect back. ## The invariant @@ -85,9 +88,14 @@ no extra capability. Waiting for a code does not eat into it — an earlier draf anchored the session to issue time, which quietly handed out less than promised. **Nothing is minted from a name.** `issue_code` takes an agent *session*, not an -agent id, and `verify_chain` exists so tampering is detectable. A function that -accepts a string and returns authority is how "who is this?" quietly becomes -unanswerable. +agent id. A human session requires a `GrantedCode`, which only a redeemed code +can produce. A function that accepts a string and returns authority is how +"who is this?" quietly becomes unanswerable. + +**Suspension and death are different statements.** `revoke` with no end time is +permanent; `revoke --until` is a pause that lapses on its own. An incident +response that ends should not need a re-enrolment, and a dead identity should +not come back because a clock ran out. ## Status @@ -96,6 +104,11 @@ A hardening pass closed the defects below. Each is now covered by a test. | Found in review | Now | |---|---| | `issue_code()` trusted an agent name, so it minted authority from a string | Takes an agent session; agent derived from it | +| Human sessions were mintable from a bare agent name | Requires a `GrantedCode`, constructible only by redeeming | +| Audit log recorded 4 of 8 code characters (20 bits left) | Logs a truncated digest instead | +| No `revoke` operation existed, only an unused column | `revoke` with optional end time; sessions killed, challenges dropped | +| Chain was unkeyed, so a database-level attacker could rewrite it | Signed checkpoints; `verify-chain` checks both | +| No busy timeout, so concurrent writers failed instantly | `busy_timeout`, WAL, rollback on error | | One-time code redeemable many times in a race (753 times in 200 trials) | Atomic compare-and-set; 300 races, one success each | | Bootstrap secret spendable more than once | Atomic compare-and-set | | Signed nonce verifiable more than once | Nonce claimed before the signature is judged | @@ -109,21 +122,25 @@ A hardening pass closed the defects below. Each is now covered by a test. Still open, and these are the reasons not to ship it yet: -- **No independent review.** Everything above is self-review. Self-review finds - the bugs you thought of. It does not find the ones you did not. This needs - someone else, ideally a cryptographer, before it guards anything real. -- **The chain is unkeyed.** Each event folds in the previous digest, so an - accidental edit or deletion is caught. An attacker with write access to the - database can recompute the chain. For a real commitment, checkpoint the head - digest somewhere the database cannot reach, or sign it. +- **No independent review.** Everything above is self-review, twice over. Self- + review finds the bugs you thought of; it does not find the ones you did not. + This needs someone else, ideally a cryptographer, before it guards anything + real. This is the blocking item, and the only one that code cannot close. - **The service around this is unwritten.** TLS, CSRF, session transport, secret - storage and the HTTP routes are not here. The library refuses to mint - authority from a name; a route that forgets to pass a session still cannot, - but that is only as good as the integration. -- **Revocation has no expiry semantics.** Revoking works; scheduled or - self-expiring revocation does not. -- **No key recovery.** Rotate and you lose the old sessions. There is no escrow, - which is a deliberate choice, not an oversight. + storage and the HTTP routes. The library refuses to mint authority from a + name, so a route that forgets to authenticate fails closed, but that is only + as good as the integration. +- **Checkpoints need somewhere to live.** A signed checkpoint inside the same + database is still only as good as the database. Real deployment wants the head + digest anchored off-box — published, or countersigned elsewhere — and this + does not do that for you. +- **No key recovery.** Rotate and the old sessions die, which is the point. There + is no escrow. Deliberate, not an oversight. +- **Rate limits are in-process.** They are per-`Store`, so several processes + behind a load balancer each get their own quota. Fine for a single writer, + wrong for a fleet. +- **SQLite, single node.** Fine for one agent or a modest service. It does not + shard, and there is no replication story. The tests assert the logic is correct. They do not make it safe.