tiny.quic.crypto.packet
Defined in crypto.
API (11)
Actions
Public operations.
nonce: Returns the 12-byte AEAD nonce for one packet number under one initialization vector, as RFC 9001 section 5.3 constructs it, so a caller sealing or opening a packet by hand computes the same nonce the peer will compute.openopenPayload: Decrypts the payload of a packet whose headerunprotecthas already parsed, and leaves the plaintext in the packet where the ciphertext was, so a receiver decrypts the payload in place.sealunprotect
Types and contracts
Public types and contracts.
Values and defaults
Public values and defaults.
packet_bytes_max: The largest QUIC packet in bytes, which RFC 9000 section 18.2 sets as the largest UDP payload at 65,527, so a caller sizing a datagram buffer takes the largest packet this code will seal or open.tag_bytes
Source
Source: lib/quic/src/crypto/packet.zig
zig
const std = @import("std");const quic = @import("../root.zig");const crypto = quic.crypto;const Aes128Gcm = std.crypto.aead.aes_gcm.Aes128Gcm;const ChaCha20Poly1305 = std.crypto.aead.chacha_poly.ChaCha20Poly1305;pub const tag_bytes: usize = 16;/// The largest QUIC packet in bytes, which RFC 9000 section 18.2 sets as the largest UDP payload at/// 65,527, so a caller sizing a datagram buffer takes the largest packet this code will seal or/// open. Sealing or opening past it gives `PacketTooLarge`.pub const packet_bytes_max: usize = 65_527;const SealFailure = error{ ConfidentialityLimitReached, InvalidHeader, InvalidLength, PacketCounterOverflow, PacketTooLarge, PacketTooShort,};const OpenFailure = error{ AuthenticationFailed, IntegrityLimitReached, InvalidHeader, InvalidLength, InvalidPacketNumber, PacketTooLarge, PacketTooShort, ReservedBits, ScratchTooShort,};pub const SealError = crypto.header.ApplyError || SealFailure;pub const OpenError = quic.packet.DecodeError || crypto.header.ApplyError || OpenFailure;pub const Header = struct { first_byte: u8, pn_len: u3, pn_offset: usize, packet_end: usize, packet_number: u62, key_phase: ?bool,};pub const Opened = struct { packet_number: u62, payload: []u8, key_phase: ?bool,};/// Returns the 12-byte AEAD nonce for one packet number under one initialization vector, as RFC/// 9001 section 5.3 constructs it, so a caller sealing or opening a packet by hand computes the/// same nonce the peer will compute. The packet number is written big-endian into the low eight/// bytes of a zero field the width of the vector, and the two are exclusive-ored. Distinct packet/// numbers give distinct nonces under one vector, so a key stays usable across packets.pub fn nonce(iv: [crypto.iv_bytes]u8, packet_number: u62) [crypto.iv_bytes]u8 { var result = iv; var encoded: [8]u8 = undefined; std.mem.writeInt(u64, &encoded, packet_number, .big); for (0..8) |index| result[4 + index] ^= encoded[index]; return result;}fn longEnd(packet: []const u8, pn_offset: usize) OpenError!usize { const decoded = try quic.packet.decodeLong(packet); const Metadata = struct { offset: usize, length: u62 }; const metadata: Metadata = switch (decoded) { .initial => |value| .{ .offset = value.packet_number_offset, .length = value.length }, .zero_rtt, .handshake => |value| .{ .offset = value.packet_number_offset, .length = value.length, }, .retry, .version_negotiation => return error.InvalidHeader, }; if (metadata.offset != pn_offset) return error.InvalidHeader; const length = std.math.cast(usize, metadata.length) orelse return error.InvalidLength; if (pn_offset > packet.len) return error.InvalidLength; if (length > packet.len - pn_offset) return error.InvalidLength; return pn_offset + length;}fn protectedEnd(packet: []const u8, pn_offset: usize) OpenError!usize { if (packet.len == 0) return error.PacketTooShort; if (pn_offset == 0) return error.InvalidHeader; if (packet[0] & 0x80 != 0) { const packet_end = try longEnd(packet, pn_offset); if (packet_end > packet_bytes_max) return error.PacketTooLarge; return packet_end; } if (packet[0] & 0x40 == 0) return error.InvalidHeader; if (pn_offset > packet.len) return error.InvalidHeader; if (packet.len > packet_bytes_max) return error.PacketTooLarge; return packet.len;}fn sampleAt(packet: []const u8, pn_offset: usize, end: usize) OpenError![16]u8 { const sample_offset = std.math.add(usize, pn_offset, 4) catch return error.PacketTooShort; const sample_end = std.math.add(usize, sample_offset, 16) catch return error.PacketTooShort; if (sample_end > end) return error.PacketTooShort; return packet[sample_offset..][0..16].*;}fn encrypt( keys: *const crypto.Keys, payload: []u8, aad: []const u8, packet_nonce: [12]u8,) [tag_bytes]u8 { var tag: [tag_bytes]u8 = undefined; const packet_key = keys.packetKey(); switch (keys.selectedSuite()) { .aes_128_gcm_sha256 => Aes128Gcm.encrypt( payload, &tag, payload, aad, packet_nonce, packet_key[0..16].*, ), .chacha20_poly1305_sha256 => ChaCha20Poly1305.encrypt( payload, &tag, payload, aad, packet_nonce, packet_key, ), } return tag;}pub fn seal( keys: *crypto.Keys, packet_number: u62, packet: []u8, pn_offset: usize, pn_len: u3, payload_len: u16,) SealError!void { if (keys.confidentialityExhausted()) return error.ConfidentialityLimitReached; if (keys.sealed_packets == std.math.maxInt(u64)) return error.PacketCounterOverflow; if (packet.len == 0) return error.PacketTooShort; if (pn_len == 0 or pn_len > 4) return error.InvalidLength; if (quic.packet.packetNumberLength(packet[0]) != pn_len) return error.InvalidHeader; const header_len = std.math.add(usize, pn_offset, pn_len) catch return error.InvalidLength; const payload_bytes: usize = payload_len; const payload_end = std.math.add(usize, header_len, payload_bytes) catch return error.InvalidLength; const packet_end = std.math.add(usize, payload_end, tag_bytes) catch return error.InvalidLength; if (packet_end > packet.len) return error.PacketTooShort; if (packet_end > packet_bytes_max) return error.PacketTooLarge; if (packet[0] & 0x80 != 0) { const encoded_end = longEnd(packet, pn_offset) catch return error.InvalidHeader; if (encoded_end != packet_end) return error.InvalidLength; } else if (packet[0] & 0x40 == 0) return error.InvalidHeader; std.debug.assert(packetNumberMatches(packet[pn_offset..header_len], packet_number)); const sample_offset = std.math.add(usize, pn_offset, 4) catch return error.PacketTooShort; const sample_end = std.math.add(usize, sample_offset, 16) catch return error.PacketTooShort; if (sample_end > packet_end) return error.PacketTooShort; const payload = packet[header_len..payload_end]; const packet_nonce = nonce(keys.initializationVector(), packet_number); const tag = encrypt(keys, payload, packet[0..header_len], packet_nonce); packet[payload_end..][0..tag_bytes].* = tag; std.debug.assert(sample_end <= packet_end); const protected_sample = packet[sample_offset..][0..16].*; const protection_mask = crypto.header.mask(keys, &protected_sample); try crypto.header.protect(protection_mask, packet[0..packet_end], pn_offset, pn_len); keys.sealed_packets += 1;}fn packetNumberMatches(bytes: []const u8, packet_number: u62) bool { std.debug.assert(bytes.len >= 1); std.debug.assert(bytes.len <= 4); var encoded: [8]u8 = undefined; std.mem.writeInt(u64, &encoded, packet_number, .big); return std.mem.eql(u8, bytes, encoded[encoded.len - bytes.len ..]);}test "RFC 9001 section 5.3 seal packet number assertion contract" { try std.testing.expect(packetNumberMatches(&.{0x34}, 0x1234)); try std.testing.expect(packetNumberMatches(&.{ 0x12, 0x34 }, 0x1234)); try std.testing.expect(!packetNumberMatches(&.{0x35}, 0x1234));}fn readProtectedPacketNumber( bytes: []const u8, protection_mask: [crypto.header.mask_bytes]u8,) u32 { std.debug.assert(bytes.len >= 1); std.debug.assert(bytes.len <= 4); var result: u32 = 0; for (0..4) |index| { if (index >= bytes.len) break; result = (result << 8) | (bytes[index] ^ protection_mask[index + 1]); } return result;}fn decrypt( keys: *const crypto.Keys, plaintext: []u8, ciphertext: []const u8, tag: [tag_bytes]u8, aad: []const u8, packet_nonce: [12]u8,) error{AuthenticationFailed}!void { std.debug.assert(plaintext.len == ciphertext.len); const packet_key = keys.packetKey(); switch (keys.selectedSuite()) { .aes_128_gcm_sha256 => try Aes128Gcm.decrypt( plaintext, ciphertext, tag, aad, packet_nonce, packet_key[0..16].*, ), .chacha20_poly1305_sha256 => try ChaCha20Poly1305.decrypt( plaintext, ciphertext, tag, aad, packet_nonce, packet_key, ), }}fn keyPhase(first_byte: u8) ?bool { if (first_byte & 0x80 != 0) return null; return first_byte & 0x04 != 0;}pub fn unprotect( keys: *const crypto.Keys, packet: []u8, pn_offset: usize, largest_acked: ?u62,) OpenError!Header { const packet_end = try protectedEnd(packet, pn_offset); const sample = try sampleAt(packet, pn_offset, packet_end); const protection_mask = crypto.header.mask(keys, &sample); const inspected = try crypto.header.inspect(protection_mask, packet[0..packet_end], pn_offset); const header_len = pn_offset + inspected.pn_len; if (packet_end - header_len < tag_bytes) return error.InvalidLength; if (largest_acked == std.math.maxInt(u62)) return error.InvalidPacketNumber; const truncated = readProtectedPacketNumber( packet[pn_offset..header_len], protection_mask, ); const bits: u6 = @as(u6, inspected.pn_len) * 8; const packet_number = quic.packet.Number.expand(truncated, bits, largest_acked); const pn_len = try crypto.header.unprotect( protection_mask, packet[0..packet_end], pn_offset, ); std.debug.assert(pn_len == inspected.pn_len); return .{ .first_byte = inspected.first_byte, .pn_len = pn_len, .pn_offset = pn_offset, .packet_end = packet_end, .packet_number = packet_number, .key_phase = keyPhase(inspected.first_byte), };}const PayloadLayout = struct { header_len: usize, tag_offset: usize,};fn payloadLayout(packet: []const u8, parsed: Header) OpenError!PayloadLayout { if (packet.len == 0) return error.PacketTooShort; if (parsed.pn_len == 0 or parsed.pn_len > 4) return error.InvalidLength; if (parsed.pn_offset == 0) return error.InvalidHeader; if (parsed.pn_offset > parsed.packet_end) return error.InvalidHeader; if (parsed.pn_len > parsed.packet_end - parsed.pn_offset) return error.InvalidLength; const packet_end = try protectedEnd(packet, parsed.pn_offset); if (packet_end != parsed.packet_end) return error.InvalidHeader; const header_len = parsed.pn_offset + parsed.pn_len; if (parsed.packet_end - header_len < tag_bytes) return error.InvalidLength; if (packet[0] != parsed.first_byte) return error.InvalidHeader; if (!packetNumberMatches(packet[parsed.pn_offset..header_len], parsed.packet_number)) { return error.InvalidPacketNumber; } if (keyPhase(parsed.first_byte) != parsed.key_phase) return error.InvalidHeader; return .{ .header_len = header_len, .tag_offset = parsed.packet_end - tag_bytes };}fn slicesOverlap(first: []const u8, second: []const u8) bool { if (first.len == 0 or second.len == 0) return false; const first_address = @intFromPtr(first.ptr); const second_address = @intFromPtr(second.ptr); if (first_address <= second_address) { return second_address - first_address < first.len; } return first_address - second_address < second.len;}fn reservedBitsSet(first_byte: u8) bool { const reserved_mask: u8 = if (first_byte & 0x80 != 0) 0x0c else 0x18; return first_byte & reserved_mask != 0;}fn openPayloadInner( keys: *crypto.Keys, packet: []u8, parsed: Header, scratch: []u8,) OpenError!Opened { const layout = try payloadLayout(packet, parsed); const ciphertext = packet[layout.header_len..layout.tag_offset]; if (scratch.len < ciphertext.len) return error.ScratchTooShort; const plaintext = scratch[0..ciphertext.len]; std.debug.assert(!slicesOverlap(packet[0..parsed.packet_end], plaintext)); const tag: [tag_bytes]u8 = packet[layout.tag_offset..][0..tag_bytes].*; try decrypt( keys, plaintext, ciphertext, tag, packet[0..layout.header_len], nonce(keys.initializationVector(), parsed.packet_number), ); if (reservedBitsSet(parsed.first_byte)) return error.ReservedBits; @memcpy(packet[layout.header_len..layout.tag_offset], plaintext); return .{ .packet_number = parsed.packet_number, .payload = packet[layout.header_len..layout.tag_offset], .key_phase = parsed.key_phase, };}/// Decrypts the payload of a packet whose header `unprotect` has already parsed, and leaves the/// plaintext in the packet where the ciphertext was, so a receiver decrypts the payload in place./// The caller supplies scratch bytes for the plaintext, and a scratch slice shorter than the/// ciphertext gives `ScratchTooShort`. The ciphertext runs from the end of the packet number to the/// authentication tag, so scratch needs the packet end less the packet number offset, the packet/// number length, and the tag bytes. Scratch must lie outside the packet bytes, and only debug/// builds check that. A failed authentication counts against the key's integrity limit, and a key/// that has reached its limit gives `IntegrityLimitReached` before any decryption.pub fn openPayload( keys: *crypto.Keys, packet: []u8, parsed: Header, scratch: []u8,) OpenError!Opened { if (keys.exhausted()) return error.IntegrityLimitReached; return openPayloadInner(keys, packet, parsed, scratch) catch |failure| { if (failure != error.AuthenticationFailed) return failure; std.debug.assert(keys.failed_opens < keys.integrityLimit()); keys.failed_opens += 1; return failure; };}fn restoreHeader(keys: *const crypto.Keys, packet: []u8, parsed: Header) void { const sample = sampleAt(packet, parsed.pn_offset, parsed.packet_end) catch unreachable; const protection_mask = crypto.header.mask(keys, &sample); crypto.header.protect( protection_mask, packet[0..parsed.packet_end], parsed.pn_offset, parsed.pn_len, ) catch unreachable;}pub fn open( keys: *crypto.Keys, packet: []u8, scratch: []u8, pn_offset: usize, largest_acked: ?u62,) OpenError!Opened { if (keys.exhausted()) return error.IntegrityLimitReached; const parsed = try unprotect(keys, packet, pn_offset, largest_acked); return openPayload(keys, packet, parsed, scratch) catch |failure| { restoreHeader(keys, packet, parsed); return failure; };}Source: lib/quic/src/crypto/root.zig:19
zig
pub const packet = @import("packet.zig");Complete caller list for crypto.packet.seal
15 direct callers.
lib.quic.src.crypto.test.test_RFC_9000_Appendix_A.3_packet_number_expansion_wrap_branches_and_maximum_guard[function] — test source atlib/quic/src/crypto/test.zig:604in nearest public ownerlib.quic.src.crypto.testlib.quic.src.crypto.test.test_RFC_9000_section_18.2_packet_maximum_plus_one_rejects_before_authentication[function] — test source atlib/quic/src/crypto/test.zig:673in nearest public ownerlib.quic.src.crypto.testlib.quic.src.crypto.test.test_RFC_9000_sections_17.2_and_17.3_reject_authenticated_reserved_bits[function] — test source atlib/quic/src/crypto/test.zig:642in nearest public ownerlib.quic.src.crypto.testlib.quic.src.crypto.test.test_RFC_9001_Appendix_A.2_client_Initial_packet_protection[function] — test source atlib/quic/src/crypto/test.zig:247in nearest public ownerlib.quic.src.crypto.testlib.quic.src.crypto.test.test_RFC_9001_Appendix_A.3_server_Initial_packet_protection[function] — test source atlib/quic/src/crypto/test.zig:268in nearest public ownerlib.quic.src.crypto.testlib.quic.src.crypto.test.test_RFC_9001_Appendix_A.5_ChaCha20-Poly1305_packet_and_key_update[function] — test source atlib/quic/src/crypto/test.zig:346in nearest public ownerlib.quic.src.crypto.testlib.quic.src.crypto.test.test_RFC_9001_section_6.1_key_update_preserves_header_protection[function] — test source atlib/quic/src/crypto/test.zig:514in nearest public ownerlib.quic.src.crypto.testlib.quic.src.crypto.test.test_RFC_9001_section_6.2_exposes_key_phase_before_payload_decryption[function] — test source atlib/quic/src/crypto/test.zig:459in nearest public ownerlib.quic.src.crypto.testlib.quic.src.crypto.test.test_RFC_9001_section_6.6_AEAD_usage_limits_include_maximum_plus_one[function] — test source atlib/quic/src/crypto/test.zig:548in nearest public ownerlib.quic.src.crypto.testlib.quic.src.crypto.test.test_RFC_9001_section_6.6_integrity_failures_survive_a_key_update[function] — test source atlib/quic/src/crypto/test.zig:405in nearest public ownerlib.quic.src.crypto.testlib.quic.src.crypto.test.test_RFC_9001_section_6.6_integrity_maximum_and_maximum_plus_one[function] — test source atlib/quic/src/crypto/test.zig:575in nearest public ownerlib.quic.src.crypto.testlib.quic.src.crypto.test.test_RFC_9001_sections_6.3_to_6.5_failed_open_preserves_packet_bytes[function] — test source atlib/quic/src/crypto/test.zig:433in nearest public ownerlib.quic.src.crypto.testlib.quic.src.properties.crypto.roundTrip[function] — private source atlib/quic/src/properties/crypto.zig:42in nearest public ownerlib.quic.src.properties.cryptolib.quic.src.properties.crypto.sealedPacket[function] — private source atlib/quic/src/properties/crypto.zig:102in nearest public ownerlib.quic.src.properties.cryptolib.quic.src.properties.protection.checkSealSample[function] — private source atlib/quic/src/properties/protection.zig:640in nearest public ownerlib.quic.src.properties.protection
Complete call list for crypto.packet.unprotect
7 direct calls.
tiny.quic.crypto.header.inspect[function] atlib/quic/src/crypto/header.zig:59tiny.quic.crypto.header.mask[function] atlib/quic/src/crypto/header.zig:22tiny.quic.crypto.header.unprotect[function] atlib/quic/src/crypto/header.zig:86lib.quic.src.crypto.packet.keyPhase[function] — private source atlib/quic/src/crypto/packet.zig:238in nearest public ownertiny.quic.crypto.packetlib.quic.src.crypto.packet.protectedEnd[function] — private source atlib/quic/src/crypto/packet.zig:85in nearest public ownertiny.quic.crypto.packetlib.quic.src.crypto.packet.readProtectedPacketNumber[function] — private source atlib/quic/src/crypto/packet.zig:194in nearest public ownertiny.quic.crypto.packetlib.quic.src.crypto.packet.sampleAt[function] — private source atlib/quic/src/crypto/packet.zig:99in nearest public ownertiny.quic.crypto.packet
Audit
| Definitions | 12 |
|---|---|
| Public names | 12 |
| Members | 9 |
| Version | 26.7.0 |
| Revision | daab053ee433 |