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:
co-authored by
Claude Fable 5
parent
291f2d7186
commit
2c8c99fd28
@@ -563,7 +563,9 @@ pub(super) async fn handle_identity_received(
|
||||
// Update peer record
|
||||
let peer = MeshPeer {
|
||||
contact_id,
|
||||
advert_name: format!("Archy-{}", &did[8..16.min(did.len())]),
|
||||
// .get(): a malformed DID shorter than the "did:key:" prefix must
|
||||
// not panic the listener on a radio-supplied string.
|
||||
advert_name: format!("Archy-{}", did.get(8..16.min(did.len())).unwrap_or(did)),
|
||||
did: Some(did.to_string()),
|
||||
pubkey_hex: Some(ed_pubkey_hex.to_string()),
|
||||
// The advert signature was verified above, so this is an authenticated
|
||||
|
||||
@@ -258,7 +258,31 @@ impl TypedEnvelope {
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify signature if present.
|
||||
/// Signing preimage v2: binds the anti-replay `seq` so a radio MITM
|
||||
/// can't reorder/replay a signed message under a different sequence
|
||||
/// number. v1 (legacy) covers only (t, v, ts).
|
||||
fn signing_preimage_v2(&self) -> Vec<u8> {
|
||||
let mut sign_data = Vec::with_capacity(1 + self.v.len() + 4 + 8);
|
||||
sign_data.push(self.t);
|
||||
sign_data.extend_from_slice(&self.v);
|
||||
sign_data.extend_from_slice(&self.ts.to_le_bytes());
|
||||
sign_data.extend_from_slice(&self.seq.to_le_bytes());
|
||||
sign_data
|
||||
}
|
||||
|
||||
fn signing_preimage_v1(&self) -> Vec<u8> {
|
||||
let mut sign_data = Vec::with_capacity(1 + self.v.len() + 4);
|
||||
sign_data.push(self.t);
|
||||
sign_data.extend_from_slice(&self.v);
|
||||
sign_data.extend_from_slice(&self.ts.to_le_bytes());
|
||||
sign_data
|
||||
}
|
||||
|
||||
/// Verify signature if present. Accepts the seq-binding v2 preimage OR
|
||||
/// the legacy (t, v, ts) preimage — senders still emit v1 until the
|
||||
/// whole fleet verifies v2 (receivers hard-drop bad signatures, so
|
||||
/// flipping the send side first would break mixed-fleet alerts). The
|
||||
/// seq-tampering window closes only when the v1 arm is removed.
|
||||
pub fn verify_signature(&self, verifying_key: &ed25519_dalek::VerifyingKey) -> Result<bool> {
|
||||
let Some(sig_bytes) = &self.sig else {
|
||||
return Ok(false);
|
||||
@@ -266,13 +290,14 @@ impl TypedEnvelope {
|
||||
let signature =
|
||||
ed25519_dalek::Signature::from_slice(sig_bytes).context("Invalid signature bytes")?;
|
||||
|
||||
let mut sign_data = Vec::with_capacity(1 + self.v.len() + 4);
|
||||
sign_data.push(self.t);
|
||||
sign_data.extend_from_slice(&self.v);
|
||||
sign_data.extend_from_slice(&self.ts.to_le_bytes());
|
||||
|
||||
if verifying_key
|
||||
.verify_strict(&self.signing_preimage_v2(), &signature)
|
||||
.is_ok()
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
verifying_key
|
||||
.verify_strict(&sign_data, &signature)
|
||||
.verify_strict(&self.signing_preimage_v1(), &signature)
|
||||
.context("Signature verification failed")?;
|
||||
Ok(true)
|
||||
}
|
||||
@@ -284,12 +309,25 @@ impl TypedEnvelope {
|
||||
|
||||
/// Set the outbound sequence number. Called by the send path after the
|
||||
/// target's counter has been incremented. Safe to call AFTER `new_signed`
|
||||
/// because the signature covers `(t, v, ts)` — not `seq`.
|
||||
/// because the v1 signature covers `(t, v, ts)` — not `seq`. Once the
|
||||
/// fleet is on a build whose `verify_signature` accepts the v2 preimage,
|
||||
/// flip senders to sign AFTER seq allocation via `signed_with_seq`.
|
||||
pub fn with_seq(mut self, seq: u64) -> Self {
|
||||
self.seq = seq;
|
||||
self
|
||||
}
|
||||
|
||||
/// v2 sender path (NOT yet wired — see `verify_signature` for the fleet
|
||||
/// migration order): set seq first, then sign binding it.
|
||||
#[allow(dead_code)]
|
||||
pub fn signed_with_seq(mut self, seq: u64, signing_key: &ed25519_dalek::SigningKey) -> Self {
|
||||
use ed25519_dalek::Signer;
|
||||
self.seq = seq;
|
||||
let signature = signing_key.sign(&self.signing_preimage_v2());
|
||||
self.sig = Some(signature.to_bytes().to_vec());
|
||||
self
|
||||
}
|
||||
|
||||
/// Encode to wire format: [0x02] [CBOR envelope].
|
||||
pub fn to_wire(&self) -> Result<Vec<u8>> {
|
||||
let mut buf = Vec::new();
|
||||
@@ -816,6 +854,37 @@ mod tests {
|
||||
assert!(envelope.verify_signature(&key.verifying_key()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_v2_seq_bound_signature() {
|
||||
use ed25519_dalek::SigningKey;
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
let key = SigningKey::generate(&mut OsRng);
|
||||
let envelope = TypedEnvelope::new(MeshMessageType::Alert, b"test".to_vec())
|
||||
.signed_with_seq(42, &key);
|
||||
assert!(envelope.verify_signature(&key.verifying_key()).unwrap());
|
||||
|
||||
// v2 binds seq: replaying the signed envelope under a different
|
||||
// sequence number must fail verification.
|
||||
let mut replayed = envelope.clone();
|
||||
replayed.seq = 43;
|
||||
assert!(replayed.verify_signature(&key.verifying_key()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_v1_signature_survives_seq_set_after_signing() {
|
||||
use ed25519_dalek::SigningKey;
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
// Mixed-fleet compatibility: current senders sign first (v1
|
||||
// preimage, no seq) and allocate seq afterwards; verify must still
|
||||
// accept that.
|
||||
let key = SigningKey::generate(&mut OsRng);
|
||||
let envelope = TypedEnvelope::new_signed(MeshMessageType::Alert, b"test".to_vec(), &key)
|
||||
.with_seq(7);
|
||||
assert!(envelope.verify_signature(&key.verifying_key()).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invoice_payload_roundtrip() {
|
||||
let invoice = InvoicePayload {
|
||||
|
||||
Reference in New Issue
Block a user