Extract reusable keychain sigchain model
This commit is contained in:
parent
5b2c30f817
commit
4013c868aa
11 changed files with 938 additions and 20 deletions
211
docs/sigchain-keychain.md
Normal file
211
docs/sigchain-keychain.md
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
# Geth Keychain Sigchain Design
|
||||
|
||||
## Purpose
|
||||
|
||||
The geth keychain is a signed operation log for mesh identity state. It is the
|
||||
source of truth for admin SSH keys, users, devices, nodes, agents, and endpoint
|
||||
bindings. Peers do not trust a mutable keychain snapshot. They replay signed
|
||||
operations and reduce the accepted log into the current keychain view.
|
||||
|
||||
This is intentionally similar to the `git-skm` pattern:
|
||||
|
||||
- `git-skm` treats a committed `allowed_signers` file as trusted only if every
|
||||
commit that changed it can be verified from a prior trusted state.
|
||||
- geth treats a keychain operation as trusted only if it is signed by an admin
|
||||
key that was trusted in the previously accepted keychain view.
|
||||
|
||||
## Data Model
|
||||
|
||||
The reusable model lives in the `geth-keychain` crate. The daemon stores it in
|
||||
SQLite and syncs it over Iroh, but the crate does not depend on SQLite, Iroh, or
|
||||
the geth daemon. Other projects can publish the same keychain as a static
|
||||
sigchain file, append it to object storage, embed it in a document, or transport
|
||||
it by any other mechanism.
|
||||
|
||||
The durable log is `KeychainOp[]` plus `KeychainOpSignature[]`. Each operation
|
||||
has a deterministic canonical signing payload under the
|
||||
`geth.keychain.v1@geth.local` namespace.
|
||||
|
||||
Important operation kinds:
|
||||
|
||||
- `KeychainInit`
|
||||
- `AdminKeyAdd`
|
||||
- `AdminKeyRevoke`
|
||||
- `UserAdd`, `UserRename`, `UserRevoke`
|
||||
- `DeviceAdd`, `DeviceRevoke`
|
||||
- `DeviceKeyAdd`, `DeviceKeyRevoke`
|
||||
- `NodeAdd`, `NodeRename`, `NodeRevoke`
|
||||
- `NodeEndpointAdd`, `NodeEndpointRevoke`
|
||||
- `AgentBind`
|
||||
|
||||
`AdminKeyAdd` records the admin key fingerprint and, for new operations, the
|
||||
OpenSSH public key material, optional principal, and optional validity metadata.
|
||||
The public key is part of the signed operation so the key registry can be
|
||||
reconstructed from the sigchain itself. Older local data may only have the
|
||||
fingerprint; geth can use stored signature public-key material as a fallback
|
||||
when exporting the current allowed signers view.
|
||||
|
||||
## Signature Rules
|
||||
|
||||
Each signed operation has a `KeychainOpSignature`:
|
||||
|
||||
- `op_id`
|
||||
- signer key fingerprint
|
||||
- signer OpenSSH public key
|
||||
- namespace
|
||||
- OpenSSH signature bytes
|
||||
- creation time
|
||||
|
||||
Signatures are produced with:
|
||||
|
||||
```sh
|
||||
ssh-keygen -Y sign -n geth.keychain.v1@geth.local -f <signing-key> <payload>
|
||||
```
|
||||
|
||||
Verification uses:
|
||||
|
||||
```sh
|
||||
ssh-keygen -Y verify \
|
||||
-f <allowed-signers> \
|
||||
-I <signer-principal> \
|
||||
-n geth.keychain.v1@geth.local \
|
||||
-s <signature>
|
||||
```
|
||||
|
||||
The signed payload is canonical binary encoding, not JSON.
|
||||
|
||||
## Verification Algorithm
|
||||
|
||||
For a candidate log ordered by `(created_at_ms, op_id)`:
|
||||
|
||||
1. Start from a local trust anchor. In the bootstrap implementation this is a
|
||||
locally initialized `KeychainInit` plus an admin key added through
|
||||
`geth init --admin-key ... --signing-key ...` or `geth keychain init
|
||||
--admin-key ...`.
|
||||
2. Maintain an accepted operation prefix and reduce it into the current
|
||||
keychain view.
|
||||
3. For each next operation, collect its signatures.
|
||||
4. Accept the operation only if at least one signature:
|
||||
- is from a key fingerprint present in the previous accepted view,
|
||||
- carries public key material that hashes to that fingerprint,
|
||||
- verifies over the canonical operation payload with OpenSSH, and
|
||||
- uses the keychain namespace.
|
||||
5. After accepting the operation, append it to the accepted prefix and reduce
|
||||
again. This lets a valid `AdminKeyAdd` authorize later operations, and a
|
||||
valid `AdminKeyRevoke` stop later authorization by that key.
|
||||
6. Reject unsigned, invalidly signed, conflicting, or out-of-authority
|
||||
operations.
|
||||
|
||||
This mirrors `git-skm`'s "verify from the previously trusted
|
||||
allowed_signers" model, but the transport and storage are geth/Iroh/SQLite
|
||||
instead of Git commits.
|
||||
|
||||
The `geth-keychain` crate exposes this as a transport-neutral verifier. Callers
|
||||
provide:
|
||||
|
||||
- ordered or unordered `KeychainOp[]`
|
||||
- `KeychainOpSignature[]`
|
||||
- a signature verification callback
|
||||
|
||||
The callback is responsible for the cryptographic backend, such as
|
||||
`ssh-keygen -Y verify`, WebCrypto, an HSM, or a test verifier. The crate owns
|
||||
the replay order, bootstrap rule, previous-view authorization rule,
|
||||
`allowed_signers` projection, and reduced keychain view.
|
||||
|
||||
## Static Sigchain File
|
||||
|
||||
For static hosting and range-friendly distribution, `geth-keychain` defines a
|
||||
JSONL sigchain representation:
|
||||
|
||||
```json
|
||||
{"op":{...},"signatures":[...]}
|
||||
{"op":{...},"signatures":[...]}
|
||||
```
|
||||
|
||||
Each line is a complete `KeychainSigchainEntry`. This keeps the file appendable
|
||||
and cacheable:
|
||||
|
||||
- A publisher can append new entries to the end of the file.
|
||||
- HTTP clients can use `ETag`, `Last-Modified`, and byte range requests to fetch
|
||||
only new bytes.
|
||||
- A client can decode complete trailing lines, ignore a partial final line until
|
||||
the next fetch, and replay verification from its last accepted checkpoint.
|
||||
- The canonical signed payload remains the `KeychainOp`; JSONL is only the
|
||||
publication container.
|
||||
|
||||
The crate provides helpers to encode/decode JSONL and flatten entries back into
|
||||
`KeychainOp[]` plus `KeychainOpSignature[]`.
|
||||
|
||||
Future work should add explicit checkpoint records containing the accepted head
|
||||
ID, byte offset, reduced view hash, and log hash. That would let static clients
|
||||
resume verification without replaying the entire file while still detecting
|
||||
rollback or truncation.
|
||||
|
||||
## Commands
|
||||
|
||||
Owner bootstrap:
|
||||
|
||||
```sh
|
||||
geth init \
|
||||
--admin-key ~/.ssh/id_ed25519_sk.pub \
|
||||
--signing-key ~/.ssh/id_ed25519_sk \
|
||||
--node-name laptop
|
||||
```
|
||||
|
||||
Add a new admin key:
|
||||
|
||||
```sh
|
||||
geth keychain admin-add \
|
||||
--admin-key ~/.ssh/new_admin.pub \
|
||||
--signing-key ~/.ssh/current_admin_sk \
|
||||
--principal admin
|
||||
```
|
||||
|
||||
Revoke an admin key:
|
||||
|
||||
```sh
|
||||
geth keychain admin-revoke <key-fingerprint> \
|
||||
--signing-key ~/.ssh/current_admin_sk
|
||||
```
|
||||
|
||||
Export the reduced active admin key registry in OpenSSH `allowed_signers`
|
||||
format:
|
||||
|
||||
```sh
|
||||
geth keychain allowed-signers > allowed_signers
|
||||
```
|
||||
|
||||
Replay and verify the local sigchain:
|
||||
|
||||
```sh
|
||||
geth keychain verify
|
||||
```
|
||||
|
||||
## Differences From Git-SKM
|
||||
|
||||
`git-skm` uses Git commit history as the append-only log and a trusted commit
|
||||
hash as the checkpoint. geth uses `KeychainOp[]` as the append-only log and the
|
||||
locally initialized owner/admin key as the bootstrap trust anchor.
|
||||
|
||||
The current geth prototype does not yet provide:
|
||||
|
||||
- signed checkpoint objects equivalent to `skm.last-verified-commit`
|
||||
- transparency-log style append proofs
|
||||
- anti-rollback protection beyond local state, HTTP cache validators, and sync
|
||||
conflict checks
|
||||
- delegated admin scopes or threshold admin signatures
|
||||
- strong compromise-recovery semantics
|
||||
|
||||
Those are future hardening items. The current prototype is intended to make the
|
||||
key registry self-describing, replayable, and testable.
|
||||
|
||||
## Security Invariants
|
||||
|
||||
- The admin SSH private key is never copied into geth state.
|
||||
- Admin key changes are keychain operations, not mutable ACL edits.
|
||||
- Public key material needed to reconstruct active admin signers is stored in
|
||||
the signed operation log.
|
||||
- Import from peers accepts only operations signed by currently trusted admin
|
||||
keys.
|
||||
- A discovered peer card or Iroh EndpointID never grants keychain authority.
|
||||
- Bearer secrets do not grant keychain mutation rights.
|
||||
Loading…
Reference in a new issue