2026-02-17 15:03:34 +00:00
|
|
|
//! 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";
|
2026-02-17 15:03:34 +00:00
|
|
|
|
|
|
|
|
/// Persistent node identity (Ed25519 keypair).
|
|
|
|
|
/// Survives reboots; used for signing, verification, and node address.
|
|
|
|
|
pub struct NodeIdentity {
|
|
|
|
|
signing_key: SigningKey,
|
2026-03-22 03:30:21 +00:00
|
|
|
_identity_dir: PathBuf,
|
2026-02-17 15:03:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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"))?;
|
2026-03-19 19:19:13 +00:00
|
|
|
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]
|
|
|
|
|
);
|
2026-03-19 19:19:13 +00:00
|
|
|
key
|
2026-02-17 15:03:34 +00:00
|
|
|
} 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()
|
|
|
|
|
);
|
2026-02-17 15:03:34 +00:00
|
|
|
signing_key
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
|
signing_key,
|
2026-03-22 03:30:21 +00:00
|
|
|
_identity_dir: identity_dir.to_path_buf(),
|
2026-02-17 15:03:34 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 01:41:24 +01:00
|
|
|
/// 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.
|
2026-03-31 01:41:24 +01:00
|
|
|
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]
|
|
|
|
|
);
|
2026-03-31 01:41:24 +01:00
|
|
|
|
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?;
|
|
|
|
|
|
2026-03-31 01:41:24 +01:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 15:03:34 +00:00
|
|
|
/// 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()
|
|
|
|
|
)
|
2026-02-17 15:03:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// DID in did:key format (W3C did:key method, Ed25519).
|
|
|
|
|
/// Format: did:key:z<base58btc(multicodec_ed25519_pub + 32-byte pubkey)>
|
2026-03-21 01:54:35 +00:00
|
|
|
pub fn did_key(&self) -> Result<String> {
|
|
|
|
|
did_key_from_pubkey_hex(&self.pubkey_hex())
|
|
|
|
|
.map_err(|e| anyhow::anyhow!("Invalid pubkey hex: {}", e))
|
2026-02-17 15:03:34 +00:00
|
|
|
}
|
2026-03-12 00:19:30 +00:00
|
|
|
|
2026-03-31 01:41:24 +01:00
|
|
|
/// 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())
|
|
|
|
|
}
|
2026-02-17 15:03:34 +00:00
|
|
|
}
|
|
|
|
|
|
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")?;
|
|
|
|
|
}
|
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")?;
|
|
|
|
|
|
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: {}...)",
|
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()))
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 15:03:34 +00:00
|
|
|
/// 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()
|
|
|
|
|
))
|
2026-02-17 15:03:34 +00:00
|
|
|
}
|
2026-03-10 23:55:11 +00:00
|
|
|
|
2026-03-12 00:19:30 +00:00
|
|
|
/// 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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 00:19:30 +00:00
|
|
|
/// 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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 23:55:11 +00:00
|
|
|
#[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();
|
2026-03-10 23:55:11 +00:00
|
|
|
|
|
|
|
|
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();
|
2026-03-10 23:55:11 +00:00
|
|
|
|
|
|
|
|
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();
|
2026-03-10 23:55:11 +00:00
|
|
|
|
2026-03-21 01:54:35 +00:00
|
|
|
let did = identity.did_key().unwrap();
|
2026-03-10 23:55:11 +00:00
|
|
|
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();
|
2026-03-10 23:55:11 +00:00
|
|
|
|
|
|
|
|
let addr = identity.node_address("abc123.onion");
|
|
|
|
|
assert!(addr.starts_with("archipelago://abc123.onion#"));
|
|
|
|
|
assert!(addr.contains(&identity.pubkey_hex()));
|
|
|
|
|
}
|
2026-03-12 00:19:30 +00:00
|
|
|
|
|
|
|
|
#[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();
|
2026-03-12 00:19:30 +00:00
|
|
|
|
2026-03-21 01:54:35 +00:00
|
|
|
let doc = identity.did_document().unwrap();
|
|
|
|
|
let did = identity.did_key().unwrap();
|
2026-03-12 00:19:30 +00:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-10 23:55:11 +00:00
|
|
|
}
|