Add static keychain publication workflow

This commit is contained in:
Eric Wendland 2026-05-27 00:59:52 +02:00
commit cfd41522d1
11 changed files with 1852 additions and 46 deletions

View file

@ -356,12 +356,25 @@ pattern of verifying key-registry changes from a prior trusted state. The
transport-neutral replay rules, application-specific signature namespaces,
allowed-signers projection, and JSONL sigchain helpers live in `geth-keychain`
so other applications can reuse the same identity-log model without depending
on the daemon, SQLite, Iroh, or local control. `geth keychain sync <node>` pulls
keychain operations and signatures from an imported peer over Iroh and imports
only operations with a valid OpenSSH signature from a currently trusted admin
key over the canonical payload. See `docs/sigchain-keychain.md` for the
detailed sigchain design. This is currently a pull-based signed operation log,
not a CRDT or Keyhive-style convergent authority.
on the daemon, SQLite, Iroh, or local control. The CLI can export the same
reduced key registry as OpenSSH `allowed_signers` or as appendable JSONL
sigchain data for website publication. It can also sign and verify arbitrary
snapshots, such as externally managed `authorized_keys`, with an active
keychain signer under an explicit OpenSSH namespace. `geth keychain
publish-bundle` writes a website-ready bundle for
`https://example.com/.well-known/sshsigchain/`, including `allowed_signers`,
`geth.sigchain.jsonl`, a signed checkpoint, and optional signed snapshots.
`geth keychain fetch --import` verifies the checkpoint and records the last
accepted checkpoint per retrieval source URL to reject older bundles. The
retrieval source may be a local mirror; the checkpoint still carries the signed
advertised publication base URL, and explicit checkpoint verification can pin it. `geth keychain
explain` and `explain-signer` provide basic auditability for why a keychain
operation or signer is trusted. `geth keychain sync <node>` pulls keychain
operations and signatures from an imported peer over Iroh and imports only
operations with a valid OpenSSH signature from a currently trusted admin key
over the canonical payload. See `docs/sigchain-keychain.md` for the detailed
sigchain design. This is currently a pull-based signed operation log, not a
CRDT or Keyhive-style convergent authority.
New devices can use the node enrollment flow instead of hand-editing keychain
state. `geth node enroll request` creates a canonical, agent-key-signed request

View file

@ -325,6 +325,37 @@ resource-scoped capability decisions.
signed admin-key registry operations.
- `[x]` `geth keychain allowed-signers` exports the active admin key view in
OpenSSH `allowed_signers` format.
- `[x]` `geth keychain allowed-signers --out <path>` writes the active
OpenSSH `allowed_signers` projection directly to a file.
- `[x]` `geth keychain sign-file --in <path> --out <sig>` signs arbitrary
snapshots, such as externally managed `authorized_keys`, with an active
admin key under an explicit namespace.
- `[x]` `geth keychain verify-file --in <path> --signature <sig>` verifies a
snapshot signature against the current keychain-derived `allowed_signers`
projection or a supplied `--allowed-signers` file.
- `[x]` `geth keychain sigchain --out <path>` writes the appendable JSONL
sigchain suitable for static website publication.
- `[x]` `geth keychain publish-bundle --out <dir>` writes a static website
bundle rooted at `https://example.com/.well-known/sshsigchain/` by default.
- `[x]` Publication bundles include `allowed_signers`, `geth.sigchain.jsonl`,
`geth.sigchain.checkpoint.json`, and a detached checkpoint signature.
- `[x]` Publication bundles can copy and sign external snapshots with
`--snapshot <name>=<path>` without making the keychain own their contents.
- `[x]` `geth keychain verify-sigchain --in <path>` verifies a JSONL sigchain
file by replaying operations and signatures.
- `[x]` `geth keychain import-sigchain --in <path>` imports a JSONL sigchain
only if replay verification rejects no operations.
- `[x]` `geth keychain verify-checkpoint` verifies checkpoint signatures,
checkpoint hashes, base URL, and sigchain head consistency.
- `[x]` `geth keychain fetch --url <base> --import` fetches static bundles
with `curl` for HTTP(S) or filesystem reads for local/file URLs.
- `[x]` Static fetch/import records the last accepted checkpoint per source
URL and rejects older checkpoints for rollback resistance.
- `[x]` `geth keychain explain <op-id>` and `explain-signer <key-id>` provide
basic audit output for operations and admin signers.
- `[x]` Agent/FIDO signing is supported through OpenSSH by passing a public
key or security-key stub to `--signing-key`; PKCS#11 is documented as an
ssh-agent-backed flow when loaded with `ssh-add -s <provider>`.
- `[x]` `geth keychain verify` replays the keychain sigchain against the
previously accepted admin-key view.
- `[x]` Reusable sigchain mechanics live in `geth-keychain`, not in daemon

View file

@ -75,6 +75,20 @@ 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:
```sh
@ -168,10 +182,17 @@ and cacheable:
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.
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
@ -205,6 +226,47 @@ format:
```sh
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:
```sh
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:
@ -221,7 +283,6 @@ 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