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§
- Header
Protector - 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 innersession_idis 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 firstHP_MASK_LENbytes 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.