← Week 2: TLS 1.3 Handshake

Day 9: ServerHello, KeyShare, and HelloRetryRequest

Phase 1 · May 22, 2026

← Week 2: TLS 1.3 Handshake

Agenda (2–3 hours)

  • Read (60 min): RFC 8446 §4.1.3–4.1.4 (ServerHello, HelloRetryRequest)
  • Study (45 min): Key share negotiation, HRR flow, deriving handshake keys
  • Practice (30 min): Force a HelloRetryRequest
  • Challenge (30 min): Written trace of key schedule
← Week 2: TLS 1.3 Handshake

ServerHello Structure

struct {
    ProtocolVersion legacy_version = 0x0303;
    Random random;              // 32 bytes; last 8 bytes may be downgrade sentinel
    opaque legacy_session_id_echo<0..32>; // echo client's session_id (compat)
    CipherSuite cipher_suite;   // selected cipher
    uint8 legacy_compression_method = 0;
    Extension extensions<6..2^16-1>;
} ServerHello;

ServerHello extensions in TLS 1.3: supported_versions (0x0304), key_share,
optionally pre_shared_key.

← Week 2: TLS 1.3 Handshake

Key Share Negotiation

Client sends one or more key_share entries (public keys for named groups).
Server picks one and responds with its own public key.

Client key_share: [(x25519, pub_c_x25519), (secp256r1, pub_c_p256)]
Server key_share: [(x25519, pub_s_x25519)]

Shared secret = ECDH(priv_s_x25519, pub_c_x25519)
             == ECDH(priv_c_x25519, pub_s_x25519)

After this exchange, both sides compute the Handshake Secret via HKDF.

← Week 2: TLS 1.3 Handshake

HelloRetryRequest

If the server doesn't support any of the client's offered groups, it sends an HRR:

HelloRetryRequest: ServerHello with random = SHA-256("HelloRetryRequest")
  key_share extension: just the selected group name (no key yet)

Client must then send a new ClientHello with a key_share for the requested group.

The HRR triggers a cookie extension to prevent DoS (server is stateless until
the second ClientHello).

Downside: adds a full round trip. Servers should prefer common groups (x25519).

← Week 2: TLS 1.3 Handshake

After ServerHello: Handshake Key Derivation

DHE = ECDH(priv_server, pub_client)   // shared secret

Handshake Secret = HKDF-Extract(
    salt = Derive-Secret(Early Secret, "derived", ""),
    IKM  = DHE
)

client_hs_traffic_secret = Derive-Secret(HS, "c hs traffic", CH..SH)
server_hs_traffic_secret = Derive-Secret(HS, "s hs traffic", CH..SH)

Everything after ServerHello is encrypted with handshake traffic keys.

← Week 2: TLS 1.3 Handshake

Practice Exercise

# Force HelloRetryRequest by offering only a group the server likely won't pick
openssl s_client -connect google.com:443 -groups secp521r1 -msg 2>&1 | \
  grep -A2 "HelloRetryRequest\|ServerHello"

# Check if HRR occurred: look for two ClientHello messages in the trace
# HRR ServerHello has random = cf21ad74e59a6111be1d8c021e65b891...
← Week 2: TLS 1.3 Handshake

Challenge Assignment

Using the RFC 8448 §3 test vectors, trace through the key schedule from
DHE (the ECDH shared secret) to client_handshake_traffic_secret:

  1. Show each HKDF-Extract / HKDF-Expand-Label call with its inputs
  2. Show the transcript hash at the point of Derive-Secret(HS, "c hs traffic", ...)
  3. Verify your computed client_handshake_traffic_secret matches RFC 8448

You can use Rust or Python — use the hkdf / hmac crates you built on Day 4.

← Week 2: TLS 1.3 Handshake

Resources

  • RFC 8446 §4.1.3: ServerHello
  • RFC 8446 §4.1.4: HelloRetryRequest
  • RFC 8446 §7.1: Key Schedule
  • RFC 8448 §3: TLS 1.3 test vectors (1-RTT handshake with x25519)