lib/quic/src/properties/crypto.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const hypothesis = @import("hypothesis");
3 const quic = @import("quic");
4
5 const crypto = quic.crypto;
6 const KeyStorage = [crypto.Keys.storage_bytes_max]u8;
7 const property_seed: u64 = 0x7175_6963_6372_7901;
8 const secret_bytes: usize = @sizeOf(crypto.Secret);
9 const payload_bytes_max: usize = 64;
10 const packet_bytes_max: usize = 1 + 4 + payload_bytes_max + crypto.packet.tag_bytes;
11
12 fn settings() hypothesis.Settings {
13 return hypothesis.Settings.quick()
14 .withSeed(property_seed)
15 .withDatabase("zig-out/hypothesis-failures/quic-crypto");
16 }
17
18 fn copySecret(bytes: []const u8) crypto.Secret {
19 std.debug.assert(bytes.len == secret_bytes);
20 var result: crypto.Secret = undefined;
21 @memcpy(&result, bytes);
22 return result;
23 }
24
25 fn deriveKeys(
26 storage: *KeyStorage,
27 suite: crypto.Suite,
28 secret: crypto.Secret,
29 ) !crypto.Keys {
30 return crypto.Keys.derive(storage, suite, secret);
31 }
32
33 fn packetNumberBytes(packet: []u8, packet_number: u62, pn_len: u3) void {
34 std.debug.assert(pn_len >= 1);
35 std.debug.assert(pn_len <= 4);
36 std.debug.assert(packet.len >= 1 + pn_len);
37 var encoded: [8]u8 = undefined;
38 std.mem.writeInt(u64, &encoded, packet_number, .big);
39 @memcpy(packet[1..][0..pn_len], encoded[encoded.len - pn_len ..]);
40 }
41
42 fn roundTrip(
43 suite: crypto.Suite,
44 secret: crypto.Secret,
45 packet_number: u62,
46 payload: []const u8,
47 pn_len: u3,
48 ) !void {
49 std.debug.assert(payload.len <= payload_bytes_max);
50 const packet_len = 1 + pn_len + payload.len + crypto.packet.tag_bytes;
51 var packet: [packet_bytes_max]u8 = @splat(0);
52 packet[0] = 0x40 | @as(u8, pn_len - 1);
53 packetNumberBytes(&packet, packet_number, pn_len);
54 const payload_offset = 1 + pn_len;
55 @memcpy(packet[payload_offset..][0..payload.len], payload);
56 var send_storage: KeyStorage = undefined;
57 var send = try deriveKeys(&send_storage, suite, secret);
58 try crypto.packet.seal(
59 &send,
60 packet_number,
61 packet[0..packet_len],
62 1,
63 pn_len,
64 @intCast(payload.len),
65 );
66 var receive_storage: KeyStorage = undefined;
67 var receive = try deriveKeys(&receive_storage, suite, secret);
68 var scratch: [payload_bytes_max]u8 = undefined;
69 const largest_acked: ?u62 = if (packet_number == 0) null else packet_number - 1;
70 const opened = try crypto.packet.open(
71 &receive,
72 packet[0..packet_len],
73 &scratch,
74 1,
75 largest_acked,
76 );
77 try std.testing.expectEqual(packet_number, opened.packet_number);
78 try std.testing.expectEqualSlices(u8, payload, opened.payload);
79 }
80
81 const RoundTrip = struct {
82 pub fn property(data: *hypothesis.ConjectureData, _: std.mem.Allocator) !void {
83 const secret = copySecret(try data.drawBytes(secret_bytes, secret_bytes));
84 const packet_number: u62 = @intCast(try data.drawInteger(
85 0,
86 std.math.maxInt(u62),
87 0,
88 ));
89 const payload = try data.drawBytes(0, payload_bytes_max);
90 for (1..5) |length| {
91 const pn_len: u3 = @intCast(length);
92 try roundTrip(.aes_128_gcm_sha256, secret, packet_number, payload, pn_len);
93 try roundTrip(.chacha20_poly1305_sha256, secret, packet_number, payload, pn_len);
94 }
95 }
96 };
97
98 test "property: RFC 9001 section 5 packet protection round trip" {
99 try hypothesis.checkNamed(RoundTrip, "quic-crypto-round-trip", settings());
100 }
101
102 fn sealedPacket(
103 suite: crypto.Suite,
104 secret: crypto.Secret,
105 packet_number: u62,
106 payload: []const u8,
107 pn_len: u3,
108 ) !struct { bytes: [packet_bytes_max]u8, length: usize } {
109 const packet_len = 1 + pn_len + payload.len + crypto.packet.tag_bytes;
110 var packet: [packet_bytes_max]u8 = @splat(0);
111 packet[0] = 0x40 | @as(u8, pn_len - 1);
112 packetNumberBytes(&packet, packet_number, pn_len);
113 const payload_offset = 1 + pn_len;
114 @memcpy(packet[payload_offset..][0..payload.len], payload);
115 var storage: KeyStorage = undefined;
116 var keys = try deriveKeys(&storage, suite, secret);
117 try crypto.packet.seal(
118 &keys,
119 packet_number,
120 packet[0..packet_len],
121 1,
122 pn_len,
123 @intCast(payload.len),
124 );
125 return .{ .bytes = packet, .length = packet_len };
126 }
127
128 fn rejectEveryByte(
129 suite: crypto.Suite,
130 secret: crypto.Secret,
131 packet_number: u62,
132 payload: []const u8,
133 pn_len: u3,
134 ) !void {
135 const sealed = try sealedPacket(suite, secret, packet_number, payload, pn_len);
136 for (0..packet_bytes_max) |index| {
137 if (index >= sealed.length) break;
138 var tampered = sealed.bytes;
139 tampered[index] ^= 0x01;
140 var storage: KeyStorage = undefined;
141 var keys = try deriveKeys(&storage, suite, secret);
142 var scratch: [payload_bytes_max]u8 = undefined;
143 const largest_acked: ?u62 = if (packet_number == 0) null else packet_number - 1;
144 if (crypto.packet.open(
145 &keys,
146 tampered[0..sealed.length],
147 &scratch,
148 1,
149 largest_acked,
150 )) |_| {
151 return error.TamperedPacketAccepted;
152 } else |_| {}
153 }
154 }
155
156 const Tamper = struct {
157 pub fn property(data: *hypothesis.ConjectureData, _: std.mem.Allocator) !void {
158 const secret = copySecret(try data.drawBytes(secret_bytes, secret_bytes));
159 const packet_number: u62 = @intCast(try data.drawInteger(
160 0,
161 std.math.maxInt(u62),
162 0,
163 ));
164 const payload = try data.drawBytes(0, 32);
165 for (1..5) |length| {
166 const pn_len: u3 = @intCast(length);
167 try rejectEveryByte(.aes_128_gcm_sha256, secret, packet_number, payload, pn_len);
168 try rejectEveryByte(.chacha20_poly1305_sha256, secret, packet_number, payload, pn_len);
169 }
170 }
171 };
172
173 test "property: RFC 9001 section 5 any protected packet byte rejects after a flip" {
174 try hypothesis.checkNamed(Tamper, "quic-crypto-tamper", settings());
175 }
176
177 const Totality = struct {
178 pub fn property(data: *hypothesis.ConjectureData, _: std.mem.Allocator) !void {
179 const source = try data.drawBytes(0, 512);
180 var packet: [512]u8 = undefined;
181 @memcpy(packet[0..source.len], source);
182 const original = packet;
183 const suite: crypto.Suite = if (try data.drawBoolean())
184 .aes_128_gcm_sha256
185 else
186 .chacha20_poly1305_sha256;
187 var storage: KeyStorage = undefined;
188 var keys = try deriveKeys(&storage, suite, @splat(0x71));
189 var scratch: [512]u8 = undefined;
190 const pn_offset = try data.drawInteger(0, 600, 0);
191 if (crypto.packet.open(
192 &keys,
193 packet[0..source.len],
194 &scratch,
195 pn_offset,
196 null,
197 )) |_| {} else |_| {
198 try std.testing.expectEqualSlices(
199 u8,
200 original[0..source.len],
201 packet[0..source.len],
202 );
203 }
204 const odcid = try data.drawBytes(0, 32);
205 _ = crypto.retry.verify(odcid, packet[0..source.len]);
206 }
207 };
208
209 test "property: RFC 9001 sections 5 and 5.8 decoders are total over random bytes" {
210 try hypothesis.checkNamed(Totality, "quic-crypto-totality", settings());
211 }
212
213 const Header = struct {
214 pub fn property(data: *hypothesis.ConjectureData, _: std.mem.Allocator) !void {
215 const mask_bytes = try data.drawBytes(5, 5);
216 const protection_mask: [5]u8 = mask_bytes[0..5].*;
217 const forms = [_]u8{ 0xc0, 0x40 };
218 for (forms) |form| {
219 for (1..5) |length| {
220 const pn_len: u3 = @intCast(length);
221 var packet: [12]u8 = @splat(0xa5);
222 packet[0] = form | @as(u8, pn_len - 1);
223 const original = packet;
224 try crypto.header.protect(protection_mask, &packet, 5, pn_len);
225 const decoded_len = try crypto.header.unprotect(protection_mask, &packet, 5);
226 try std.testing.expectEqual(pn_len, decoded_len);
227 try std.testing.expectEqual(original, packet);
228 }
229 }
230 }
231 };
232
233 test "property: RFC 9001 section 5.4 header protection is symmetric" {
234 try hypothesis.checkNamed(Header, "quic-header-protection", settings());
235 }