Scapy TLS 1.2 Packet Crafter: Custom Handshake Generation
By Shay Mordechai | June 17, 2025
This project was built during extended reserve duty under field conditions. I needed to stay sharp technically, so I went deep into RFC 5246 — not as a user of TLS, but as someone building it from scratch at the bit and byte level.
The goal: generate a valid PCAP simulating a full TLS 1.2 handshake between a server and two clients. No OS SSL libraries. Everything assembled manually using Scapy and the cryptography library — 4,030 lines across 22 Python modules.
What I Actually Implemented
1. Client Hello & SNI
Generating the 32-byte client random securely and constructing extensions at the byte level, including SNI, supported groups, and signature algorithms.
session.client_random = os.urandom(32)
gmt_time = int.from_bytes(session.client_random[:4], 'big')
random_bytes = session.client_random[4:]
return TLSClientHello(
version=TLSVersion.TLS_1_2,
ciphers=[TLS_RSA_WITH_AES_128_CBC_SHA256],
ext=extensions.get_extension_list(),
gmt_unix_time=gmt_time,
random_bytes=random_bytes
)
2. Master Secret Derivation (PRF)
TLS 1.2 derives the master secret using a Pseudo-Random Function built on HMAC-SHA256. I implemented P_hash directly from the RFC — no shortcuts.
seed = b"master secret" + client_random + server_random master_secret = P_hash(pre_master_secret, seed, 48)
3. Key Block Generation
From the master secret, both sides derive MAC keys, encryption keys, and IVs. For TLS_RSA_WITH_AES_128_CBC_SHA256 that means 128 bytes of key material total.
seed = b"key expansion" + server_random + client_random key_material = P_hash(master_secret, seed, 128) # Extract in RFC 5246 order: # client_write_mac_key (32) + server_write_mac_key (32) # + client_write_key (16) + server_write_key (16) # + client_write_iv (16) + server_write_iv (16)
4. Application Data Encryption
AES-128-CBC with HMAC-SHA256. Had to implement the MAC-then-encrypt construction correctly: HMAC over sequence number + record header + plaintext, then PKCS#7 padding, then CBC encryption.
mac_data = (
seq_num.to_bytes(8, 'big') +
b'\x17' + # Application Data type
TLSVersion.TLS_1_2.value.to_bytes(2, 'big') +
len(plaintext).to_bytes(2, 'big') +
plaintext
)
mac = hmac.new(mac_key, mac_data, hashlib.sha256).digest()
The Hard Part: Wireshark Decryption
Getting Wireshark to fully decrypt the generated PCAP was the real debugging challenge. The dissector was rejecting Application Data due to MAC/padding mismatches. I implemented runtime SSLKEYLOGFILE generation — exporting CLIENT_RANDOM and master_secret — and used that to trace exactly where my binary layout diverged from the expected format. Fixed the padding construction and got full protocol compliance.
What This Taught Me
Understanding TLS as a user and understanding it as an implementer are very different things. The RFC looks clean on paper. The actual byte layout, the ordering of MAC computation, the padding edge cases — those only become clear when Wireshark tells you the MAC is wrong and you have to figure out why.
Full code: github.com/shay0129/scapy-tls-pcap-creator