archy/core/archipelago/src/identity.rs

642 lines
24 KiB
Rust
Raw Normal View History

//! Node identity: persistent Ed25519 key for private identification.
//! Enables future P2P features (file transfer, streaming, ecash/Lightning).
//! Supports did:key (W3C) for Web5/DID interoperability.
use anyhow::{Context, Result};
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use rand::rngs::OsRng;
use std::path::{Path, PathBuf};
use tokio::fs;
const NODE_KEY_FILE: &str = "node_key";
const NODE_KEY_PUB_FILE: &str = "node_key.pub";
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
const FIPS_KEY_FILE: &str = "fips_key";
const FIPS_KEY_PUB_FILE: &str = "fips_key.pub";
/// Persistent node identity (Ed25519 keypair).
/// Survives reboots; used for signing, verification, and node address.
pub struct NodeIdentity {
signing_key: SigningKey,
_identity_dir: PathBuf,
}
impl NodeIdentity {
/// Load existing identity or create and persist a new one.
pub async fn load_or_create(identity_dir: &Path) -> Result<Self> {
fs::create_dir_all(identity_dir)
.await
.context("Failed to create identity directory")?;
let key_path = identity_dir.join(NODE_KEY_FILE);
let pub_path = identity_dir.join(NODE_KEY_PUB_FILE);
let signing_key = if key_path.exists() {
let bytes = fs::read(&key_path)
.await
.context("Failed to read node key")?;
let arr: [u8; 32] = bytes
.try_into()
.map_err(|_| anyhow::anyhow!("Invalid node key length"))?;
let key = SigningKey::from_bytes(&arr);
let pubkey_hex = hex::encode(key.verifying_key().as_bytes());
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
tracing::info!(
"Loaded existing node identity (pubkey: {}...)",
&pubkey_hex[..16]
);
key
} else {
let signing_key = SigningKey::generate(&mut OsRng);
fs::write(&key_path, signing_key.to_bytes())
.await
.context("Failed to write node key")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600))
.await
.context("Failed to set key permissions")?;
}
fs::write(&pub_path, signing_key.verifying_key().as_bytes())
.await
.context("Failed to write node public key")?;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
tracing::info!(
"🔑 Generated new node identity at {}",
identity_dir.display()
);
signing_key
};
Ok(Self {
signing_key,
_identity_dir: identity_dir.to_path_buf(),
})
}
/// Create node identity from a BIP-39 master seed (deterministic derivation).
/// Writes derived key to disk in the same format as load_or_create.
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
/// Also derives and persists the FIPS mesh transport key so the
/// FIPS system service can be unmasked after onboarding.
pub async fn from_seed(identity_dir: &Path, seed: &crate::seed::MasterSeed) -> Result<Self> {
fs::create_dir_all(identity_dir)
.await
.context("Failed to create identity directory")?;
let signing_key = crate::seed::derive_node_ed25519(seed)?;
let key_path = identity_dir.join(NODE_KEY_FILE);
let pub_path = identity_dir.join(NODE_KEY_PUB_FILE);
fs::write(&key_path, signing_key.to_bytes())
.await
.context("Failed to write node key")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600))
.await
.context("Failed to set key permissions")?;
}
fs::write(&pub_path, signing_key.verifying_key().as_bytes())
.await
.context("Failed to write node public key")?;
let pubkey_hex = hex::encode(signing_key.verifying_key().as_bytes());
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
tracing::info!(
"Derived node identity from seed (pubkey: {}...)",
&pubkey_hex[..16]
);
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
write_fips_key_from_seed(identity_dir, seed).await?;
Ok(Self {
signing_key,
_identity_dir: identity_dir.to_path_buf(),
})
}
/// Check if a node key already exists on disk.
pub fn key_exists(identity_dir: &Path) -> bool {
identity_dir.join(NODE_KEY_FILE).exists()
}
2026-03-17 00:03:08 +00:00
/// Access the signing key (for key derivation, e.g. mesh encryption).
pub fn signing_key(&self) -> &SigningKey {
&self.signing_key
}
/// Public key as hex string (for ServerInfo, Nostr, etc.)
pub fn pubkey_hex(&self) -> String {
hex::encode(self.signing_key.verifying_key().as_bytes())
}
/// Stable node ID derived from pubkey (first 16 chars of hex).
pub fn node_id(&self) -> String {
self.pubkey_hex().chars().take(16).collect()
}
/// Sign data; returns hex-encoded signature.
pub fn sign(&self, data: &[u8]) -> String {
hex::encode(self.signing_key.sign(data).to_bytes())
}
/// Verify a signature from a peer (pubkey hex, data, signature hex).
pub fn verify(pubkey_hex: &str, data: &[u8], sig_hex: &str) -> Result<bool> {
let bytes = hex::decode(pubkey_hex).context("Invalid pubkey hex")?;
let verifying_key = VerifyingKey::from_bytes(
bytes
.as_slice()
.try_into()
.map_err(|_| anyhow::anyhow!("Invalid pubkey length"))?,
)?;
let sig_bytes = hex::decode(sig_hex).context("Invalid signature hex")?;
let sig = Signature::from_bytes(
sig_bytes
.as_slice()
.try_into()
.map_err(|_| anyhow::anyhow!("Invalid signature length"))?,
);
Ok(verifying_key.verify(data, &sig).is_ok())
}
/// Node address format for invites: archipelago://<onion>#<pubkey>
pub fn node_address(&self, onion: &str) -> String {
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
format!(
"archipelago://{}#{}",
onion.trim_end_matches('/'),
self.pubkey_hex()
)
}
/// DID in did:key format (W3C did:key method, Ed25519).
/// Format: did:key:z&lt;base58btc(multicodec_ed25519_pub + 32-byte pubkey)&gt;
pub fn did_key(&self) -> Result<String> {
did_key_from_pubkey_hex(&self.pubkey_hex())
.map_err(|e| anyhow::anyhow!("Invalid pubkey hex: {}", e))
}
/// Generate a W3C DID Document for this identity.
#[allow(dead_code)]
pub fn did_document(&self) -> Result<serde_json::Value> {
did_document_from_pubkey_hex(&self.pubkey_hex())
}
}
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 mesh transport key ────────────────────────────────────────────
//
// FIPS (Free Internetworking Peering System) uses a secp256k1 keypair as its
// native node identity — independent of the Nostr-node key so compromise of
// one surface cannot impersonate on the other. Both are seed-derived, so the
// FIPS npub is recoverable from the master mnemonic.
//
// Key material is written by `NodeIdentity::from_seed` only. Pre-onboarding
// the files do not exist and `archipelago-fips.service` stays masked.
use nostr_sdk::ToBech32;
async fn write_fips_key_from_seed(
identity_dir: &Path,
seed: &crate::seed::MasterSeed,
) -> Result<()> {
let keys = crate::seed::derive_fips_key(seed)?;
let key_path = identity_dir.join(FIPS_KEY_FILE);
let pub_path = identity_dir.join(FIPS_KEY_PUB_FILE);
fix(fips,iso): bulletproof FIPS from install — no Activate button needed Problems addressed (all observed on .198): * fips_key was written as raw 32 bytes; upstream fips daemon reads it with read_to_string() and bailed with "stream did not contain valid UTF-8", crashlooping indefinitely. * Activate button racy: user had to hit it, and it would keep failing silently because the daemon couldn't parse its own config. * FIPS schema drift (already fixed in 7d8a5864) put the config write path behind the same broken "Activate" flow, so the fix alone didn't help existing nodes. * Journal was on tmpfs — every reboot wiped install/onboarding history, making post-hoc debugging impossible. Changes: * identity.rs: write fips_key as bech32 nsec + newline. load_fips_keys now auto-migrates legacy 32-byte files to bech32 the first time it reads them, so OTA updates from v1.5.0-alpha self-heal without user action. * server.rs: post-onboarding auto-activate task runs on every archipelago startup. If fips_key exists it ensures /etc/fips/fips.yaml is schema-current and starts archipelago-fips.service. Pre-onboarding nodes stay quiet (guarded on fips_key_exists). * ISO build: un-mask archipelago-fips + archipelago-wg + wg-address — all use ConditionPathExists on their key files, so systemd silently skips them pre-onboarding (no MOTD [FAILED]). Only nostr-vpn stays masked (legacy service, superseded by upstream fips). * Journald made persistent via /var/log/journal + 500M cap, so install and first-boot logs survive reboots for diagnosis. After this, a fresh install + onboarding should bring FIPS up automatically with no user interaction. The UI "Activate" button can stay as an escape hatch (the RPC is still there) but is no longer on the critical path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:33:21 -04:00
// fips daemon reads the key with `fs::read_to_string` and expects a
// bech32 nsec line — raw 32-byte secret bytes fail its UTF-8 check
// ("failed to read config file /etc/fips/fips.key: stream did not
// contain valid UTF-8"). Write the bech32 form with a trailing
// newline so both archipelago and fips load it cleanly.
let nsec = keys
.secret_key()
.to_bech32()
.context("Failed to encode FIPS nsec")?;
fs::write(&key_path, format!("{nsec}\n"))
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
.await
.context("Failed to write FIPS key")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600))
.await
.context("Failed to set FIPS key permissions")?;
}
release(v1.7.14-alpha): install overlay + FIPS real fix + AIUI restore Install UX SystemUpdate.vue now shows a full-screen overlay after apply: the BitcoinFaceAscii logo, a target-version label, an indeterminate progress stripe (solid orange; solid green on ready), and an elapsed-time readout. Polls /health every 1.5s and auto-reloads once the backend reports the new version. 3-min stall → "Reload now" button. Download UI also shows a spinner + "Finishing download — verifying checksum…" while the fake bar sits at 95%. FIPS reconnect — for real this time New fips.reconnect RPC does stop → start → wait 20s → re-poll → classify. Classification buckets: connected / daemon_down / no_seed_key / no_outbound_udp_or_anchor_down / peers_but_no_anchor, each with a plain-language hint surfaced verbatim by the Reconnect button. The real reason nodes like .198/.253 couldn't reach the anchor: identity::write_fips_key_from_seed was writing fips_key.pub as a bech32 npub TEXT file, but upstream fips expects 32 raw bytes. The daemon silently authenticated with garbage. Fix: PublicKey::to_bytes() → raw 32 bytes, and new fips::config::normalize_pub_file migrates legacy files by decoding the npub and rewriting in place. fips.reconnect also re-installs the config + healed keys to /etc/fips before restarting. AIUI preservation + restore apply_update was wiping /opt/archipelago/web-ui/aiui because the Vue build doesn't include it — every OTA lost the Claude sidebar. The preserve block now copies aiui/ + archipelago-companion.apk from the old web-ui into the staging dir before the swap, and prefers new-tar versions if present. To restore it on the three nodes that already lost it (.116/.198/.253), this release bundles the 85 MB aiui build into the frontend tarball. Frontend component size is now ~155 MB. Download / install timeouts Backend download client timeout 1800s → 3600s (1 h). Larger tarball + slow gitea raw throughput put us above the old cap. Frontend update.download rpc timeout 30 min → 65 min to match. package.install rpc timeout 15 min → 45 min — IndeedHub pulls 6 images and was timing out mid-install. UI nit "Rollback to Previous" → "Rollback Available". App-catalog proxy already landed in v1.7.13. Artefacts: archipelago 725e18e6…3c525e6 40462288 archipelago-frontend-1.7.14-alpha.tar.gz c35284be…ff2c16 162077052 (+aiui) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:40:25 -04:00
// Upstream fips daemon expects 32 raw bytes in /etc/fips/fips.pub —
// not a bech32 npub string. Writing the bech32 form here meant the
// installed .pub file was a 63-char text file the daemon parsed as
// 63 raw bytes of garbage, so it couldn't identify itself to peers
// and the anchor never handshook. Write the raw public-key bytes
// (PublicKey::to_bytes returns a [u8; 32]) so the daemon reads
// them directly.
let raw_pub: [u8; 32] = keys.public_key().to_bytes();
fs::write(&pub_path, raw_pub)
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
.await
.context("Failed to write FIPS public key")?;
release(v1.7.14-alpha): install overlay + FIPS real fix + AIUI restore Install UX SystemUpdate.vue now shows a full-screen overlay after apply: the BitcoinFaceAscii logo, a target-version label, an indeterminate progress stripe (solid orange; solid green on ready), and an elapsed-time readout. Polls /health every 1.5s and auto-reloads once the backend reports the new version. 3-min stall → "Reload now" button. Download UI also shows a spinner + "Finishing download — verifying checksum…" while the fake bar sits at 95%. FIPS reconnect — for real this time New fips.reconnect RPC does stop → start → wait 20s → re-poll → classify. Classification buckets: connected / daemon_down / no_seed_key / no_outbound_udp_or_anchor_down / peers_but_no_anchor, each with a plain-language hint surfaced verbatim by the Reconnect button. The real reason nodes like .198/.253 couldn't reach the anchor: identity::write_fips_key_from_seed was writing fips_key.pub as a bech32 npub TEXT file, but upstream fips expects 32 raw bytes. The daemon silently authenticated with garbage. Fix: PublicKey::to_bytes() → raw 32 bytes, and new fips::config::normalize_pub_file migrates legacy files by decoding the npub and rewriting in place. fips.reconnect also re-installs the config + healed keys to /etc/fips before restarting. AIUI preservation + restore apply_update was wiping /opt/archipelago/web-ui/aiui because the Vue build doesn't include it — every OTA lost the Claude sidebar. The preserve block now copies aiui/ + archipelago-companion.apk from the old web-ui into the staging dir before the swap, and prefers new-tar versions if present. To restore it on the three nodes that already lost it (.116/.198/.253), this release bundles the 85 MB aiui build into the frontend tarball. Frontend component size is now ~155 MB. Download / install timeouts Backend download client timeout 1800s → 3600s (1 h). Larger tarball + slow gitea raw throughput put us above the old cap. Frontend update.download rpc timeout 30 min → 65 min to match. package.install rpc timeout 15 min → 45 min — IndeedHub pulls 6 images and was timing out mid-install. UI nit "Rollback to Previous" → "Rollback Available". App-catalog proxy already landed in v1.7.13. Artefacts: archipelago 725e18e6…3c525e6 40462288 archipelago-frontend-1.7.14-alpha.tar.gz c35284be…ff2c16 162077052 (+aiui) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:40:25 -04:00
let npub_for_log = keys.public_key().to_bech32().unwrap_or_default();
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
tracing::info!(
"Derived FIPS mesh key from seed (npub: {}...)",
release(v1.7.14-alpha): install overlay + FIPS real fix + AIUI restore Install UX SystemUpdate.vue now shows a full-screen overlay after apply: the BitcoinFaceAscii logo, a target-version label, an indeterminate progress stripe (solid orange; solid green on ready), and an elapsed-time readout. Polls /health every 1.5s and auto-reloads once the backend reports the new version. 3-min stall → "Reload now" button. Download UI also shows a spinner + "Finishing download — verifying checksum…" while the fake bar sits at 95%. FIPS reconnect — for real this time New fips.reconnect RPC does stop → start → wait 20s → re-poll → classify. Classification buckets: connected / daemon_down / no_seed_key / no_outbound_udp_or_anchor_down / peers_but_no_anchor, each with a plain-language hint surfaced verbatim by the Reconnect button. The real reason nodes like .198/.253 couldn't reach the anchor: identity::write_fips_key_from_seed was writing fips_key.pub as a bech32 npub TEXT file, but upstream fips expects 32 raw bytes. The daemon silently authenticated with garbage. Fix: PublicKey::to_bytes() → raw 32 bytes, and new fips::config::normalize_pub_file migrates legacy files by decoding the npub and rewriting in place. fips.reconnect also re-installs the config + healed keys to /etc/fips before restarting. AIUI preservation + restore apply_update was wiping /opt/archipelago/web-ui/aiui because the Vue build doesn't include it — every OTA lost the Claude sidebar. The preserve block now copies aiui/ + archipelago-companion.apk from the old web-ui into the staging dir before the swap, and prefers new-tar versions if present. To restore it on the three nodes that already lost it (.116/.198/.253), this release bundles the 85 MB aiui build into the frontend tarball. Frontend component size is now ~155 MB. Download / install timeouts Backend download client timeout 1800s → 3600s (1 h). Larger tarball + slow gitea raw throughput put us above the old cap. Frontend update.download rpc timeout 30 min → 65 min to match. package.install rpc timeout 15 min → 45 min — IndeedHub pulls 6 images and was timing out mid-install. UI nit "Rollback to Previous" → "Rollback Available". App-catalog proxy already landed in v1.7.13. Artefacts: archipelago 725e18e6…3c525e6 40462288 archipelago-frontend-1.7.14-alpha.tar.gz c35284be…ff2c16 162077052 (+aiui) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:40:25 -04:00
npub_for_log.chars().take(20).collect::<String>()
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
);
Ok(())
}
/// Check whether the FIPS keypair has been materialised on disk.
/// Returns true only after onboarding has written the seed-derived key.
#[allow(dead_code)]
pub fn fips_key_exists(identity_dir: &Path) -> bool {
identity_dir.join(FIPS_KEY_FILE).exists()
}
/// Load the persisted FIPS keypair. Returns `Ok(None)` if onboarding has
/// not yet written the key (pre-onboarding node); errors only on I/O or
/// corruption of an existing file.
#[allow(dead_code)]
pub async fn load_fips_keys(identity_dir: &Path) -> Result<Option<nostr_sdk::Keys>> {
let key_path = identity_dir.join(FIPS_KEY_FILE);
fix(fips,iso): bulletproof FIPS from install — no Activate button needed Problems addressed (all observed on .198): * fips_key was written as raw 32 bytes; upstream fips daemon reads it with read_to_string() and bailed with "stream did not contain valid UTF-8", crashlooping indefinitely. * Activate button racy: user had to hit it, and it would keep failing silently because the daemon couldn't parse its own config. * FIPS schema drift (already fixed in 7d8a5864) put the config write path behind the same broken "Activate" flow, so the fix alone didn't help existing nodes. * Journal was on tmpfs — every reboot wiped install/onboarding history, making post-hoc debugging impossible. Changes: * identity.rs: write fips_key as bech32 nsec + newline. load_fips_keys now auto-migrates legacy 32-byte files to bech32 the first time it reads them, so OTA updates from v1.5.0-alpha self-heal without user action. * server.rs: post-onboarding auto-activate task runs on every archipelago startup. If fips_key exists it ensures /etc/fips/fips.yaml is schema-current and starts archipelago-fips.service. Pre-onboarding nodes stay quiet (guarded on fips_key_exists). * ISO build: un-mask archipelago-fips + archipelago-wg + wg-address — all use ConditionPathExists on their key files, so systemd silently skips them pre-onboarding (no MOTD [FAILED]). Only nostr-vpn stays masked (legacy service, superseded by upstream fips). * Journald made persistent via /var/log/journal + 500M cap, so install and first-boot logs survive reboots for diagnosis. After this, a fresh install + onboarding should bring FIPS up automatically with no user interaction. The UI "Activate" button can stay as an escape hatch (the RPC is still there) but is no longer on the critical path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:33:21 -04:00
// Read as raw bytes so we can detect and migrate both formats:
// - v1.6+: bech32 nsec text (what upstream fips expects)
// - <=v1.5: raw 32-byte secret (incompatible with upstream fips)
// When we find the legacy format, rewrite the file in bech32 in place
// so archipelago-fips.service stops crashlooping after an OTA update
// from a release that shipped the old format.
let bytes = match fs::read(&key_path).await {
Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(e).context("Failed to read FIPS key"),
};
// Try bech32 first.
if let Ok(text) = std::str::from_utf8(&bytes) {
if let Ok(secret) = nostr_sdk::SecretKey::parse(text.trim()) {
return Ok(Some(nostr_sdk::Keys::new(secret)));
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
}
}
fix(fips,iso): bulletproof FIPS from install — no Activate button needed Problems addressed (all observed on .198): * fips_key was written as raw 32 bytes; upstream fips daemon reads it with read_to_string() and bailed with "stream did not contain valid UTF-8", crashlooping indefinitely. * Activate button racy: user had to hit it, and it would keep failing silently because the daemon couldn't parse its own config. * FIPS schema drift (already fixed in 7d8a5864) put the config write path behind the same broken "Activate" flow, so the fix alone didn't help existing nodes. * Journal was on tmpfs — every reboot wiped install/onboarding history, making post-hoc debugging impossible. Changes: * identity.rs: write fips_key as bech32 nsec + newline. load_fips_keys now auto-migrates legacy 32-byte files to bech32 the first time it reads them, so OTA updates from v1.5.0-alpha self-heal without user action. * server.rs: post-onboarding auto-activate task runs on every archipelago startup. If fips_key exists it ensures /etc/fips/fips.yaml is schema-current and starts archipelago-fips.service. Pre-onboarding nodes stay quiet (guarded on fips_key_exists). * ISO build: un-mask archipelago-fips + archipelago-wg + wg-address — all use ConditionPathExists on their key files, so systemd silently skips them pre-onboarding (no MOTD [FAILED]). Only nostr-vpn stays masked (legacy service, superseded by upstream fips). * Journald made persistent via /var/log/journal + 500M cap, so install and first-boot logs survive reboots for diagnosis. After this, a fresh install + onboarding should bring FIPS up automatically with no user interaction. The UI "Activate" button can stay as an escape hatch (the RPC is still there) but is no longer on the critical path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:33:21 -04:00
// Fall through: treat as legacy raw bytes and migrate.
if bytes.len() == 32 {
let secret = nostr_sdk::SecretKey::from_slice(&bytes)
.map_err(|e| anyhow::anyhow!("Corrupt FIPS key on disk: {}", e))?;
let nsec = secret
.to_bech32()
.map_err(|e| anyhow::anyhow!("Failed to encode migrated nsec: {}", e))?;
fs::write(&key_path, format!("{nsec}\n"))
.await
.context("Failed to rewrite FIPS key in bech32 format")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600))
.await
.context("Failed to re-set FIPS key permissions after migration")?;
}
tracing::info!("Migrated legacy raw-bytes FIPS key to bech32 nsec text");
return Ok(Some(nostr_sdk::Keys::new(secret)));
}
anyhow::bail!(
"Corrupt FIPS key on disk (not bech32 nsec and not 32 raw bytes, size={})",
bytes.len()
)
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
}
/// Return the FIPS npub (bech32) if the key has been materialised.
#[allow(dead_code)]
pub async fn fips_npub(identity_dir: &Path) -> Result<Option<String>> {
Ok(load_fips_keys(identity_dir)
.await?
.and_then(|k| k.public_key().to_bech32().ok()))
}
/// Convert Ed25519 pubkey (hex) to did:key format.
/// Used by RPC when identity is loaded from state.
pub fn did_key_from_pubkey_hex(pubkey_hex: &str) -> Result<String> {
let bytes = hex::decode(pubkey_hex).context("Invalid pubkey hex")?;
if bytes.len() != 32 {
return Err(anyhow::anyhow!("Invalid pubkey length"));
}
let mut multicodec_pubkey = [0u8; 34];
multicodec_pubkey[0] = 0xed;
multicodec_pubkey[1] = 0x01;
multicodec_pubkey[2..34].copy_from_slice(&bytes);
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
Ok(format!(
"did:key:z{}",
bs58::encode(multicodec_pubkey).into_string()
))
}
/// Generate a W3C DID Core v1.0 compliant DID Document from an Ed25519 public key.
/// Follows: https://www.w3.org/TR/did-core/
/// Includes: verificationMethod, authentication, assertionMethod, keyAgreement contexts.
pub fn did_document_from_pubkey_hex(pubkey_hex: &str) -> Result<serde_json::Value> {
let did = did_key_from_pubkey_hex(pubkey_hex)?;
let pubkey_bytes = hex::decode(pubkey_hex).context("Invalid pubkey hex")?;
let pubkey_multibase = format!("z{}", bs58::encode(&pubkey_bytes).into_string());
let key_id = format!("{}#key-1", did);
// Build X25519 key agreement key from Ed25519 public key
// Ed25519 -> X25519 conversion (Montgomery form)
let ed_point = curve25519_dalek::edwards::CompressedEdwardsY(
pubkey_bytes
.as_slice()
.try_into()
.map_err(|_| anyhow::anyhow!("Invalid pubkey length"))?,
);
let x25519_key = if let Some(point) = ed_point.decompress() {
let montgomery = point.to_montgomery();
format!("z{}", bs58::encode(montgomery.as_bytes()).into_string())
} else {
// Fallback: use Ed25519 key if conversion fails
pubkey_multibase.clone()
};
let x25519_key_id = format!("{}#key-x25519-1", did);
Ok(serde_json::json!({
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/ed25519-2020/v1",
"https://w3id.org/security/suites/x25519-2020/v1"
],
"id": did,
"verificationMethod": [
{
"id": key_id,
"type": "Ed25519VerificationKey2020",
"controller": did,
"publicKeyMultibase": pubkey_multibase
},
{
"id": x25519_key_id,
"type": "X25519KeyAgreementKey2020",
"controller": did,
"publicKeyMultibase": x25519_key
}
],
"authentication": [key_id],
"assertionMethod": [key_id],
"capabilityInvocation": [key_id],
"capabilityDelegation": [key_id],
"keyAgreement": [x25519_key_id]
}))
}
2026-03-12 12:56:59 +00:00
/// Generate a DID Document that includes both the Ed25519 key and a Nostr secp256k1 key.
/// The Nostr key is added as an additional verification method, formally pairing
/// the two identities so a user can use either protocol.
pub fn did_document_with_nostr(
pubkey_hex: &str,
nostr_pubkey_hex: &str,
) -> Result<serde_json::Value> {
let mut doc = did_document_from_pubkey_hex(pubkey_hex)?;
let did = did_key_from_pubkey_hex(pubkey_hex)?;
let nostr_key_id = format!("{}#key-nostr-1", did);
// Add EcdsaSecp256k1VerificationKey2019 context
if let Some(contexts) = doc["@context"].as_array_mut() {
contexts.push(serde_json::json!(
"https://w3id.org/security/suites/secp256k1-2019/v1"
));
}
// Add Nostr secp256k1 key to verificationMethod array
if let Some(vms) = doc["verificationMethod"].as_array_mut() {
vms.push(serde_json::json!({
"id": nostr_key_id,
"type": "EcdsaSecp256k1VerificationKey2019",
"controller": did,
"publicKeyHex": nostr_pubkey_hex
}));
}
// Add to authentication (Nostr key can also authenticate)
if let Some(auth) = doc["authentication"].as_array_mut() {
auth.push(serde_json::json!(nostr_key_id));
}
Ok(doc)
}
/// Extract the raw 32-byte Ed25519 public key from a did:key string.
pub fn pubkey_bytes_from_did_key(did: &str) -> Result<[u8; 32]> {
let multibase_str = did
.strip_prefix("did:key:z")
.ok_or_else(|| anyhow::anyhow!("Invalid did:key format"))?;
let decoded = bs58::decode(multibase_str)
.into_vec()
.context("Invalid base58 in did:key")?;
if decoded.len() != 34 || decoded[0] != 0xed || decoded[1] != 0x01 {
return Err(anyhow::anyhow!("Invalid Ed25519 multicodec prefix"));
}
let mut pubkey = [0u8; 32];
pubkey.copy_from_slice(&decoded[2..34]);
Ok(pubkey)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_load_or_create_generates_new_identity() {
let dir = tempfile::tempdir().unwrap();
let identity_dir = dir.path().join("identity");
let identity = NodeIdentity::load_or_create(&identity_dir).await.unwrap();
// pubkey_hex should be 64 hex chars (32 bytes)
assert_eq!(identity.pubkey_hex().len(), 64);
// node_id should be first 16 chars of pubkey_hex
assert_eq!(identity.node_id(), &identity.pubkey_hex()[..16]);
}
#[tokio::test]
async fn test_load_or_create_persists_and_reloads() {
let dir = tempfile::tempdir().unwrap();
let identity_dir = dir.path().join("identity");
let identity1 = NodeIdentity::load_or_create(&identity_dir).await.unwrap();
let pubkey1 = identity1.pubkey_hex();
let identity2 = NodeIdentity::load_or_create(&identity_dir).await.unwrap();
let pubkey2 = identity2.pubkey_hex();
assert_eq!(pubkey1, pubkey2);
}
#[tokio::test]
async fn test_sign_and_verify() {
let dir = tempfile::tempdir().unwrap();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let identity = NodeIdentity::load_or_create(&dir.path().join("id"))
.await
.unwrap();
let data = b"hello world";
let sig = identity.sign(data);
let valid = NodeIdentity::verify(&identity.pubkey_hex(), data, &sig).unwrap();
assert!(valid);
}
#[tokio::test]
async fn test_verify_wrong_data() {
let dir = tempfile::tempdir().unwrap();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let identity = NodeIdentity::load_or_create(&dir.path().join("id"))
.await
.unwrap();
let sig = identity.sign(b"hello");
let valid = NodeIdentity::verify(&identity.pubkey_hex(), b"wrong", &sig).unwrap();
assert!(!valid);
}
#[tokio::test]
async fn test_did_key_format() {
let dir = tempfile::tempdir().unwrap();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let identity = NodeIdentity::load_or_create(&dir.path().join("id"))
.await
.unwrap();
let did = identity.did_key().unwrap();
assert!(did.starts_with("did:key:z"));
}
#[test]
fn test_did_key_from_pubkey_hex() {
// 32-byte all-zeros pubkey in hex
let hex = "0000000000000000000000000000000000000000000000000000000000000000";
let did = did_key_from_pubkey_hex(hex).unwrap();
assert!(did.starts_with("did:key:z"));
}
#[test]
fn test_did_key_from_invalid_hex() {
assert!(did_key_from_pubkey_hex("not_hex").is_err());
}
#[test]
fn test_did_key_from_wrong_length() {
assert!(did_key_from_pubkey_hex("0011").is_err());
}
#[tokio::test]
async fn test_node_address_format() {
let dir = tempfile::tempdir().unwrap();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let identity = NodeIdentity::load_or_create(&dir.path().join("id"))
.await
.unwrap();
let addr = identity.node_address("abc123.onion");
assert!(addr.starts_with("archipelago://abc123.onion#"));
assert!(addr.contains(&identity.pubkey_hex()));
}
#[tokio::test]
async fn test_did_document_w3c_structure() {
let dir = tempfile::tempdir().unwrap();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let identity = NodeIdentity::load_or_create(&dir.path().join("id"))
.await
.unwrap();
let doc = identity.did_document().unwrap();
let did = identity.did_key().unwrap();
// Verify @context
let context = doc["@context"].as_array().unwrap();
assert_eq!(context[0], "https://www.w3.org/ns/did/v1");
// Verify id matches did:key
assert_eq!(doc["id"], did);
// Verify verificationMethod has Ed25519 and X25519 keys
let vms = doc["verificationMethod"].as_array().unwrap();
assert_eq!(vms.len(), 2);
assert_eq!(vms[0]["type"], "Ed25519VerificationKey2020");
assert_eq!(vms[1]["type"], "X25519KeyAgreementKey2020");
assert_eq!(vms[0]["controller"], did);
// Verify authentication references key-1
let auth = doc["authentication"].as_array().unwrap();
assert_eq!(auth[0], format!("{}#key-1", did));
// Verify assertionMethod
assert!(doc["assertionMethod"].as_array().is_some());
// Verify keyAgreement references x25519 key
let ka = doc["keyAgreement"].as_array().unwrap();
assert_eq!(ka[0], format!("{}#key-x25519-1", did));
}
#[test]
fn test_did_document_from_pubkey_hex() {
let hex = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa3f4a18446b7e21e7e2c33";
let doc = did_document_from_pubkey_hex(hex).unwrap();
assert_eq!(doc["@context"].as_array().unwrap().len(), 3);
assert!(doc["id"].as_str().unwrap().starts_with("did:key:z"));
}
#[test]
fn test_pubkey_bytes_from_did_key_roundtrip() {
let hex = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa3f4a18446b7e21e7e2c33";
let did = did_key_from_pubkey_hex(hex).unwrap();
let recovered = pubkey_bytes_from_did_key(&did).unwrap();
assert_eq!(hex::encode(recovered), hex);
}
#[test]
fn test_pubkey_bytes_from_invalid_did() {
assert!(pubkey_bytes_from_did_key("did:web:example.com").is_err());
assert!(pubkey_bytes_from_did_key("did:key:invalid").is_err());
}
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
#[tokio::test]
async fn test_fips_key_absent_before_onboarding() {
let dir = tempfile::tempdir().unwrap();
let id_dir = dir.path().join("identity");
fs::create_dir_all(&id_dir).await.unwrap();
assert!(!fips_key_exists(&id_dir));
assert!(load_fips_keys(&id_dir).await.unwrap().is_none());
assert!(fips_npub(&id_dir).await.unwrap().is_none());
}
#[tokio::test]
async fn test_fips_key_written_from_seed_and_roundtrips() {
use crate::seed::MasterSeed;
const M: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art";
let dir = tempfile::tempdir().unwrap();
let id_dir = dir.path().join("identity");
let (_, seed) = MasterSeed::from_mnemonic_words(M).unwrap();
let _ = NodeIdentity::from_seed(&id_dir, &seed).await.unwrap();
assert!(fips_key_exists(&id_dir));
let loaded = load_fips_keys(&id_dir).await.unwrap().unwrap();
let expected = crate::seed::derive_fips_key(&seed).unwrap();
assert_eq!(
loaded.public_key().to_hex(),
expected.public_key().to_hex(),
"loaded FIPS key must match seed-derived key"
);
let npub = fips_npub(&id_dir).await.unwrap().unwrap();
assert!(npub.starts_with("npub1"), "got: {}", npub);
}
#[tokio::test]
async fn test_fips_private_key_is_chmod_600() {
#[cfg(unix)]
{
use crate::seed::MasterSeed;
use std::os::unix::fs::PermissionsExt;
const M: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art";
let dir = tempfile::tempdir().unwrap();
let id_dir = dir.path().join("identity");
let (_, seed) = MasterSeed::from_mnemonic_words(M).unwrap();
NodeIdentity::from_seed(&id_dir, &seed).await.unwrap();
let meta = fs::metadata(id_dir.join(FIPS_KEY_FILE)).await.unwrap();
let mode = meta.permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "FIPS private key must be 0600, got {:o}", mode);
}
}
}