flare_mlake · API · quic

flare.quic.protection

flare.quic.protection -- QUIC v1 per-packet header + AEAD protection (RFC 9001 §5.3-5.4).

The :func:unprotect_initial_packet and :func:protect_initial_packet entry points compose the three primitives already in :mod:flare.quic.crypto:

  • :func:flare.quic.crypto.derive_initial_secrets (RFC 9001 §5.2)
  • :func:flare.quic.crypto.derive_packet_keys (RFC 9001 §5.1)
  • :class:flare.quic.crypto.OpenSslQuicCrypto (AEAD + HP mask)

into the full inbound / outbound packet pipeline that the QUIC server reactor wires per datagram:

  • unprotect (inbound): strip header protection, reconstruct the truncated packet number per RFC 9000 §A.3, decrypt the AEAD payload with the unprotected header as associated data, return the frame bytes.
  • protect (outbound): encrypt the frame bytes with the unprotected header as AAD, apply header protection to the packet-number bytes + the low header bits, return the wire bytes ready for sendto.

The post-Initial encryption levels extend the Initial pair:

  • :func:unprotect_handshake_packet / :func:protect_handshake_packet -- long-header packets at the Handshake encryption level (RFC 9000 §17.2.4). Same AEAD + HP shape as Initial but no token field in the long-header extras, and the per-direction secret is supplied by the caller (the rustls QUIC bridge surfaces it through the session slab).
  • :func:unprotect_1rtt_packet / :func:protect_1rtt_packet -- short-header packets at the 1-RTT encryption level (RFC 9000 §17.3). Short-header parsing instead of long-header; key_phase is exposed for key-update tracking but applying a new key is outside this module's scope.

References:

  • RFC 9001 §5.3 "AEAD Usage".
  • RFC 9001 §5.4 "Header Protection".
  • RFC 9000 §17.2 "Long Header Packets" + §17.3 "Short Header Packets".
  • RFC 9000 §A.3 "Sample Packet Number Decoding Algorithm".
  • aioquic aioquic.quic.crypto.CryptoPair / packet_protection.

Structs

UnprotectedPacket

struct UnprotectedPacket

The output of :func:unprotect_initial_packet.

header is the unprotected packet header (first byte plus the rest of the long header fields plus the packet number), payload is the AEAD-decrypted frame bytes, and packet_number is the reconstructed full packet number per RFC 9000 §A.3. pn_length is the on-wire packet-number encoding length in bytes (1..4) so the caller can advance cursors without redoing the parse.

Implements Copyable, Movable

Fields
header: List[UInt8]
payload: List[UInt8]
packet_number: UInt64
pn_length: Int

Functions

decode_packet_number

def decode_packet_number(truncated_pn: UInt64, pn_length: Int, largest_pn: UInt64) -> UInt64

RFC 9000 §A.3 "Sample Packet Number Decoding Algorithm".

Given the on-wire truncated packet number (pn_length low bytes) and the largest packet number the receiver has seen so far, reconstruct the full packet number. The algorithm picks the candidate that is closest to largest_pn + 1 -- both higher and lower windows are checked so out-of-order packets decode correctly.

unprotect_initial_packet

def unprotect_initial_packet(datagram: Span[UInt8], dcid: ConnectionId, is_server: Bool, largest_received_pn: UInt64, aead_choice: Int = Int(0)) -> UnprotectedPacket

Strip header protection + AEAD-decrypt an Initial packet.

dcid is the Destination Connection ID the client placed on its first-flight Initial; both endpoints derive the initial secrets from it (RFC 9001 §5.2). is_server picks the reader's secret: the server reads with the client's secret and writes with the server's secret. largest_received_pn is fed into the RFC 9000 §A.3 reconstruction so out-of-order packets decode correctly.

Raises if the packet header is malformed, if the AEAD tag fails, or if any of the cursors would overflow the datagram.

protect_initial_packet

