//! FIPS daemon config + key materialisation. //! //! Writes `/etc/fips/fips.yaml`, `/etc/fips/fips.key`, and //! `/etc/fips/fips.pub` from the archipelago node's seed-derived FIPS //! keypair, then chmod 0600 the private key. //! //! Privileged filesystem writes go through a `sudo install` invocation //! rather than opening `/etc/fips/*` directly — the archipelago service //! user cannot write `/etc` itself. The sudoers policy in the ISO //! whitelists `install` into `/etc/fips/`. use anyhow::{Context, Result}; use std::path::Path; use tokio::process::Command; use super::{DAEMON_CONFIG_PATH, DAEMON_KEY_PATH, DAEMON_PUB_PATH, DEFAULT_UDP_PORT}; /// Write the FIPS daemon config based on the local npub and default /// transports. Overwrites any existing file — callers are expected to /// re-run this whenever the key or daemon version changes. /// /// Schema is intentionally minimal: node identity comes from the key /// file on disk (the daemon handles it), transports enable UDP + Tor, /// IPv6 TUN + DNS on defaults. Static peer list is empty — archipelago /// feeds peers dynamically via federation updates. pub fn render_config_yaml() -> String { format!( "# Generated by archipelago — do not edit by hand.\n\ # Regenerated on every key change and daemon upgrade.\n\ identity:\n \ key_file: {key_path}\n \ pub_file: {pub_path}\n\ transports:\n \ udp:\n \ enabled: true\n \ port: {port}\n \ tor:\n \ enabled: true\n\ tun:\n \ enabled: true\n\ dns:\n \ enabled: true\n \ suffix: .fips\n\ peers: []\n", key_path = DAEMON_KEY_PATH, pub_path = DAEMON_PUB_PATH, port = DEFAULT_UDP_PORT, ) } /// Install the local FIPS key + rendered config into `/etc/fips/`. /// Requires the seed-derived key to already exist at `identity_dir/fips_key`. pub async fn install(identity_dir: &Path) -> Result<()> { let src_key = identity_dir.join("fips_key"); let src_pub = identity_dir.join("fips_key.pub"); if !src_key.exists() { anyhow::bail!( "FIPS key not materialised at {} — run seed onboarding first", src_key.display() ); } // Ensure /etc/fips exists with mode 0755. sudo_install_dir("/etc/fips").await?; // Render + write the yaml via a staging file the archipelago user owns, // then `sudo install` it into place so we never need to write to // /etc directly. let yaml = render_config_yaml(); let stage = std::env::temp_dir().join(format!("fips-{}.yaml", std::process::id())); tokio::fs::write(&stage, yaml) .await .context("Failed to stage fips.yaml")?; let install_result = sudo_install_file(&stage, DAEMON_CONFIG_PATH, "0644").await; let _ = tokio::fs::remove_file(&stage).await; install_result?; sudo_install_file(&src_key, DAEMON_KEY_PATH, "0600").await?; sudo_install_file(&src_pub, DAEMON_PUB_PATH, "0644").await?; Ok(()) } async fn sudo_install_dir(path: &str) -> Result<()> { let out = Command::new("sudo") .args(["install", "-d", "-m", "0755", path]) .output() .await .with_context(|| format!("sudo install -d {}", path))?; if !out.status.success() { anyhow::bail!( "sudo install -d {}: {}", path, String::from_utf8_lossy(&out.stderr).trim() ); } Ok(()) } async fn sudo_install_file(src: &Path, dest: &str, mode: &str) -> Result<()> { let out = Command::new("sudo") .args([ "install", "-m", mode, src.to_str().context("Non-UTF8 source path")?, dest, ]) .output() .await .with_context(|| format!("sudo install {} -> {}", src.display(), dest))?; if !out.status.success() { anyhow::bail!( "sudo install {} -> {}: {}", src.display(), dest, String::from_utf8_lossy(&out.stderr).trim() ); } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_rendered_yaml_contains_paths_and_port() { let yaml = render_config_yaml(); assert!(yaml.contains(DAEMON_KEY_PATH)); assert!(yaml.contains(DAEMON_PUB_PATH)); assert!(yaml.contains(&DEFAULT_UDP_PORT.to_string())); assert!(yaml.contains("udp:")); assert!(yaml.contains("tor:")); assert!(yaml.contains("tun:")); } #[tokio::test] async fn test_install_refuses_when_key_missing() { let dir = tempfile::tempdir().unwrap(); let err = install(dir.path()).await.unwrap_err(); assert!(err.to_string().contains("FIPS key not materialised")); } }