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 }