131 lines
4.6 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
//! User-triggered FIPS upgrade from upstream `main`.
//!
//! Flow (no auto-update, no background polling — user clicks a button):
//! 1. Query GitHub for the latest commit on `main` of jmcorgan/fips.
//! 2. Compare with the installed daemon version reported by
//! `fipsctl --version`. If identical, report "up to date".
//! 3. Fetch the built .deb artefact for that commit + its SHA256.
//! 4. SHA256-verify the download.
//! 5. `sudo dpkg -i` the .deb, `sudo systemctl restart` the service.
//!
//! The artefact URL / SHA256 source is not yet fixed — upstream doesn't
//! publish stable release assets for `main` builds. This module currently
//! implements steps 12 (the "is there anything newer?" query) and stubs
//! out 35 so the RPC/UI can wire through. The apply path returns a
//! clear "not yet available" error until the artefact source is decided.
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use super::{service, UPSTREAM_REPO};
const GITHUB_API: &str = "https://api.github.com";
const USER_AGENT: &str = "archipelago-fips-updater";
/// Result of `check_update()` — what the dashboard renders.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateCheck {
/// Currently installed daemon version (from `fipsctl --version`).
pub current: Option<String>,
/// Short SHA of the latest commit on upstream `main`.
pub latest_commit: String,
/// True when the installed version string does not mention the latest SHA.
pub update_available: bool,
/// Human-readable note for the UI.
pub notes: String,
}
/// Query GitHub for the latest commit on `main` and compare to the
/// installed version. Never errors on "no package installed" — that is
/// itself a valid state where an update is available (install needed).
pub async fn check() -> Result<UpdateCheck> {
let current = service::daemon_version().await.ok();
let latest = fetch_latest_main_sha().await?;
let short = latest.chars().take(7).collect::<String>();
let update_available = match &current {
Some(v) => !v.contains(&short),
None => true,
};
let notes = if update_available {
format!(
"Upstream main is at {}; installed: {}",
short,
current.as_deref().unwrap_or("not installed")
)
} else {
format!("Up to date ({})", short)
};
Ok(UpdateCheck {
current,
latest_commit: short,
update_available,
notes,
})
}
/// Apply the update. Stubbed pending a stable artefact source for
/// per-commit builds of the `fips` debian package. When this is wired
/// up it must: download → SHA256-verify → `sudo dpkg -i` → restart.
pub async fn apply() -> Result<()> {
anyhow::bail!(
"FIPS auto-apply not yet wired — upstream does not publish stable \
per-commit .deb artefacts for main. Upgrade manually for now: \
`git pull && cargo deb && sudo dpkg -i target/debian/fips_*.deb`."
)
}
async fn fetch_latest_main_sha() -> Result<String> {
let url = format!("{}/repos/{}/commits/main", GITHUB_API, UPSTREAM_REPO);
let client = reqwest::Client::builder()
.user_agent(USER_AGENT)
.timeout(std::time::Duration::from_secs(15))
.build()
.context("Build HTTP client")?;
let resp = client
.get(&url)
.header("Accept", "application/vnd.github+json")
.send()
.await
.context("GitHub commits API")?;
if !resp.status().is_success() {
anyhow::bail!("GitHub API returned {}", resp.status());
}
let body: serde_json::Value = resp.json().await.context("Parse commits JSON")?;
let sha = body
.get("sha")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("GitHub commits response missing sha field"))?;
Ok(sha.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_apply_returns_clear_stub_error() {
let err = apply().await.unwrap_err().to_string();
assert!(
err.contains("not yet wired"),
"apply() should return an explicit not-yet-wired error, got: {}",
err
);
}
#[test]
fn test_update_check_serialises() {
let uc = UpdateCheck {
current: Some("0.2.0-abc1234".to_string()),
latest_commit: "def5678".to_string(),
update_available: true,
notes: "test".to_string(),
};
let json = serde_json::to_string(&uc).unwrap();
assert!(json.contains("latest_commit"));
assert!(json.contains("update_available"));
}
}