geth/docs/sigchain-keychain.md

11 KiB

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:

ssh-keygen -Y sign -n geth.keychain.v1@geth.local -f <signing-key> <payload>

<signing-key> can be:

  • a local private OpenSSH key file,
  • a FIDO/YubiKey OpenSSH security-key stub,
  • a public key whose private half is already loaded in ssh-agent, or
  • a public key backed by a PKCS#11 token loaded into ssh-agent with ssh-add -s <provider>.

Encrypted private key files should normally be unlocked into ssh-agent before running geth control commands. The daemon never stores private keys or passphrases. Direct PKCS#11 signing is intentionally not a first-class geth backend because portable ssh-keygen -Y sign flows do not expose the same provider flag as OpenSSH certificate signing.

Verification uses:

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:

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:

{"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[].

geth writes checkpoint records with geth keychain publish-bundle. The checkpoint contains the accepted head, operation/signature counts, byte length, BLAKE3 hashes for the sigchain and allowed signers projection, a reduced-view hash, generation time, and the discovery base URL. The checkpoint is signed as geth.sigchain.checkpoint.json.sig so static clients can detect rollback or truncation before importing updates. geth keychain fetch --import stores the last accepted checkpoint for a source URL and rejects older checkpoints from the same source. The fetch URL is a retrieval location and may be a file:// mirror for testing; the checkpoint's signed base_url remains the advertised static publication location. Use geth keychain verify-checkpoint --base-url <url> when a consumer needs to pin that advertised value explicitly.

Commands

Owner bootstrap:

geth init \
  --admin-key ~/.ssh/id_ed25519_sk.pub \
  --signing-key ~/.ssh/id_ed25519_sk \
  --node-name laptop

Add a new admin key:

geth keychain admin-add \
  --admin-key ~/.ssh/new_admin.pub \
  --signing-key ~/.ssh/current_admin_sk \
  --principal admin

Revoke an admin key:

geth keychain admin-revoke <key-fingerprint> \
  --signing-key ~/.ssh/current_admin_sk

Export the reduced active admin key registry in OpenSSH allowed_signers format:

geth keychain allowed-signers > allowed_signers
geth keychain allowed-signers --out allowed_signers
geth keychain sign-file \
  --in authorized_keys \
  --out authorized_keys.sig \
  --signing-key ~/.ssh/id_ed25519_sk
geth keychain verify-file \
  --in authorized_keys \
  --signature authorized_keys.sig
geth keychain sigchain --out geth.sigchain.jsonl
geth keychain publish-bundle \
  --out public/.well-known/sshsigchain \
  --signing-key ~/.ssh/id_ed25519_sk \
  --snapshot authorized_keys=authorized_keys
geth keychain verify-checkpoint \
  --checkpoint geth.sigchain.checkpoint.json \
  --signature geth.sigchain.checkpoint.json.sig \
  --sigchain geth.sigchain.jsonl \
  --allowed-signers allowed_signers
geth keychain fetch --url https://example.com/.well-known/sshsigchain/ --import
geth keychain verify-sigchain --in geth.sigchain.jsonl
geth keychain import-sigchain --in geth.sigchain.jsonl
geth keychain explain <op-id>
geth keychain explain-signer <key-id>

The default static discovery base URL used by publish-bundle is https://example.com/.well-known/sshsigchain/. The keychain does not manage authorized_keys policy. It signs an arbitrary snapshot you provide, which lets other projects keep their own SSH login policy while rooting snapshot approval in the keychain. By default sign-file uses the personal identity namespace geth.authorized-keys.v1@eric.wendland.dev; pass --namespace for project-specific snapshots. The signer must be an active key in the current allowed_signers projection, so website consumers can fetch the snapshot, signature, and allowed signers and verify:

ssh-keygen -Y verify \
  -f allowed_signers \
  -I admin \
  -n geth.authorized-keys.v1@eric.wendland.dev \
  -s authorized_keys.sig < authorized_keys

Replay and verify the local sigchain:

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:

  • 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.