def protect_initial_packet(unprotected_header_prefix: Span[UInt8], packet_number: UInt64, pn_length: Int, plaintext: Span[UInt8], dcid: ConnectionId, is_server: Bool, aead_choice: Int = Int(0)) -> List[UInt8]

Build a fully protected Initial packet ready for sendto.

unprotected_header_prefix is the long-header bytes from the first byte through the payload-length varint (i.e. the output of :func:flare.quic.packet.encode_long_header plus the encoded token + payload-length). The function appends:

  1. The packet-number bytes (truncated to pn_length).
  2. The AEAD ciphertext + 16-byte tag.

Then it applies header protection to the packet-number bytes and the low 4 bits of the first byte per RFC 9001 §5.4.

packet_number is reused as the AEAD nonce input (RFC 9001 §5.3). The caller is responsible for sequencing packet numbers per the RFC's monotonic-per-key rules.

unprotect_handshake_packet

def unprotect_handshake_packet(datagram: Span[UInt8], reader_secret: Span[UInt8], largest_received_pn: UInt64, aead_choice: Int = Int(0)) -> UnprotectedPacket

Strip header protection + AEAD-decrypt a Handshake packet.

Identical RFC 9001 §5.3-§5.4 shape as :func:unprotect_initial_packet, but:

  • The long-header extras carry only a payload-length varint (RFC 9000 §17.2.4 -- no token field, unlike Initial).
  • The per-direction secret comes from the TLS handshake (the rustls KeyChange the Q9-W bridge surfaces) rather than the DCID-derived Initial secret.

reader_secret is the 32-byte (HKDF-SHA-256) handshake traffic secret for the peer's direction: on a server reading a client Handshake packet this is the client_handshake_secret.

protect_handshake_packet

def protect_handshake_packet(unprotected_header_prefix: Span[UInt8], packet_number: UInt64, pn_length: Int, plaintext: Span[UInt8], writer_secret: Span[UInt8], aead_choice: Int = Int(0)) -> List[UInt8]

Build a fully protected Handshake packet ready for sendto.

unprotected_header_prefix is the long-header bytes from the first byte through the payload-length varint -- i.e. the output of :func:flare.quic.packet.encode_long_header plus the encoded payload-length (no token field for Handshake per RFC 9000 §17.2.4). The function appends the packet-number bytes, AEAD-encrypts the plaintext with the unprotected header as AAD, then applies header protection.

writer_secret is the local-side handshake traffic secret (server_handshake_secret on the server send path); the rustls KeyChange surfaces it through Q9-W's session slab.

unprotect_1rtt_packet

def unprotect_1rtt_packet(datagram: Span[UInt8], reader_secret: Span[UInt8], largest_received_pn: UInt64, dcid_length: Int, aead_choice: Int = Int(0)) -> UnprotectedPacket

Strip header protection + AEAD-decrypt a 1-RTT (short header) packet.

dcid_length is the connection's pinned local CID length (the short header does not encode the DCID length on the wire, per RFC 9000 §17.3). reader_secret is the peer's application traffic secret (client_application_traffic_secret on the server read path).

The HP sample is taken at the four-byte offset after the packet number per RFC 9001 §5.4.2; that requires the ciphertext to span at least pn_offset + 4 + 16 bytes. The first byte's bits 0-4 (reserved + key-phase + pn_length) are cleared by header protection.

protect_1rtt_packet

def protect_1rtt_packet(short_header_prefix: Span[UInt8], packet_number: UInt64, pn_length: Int, plaintext: Span[UInt8], writer_secret: Span[UInt8], aead_choice: Int = Int(0)) -> List[UInt8]

Build a fully protected 1-RTT packet ready for sendto.

short_header_prefix is the unprotected short-header bytes from the first byte through the DCID (i.e. the output of :func:flare.quic.packet.encode_short_header). The function appends the packet-number bytes, AEAD-encrypts the plaintext with the unprotected short header as AAD, then applies header protection.

Header protection masks five bits of the first byte per RFC 9001 §5.4.1 (reserved 4-3 + key-phase 2 + pn-length 1-0), versus the long header's four bits.