Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Failing after 2s
Bakes the FIPS (Free Internetworking Peering System) mesh daemon into the node stack, supervised by archipelago alongside Tor. Runs as a system service, identity derives from the same BIP-39 master seed, and user-triggered updates track upstream main. Identity seed.rs: new HKDF label archipelago/fips/secp256k1/v1 → dedicated secp256k1 key, distinct from the Nostr-node key for crypto isolation but still seed-recoverable identity.rs: writes fips_key[.pub] to /data/identity on onboarding, chmod 0600; fips_key_exists / load_fips_keys / fips_npub accessors Transport TransportKind::Fips=3 inserted between LAN and Tor (Tor bumps to 4) → router prefers FIPS over Tor for all peer traffic PeerRecord gains fips_npub + last_fips fields (serde(default) for backward-compat with older nodes) transport/fips.rs: NodeTransport stub, reports unavailable until the daemon is live so router falls through to Tor cleanly Federation invites FederatedNode and FederationInvite carry optional fips_npub create_invite / accept_invite / peer-joined callback thread it end to end; signature domain deliberately unchanged — FIPS Noise does its own session auth, so the unsigned hint only affects path selection crate::fips config.rs: renders /etc/fips/fips.yaml and sudo-installs key material service.rs: systemctl status/activate/restart/mask wrappers update.rs: GitHub API check against upstream main; apply stubbed until per-commit .deb artefact source is decided RPC + dashboard fips.status / fips.check-update / fips.apply-update / fips.install / fips.restart registered in dispatcher HomeNetworkCard.vue shipped standalone (unmounted — place in Home.vue when ready); shows state pill, version, FIPS npub, update button, activate button when key is present but service is down ISO + systemd archipelago-fips.service: conditional on key presence, masked by default — backend unmasks after onboarding writes the key build-auto-installer-iso.sh: multi-stage Dockerfile builds the FIPS .deb from jmcorgan/fips main (fail-loud), COPYs it into rootfs, apt installs it so trixie resolves deps; unit copied + masked Version bump: 1.3.5 → 1.4.0 Tests: 33 new/updated passing (seed, identity, transport, federation, fips module, transport::fips). Known gaps: fips.apply-update returns a clear stub error until upstream publishes per-commit .deb artefacts; HomeNetworkCard is not mounted in Home.vue by default. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
145 lines
4.7 KiB
Rust
145 lines
4.7 KiB
Rust
//! 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"));
|
|
}
|
|
}
|