← Week 3: mTLS and TLS Extensions

Day 16: Record Layer — Encryption, Nonces, and Padding

Phase 1 · May 29, 2026

← Week 3: mTLS and TLS Extensions

Agenda (2–3 hours)

  • Read (60 min): RFC 8446 §5 (Record Protocol) — read fully
  • Study (45 min): TLSCiphertext structure, nonce construction, content type hiding
  • Practice (45 min): Parse record headers; verify nonce construction
  • Challenge (30 min): Rust record header parser
← Week 3: mTLS and TLS Extensions

TLSCiphertext On the Wire

 0                   1                   2
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  0x17 (23)  |  0x03  |  0x03  |    length     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                 encrypted_record              |
|     (AEAD ciphertext + tag, length bytes)     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

opaque_type is always 0x17 (application_data) for encrypted records.
The real ContentType is encrypted as the last byte of the plaintext before AEAD.

← Week 3: mTLS and TLS Extensions

What's Actually Being Encrypted

TLSInnerPlaintext {
    content: opaque[length_of_content]  // the actual payload
    type:    ContentType                 // 1 byte: real content type
    zeros:   uint8[0..]                  // optional padding (all zeros)
}

The AEAD encrypts the entire TLSInnerPlaintext.
Associated data (authenticated but not encrypted) = the 5-byte record header.

This hides whether a record carries handshake data, application data, or an alert.

← Week 3: mTLS and TLS Extensions

Per-Record Nonce Construction

write_iv    = HKDF-Expand-Label(secret, "iv", "", iv_length)  // 12 bytes
seq_num     = current sequence number (64-bit, increments per record)
padded_seq  = seq_num left-padded with zeros to iv_length (12 bytes)
nonce       = write_iv XOR padded_seq

Properties:

  • Sequence number starts at 0 for each new traffic key
  • XOR with fixed IV makes nonce unique across records
  • Side effects of nonce reuse = AEAD catastrophic failure (attacker can recover key stream)
  • TLS mandates key update before 2^24.5 records to prevent nonce exhaustion
← Week 3: mTLS and TLS Extensions

Practice Exercise

# Capture a TLS session and dump raw records
tshark -r /tmp/phase1_day14.pcapng -Y "tls" \
  -T fields -e tls.record.opaque_type -e tls.record.length \
  -e tls.record.content_type 2>/dev/null

# Show raw bytes of first few records (use the pcap from Day 14)
tshark -r /tmp/phase1_day14.pcapng -x -Y "tls" | head -40

Identify: which records have opaque_type=23 (encrypted) vs opaque_type=22 (plaintext handshake).

← Week 3: mTLS and TLS Extensions

Challenge Assignment

Write a Rust function that parses the TLS record layer header from raw bytes:

#[derive(Debug)]
struct TlsRecordHeader {
    content_type: u8,
    version: u16,
    length: u16,
}

fn parse_record_header(bytes: &[u8]) -> Option<(TlsRecordHeader, &[u8])>

Then: given the write_iv and sequence number from an RFC 8448 test vector,
compute the per-record nonce and verify it matches the expected value in the RFC.

← Week 3: mTLS and TLS Extensions

Resources

  • RFC 8446 §5.1: Record Layer (TLSPlaintext)
  • RFC 8446 §5.2: Record Payload Protection (TLSCiphertext)
  • RFC 8446 §5.3: Per-Record Nonce
  • RFC 8448 §3: test vectors include write_iv and nonce values