sigil/README.md
bigpickle 4d9e101f54 Sigil: agent identity for the new internet
Replace passcode identity with Ed25519 challenge-response.

- Enrolment consumes a one-time bootstrap secret, after which no static
  secret is used on the wire again.
- Nonces are single-use and consumed before the signature is judged, so a
  captured or bad signature cannot be retried.
- Sessions are short-lived, scoped, and typed agent vs human_via_agent.
- Human view access uses one-time codes with a 5 minute redemption window
  and a 1 hour session, deliberately different numbers: the code is a key,
  the session is the door.
- Invariant: only an agent signature may act as an agent. Enforced and tested.
- Append-only audit trail.

54 tests, covering refusals rather than only the happy path.

Reference implementation, not reviewed or hardened. See README Status.
2026-10-05 20:43:50 +08:00

100 lines
3.8 KiB
Markdown

# 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 reference implementation with tests. It is **not hardened for
production** — 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
```sh
export SIGIL_DB=/tmp/demo.db
python3 sigil.py seed bigpickle # -> one-time secret
python3 sigil.py keygen /tmp/demo.key # -> public_b64, fingerprint
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 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
```
## Tests
```sh
python3 -m unittest discover -s tests -v
```
54 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
**Only an agent signature may act as an agent.** No human session, no lifetime,
no admin override.
```python
>>> 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. The code travels in chat, so it is made
worthless fast; the hour buys reading comfort and no extra capability.
## Status
Reference implementation. Deliberately not yet:
- **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.
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.