Authenticated Handshake
Authenticated Handshake
Platform: cryptohack | Category: Forensics | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-21 | Status: Solved Techniques: hkdf_expand_label, hmac_verify_data, server_finished_cross_validation, tls13_keylog_decryption, transcript_hash
Summary
Task: TLS 1.3 PCAP + SSLKEYLOG; reconstruct the client Finished verify_data. Solution: decrypt the capture with tshark+keylog, extract the 6 tls.handshake_raw bodies (no record headers) for the transcript, derive finished_key via HKDF-Expand-Label from CLIENT_HANDSHAKE_TRAFFIC_SECRET, then HMAC-SHA384 over the transcript hash; cross-validated against the captured server Finished.
Authenticated Handshake — CryptoHack
Description
This is the CryptoHack "Authenticated Handshake" challenge (TLS Part 1 series). TLS 1.3 authentication happens in three handshake messages: Certificate, CertificateVerify, and Finished. The client Finished verify_data is:
verify_data = HMAC(finished_key, Transcript-Hash(Handshake Context, Certificate*, CertificateVerify*))
- The HMAC uses the hash from the agreed cipher suite (here SHA-384).
- finished_key is derived from the CLIENT_HANDSHAKE_TRAFFIC_SECRET (from keylogfile.txt) via HKDF-Expand-Label with label "finished".
- Transcript-Hash = hash of all handshake messages concatenated WITHOUT record-layer headers.
The provided pcap captures the handshake up to (but not including) the client's encrypted Finished. The goal: fill in the blanks in client_finished.py and submit the computed verify_data hex.
English summary: Given a TLS 1.3 capture, an SSLKEYLOG file, and a skeleton
client_finished.py with ? blanks, recompute the client Finished verify_data
and submit it as the answer. There is no FLAG{...} format — the hex string is
the flag.
Analysis
TLS 1.3 vs TLS 1.2 Finished
In TLS 1.3 the Finished message authenticates the entire handshake transcript
including Certificate and CertificateVerify (unlike TLS 1.2 where these come
later). The client Finished verify_data is:
finished_key = HKDF-Expand-Label(client_handshake_traffic_secret, "finished", "", Hash.length)
verify_data = HMAC(finished_key, Transcript-Hash(ClientHello … server Finished))
The agreed cipher suite uses SHA-384, so Hash.length = 48.
Everything after ServerHello is encrypted
A naive tshark -Y "tls.handshake" shows only ClientHello (frame 8) and
ServerHello (frame 10) — the EncryptedExtensions, Certificate, CertificateVerify
and server Finished are encrypted under the handshake traffic keys. They become
readable only after feeding tshark the SSLKEYLOG file.
The transcript needs handshake-layer bytes, not record-layer bytes
Transcript-Hash is computed over handshake messages = type(1) ‖ length(3) ‖ body,
without the 5-byte TLS record header. Wireshark exposes exactly these bytes in
the JSON field tls.handshake_raw, which made extraction clean.
Solution
1. Decrypt the capture with the keylog file
bash read-only • copy allowedtshark -r no-finished-tls3_*.pcapng \ -o "tls.keylog_file:keylogfile_*.txt" \ -Y "tls.handshake" -T fields -e frame.number -e tls.handshake.type
After decryption, frame 10 reveals handshake types 2,8,11,15,20
(ServerHello, EncryptedExtensions, Certificate, CertificateVerify, Finished).
2. Dump decrypted JSON with raw bytes and extract the 6 messages
bash read-only • copy allowedtshark -r no-finished-tls3_*.pcapng \ -o "tls.keylog_file:keylogfile_*.txt" \ -Y "tls.handshake" -T json -x > frames.json
There are exactly 6 tls.handshake_raw blocks, in order:
| # | Message | Type | Size |
|---|---|---|---|
| 1 | ClientHello | 1 | 512 B |
| 2 | ServerHello | 2 | 122 B |
| 3 | EncryptedExtensions | 8 | 19 B |
| 4 | Certificate | 11 | 4037 B |
| 5 | CertificateVerify | 15 | 264 B |
| 6 | (server) Finished | 20 | 52 B |
extract.py pulls them with a regex
"tls\.handshake_raw":\s*\[\s*"([0-9a-fA-F]+)" and writes messages.json keyed
by the variable names used in the skeleton.
3. Take the client handshake traffic secret from the keylog
Line 4 of keylogfile.txt:
CLIENT_HANDSHAKE_TRAFFIC_SECRET ... ca8fb94500b13314d4c47158b1e9c7e5d3374cf9c5703b6d8ab879e99af1529d0e013b84ae1e7b15233ff64a1ed6e06c
4. Derive finished_key and compute verify_data
finished_key = HKDF-Expand-Label(secret, "finished", "", 48, SHA-384), then
HMAC-SHA384 over SHA384(transcript).
python read-only • copy allowedimport hmac, hashlib, struct, json from math import ceil HASH_ALG = hashlib.sha384 HASH_LEN = HASH_ALG().digest_size # 48 def tls_HMAC(k, b, alg): return bytearray(hmac.new(k, b, alg).digest()) def HKDF_expand(prk, info, length, alg): hash_len = alg().digest_size t = bytearray(); okm = bytearray() for i in range(1, ceil(length / hash_len) + 2): t = tls_HMAC(prk, t + info + bytearray([i]), alg) okm += t return okm[:length] def HKDF_expand_label(secret, label, hashValue, length, alg): hkdfLabel = struct.pack('>H', length) seq = bytearray(b"tls13 ") + label hkdfLabel += bytes([len(seq)]) + seq # opaque label<7..255> hkdfLabel += bytes([len(hashValue)]) + hashValue # opaque context<0..255> return HKDF_expand(secret, hkdfLabel, length, alg) def verify_data(finished_key, transcript, alg): return tls_HMAC(finished_key, alg(transcript).digest(), alg) msgs = json.load(open("messages.json")) client_hello = bytes.fromhex(msgs["client_hello"]) server_hello = bytes.fromhex(msgs["server_hello"]) server_encrypted_extensions = bytes.fromhex(msgs["server_encrypted_extensions"]) server_certificate_message = bytes.fromhex(msgs["server_certificate_message"]) server_certificateverify_message = bytes.fromhex(msgs["server_certificateverify_message"]) server_finished = bytes.fromhex(msgs["server_finished"]) client_handshake_traffic_secret = bytes.fromhex( "ca8fb94500b13314d4c47158b1e9c7e5d3374cf9c5703b6d8ab879e99af1529d0e013b84ae1e7b15233ff64a1ed6e06c") finished_key = HKDF_expand_label( client_handshake_traffic_secret, b"finished", b"", HASH_LEN, HASH_ALG) transcript = (client_hello + server_hello + server_encrypted_extensions + server_certificate_message + server_certificateverify_message + server_finished) print(verify_data(finished_key, transcript, HASH_ALG).hex())
5. Cross-validate against the captured server Finished (key insight)
Before trusting the client answer, recompute the server Finished using
SERVER_HANDSHAKE_TRAFFIC_SECRET over the transcript ClientHello … CertificateVerify.
The result must equal the server Finished bytes captured in the pcap
(1400 0030 76f8f283…2a751689). It matched exactly, proving that the
HKDF-Expand-Label structure, the transcript concatenation, and the HMAC are all
correct — so the client computation (which we cannot observe in the pcap, since the
client Finished is not captured) is trustworthy.