rustls — Pure-Rust TLS
Deep Knowledge: Use
mcp__documentation__fetch_docswith technology:rustls.
Why rustls
| Feature | rustls | openssl-rs | native-tls |
|---|---|---|---|
| Implementation | Pure Rust | Bindings to OpenSSL C | Bindings to OS TLS (Sec/SChannel/OpenSSL) |
| Memory safety | ✅ | ❌ Periodic CVEs | Inherits OS TLS bugs |
| Cross-compile to mobile | ✅ Trivial | Requires OpenSSL build | Platform-dependent |
| TLS 1.3 | ✅ Default | ✅ | Depends on OS |
| TLS 1.2 | ✅ | ✅ | ✅ |
| TLS 1.0/1.1 | ❌ Removed | Configurable | Depends on OS |
| Audit | ✅ NCC + ISRG | C codebase | Platform-dependent |
| Adoption | Growing fast (Cloudflare, Deno, Tokio stack) | Legacy | Legacy |
For Rust libs targeting mobile (Android via cargo-ndk, iOS native Cargo): rustls is the only sane choice — OpenSSL cross-compile is painful, native-tls has different APIs per platform.
Setup
[dependencies]
rustls = "0.23"
rustls-pemfile = "2"
webpki-roots = "0.26" # Mozilla CA bundle
tokio-rustls = "0.26" # async wrapper
rustls-pki-types = "1"
# Convenience: enable rustls in HTTP clients
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "rustls-tls-webpki-roots"] }
hyper-rustls = "0.27"
reqwest with default-features = false is critical — otherwise it pulls in native-tls (= OpenSSL).
Client — Connect Over TLS
use rustls::{ClientConfig, RootCertStore};
use rustls_pki_types::ServerName;
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio_rustls::TlsConnector;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Use Mozilla's trust roots
let mut root_store = RootCertStore::empty();
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
let connector = TlsConnector::from(Arc::new(config));
let tcp = TcpStream::connect("api.example.com:443").await?;
let domain = ServerName::try_from("api.example.com")?;
let mut tls = connector.connect(domain, tcp).await?;
tls.write_all(b"GET / HTTP/1.0\r\nHost: api.example.com\r\n\r\n").await?;
let mut response = String::new();
tls.read_to_string(&mut response).await?;
println!("{}", response);
Ok(())
}
Reqwest with rustls (Recommended for Most Apps)
[dependencies]
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json", "rustls-tls-webpki-roots"] }
let client = reqwest::Client::builder()
.use_rustls_tls()
.https_only(true) // refuse plain HTTP
.build()?;
let resp: serde_json::Value = client
.get("https://api.example.com/health")
.send()
.await?
.json()
.await?;
Server — Listen with TLS
use rustls::ServerConfig;
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use std::fs::File;
use std::io::BufReader;
use tokio::net::TcpListener;
use tokio_rustls::TlsAcceptor;
fn load_certs(path: &str) -> Result<Vec<CertificateDer<'static>>, Box<dyn std::error::Error>> {
let mut reader = BufReader::new(File::open(path)?);
let certs: Vec<_> = rustls_pemfile::certs(&mut reader)
.filter_map(Result::ok)
.collect();
Ok(certs)
}
fn load_key(path: &str) -> Result<PrivateKeyDer<'static>, Box<dyn std::error::Error>> {
let mut reader = BufReader::new(File::open(path)?);
let key = rustls_pemfile::private_key(&mut reader)?
.ok_or("no private key found")?;
Ok(key)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let certs = load_certs("server.crt")?;
let key = load_key("server.key")?;
let config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)?;
let acceptor = TlsAcceptor::from(Arc::new(config));
let listener = TcpListener::bind("0.0.0.0:8443").await?;
loop {
let (tcp, _addr) = listener.accept().await?;
let acceptor = acceptor.clone();
tokio::spawn(async move {
if let Ok(mut tls) = acceptor.accept(tcp).await {
// handle TLS-wrapped stream
}
});
}
}
Mutual TLS (mTLS — Client Cert Auth)
// Server requires client cert
use rustls::server::WebPkiClientVerifier;
let mut client_root_store = RootCertStore::empty();
client_root_store.add_parsable_certificates(load_certs("ca.crt")?);
let verifier = WebPkiClientVerifier::builder(client_root_store.into()).build()?;
let config = ServerConfig::builder()
.with_client_cert_verifier(verifier)
.with_single_cert(server_certs, server_key)?;
// Client presents cert
let client_certs = load_certs("client.crt")?;
let client_key = load_key("client.key")?;
let config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_client_auth_cert(client_certs, client_key)?;
Certificate Pinning
For BHODL-style wallet apps, pin server cert hash to defend against MITM (rogue CA, compromised TLS termination).
use rustls::client::danger::{ServerCertVerified, ServerCertVerifier, HandshakeSignatureValid};
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::DigitallySignedStruct;
#[derive(Debug)]
struct PinnedCertVerifier {
expected_pins: Vec<[u8; 32]>, // SHA-256 of leaf cert SPKI
}
impl ServerCertVerifier for PinnedCertVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer,
_intermediates: &[CertificateDer],
_server_name: &ServerName,
_ocsp: &[u8],
_now: UnixTime,
) -> Result<ServerCertVerified, rustls::Error> {
use sha2::{Sha256, Digest};
let leaf_hash: [u8; 32] = Sha256::digest(end_entity.as_ref()).into();
if self.expected_pins.iter().any(|pin| pin == &leaf_hash) {
Ok(ServerCertVerified::assertion())
} else {
Err(rustls::Error::General("cert not pinned".into()))
}
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &CertificateDer,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
rustls::crypto::ring::default_provider()
.signature_verification_algorithms
.supported_schemes()
}
}
let verifier = PinnedCertVerifier {
expected_pins: vec![
[0xab, 0xcd, /* ... 30 more bytes */],
[0xff, 0xee, /* backup pin */],
],
};
let config = ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(verifier))
.with_no_client_auth();
Critical for wallets: pin SPKI hash, not full cert (allows cert rotation without app update if SPKI stays same). Always have backup pins.
ALPN (HTTP/2, HTTP/3 Negotiation)
let mut config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
For h3 (QUIC): use quinn crate (separate, not raw TLS).
Session Resumption
use rustls::client::Resumption;
let config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
// session resumption is on by default
To customize:
config.resumption = Resumption::store(Arc::new(MyTicketStorage));
Speeds up reconnections (1-RTT vs 2-RTT handshake).
Custom Crypto Provider (FIPS, AWS-LC-rs)
rustls 0.23+ separates crypto providers:
# Default: ring (BoringSSL crypto, well-audited)
rustls = "0.23"
# Alternative: aws-lc-rs (FIPS-validated, faster on x86_64)
rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs"] }
use rustls::crypto::{ring, aws_lc_rs};
let provider = ring::default_provider(); // or aws_lc_rs::default_provider()
provider.install_default()?;
ring works everywhere (incl. mobile). aws-lc-rs faster on x86_64 server but heavier deps.
Loading Certs from Various Formats
From PEM file
let mut reader = BufReader::new(File::open("ca.pem")?);
let certs: Vec<CertificateDer> = rustls_pemfile::certs(&mut reader)
.collect::<Result<Vec<_>, _>>()?;
From DER bytes (binary)
let der_bytes: Vec<u8> = std::fs::read("cert.der")?;
let cert = CertificateDer::from(der_bytes);
Embedded in binary (recommended for mobile)
const CA_PEM: &[u8] = include_bytes!("../certs/ca.pem");
let mut reader = std::io::Cursor::new(CA_PEM);
let certs: Vec<_> = rustls_pemfile::certs(&mut reader)
.collect::<Result<Vec<_>, _>>()?;
webpki-roots vs Native Roots
[dependencies]
webpki-roots = "0.26" # Mozilla NSS CA bundle, embedded
# OR
rustls-native-certs = "0.8" # Use OS trust store
// webpki-roots (recommended for cross-platform)
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
// rustls-native-certs (use OS roots — Keychain on macOS, etc.)
for cert in rustls_native_certs::load_native_certs()? {
root_store.add(cert)?;
}
| Strategy | Pros | Cons |
|---|---|---|
webpki-roots | Consistent across platforms, audited bundle | Stale until app update |
rustls-native-certs | OS-managed, fresh | Different per platform, OS bugs propagate |
For mobile wallets: webpki-roots + cert pinning for known endpoints.
Mobile Cross-Compile
# Android (cargo-ndk)
cargo ndk -t arm64-v8a -o jniLibs build --release \
--features="rustls,rustls/ring"
# iOS
cargo build --release --target aarch64-apple-ios \
--features="rustls/ring"
ring cross-compiles cleanly to all common targets (Android ARM, iOS arm64, WASM in 2025+).
Testing TLS
#[tokio::test]
async fn test_tls_handshake() {
use rcgen::generate_simple_self_signed;
// Generate self-signed cert for testing
let cert = generate_simple_self_signed(vec!["localhost".into()]).unwrap();
let cert_der = cert.cert.der().clone();
let key_der = PrivateKeyDer::Pkcs8(cert.key_pair.serialize_der().into());
// Server
let server_config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(vec![cert_der.clone()], key_der)
.unwrap();
// Client trusts the test cert
let mut root_store = RootCertStore::empty();
root_store.add(cert_der).unwrap();
let client_config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
// ... test handshake
}
Performance
- TLS 1.3 handshake: 1-RTT (or 0-RTT with PSK)
- AES-GCM: hardware-accelerated on Intel/ARM via
ring - ChaCha20-Poly1305: better on ARM without AES-NI
- Memory: ~32-64 KB per connection
- Throughput: 1-10 Gbps on modern CPU (rivals OpenSSL)
Anti-Patterns
| Anti-pattern | Why it's bad | Correct approach |
|---|---|---|
default-features = true on reqwest | Pulls in OpenSSL | default-features = false + explicit rustls-tls feature |
Disabling cert verification (dangerous_accept_invalid_certs(true)) | MITM trivial | Only in tests with self-signed cert |
| Pinning full cert (not SPKI) | Breaks on cert rotation | Pin SPKI hash |
| No backup pin | Locks app on cert change | Always 2+ pins |
rustls-native-certs on mobile | Inconsistent OS behavior | webpki-roots for predictability |
Sharing Arc<ClientConfig> across thousands of conns | Lock contention possible | Generally fine; profile if hot path |
Custom verifier without dangerous() builder | Compile error | Use .dangerous().with_custom_certificate_verifier(...) |
| Mixing TLS 1.0/1.1 expectations | Removed from rustls | Negotiate 1.2+/1.3 |
| Forgetting ALPN for HTTP/2 server | Protocol negotiation fails | Set alpn_protocols |
| OpenSSL bindings for "performance" | rustls is competitive + safer | Use rustls unless benchmarks justify |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
UnknownIssuer cert error | Trust roots empty | Add webpki-roots or pinned cert |
BadEncoding for cert | PEM/DER format mismatch | Verify with openssl x509 -in cert.pem -text |
Cross-compile failure: ring build error | Missing C toolchain on host | Install clang/gcc for cross-compile |
InvalidServerName error | Wrong hostname format | Use raw hostname, no port |
| Slow handshake | TLS 1.2 forced | Enable TLS 1.3 (default in rustls) |
| Client cert rejected | CA chain incomplete | Verify intermediate certs in chain |
NoCipherSuitesInCommon | Custom cipher list mismatch | Use defaults |
reqwest pulls openssl despite rustls-tls feature | Transitive dep enables default-tls | Audit cargo tree for openssl-sys |
| OOM after many TLS connections | Session cache unlimited | Configure session cache size |
| Mobile app size increased | rustls + crypto provider added | Acceptable cost (~500KB-1MB), often replaces OpenSSL anyway |
When NOT to Use This Skill
| Scenario | Use Instead |
|---|---|
| OpenSSL-specific (FIPS legacy, EC engine) | OpenSSL skill |
| Apple SecureTransport / Windows SChannel | Platform skills |
| Tor anonymity | network/arti |
| QUIC / HTTP/3 | quinn crate (separate) |
| TLS protocol theory | RFC 8446 / OWASP |
| WebPKI parsing | webpki crate alone |