>_ josetilabs.com
← blog

17 Oct 2026 · security, tls, linux · 6 min

What happens in a TLS handshake

The full TLS 1.3 handshake read 778 bytes from the server. Resuming the session with the ticket the server had handed out, the same connection read 241 bytes. None of those lines land in your server log, and it still decides whether the connection is encrypted or plain noise.

The first message and the fresh key

The ClientHello is 306 bytes and carries two things that matter: the highest version the client accepts, and a freshly generated X25519 public key. Not the server’s key, nothing stored on disk. It is a throwaway key, born for that connection and forgotten after.

Here is what the client sent, when you ask openssl to show the state machine:

bash
openssl s_client -connect example.com:443 -servername example.com -state 2>&1 | head -20

The server answers with the ServerHello, which picks the final version and hands back its own public key. The two halves together produce a secret neither side could compute alone. From there on, the conversation is encrypted with it.

Sequencia do handshake TLS 1.3: ClientHello, ServerHello com chave X25519, Extensions cifradas, certificado P-256 assinado com ECDSA, Finished, e ticket de sessao entregue pelo servidor
Six messages, one round trip. From the third onward, what travels is already encrypted with the secret those two keys built.

The certificate and what it proves

After the ServerHello comes the certificate. On the one I measured, the server key is P-256 and the signature is ECDSA over SHA-256, in a 607 byte certificate:

bash
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -issuer -dates

The certificate is not encrypted, and it should not be. It is the part of the conversation the client has to read in the clear to work out who is on the other side. What it guarantees is the name and the signature, not the content of the page. For what a certificate does and does not settle, the twelve forged certificates case shows the weak side.

On its own it settles nothing without the chain. Each one points at whoever issued it, and that chain has to end at a root the system trusts. Above the root sits the pin that stops an intermediate from issuing any certificate for your name.

Finished and the session ticket

The handshake is only done when both sides send Finished, signed with the secret derived from the keys. That closes off the man in the middle: whoever sits on the path sees the public key but cannot produce a valid Finished.

Then comes the most underrated message of all, which arrives after the handshake is already done: the session ticket, a key that summarizes everything that was negotiated.

Measure the whole handshake on your own machine

The script below starts a local TLS server with a self-signed certificate, connects to it with s_client, and prints the six messages in order. Nothing leaves the machine.

bash
#!/usr/bin/env bash
# handshake: brings up a local TLS 1.3 server, measures full and resumed.
set -u

if ! command -v openssl > /dev/null 2>&1; then
  echo "openssl is missing: install it with apt install openssl and run again"
  exit 0
fi

DIR="$(mktemp -d)"
PORT=14433

# self-signed certificate, P-256, valid for 2 days
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
  -nodes -keyout "$DIR/key.pem" -out "$DIR/cert.pem" -days 2 \
  -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost" 2>/dev/null

echo "== certificate =="
openssl x509 -in "$DIR/cert.pem" -noout -subject -dates

openssl s_server -accept $PORT -cert "$DIR/cert.pem" -key "$DIR/key.pem" \
  -www -tls1_3 > /dev/null 2>&1 &
SRV=$!
sleep 2

ask() { printf 'GET / HTTP/1.0\r\n\r\n'; sleep 2; }

echo
echo "== full handshake, with the states =="
ask | openssl s_client -connect 127.0.0.1:$PORT -servername localhost \
  -sess_out "$DIR/sess.pem" -state 2>&1 | grep -E "SSL_connect|Server Temp Key|Cipher is"

echo
echo "== resumed handshake, with the ticket =="
ask | openssl s_client -connect 127.0.0.1:$PORT -servername localhost \
  -sess_in "$DIR/sess.pem" 2>&1 | grep -E "Reused|New,|handshake has read"

kill $SRV 2>/dev/null
rm -rf "$DIR"

How to verify

The two lines to look at are the New, and the Reused,. On the first run you get New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384, on the second Reused, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384. The Reused is what tells you the ticket worked.

The self-signed certificate makes verification fail, and that is fine: you will see Verification error: self-signed certificate, which is the correct complaint about a certificate nobody signed. Against a public site the same command shows Verification: OK.

How big the handshake is

Here is the number I like looking at most. A full handshake reads 778 bytes from the server; a resumed one reads 241. That is a 69 percent drop, and it comes down to a single thing: the certificate is 607 bytes and does not need to be sent again.

Comparacao de bytes lidos do servidor em dois handshakes TLS 1.3: o handshake completo le 778 bytes e o retomado com ticket le 241 bytes, uma queda de 69 por cento
778 bytes on the first conversation, 241 on the second. The 607 byte certificate is the whole difference between the two bars.

On a public site with a 4 certificate chain the numbers move: the full handshake read 3,727 bytes on TLS 1.3 and 3,878 on TLS 1.2. The chain weighs more than the version difference, which is why 6 day certificates make sense: less chain to carry.

If it fails

connect: Connection refused when connecting. The s_server never came up. Check whether port 14433 was already taken with ss -ltnp | grep 14433 and switch the script to another one.

openssl is missing. The script says so and stops before running anything else. Install the package and run it again.

Reused never shows up. The server may be missing -tls1_3, or the session expired between the two runs. Run the two lines back to back, without a long pause in between.

What changed from 1.2 to 1.3

TLS 1.2 TLS 1.3
messages 2 round trips 1 round trip
certificate signature over the whole handshake over the certificate only
common cipher ECDHE-ECDSA-CHACHA20-POLY1305 TLS_AES_256_GCM_SHA384
bytes read on the public example 3,878 3,727

The win in 1.3 is not only time. Taking the signature off the whole handshake makes the server signature cover several sessions, which is the part that makes it cheaper.

FAQ

Is the handshake the same as the connection? No. After Finished the connection is established, and the same key keeps encrypting the data. The handshake happens once, the connection lasts.

Why does the public key travel in the clear in the ClientHello? Because there is no secret yet. Anyone who reads the ClientHello learns the public key of a session they just stopped themselves from decrypting.

Does TLS 1.3 drop the certificate? No. It leaves the critical path and arrives after the key is established, but it is still the only proof of who the server is. For what it proves and does not prove, see the forged certificates and the CAA list.

Is it worth forcing TLS 1.3? It is, and the cost is small: session resumption is itself the mechanism that cuts per-connection work. What is not worth doing is promising 1.3 and leaving old clients with no path.

Sources

Read next

// By e-mail

Posts published here also go out by e-mail. Delivery has not started yet: whoever leaves an address now joins the first batch.

I store only your e-mail, the date and the record of this consent. No cookies, no trackers, nothing shared with third parties. Details in the privacy policy.