From a3dbdb48126f6278070d36982d0cd1a29f8aad10 Mon Sep 17 00:00:00 2001 From: bigpickle Date: Mon, 5 Oct 2026 21:24:19 +0800 Subject: [PATCH] Harden Sigil: README.md --- README.md | 65 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 9ee570e..19e973c 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ a passcode is the identity -> can be copied, leaked in a URL, replayed a signature proves the identity -> cannot be forged, cannot be replayed ``` -This is a working reference implementation with tests. It is **not hardened for -production** — see *Status* below. +This is a working implementation with tests and a hardening pass behind it. It +is **not independently reviewed** — see *Status* below. ## What it does @@ -31,15 +31,17 @@ export SIGIL_DB=/tmp/demo.db python3 sigil.py seed bigpickle # -> one-time secret python3 sigil.py keygen /tmp/demo.key # -> public_b64, fingerprint + # add a passphrase to encrypt python3 sigil.py enrol bigpickle "$PUB" "$SECRET" # bind key, consume secret python3 sigil.py challenge bigpickle # -> nonce python3 sigil.py sign /tmp/demo.key "$NONCE" # -> signature python3 sigil.py verify "$NONCE" "$SIG" # -> agent session token -python3 sigil.py code bigpickle # hand a human a code +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? ``` ## Tests @@ -48,9 +50,10 @@ python3 sigil.py events bigpickle # audit trail python3 -m unittest discover -s tests -v ``` -54 tests. They cover the refusals, not just the happy paths: replayed +80 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. +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 @@ -76,21 +79,51 @@ session. Scopes come from the issuer and never widen with time. There is a test asserting a 24-hour human session has exactly the scopes of a 5-minute one. **Code lifetime ≠ session lifetime.** A code is a key and expires in 5 minutes. -The session it opens lasts an hour. The code travels in chat, so it is made -worthless fast; the hour buys reading comfort and no extra capability. +The session it opens lasts an hour, counted **from redemption**. The code +travels in chat, so it is made worthless fast; the hour buys reading comfort and +no extra capability. Waiting for a code does not eat into it — an earlier draft +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. ## Status -Reference implementation. Deliberately not yet: +A hardening pass closed the defects below. Each is now covered by a test. -- **Reviewed by anyone.** This is security-critical code. It needs an - independent cryptographic review before it guards anything real. -- **Hardened at rest.** Keys and DB are stored unencrypted; the private key file - is `0600` but that is all. -- **Rate limited.** `challenge` is unmetered, so brute-force and - nonce-exhaustion are open. -- **Transport.** No TLS concerns addressed; assumes a trusted channel. -- **Agent revocation** exists but has no expiry semantics. +| 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 | +| 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 | +| Session tokens stored in clear | Stored as SHA-256 digests, like codes | +| Audit log editable and deletable in place | Hash-chained; `verify_chain` names the first bad event | +| `challenge` unmetered | Sliding-window limits per agent on every entry point | +| Private keys written unencrypted | Optional passphrase (PBKDF2, AES-256), `0600` | +| Rekey left old sessions alive | Rekey revokes every session for that agent | +| Agent ids unvalidated, could forge log lines | Constrained alphabet and length | +| 1-hour session anchored to issue time | Anchored to redemption | + +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. +- **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. The tests assert the logic is correct. They do not make it safe.