<!-- https://pkizerotohero.com/handbook/tls-handshake/ | Markdown mirror of the HTML page | PKI Zero to Hero, by Duyutech BV -->

> The TLS 1.3 handshake step by step with real openssl output: ClientHello, key share, SNI, ALPN, CertificateVerify, Finished and forward secrecy.

Source: https://pkizerotohero.com/handbook/tls-handshake/ · Last reviewed: 2026-10-20

Part IV: Trust and HTTPS · Chapter 15

# The TLS 1.3 handshake

**In one sentence:**

In one round trip, client and server agree fresh keys with ECDHE, the server proves it holds the certificate's key by signing the handshake, and both confirm nothing was altered.

## Who is this for

- [Developer](https://pkizerotohero.com/start/#developer)
- [IT ops](https://pkizerotohero.com/start/#itops)
- [Expert](https://pkizerotohero.com/start/#expert)
- [Beginner](https://pkizerotohero.com/start/#beginner)
- [Leader](https://pkizerotohero.com/start/#leader)

Level: **Practitioner**

- [New to this? Start with The idea](https://pkizerotohero.com/handbook/tls-handshake/#the-idea)
- [Developer? Jump to How it really works](https://pkizerotohero.com/handbook/tls-handshake/#how-it-really-works)
- [IT ops? At work](https://pkizerotohero.com/handbook/tls-handshake/#at-work)
- [Leader? Why it matters](https://pkizerotohero.com/handbook/tls-handshake/#why-it-matters-to-the-business)

## The idea

- Beginner

Every time your browser opens an HTTPS page, it first has a short conversation with the server, before a single byte of the page is sent. This conversation is the [TLS handshake](https://pkizerotohero.com/handbook/glossary/#tls-handshake). It has three jobs:

1. **Agree on fresh secret keys** that nobody listening on the network can work out.
2. **Prove that the server really is the name you asked for**, using its certificate and its private key.
3. **Make sure nobody changed the conversation** on the way.

In TLS 1.3, the current version, this takes a single round trip:

- **The browser says hello.** “I want `shop.example.com`. Here are the encryption methods I support, and here is my half of a key exchange.”
- **The server answers.** “Let’s use this method. Here is my half.” Each side now combines its own secret with the other’s half and gets the same shared secret, exactly as in [chapter 4](https://pkizerotohero.com/handbook/key-exchange/). Everything after this point is encrypted.
- **The server proves who it is.** It sends its certificate chain, then a signature over the whole conversation so far, made with the private key that belongs to the certificate. Only the real owner of that key can produce it.
- **Both sides confirm.** Each sends a short check value over the whole conversation. If anything was changed on the way, the values do not match and the connection stops.

Two things in this story surprise people. First, **the certificate’s key does not encrypt your data**. It only signs. The keys that encrypt the web page come from the fresh key exchange and are thrown away afterwards. Second, because those keys are thrown away, someone who records your traffic today and steals the server’s private key next year still cannot decrypt the recording. That property is called [forward secrecy](https://pkizerotohero.com/handbook/glossary/#forward-secrecy).

In the game

In the game: the Barbican

In Level 15, Ken puts the steps at the Barbican, the castle’s gatehouse, in the right order. The visitor and the gatekeeper first agree on a secret across the drawbridge without saying it aloud. Then the gatekeeper shows the castle’s certificate and signs the day’s gate log with the castle’s key, so the visitor knows the gate is the real one.

Where the analogy stops being true: a gate might keep the same password for a day, but TLS makes new keys for every connection and throws them away. And the gatekeeper’s signature is not a lock on the gate: it encrypts nothing. It only proves who is speaking.

## How it really works

- Developer
- Expert

TLS 1.3 is defined in RFC 9846. The handshake messages are described in its [section 4](https://www.rfc-editor.org/rfc/rfc9846#section-4), and [section 2](https://www.rfc-editor.org/rfc/rfc9846#section-2) gives the full picture.

*Diagram: A message sequence between a client on the left and a server on the right. 1, client to server: ClientHello with key share, SNI and ALPN. 2, server to client: ServerHello with the server's key share. A line marks that everything below is encrypted. 3, server to client: EncryptedExtensions with the chosen ALPN protocol. 4: Certificate, the server's chain. 5: CertificateVerify, a signature over the handshake made with the certificate's private key. 6: server Finished. 7, client to server: client Finished. 8, both ways: application data encrypted with traffic keys. A note says the client checks the chain, the name and the CertificateVerify signature, each marked with a tick.*

TLS 1.3 needs one round trip. The certificate's key signs; the ECDHE key exchange produces the encryption keys.

### What is the order of messages?

1. **ClientHello** ([section 4.2.2](https://www.rfc-editor.org/rfc/rfc9846#section-4.2.2)). A random value, the cipher suites the client supports, and extensions. The important ones:
    - `supported_versions`: TLS 1.3 is negotiated here, not in the old version field;
    - `supported_groups` and `key_share` ([section 4.3.8](https://www.rfc-editor.org/rfc/rfc9846#section-4.3.8)): the client’s ephemeral public key for one or more key-exchange groups, for example X25519;
    - `signature_algorithms`: which signatures the client can verify;
    - `server_name`, the [Server Name Indication](https://pkizerotohero.com/handbook/glossary/#sni) (SNI), and ALPN, explained below.
2. **ServerHello** ([section 4.2.3](https://www.rfc-editor.org/rfc/rfc9846#section-4.2.3)). The chosen cipher suite and the server’s own ephemeral key share. Both sides now compute the same ECDHE shared secret and derive handshake keys from it. “All handshake messages after the ServerHello are now encrypted” ([section 1.3](https://www.rfc-editor.org/rfc/rfc9846#section-1.3)). If the client guessed a group the server does not want, the server first sends a HelloRetryRequest and the client tries again ([section 4.2.4](https://www.rfc-editor.org/rfc/rfc9846#section-4.2.4)).
3. **EncryptedExtensions** ([section 4.4.1](https://www.rfc-editor.org/rfc/rfc9846#section-4.4.1)). Extensions that do not affect the keys, such as the chosen ALPN protocol.
4. **CertificateRequest**, only when the server wants a client certificate ([chapter 23](https://pkizerotohero.com/handbook/mutual-tls/)).
5. **Certificate** ([section 4.5.1](https://www.rfc-editor.org/rfc/rfc9846#section-4.5.1)). The server’s chain, leaf first ([chapter 13](https://pkizerotohero.com/handbook/certificate-chains/)).
6. **CertificateVerify** ([section 4.5.2](https://www.rfc-editor.org/rfc/rfc9846#section-4.5.2)). The server’s signature over the transcript so far.
7. **Finished** ([section 4.5.3](https://www.rfc-editor.org/rfc/rfc9846#section-4.5.3)). A MAC over the whole transcript, using a key derived from the handshake secret.
8. The client checks everything, sends its own **Finished**, and application data flows, encrypted with traffic keys derived from the same secret.

### What does the certificate’s key actually do?

It signs **CertificateVerify**, and nothing else. The signed content is 64 spaces, the context string “TLS 1.3, server CertificateVerify”, a zero byte, and the hash of every handshake message so far, including both key shares. That proves three things at once:

- the server holds the private key that matches the certificate’s public key;
- the signature belongs to this connection, because the transcript includes both sides’ fresh random values and key shares;
- the signature cannot be reused in another protocol, because of the context string.

The client verifies it with the public key from the leaf certificate, after validating the chain and the name. A certificate without its private key is useless to an attacker, because they cannot produce this signature. RSA keys must sign with RSA-PSS in TLS 1.3 ([section 4.3.3](https://www.rfc-editor.org/rfc/rfc9846#section-4.3.3)).

This is the biggest change from older TLS. In TLS 1.2 with RSA key exchange, the client encrypted the premaster secret to the server’s certificate key, so anyone who later stole that key could decrypt recorded sessions. TLS 1.3 removed this: “Static RSA and Diffie-Hellman cipher suites have been removed”, and “all public-key based key exchange mechanisms now provide forward secrecy” ([section 1.3](https://www.rfc-editor.org/rfc/rfc9846#section-1.3)).

### What is forward secrecy, and where are its limits?

In [ECDHE](https://pkizerotohero.com/handbook/glossary/#ecdhe), each side makes a new key pair for the connection, uses it once and discards it. The session keys exist only in memory during the connection. A stolen certificate key lets an attacker impersonate the server from then on, until the certificate is revoked or expires, but it does not unlock past recordings.

Two limits are worth knowing:

- **Resumption.** A client can resume an earlier session with a pre-shared key from a session ticket. Used alone, without a new ECDHE exchange, that comes “at the cost of losing forward secrecy for the application data” ([section 2.2](https://www.rfc-editor.org/rfc/rfc9846#section-2.2)). Most clients combine the ticket with a fresh key share.
- **Quantum computers.** A large future quantum computer could break today’s ECDHE from a recording, so browsers now add a post-quantum algorithm to the key exchange: X25519MLKEM768 hybrid post-quantum key exchange is on by default in Chrome, Firefox and Apple's operating systems (late 2024 in Chrome 131 and Firefox 132). See [chapter 26](https://pkizerotohero.com/handbook/post-quantum/).

### What are SNI and default certificates?

One IP address often serves hundreds of websites. The server must pick a certificate before any HTTP request arrives, so the client names the site in the ClientHello’s `server_name` extension ([RFC 6066, section 3](https://www.rfc-editor.org/rfc/rfc6066#section-3)). The TLS 1.3 specification says the server uses it “to guide certificate selection”, and that clients “SHOULD send this extension” ([section 4.5.1.2](https://www.rfc-editor.org/rfc/rfc9846#section-4.5.1.2)).

When a client sends no SNI, the server can only guess. It sends its **default certificate**, usually for some other name, or it ends the handshake with an alert, such as `unrecognized_name` (112). Clients that connect to a bare IP address send no SNI at all, because “Literal IPv4 and IPv6 addresses are not permitted” in it.

SNI travels in plain text, so anyone on the network path can see which site you visit, even though they cannot read the page. Encrypted Client Hello hides it, standardised in RFC 9849 (March 2026).

### What is ALPN?

Application-Layer Protocol Negotiation ([RFC 7301](https://www.rfc-editor.org/rfc/rfc7301)) lets the client list the protocols it can speak inside TLS, such as `h2` (HTTP/2) and `http/1.1`. The server picks one and announces it in EncryptedExtensions. ACME’s TLS-ALPN-01 challenge uses the same mechanism with the special protocol `acme-tls/1` ([chapter 11](https://pkizerotohero.com/handbook/domain-validation/)).

### What about older versions?

TLS 1.2 is still widely supported, and with ECDHE cipher suites it also gives forward secrecy. TLS 1.0 and 1.1 are retired: RFC 8996 formally deprecated TLS 1.0 and 1.1 (March 2021), and current browsers no longer accept them by default.

## At work

- Developer
- IT ops

### How do I watch a real handshake?

`openssl s_client` can print every handshake message. We connected to letsencrypt.org in October 2026 with OpenSSL 3.2, asked for HTTP/2 or HTTP/1.1, and kept only the message and extension lines of the `-trace` output:

```bash
openssl s_client -connect letsencrypt.org:443 -servername letsencrypt.org -alpn h2,http/1.1 -trace </dev/null
```

```text
    ClientHello, Length=339
        extension_type=server_name(0), length=20
        extension_type=supported_groups(10), length=22
        extension_type=application_layer_protocol_negotiation(16), length=14
        extension_type=signature_algorithms(13), length=48
        extension_type=supported_versions(43), length=5
        extension_type=psk_key_exchange_modes(45), length=2
        extension_type=key_share(51), length=38
        (5 other extensions omitted)
    ServerHello, Length=118
      cipher_suite {0x13, 0x01} TLS_AES_128_GCM_SHA256
        extension_type=supported_versions(43), length=2
        extension_type=key_share(51), length=36
    EncryptedExtensions, Length=15
        extension_type=application_layer_protocol_negotiation(16), length=5
        extension_type=server_name(0), length=0
    Certificate, Length=3600
    CertificateVerify, Length=74
      Signature Algorithm: ecdsa_secp256r1_sha256 (0x0403)
    Finished, Length=32
    Finished, Length=32
```

The order matches the specification exactly: the client’s key share and SNI in the ClientHello, the server’s key share in the ServerHello, then encrypted messages carrying the chosen ALPN protocol, a 3,600-byte certificate chain, an ECDSA signature in CertificateVerify, and the two Finished messages, the server’s first and then the client’s. With `-msg` instead of `-trace` you also see a ChangeCipherSpec record from the client. That is a harmless leftover sent for compatibility with old network boxes, described in [appendix E.4](https://www.rfc-editor.org/rfc/rfc9846#appendix-E.4).

### How do I read the summary?

The normal output ends with a summary. The lines that matter, from a connection with `-alpn h2,http/1.1`:

```text
Peer signing digest: SHA256
Peer signature type: ECDSA
Server Temp Key: X25519, 253 bits
...
New, TLSv1.3, Cipher is TLS_AES_128_GCM_SHA256
...
ALPN protocol: h2
...
Verify return code: 0 (ok)
```

- **`Server Temp Key: X25519`**: the server’s ephemeral ECDHE key. “Temp” is the forward secrecy. A current browser would negotiate a hybrid post-quantum group here if the server supports one; OpenSSL 3.2 does not support one yet, while OpenSSL 3.5 and later offer X25519MLKEM768 by default.
- **`Peer signature type: ECDSA`**: the CertificateVerify signature, made with the certificate’s key.
- **`TLS_AES_128_GCM_SHA256`**: the symmetric cipher that encrypts the data with keys from the key exchange.
- **`ALPN protocol: h2`**: the server chose HTTP/2.

For a compact view that also checks the host name, use `-brief` with `-verify_hostname`:

```bash
openssl s_client -connect letsencrypt.org:443 -servername letsencrypt.org -verify_hostname letsencrypt.org -brief </dev/null
```

```text
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_128_GCM_SHA256
Peer certificate: CN=letsencrypt.org
Hash used: SHA256
Signature type: ECDSA
Verification: OK
Verified peername: letsencrypt.org
Server Temp Key: X25519, 253 bits
```

### What happens without SNI?

`openssl s_client` sends SNI automatically from the `-connect` name, which “is the default since OpenSSL 1.1.1” ([OpenSSL docs](https://docs.openssl.org/master/man1/openssl-s_client/)). `-noservername` turns it off, which shows what an old or misconfigured client gets. The same three servers, in October 2026:

```text
$ openssl s_client -connect letsencrypt.org:443 -noservername </dev/null
 0 s:C=US, ST=California, L=San Francisco, O=Netlify, Inc, CN=*.netlify.app
Verify return code: 0 (ok)

$ openssl s_client -connect www.rfc-editor.org:443 -noservername </dev/null
… ssl/tls alert handshake failure … SSL alert number 40

$ openssl s_client -connect nginx.org:443 -noservername </dev/null
… tlsv1 unrecognized name … SSL alert number 112
```

The first server answered with its hosting platform’s default certificate, for `*.netlify.app`. The second ended the handshake with `handshake_failure` (40), and the third with `unrecognized_name` (112). A browser would show a name error for the first and a connection error for the others.

Notice the trap in the first result: `Verify return code: 0 (ok)`. `s_client` checks the chain but **not the host name** unless you ask. With `-verify_hostname letsencrypt.org` added, the same connection reports:

```text
verify error:num=62:hostname mismatch
Verify return code: 62 (hostname mismatch)
```

Always add `-verify_hostname` when you use `s_client` to test a certificate, or test with curl, which checks names by default.

### How do I check protocol versions?

Force a version with `-tls1_2` or `-tls1_3`:

```text
$ openssl s_client -connect letsencrypt.org:443 -servername letsencrypt.org -tls1_2 -brief </dev/null
Protocol version: TLSv1.2
Ciphersuite: ECDHE-ECDSA-AES128-GCM-SHA256
...
Server Temp Key: X25519, 253 bits

$ openssl s_client -connect letsencrypt.org:443 -servername letsencrypt.org -tls1_1 -brief </dev/null
… no protocols available …
```

The server still accepts TLS 1.2, with ECDHE. The TLS 1.1 attempt never left the machine: OpenSSL 3.2 refuses to offer TLS 1.1 at its default security level. On your own servers, allow TLS 1.2 and 1.3 only. In nginx that is `ssl_protocols TLSv1.2 TLSv1.3;`, and each `server` block with its own `server_name` and certificate is selected through SNI. Mark one block `default_server` deliberately, so that clients without SNI get a predictable answer.

### Where do certificate errors appear?

Every certificate problem in the earlier chapters surfaces here, when the client processes the Certificate and CertificateVerify messages:

- name mismatch: [`NET::ERR_CERT_COMMON_NAME_INVALID`](https://pkizerotohero.com/errors/chrome-err-cert-common-name-invalid/) ([chapter 9](https://pkizerotohero.com/handbook/names-and-key-usage/));
- unknown root: [`NET::ERR_CERT_AUTHORITY_INVALID`](https://pkizerotohero.com/errors/chrome-err-cert-authority-invalid/) ([chapter 12](https://pkizerotohero.com/handbook/trust-stores/));
- missing intermediate: [`unable to get local issuer certificate`](https://pkizerotohero.com/errors/curl-unable-to-get-local-issuer-certificate/) ([chapter 13](https://pkizerotohero.com/handbook/certificate-chains/));
- expired or not yet valid: [`NET::ERR_CERT_DATE_INVALID`](https://pkizerotohero.com/errors/chrome-err-cert-date-invalid/) ([chapter 8](https://pkizerotohero.com/handbook/x509-certificates/));
- on .NET and PowerShell, all of these become [`The remote certificate is invalid according to the validation procedure`](https://pkizerotohero.com/errors/dotnet-remote-certificate-invalid/).

The error finder maps the message you see to its cause:

*Interactive tool: Paste your error (runs in the browser at [/tools/error-finder/](https://pkizerotohero.com/tools/error-finder/))*

Paste the exact error text from your browser, tool or log, and follow the link to its page.

## Common mistakes

- **“The certificate’s key encrypts the traffic.”** In TLS 1.3 it only signs the handshake in CertificateVerify. The traffic keys come from ECDHE.
- **“If the server’s private key leaks, all past traffic can be decrypted.”** Not with TLS 1.3 or ECDHE in TLS 1.2: the session keys were never derived from the certificate key. The leak does allow impersonation until the certificate is replaced and revoked.
- **“HTTPS hides which site I visit.”** The SNI in the ClientHello is plain text, and so is the DNS lookup unless it is encrypted. Encrypted Client Hello closes the first gap where servers support it.
- **“`Verify return code: 0 (ok)` means the certificate is right for the host.”** `s_client` checks the name only with `-verify_hostname`.
- **“The server picks its certificate from the HTTP Host header.”** The certificate is sent before any HTTP request. The server chooses it from the SNI.
- **“TLS 1.0 is fine as long as the certificate is good.”** TLS 1.0 and 1.1 are formally deprecated, and current browsers refuse them by default.

## Why it matters to the business

- Leader

Why it matters to the business

One round trip that carries all your trust

The handshake is where your certificates, keys and server settings meet every customer, so every certificate mistake becomes a visible failure here. TLS 1.3 gives forward secrecy by design: a key stolen tomorrow does not expose today’s traffic. Make sure your services allow only TLS 1.2 and 1.3, that old protocol versions are switched off, and that you have a plan for post-quantum key exchange, which browsers already use. Ask for a regular report of protocol versions and certificate status across your public endpoints.

## Check yourself

Check yourself

In a TLS 1.3 handshake, what does the server's certificate private key do?

1. It encrypts the session key for the client
2. It signs the handshake transcript in the CertificateVerify message
3. It encrypts the web pages sent to the client

Show answer

Answer B It signs the handshake transcript in the CertificateVerify message

What happened

It signs the handshake transcript, and nothing else.

Why

The encryption keys come from the ephemeral ECDHE exchange. The signature proves that the server holds the private key that belongs to its certificate.

The rule

TLS 1.3 key exchange is ephemeral. The certificate key only authenticates.

Check yourself

An attacker records TLS 1.3 traffic today and steals the server's private key next year. Can they decrypt the recording?

1. Yes, the private key decrypts everything
2. No, because the session keys came from ephemeral ECDHE keys that were discarded
3. Only if the certificate has expired by then

Show answer

Answer B No, because the session keys came from ephemeral ECDHE keys that were discarded

What happened

They cannot decrypt it with the stolen key.

Why

The session keys were derived from one-time ECDHE key pairs that no longer exist. The certificate key only signed the handshake.

The rule

Forward secrecy protects past sessions even if the long-term key leaks later.

Check yourself

A server hosts 50 websites on one IP address. How does it know which certificate to send?

1. From the Host header of the HTTP request
2. From the SNI extension in the ClientHello
3. It sends all 50 certificates and lets the client choose

Show answer

Answer B From the SNI extension in the ClientHello

What happened

It reads the host name from SNI in the ClientHello.

Why

The certificate is sent during the handshake, before any HTTP request exists, so the name must arrive in the first TLS message.

The rule

SNI selects the certificate. Without SNI, the server can only send its default.

## Go deeper

- [RFC 9846](https://www.rfc-editor.org/rfc/rfc9846): the TLS 1.3 specification. Section 2 gives an overview, and section 4 describes every handshake message.
- [RFC 6066, section 3](https://www.rfc-editor.org/rfc/rfc6066#section-3): Server Name Indication.
- [RFC 7301](https://www.rfc-editor.org/rfc/rfc7301): Application-Layer Protocol Negotiation.
- [RFC 9849](https://www.rfc-editor.org/rfc/rfc9849): Encrypted Client Hello, which hides the SNI.
- [RFC 8996](https://www.rfc-editor.org/rfc/rfc8996): the deprecation of TLS 1.0 and 1.1.
- [OpenSSL `s_client`](https://docs.openssl.org/master/man1/openssl-s_client/): `-servername`, `-alpn`, `-msg`, `-trace` and `-brief`.
- [nginx: configuring HTTPS servers](https://nginx.org/en/docs/http/configuring_https_servers.html): SNI, default servers and protocol settings.

The crash course covers the handshake in [lesson 7, The handshake and its errors](https://pkizerotohero.com/course/handshake-and-errors/).

**Early edition.** Checked against the standards; an independent expert review is in progress. Found a mistake? [Tell us](https://pkizerotohero.com/contact/?topic=content-error&page=/handbook/tls-handshake/).

Last reviewed 20 October 2026Edition 0.9

## Keep going

Crash course: [Lesson 7: The handshake and its errors](https://pkizerotohero.com/course/handshake-and-errors/)

### Error pages for this chapter

- [NET::ERR\_CERT\_COMMON\_NAME\_INVALID](https://pkizerotohero.com/errors/chrome-err-cert-common-name-invalid/)
- [NET::ERR\_CERT\_AUTHORITY\_INVALID](https://pkizerotohero.com/errors/chrome-err-cert-authority-invalid/)
- [NET::ERR\_CERT\_DATE\_INVALID](https://pkizerotohero.com/errors/chrome-err-cert-date-invalid/)
- [SSL certificate problem: unable to get local issuer certificate](https://pkizerotohero.com/errors/curl-unable-to-get-local-issuer-certificate/)
- [The remote certificate is invalid according to the validation procedure](https://pkizerotohero.com/errors/dotnet-remote-certificate-invalid/)

### How-to guides

- [How to work behind a TLS-inspecting corporate proxy, safely](https://pkizerotohero.com/guides/corporate-proxy-certificates/)
- [How to install a certificate in nginx](https://pkizerotohero.com/guides/install-certificate-nginx/)
- [How to install a certificate in Apache httpd](https://pkizerotohero.com/guides/install-certificate-apache/)
- [How to install a certificate in IIS](https://pkizerotohero.com/guides/install-certificate-iis/)
- [How to install a certificate in Tomcat and other Java servers](https://pkizerotohero.com/guides/install-certificate-tomcat/)
- [How to configure TLS securely (versions, ciphers, HSTS)](https://pkizerotohero.com/guides/tls-configuration/)
- [How to test post-quantum hybrid TLS](https://pkizerotohero.com/guides/post-quantum-tls-test/)

### Tools

- [Paste your error](https://pkizerotohero.com/tools/error-finder/): Paste a certificate error from any tool and get the page that explains it.
- [Chain checker](https://pkizerotohero.com/tools/chain/): Paste a chain and see which link is missing, expired or out of order.
- [Hostname matcher](https://pkizerotohero.com/tools/hostname/): Test whether a name matches a certificate's SANs and wildcards.

### Topics

- [TLS and HTTPS](https://pkizerotohero.com/topics/tls/)
