6.2 KiB
Sigil
Agent identity for the new internet.
An agent proves who it is by signing, not by holding a passcode. A passcode is a credential, so anyone who copies it becomes you. A signature is evidence, so forging one requires the private key — which never leaves the agent.
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 implementation with tests and a hardening pass behind it. It is not independently reviewed — see Status below.
What it does
| Enrolment | Bind an Ed25519 public key to an agent, once, using a one-time secret |
| Challenge-response | Platform issues a nonce, agent signs it, platform verifies |
| Sessions | Short-lived, scoped, and typed as agent or human_via_agent |
| View codes | A human looks at what their agent sees, through a single-use code |
| Audit trail | Append-only record of every identity and access event |
Try it
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 /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
python3 -m unittest discover -s tests -v
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. test_hardening.py has one test per defect found in
review, so any of them regressing brings that defect back.
The invariant
Only an agent signature may act as an agent. No human session, no lifetime, no admin override.
>>> sessions.assert_may_act_as_agent(view_session)
sigil.errors.AgentImpersonationDenied: human_via_agent session may not act as
'bigpickle': only an agent signature can
This is why human access is a window, not a costume. A human who redeems a
view code is logged as bigpickle's human, permanently distinguishable from
bigpickle. If that guarantee erodes, every conversation an agent has becomes
unreliable, because nothing distinguishes the agent from someone holding its
credentials.
Two properties worth stating out loud
Expiry and authority are separate axes. A long session is not a privileged 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, 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
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 |
| 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.
See also
DESIGN.md — the design page: rationale, flows, session stamps, the invariant
and its reasoning, and what is deferred.