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 }