lib/quic/src/crypto/keys.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const alloc_phase = @import("alloc_phase");
2 const std = @import("std");
3
4 const Hkdf = std.crypto.kdf.hkdf.HkdfSha256;
5 const tls = std.crypto.tls;
6
7 pub const secret_bytes: usize = 32;
8 pub const key_bytes_max: usize = 32;
9 pub const iv_bytes: usize = 12;
10
11 /// The count of packets one AES-128-GCM key may seal, which RFC 9001 section 6.6 puts at 2^23. A
12 /// caller compares it against the packets one key has sealed to decide when a key update is due.
13 /// Reaching it stops further sealing under that key.
14 pub const aes_confidentiality_limit: u64 = @as(u64, 1) << 23;
15 /// The number of packets before the AES-GCM confidentiality limit at which an update is already
16 /// called for. RFC 9001 section 6.6 requires this margin, and the update comes one packet ahead of
17 /// the hard limit so a sender never has to stop. A key that has sealed the confidentiality limit
18 /// less this margin reports that it needs an update. This margin sits below the confidentiality
19 /// limit, which the compiler checks.
20 pub const aes_update_margin: u64 = 1;
21 /// The count of packets that may fail authentication under one AES-128-GCM key, which RFC 9001
22 /// section 6.6 puts at 2^52. A caller compares it against failed decryptions to decide when an
23 /// attacker has had too many guesses. Reaching it stops further opening under that key.
24 pub const aes_integrity_limit: u64 = @as(u64, 1) << 52;
25 /// The absence of a confidentiality limit for ChaCha20-Poly1305, because RFC 9001 section 6.6 puts
26 /// the bound above the 2^62 packets a connection can number. A caller reading the limits for a
27 /// suite finds out that this one carries no packet count to watch. A ChaCha20-Poly1305 key
28 /// therefore reports that it needs no update and that its confidentiality is intact, whatever it
29 /// has sealed.
30 pub const chacha_confidentiality_limit: ?u64 = null;
31 /// The count of packets that may fail authentication under one ChaCha20-Poly1305 key, which RFC
32 /// 9001 section 6.6 puts at 2^36. A caller compares it against failed decryptions under this suite.
33 /// Reaching it stops further opening under that key.
34 pub const chacha_integrity_limit: u64 = @as(u64, 1) << 36;
35
36 pub const Secret = [secret_bytes]u8;
37
38 pub const Suite = enum {
39 aes_128_gcm_sha256,
40 chacha20_poly1305_sha256,
41
42 pub fn keyLength(self: Suite) u6 {
43 return switch (self) {
44 .aes_128_gcm_sha256 => 16,
45 .chacha20_poly1305_sha256 => 32,
46 };
47 }
48 };
49
50 pub const Limits = struct {
51 suite: Suite,
52 };
53
54 const Material = extern struct {
55 key: [key_bytes_max]u8,
56 iv: [iv_bytes]u8,
57 hp: [key_bytes_max]u8,
58 };
59
60 pub const storage_bytes: usize = @sizeOf(Material);
61
62 pub const Capacity = struct {
63 suite: Suite,
64 key_bytes: u6,
65 storage_bytes: usize,
66
67 pub const DeriveError = error{CapacityOverflow};
68
69 pub fn derive(limits: Limits) DeriveError!Capacity {
70 const key_lanes = std.math.mul(usize, key_bytes_max, 2) catch
71 return error.CapacityOverflow;
72 const derived_bytes = std.math.add(usize, key_lanes, iv_bytes) catch
73 return error.CapacityOverflow;
74 if (derived_bytes != storage_bytes) return error.CapacityOverflow;
75 return .{
76 .suite = limits.suite,
77 .key_bytes = limits.suite.keyLength(),
78 .storage_bytes = derived_bytes,
79 };
80 }
81 };
82
83 const KeyLimits = Limits;
84 const KeyCapacity = Capacity;
85
86 pub const Keys = struct {
87 phase: alloc_phase.capacity.Phase,
88 capacity: KeyCapacity,
89 storage: []align(storage_alignment) u8,
90 sealed_packets: u64,
91 failed_opens: u64,
92
93 pub const storage_alignment: usize = @alignOf(Material);
94 pub const storage_bytes_max: usize = storage_bytes;
95 pub const Storage = []align(storage_alignment) u8;
96 pub const Limits: type = KeyLimits;
97 pub const Capacity: type = KeyCapacity;
98 pub const InitError = KeyCapacity.DeriveError || error{StorageTooShort};
99 pub const work_limits: alloc_phase.capacity.WorkLimits = .{
100 .transition_steps_max = 1,
101 .cleanup_steps_per_call_max = 0,
102 .cleanup_calls_at_capacity_max = 0,
103 };
104
105 pub const claim: alloc_phase.capacity.Declaration = .{
106 .source = .{
107 .id = "quic.crypto_keys",
108 .kind = .startup_static,
109 .limit_source = .caller,
110 .storage = .{
111 .covered = &.{
112 .{
113 .id = "packet_key_bytes",
114 .lifetime = .steady,
115 .detail = "fixed caller-provisioned packet protection key bytes",
116 },
117 .{
118 .id = "packet_iv_bytes",
119 .lifetime = .steady,
120 .detail = "fixed caller-provisioned packet protection IV bytes",
121 },
122 .{
123 .id = "header_key_bytes",
124 .lifetime = .steady,
125 .detail = "fixed caller-provisioned header protection key bytes",
126 },
127 },
128 .excluded = &.{
129 "caller-owned packet and traffic secret bytes",
130 "fixed suite tag and usage counters",
131 },
132 },
133 .capacity = .{
134 .inputs = &.{},
135 .type_selectors = &.{
136 alloc_phase.capacity.bindType(Material, "key_material"),
137 },
138 .nodes = &.{
139 .{ .constant = 1 },
140 .{ .scale = .{
141 .node = 0,
142 .coefficient = .{ .size_of_concrete_type = 0 },
143 } },
144 },
145 .assertions = &.{.{
146 .scope = .closure_total,
147 .measure = .retained,
148 .relation = .exact,
149 .expression = 1,
150 }},
151 },
152 .overload = .{
153 .kind = .reject_before_seal,
154 .detail = "short caller storage rejects before key derivation",
155 },
156 .risks = .{
157 .transitive = .{
158 .status = .witnessed,
159 .detail = "standard cryptographic primitives use fixed value storage",
160 },
161 .foreign = .{
162 .status = .excluded,
163 .detail = "key derivation and packet protection cross no foreign boundary",
164 },
165 },
166 .work = .{ .equation = "initialization to steady transition steps <= 1" },
167 .obligations = &.{
168 .{ .key = "quic_crypto_keys_capacity", .role = .capacity_model },
169 .{ .key = "quic_crypto_keys_overload", .role = .overload },
170 .{ .key = "quic_crypto_keys_transitive", .role = .transitive_risk },
171 .{ .key = "quic_crypto_keys_work", .role = .work_bound },
172 },
173 },
174 .bindings = .{ .owner = @This() },
175 };
176
177 pub fn init(storage: Storage, limits: KeyLimits) InitError!Keys {
178 const capacity = try KeyCapacity.derive(limits);
179 if (storage.len < capacity.storage_bytes) return error.StorageTooShort;
180 const owned = storage[0..capacity.storage_bytes];
181 @memset(owned, 0);
182 return .{
183 .phase = .initialization,
184 .capacity = capacity,
185 .storage = owned,
186 .sealed_packets = 0,
187 .failed_opens = 0,
188 };
189 }
190
191 pub fn activate(self: *Keys) void {
192 std.debug.assert(self.phase == .initialization);
193 std.debug.assert(self.storage.len == self.capacity.storage_bytes);
194 self.phase = .steady;
195 }
196
197 pub fn derive(storage: Storage, suite: Suite, secret: Secret) InitError!Keys {
198 var result = try init(storage, .{ .suite = suite });
199 result.writePacketMaterial(secret);
200 result.writeHeaderMaterial(secret);
201 result.activate();
202 return result;
203 }
204
205 pub fn deinit(self: *Keys) Storage {
206 std.debug.assert(self.phase == .steady);
207 std.crypto.secureZero(u8, self.storage);
208 self.phase = .teardown;
209 const owned = self.storage;
210 self.* = undefined;
211 return owned;
212 }
213
214 fn material(self: *Keys) *Material {
215 std.debug.assert(self.phase != .teardown);
216 std.debug.assert(self.storage.len == storage_bytes);
217 return @ptrCast(self.storage.ptr);
218 }
219
220 fn materialConst(self: *const Keys) *const Material {
221 std.debug.assert(self.phase == .steady);
222 std.debug.assert(self.storage.len == storage_bytes);
223 return @ptrCast(self.storage.ptr);
224 }
225
226 fn writePacketMaterial(self: *Keys, secret: Secret) void {
227 const value = self.material();
228 value.key = @splat(0);
229 value.iv = tls.hkdfExpandLabel(Hkdf, secret, "quic iv", "", iv_bytes);
230 switch (self.capacity.suite) {
231 .aes_128_gcm_sha256 => value.key[0..16].* =
232 tls.hkdfExpandLabel(Hkdf, secret, "quic key", "", 16),
233 .chacha20_poly1305_sha256 => value.key =
234 tls.hkdfExpandLabel(Hkdf, secret, "quic key", "", 32),
235 }
236 }
237
238 fn writeHeaderMaterial(self: *Keys, secret: Secret) void {
239 const value = self.material();
240 value.hp = @splat(0);
241 switch (self.capacity.suite) {
242 .aes_128_gcm_sha256 => value.hp[0..16].* =
243 tls.hkdfExpandLabel(Hkdf, secret, "quic hp", "", 16),
244 .chacha20_poly1305_sha256 => value.hp =
245 tls.hkdfExpandLabel(Hkdf, secret, "quic hp", "", 32),
246 }
247 }
248
249 pub fn next(suite: Suite, secret: Secret) Secret {
250 return switch (suite) {
251 .aes_128_gcm_sha256,
252 .chacha20_poly1305_sha256,
253 => tls.hkdfExpandLabel(Hkdf, secret, "quic ku", "", secret_bytes),
254 };
255 }
256
257 pub fn update(self: *Keys, secret: Secret) void {
258 std.debug.assert(self.phase == .steady);
259 self.writePacketMaterial(secret);
260 self.sealed_packets = 0;
261 }
262
263 pub fn packetKey(self: *const Keys) [key_bytes_max]u8 {
264 return self.materialConst().key;
265 }
266
267 pub fn initializationVector(self: *const Keys) [iv_bytes]u8 {
268 return self.materialConst().iv;
269 }
270
271 pub fn headerKey(self: *const Keys) [key_bytes_max]u8 {
272 return self.materialConst().hp;
273 }
274
275 pub fn selectedSuite(self: *const Keys) Suite {
276 std.debug.assert(self.phase == .steady);
277 return self.capacity.suite;
278 }
279
280 pub fn keyLength(self: *const Keys) u6 {
281 std.debug.assert(self.phase == .steady);
282 return self.capacity.key_bytes;
283 }
284
285 pub fn needsUpdate(self: *const Keys) bool {
286 std.debug.assert(self.phase == .steady);
287 return switch (self.capacity.suite) {
288 .aes_128_gcm_sha256 => self.sealed_packets >=
289 aes_confidentiality_limit - aes_update_margin,
290 .chacha20_poly1305_sha256 => chacha_confidentiality_limit != null,
291 };
292 }
293
294 pub fn confidentialityExhausted(self: *const Keys) bool {
295 std.debug.assert(self.phase == .steady);
296 return switch (self.capacity.suite) {
297 .aes_128_gcm_sha256 => self.sealed_packets >= aes_confidentiality_limit,
298 .chacha20_poly1305_sha256 => chacha_confidentiality_limit != null,
299 };
300 }
301
302 pub fn integrityLimit(self: *const Keys) u64 {
303 std.debug.assert(self.phase == .steady);
304 return switch (self.capacity.suite) {
305 .aes_128_gcm_sha256 => aes_integrity_limit,
306 .chacha20_poly1305_sha256 => chacha_integrity_limit,
307 };
308 }
309
310 pub fn exhausted(self: *const Keys) bool {
311 std.debug.assert(self.phase == .steady);
312 return self.failed_opens >= self.integrityLimit();
313 }
314 };
315
316 comptime {
317 std.debug.assert(aes_update_margin < aes_confidentiality_limit);
318 }
319
320 comptime {
321 alloc_phase.capacity.requireProvisionedExactOwnerShape(Keys);
322 }