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:
@@ -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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user