lib/quic/src/crypto/retry.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const quic = @import("../root.zig");
3
4 const Aes128 = std.crypto.core.aes.Aes128;
5 const Ghash = std.crypto.onetimeauth.Ghash;
6
7 pub const tag_bytes: usize = Ghash.mac_length;
8 pub const connection_id_bytes_max: usize = 20;
9 pub const pseudo_bytes_max: usize = quic.crypto.packet.packet_bytes_max - tag_bytes;
10
11 /// The 16-byte AES-128 key RFC 9001 section 5.8 fixes for QUIC version 1 Retry integrity tags. A
12 /// caller checking a Retry integrity tag against another implementation needs the key that
13 /// implementation used. Every endpoint uses it, so the tag shows the sender saw the original
14 /// destination connection ID. It carries no proof of the sender's identity.
15 pub const v1_key = [_]u8{
16 0xbe, 0x0c, 0x69, 0x0b, 0x9f, 0x66, 0x57, 0x5a,
17 0x1d, 0x76, 0x6b, 0x54, 0xe3, 0x68, 0xc8, 0x4e,
18 };
19 /// The 12-byte nonce RFC 9001 section 5.8 fixes for QUIC version 1 Retry integrity tags. It pairs
20 /// with the key for the same check. The tag computation uses it with the fixed key over the
21 /// pseudo-packet.
22 pub const v1_nonce = [_]u8{
23 0x46, 0x15, 0x99, 0xd3, 0x5d, 0x63,
24 0x2b, 0xf2, 0x23, 0x98, 0x25, 0xbb,
25 };
26
27 pub const TagError = error{
28 ConnectionIdTooLong,
29 PacketTooLarge,
30 };
31
32 fn authenticate(prefix: []const u8, retry_pseudo: []const u8) [tag_bytes]u8 {
33 const aad_len = prefix.len + retry_pseudo.len;
34 const cipher = Aes128.initEnc(v1_key);
35 var hash_key: [Ghash.key_length]u8 = undefined;
36 cipher.encrypt(&hash_key, &@splat(0));
37 const block_count = @divCeil(aad_len, Ghash.block_length) + 1;
38 var mac = Ghash.initForBlockCount(&hash_key, block_count);
39 mac.update(prefix);
40 mac.update(retry_pseudo);
41 mac.pad();
42 var lengths: [Ghash.block_length]u8 = @splat(0);
43 std.mem.writeInt(u64, lengths[0..8], @as(u64, aad_len) * 8, .big);
44 mac.update(&lengths);
45 var result: [tag_bytes]u8 = undefined;
46 mac.final(&result);
47 var counter: [Aes128.block.block_length]u8 = @splat(0);
48 counter[0..v1_nonce.len].* = v1_nonce;
49 std.mem.writeInt(u32, counter[v1_nonce.len..][0..4], 1, .big);
50 var encrypted_counter: [Aes128.block.block_length]u8 = undefined;
51 cipher.encrypt(&encrypted_counter, &counter);
52 for (0..tag_bytes) |index| result[index] ^= encrypted_counter[index];
53 return result;
54 }
55
56 pub fn tag(odcid: []const u8, retry_pseudo: []const u8) TagError![tag_bytes]u8 {
57 if (odcid.len > connection_id_bytes_max) return error.ConnectionIdTooLong;
58 if (retry_pseudo.len > pseudo_bytes_max) return error.PacketTooLarge;
59 var prefix: [1 + connection_id_bytes_max]u8 = undefined;
60 prefix[0] = @intCast(odcid.len);
61 @memcpy(prefix[1..][0..odcid.len], odcid);
62 return authenticate(prefix[0 .. 1 + odcid.len], retry_pseudo);
63 }
64
65 pub fn verify(odcid: []const u8, retry_packet: []const u8) bool {
66 if (odcid.len > connection_id_bytes_max) return false;
67 if (retry_packet.len <= tag_bytes) return false;
68 if (retry_packet.len > quic.crypto.packet.packet_bytes_max) return false;
69 const tag_offset = retry_packet.len - tag_bytes;
70 const expected = tag(odcid, retry_packet[0..tag_offset]) catch return false;
71 const actual: [tag_bytes]u8 = retry_packet[tag_offset..][0..tag_bytes].*;
72 return std.crypto.timing_safe.eql([tag_bytes]u8, expected, actual);
73 }