Skip to content

TLS in Practice

Everything so far has been paper — keys and certificates at rest. TLS is what they do at work. OpenSSL can stand at both ends of the conversation: s_server is a small TLS server for testing, s_client a client that connects, shows its working, and reports exactly what it thought of the certificate it was handed. Between them, on one machine, you can run the whole protocol and read it.

A server

From the lab directory, serving the lab's server certificate on port 8443:

$ openssl s_server -accept 8443 -key server.key.pem -cert server.cert.pem -cert_chain chain.pem -www

-cert_chain points at the bundle the server should present alongside its own certificate — the intermediate (and, in the lab's file, the root; harmless here, though real servers send intermediates only, since the root must come from the client's own store to mean anything). -www makes it answer an HTTP request with a status page; without it, s_server echoes a tiny interactive protocol. Leave it running and open a second terminal.

A client that trusts the lab

$ openssl s_client -connect 127.0.0.1:8443 -CAfile root/root.cert.pem -verify_hostname server.example.test

Three clauses do the trusting: -CAfile replaces the trust store with the lab root alone; -verify_hostname says which name the certificate must cover (necessary because we dialled an IP address — there was no name to infer it from); and the connection itself supplies the rest. The output, in the order s_client prints it, is the handshake's story:

depth=2 CN = Mere Mortals Root CA, O = Example Lab
verify return:1
depth=1 CN = Mere Mortals Intermediate CA, O = Example Lab
verify return:1
depth=0 CN = server.example.test, O = Example Lab
verify return:1
CONNECTED(00000003)
---
Certificate chain
 0 s:CN = server.example.test, O = Example Lab
   i:CN = Mere Mortals Intermediate CA, O = Example Lab
   a:PKEY: rsaEncryption, 2048 (bit); sigalg: RSA-SHA256
   v:NotBefore: Oct 18 17:39:13 2026 GMT; NotAfter: Oct 18 17:39:13 2027 GMT
 1 s:CN = Mere Mortals Intermediate CA, O = Example Lab
   i:CN = Mere Mortals Root CA, O = Example Lab
 [...]
 2 s:CN = Mere Mortals Root CA, O = Example Lab
   i:CN = Mere Mortals Root CA, O = Example Lab
 [...]

The walk from chapter 7, performed live: depth counts down from the root the server presented to the leaf at depth 0, each verify return:1 a signature checked on the way. Then the summary that matters:

New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server public key is 2048 bit
[...]
Verify return code: 0 (ok)

and it is a working connection — ask it for the page (s_server's -www answers):

$ printf 'GET / HTTP/1.0\r\n\r\n' | openssl s_client -connect 127.0.0.1:8443 -CAfile root/root.cert.pem -quiet
HTTP/1.0 200 ok
Content-type: text/html

Small print worth knowing: the full output also carries the line Server Temp Key: X25519, 253 bits — the ephemeral key the two ends generated fresh for this session. That, not the certificate's RSA key, is what encrypts the traffic: the certificate's job in TLS is authentication (proving the name), and the handshake agrees a disposable key for confidentiality. It is why a certificate can be public without the recorded traffic being readable later.

Names: SNI, briefly

Real servers hold many names on one address and choose a certificate based on the name the client asks for — the Server Name Indication. s_client sends it with -servername. Against the public internet (the one non-lab capture in this guide, taken on the build machine):

$ openssl s_client -connect example.com:443 -servername example.com
subject=CN = example.com
issuer=C = US, O = SSL Corporation, CN = Cloudflare TLS Issuing ECC CA 3
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Verify return code: 0 (ok)

No -CAfile: the system trust store vouched, exactly as your browser's does. Same client, same sentences — the lab and the internet differ only in who issued the paper.

Versions and ciphers: negotiation, then force

Left alone, both ends offer what they have and settle on the best common ground — TLS 1.3 here. Pin the server to TLS 1.2 (restart s_server with -tls1_2 added) and the settlement moves:

New, TLSv1.2, Cipher is ECDHE-RSA-AES256-GCM-SHA384
    Protocol  : TLSv1.2

The cipher name changes its shape, too: 1.3 suites are short fixed names (TLS_AES_256_ GCM_SHA384); 1.2 suites describe the whole bargain — key exchange (ECDHE), signature (RSA), bulk cipher and mode (AES256-GCM), hash (SHA384). openssl ciphers -v lists what your build can speak, in those terms.

Now make the client demand what the server won't give — -tls1_3 against the 1.2-only server:

$ openssl s_client -connect 127.0.0.1:8443 -CAfile root/root.cert.pem -tls1_3
[...]
error:0A00042E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version:...:SSL alert number 70

Alert 70 is TLS's own error code for "protocol version" — the server answered the hello with a refusal, in protocol. And note the trap in that output: it ends with Verify return code: 0 (ok). The verify code grades the certificate; no certificate was ever reached, so nothing failed it. Judge a connection by whether it connected — the alert, the cipher line, the missing page — not by the friendliest number on the page.

What breaks at the certificate layer, even when versions agree, is chapter 11 — every failure there is reproducible against the server you just ran.