Skip to main content

Module header_protection

Module header_protection 

Source
Available on crate feature std only.
Expand description

Header protection (QUIC RFC 9001 §5.4) — the per-packet mask that hides the header fields from a passive on-path observer. Since WIRE v6 the WHOLE 15-byte header is masked — version ‖ packet_number ‖ flags ‖ stream_id ‖ epoch ‖ path_id, the bytes at wire offset [0..15] (HP_MASK_LEN = 15; no constant cleartext byte). (Was [1..15]/14 bytes in WIRE v5, [33..47] in v4 — see the HP_MASK_LEN doc for the per-version history.)

The mask is cipher(hp_key, sample) where sample is the first 16 bytes of the packet’s AEAD ciphertext (the tag is always present, so a sample exists even for an empty payload). The sample is drawn from the payload ciphertext, which is never masked — so there is no circular dependency (cleaner than QUIC, whose packet number sits inside the sampled span). The AEAD AAD remains the cleartext header image, so HP is an orthogonal outer wrapping: a wire mutation of the masked region unmasks to a wrong header → wrong AAD → AEAD fails. It adds no new decryption oracle.

§Why the HP key is session-stable (NOT epoch-rotated)

QUIC RFC 9001 §6.1 keeps the header-protection key constant across key updates, and so do we. epoch lives inside the masked region, so the receiver must remove header protection before it knows the packet’s epoch. If the hp key rotated per epoch, a receiver one epoch behind the sender (the exact case Session::decrypt_packet_accepting_rekey exists to handle) could not pick the right hp key → garbage epoch → the catch-up path can’t read header.epoch → rekey-catchup deadlock. Deriving the hp keys once from the initial session secret and holding them stable avoids that. Forward secrecy of confidentiality is unaffected: the hp key masks only header metadata, never payload — the AEAD keys still ratchet with full FS.

§FIPS

Header protection is anti-DPI obfuscation, not confidentiality (the same posture as the mimicry transport’s outer record layer — see Security Invariant #3). Under --features fips the AES mask still routes through the FIPS substrate (aws_lc_rs::cipher AES-256-ECB) so the masking primitive stays inside the validated module; the ChaCha20 mask is unreachable there because the cipher suite is pinned to AES-256-GCM at session construction.

Structs§

HeaderProtector
Per-direction, session-stable header-protection keys plus the negotiated cipher suite (which selects AES-256-ECB vs ChaCha20 for the mask). Derived once from the initial session secret and held for the session’s lifetime; see the module docs for why this does not rotate with the AEAD epoch.

Constants§

HP_MASK_LEN
Bytes of the packet header protected by HP — WIRE v6 (anti-fingerprint): the contiguous region at wire offset [0..15] — the WHOLE 15-byte header, version(1) ‖ packet_number(8) ‖ flags(2) ‖ stream_id(2) ‖ epoch(1) ‖ path_id(1). The version byte is now masked too (no constant cleartext byte); the inner session_id is off-wire and routing is by the outer (rotating) ConnId. (Was [1..15]/14 in v5, [33..47] in v4.) The mask itself is a full 16-byte block; the first HP_MASK_LEN bytes are applied.
HP_SAMPLE_LEN
Bytes of AEAD ciphertext sampled to seed the mask cipher — one AES block (RFC 9001 §5.4.2). Always available: every data-plane packet carries at least the 16-byte AEAD tag.