Files
clawstor/claw-store/Cargo.toml
T
Omar Sobh 916add37df Phase 1d: persistent NodeIdentity + FleetCa + fleet-ca CLI
Closes out Phase 1. A production operator can now cut a fleet CA,
sign per-node leaves, drop the resulting PEMs at
/etc/claw-store/tls/, point [cluster.tls] at them, and the daemon
loads real mTLS material on startup — no more ephemeral in-process
CA hack.

New public API in cluster::transport:
- FleetCa::generate(cn) — new self-signed root CA
- FleetCa::save(dir) / FleetCa::load(dir) — round-trip PEM
- FleetCa::sign_leaf(name) — mint an in-memory NodeIdentity
- FleetCa::sign_leaf_to_pem(name, out_dir) — write ca.crt + node.crt +
  node.key (node.key at 0o600 on Unix)
- NodeIdentity::from_pem_files(ca, cert, key) — production load path
- NodeIdentity::from_pem_dir(dir) — canonical filename layout
- NodeIdentity::from_cluster_config(cfg) — pick up [cluster.tls] paths

Manual Debug for FleetCa redacts the private key.

Config extension:
- [cluster.tls] ca_cert / node_cert / node_key (all PathBuf).
- Optional at the top level; callers that need mTLS surface a clear
  error when it's absent.

Deps:
- rcgen features += "x509-parser" (for FleetCa::load's from_ca_cert_pem).
- rustls-pemfile 2 (parse PEM back into DER for rustls).

CLI (new commands; short-circuit config load so they run on fresh
boxes without /etc/claw-store/config.toml):
- fleet-ca-init --dir <dir> [--cn <name>]
    generates ca.crt + ca.key (both 0o600).
- fleet-ca-sign --ca-dir <dir> --node <name> --out-dir <dir>
    writes ca.crt + node.crt + node.key (node.key at 0o600).
- cluster-ping now accepts --tls-dir <dir> to load persistent
  NodeIdentity from disk (produced by fleet-ca-sign).

Tests (8 new, all real — no mocks, real filesystem, real TLS handshake):
- fleet_ca_rejects_empty_common_name
- fleet_ca_save_and_load_round_trip_preserves_signing (asserts 0o600
  on ca.key)
- fleet_ca_load_errors_when_files_missing
- sign_leaf_to_pem_writes_all_three_files_with_correct_permissions
  (asserts 0o600 on node.key)
- persistent_identity_round_trips_through_disk_and_pings — end-to-end:
  cut CA on disk, reload it, sign two leaves via sign_leaf_to_pem,
  reload them via from_pem_dir, run real QUIC ping/pong. This is the
  operator flow.
- node_identity_from_cluster_config_errors_without_tls_section
- node_identity_from_cluster_config_loads_pem_paths
- from_pem_files_errors_on_missing_ca_file

80 tests pass. Pre-existing macOS-only hot test unchanged.

Also verified live CLI smoke test:
  fleet-ca-init → ca.crt + ca.key at 0o600
  fleet-ca-sign → ca.crt + node.crt + node.key at 0o600
  Files parse as valid X.509.

File sizes (all under 1300-line ceiling):
- cluster/transport.rs: 916
- cluster/gossip.rs: 576
- cluster.rs: 275
- config.rs: 503
- main.rs: 662

Phase 1 complete. Next up:
- Phase 1e (daemon integration): gossip + QUIC RPC server wired into
  claw-store daemon; hot-tier metrics periodically pushed; a real
  PeerStatus RPC alongside ping.
- Phase 2: content-addressed blob store (BLAKE3 chunking, put/get).
2026-07-11 22:04:54 -07:00

55 lines
2.2 KiB
TOML

[package]
name = "claw-store"
version = "0.3.0"
edition = "2021"
[[bin]]
name = "claw-store"
path = "src/main.rs"
[dependencies]
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
toml = "0.8"
tokio = { version = "1", features = ["full"] }
sysinfo = "0.30"
anyhow = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
chrono = { version = "0.4", features = ["serde"] }
axum = { version = "0.7", features = ["macros"] }
tower-http = { version = "0.5", features = ["cors", "fs"] }
tokio-stream = "0.1"
serde_json = "1"
# v0.2.0 — flock(2) wrapper for atomic+locked manifest writes
# (manifest.rs). Already a transitive dep; declaring it directly
# makes the call site obvious.
libc = "0.2"
# v0.2.0 — sibling-tempfile + rename for atomic manifest persistence.
# Was previously dev-dep only; promoted to main.
tempfile = "3"
# v0.11.1 — cluster membership via scuttlebutt gossip + phi-accrual failure
# detection. Wraps in cluster/gossip.rs. UDP transport, keyed KV state per
# node, seed_nodes bootstrap from [[cluster.peers]] config.
chitchat = "0.11"
# v0.11 — QUIC transport for peer RPC (Phase 1c). UDP-based; runs on a
# separate port from chitchat gossip. TLS 1.3 by default; we wire mTLS
# against a fleet root CA in cluster/transport.rs.
quinn = { version = "0.11", default-features = false, features = ["runtime-tokio", "rustls-ring"] }
# v0.23 — TLS 1.3 provider driving quinn's crypto. Pinned to the ring
# provider so a single CryptoProvider is installed process-wide.
rustls = { version = "0.23", default-features = false, features = ["ring"] }
# v0.13 — X.509 cert generation for the fleet CA + per-node leaf certs.
# Used both by production bootstrap (writes PEM to /etc/claw-store/tls/)
# and by tests (in-memory ephemeral CA).
rcgen = { version = "0.13", features = ["pem", "x509-parser"] }
# v2 — parse PEM files into DER for rustls consumption. Used in
# NodeIdentity::from_pem_files (Phase 1d) so persistent identity round-trips
# through the file system on daemon restart.
rustls-pemfile = "2"
[dev-dependencies]
tempfile = "3"
tower = { version = "0.5", features = ["util"] }
http-body-util = "0.1"