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

@@ -2,21 +2,99 @@
//!
//! Delegates all operations to the local [`Store`], acting as a trusted relay
//! from authenticated peer servers.
//!
//! **Security:** Each handler validates the request's `origin` field against
//! the `verified_peer_domain` extracted from the mTLS client certificate at
//! connection time. Per-peer rate limits prevent abuse.
use std::sync::Arc;
use capnp::capability::Promise;
use dashmap::DashMap;
use quicproquo_proto::federation_capnp::federation_service;
use tokio::sync::Notify;
use dashmap::DashMap;
use crate::auth::RateEntry;
use crate::storage::Store;
/// Per-peer federation rate limit: max requests within a 60-second window.
const FED_RATE_LIMIT_WINDOW_SECS: u64 = 60;
const FED_RATE_LIMIT_MAX: u32 = 200;
/// Inbound federation RPC handler.
pub struct FederationServiceImpl {
pub store: Arc<dyn Store>,
pub waiters: Arc<DashMap<Vec<u8>, Arc<Notify>>>,
pub local_domain: String,
/// The peer domain extracted from the mTLS client certificate's CN/SAN
/// at connection time. All requests must declare an `origin` matching this.
pub verified_peer_domain: Option<String>,
/// Per-peer rate limiter (keyed by peer domain).
pub rate_limits: Arc<DashMap<String, RateEntry>>,
}
/// Validate that the request's `origin` matches the mTLS-verified peer domain.
fn validate_origin(
verified: &Option<String>,
declared: &str,
) -> Result<(), capnp::Error> {
match verified {
Some(ref expected) if expected == declared => Ok(()),
Some(ref expected) => Err(capnp::Error::failed(format!(
"federation auth: origin '{}' does not match mTLS cert '{}'",
declared, expected
))),
None => Err(capnp::Error::failed(
"federation auth: no verified peer domain (mTLS required)".into(),
)),
}
}
/// Extract and validate the origin string from the request's auth field.
fn extract_and_validate_origin(
service: &FederationServiceImpl,
get_auth: Result<quicproquo_proto::federation_capnp::federation_auth::Reader<'_>, capnp::Error>,
) -> Result<String, capnp::Error> {
let auth = get_auth
.map_err(|_| capnp::Error::failed("federation auth: missing auth field".into()))?;
let origin_reader = auth.get_origin()
.map_err(|_| capnp::Error::failed("federation auth: missing origin".into()))?;
let origin = origin_reader.to_str()
.map_err(|_| capnp::Error::failed("federation auth: origin is not valid UTF-8".into()))?;
if origin.is_empty() {
return Err(capnp::Error::failed("federation auth: origin must not be empty".into()));
}
validate_origin(&service.verified_peer_domain, origin)?;
check_federation_rate_limit(&service.rate_limits, origin)?;
Ok(origin.to_string())
}
/// Per-peer federation rate limiter.
fn check_federation_rate_limit(
rate_limits: &DashMap<String, RateEntry>,
peer_domain: &str,
) -> Result<(), capnp::Error> {
let now = crate::auth::current_timestamp();
let mut entry = rate_limits.entry(peer_domain.to_string()).or_insert(RateEntry {
count: 0,
window_start: now,
});
if now - entry.window_start >= FED_RATE_LIMIT_WINDOW_SECS {
entry.count = 1;
entry.window_start = now;
} else {
entry.count += 1;
if entry.count > FED_RATE_LIMIT_MAX {
return Err(capnp::Error::failed(format!(
"federation rate limit exceeded for peer '{peer_domain}'"
)));
}
}
Ok(())
}
impl federation_service::Server for FederationServiceImpl {
@@ -30,6 +108,12 @@ impl federation_service::Server for FederationServiceImpl {
Err(e) => return Promise::err(capnp::Error::failed(format!("bad params: {e}"))),
};
// Validate origin against mTLS cert and apply rate limit.
let origin = match extract_and_validate_origin(self, p.get_auth()) {
Ok(o) => o,
Err(e) => return Promise::err(e),
};
let recipient_key = match p.get_recipient_key() {
Ok(v) => v.to_vec(),
Err(e) => return Promise::err(capnp::Error::failed(format!("bad recipient_key: {e}"))),
@@ -40,13 +124,6 @@ impl federation_service::Server for FederationServiceImpl {
};
let channel_id = p.get_channel_id().unwrap_or_default().to_vec();
if let Ok(a) = p.get_auth() {
if let Ok(origin) = a.get_origin() {
let origin = origin.to_str().unwrap_or("?");
tracing::debug!(origin = origin, "federation relay_enqueue");
}
}
if recipient_key.len() != 32 {
return Promise::err(capnp::Error::failed("recipient_key must be 32 bytes".into()));
}
@@ -67,6 +144,7 @@ impl federation_service::Server for FederationServiceImpl {
}
tracing::info!(
origin = %origin,
recipient_prefix = %hex::encode(&recipient_key[..4]),
seq = seq,
"federation: relayed enqueue"
@@ -85,6 +163,12 @@ impl federation_service::Server for FederationServiceImpl {
Err(e) => return Promise::err(capnp::Error::failed(format!("bad params: {e}"))),
};
// Validate origin against mTLS cert and apply rate limit.
let _origin = match extract_and_validate_origin(self, p.get_auth()) {
Ok(o) => o,
Err(e) => return Promise::err(e),
};
let recipient_keys = match p.get_recipient_keys() {
Ok(v) => v,
Err(e) => return Promise::err(capnp::Error::failed(format!("bad recipient_keys: {e}"))),
@@ -134,11 +218,21 @@ impl federation_service::Server for FederationServiceImpl {
params: federation_service::ProxyFetchKeyPackageParams,
mut results: federation_service::ProxyFetchKeyPackageResults,
) -> Promise<(), capnp::Error> {
let identity_key = match params.get().and_then(|p| p.get_identity_key()) {
Ok(v) => v.to_vec(),
let p = match params.get() {
Ok(p) => p,
Err(e) => return Promise::err(capnp::Error::failed(format!("bad params: {e}"))),
};
// Validate origin against mTLS cert and apply rate limit.
if let Err(e) = extract_and_validate_origin(self, p.get_auth()) {
return Promise::err(e);
}
let identity_key = match p.get_identity_key() {
Ok(v) => v.to_vec(),
Err(e) => return Promise::err(capnp::Error::failed(format!("bad identity_key: {e}"))),
};
match self.store.fetch_key_package(&identity_key) {
Ok(Some(pkg)) => results.get().set_package(&pkg),
Ok(None) => results.get().set_package(&[]),
@@ -153,11 +247,21 @@ impl federation_service::Server for FederationServiceImpl {
params: federation_service::ProxyFetchHybridKeyParams,
mut results: federation_service::ProxyFetchHybridKeyResults,
) -> Promise<(), capnp::Error> {
let identity_key = match params.get().and_then(|p| p.get_identity_key()) {
Ok(v) => v.to_vec(),
let p = match params.get() {
Ok(p) => p,
Err(e) => return Promise::err(capnp::Error::failed(format!("bad params: {e}"))),
};
// Validate origin against mTLS cert and apply rate limit.
if let Err(e) = extract_and_validate_origin(self, p.get_auth()) {
return Promise::err(e);
}
let identity_key = match p.get_identity_key() {
Ok(v) => v.to_vec(),
Err(e) => return Promise::err(capnp::Error::failed(format!("bad identity_key: {e}"))),
};
match self.store.fetch_hybrid_key(&identity_key) {
Ok(Some(pk)) => results.get().set_hybrid_public_key(&pk),
Ok(None) => results.get().set_hybrid_public_key(&[]),
@@ -172,7 +276,17 @@ impl federation_service::Server for FederationServiceImpl {
params: federation_service::ProxyResolveUserParams,
mut results: federation_service::ProxyResolveUserResults,
) -> Promise<(), capnp::Error> {
let username = match params.get().and_then(|p| p.get_username()) {
let p = match params.get() {
Ok(p) => p,
Err(e) => return Promise::err(capnp::Error::failed(format!("bad params: {e}"))),
};
// Validate origin against mTLS cert and apply rate limit.
if let Err(e) = extract_and_validate_origin(self, p.get_auth()) {
return Promise::err(e);
}
let username = match p.get_username() {
Ok(u) => match u.to_str() {
Ok(s) => s.to_string(),
Err(e) => return Promise::err(capnp::Error::failed(format!("bad utf-8: {e}"))),
@@ -194,8 +308,42 @@ impl federation_service::Server for FederationServiceImpl {
_params: federation_service::FederationHealthParams,
mut results: federation_service::FederationHealthResults,
) -> Promise<(), capnp::Error> {
// Health check does not require origin validation (diagnostic endpoint).
results.get().set_status("ok");
results.get().set_server_domain(&self.local_domain);
Promise::ok(())
}
}
/// Extract the peer domain from the mTLS client certificate's first SAN (DNS name)
/// or CN, given the QUIC connection's peer identity (a certificate chain).
pub fn extract_peer_domain(conn: &quinn::Connection) -> Option<String> {
let identity = conn.peer_identity()?;
let certs = identity.downcast::<Vec<rustls::pki_types::CertificateDer<'static>>>().ok()?;
let first_cert = certs.first()?;
// Parse the DER certificate to extract SAN DNS names or CN.
let (_, parsed) = x509_parser::parse_x509_certificate(first_cert.as_ref()).ok()?;
// Prefer SAN DNS names.
if let Ok(Some(san)) = parsed.subject_alternative_name() {
for name in &san.value.general_names {
if let x509_parser::extensions::GeneralName::DNSName(dns) = name {
return Some(dns.to_string());
}
}
}
// Fall back to CN.
for rdn in parsed.subject().iter() {
for attr in rdn.iter() {
if attr.attr_type() == &x509_parser::oid_registry::OID_X509_COMMON_NAME {
if let Ok(cn) = attr.as_str() {
return Some(cn.to_string());
}
}
}
}
None
}