Full notes: TLS 1.3 Handshake: Key Exchange, Authentication, and Certificate Chain of Trust

Key Concepts

What Changed from TLS 1.2 to TLS 1.3

TLS 1.3 (RFC 8446) is a complete redesign. It reduces the handshake from 2-RTT to 1-RTT, mandates forward secrecy (ephemeral keys always), removes RSA key transport, encrypts the server certificate (was plaintext in 1.2), reduces cipher suites from 37+ to 5 AEAD-only suites, and removes renegotiation, compression (CRIME attack), and static RSA. The removal of RSA key transport is critical — in TLS 1.2 with RSA, compromising the server’s private key allowed decrypting all past recorded sessions.

AspectTLS 1.2TLS 1.3
Round trips2-RTT1-RTT (0-RTT optional)
Key exchangeRSA or (EC)DHEECDHE only
Forward secrecyOptionalMandatory
CertificatePlaintextEncrypted
Cipher suites37+5 (AEAD only)

The Full 1-RTT Handshake Flow

Only ClientHello and ServerHello are plaintext. Everything after ServerHello is encrypted with handshake traffic keys.

ClientHello  ------->  (plaintext: random, ECDHE pub, cipher suites, SNI)
             <-------  ServerHello  (plaintext: random, ECDHE pub, selected suite)
  ~~~ compute shared secret, derive handshake keys ~~~
             <-------  {EncryptedExtensions}  (ALPN, etc.)
             <-------  {Certificate}          (X.509 chain)
             <-------  {CertificateVerify}    (signature over transcript)
             <-------  {Finished}             (HMAC over transcript)
Finished     ------->                         (HMAC over transcript)
  ~~~ derive application traffic keys ~~~
  <========= Application Data ==========>

ClientHello Wire-Level Details

Wrapped in a TLS Record (ContentType: 0x16, Version: 0x0301 for middlebox compat). The legacy_version is frozen at 0x0303 (TLS 1.2) — real version is negotiated via the supported_versions extension (0x0304 = TLS 1.3). Other critical extensions: key_share (client’s ephemeral ECDHE public key), supported_groups (x25519, secp256r1, etc.), signature_algorithms (for verifying CertificateVerify), server_name (SNI, plaintext), alpn (h2, http/1.1), psk_key_exchange_modes (for resumption).

ServerHello

Server selects a cipher suite, sends its ephemeral ECDHE public key in key_share, and confirms TLS 1.3 via supported_versions: 0x0304. After ServerHello, both sides have each other’s ECDHE public keys and independently compute the shared secret.

TLS 1.3 Cipher Suites

Only 5 suites, specifying AEAD + hash only (key exchange is always ECDHE via extensions):

CodeNameAEADHash
0x1301TLS_AES_128_GCM_SHA256AES-128-GCMSHA-256
0x1302TLS_AES_256_GCM_SHA384AES-256-GCMSHA-384
0x1303TLS_CHACHA20_POLY1305_SHA256ChaCha20-Poly1305SHA-256

Key Exchange: ECDHE (Not the Certificate)

The server’s certificate public key is NOT used to encrypt session keys or data. Session keys come entirely from an ephemeral Diffie-Hellman exchange. Both sides generate throwaway key pairs, exchange public halves in key_share, and compute identical shared secrets: x25519(privC, pubS) == x25519(privS, pubC). An eavesdropper seeing both public keys cannot compute the shared secret.

HKDF Key Schedule

The shared secret feeds into HKDF (RFC 5869) through a structured derivation: Early Secret (from PSK or 0) Handshake Secret (from ECDHE shared secret) Master Secret. At each step, the handshake transcript hash is mixed in, binding keys to the specific handshake. Three distinct key sets: handshake traffic keys (encrypt Certificate, CertificateVerify, Finished), application traffic keys (encrypt app data), and resumption keys (for NewSessionTicket).

The Certificate: Authentication, Not Encryption

The certificate contains an X.509 chain with: Subject (CN), Issuer, validity period, Subject Public Key (ECDSA or RSA), Subject Alt Names, Key Usage, and the issuer’s signature. The public key has ONE job: verifying the CertificateVerify signature. Common misconception: “client encrypts data with server’s public key” — this was true for TLS 1.2 RSA key transport but is completely wrong for TLS 1.3.

CertificateVerify: Proof of Private Key Possession

The server signs the handshake transcript hash with its certificate private key. The client verifies using the certificate’s public key. This proves: (1) the server possesses the matching private key, and (2) no handshake message was tampered with (because the transcript hash is signed).

Certificate Chain of Trust

The server sends a chain: Leaf cert (signed by intermediate CA) Intermediate cert (signed by root CA). Root CA is NOT sent — it’s pre-installed in the client’s trust store. Client validation checks: signature chain verification, hostname match (SAN/CN vs SNI), validity dates, key usage (digitalSignature), revocation (OCSP/CRL), basic constraints (CA:TRUE for intermediates, CA:FALSE for leaf).

