rustls — modern, safe TLS implementation in pure Rust. Drop-in replacement for OpenSSL/native-tls in Rust apps. No C dependencies — perfect for mobile cross- compile and embedded targets. Covers ClientConfig + ServerConfig, certificate verification with webpki-roots, mTLS, custom verifier (cert pinning), ALPN negotiation (HTTP/2, HTTP/3), session resumption, integration with hyper + reqwest + tokio. USE WHEN: user mentions "rustls", "ClientConfig", "ServerConfig", "webpki-roots", "rustls-pemfile", "rustls cert pinning", "rustls mTLS", "rustls Tokio", "rustls hyper" DO NOT USE FOR: OpenSSL specifics - use OpenSSL skill (or platform TLS) DO NOT USE FOR: Apple/Windows native TLS - use platform-specific skills DO NOT USE FOR: Tor anonymous transport - use `network/arti` DO NOT USE FOR: TLS protocol theory - use OWASP / RFC docs

GitHub
Install command
npx skhub add claude-dev-suite/rustls
Markdown
SKILL.md

rustls — Pure-Rust TLS

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: rustls.

Why rustls

Featurerustlsopenssl-rsnative-tls
ImplementationPure RustBindings to OpenSSL CBindings to OS TLS (Sec/SChannel/OpenSSL)
Memory safety✅❌ Periodic CVEsInherits OS TLS bugs
Cross-compile to mobile✅ TrivialRequires OpenSSL buildPlatform-dependent
TLS 1.3✅ Default✅Depends on OS
TLS 1.2✅✅✅
TLS 1.0/1.1❌ RemovedConfigurableDepends on OS
Audit✅ NCC + ISRGC codebasePlatform-dependent
AdoptionGrowing fast (Cloudflare, Deno, Tokio stack)LegacyLegacy

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)?;
}
StrategyProsCons
webpki-rootsConsistent across platforms, audited bundleStale until app update
rustls-native-certsOS-managed, freshDifferent 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-patternWhy it's badCorrect approach
default-features = true on reqwestPulls in OpenSSLdefault-features = false + explicit rustls-tls feature
Disabling cert verification (dangerous_accept_invalid_certs(true))MITM trivialOnly in tests with self-signed cert
Pinning full cert (not SPKI)Breaks on cert rotationPin SPKI hash
No backup pinLocks app on cert changeAlways 2+ pins
rustls-native-certs on mobileInconsistent OS behaviorwebpki-roots for predictability
Sharing Arc<ClientConfig> across thousands of connsLock contention possibleGenerally fine; profile if hot path
Custom verifier without dangerous() builderCompile errorUse .dangerous().with_custom_certificate_verifier(...)
Mixing TLS 1.0/1.1 expectationsRemoved from rustlsNegotiate 1.2+/1.3
Forgetting ALPN for HTTP/2 serverProtocol negotiation failsSet alpn_protocols
OpenSSL bindings for "performance"rustls is competitive + saferUse rustls unless benchmarks justify

Troubleshooting

SymptomCauseFix
UnknownIssuer cert errorTrust roots emptyAdd webpki-roots or pinned cert
BadEncoding for certPEM/DER format mismatchVerify with openssl x509 -in cert.pem -text
Cross-compile failure: ring build errorMissing C toolchain on hostInstall clang/gcc for cross-compile
InvalidServerName errorWrong hostname formatUse raw hostname, no port
Slow handshakeTLS 1.2 forcedEnable TLS 1.3 (default in rustls)
Client cert rejectedCA chain incompleteVerify intermediate certs in chain
NoCipherSuitesInCommonCustom cipher list mismatchUse defaults
reqwest pulls openssl despite rustls-tls featureTransitive dep enables default-tlsAudit cargo tree for openssl-sys
OOM after many TLS connectionsSession cache unlimitedConfigure session cache size
Mobile app size increasedrustls + crypto provider addedAcceptable cost (~500KB-1MB), often replaces OpenSSL anyway

When NOT to Use This Skill

ScenarioUse Instead
OpenSSL-specific (FIPS legacy, EC engine)OpenSSL skill
Apple SecureTransport / Windows SChannelPlatform skills
Tor anonymitynetwork/arti
QUIC / HTTP/3quinn crate (separate)
TLS protocol theoryRFC 8446 / OWASP
WebPKI parsingwebpki crate alone
Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

MIT

Source path

skills/network/rustls

Default branch

main

Latest commit

9496306

Tree SHA

fe4e2f1