Side Channel Attack in Actix with Mutual Tls
Side Channel Attack in Actix with Mutual Tls — how this specific combination creates or exposes the vulnerability
A side channel attack in Actix with Mutual TLS (mTLS) occurs when an attacker infers sensitive information from observable characteristics of the TLS handshake or request processing rather than breaking encryption. In mTLS, both client and server present certificates, and the handshake establishes identity before application logic runs. If Actix services perform certificate validation or handshake steps with timing variance, error messages that differ based on certificate validity, or branch conditions that depend on certificate metadata, these become observable signals.
For example, an Actix server configured to verify client certificates may take measurably longer to reject an invalid certificate that parses correctly but fails chain validation, compared to a completely malformed TLS record. An attacker can measure round-trip times across many requests to distinguish between valid-but-untrusted and invalid certificates, gradually narrowing the set of trusted certificate fingerprints or inferring when a specific client certificate is revoked or rotated. This is a timing side channel because the server’s response time leaks information about internal validation steps.
Similarly, differences in HTTP response codes or headers—such as returning 400 versus 401 depending on whether a certificate was accepted but authorization failed—can expose whether mTLS succeeded. In Actix, if middleware or guards branch logic based on whether a peer certificate is present and valid, these branches may be distinguishable through response timing or status code patterns. An attacker can correlate request timing and status codes to learn when a legitimate client certificate is used, effectively bypassing authorization boundaries by inferring when a valid mTLS identity is present.
Another vector specific to Actix with mTLS arises from how the runtime integrates with Rust’s TLS library (e.g., native-tls or rustls). If the server processes requests differently depending on certificate fields—such as extracting a username from a client certificate and using it in database queries—the query timing or behavior may vary with the length of the username or presence of certain attributes. This can lead to observable timing differences or error patterns that reveal information about the client’s identity or permissions, even though the TLS layer itself remains cryptographically secure.
These side channels do not break the cryptographic strength of mTLS, but they undermine the practical security of the identity assertions it provides. In Actix, where performance and correctness are often high priorities, it is important to ensure that mTLS verification and request handling execute in constant time and produce uniform responses regardless of certificate validity. Failing to do so can allow an attacker to build a profile of valid certificates or infer authorization states by measuring subtle differences in latency, status codes, or response headers across repeated connections.
Mutual Tls-Specific Remediation in Actix — concrete code fixes
To mitigate side channel risks in Actix with mTLS, design validation and request handling to be constant time and uniform in observable behavior. Use the same code path and response characteristics regardless of whether a client certificate is missing, invalid, or valid. Below are concrete remediation patterns and code examples.
1. Uniform TLS verification with consistent error handling
Ensure that certificate validation errors result in the same HTTP status and minimal timing difference. Use a helper to standardize the rejection response.
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use actix_web::middleware::Logger;
use std::sync::Arc;
use rustls::{ServerConfig, Certificate, PrivateKey};
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
async fn handle_mtls_index() -> impl Responder {
HttpResponse::Ok().body("Authenticated via mTLS")
}
async fn handle_mtls_fallback() -> impl Responder {
// Always return the same status and body shape for any mTLS failure
HttpResponse::Unauthorized().body("Authentication required")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
// Load server certificate and key
let certs = load_certs("server-cert.pem");
let key = load_private_key("server-key.pem");
let mut server_config = ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(certs, key)
.expect("Invalid server cert/key");
// Require and validate client certificates
server_config.client_auth_root_subjects = Arc::new(|_| vec![]); // populate with allowed DNs or hashes
server_config.verify_peer = true;
HttpServer::new(move || {
App::new()
.wrap(Logger::default())
.route("/api/secure", web::get().().to(|| async { handle_mtls_fallback() }))
})
.bind_rustls("127.0.0.1:8443", server_config)?
.run()
.await
}
fn load_certs(path: &str) -> Vec> {
// implementation omitted for brevity
vec![]
}
fn load_private_key(path: &str) -> PrivateKeyDer<'static> {
// implementation omitted for brevity
PrivateKeyDer::Pkcs8(vec![].into())
}
2. Constant-time certificate metadata checks
If you inspect certificate fields (e.g., SAN or CN), avoid branching logic that depends on the presence or length of those fields. Instead, normalize the information and process it in a uniform way.
use actix_web::{web, HttpResponse};
use openssl::x509::X509;
use std::time::Duration;
fn extract_username_safely(cert: &X509) -> String {
// Always return a fixed-length placeholder if extraction is not constant time
// Prefer hashing the certificate fingerprint for authorization rather than raw fields
let fingerprint = cert.fingerprint(openssl::hash::MessageDigest::sha256())
.map(|bytes| format!("{:x}", bytes))
.unwrap_or_else(|_| "unknown".to_string());
fingerprint
}
async fn mtls_protected(req: actix_web::HttpRequest) -> HttpResponse {
if let Some(cert) = req.extensions().get::() {
let identity = extract_username_safely(cert);
// Use identity in a constant-time lookup (e.g., hash map with fixed-time comparisons)
HttpResponse::Ok().body(format!("Hello, {}", identity))
} else {
HttpResponse::Unauthorized().body("Authentication required")
}
}
3. Mitigate timing differences in authorization logic
When mapping mTLS identities to permissions, avoid early returns or different execution paths based on validity. Use a two-phase approach: first verify mTLS, then apply authorization in a way that does not reveal which step failed.
use actix_web::{web, HttpResponse};
use std::collections::HashMap;
use std::time::Duration;
// Simulate constant-time authorization check
fn authorize_identity(identity: &str) -> bool {
// Use a hash-based lookup or constant-time comparison to avoid branching on identity validity
let permissions: HashMap<&str, &str> = [
("a1b2c3", "read:data"),
("d4e5f6", "write:data"),
].iter().cloned().collect();
permissions.get(identity).is_some()
}
async fn handle_authorized(req: actix_web::HttpRequest) -> HttpResponse {
// Always perform the same steps regardless of earlier failures
let cert = match req.extensions().get::() {
Some(c) => c,
None => return HttpResponse::Unauthorized().body("Authentication required"),
};
let identity = extract_username_safely(cert);
if authorize_identity(&identity) {
HttpResponse::Ok().body(format!("Access granted for {}", identity))
} else {
// Return same status shape as success case to avoid leaking via timing or status code
HttpResponse::Forbidden().body("Access denied")
}
}
4. Logging and observability without leaking info
Ensure logs do not reveal whether a certificate was accepted, rejected, or which field was invalid. Use a single log level for mTLS outcomes and avoid enriching logs with certificate metadata that could aid an attacker.
// Example: log only outcome, not certificate details
info!("mtls_request: status=ok"); // or status=unauthorized, always same format