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 }