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 }