Side Channel Attack in Axum with Mutual Tls
Side Channel Attack in Axum with Mutual Tls — how this specific combination creates or exposes the vulnerability
A side channel attack in an Axum service that uses mutual Transport Layer Security (mTLS) exploits timing, error handling, or resource usage differences rather than breaking the cryptographic protocol itself. When mTLS is enforced, the handshake validates client certificates before the application processes requests. If certificate validation or per-request authorization logic introduces measurable variation in response times, an attacker can infer information about valid certificates, authorization states, or rate-limiting thresholds.
In Axum, this typically arises when the runtime performs different work depending on certificate metadata. For example, checking a certificate against a revocation list or mapping a certificate to an internal user may involve I/O or branching that is not constant-time. An attacker observing response latencies across many requests can distinguish successful validation from failures, or infer when a certificate is rate-limited or blocked. Because mTLS terminates TLS at the server, Axum sees the authenticated identity only after the handshake; if your authorization layer introduces non-constant delays, the handshake duration plus application latency can leak which identities or roles exist in the system.
Concrete attack patterns include measuring the time difference between requests that present valid client certificates versus those that present invalid or revoked certificates. If the server responds faster for valid certs due to early acceptance and slower paths for invalid certs (e.g., logging, additional lookups), this timing discrepancy becomes an observable signal. Another scenario involves probing rate limits: by cycling through many distinct valid certificates, an attacker can map per-client quotas and discover throttling boundaries, which is useful for planning denial-of-service or credential-stuffing strategies.
These risks are not inherent to mTLS, but to implementation details in Axum middleware and handlers. For instance, performing database or HTTP calls to validate certificate metadata in hot paths, using non-constant-time comparisons for certificate fingerprints, or returning different HTTP status codes for TLS verification failures can amplify side channels. Since mTLS ensures peer identity, developers might assume authorization checks are cheap and consistent; however, without deliberate design for uniformity, the combination of mTLS and uneven authorization logic creates a measurable side channel.
To detect such issues, scans using middleBrick’s authorization and input validation checks can surface timing-related findings when combined with manual review of handlers and middleware. Instrumenting request logging to exclude sensitive metadata and ensuring that all certificate-validation branches have similar execution paths and durations are essential hardening steps in an Axum service.
Mutual Tls-Specific Remediation in Axum — concrete code fixes
Remediation focuses on making certificate validation and per-request authorization uniform in timing and behavior, and avoiding information leakage through status codes or headers. Below are concrete Axum examples that demonstrate safe patterns.
1. Constant-time certificate fingerprint comparison and uniform error handling:
use axum::{
extract::connect_info::ConnectInfo,
response::IntoResponse,
routing::get,
Router,
};
use std::net::SocketAddr;
use tls_parser::Certificate;
use subtle::ConstantTimeEq;
async fn handle_secure(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
maybe_cert: Option<axum::extract::State<SharedState>>
) -> impl IntoResponse {
// Example: extract peer certificate from mTLS extension (pseudo-API)
let peer_cert_fingerprint = match maybe_cert {
Some(state) => state.peer_fingerprint.clone(),
None => {
// Return a generic unauthorized response without revealing why
return (axum::http::StatusCode::UNAUTHORIZED, "Unauthorized").into_response();
}
};
// Constant-time check against an allowed list
let allowed_fingerprint = [0u8; 32]; // placeholder for expected digest
let valid = peer_cert_fingerprint.ct_eq(&allowed_fingerprint).unwrap_u8() == 1;
if !valid {
// Use same status and minimal body to avoid signaling which cert was wrong
(axum::http::StatusCode::UNAUTHORIZED, "Unauthorized").into_response()
} else {
axum::Json(serde_json::json!({ "status": "ok" })).into_response()
}
}
This example shows using subtle for constant-time equality checks and returning the same status and body for both missing and invalid certificates to reduce information leakage.
2. Shared-state, pre-validated identity to avoid per-request I/O variability:
use axum::{
extract::State,
response::IntoResponse,
Json,
};
use std::sync::Arc;
use tokio::sync::RwLock;
struct SharedState {
// Map from certificate fingerprint to normalized role, precomputed at handshake
identities: std::collections::HashMap<[u8; 32], String>,
}
async fn authorized_handler(
State(state): State<Arc<RwLock<SharedState>>>,
// Assume peer fingerprint was attached by middleware after mTLS verification
fingerprint: axum::extract::Extension<[u8; 32]>
) -> impl IntoResponse {
let state = state.read().await;
// Constant-time map lookup pattern: always traverse the map uniformly
let role = state.identities.get(&fingerprint.0).map(|s| s.as_str()).unwrap_or("unknown");
Json(serde_json::json!({ "role": role })).into_response()
}
By resolving the identity to a role in middleware and storing it in request extensions, you avoid performing I/O inside hot handlers, which can vary in duration based on external systems.
3. Middleware that enforces mTLS and attaches clean identity metadata:
use axum::{
async_trait,
extract::Extension,
middleware::Next,
response::Response,
Request,
};
use std::future::Ready;
struct MtlsIdentity([u8; 32]);
pub async fn mtls_middleware(
mut req: Request,
next: Next,
) -> Response {
// Pseudo: retrieve peer certs from the TLS context provided by the runtime
let certs = match req.extensions().get::>>() {
Some(c) => c,
None => return unauthorized_response(),
};
let fp = fingerprint_first_cert(&certs); // constant-time digest
// Attach normalized identity for downstream handlers
req.extensions_mut().insert(MtlsIdentity(fp));
next.run(req).await
}
fn fingerprint_first_cert(certs: &[Vec]) -> [u8; 32] {
// Use a constant-time hash (e.g., SHA-256) and return fixed-size output
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
if let Some(first) = certs.first() {
hasher.update(first);
}
let mut out = [0u8; 32];
out.copy_from_slice(hasher.finalize().as_slice());
out
}
fn unauthorized_response() -> Response {
// Keep responses uniform
(axum::http::StatusCode::UNAUTHORIZED, "Unauthorized").into_response()
}
Use this middleware to ensure every request goes through the same validation path and that downstream handlers receive stable, precomputed metadata, reducing timing and branching variability.