lib/quic/src/properties/tls.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const hypothesis = @import("hypothesis");
3 const quic = @import("quic");
4
5 const tls = quic.tls;
6 const property_seed: u64 = 0x7175_6963_746c_7301;
7 const limits = tls.Limits{ .max_message = 512 };
8 const storage_bytes: usize = 4_124;
9
10 fn settings() hypothesis.Settings {
11 return hypothesis.Settings.quick()
12 .withSeed(property_seed)
13 .withDatabase("zig-out/hypothesis-failures/quic-tls");
14 }
15
16 fn fixedRandom(hello: u8, key: u8) tls.FixedRandom {
17 return .{ .hello = @splat(hello), .key_exchange = @splat(key) };
18 }
19
20 fn serverEngine(owner: *tls.Storage, bytes: tls.Storage.Storage) !tls.Engine {
21 owner.* = try tls.Storage.init(bytes, limits);
22 return tls.Engine.init(.{
23 .role = .server,
24 .identity = try tls.Identity.fromSeed(@splat(0x71)),
25 .expected_peer = null,
26 .alpn = "tiny/1",
27 .transport_parameters = &.{ 0x01, 0x02 },
28 .server_name = null,
29 .random = tls.Random.testing(fixedRandom(0x72, 0x73)),
30 }, limits, owner);
31 }
32
33 const DecoderTotality = struct {
34 pub fn property(data: *hypothesis.ConjectureData, _: std.mem.Allocator) !void {
35 const bytes = try data.drawBytes(0, 1_024);
36 const level: tls.Level = @fromBackingInt(@intCast(try data.drawInteger(0, 2, 0)));
37 var backing: [storage_bytes]u8 align(tls.Storage.storage_alignment) = undefined;
38 var owner: tls.Storage = undefined;
39 var engine = try serverEngine(&owner, &backing);
40 defer {
41 engine.deinit();
42 _ = owner.deinit();
43 }
44 engine.receive(level, bytes) catch |err| switch (err) {
45 error.TlsAlert, error.MessageTooLarge => {
46 try std.testing.expect(engine.alert() != null);
47 },
48 };
49 }
50 };
51
52 test "property: RFC 8446 section 4 handshake decoder is total over arbitrary bytes" {
53 try hypothesis.checkNamed(DecoderTotality, "quic-tls-totality", settings());
54 }
55
56 fn random32(data: *hypothesis.ConjectureData) ![32]u8 {
57 const bytes = try data.drawBytes(32, 32);
58 return bytes[0..32].*;
59 }
60
61 fn nonempty(data: *hypothesis.ConjectureData, maximum: usize) ![]const u8 {
62 const bytes = try data.drawBytes(0, maximum);
63 return if (bytes.len == 0) "x" else bytes;
64 }
65
66 fn expectClientHelloRoundTrip(data: *hypothesis.ConjectureData) !void {
67 const name = if (try data.drawBoolean()) try nonempty(data, 24) else null;
68 var first: [512]u8 = undefined;
69 var output = quic.cursor.Write.init(&first);
70 try tls.message.profile.encodeClientHello(.{
71 .random = try random32(data),
72 .key_share = try random32(data),
73 .alpn = try nonempty(data, 16),
74 .transport_parameters = try data.drawBytes(0, 48),
75 .server_name = name,
76 }, &output);
77 const decoded = try tls.message.decodeClientHello(output.written());
78 var second: [512]u8 = undefined;
79 var encoded = quic.cursor.Write.init(&second);
80 try tls.message.encodeClientHello(decoded, &encoded);
81 try std.testing.expectEqualSlices(u8, output.written(), encoded.written());
82 }
83
84 fn expectServerHelloRoundTrip(data: *hypothesis.ConjectureData) !void {
85 var first: [128]u8 = undefined;
86 var output = quic.cursor.Write.init(&first);
87 try tls.message.profile.encodeServerHello(.{
88 .random = try random32(data),
89 .key_share = try random32(data),
90 .cipher_suite = .AES_128_GCM_SHA256,
91 }, &output);
92 const decoded = try tls.message.decodeServerHello(output.written());
93 var second: [128]u8 = undefined;
94 var encoded = quic.cursor.Write.init(&second);
95 try tls.message.encodeServerHello(decoded, &encoded);
96 try std.testing.expectEqualSlices(u8, output.written(), encoded.written());
97 }
98
99 fn expectEncryptedRoundTrip(data: *hypothesis.ConjectureData) !void {
100 var first: [256]u8 = undefined;
101 var output = quic.cursor.Write.init(&first);
102 try tls.message.profile.encodeEncryptedExtensions(.{
103 .alpn = try nonempty(data, 16),
104 .transport_parameters = try data.drawBytes(0, 48),
105 .acknowledge_server_name = try data.drawBoolean(),
106 }, &output);
107 const decoded = try tls.message.decodeEncryptedExtensions(output.written());
108 var second: [256]u8 = undefined;
109 var encoded = quic.cursor.Write.init(&second);
110 try tls.message.encodeEncryptedExtensions(decoded, &encoded);
111 try std.testing.expectEqualSlices(u8, output.written(), encoded.written());
112 }
113
114 fn expectRequestRoundTrip() !void {
115 var first: [32]u8 = undefined;
116 var output = quic.cursor.Write.init(&first);
117 try tls.message.profile.encodeCertificateRequest(&output);
118 const decoded = try tls.message.decodeCertificateRequest(output.written());
119 var second: [32]u8 = undefined;
120 var encoded = quic.cursor.Write.init(&second);
121 try tls.message.encodeCertificateRequest(decoded, &encoded);
122 try std.testing.expectEqualSlices(u8, output.written(), encoded.written());
123 }
124
125 fn expectCertificateRoundTrip(data: *hypothesis.ConjectureData) !void {
126 var first: [96]u8 = undefined;
127 var output = quic.cursor.Write.init(&first);
128 try tls.message.profile.encodeCertificate(try random32(data), &output);
129 const decoded = try tls.message.decodeCertificate(output.written());
130 var second: [96]u8 = undefined;
131 var encoded = quic.cursor.Write.init(&second);
132 try tls.message.encodeCertificate(decoded, &encoded);
133 try std.testing.expectEqualSlices(u8, output.written(), encoded.written());
134 }
135
136 fn expectVerifyAndFinishedRoundTrip(data: *hypothesis.ConjectureData) !void {
137 const first_half = try random32(data);
138 const second_half = try random32(data);
139 var signature: [64]u8 = undefined;
140 signature[0..32].* = first_half;
141 signature[32..64].* = second_half;
142 var first: [96]u8 = undefined;
143 var output = quic.cursor.Write.init(&first);
144 try tls.message.profile.encodeCertificateVerify(signature, &output);
145 const verify = try tls.message.decodeCertificateVerify(output.written());
146 var second: [96]u8 = undefined;
147 var encoded = quic.cursor.Write.init(&second);
148 try tls.message.encodeCertificateVerify(verify, &encoded);
149 try std.testing.expectEqualSlices(u8, output.written(), encoded.written());
150 try expectFinishedRoundTrip(try random32(data));
151 }
152
153 fn expectFinishedRoundTrip(verify_data: [32]u8) !void {
154 var first: [36]u8 = undefined;
155 var output = quic.cursor.Write.init(&first);
156 try tls.message.profile.encodeFinished(verify_data, &output);
157 const decoded = try tls.message.decodeFinished(output.written());
158 var second: [36]u8 = undefined;
159 var encoded = quic.cursor.Write.init(&second);
160 try tls.message.encodeFinished(decoded, &encoded);
161 try std.testing.expectEqualSlices(u8, output.written(), encoded.written());
162 }
163
164 const MessageRoundTrip = struct {
165 pub fn property(data: *hypothesis.ConjectureData, _: std.mem.Allocator) !void {
166 try expectClientHelloRoundTrip(data);
167 try expectServerHelloRoundTrip(data);
168 try expectEncryptedRoundTrip(data);
169 try expectRequestRoundTrip();
170 try expectCertificateRoundTrip(data);
171 try expectVerifyAndFinishedRoundTrip(data);
172 }
173 };
174
175 test "property: RFC 8446 section 4 handshake messages round trip" {
176 try hypothesis.checkNamed(MessageRoundTrip, "quic-tls-message-roundtrip", settings());
177 }
178
179 const Pair = struct {
180 client_bytes: [storage_bytes]u8 align(tls.Storage.storage_alignment),
181 server_bytes: [storage_bytes]u8 align(tls.Storage.storage_alignment),
182 client_storage: tls.Storage,
183 server_storage: tls.Storage,
184 client: tls.Engine,
185 server: tls.Engine,
186
187 fn init(self: *Pair) !void {
188 const client_identity = try tls.Identity.fromSeed(@splat(0x11));
189 const server_identity = try tls.Identity.fromSeed(@splat(0x22));
190 self.client_storage = try tls.Storage.init(&self.client_bytes, limits);
191 self.server_storage = try tls.Storage.init(&self.server_bytes, limits);
192 self.client = try pairEngine(.client, client_identity, &self.client_storage);
193 self.server = try pairEngine(.server, server_identity, &self.server_storage);
194 }
195
196 fn deinit(self: *Pair) void {
197 self.client.deinit();
198 self.server.deinit();
199 _ = self.client_storage.deinit();
200 _ = self.server_storage.deinit();
201 }
202 };
203
204 fn pairEngine(role: tls.Role, identity: tls.Identity, owner: *tls.Storage) !tls.Engine {
205 return tls.Engine.init(.{
206 .role = role,
207 .identity = identity,
208 .expected_peer = null,
209 .alpn = "tiny/1",
210 .transport_parameters = if (role == .client) &.{0x01} else &.{0x02},
211 .server_name = null,
212 .random = tls.Random.testing(if (role == .client)
213 fixedRandom(0x31, 0x32)
214 else
215 fixedRandom(0x41, 0x42)),
216 }, limits, owner);
217 }
218
219 fn bodyBytes(bytes: []const u8) !usize {
220 var offset: usize = 0;
221 var total: usize = 0;
222 for (0..8) |_| {
223 if (offset == bytes.len) return total;
224 if (bytes.len - offset < 4) return error.Truncated;
225 const length = (@as(usize, bytes[offset + 1]) << 16) |
226 (@as(usize, bytes[offset + 2]) << 8) | bytes[offset + 3];
227 if (length > bytes.len - offset - 4) return error.Truncated;
228 total += length;
229 offset += length + 4;
230 }
231 return error.TooManyMessages;
232 }
233
234 fn bodyOffset(bytes: []const u8, wanted: usize) !usize {
235 var offset: usize = 0;
236 var skipped: usize = 0;
237 for (0..8) |_| {
238 if (offset == bytes.len) return error.NotFound;
239 const length = (@as(usize, bytes[offset + 1]) << 16) |
240 (@as(usize, bytes[offset + 2]) << 8) | bytes[offset + 3];
241 if (wanted - skipped < length) return offset + 4 + wanted - skipped;
242 skipped += length;
243 offset += length + 4;
244 }
245 return error.NotFound;
246 }
247
248 fn deliver(
249 sender: *tls.Engine,
250 receiver: *tls.Engine,
251 level: tls.Level,
252 wire: []u8,
253 data: *hypothesis.ConjectureData,
254 mutate: bool,
255 ) !bool {
256 const count = sender.emit(level, wire);
257 if (count == 0) return error.EmptyFlight;
258 if (mutate) {
259 const count_body = try bodyBytes(wire[0..count]);
260 const wanted = try data.drawInteger(0, count_body - 1, 0);
261 wire[try bodyOffset(wire[0..count], wanted)] ^= 1;
262 }
263 receiver.receive(level, wire[0..count]) catch |err| switch (err) {
264 error.TlsAlert, error.MessageTooLarge => {
265 try std.testing.expect(receiver.alert() != null);
266 return true;
267 },
268 };
269 return false;
270 }
271
272 const Tamper = struct {
273 pub fn property(data: *hypothesis.ConjectureData, _: std.mem.Allocator) !void {
274 const selected = try data.drawInteger(0, 3, 0);
275 var pair: Pair = undefined;
276 try pair.init();
277 defer pair.deinit();
278 var wire: [2_048]u8 = undefined;
279 if (try deliver(&pair.client, &pair.server, .initial, &wire, data, selected == 0)) return;
280 if (try deliver(&pair.server, &pair.client, .initial, &wire, data, selected == 1)) return;
281 if (try deliver(&pair.server, &pair.client, .handshake, &wire, data, selected == 2)) return;
282 if (try deliver(&pair.client, &pair.server, .handshake, &wire, data, selected == 3)) return;
283 try std.testing.expect(false);
284 }
285 };
286
287 test "property: RFC 8446 section 4 modified handshake bytes never complete" {
288 try hypothesis.checkNamed(Tamper, "quic-tls-tamper", settings());
289 }
290
291 const MessageBound = struct {
292 pub fn property(_: *hypothesis.ConjectureData, _: std.mem.Allocator) !void {
293 const small_limits = tls.Limits{ .max_message = 256 };
294 var bytes: [2_076]u8 align(tls.Storage.storage_alignment) = undefined;
295 var owner = try tls.Storage.init(&bytes, small_limits);
296 var engine = try tls.Engine.init(.{
297 .role = .client,
298 .identity = try tls.Identity.fromSeed(@splat(0x51)),
299 .expected_peer = null,
300 .alpn = "a",
301 .transport_parameters = &.{},
302 .server_name = null,
303 .random = tls.Random.testing(fixedRandom(0x52, 0x53)),
304 }, small_limits, &owner);
305 var message: [260]u8 = @splat(0);
306 message[0] = @backingInt(std.crypto.tls.HandshakeType.new_session_ticket);
307 message[2] = 1;
308 message[14] = 243;
309 try engine.receive(.one_rtt, &message);
310 const too_large = [_]u8{ message[0], 0, 1, 1 };
311 try std.testing.expectError(error.MessageTooLarge, engine.receive(.one_rtt, &too_large));
312 engine.deinit();
313 _ = owner.deinit();
314 }
315 };
316
317 test "property: RFC 8446 section 4 message maximum accepts and maximum plus one rejects" {
318 try hypothesis.checkNamed(MessageBound, "quic-tls-message-bound", settings());
319 }