lib/quic/src/tls/engine/storage.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_phase = @import("alloc_phase");
  3 
  4 /// Fixes how much room the Handshake region needs, so the derived storage size covers the whole
  5 /// server flight. The Handshake region is sized as that many maximum-length messages, because the
  6 /// number of handshake messages in the server's flight is five. The server queues
  7 /// EncryptedExtensions, CertificateRequest, Certificate, CertificateVerify, and Finished, in that
  8 /// order.
  9 const server_flight_messages_max: usize = 5;
 10 
 11 pub const Storage = struct {
 12     pub const storage_alignment: usize = 16;
 13     pub const Storage = []align(storage_alignment) u8;
 14     pub const Limits = struct { max_message: u16 };
 15     pub const Capacity = struct {
 16         message_bytes: usize,
 17         receive_bytes: usize,
 18         initial_bytes: usize,
 19         handshake_bytes: usize,
 20         peer_bytes: usize,
 21         storage_bytes: usize,
 22 
 23         pub const DeriveError = error{ MessageLimitEmpty, CapacityOverflow };
 24 
 25         pub fn derive(limits: Limits) DeriveError!Capacity {
 26             if (limits.max_message == 0) return error.MessageLimitEmpty;
 27             const message_bytes = std.math.add(
 28                 usize,
 29                 limits.max_message,
 30                 4,
 31             ) catch return error.CapacityOverflow;
 32             const handshake_bytes = std.math.mul(
 33                 usize,
 34                 message_bytes,
 35                 server_flight_messages_max,
 36             ) catch return error.CapacityOverflow;
 37             const message_regions = std.math.mul(
 38                 usize,
 39                 message_bytes,
 40                 7,
 41             ) catch return error.CapacityOverflow;
 42             const storage_bytes = std.math.add(
 43                 usize,
 44                 message_regions,
 45                 limits.max_message,
 46             ) catch return error.CapacityOverflow;
 47             return .{
 48                 .message_bytes = message_bytes,
 49                 .receive_bytes = message_bytes,
 50                 .initial_bytes = message_bytes,
 51                 .handshake_bytes = handshake_bytes,
 52                 .peer_bytes = limits.max_message,
 53                 .storage_bytes = storage_bytes,
 54             };
 55         }
 56     };
 57     pub const InitError = Capacity.DeriveError || error{StorageTooShort};
 58     pub const work_limits: alloc_phase.capacity.WorkLimits = .{
 59         .transition_steps_max = 1,
 60         .cleanup_steps_per_call_max = 0,
 61         .cleanup_calls_at_capacity_max = 0,
 62     };
 63 
 64     pub const claim: alloc_phase.capacity.Declaration = .{
 65         .source = .{
 66             .id = "quic.tls_storage",
 67             .kind = .phase_static,
 68             .limit_source = .caller,
 69             .storage = .{
 70                 .covered = &.{
 71                     .{
 72                         .id = "handshake_input_output_and_peer_bytes",
 73                         .lifetime = .steady,
 74                         .detail = "one receive message, two flights, and peer parameters",
 75                     },
 76                 },
 77                 .excluded = &.{
 78                     "caller configuration and identity key storage",
 79                     "QUIC CRYPTO reassembly and packet protection storage",
 80                     "cryptographic primitive stack storage and external I/O state",
 81                 },
 82             },
 83             .capacity = .{
 84                 .inputs = &.{
 85                     alloc_phase.capacity.bindInput(Limits, "max_message", "max_message"),
 86                 },
 87                 .type_selectors = &.{},
 88                 .nodes = &.{
 89                     .{ .input = 0 },
 90                     .{ .constant = 4 },
 91                     .{ .add = .{ .left = 0, .right = 1 } },
 92                     .{
 93                         .scale = .{ .node = 2, .coefficient = .{ .literal = 7 } },
 94                     },
 95                     .{ .add = .{ .left = 3, .right = 0 } },
 96                 },
 97                 .assertions = &.{.{
 98                     .scope = .closure_total,
 99                     .measure = .retained,
100                     .relation = .exact,
101                     .expression = 4,
102                 }},
103             },
104             .overload = .{
105                 .kind = .reject_before_seal,
106                 .detail = "empty limits and short storage reject before activation",
107             },
108             .risks = .{
109                 .transitive = .{
110                     .status = .witnessed,
111                     .detail = "message parsing and emission use caller storage only",
112                 },
113                 .foreign = .{
114                     .status = .excluded,
115                     .detail = "standard cryptography retains no dynamic engine storage",
116                 },
117             },
118             .work = .{ .equation = "initialization to handshake transition steps <= 1" },
119             .obligations = &.{
120                 .{ .key = "quic_tls_capacity", .role = .capacity_model },
121                 .{ .key = "quic_tls_boundary", .role = .overload },
122                 .{ .key = "quic_tls_transitive", .role = .transitive_risk },
123                 .{ .key = "quic_tls_foreign", .role = .foreign_risk },
124                 .{ .key = "quic_tls_work", .role = .work_bound },
125                 .{ .key = "quic_tls_root", .role = .custom },
126             },
127         },
128         .bindings = .{
129             .owner = @This(),
130             .seal = .{
131                 .family = alloc_phase.capacity.selector(@This().activate),
132                 .premise = .{
133                     .class = .checked_semantic_fact,
134                     .authority = .checker,
135                 },
136             },
137             .teardown = .{
138                 .family = alloc_phase.capacity.selector(@This().deinit),
139                 .premise = .{
140                     .class = .checked_semantic_fact,
141                     .authority = .checker,
142                 },
143             },
144         },
145     };
146 
147     phase: alloc_phase.capacity.Phase,
148     capacity: Capacity,
149     storage: []align(storage_alignment) u8,
150 
151     pub fn init(bytes: @This().Storage, limits: Limits) InitError!@This() {
152         const capacity = try Capacity.derive(limits);
153         if (bytes.len < capacity.storage_bytes) return error.StorageTooShort;
154         return .{
155             .phase = .initialization,
156             .capacity = capacity,
157             .storage = bytes[0..capacity.storage_bytes],
158         };
159     }
160 
161     pub fn activate(self: *@This()) void {
162         std.debug.assert(self.phase == .initialization);
163         self.phase = .steady;
164     }
165 
166     pub fn deinit(self: *@This()) @This().Storage {
167         std.debug.assert(self.phase == .steady);
168         std.crypto.secureZero(u8, self.storage);
169         self.phase = .teardown;
170         const bytes = self.storage;
171         self.* = undefined;
172         return bytes;
173     }
174 
175     /// A client engine writes its ClientHello into the Initial flight region during startup, before
176     /// the storage is sealed into its steady phase. The ordinary accessor requires the steady
177     /// phase, so this function returns that region while the storage is still in its initialization
178     /// phase. The call returns the same bytes the steady accessor later returns.
179     pub fn initialBytesForInitialization(self: *@This()) []u8 {
180         std.debug.assert(self.phase == .initialization);
181         const start = self.capacity.receive_bytes;
182         return self.storage[start..][0..self.capacity.initial_bytes];
183     }
184 
185     pub fn receiveBytes(self: *@This()) []u8 {
186         self.assertSteady();
187         return self.storage[0..self.capacity.receive_bytes];
188     }
189 
190     pub fn initialBytes(self: *@This()) []u8 {
191         self.assertSteady();
192         const start = self.capacity.receive_bytes;
193         return self.storage[start..][0..self.capacity.initial_bytes];
194     }
195 
196     pub fn handshakeBytes(self: *@This()) []u8 {
197         self.assertSteady();
198         const start = self.capacity.receive_bytes + self.capacity.initial_bytes;
199         return self.storage[start..][0..self.capacity.handshake_bytes];
200     }
201 
202     pub fn peerBytes(self: *@This()) []u8 {
203         self.assertSteady();
204         const start = self.capacity.receive_bytes +
205             self.capacity.initial_bytes + self.capacity.handshake_bytes;
206         return self.storage[start..][0..self.capacity.peer_bytes];
207     }
208 
209     fn assertSteady(self: *const @This()) void {
210         std.debug.assert(self.phase == .steady);
211         std.debug.assert(self.storage.len >= self.capacity.storage_bytes);
212     }
213 };
214 
215 comptime {
216     alloc_phase.capacity.requireProvisionedExactOwnerShape(Storage);
217 }
218 
219 test "TLS storage accepts exact capacity and rejects one byte less" {
220     comptime {
221         @stardustClaim(
222             alloc_phase.capacity.witness(Storage, "quic_tls_capacity"),
223             null,
224             null,
225             null,
226             null,
227             null,
228             null,
229         );
230         @stardustClaim(
231             alloc_phase.capacity.witness(Storage, "quic_tls_boundary"),
232             null,
233             null,
234             null,
235             null,
236             null,
237             null,
238         );
239         @stardustClaim(
240             alloc_phase.capacity.witness(Storage, "quic_tls_transitive"),
241             null,
242             null,
243             null,
244             null,
245             null,
246             null,
247         );
248         @stardustClaim(
249             alloc_phase.capacity.witness(Storage, "quic_tls_foreign"),
250             null,
251             null,
252             null,
253             null,
254             null,
255             null,
256         );
257         @stardustClaim(
258             alloc_phase.capacity.witness(Storage, "quic_tls_work"),
259             null,
260             null,
261             null,
262             null,
263             null,
264             null,
265         );
266         @stardustClaim(
267             alloc_phase.capacity.witness(Storage, "quic_tls_root"),
268             null,
269             null,
270             null,
271             null,
272             null,
273             null,
274         );
275     }
276     const limits = Storage.Limits{ .max_message = 128 };
277     const capacity = try Storage.Capacity.derive(limits);
278     var exact: [1_052]u8 align(Storage.storage_alignment) = undefined;
279     @memset(&exact, 0xa5);
280     try std.testing.expectEqual(exact.len, capacity.storage_bytes);
281     var accepted = try Storage.init(&exact, limits);
282     accepted.activate();
283     const returned = accepted.deinit();
284     try std.testing.expect(std.mem.allEqual(u8, returned, 0));
285     try std.testing.expectError(error.StorageTooShort, Storage.init(
286         @alignCast(exact[0 .. exact.len - 1]),
287         limits,
288     ));
289 }