mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
Implement identity management for the FIPS daemon with two modes: Ephemeral (default): A fresh keypair is generated on every start. Key files (fips.key, fips.pub) are written for operator visibility but overwritten on each restart. This is privacy-friendly and requires no configuration. Persistent (opt-in via `node.identity.persistent: true`): Uses three-tier identity resolution: 1. Explicit nsec in config file (advanced users) 2. Persistent key file alongside config (reused across restarts) 3. Generate new keypair, persist to key file Key files follow the SSH id_ed25519/id_ed25519.pub convention: - fips.key: bare bech32 nsec string, mode 0600 - fips.pub: bare bech32 npub string, mode 0644 The daemon always writes fips.pub on startup so operators can find their current identity via `cat /etc/fips/fips.pub`. Identity resolution extracted into testable `resolve_identity()` function in config module. Graceful degradation: key file write failure silently falls back to ephemeral identity. Add `fipsctl keygen` subcommand for manual key generation: - `-d <dir>` output directory (default: /etc/fips) - `-f` force overwrite existing files - `-s` print nsec/npub to stdout instead of writing files - Warns that `persistent: true` must be set in config - Works without a running daemon Includes unit tests for key file read/write roundtrip, file permissions, whitespace trimming, empty file error, path derivation, ephemeral-by-default behavior, ephemeral key cycling, persistent key file loading, and persistent generate-and-reuse lifecycle.
234 lines
6.9 KiB
Rust
234 lines
6.9 KiB
Rust
//! fipsctl — FIPS control client
|
|
//!
|
|
//! Connects to the FIPS daemon's Unix domain control socket, sends a
|
|
//! query command, and pretty-prints the JSON response.
|
|
|
|
use clap::{Parser, Subcommand};
|
|
use fips::config::{write_key_file, write_pub_file};
|
|
use fips::{encode_nsec, Identity};
|
|
use std::io::{BufRead, BufReader, Write};
|
|
use std::os::unix::net::UnixStream;
|
|
use std::path::{Path, PathBuf};
|
|
use std::time::Duration;
|
|
|
|
/// FIPS control client
|
|
#[derive(Parser, Debug)]
|
|
#[command(name = "fipsctl", version, about = "Query a running FIPS daemon")]
|
|
struct Cli {
|
|
/// Control socket path override
|
|
#[arg(short = 's', long)]
|
|
socket: Option<PathBuf>,
|
|
|
|
#[command(subcommand)]
|
|
command: Commands,
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
enum Commands {
|
|
/// Show node information
|
|
Show {
|
|
#[command(subcommand)]
|
|
what: ShowCommands,
|
|
},
|
|
/// Generate a new FIPS identity keypair
|
|
Keygen {
|
|
/// Output directory for fips.key and fips.pub
|
|
#[arg(short = 'd', long = "dir", default_value = "/etc/fips")]
|
|
dir: PathBuf,
|
|
/// Overwrite existing key files
|
|
#[arg(short = 'f', long = "force")]
|
|
force: bool,
|
|
/// Print nsec and npub to stdout instead of writing files
|
|
#[arg(short = 's', long = "stdout")]
|
|
stdout: bool,
|
|
},
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
enum ShowCommands {
|
|
/// Node status overview
|
|
Status,
|
|
/// Authenticated peers
|
|
Peers,
|
|
/// Active links
|
|
Links,
|
|
/// Spanning tree state
|
|
Tree,
|
|
/// End-to-end sessions
|
|
Sessions,
|
|
/// Bloom filter state
|
|
Bloom,
|
|
/// MMP metrics summary
|
|
Mmp,
|
|
/// Coordinate cache stats
|
|
Cache,
|
|
/// Pending handshake connections
|
|
Connections,
|
|
/// Transport instances
|
|
Transports,
|
|
/// Routing table summary
|
|
Routing,
|
|
}
|
|
|
|
impl ShowCommands {
|
|
fn command_name(&self) -> &'static str {
|
|
match self {
|
|
ShowCommands::Status => "show_status",
|
|
ShowCommands::Peers => "show_peers",
|
|
ShowCommands::Links => "show_links",
|
|
ShowCommands::Tree => "show_tree",
|
|
ShowCommands::Sessions => "show_sessions",
|
|
ShowCommands::Bloom => "show_bloom",
|
|
ShowCommands::Mmp => "show_mmp",
|
|
ShowCommands::Cache => "show_cache",
|
|
ShowCommands::Connections => "show_connections",
|
|
ShowCommands::Transports => "show_transports",
|
|
ShowCommands::Routing => "show_routing",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Determine the default socket path.
|
|
fn default_socket_path() -> PathBuf {
|
|
if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
|
|
PathBuf::from(format!("{runtime_dir}/fips/control.sock"))
|
|
} else if Path::new("/run/fips/control.sock").exists() {
|
|
PathBuf::from("/run/fips/control.sock")
|
|
} else {
|
|
PathBuf::from("/tmp/fips-control.sock")
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let cli = Cli::parse();
|
|
|
|
// Commands that don't require a running daemon
|
|
match &cli.command {
|
|
Commands::Keygen {
|
|
dir,
|
|
force,
|
|
stdout,
|
|
} => {
|
|
let identity = Identity::generate();
|
|
let nsec = encode_nsec(&identity.keypair().secret_key());
|
|
let npub = identity.npub();
|
|
|
|
if *stdout {
|
|
println!("{}", nsec);
|
|
println!("{}", npub);
|
|
return;
|
|
}
|
|
|
|
let key_path = dir.join("fips.key");
|
|
let pub_path = dir.join("fips.pub");
|
|
|
|
if key_path.exists() && !force {
|
|
eprintln!("error: key file already exists: {}", key_path.display());
|
|
eprintln!("Use --force to overwrite.");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
if let Err(e) = std::fs::create_dir_all(dir) {
|
|
eprintln!("error: cannot create directory {}: {}", dir.display(), e);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
if let Err(e) = write_key_file(&key_path, &nsec) {
|
|
eprintln!("error: failed to write key file: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
if let Err(e) = write_pub_file(&pub_path, &npub) {
|
|
eprintln!("error: failed to write pub file: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
eprintln!("{}", npub);
|
|
eprintln!("Key files written to: {}/", dir.display());
|
|
eprintln!();
|
|
eprintln!("NOTE: Set 'node.identity.persistent: true' in fips.yaml");
|
|
eprintln!(" or these keys will be overwritten on next daemon start.");
|
|
return;
|
|
}
|
|
Commands::Show { .. } => {}
|
|
}
|
|
|
|
let socket_path = cli.socket.unwrap_or_else(default_socket_path);
|
|
let command_name = match &cli.command {
|
|
Commands::Show { what } => what.command_name(),
|
|
Commands::Keygen { .. } => unreachable!(),
|
|
};
|
|
|
|
// Connect to the control socket
|
|
let mut stream = match UnixStream::connect(&socket_path) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
eprintln!(
|
|
"error: cannot connect to {}: {}",
|
|
socket_path.display(),
|
|
e
|
|
);
|
|
eprintln!("Is the FIPS daemon running?");
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
// Set timeouts
|
|
let timeout = Duration::from_secs(2);
|
|
let _ = stream.set_read_timeout(Some(timeout));
|
|
let _ = stream.set_write_timeout(Some(timeout));
|
|
|
|
// Send request
|
|
let request = format!("{{\"command\":\"{}\"}}\n", command_name);
|
|
if let Err(e) = stream.write_all(request.as_bytes()) {
|
|
eprintln!("error: failed to send request: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
// Shutdown write half to signal end of request
|
|
let _ = stream.shutdown(std::net::Shutdown::Write);
|
|
|
|
// Read response
|
|
let reader = BufReader::new(&stream);
|
|
let response_line = match reader.lines().next() {
|
|
Some(Ok(line)) => line,
|
|
Some(Err(e)) => {
|
|
eprintln!("error: failed to read response: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
None => {
|
|
eprintln!("error: no response from daemon");
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
// Parse and pretty-print
|
|
let Ok(value) = serde_json::from_str::<serde_json::Value>(&response_line) else {
|
|
// Not JSON, print raw
|
|
println!("{}", response_line);
|
|
return;
|
|
};
|
|
|
|
let status = value
|
|
.get("status")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("unknown");
|
|
|
|
if status == "error" {
|
|
let msg = value
|
|
.get("message")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("unknown error");
|
|
eprintln!("error: {}", msg);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
// Pretty-print the data field (or whole response if no data field)
|
|
let output = if let Some(data) = value.get("data") {
|
|
serde_json::to_string_pretty(data).unwrap_or(response_line)
|
|
} else {
|
|
serde_json::to_string_pretty(&value).unwrap_or(response_line)
|
|
};
|
|
println!("{}", output);
|
|
}
|