// QUIC 1-RTT (Application) packet protection (RFC 9000 §17.3 short header + RFC 9001 §5.3–§5.4).
// Once the handshake completes, every application packet uses a short header — no length
// field (the payload runs to the end of the datagram) and no connection-id lengths on the
// wire (the receiver knows its own) — protected with the application traffic keys. The
// AEAD sealing and header protection are shared with the Initial/Handshake paths; only the
// header layout and the receiver's need for the destination-CID length differ. This is the
// third and last packet-protection key space.
///|
/// Assemble a short (1-RTT) header (RFC 9000 §17.3): the first byte (form 0, fixed bit,
/// spin, two reserved zero bits, key phase, and the two-bit packet-number length minus
/// one), the destination connection id with no length prefix, and the packet number.
fn quic_short_header_bytes(
spin : Bool,
key_phase : Bool,
dcid : Bytes,
packet_number : Int64,
pn_length : Int,
) -> Bytes {
let first = 0x40 |
(if spin { 0x20 } else { 0 }) |
(if key_phase { 0x04 } else { 0 }) |
((pn_length - 1) & 0x03)
let h = Buffer()
h.write_byte(first.to_byte())
h.write_bytes(dcid[:])
for i = pn_length - 1; i >= 0; i = i - 1 {
h.write_byte(((packet_number >> (i * 8)) & 0xffL).to_byte())
}
h.to_bytes()
}
///|
/// Protect a QUIC 1-RTT packet: assemble the short header, AEAD-seal `payload` with the
/// unprotected header as associated data, then apply header protection (RFC 9001 §5.3–§5.4).
pub fn quic_protect_short(
spin : Bool,
key_phase : Bool,
dcid : Bytes,
packet_number : Int64,
pn_length : Int,
payload : Bytes,
key : Bytes,
iv : Bytes,
hp : Bytes,
) -> Bytes {
let header = quic_short_header_bytes(
spin, key_phase, dcid, packet_number, pn_length,
)
let pn_offset = header.length() - pn_length
let nonce = quic_nonce(iv, packet_number)
let box = aes128_gcm_seal(key, nonce, payload, header)
let packet = Buffer()
packet.write_bytes(header[:])
packet.write_bytes(box[:])
quic_header_protect(packet.to_bytes(), pn_offset, pn_length, hp)
}
///|
/// Remove protection from a received 1-RTT packet, given the length of the destination
/// connection id this endpoint issued: the packet-number field starts right after the
/// first byte and the connection id, and the payload runs to the end (RFC 9000 §17.3).
/// The protected long-header trailer strip (header protection over the packet number, AEAD
/// over the payload) is shared with the Initial/Handshake paths. `None` if it fails.
pub fn quic_unprotect_short(
packet : Bytes,
dcid_len : Int,
key : Bytes,
iv : Bytes,
hp : Bytes,
) -> (Bytes, Int64)? {
quic_unprotect_initial(packet, 1 + dcid_len, key, iv, hp)
}
///|
/// Build a protected 1-RTT packet carrying `frames`: encode them into a payload and
/// protect it with the application-space keys (RFC 9001 §5.3–§5.4).
pub fn quic_send_short(
spin : Bool,
key_phase : Bool,
dcid : Bytes,
packet_number : Int64,
pn_length : Int,
frames : Array[QuicFrame],
keys : QuicPacketKeys,
) -> Bytes {
quic_protect_short(
spin,
key_phase,
dcid,
packet_number,
pn_length,
quic_encode_payload(frames),
keys.key,
keys.iv,
keys.hp,
)
}
///|
/// Receive a protected 1-RTT `packet` with the application-space keys, given the length of
/// the destination connection id this endpoint issued: remove protection, parse the
/// payload, and return its frames and packet number. `None` if authentication fails.
pub fn quic_recv_short(
packet : Bytes,
dcid_len : Int,
keys : QuicPacketKeys,
) -> (Array[QuicFrame], Int64)? raise QuicPayloadError {
match quic_unprotect_short(packet, dcid_len, keys.key, keys.iv, keys.hp) {
Some((payload, pn)) => Some((quic_parse_payload(payload), pn))
None => None
}
}