feat(fips): integrate jmcorgan/fips as preferred non-Tor transport + v1.4.0
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Failing after 2s
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>
This commit is contained in:
@@ -404,6 +404,13 @@ impl RpcHandler {
|
||||
}
|
||||
"monitoring.export" => self.handle_monitoring_export(params).await,
|
||||
|
||||
// FIPS mesh transport
|
||||
"fips.status" => self.handle_fips_status().await,
|
||||
"fips.check-update" => self.handle_fips_check_update().await,
|
||||
"fips.apply-update" => self.handle_fips_apply_update().await,
|
||||
"fips.install" => self.handle_fips_install().await,
|
||||
"fips.restart" => self.handle_fips_restart().await,
|
||||
|
||||
// System updates
|
||||
"update.check" => self.handle_update_check().await,
|
||||
"update.status" => self.handle_update_status().await,
|
||||
|
||||
@@ -44,9 +44,19 @@ impl RpcHandler {
|
||||
anyhow::bail!("Tor address not available. Tor may not be running.");
|
||||
}
|
||||
|
||||
let code = federation::create_invite(&self.config.data_dir, &did, &onion, &pubkey).await?;
|
||||
let identity_dir = self.config.data_dir.join("identity");
|
||||
let fips_npub = identity::fips_npub(&identity_dir).await.unwrap_or(None);
|
||||
|
||||
info!(did = %did, "Generated federation invite");
|
||||
let code = federation::create_invite(
|
||||
&self.config.data_dir,
|
||||
&did,
|
||||
&onion,
|
||||
&pubkey,
|
||||
fips_npub.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!(did = %did, fips_advertised = fips_npub.is_some(), "Generated federation invite");
|
||||
Ok(serde_json::json!({
|
||||
"code": code,
|
||||
"did": did,
|
||||
@@ -72,12 +82,14 @@ impl RpcHandler {
|
||||
|
||||
let identity_dir = self.config.data_dir.join("identity");
|
||||
let node_identity = identity::NodeIdentity::load_or_create(&identity_dir).await?;
|
||||
let local_fips_npub = identity::fips_npub(&identity_dir).await.unwrap_or(None);
|
||||
let node = federation::accept_invite(
|
||||
&self.config.data_dir,
|
||||
code,
|
||||
&local_did,
|
||||
&local_onion,
|
||||
&local_pubkey,
|
||||
local_fips_npub.as_deref(),
|
||||
|data| node_identity.sign(data),
|
||||
)
|
||||
.await?;
|
||||
@@ -402,6 +414,12 @@ impl RpcHandler {
|
||||
.get("pubkey")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing 'pubkey'"))?;
|
||||
// Optional, unsigned: peer's FIPS mesh npub. Carried for transport
|
||||
// selection only; FIPS handshake re-authenticates the session.
|
||||
let fips_npub = params
|
||||
.get("fips_npub")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
// Verify ed25519 signature to prevent federation spoofing (H2 security fix)
|
||||
let signature = params.get("signature").and_then(|v| v.as_str());
|
||||
@@ -426,18 +444,24 @@ impl RpcHandler {
|
||||
|
||||
let nodes = federation::load_nodes(&self.config.data_dir).await?;
|
||||
if let Some(existing) = nodes.iter().find(|n| n.did == did) {
|
||||
// If already known but missing onion/pubkey, update them
|
||||
if existing.onion.is_empty() || existing.pubkey.is_empty() {
|
||||
// If already known but missing onion/pubkey/fips_npub, update them
|
||||
let needs_onion = existing.onion.is_empty();
|
||||
let needs_pubkey = existing.pubkey.is_empty();
|
||||
let needs_fips = existing.fips_npub.is_none() && fips_npub.is_some();
|
||||
if needs_onion || needs_pubkey || needs_fips {
|
||||
let mut updated = existing.clone();
|
||||
if existing.onion.is_empty() && !onion.is_empty() {
|
||||
if needs_onion && !onion.is_empty() {
|
||||
updated.onion = onion.to_string();
|
||||
}
|
||||
if existing.pubkey.is_empty() && !pubkey.is_empty() {
|
||||
if needs_pubkey && !pubkey.is_empty() {
|
||||
updated.pubkey = pubkey.to_string();
|
||||
}
|
||||
if needs_fips {
|
||||
updated.fips_npub = fips_npub.clone();
|
||||
}
|
||||
updated.last_seen = Some(chrono::Utc::now().to_rfc3339());
|
||||
federation::update_node(&self.config.data_dir, &updated).await?;
|
||||
info!(peer_did = %did, peer_onion = %onion, "Updated existing peer with missing onion/pubkey");
|
||||
info!(peer_did = %did, peer_onion = %onion, "Updated existing peer with fresh identity fields");
|
||||
}
|
||||
return Ok(serde_json::json!({ "accepted": true, "already_known": true }));
|
||||
}
|
||||
@@ -451,6 +475,7 @@ impl RpcHandler {
|
||||
added_at: chrono::Utc::now().to_rfc3339(),
|
||||
last_seen: None,
|
||||
last_state: None,
|
||||
fips_npub,
|
||||
};
|
||||
|
||||
federation::add_node(&self.config.data_dir, node).await?;
|
||||
@@ -866,11 +891,14 @@ impl RpcHandler {
|
||||
// Generate a one-shot federation invite. The code embeds OUR onion
|
||||
// and OUR pubkey, but it leaves this box only inside the NIP-44
|
||||
// ciphertext below.
|
||||
let identity_dir = self.config.data_dir.join("identity");
|
||||
let local_fips_npub = identity::fips_npub(&identity_dir).await.unwrap_or(None);
|
||||
let invite_code = federation::create_invite(
|
||||
&self.config.data_dir,
|
||||
&local_did,
|
||||
&local_onion,
|
||||
&local_pubkey,
|
||||
local_fips_npub.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
47
core/archipelago/src/api/rpc/fips.rs
Normal file
47
core/archipelago/src/api/rpc/fips.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
//! RPC handlers for the FIPS mesh transport subsystem.
|
||||
//!
|
||||
//! Surface is deliberately thin: a read-only `fips.status`, a user-gated
|
||||
//! `fips.check-update`, a stubbed `fips.apply-update`, and a
|
||||
//! `fips.install` that (re-)materialises the daemon config + key and
|
||||
//! activates the service. All writes go through `sudo` helpers in
|
||||
//! `crate::fips`.
|
||||
|
||||
use super::RpcHandler;
|
||||
use crate::fips;
|
||||
use anyhow::Result;
|
||||
|
||||
impl RpcHandler {
|
||||
pub(super) async fn handle_fips_status(&self) -> Result<serde_json::Value> {
|
||||
let identity_dir = fips::identity_dir_from(&self.config.data_dir);
|
||||
let status = fips::FipsStatus::query(&identity_dir).await;
|
||||
Ok(serde_json::to_value(status)?)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_fips_check_update(&self) -> Result<serde_json::Value> {
|
||||
let check = fips::update::check().await?;
|
||||
Ok(serde_json::to_value(check)?)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_fips_apply_update(&self) -> Result<serde_json::Value> {
|
||||
fips::update::apply().await?;
|
||||
Ok(serde_json::json!({ "applied": true }))
|
||||
}
|
||||
|
||||
/// Install config + key into /etc/fips and activate the service.
|
||||
/// Intended to be called:
|
||||
/// - once by the seed-onboarding flow, right after the FIPS key
|
||||
/// is written to /data/identity/fips_key, and
|
||||
/// - on user demand from the dashboard if something drifted.
|
||||
pub(super) async fn handle_fips_install(&self) -> Result<serde_json::Value> {
|
||||
let identity_dir = fips::identity_dir_from(&self.config.data_dir);
|
||||
fips::config::install(&identity_dir).await?;
|
||||
fips::service::activate(fips::SERVICE_UNIT).await?;
|
||||
let status = fips::FipsStatus::query(&identity_dir).await;
|
||||
Ok(serde_json::to_value(status)?)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_fips_restart(&self) -> Result<serde_json::Value> {
|
||||
fips::service::restart(fips::SERVICE_UNIT).await?;
|
||||
Ok(serde_json::json!({ "restarted": true }))
|
||||
}
|
||||
}
|
||||
@@ -278,12 +278,16 @@ impl RpcHandler {
|
||||
let identity_dir2 = self.config.data_dir.join("identity");
|
||||
let node_identity =
|
||||
crate::identity::NodeIdentity::load_or_create(&identity_dir2).await?;
|
||||
let local_fips_npub = crate::identity::fips_npub(&identity_dir2)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
match crate::federation::accept_invite(
|
||||
&self.config.data_dir,
|
||||
invite_code,
|
||||
&local_did,
|
||||
&local_onion,
|
||||
&local_pubkey,
|
||||
local_fips_npub.as_deref(),
|
||||
|bytes| node_identity.sign(bytes),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -8,6 +8,7 @@ mod credentials;
|
||||
mod dispatcher;
|
||||
mod dwn;
|
||||
mod federation;
|
||||
mod fips;
|
||||
mod handshake;
|
||||
mod identity;
|
||||
mod interfaces;
|
||||
|
||||
Reference in New Issue
Block a user