PlatformTrust Store Location
macOSSystem Keychain
Linux (Debian)/etc/ssl/certs/ca-certificates.crt
FirefoxMozilla NSS (independent of OS)
Go programsOS trust store / crypto/x509.SystemCertPool()
Node.jsOpenSSL CA bundle / NODE_EXTRA_CA_CERTS

Finished Messages: Handshake Integrity

Both sides send HMAC over the handshake transcript using the finished key. Proves both derived the same handshake keys and no messages were tampered with. After both Finished messages, both sides derive application traffic keys. The server can send 0.5-RTT data (application data) immediately after its Finished, before receiving the client’s Finished.

Perfect Forward Secrecy (PFS)

Ephemeral ECDHE key pairs are generated fresh per session and discarded afterward. Compromising the server’s long-term certificate key lets an attacker impersonate the server in future connections, but CANNOT decrypt past sessions because past session keys were derived from ephemeral keys that no longer exist.

MITM Prevention

Without certificates, an attacker can substitute their own ECDHE key and decrypt/re-encrypt traffic. With certificates, the attacker cannot produce a valid CertificateVerify because they don’t have the server’s certificate private key. The CertificateVerify signs the handshake transcript including key_share values — substituting a key share changes the transcript hash, making the signature invalid.

0-RTT Resumption (PSK)

After a successful handshake, the server sends a NewSessionTicket with a PSK. On the next connection, the client sends ClientHello + key_share + PSK + early application data in the first message. Warning: 0-RTT data is NOT forward-secret (encrypted under PSK, not fresh ECDHE) and IS replayable. Only for idempotent operations (GET, not POST).

PKCE is NOT Part of TLS

PKCE (RFC 7636) is an OAuth 2.0 extension at the application layer. TLS operates at the transport layer. They have nothing in common despite both involving “keys” and “codes.”

Inspecting TLS 1.3 Handshakes

With openssl s_client -connect ... -tls1_3 -msg, curl -vvv, or Wireshark (filter: tls.handshake). In TLS 1.3, only ClientHello and ServerHello are visible in Wireshark; everything after appears as encrypted Application Data. Use SSLKEYLOGFILE to dump per-session keys for decryption.

Version Field Compatibility

Record layer version is 0x0301 (TLS 1.0), legacy_version is 0x0303 (TLS 1.2) — both frozen for middlebox compatibility. Real version is negotiated exclusively through supported_versions extension. legacy_session_id and ChangeCipherSpec are also retained for the same reason.

Quick Reference

ClientHello  ------->  (plaintext: ECDHE pub, suites, SNI, supported_versions)
             <-------  ServerHello  (plaintext: ECDHE pub, selected suite)
  ~~~ ECDHE shared secret -> HKDF -> handshake keys ~~~
             <-------  {EncryptedExtensions}  (encrypted)
             <-------  {Certificate}          (encrypted -- was plaintext in 1.2!)
             <-------  {CertificateVerify}    (encrypted)
             <-------  {Finished}             (encrypted)
Finished     ------->                         (encrypted)
  ~~~ HKDF -> application traffic keys ~~~
  <========= Application Data ==========>
Key Schedule StageDerives
Early Secret (PSK or 0)Early traffic keys (0-RTT)
Handshake Secret (ECDHE)Handshake traffic keys
Master SecretApplication traffic keys, resumption keys

Client validation: signature chain hostname match (SAN/CN) validity dates key usage revocation (OCSP/CRL) basic constraints

Version fields: Record = 0x0301 (TLS 1.0), legacy_version = 0x0303 (TLS 1.2), real = supported_versions: 0x0304 (TLS 1.3). All frozen for middlebox compat.

Key Takeaways

  • Session keys are from ECDHE, not the certificate. The certificate only authenticates (verifies CertificateVerify). This is the most commonly misunderstood aspect.
  • TLS 1.3 mandates forward secrecy — compromising the server’s long-term key cannot decrypt past traffic because ephemeral ECDHE keys are discarded per session.
  • Everything after ServerHello is encrypted (including the certificate), hiding server identity from passive eavesdroppers. TLS 1.2 sent certificates in plaintext.
  • SNI in ClientHello is still plaintext — an eavesdropper can see which hostname you’re connecting to. ECH (Encrypted Client Hello) addresses this.
  • 0-RTT is fast but replayable and not forward-secret. Only use for idempotent requests.
  • Version fields (0x0301, 0x0303) are frozen for middlebox compatibility. Real negotiation is via supported_versions extension.
  • CertificateVerify proves private key possession AND binds the signature to this specific handshake (prevents replay from other sessions).
  • HKDF key schedule mixes in transcript hashes at each step, so any tampered message causes key derivation to diverge and Finished verification to fail.