fix: security hardening — 40 findings from full codebase review

Full codebase review by 4 independent agents (security, architecture,
code quality, correctness) identified ~80 findings. This commit fixes 40
of them across all workspace crates.

Critical fixes:
- Federation service: validate origin against mTLS cert CN/SAN (C1)
- WS bridge: add DM channel auth, size limits, rate limiting (C2)
- hpke_seal: panic on error instead of silent empty ciphertext (C3)
- hpke_setup_sender_and_export: error on parse fail, no PQ downgrade (C7)

Security fixes:
- Zeroize: seed_bytes() returns Zeroizing<[u8;32]>, private_to_bytes()
  returns Zeroizing<Vec<u8>>, ClientAuth.access_token, SessionState.password,
  conversation hex_key all wrapped in Zeroizing
- Keystore: 0o600 file permissions on Unix
- MeshIdentity: 0o600 file permissions on Unix
- Timing floors: resolveIdentity + WS bridge resolve_user get 5ms floor
- Mobile: TLS verification gated behind insecure-dev feature flag
- Proto: from_bytes default limit tightened from 64 MiB to 8 MiB

Correctness fixes:
- fetch_wait: register waiter before fetch to close TOCTOU window
- MeshEnvelope: exclude hop_count from signature (forwarding no longer
  invalidates sender signature)
- BroadcastChannel: encrypt returns Result instead of panicking
- transcript: rename verify_transcript_chain → validate_transcript_structure
- group.rs: extract shared process_incoming() for receive_message variants
- auth_ops: remove spurious RegistrationRequest deserialization
- MeshStore.seen: bounded to 100K with FIFO eviction

Quality fixes:
- FFI error classification: typed downcast instead of string matching
- Plugin HookVTable: SAFETY documentation for unsafe Send+Sync
- clippy::unwrap_used: warn → deny workspace-wide
- Various .unwrap_or("") → proper error returns

Review report: docs/REVIEW-2026-03-04.md
152 tests passing (72 core + 35 server + 14 E2E + 1 doctest + 30 P2P)
This commit is contained in:
2026-03-04 07:52:12 +01:00
parent 4694a3098b
commit 394199b19b
58 changed files with 3893 additions and 414 deletions

View File

@@ -89,11 +89,7 @@ async fn connect_inner(
) -> anyhow::Result<(Endpoint, quinn::Connection)> {
let _ = rustls::crypto::ring::default_provider().install_default();
// Build a permissive client config (skip server cert verification for dev/testing).
let crypto = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(SkipServerVerification))
.with_no_client_auth();
let crypto = build_client_tls_config()?;
let mut client_config = quinn::ClientConfig::new(Arc::new(
quinn::crypto::rustls::QuicClientConfig::try_from(crypto)
@@ -159,11 +155,36 @@ pub unsafe extern "C" fn qnpc_disconnect(handle: *mut MobileHandle) {
}
}
// ── Internal: skip server cert verification for testing ─────────────────────
// ── TLS configuration ───────────────────────────────────────────────────────
/// Build the rustls `ClientConfig` for the QUIC transport.
///
/// Without the `insecure-dev` feature, this uses the platform's native root
/// certificates for server verification. With `insecure-dev` enabled, all
/// certificate verification is skipped (MITM-vulnerable — dev/testing only).
fn build_client_tls_config() -> anyhow::Result<rustls::ClientConfig> {
#[cfg(feature = "insecure-dev")]
{
Ok(rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(SkipServerVerification))
.with_no_client_auth())
}
#[cfg(not(feature = "insecure-dev"))]
{
let mut root_store = rustls::RootCertStore::empty();
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
Ok(rustls::ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth())
}
}
#[cfg(feature = "insecure-dev")]
#[derive(Debug)]
struct SkipServerVerification;
#[cfg(feature = "insecure-dev")]
impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
fn verify_server_cert(
&self,
@@ -216,6 +237,87 @@ mod tests {
use super::*;
use std::net::UdpSocket;
/// Test-only insecure verifier (always available in test builds).
#[derive(Debug)]
struct TestSkipServerVerification;
impl rustls::client::danger::ServerCertVerifier for TestSkipServerVerification {
fn verify_server_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
vec![
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
rustls::SignatureScheme::ED25519,
rustls::SignatureScheme::RSA_PSS_SHA256,
rustls::SignatureScheme::RSA_PSS_SHA384,
rustls::SignatureScheme::RSA_PSS_SHA512,
rustls::SignatureScheme::RSA_PKCS1_SHA256,
rustls::SignatureScheme::RSA_PKCS1_SHA384,
rustls::SignatureScheme::RSA_PKCS1_SHA512,
]
}
}
/// Connect to a test server using the insecure cert verifier.
async fn test_connect_inner(
addr: SocketAddr,
server_name: &str,
) -> anyhow::Result<(Endpoint, quinn::Connection)> {
let _ = rustls::crypto::ring::default_provider().install_default();
let crypto = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(TestSkipServerVerification))
.with_no_client_auth();
let mut client_config = quinn::ClientConfig::new(Arc::new(
quinn::crypto::rustls::QuicClientConfig::try_from(crypto)
.map_err(|e| anyhow::anyhow!("QUIC client config: {e}"))?,
));
let mut transport = quinn::TransportConfig::default();
transport.max_idle_timeout(Some(
std::time::Duration::from_secs(120)
.try_into()
.expect("120s valid"),
));
client_config.transport_config(Arc::new(transport));
let mut endpoint = Endpoint::client("0.0.0.0:0".parse().unwrap())?;
endpoint.set_default_client_config(client_config);
let connection = endpoint.connect(addr, server_name)?.await?;
Ok((endpoint, connection))
}
/// Prove QUIC connection migration: connect, send messages, rebind the
/// UDP socket (simulating wifi→cellular), send more messages, verify
/// all messages arrive.
@@ -228,8 +330,8 @@ mod tests {
// Start an in-process echo server.
let server_addr = start_echo_server().await;
// Connect client.
let (endpoint, connection) = connect_inner(server_addr, "localhost")
// Connect client using test-only insecure verifier.
let (endpoint, connection) = test_connect_inner(server_addr, "localhost")
.await
.expect("connect");