← Week 2: TLS 1.3 Handshake

Day 11: Certificate, CertificateVerify, and Finished

Phase 1 · May 24, 2026

← Week 2: TLS 1.3 Handshake

Agenda (2–3 hours)

  • Read (60 min): RFC 8446 §4.4.1–4.4.4 (Certificate, CertificateVerify, Finished)
  • Study (45 min): How these three messages complete server authentication
  • Practice (45 min): Decrypt TLS 1.3 session with SSLKEYLOGFILE + Wireshark
  • Challenge (30 min): Find and annotate the Finished HMAC
← Week 2: TLS 1.3 Handshake

The Authentication Handshake Messages

After ServerHello, the server sends (encrypted with server_hs_traffic_key):

EncryptedExtensions  → extensions that don't need to be in ServerHello
Certificate          → server's cert chain
CertificateVerify    → server's signature proving it holds the private key
Finished             → HMAC binding the handshake to the derived keys

These are the four messages that authenticate the server. Client sends
Certificate + CertificateVerify + Finished only if mTLS is requested.

← Week 2: TLS 1.3 Handshake

Certificate Message

struct {
    opaque certificate_request_context<0..2^8-1>; // empty in server cert
    CertificateEntry certificate_list<0..2^24-1>;
} Certificate;

struct {
    opaque cert_data<1..2^24-1>;  // DER-encoded X.509 cert
    Extension extensions<0..2^16-1>;
} CertificateEntry;

The certificate_list is ordered: end-entity cert first, then intermediates.
Root CA is typically omitted (client has it in trust store).

← Week 2: TLS 1.3 Handshake

CertificateVerify

struct {
    SignatureScheme algorithm;  // e.g., ecdsa_secp256r1_sha256
    opaque signature<0..2^16-1>;
} CertificateVerify;

What's being signed:

signature_input = 64 spaces (0x20)
               || "TLS 1.3, server CertificateVerify"
               || 0x00
               || Transcript-Hash(Handshake Context, Certificate)

This construction prevents cross-protocol attacks. The transcript hash
binds the signature to this specific handshake.

← Week 2: TLS 1.3 Handshake

Finished Message

struct {
    opaque verify_data[Hash.length];
} Finished;

verify_data = HMAC(
    key = HKDF-Expand-Label(BaseKey, "finished", "", Hash.length),
    msg = Transcript-Hash(Handshake Context, Certificate, CertificateVerify)
)

For the server Finished, BaseKey = server_handshake_traffic_secret.
After the client verifies the Finished, it sends its own Finished.
After both Finished messages, the handshake is complete and application traffic keys activate.

← Week 2: TLS 1.3 Handshake

Practice Exercise: Decrypt TLS with SSLKEYLOGFILE

# Chrome/Firefox support SSLKEYLOGFILE for key export
export SSLKEYLOGFILE=/tmp/tls_keys.log

# Or use openssl (it writes keys automatically in newer versions)
openssl s_client -connect example.com:443 -keylogfile /tmp/tls_keys.log &
echo | openssl s_client -connect example.com:443 -keylogfile /tmp/tls_keys.log

# In Wireshark: Edit → Preferences → Protocols → TLS → (Pre)-Master-Secret log
# Point to /tmp/tls_keys.log, then view decrypted handshake
← Week 2: TLS 1.3 Handshake

Challenge Assignment

Using SSLKEYLOGFILE + Wireshark (or tshark):

  1. Capture a TLS 1.3 session to any HTTPS server
  2. Decrypt it using the key log
  3. Locate the Finished message (handshake type 20)
  4. Find the verify_data field in the decrypted Finished
  5. Manually verify the Finished HMAC: extract server_handshake_traffic_secret
    from the key log, compute the HMAC yourself in Rust, confirm it matches

This proves you can trace the key schedule end-to-end against a real TLS session.

← Week 2: TLS 1.3 Handshake

Resources

  • RFC 8446 §4.4: Post-ServerHello handshake messages
  • RFC 8446 §4.4.4: Finished message definition
  • Wireshark TLS wiki: how to use SSLKEYLOGFILE
  • RFC 8448 §3 contains expected verify_data values for test vector validation