141 lines
5.1 KiB
Rust
Raw Normal View History

feat(fips): integrate jmcorgan/fips as preferred non-Tor transport + v1.4.0 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>
2026-04-18 22:57:51 -04:00
//! FIPS (Free Internetworking Peering System) daemon integration.
//!
//! github.com/jmcorgan/fips — a spanning-tree mesh routing protocol that
//! uses Nostr secp256k1 keys as native node identity. Archipelago ships
//! the daemon as an apt package, feeds it the seed-derived key from
//! `/data/identity/fips_key`, and supervises it via
//! `archipelago-fips.service`.
//!
//! This module is the in-process bridge:
//! - [`service`]: systemctl status / start / stop / restart / unmask.
//! - [`config`]: materialise `/etc/fips/fips.yaml` + install the key.
//! - [`update`]: query GitHub (tracking `main`) for a newer build,
//! verify SHA256, install via dpkg, restart.
//!
//! Privileged operations shell out via `sudo systemctl …` and `sudo dpkg …`
//! (mirroring the vpn/update patterns already in the codebase); the
//! sudoers rule shipped in the ISO whitelists exactly those commands for
//! the `archipelago` service user.
//!
//! FIPS is dark on the wire until onboarding writes the key. Before that,
//! `FipsStatus::installed` reports the package state and `service_active`
//! returns false; the transport router keeps routing via Tor.
// Consumers land in the next phase (RPC endpoints + onboarding hookup);
// the module is deliberately API-ready ahead of those call-sites.
#![allow(dead_code)]
pub mod config;
pub mod service;
pub mod update;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
/// Systemd unit name supervised by archipelago.
pub const SERVICE_UNIT: &str = "archipelago-fips.service";
/// Path the FIPS daemon reads its config from (Debian package default).
pub const DAEMON_CONFIG_PATH: &str = "/etc/fips/fips.yaml";
/// Path the FIPS daemon reads its private key from.
pub const DAEMON_KEY_PATH: &str = "/etc/fips/fips.key";
/// Path the FIPS daemon reads its public key from.
pub const DAEMON_PUB_PATH: &str = "/etc/fips/fips.pub";
/// Upstream repository the updater tracks (branch `main`).
pub const UPSTREAM_REPO: &str = "jmcorgan/fips";
/// Default UDP port the daemon listens on.
pub const DEFAULT_UDP_PORT: u16 = 8668;
/// Aggregated runtime status of the FIPS subsystem, surfaced to the dashboard.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FipsStatus {
/// Whether the `fips` debian package is installed on the host.
pub installed: bool,
/// Installed daemon version string reported by `fipsctl --version`,
/// or None if not installed / not queryable.
pub version: Option<String>,
/// `systemctl is-active archipelago-fips.service` result: "active",
/// "inactive", "failed", "masked", "unknown".
pub service_state: String,
/// True iff service_state == "active".
pub service_active: bool,
/// Whether the seed-derived FIPS key has been materialised on disk.
/// The service cannot start meaningfully until this is true.
pub key_present: bool,
/// Local FIPS npub (bech32), present only once the key is on disk.
pub npub: Option<String>,
}
impl FipsStatus {
/// Snapshot the current state across package, key, and service.
pub async fn query(identity_dir: &Path) -> Self {
let installed = service::package_installed().await;
let version = if installed {
service::daemon_version().await.ok()
} else {
None
};
let service_state = service::unit_state(SERVICE_UNIT).await;
let service_active = service_state == "active";
let key_present = crate::identity::fips_key_exists(identity_dir);
let npub = crate::identity::fips_npub(identity_dir)
.await
.unwrap_or(None);
Self {
installed,
version,
service_state,
service_active,
key_present,
npub,
}
}
}
/// Compose a data-dirrelative identity directory path.
/// Mirrors the convention used elsewhere in the codebase so callers don't
/// have to repeat the `.join("identity")` each time.
pub fn identity_dir_from(data_dir: &Path) -> PathBuf {
data_dir.join("identity")
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_status_reports_no_key_pre_onboarding() {
let dir = tempfile::tempdir().unwrap();
let id_dir = dir.path().join("identity");
tokio::fs::create_dir_all(&id_dir).await.unwrap();
let status = FipsStatus::query(&id_dir).await;
assert!(!status.key_present, "no key before onboarding");
assert!(status.npub.is_none());
// `installed`, `service_state`, `version` depend on the host and are
// not asserted here — query() must return cleanly regardless.
}
#[test]
fn test_identity_dir_from() {
let data = Path::new("/var/lib/archipelago");
assert_eq!(
identity_dir_from(data),
Path::new("/var/lib/archipelago/identity")
);
}
#[test]
fn test_constants_have_expected_shape() {
assert!(SERVICE_UNIT.ends_with(".service"));
assert!(DAEMON_CONFIG_PATH.starts_with('/'));
assert!(DAEMON_KEY_PATH.starts_with('/'));
assert_eq!(UPSTREAM_REPO, "jmcorgan/fips");
}
}