243 lines
8 KiB
Markdown
243 lines
8 KiB
Markdown
# 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.
|
|
|
|
The namespace is part of a `KeychainProfile`. geth uses:
|
|
|
|
- `geth.keychain.v1@geth.local`
|
|
- `geth.node-enrollment-request.v1@geth.local`
|
|
- default admin principal `admin`
|
|
|
|
Other applications should create their own profile with explicit namespaces or
|
|
`KeychainProfile::for_application("<app>", "<domain>")`. For example,
|
|
`for_application("acme-notes", "example.com")` produces
|
|
`acme-notes.keychain.v1@example.com`. This prevents signatures from one
|
|
application's keychain from being replayed into another application's
|
|
keychain.
|
|
|
|
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 `KeychainProfile`
|
|
- a `KeychainSignatureVerifier`
|
|
|
|
The verifier is responsible for the cryptographic backend, such as
|
|
`ssh-keygen -Y verify`, WebCrypto, an HSM, a service-side verifier, or a test
|
|
verifier. The crate owns the replay order, bootstrap rule, profile namespace
|
|
check, previous-view authorization rule, `allowed_signers` projection, and
|
|
reduced keychain view.
|
|
|
|
Minimal reusable Rust shape:
|
|
|
|
```rust
|
|
let profile = geth_keychain::KeychainProfile::for_application(
|
|
"acme-notes",
|
|
"example.com",
|
|
)?;
|
|
let entries = geth_keychain::decode_sigchain_jsonl(sigchain_text)?;
|
|
let (ops, signatures) = geth_keychain::flatten_sigchain_entries(&entries);
|
|
let report = geth_keychain::verify_sigchain_with_profile(
|
|
&ops,
|
|
&signatures,
|
|
&profile,
|
|
&my_verifier,
|
|
);
|
|
```
|
|
|
|
## 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.
|