fix(security): bind seq into mesh signatures (v2 preimage), guard DID slice, cfg-gate dev password

- mesh: verify_signature accepts a v2 preimage (t,v,ts,seq) alongside
  legacy v1 (t,v,ts); signed_with_seq() is the v2 sender path, not yet
  wired — senders stay v1 until the fleet verifies v2 (receivers
  hard-drop bad sigs, so flipping send-side first would break
  mixed-fleet alerts). Tests: v2 verify, v2 seq-tamper rejection,
  v1 sign-then-set-seq compat.
- mesh listener: malformed radio-supplied DID shorter than the
  'did🔑' prefix can no longer panic advert_name (slice -> .get()).
- auth: the pre-setup password123 dev login and the constant itself are
  now #[cfg(debug_assertions)] — no release binary carries the bypass,
  whatever its runtime config says.
- orchestrator: canned host-facts under #[cfg(test)] — awaiting real
  subprocesses under tokio's paused test clock deadlocks against
  auto-advanced timers (the old blocking detection only worked by never
  yielding).
- drop two now-unused std::process::Command imports left by 4c75bb3d.

Tests: mesh 110/110 (incl. 2 new), api 68/68, container 159/159,
archipelago-container check clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-07-04 17:49:52 -04:00
co-authored by Claude Fable 5
parent 291f2d7186
commit 2c8c99fd28
7 changed files with 145 additions and 37 deletions
@@ -32,7 +32,6 @@ use async_trait::async_trait;
use std::collections::{HashMap, HashSet};
use std::os::unix::fs::FileTypeExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::{Mutex, RwLock};
@@ -2725,19 +2724,34 @@ impl ProdContainerOrchestrator {
}
async fn detect_host_facts(&self) -> HostFacts {
let host_ip = Self::detect_host_ip()
.await
.unwrap_or_else(|| "127.0.0.1".to_string());
let host_mdns = Self::detect_host_mdns().await;
let disk_gb = self.disk_gb().await;
HostFacts {
host_ip,
host_mdns,
disk_gb,
// Cheap default; resolve_dynamic_env fills the real node name on
// demand (it costs a podman call) only for manifests that use
// {{BITCOIN_HOST}}, rather than every app on every reconcile.
bitcoin_host: "bitcoin-knots".to_string(),
// Unit tests run under tokio's paused clock; awaiting a real
// subprocess there deadlocks against auto-advanced timers (the old
// BLOCKING detection only worked by never yielding). Canned facts.
#[cfg(test)]
{
return HostFacts {
host_ip: "127.0.0.1".to_string(),
host_mdns: "test.local".to_string(),
disk_gb: self.test_disk_gb.unwrap_or(1000),
bitcoin_host: "bitcoin-knots".to_string(),
};
}
#[allow(unreachable_code)]
{
let host_ip = Self::detect_host_ip()
.await
.unwrap_or_else(|| "127.0.0.1".to_string());
let host_mdns = Self::detect_host_mdns().await;
let disk_gb = self.disk_gb().await;
HostFacts {
host_ip,
host_mdns,
disk_gb,
// Cheap default; resolve_dynamic_env fills the real node name on
// demand (it costs a podman call) only for manifests that use
// {{BITCOIN_HOST}}, rather than every app on every reconcile.
bitcoin_host: "bitcoin-knots".to_string(),
}
}
}
@@ -2745,10 +2759,15 @@ impl ProdContainerOrchestrator {
/// `bitcoin-core`) for the `{{BITCOIN_HOST}}` derived-env placeholder.
/// Defaults to `bitcoin-knots` when none is running (B12).
async fn bitcoin_host(&self) -> String {
// No real podman under the tests' paused clock (see detect_host_facts).
#[cfg(test)]
if let Some(host) = &self.test_bitcoin_host {
return host.clone();
{
return self
.test_bitcoin_host
.clone()
.unwrap_or_else(|| "bitcoin-knots".to_string());
}
#[allow(unreachable_code)]
// Mirrors api::rpc::package::dependencies (the legacy install path);
// both Bitcoin node variants are reachable on archy-net by name.
const BITCOIN_NAMES: &[&str] = &["bitcoin-knots", "bitcoin-core", "bitcoin"];
@@ -2839,11 +2858,15 @@ impl ProdContainerOrchestrator {
}
async fn disk_gb(&self) -> u64 {
// No real df under the tests' paused clock (see detect_host_facts).
#[cfg(test)]
if let Some(disk_gb) = self.test_disk_gb {
return disk_gb;
{
return self.test_disk_gb.unwrap_or(1000);
}
#[allow(unreachable_code)]
{
Self::detect_disk_gb().await
}
Self::detect_disk_gb().await
}
/// Ensure app-specific secrets exist *before* env resolution. The Bitcoin