Harden Sigil: README.md

This commit is contained in:
bigpickle 2026-10-05 21:24:19 +08:00 committed by leora
parent f4a044f1d6
commit 5e1db8afcd

View File

@ -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.