// 01 ACTIVE SYSTEMS
kith
One identity, many keys. Each person gets a single self-certifying root, a did:kith backed by a signed key event log, controlled by a policy over every signer they already have: passkeys, hardware keys, Ethereum wallets, SSH keys, software keys. Applications never see any of that. The hub is a standard OpenID Connect provider, so anything that can "Sign in with…" can sign in with Kith and gets an ordinary JWT back. A draft spec in ten parts with shared conformance vectors, and reference implementations in Rust, TypeScript and Python.
01 An account: a recovery key first, then a passkey
02 Sign in with Kith, from any OpenID Connect app
03 Adding a phone is a QR code
04 One root, a second device, every change in the log
05 A TOTP code adds a role
06 Revoked, pushed, and gone from the next token
The root and what hangs off it
Keys cascade: the identity authorizes devices, a device mints short-lived session keys, and a session can delegate a narrow slice of itself to someone else. Different changes need different quorums (two of three passkeys to add a device, the cold recovery keys to rotate the root), and the key event log records each one, signed, so anyone can replay how an identity came to hold its current keys without trusting the server that stores it.
On a Mac the CLI keeps the identity's keys in the login keychain and the device's own key in the Secure Enclave. A terminal joins an identity with kith device join; a phone joins by scanning the QR code the hub's account page shows under Add a device. Either way the new device waits until the identity's manage policy approves it.
Signing in
Evidence goes in and a JWT comes out. The hub accepts a device session chain, a signed challenge in the style of Sign-In with Ethereum, a passkey, or a TOTP code for step-up, and issues a token whose aal and amr claims say how strong that evidence was.
Over OpenID Connect that becomes a PKCE code flow with username-less passkey login, SSO, step-up through acr_values and pairwise subjects, so Auth.js, Django allauth, oauth2-proxy and Grafana use it unchanged. With --signup, anyone can make an account in the browser: a recovery key first, of which the hub only ever receives the public half, then a passkey. Signing in with a provider such as Telegram first works too. The hub behind this site's analytics console runs at accounts.cartercole.net.
Authorization
Cedar policies, with explicit, nested and dynamic groups and roles. A dynamic group is a proposition, such as "in finance, and signed in at AAL2". The engine plugs into GraphQL at the root, field and row level in Node and Python, and root fields are denied unless a policy allows them.
An organization publishes its policies as a content-addressed DAG of IPFS-compatible blocks, and its key event log commits to the root. A server answering one GraphQL field fetches only that field's policies, from anyone, and can prove nothing was left out.
Revocations that can prove how fresh they are
Revocations are pushed as a signed, hash-chained list, with heartbeats that embed a drand or NIST beacon round. A heartbeat could not have been signed before its beacon was published, so its age is proved rather than claimed. Each verifier states how long it can go between syncs (fresh, then degraded, then it fails closed), and a policy can demand more: under sixty seconds for a payment, say. OpenTimestamps anchors the whole history to Bitcoin.
Also in the box
- Aliases. Public ones are proved in both directions and resolve through WebFinger, DNS or did:web back-links. Private ones stay unlinkable until you choose to prove the link.
- Work as a score. An account can show it spent compute, as a nonce whose hash with the DID has many leading zero bits, and a policy can ask for it:
principal.work >= 24. - Crypto-enforced sharing for data kept on storage you don't trust: skip-ratchet group keys in the style of WNFS, sealed to device keys.
- Verified contacts. An email address or phone number confirmed by a code, handed to apps with a signed attestation, and private unless you publish it.
Stated honestly
This is a draft spec with a working reference implementation, and it is not production ready. On Linux the CLI keeps keys in plaintext, the hub has no TLS of its own, GPG signers are specified but not implemented, and Bitcoin signers are planned. brew install cartercole/tap/kith installs the CLI and the hub, and each release carries archives for macOS and Linux on ARM64 and x86-64.
About these pictures
figures/kith.sh in this site's repository draws all six from the 0.3.0 release, built from its tag, against throwaway hubs on this machine. Every identity, passkey, recovery key and TOTP secret in them was made for that run and thrown away after it. The pages were driven in headless Chrome with a virtual authenticator standing in for the passkey. The CLI keeps its keys in plaintext files (--keystore file) so the run never touches the keychain. The revocation picture's heartbeats carry real drand rounds. Identities, proof of work and timestamps differ on every run.
The tour is filmed by the same script, the same way, a step at a time with a real pointer. A second browser with its own virtual passkey plays the phone that scans the QR code. The tokens, the key event log and the terminals in it are that run's own. The voice is Orpheus, run locally.