geth/README.md

442 lines
22 KiB
Markdown

# geth
`geth` is a personal, local-first mesh runtime for scripts, devices, databases,
documents, blobs, pipes, and future multi-user collaboration.
This project is not the Ethereum `geth` client. The project and executable are
still named `geth`.
## One Binary
There is one executable: `geth`.
It has daemon mode and control mode:
```sh
geth init
geth daemon run
geth daemon service install
geth status
geth node id
geth resource list
geth cas add ./file
```
The daemon owns local identity, the Iroh endpoint, trust state, resource
registry, module router, local metadata store, and synchronized data structures.
Most non-daemon commands talk to the daemon through a local Unix socket at
`$GETH_HOME/run/geth.sock`.
`geth init --admin-key <public-key> --signing-key <private-key> --node-name
<name>` records an owner/admin keychain, the local user/device/node binding, and
signs canonical keychain payloads through `ssh-keygen -Y sign` using the
`geth.keychain.v1@geth.local` namespace. This is the bootstrap path for
admin/YubiKey-rooted trust. `geth keychain sync <node>` pulls the signed
keychain operation log from an imported peer and imports only operations with
valid OpenSSH signatures from currently trusted admin keys. `geth node list`
shows the active reduced node view, and `geth node rename/revoke` require
`--signing-key` so device-management changes can replicate as verified admin
statements. `geth node grant/revoke-grant` and `geth auth grant/revoke` also
require `--signing-key` in the CLI and store signed auth operations for
replication.
SSH certificate-flow and revocation records carry agent-key signed provenance
over canonical payloads, and sync import rejects new unsigned or invalidly
signed records.
The daemon can also install itself as a user service:
```sh
geth daemon service install
geth daemon service status
geth daemon service uninstall
```
The bootstrap service managers are systemd user units on Linux, launchd user
agents on macOS, and per-user scheduled tasks on Windows. These are user-level
services, not system services.
## Transport And SSH
All remote node-to-node geth communication is designed to happen over Iroh only.
SSH is not a geth transport backend, and there is no SSH fallback transport.
The default node config uses Iroh's default relay policy for practical
connectivity; set `[iroh].relay_mode = "disabled"` for local-only/offline
development. Named custom relay maps can be selected with
`relay_mode = "custom"` and `relay_map = "<name>"`. Iroh local-network
discovery is enabled by default with `[iroh].local_discovery = true`.
SSH keys are used as admin trust anchors and ecosystem integration points.
OpenSSH, FIDO, and YubiKey-backed keys can sign geth trust objects through
canonical geth envelopes with explicit namespaces such as
`geth.keychain.v1@geth.local`. SSH proxying carries SSH protocol bytes over an
authorized Iroh stream, but SSH is still not a geth transport backend.
SSH certificate request and renewal flows are managed as geth metadata. A node
can create a certificate request, another machine can approve it and receive an
explicit `ssh-keygen -s ...` command suitable for a CA key or YubiKey-backed CA,
or pass `--sign` to run `ssh-keygen` immediately and import the resulting
`-cert.pub` for distribution. Certificate and key revocation entries are tracked
locally and can be exported as JSONL or as an OpenSSH KRL specification file or
a binary OpenSSH KRL generated through `ssh-keygen -k`. `geth ssh cert sync
<node-id>` and
`geth ssh revocation sync <node-id>` pull certificate-flow and revocation
metadata from an authorized peer over Iroh.
## MVP Features
The bootstrap implementation provides:
- `geth init`
- `geth init --admin-key <public-key> --signing-key <private-key> --node-name <name>`
- `geth daemon run`
- `geth daemon service install|uninstall|start|stop|status|print`
- `geth status`
- `geth node id`
- `geth node list`
- `geth node enroll request --node-name <name> --capability <resource=capability> [--out <path>]`
- `geth node enroll submit <owner-node> [--request-id <id>|--path <path>]`
- `geth node enroll import <path>`
- `geth node enroll list [--status pending|approved|rejected]`
- `geth node enroll approve <request-id> --signing-key <private-key>`
- `geth node enroll sync <owner-node>`
- `geth node rename <node-or-name> <name> --signing-key <private-key>`
- `geth node revoke <node-or-name> --signing-key <private-key>`
- `geth node endpoint-add <node-or-name> <endpoint-id> --signing-key <private-key>`
- `geth node endpoint-revoke <node-or-name> <endpoint-id> --signing-key <private-key>`
- `geth node grant <node-or-name> <resource> <capability> --signing-key <private-key> [--grant-id <id>]`
- `geth node revoke-grant <resource> <grant-id> --signing-key <private-key>`
- `geth peer export [--out <path>]`
- `geth peer import <path>`
- `geth peer list`
- `geth peer ping <node-id>`
- `geth peer auth-check <node-id> <resource> <capability>`
- `geth resource list`
- `geth resource create <kind> <name>`
- `geth keychain init [--admin-key <path>] [--signing-key <path>]`
- `geth keychain status`
- `geth keychain sync <node-id-or-name>`
- `geth auth sync <node-id-or-name>`
- `geth sync status`
- `geth sync now [node-id-or-name]`
- `geth secret status`
- `geth secret create <resource>`
- `geth secret rotate <resource>`
- `geth secret bearer create <resource> --capability <capability>`
- `geth secret bearer list`
- `geth secret bearer challenge <resource> --capability <capability>`
- `geth secret bearer prove <token> <resource> --nonce <nonce> --capability <capability>`
- `geth secret bearer verify <token> <resource> --nonce <nonce> --response <response> --capability <capability>`
- `geth secret bearer revoke <resource> <bearer-id>`
- `geth auth explain <subject> <resource> <capability>`
- `geth auth grant <subject> <resource> <capability> --signing-key <private-key> [--grant-id <id>]`
- `geth auth revoke <resource> <grant-id> --signing-key <private-key>`
- local filesystem CAS commands: `add`, `get`, `fetch`, `hash`, `has`, `pin`,
`unpin`, `cleanup`, `providers`, `list`; remote fetch accepts
`--bearer-secret <secret>`
- private CAS envelope commands:
`geth cas add-private <resource> <path>` and
`geth cas get-private <resource> <hash> --out <path>`. These use local
resource secret epochs and are a prototype envelope, not audited AEAD.
- local CAS tree objects describe file trees and are stored as CAS blobs
- local file-root commands: `geth cas root add/list/scan/sync/apply`; root sync
pulls authorized remote tree metadata and CAS tree bytes into a peer-qualified
remote root, and apply materializes a tree without deleting files or
overwriting local edits. Repeated syncs keep the previous imported remote
tree as the base and record durable conflicts when local and remote roots
both changed.
- local file conflict metadata commands:
`geth cas conflict record/list/resolve`
- local DB resource registration: `geth db add <name> <path>` and
`geth db status <name>` with schema and `crsql_changes` metadata; the DB
crate and daemon can extract typed local `crsql_changes` batches through
`geth db changes <name>` and exchange authorized remote batches with
`geth db sync <node-id> <name>`
- local SQLite-backed KV commands: `geth kv create/set/get`; `kv set` accepts
`--subject <principal>` to exercise local capability checks for non-local
callers; `geth kv sync <node-id> <name> [--bearer-secret <secret>]` pulls
authorized remote updates
- local JSON document commands: `geth document create/status/set/get`; `geth
document sync <node-id> <name> [--bearer-secret <secret>]` pulls authorized
remote JSON state
- local daemon-lifetime pubsub snapshots: `geth pubsub pub/sub`; `geth pubsub
pub <topic> <message> --node <node-id>` publishes to an authorized peer;
`geth pubsub sub <topic> --node <node-id>` reads an authorized peer snapshot
- SSH certificate flow metadata:
- `geth ssh cert request --public-key <path> --principal <name> [--subject <principal>]`
- `geth ssh cert requests [--subject <principal>]`
- `geth ssh cert approve <request-id> --ca-key <path> [--sign] [--subject <principal>]`
- `geth ssh cert import <request-id> --cert <path> [--subject <principal>]`
- `geth ssh cert list [--subject <principal>]`
- `geth ssh cert sync <node-id> [--bearer-secret <secret>]`
- `geth ssh revocation add <kind> <target> [--subject <principal>]`
- `geth ssh revocation list [--subject <principal>]`
- `geth ssh revocation export --out <path> [--format jsonl|openssh-krl-spec|openssh-krl] [--subject <principal>]`
- `geth ssh revocation import <path> [--format jsonl|openssh-krl-spec] [--subject <principal>]`
- `geth ssh revocation sync <node-id> [--bearer-secret <secret>]`
- SSH proxy/admin over Iroh:
`geth ssh proxy <node-id> [--bearer-secret <secret>]` and
`geth ssh admin-shell <node-id> <help|status|node-id> [--bearer-secret <secret>]`
- pipe registry/message commands:
`geth pipe listen <name> [--node <node-id>] [--bearer-secret <secret>]`,
`geth pipe connect <name> [--node <node-id>] [--bearer-secret <secret>]`,
`geth pipe send <name> [message|--in <path>|--in -] [--node <node-id>] [--bearer-secret <secret>]`,
`geth pipe recv <name> [--peek]`, and
`geth pipe forward-tcp --listen 127.0.0.1:<port> --node <node-id> --target 127.0.0.1:<port>`
or `geth pipe forward-unix --listen /tmp/local.sock --node <node-id> --target /tmp/remote.sock`
`geth peer export/import/list` is for untrusted peer-card exchange. Peer cards
include the Iroh EndpointID plus currently known relay/direct addresses.
`geth peer ping <node-id>` uses the local daemon's Iroh endpoint to dial an
imported peer card and exchange a signed candidate-only peer-card ping.
`geth peer auth-check <node-id> <resource> <capability>` sends a protected
Iroh control request: the remote daemon verifies that the caller's signed peer
card binds the actual Iroh EndpointID before reducing resource-local auth ops.
`geth cas fetch <node-id> <hash>` uses the same protected Iroh control path to
request a blob from a peer. The remote daemon only returns bytes when the caller
has `cas.fetch` on `resource:cas:local`, and the caller verifies that the bytes
hash to the requested BLAKE3 CAS hash before storing them locally. Successful
fetches record the serving peer as a local provider, visible with
`geth cas providers <hash>`. This is the bootstrap transfer path; future work
will move provider/fetch behavior to `iroh-blobs`.
Remote resource commands that accept `--bearer-secret` can also authorize with a
resource-scoped bearer proof generated from the private bearer token returned at
creation time. The persisted auth log stores a public bearer id and token
verifier, not the private token. This does not enroll the caller as a trusted
node; it only unlocks the requested capability on that one resource.
`geth ssh cert sync <node-id>` requires `ssh_cert.sync` on `resource:ssh:certs`
at the peer. `geth ssh revocation sync <node-id>` requires
`ssh_revocation.sync` on `resource:ssh:revocations`. Both commands merge
authorized peer metadata into the local store for offline listing and later
approval/signing workflows. While the daemon is running, it also performs a
background live-sync tick for known peers. The default interval is 30 seconds
and can be changed in `config.toml` with `[sync] live_sync_enabled` and
`live_sync_interval_ms`. Live-sync stores per-peer high-water cursors in local
metadata so repeated ticks request only newer SSH certificate-flow and
revocation records. Sync import preserves local metadata by rejecting
conflicting records with ids that already exist locally.
Before probing individual modules, the daemon asks the peer for an authorized
sync-status summary over Iroh. The peer only returns stream watermarks for
resources where the caller already has the matching capability, letting the
local daemon skip unchanged or unauthorized streams.
File roots advertise `cas-tree:<name>` watermarks when the caller has
`cas.fetch`; background live-sync imports updated tree metadata and CAS tree
bytes into peer-qualified remote roots without writing files.
When a previous imported remote tree is available, sync compares that base
against the current local same-named root and the newly imported remote tree.
Concurrent edit, delete/edit, and divergent rename conflicts are recorded in
the local conflict table for later resolution.
`geth cas root apply <root> --to <path>` can then materialize that tree locally:
it creates missing directories/files, never deletes extra files, never
overwrites differing local files, and records conflicts for manual resolution.
Named KV stores participate in the same live-sync loop once they exist locally:
manual `geth kv sync <node-id> <name>` and background ticks require `kv.read`
on the remote `resource:kv:<name>` and import only remote entries that are not
older than the local value.
Remote pubsub publish uses the protected Iroh control path too. The remote peer
requires `pubsub.publish` on `resource:pubsub:<topic>` before recording the
message in its local daemon-lifetime ring buffer. Pubsub remains lossy and is
not durable storage; facts that must survive restart or reconcile offline
belong in CAS, KV, document, or DB resources. Remote pubsub subscribe uses the
same protected path and requires `pubsub.subscribe` on
`resource:pubsub:<topic>` before returning the peer's current daemon-lifetime
snapshot for that topic.
Remote pipe connect uses the same protected Iroh control path and requires
`pipe.connect` on `resource:pipe:<name>`. The current prototype records a remote
connection attempt and whether a listener exists. `geth pipe send <name>
[message|--in <path>|--in -] --node <node-id>` uses the dedicated `/geth/pipe/1`
Iroh ALPN to write a byte message to an authorized peer listener, and `geth pipe
recv <name>` drains local daemon-lifetime messages.
Remote pipe listen uses the same protected path:
`geth pipe listen <name> --node <node-id>` requires `pipe.listen` on
`resource:pipe:<name>` before registering a daemon-lifetime listener on the
peer.
`geth pipe forward-tcp --listen 127.0.0.1:<local-port> --node <node-id> --target
127.0.0.1:<remote-port>` starts a local loopback TCP listener. Each accepted
connection asks the local daemon to open an authorized `/geth/pipe/1` byte
stream to the peer. The remote daemon validates the signed endpoint/card binding
and requires `pipe.forward` on `resource:pipe-tcp:<target>` before connecting to
the remote loopback TCP target. This is loopback-only in the prototype to avoid
turning geth into an accidental open proxy.
`geth pipe forward-unix --listen <local-socket> --node <node-id> --target
<remote-socket>` uses the same `/geth/pipe/1` byte stream and requires
`pipe.forward` on `resource:pipe-unix:<target>` before connecting to the remote
Unix socket. Unix socket paths must be absolute.
`geth ssh proxy <node-id>` is usable as an OpenSSH `ProxyCommand`: the CLI opens
a local daemon stream, the daemon opens the dedicated `/geth/ssh-proxy/1` Iroh
ALPN, the remote daemon validates the caller's endpoint/card binding and
requires `ssh_proxy.connect` on `resource:ssh-proxy:local`, and only then
connects the stream to `127.0.0.1:22`. SSH remains normal OpenSSH on top of that
byte stream; SSH is not a geth transport backend.
`geth ssh admin-shell <node-id> <command>` is a restricted geth admin workflow
over the protected Iroh control path. It requires `ssh_proxy.admin_shell` on the
same resource and supports only built-in commands (`help`, `status`, `node-id`);
it does not execute host shell commands.
Document sync is a bootstrap JSON last-writer-wins path before Automerge:
manual `geth document sync <node-id> <name>` and background live-sync require
`document.read` on `resource:document:<name>` and import only state that is not
older than the local document timestamp.
DB sync is a staged cr-sqlite path: manual `geth db sync <node-id> <name>` and
background live-sync require `db.sync` on `resource:db:<name>`, exchange typed
`crsql_changes` batches over the protected Iroh control path, and check remote
schema metadata against the local DB before applying and advancing the per-peer
cursor. Compatible batches are inserted into the local `crsql_changes` table or
view; for real cr-sqlite databases, loading/configuring cr-sqlite remains the
database owner's responsibility.
Importing or pinging a peer card never grants capabilities by itself.
When `[iroh].local_discovery = true`, the daemon also advertises and discovers
signed peer cards on LAN using a geth-specific mDNS TXT payload. That payload is
candidate metadata only; all geth node-to-node requests still run over Iroh.
## Resource Modules
Everything meaningful is modeled as a resource. Planned resource kinds are:
- `db`: SQLite/cr-sqlite synchronization
- `kv`: Iroh Documents backed key-value stores
- `pipe`: dumbpipe-like byte streams over Iroh; the bootstrap has a local
daemon registry only
- `document`: Automerge documents over Iroh streams
- `pubsub`: lossy notifications, not authoritative storage; the bootstrap
keeps only an in-memory daemon-lifetime ring buffer
- `cas`: content-addressed blob storage and distribution
- `ssh-proxy`: authorized SSH proxy/admin access over Iroh
Authorization is resource-scoped and capability-based. Bearer secrets may grant
specific resource capabilities but do not create trusted node identity. The auth
evaluator supports scoped KV write grants such as `kv.write_prefix:apps/foo/`
for `kv.write_key:apps/foo/config` explain checks. `geth kv set --subject
<principal>` enforces those local grants for test callers; the local node/agent
still has owner access for local administration. SSH certificate and revocation
commands also accept `--subject <principal>` on local metadata operations to
exercise the same capability checks: certificate requests/read/approval/import
use `ssh_cert.request`, `ssh_cert.read`, `ssh_cert.approve`, and
`ssh_cert.import` on `resource:ssh:certs`, while revocation publish/read/import
use `ssh_revocation.publish`, `ssh_revocation.read`, and
`ssh_revocation.import` on `resource:ssh:revocations`.
## Local State
If `GETH_HOME` is set, geth uses it. Otherwise it uses an OS-specific data
directory. The bootstrap layout is:
```text
$GETH_HOME/
geth.sqlite
config.toml
identity/agent.ed25519
identity/iroh.ed25519
cas/blobs/
run/geth.sock
```
## Quick Start
In one shell:
```sh
export GETH_HOME="$(mktemp -d)"
cargo run -p geth -- init
cargo run -p geth -- daemon run
```
In another shell:
```sh
export GETH_HOME="<same dir>"
cargo run -p geth -- status
cargo run -p geth -- node id
echo "hello geth" > /tmp/hello-geth.txt
cargo run -p geth -- cas add /tmp/hello-geth.txt
cargo run -p geth -- cas list
```
## Owner And Node Management
The intended owner setup is SSH-admin-rooted:
```sh
geth init \
--admin-key ~/.ssh/id_ed25519_sk.pub \
--signing-key ~/.ssh/id_ed25519_sk \
--node-name laptop \
--capability resource:ssh-proxy:local=ssh_proxy.admin_shell
```
When any owner setup option is used, both `--admin-key` and `--signing-key` are
required. This prevents accidentally creating an unsigned owner/device/node
statement that cannot be accepted by another node during keychain sync.
This records signed keychain operations for `KeychainInit`, `AdminKeyAdd`,
`UserAdd`, `DeviceAdd`, `NodeAdd`, and `AgentBind`. The current node identity is
stable above endpoint rotation: future endpoint bindings should attach to the
node, not replace it. Node management is done through the reduced keychain view:
```sh
geth node list
geth node rename laptop work-laptop --signing-key ~/.ssh/id_ed25519_sk
geth node endpoint-add work-laptop <iroh-endpoint-id> --signing-key ~/.ssh/id_ed25519_sk
geth node grant work-laptop resource:ssh-proxy:local ssh_proxy.connect \
--signing-key ~/.ssh/id_ed25519_sk
geth node revoke-grant resource:ssh-proxy:local <grant-id> \
--signing-key ~/.ssh/id_ed25519_sk
geth node revoke work-laptop --signing-key ~/.ssh/id_ed25519_sk
```
The enrollment flow for a new node is:
```sh
# On the new node, initialize local state and trust the owner's admin public key:
geth init
geth daemon run
geth keychain init --admin-key ~/.ssh/id_ed25519_sk.pub
# Then create a signed enrollment request:
geth node enroll request \
--node-name workstation \
--capability resource:ssh-proxy:local=ssh_proxy.connect \
--out /tmp/workstation-enrollment.json
# Either submit over Iroh to an imported owner peer:
geth node enroll submit owner-laptop --path /tmp/workstation-enrollment.json
# Or import the JSON on the owner/YubiKey machine:
geth node enroll import /tmp/workstation-enrollment.json
geth node enroll list --status pending
geth node enroll approve <request-id> --signing-key ~/.ssh/id_ed25519_sk
# Back on the new node, pull signed identity and authorization state:
geth sync now owner-laptop
geth sync status
```
Enrollment requests are signed by the requesting agent key. Approval records
signed keychain operations for the new device/node/agent binding and signed auth
operations for requested resource capabilities. The requesting node must already
know the owner's admin public key so it can verify the signed operation logs
before importing them. `geth sync now` pulls both signed logs from the owner
node through the same path used by background live sync.
`geth keychain sync <node>` pulls signed keychain operations from an imported
peer over Iroh and rejects operations that do not have a valid OpenSSH signature
from a currently trusted admin key over the canonical keychain payload. This is
the current replicated device-management substrate. It is still a pull-based
operation log, not yet a CRDT or Keyhive-style convergent authority.
The daemon also runs best-effort live sync for imported peers. `geth sync now
[node]` triggers the same sync pass immediately, and `geth sync status` reports
the last local attempt, success, cursor, import count, rejection count, and
error per peer stream. Keychain and auth sync now use per-peer high-water
cursors, while receivers still verify every imported signed operation before it
can affect the reduced keychain or authorization views.
## Authorization Direction
The MVP defines the split between:
- keychain: SSH-rooted users, devices, nodes, agents, and endpoint bindings
- auth: resource-local signed authorization operations and capability grants
- secrets: resource master secrets, epochs, envelopes, and bearer access
The current code does not implement Keyhive, BeeKEM, strong forward secrecy, or
post-compromise security. It leaves room for future local-first, replicated auth
logs and BeeKEM/CGKA-style group key evolution.