Harden Sigil: README.md

This commit is contained in:
bigpickle 2026-10-05 21:24:19 +08:00
parent 4d9e101f54
commit a3dbdb4812

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 a signature proves the identity -> cannot be forged, cannot be replayed
``` ```
This is a working reference implementation with tests. It is **not hardened for This is a working implementation with tests and a hardening pass behind it. It
production** — see *Status* below. is **not independently reviewed** — see *Status* below.
## What it does ## What it does
@ -31,15 +31,17 @@ export SIGIL_DB=/tmp/demo.db
python3 sigil.py seed bigpickle # -> one-time secret python3 sigil.py seed bigpickle # -> one-time secret
python3 sigil.py keygen /tmp/demo.key # -> public_b64, fingerprint 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 enrol bigpickle "$PUB" "$SECRET" # bind key, consume secret
python3 sigil.py challenge bigpickle # -> nonce python3 sigil.py challenge bigpickle # -> nonce
python3 sigil.py sign /tmp/demo.key "$NONCE" # -> signature python3 sigil.py sign /tmp/demo.key "$NONCE" # -> signature
python3 sigil.py verify "$NONCE" "$SIG" # -> agent session token 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 redeem X3ZB-NTLG # -> human_via_agent token
python3 sigil.py whoami "$TOKEN" # kind, scopes, may_act_as_agent python3 sigil.py whoami "$TOKEN" # kind, scopes, may_act_as_agent
python3 sigil.py events bigpickle # audit trail python3 sigil.py events bigpickle # audit trail
python3 sigil.py verify-chain # is the log intact?
``` ```
## Tests ## Tests
@ -48,9 +50,10 @@ python3 sigil.py events bigpickle # audit trail
python3 -m unittest discover -s tests -v 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 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 ## 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. 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. **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 The session it opens lasts an hour, counted **from redemption**. The code
worthless fast; the hour buys reading comfort and no extra capability. 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 ## 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 | Found in review | Now |
independent cryptographic review before it guards anything real. |---|---|
- **Hardened at rest.** Keys and DB are stored unencrypted; the private key file | `issue_code()` trusted an agent name, so it minted authority from a string | Takes an agent session; agent derived from it |
is `0600` but that is all. | One-time code redeemable many times in a race (753 times in 200 trials) | Atomic compare-and-set; 300 races, one success each |
- **Rate limited.** `challenge` is unmetered, so brute-force and | Bootstrap secret spendable more than once | Atomic compare-and-set |
nonce-exhaustion are open. | Signed nonce verifiable more than once | Nonce claimed before the signature is judged |
- **Transport.** No TLS concerns addressed; assumes a trusted channel. | Session tokens stored in clear | Stored as SHA-256 digests, like codes |
- **Agent revocation** exists but has no expiry semantics. | 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. The tests assert the logic is correct. They do not make it safe.