Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lesson 14: Real TLS with tokio-rustls

Alice’s Bookstore — Chapter 14

Alice looks at her hand-built protocol: DH key exchange, HKDF, ChaCha20-Poly1305, Ed25519 signatures, counter nonces. It works, but…

“Bob, I built this to learn. But should I actually use it in production? I’m worried I missed something — a subtle timing attack, a nonce edge case, some protocol flaw I don’t even know about.”

“Absolutely not. Never use your own crypto in production. You built it to understand what happens inside TLS. Now you replace it with a battle-tested library that does the same thing — but has been audited by hundreds of experts.”

“So all that work was for nothing?”

“No — it was for understanding. When you use tokio-rustls now, you’ll know exactly what it’s doing under the hood. You won’t be cargo-culting. You’ll debug TLS issues faster than anyone who never built their own.”

Real-life analogy: from hand-built to factory-made

You've been building a car from scratch:
  Engine (encryption) → Transmission (key exchange) → Brakes (authentication)

Now you walk into a car factory (rustls):
  "Oh, they do the same thing I did, but with:
   - Better engineering
   - Safety testing
   - Mass production efficiency
   - Features I didn't think of (session resumption, ALPN, SNI)"

You understand every part because you built one yourself.

The payoff

You’ve built TLS from scratch: key exchange (Lesson 4), key derivation (Lesson 5), encryption (Lesson 2), authentication (Lesson 12), replay defense (Lesson 12). Now use a production TLS library and see how everything maps.

rustls vs OpenSSL

Two main TLS libraries in the Rust ecosystem:

rustlsOpenSSL (via openssl crate)
LanguagePure RustC (with Rust bindings)
SafetyMemory-safe by defaultLong history of CVEs (Heartbleed, etc.)
PerformanceCompetitive, sometimes fasterHardware-accelerated AES
CiphersModern only (TLS 1.2+)Everything including legacy
DependenciesNo C toolchain neededRequires OpenSSL headers/lib

rustls is the natural choice for Rust projects. tokio-rustls wraps it for async I/O.

How rustls maps to your lessons

When you call TlsConnector::connect(), here’s what happens inside:

1. Client sends ClientHello
   → "I support X25519 key exchange, ChaCha20-Poly1305 encryption"       (Lessons 2, 4)

2. Server responds with ServerHello + Certificate + CertificateVerify
   → Server's DH public key                                               (Lesson 4)
   → Server's X.509 certificate                                           (Lesson 6)
   → Signature over handshake transcript                                   (Lesson 3)

3. Both sides derive keys
   → HKDF from DH shared secret                                           (Lesson 5)

4. Encrypted application data flows
   → ChaCha20-Poly1305 with counter nonces                                (Lessons 2, 10)
   → Server authenticated via certificate                                  (Lesson 12)

Everything you built by hand — rustls does automatically in ~2ms.

The code change

Your Lesson 14 server/client required ~50 lines of handshake code. With tokio-rustls:

Server:

#![allow(unused)]
fn main() {
let acceptor = TlsAcceptor::from(Arc::new(server_config));
let (tcp_stream, _) = listener.accept().await?;
let tls_stream = acceptor.accept(tcp_stream).await?;
// tls_stream implements AsyncRead + AsyncWrite
// Just read and write plaintext — TLS handles everything
}

Client:

#![allow(unused)]
fn main() {
let connector = TlsConnector::from(Arc::new(client_config));
let tcp_stream = TcpStream::connect("127.0.0.1:7878").await?;
let tls_stream = connector.connect(server_name, tcp_stream).await?;
// Read and write plaintext
}

That’s it. The entire handshake — DH, signatures, certificates, key derivation — happens inside accept() / connect().

Generating proper certificates

For Lesson 6, you generated a self-signed cert with openssl CLI. For rustls, you need:

  1. A CA certificate (self-signed root)
  2. A server certificate signed by that CA
  3. The server’s private key

You can use the rcgen crate to generate these in Rust, or use openssl:

# Generate CA key and cert
openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout ca.key -out ca.crt -days 365 -subj "/CN=My CA"

# Generate server key and CSR
openssl req -newkey rsa:2048 -nodes \
  -keyout server.key -out server.csr -subj "/CN=localhost"

# Sign server cert with CA
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out server.crt -days 365

The client trusts ca.crt. The server presents server.crt + server.key.

Real-world scenarios

Adding TLS to any TCP service

You have a plaintext TCP service (database, cache, message broker). Adding TLS:

  1. Generate certificates
  2. Server: wrap TcpListener with TlsAcceptor
  3. Client: wrap TcpStream with TlsConnector
  4. No other code changes — the TLS stream implements the same Read/Write traits

This is how PostgreSQL, Redis, and gRPC add TLS. The application protocol doesn’t change.

The difference you can see

Run your Lesson 14 server and capture traffic:

sudo tcpdump -i lo0 port 7878 -w lesson8.pcap

Run your Lesson 11 server and capture traffic:

sudo tcpdump -i lo0 port 7878 -w lesson11.pcap

Open both in Wireshark. Lesson 12 shows your custom handshake (raw DH keys). Lesson 11 shows a standard TLS 1.3 handshake that Wireshark can parse and label: ClientHello, ServerHello, Certificate, Finished, Application Data.

Exercises

Exercise 1: TLS echo server (implemented in 14-real-tls-server.rs and 14-real-tls-client.rs)

Build an echo server using tokio-rustls. Generate certificates, configure server and client, verify encrypted communication works.

Exercise 2: Inspect with openssl s_client

Start your TLS server, then connect with:

openssl s_client -connect 127.0.0.1:7878 -CAfile ca.crt

You’ll see the full TLS handshake: protocol version, cipher suite, certificate chain. Type a message — it gets echoed through real TLS.

Exercise 3: Cipher suite selection

Print which cipher suite was negotiated:

#![allow(unused)]
fn main() {
let (_, server_conn) = tls_stream.get_ref();
println!("Cipher: {:?}", server_conn.negotiated_cipher_suite());
}

Try configuring the server to only allow ChaCha20-Poly1305 or only AES-GCM and see what gets selected.

Exercise 4: Compare with your hand-built TLS

Run both Lesson 12 and Lesson 11 servers. Use time to measure handshake latency. Compare code complexity. Think about what rustls handles that your implementation doesn’t: cipher negotiation, session resumption, certificate chain validation, ALPN, SNI, etc.