tiny.quic.tls
Defined in tiny.quic.
The TLS 1.3 handshake a QUIC connection needs runs as a state machine over bytes the caller carries.
API (60)
Actions
Public operations.
Alert.quicError: Returns the QUIC transport error code for one TLS alert.Capacity.deriveEngine.alertEngine.cipherSuiteEngine.confirmEngine.deinit: Overwrites every retained handshake secret and every region of the engine's storage with zeros so a caller finishing with a connection leaves no key material behind in the block it hands back.Engine.emitEngine.emitted: Returns the handshake bytes the caller has already taken at one encryption level.Engine.initEngine.peerEngine.receiveEngine.secretEngine.stateIdentity.fromSeedIdentity.publicIdentity.signRandom.testing: Builds a random source whose bytes the caller fixed in advance.Random.valuesStorage.Capacity.deriveStorage.activateStorage.deinitStorage.handshakeBytesStorage.initStorage.initialBytesStorage.initialBytesForInitialization: A client engine writes its ClientHello into the Initial flight region during startup, before the storage is sealed into its steady phase.Storage.peerBytesStorage.receiveBytesdecodeSubjectPublicKeyInfoencodeSubjectPublicKeyInfoverifySignature
Types and contracts
Public types and contracts.
AlertCapacityCapacity.DeriveErrorConfigDirectionEngineErrorFixedRandomIdentityInitErrorLevelLimitsPeerPublicKeyRandomRoleSecretStatus: How far the handshake has reached: still running, complete, or confirmed.StorageStorage.CapacityStorage.Capacity.DeriveErrorStorage.InitErrorStorage.LimitsStorage.Storage
Namespaces
Public namespaces.
Values and defaults
Public values and defaults.
Source
Source: lib/quic/src/tls/alert.zig:3
pub const Alert = enum(u8) { unexpected_message = 10, handshake_failure = 40, bad_certificate = 42, unsupported_certificate = 43, illegal_parameter = 47, decode_error = 50, decrypt_error = 51, protocol_version = 70, internal_error = 80, missing_extension = 109, unsupported_extension = 110, unrecognized_name = 112, certificate_required = 116, no_application_protocol = 120, /// Returns the QUIC transport error code for one TLS alert. A connection closing after a /// handshake failure calls it to get the code it puts in the CONNECTION_CLOSE frame. RFC 9001 /// section 4.8 forms the code by adding the alert number to 0x0100. Distinct alerts give /// distinct codes, so the peer learns which check failed. pub fn quicError(self: Alert) u16 { return 0x0100 + @as(u16, @backingInt(self)); }};Source: lib/quic/src/tls/engine/machine.zig:39
pub const Engine = struct { config: model.Config, limits: storage_mod.Storage.Limits, storage: *storage_mod.Storage, phase: Phase, transcript: Sha256, key_pair: X25519.KeyPair, hello_random: [32]u8, schedule: ?tls.schedule.Schedule, application: ?tls.schedule.Application, receive_length: usize, receive_total: usize, receive_level: ?model.Level, initial_length: usize, initial_offset: usize, handshake_length: usize, handshake_offset: usize, peer_key: tls.PublicKey, peer_key_present: bool, peer_verified: bool, peer_parameters_length: usize, pending_alert: ?tls.Alert, cipher_suite: ?std.crypto.tls.CipherSuite, handshake_secrets_present: bool, pub fn init( config: model.Config, limits: storage_mod.Storage.Limits, storage: *storage_mod.Storage, ) InitError!Engine { try validateConfig(config, limits, storage); var random = try config.random.values(); defer std.crypto.secureZero(u8, std.mem.asBytes(&random)); var key_pair = X25519.KeyPair.generateDeterministic(random.key_exchange) catch { return error.InvalidKey; }; defer std.crypto.secureZero(u8, &key_pair.secret_key); var self = Engine{ .config = config, .limits = limits, .storage = storage, .phase = if (config.role == .client) .client_wait_server_hello else .server_wait_client_hello, .transcript = Sha256.init(.{}), .key_pair = key_pair, .hello_random = random.hello, .schedule = null, .application = null, .receive_length = 0, .receive_total = 0, .receive_level = null, .initial_length = 0, .initial_offset = 0, .handshake_length = 0, .handshake_offset = 0, .peer_key = undefined, .peer_key_present = false, .peer_verified = false, .peer_parameters_length = 0, .pending_alert = null, .cipher_suite = null, .handshake_secrets_present = false, }; errdefer std.crypto.secureZero(u8, std.mem.asBytes(&self)); if (config.role == .client) { try self.queueClientHello(random.hello, storage.initialBytesForInitialization()); } storage.activate(); return self; } /// Overwrites every retained handshake secret and every region of the engine's storage with /// zeros so a caller finishing with a connection leaves no key material behind in the block it /// hands back. The four storage regions and the engine value itself are all covered. The call /// frees nothing, because the caller owns the block. pub fn deinit(self: *Engine) void { const storage = self.storage; std.crypto.secureZero(u8, storage.receiveBytes()); std.crypto.secureZero(u8, storage.initialBytes()); std.crypto.secureZero(u8, storage.handshakeBytes()); std.crypto.secureZero(u8, storage.peerBytes()); std.crypto.secureZero(u8, std.mem.asBytes(self)); } pub fn receive(self: *Engine, level: model.Level, bytes: []const u8) Error!void { if (self.pending_alert != null) return error.TlsAlert; if (self.receive_level) |partial_level| { if (partial_level != level) return self.reject(.unexpected_message); } for (bytes) |byte| { const input = self.storage.receiveBytes(); if (self.receive_length >= input.len) return self.reject(.decode_error); input[self.receive_length] = byte; self.receive_length += 1; if (self.receive_length == 1) self.receive_level = level; if (self.receive_length == 4) try self.readMessageLength(input[0..4]); if (self.receive_total == 0) continue; if (self.receive_length != self.receive_total) continue; try self.process(level, input[0..self.receive_total]); self.receive_length = 0; self.receive_total = 0; } if (self.receive_length == 0) self.receive_level = null; } pub fn emit(self: *Engine, level: model.Level, output: []u8) usize { if (self.pending_alert != null) return 0; const written = switch (level) { .initial => emitQueue( self.storage.initialBytes(), &self.initial_offset, self.initial_length, output, ), .handshake => emitQueue( self.storage.handshakeBytes(), &self.handshake_offset, self.handshake_length, output, ), .one_rtt => 0, }; if (self.phase == .client_sending_finished) { if (self.handshake_offset == self.handshake_length) self.phase = .client_complete; } return written; } /// Returns the handshake bytes the caller has already taken at one encryption level. The bytes /// stay in the engine's storage, so an endpoint that lost a CRYPTO frame retransmits from them, /// which RFC 9000 section 13.3 asks of a sender. The 1-RTT level gives an empty slice, because /// it carries no handshake bytes. The slice stays valid while the engine does, because it /// points into the engine's storage. pub fn emitted(self: *const Engine, level: model.Level) []const u8 { std.debug.assert(self.initial_offset <= self.initial_length); std.debug.assert(self.handshake_offset <= self.handshake_length); return switch (level) { .initial => self.storage.initialBytes()[0..self.initial_offset], .handshake => self.storage.handshakeBytes()[0..self.handshake_offset], .one_rtt => &.{}, }; } pub fn secret( self: *const Engine, level: model.Level, direction: model.Direction, ) ?model.Secret { if (self.pending_alert != null) return null; return switch (level) { .initial => null, .handshake => self.handshakeSecret(direction), .one_rtt => self.applicationSecret(direction), }; } pub fn state(self: *const Engine) model.Status { return switch (self.phase) { .client_complete => .handshake_complete, .client_confirmed, .server_confirmed => .handshake_confirmed, else => .handshaking, }; } pub fn confirm(self: *Engine) void { if (self.phase == .client_complete) { self.eraseHandshakeTrafficSecrets(); self.phase = .client_confirmed; } } pub fn peer(self: *const Engine) ?model.Peer { if (self.pending_alert != null) return null; if (!self.peer_verified) return null; const peer_bytes = self.storage.peerBytes(); return .{ .public_key = self.peer_key, .alpn = self.config.alpn, .transport_parameters = peer_bytes[0..self.peer_parameters_length], }; } pub fn alert(self: *const Engine) ?tls.Alert { return self.pending_alert; } pub fn cipherSuite(self: *const Engine) ?std.crypto.tls.CipherSuite { return self.cipher_suite; } fn readMessageLength(self: *Engine, header: []const u8) Error!void { std.debug.assert(header.len == 4); const body_length = (@as(usize, header[1]) << 16) | (@as(usize, header[2]) << 8) | header[3]; if (body_length > self.limits.max_message) { self.pending_alert = .decode_error; self.phase = .alerted; return error.MessageTooLarge; } self.receive_total = body_length + 4; } fn process(self: *Engine, level: model.Level, bytes: []const u8) Error!void { const value = tls.message.decode(bytes) catch return self.reject(.decode_error); if (value.kind == .key_update) return self.reject(.unexpected_message); if (value.kind == .new_session_ticket) { if (level != .one_rtt) return self.reject(.unexpected_message); if (self.config.role == .server) return self.reject(.unexpected_message); _ = tls.message.decodeNewSessionTicket(bytes) catch return self.reject(.decode_error); return; } if (self.config.role == .client) return self.processClient(level, bytes); return self.processServer(level, bytes); } fn processClient( self: *Engine, level: model.Level, bytes: []const u8, ) Error!void { return switch (self.phase) { .client_wait_server_hello => self.clientServerHello(level, bytes), .client_wait_encrypted_extensions => self.clientEncrypted(level, bytes), .client_wait_certificate_request => self.clientRequest(level, bytes), .client_wait_certificate => self.receiveCertificate(level, bytes, false), .client_wait_certificate_verify => self.receiveCertificateVerify(level, bytes, false), .client_wait_finished => self.clientFinished(level, bytes), else => self.reject(.unexpected_message), }; } fn processServer( self: *Engine, level: model.Level, bytes: []const u8, ) Error!void { return switch (self.phase) { .server_wait_client_hello => self.serverClientHello(level, bytes), .server_wait_certificate => self.receiveCertificate(level, bytes, true), .server_wait_certificate_verify => self.receiveCertificateVerify(level, bytes, true), .server_wait_finished => self.serverFinished(level, bytes), else => self.reject(.unexpected_message), }; } fn clientServerHello(self: *Engine, level: model.Level, bytes: []const u8) Error!void { if (level != .initial) return self.reject(.unexpected_message); const hello = tls.message.decodeServerHello(bytes) catch return self.reject(.decode_error); if (std.mem.eql(u8, &hello.random, &std.crypto.tls.hello_retry_request_sequence)) { return self.reject(.handshake_failure); } const suite = validServerHello(hello) orelse return self.reject(.illegal_parameter); const selected = extension.serverSelection(hello.extensions) catch |err| { return self.reject(extensionAlert(err)); }; if (!selected.tls_1_3) { if (hasDowngradeSentinel(hello.random)) return self.reject(.illegal_parameter); return self.reject(.protocol_version); } const peer_share = selected.key_share orelse return self.reject(.missing_extension); self.transcript.update(bytes); try self.installSchedule(peer_share); self.cipher_suite = suite; self.phase = .client_wait_encrypted_extensions; } fn clientEncrypted(self: *Engine, level: model.Level, bytes: []const u8) Error!void { if (level != .handshake) return self.reject(.unexpected_message); const encrypted = tls.message.decodeEncryptedExtensions(bytes) catch { return self.reject(.decode_error); }; const selected = extension.encryptedSelection( encrypted.extensions, self.config.alpn, self.config.server_name != null, ) catch |err| return self.reject(extensionAlert(err)); if (selected.alpn == null) return self.reject(.no_application_protocol); const parameters = selected.transport_parameters orelse { return self.reject(.missing_extension); }; if (!selected.server_raw_key) return self.reject(.unsupported_certificate); if (!selected.client_raw_key) return self.reject(.unsupported_certificate); try self.retainParameters(parameters); self.transcript.update(bytes); self.phase = .client_wait_certificate_request; } fn clientRequest(self: *Engine, level: model.Level, bytes: []const u8) Error!void { if (level != .handshake) return self.reject(.unexpected_message); const request = tls.message.decodeCertificateRequest(bytes) catch { return self.reject(.decode_error); }; if (request.context.len != 0) return self.reject(.illegal_parameter); const offered = extension.requestOffersEd25519(request.extensions) catch |err| { return self.reject(extensionAlert(err)); }; if (!offered) return self.reject(.handshake_failure); self.transcript.update(bytes); self.phase = .client_wait_certificate; } fn serverClientHello(self: *Engine, level: model.Level, bytes: []const u8) Error!void { if (level != .initial) return self.reject(.unexpected_message); const hello = tls.message.decodeClientHello(bytes) catch return self.reject(.decode_error); const suite = validClientHello(hello, self.config.cipher_suite) orelse return self.reject(.illegal_parameter); const offer = extension.clientOffer(hello.extensions, self.config.alpn) catch |err| { return self.reject(extensionAlert(err)); }; try self.validateOffer(offer); const peer_share = offer.key_share orelse return self.reject(.missing_extension); try self.retainParameters(offer.transport_parameters.?); self.transcript.update(bytes); try self.queueServerFlight(peer_share, offer.server_name != null, suite); self.cipher_suite = suite; self.phase = .server_wait_certificate; } fn validateOffer(self: *Engine, offer: extension.ClientOffer) Error!void { if (!offer.tls_1_3) return self.reject(.protocol_version); if (!offer.x25519_group) return self.reject(.handshake_failure); if (!offer.ed25519_signature) return self.reject(.handshake_failure); if (offer.alpn == null) return self.reject(.no_application_protocol); if (offer.transport_parameters == null) return self.reject(.missing_extension); if (!offer.server_raw_key) return self.reject(.unsupported_certificate); if (!offer.client_raw_key) return self.reject(.unsupported_certificate); if (self.config.server_name) |wanted| { const received = offer.server_name orelse return self.reject(.unrecognized_name); if (!std.mem.eql(u8, wanted, received)) return self.reject(.unrecognized_name); } } fn receiveCertificate( self: *Engine, level: model.Level, bytes: []const u8, client_certificate: bool, ) Error!void { if (level != .handshake) return self.reject(.unexpected_message); const certificate = tls.message.decodeCertificate(bytes) catch { return self.reject(.decode_error); }; if (certificate.request_context.len != 0) return self.reject(.illegal_parameter); var entries = certificate.entries.iterator(); const entry = entries.next() catch return self.reject(.decode_error); if (entry == null) { if (client_certificate) return self.reject(.certificate_required); return self.reject(.decode_error); } const second = entries.next() catch return self.reject(.decode_error); if (second != null) return self.reject(.unsupported_certificate); var entry_extensions = entry.?.extensions.iterator(); if ((entry_extensions.next() catch return self.reject(.decode_error)) != null) { return self.reject(.unsupported_extension); } self.peer_key = tls.decodeSubjectPublicKeyInfo(entry.?.data) catch { return self.reject(.bad_certificate); }; self.peer_key_present = true; if (self.config.expected_peer) |expected| { if (!std.crypto.timing_safe.eql(tls.PublicKey, expected, self.peer_key)) { return self.reject(.bad_certificate); } } self.transcript.update(bytes); self.phase = if (client_certificate) .server_wait_certificate_verify else .client_wait_certificate_verify; } fn receiveCertificateVerify( self: *Engine, level: model.Level, bytes: []const u8, client_signature: bool, ) Error!void { if (level != .handshake) return self.reject(.unexpected_message); if (!self.peer_key_present) return self.reject(.unexpected_message); const value = tls.message.decodeCertificateVerify(bytes) catch { return self.reject(.decode_error); }; if (value.algorithm != .ed25519) return self.reject(.illegal_parameter); if (value.signature.len != 64) return self.reject(.decrypt_error); const hash = self.transcriptHash(); const content = certificateVerifyContent(client_signature, hash); tls.verifySignature( self.peer_key, &content, value.signature[0..64].*, ) catch return self.reject(.decrypt_error); self.transcript.update(bytes); self.peer_verified = true; self.phase = if (client_signature) .server_wait_finished else .client_wait_finished; } fn clientFinished(self: *Engine, level: model.Level, bytes: []const u8) Error!void { if (level != .handshake) return self.reject(.unexpected_message); try self.verifyFinished(bytes, self.schedule.?.server_handshake); self.transcript.update(bytes); self.application = self.schedule.?.application(self.transcriptHash()); self.schedule.?.eraseMasterSecret(); try self.queueClientFlight(); self.phase = .client_sending_finished; } fn serverFinished(self: *Engine, level: model.Level, bytes: []const u8) Error!void { if (level != .handshake) return self.reject(.unexpected_message); try self.verifyFinished(bytes, self.schedule.?.client_handshake); self.transcript.update(bytes); self.eraseHandshakeTrafficSecrets(); self.phase = .server_confirmed; } fn verifyFinished( self: *Engine, bytes: []const u8, traffic_secret: model.Secret, ) Error!void { const value = tls.message.decodeFinished(bytes) catch return self.reject(.decode_error); if (value.verify_data.len != 32) return self.reject(.decrypt_error); const expected = tls.schedule.finishedVerifyData(traffic_secret, self.transcriptHash()); if (!std.crypto.timing_safe.eql(model.Secret, expected, value.verify_data[0..32].*)) { return self.reject(.decrypt_error); } } fn installSchedule(self: *Engine, peer_share: [32]u8) Error!void { var shared = X25519.scalarmult(self.key_pair.secret_key, peer_share) catch { std.crypto.secureZero(u8, &self.key_pair.secret_key); return self.reject(.illegal_parameter); }; defer std.crypto.secureZero(u8, &shared); std.crypto.secureZero(u8, &self.key_pair.secret_key); var installed = tls.schedule.Schedule.init(shared, self.transcriptHash()); installed.eraseDerivationSecrets(); self.schedule = installed; self.handshake_secrets_present = true; } fn queueClientHello( self: *Engine, random: [32]u8, output_bytes: []u8, ) InitError!void { var output = quic.cursor.Write.init(output_bytes); tls.message.profile.encodeClientHello(.{ .random = random, .key_share = self.key_pair.public_key, .alpn = self.config.alpn, .transport_parameters = self.config.transport_parameters, .server_name = self.config.server_name, .cipher_suite = self.config.cipher_suite, }, &output) catch return error.EncodingFailed; self.initial_length = output.index; self.transcript.update(output.written()); } fn queueServerFlight( self: *Engine, peer_share: [32]u8, acknowledge_name: bool, suite: std.crypto.tls.CipherSuite, ) Error!void { var initial = quic.cursor.Write.init(self.storage.initialBytes()); tls.message.profile.encodeServerHello(.{ .random = self.hello_random, .key_share = self.key_pair.public_key, .cipher_suite = suite, }, &initial) catch return self.reject(.internal_error); self.initial_length = initial.index; self.transcript.update(initial.written()); try self.installSchedule(peer_share); try self.queueServerAuthentication(acknowledge_name); } fn queueServerAuthentication(self: *Engine, acknowledge_name: bool) Error!void { var output = quic.cursor.Write.init(self.storage.handshakeBytes()); tls.message.profile.encodeEncryptedExtensions(.{ .alpn = self.config.alpn, .transport_parameters = self.config.transport_parameters, .acknowledge_server_name = acknowledge_name, }, &output) catch return self.reject(.internal_error); self.hashNew(output.written(), &self.handshake_length); tls.message.profile.encodeCertificateRequest(&output) catch { return self.reject(.internal_error); }; self.hashNew(output.written(), &self.handshake_length); tls.message.profile.encodeCertificate(self.config.identity.public(), &output) catch { return self.reject(.internal_error); }; self.hashNew(output.written(), &self.handshake_length); try self.queueSignedAndFinished(&output, false); self.application = self.schedule.?.application(self.transcriptHash()); self.schedule.?.eraseMasterSecret(); } fn queueClientFlight(self: *Engine) Error!void { var output = quic.cursor.Write.init(self.storage.handshakeBytes()); tls.message.profile.encodeCertificate( self.config.identity.public(), &output, ) catch return self.reject(.internal_error); self.hashNew(output.written(), &self.handshake_length); try self.queueSignedAndFinished(&output, true); } fn queueSignedAndFinished( self: *Engine, output: *quic.cursor.Write, client_signature: bool, ) Error!void { const hash = self.transcriptHash(); const content = certificateVerifyContent(client_signature, hash); const signature = self.config.identity.sign(&content) catch { return self.reject(.internal_error); }; tls.message.profile.encodeCertificateVerify(signature, output) catch { return self.reject(.internal_error); }; self.hashNew(output.written(), &self.handshake_length); const traffic = if (client_signature) self.schedule.?.client_handshake else self.schedule.?.server_handshake; const verify_data = tls.schedule.finishedVerifyData(traffic, self.transcriptHash()); tls.message.profile.encodeFinished(verify_data, output) catch { return self.reject(.internal_error); }; self.hashNew(output.written(), &self.handshake_length); } fn hashNew(self: *Engine, bytes: []const u8, previous_length: *usize) void { std.debug.assert(previous_length.* <= bytes.len); self.transcript.update(bytes[previous_length.*..]); previous_length.* = bytes.len; } fn retainParameters(self: *Engine, parameters: []const u8) Error!void { const output = self.storage.peerBytes(); if (parameters.len > output.len) return self.reject(.internal_error); @memcpy(output[0..parameters.len], parameters); self.peer_parameters_length = parameters.len; } fn transcriptHash(self: *const Engine) model.Secret { var transcript = self.transcript; var digest: model.Secret = undefined; transcript.final(&digest); return digest; } fn handshakeSecret(self: *const Engine, direction: model.Direction) ?model.Secret { if (!self.handshake_secrets_present) return null; const value = self.schedule orelse return null; return switch (self.config.role) { .client => if (direction == .write) value.client_handshake else value.server_handshake, .server => if (direction == .write) value.server_handshake else value.client_handshake, }; } fn applicationSecret(self: *const Engine, direction: model.Direction) ?model.Secret { if (self.config.role == .server and direction == .read) { if (self.phase != .server_confirmed) return null; } const value = self.application orelse return null; return switch (self.config.role) { .client => if (direction == .write) value.client else value.server, .server => if (direction == .write) value.server else value.client, }; } fn eraseHandshakeTrafficSecrets(self: *Engine) void { if (!self.handshake_secrets_present) return; self.schedule.?.eraseHandshakeTrafficSecrets(); self.handshake_secrets_present = false; } fn reject(self: *Engine, alert_value: tls.Alert) Error { self.pending_alert = alert_value; self.phase = .alerted; return error.TlsAlert; }};Source: lib/quic/src/tls/engine/machine.zig:11
pub const Error = error{ TlsAlert, MessageTooLarge };Source: lib/quic/src/tls/engine/model.zig:39
pub const Config = struct { role: Role, identity: tls.Identity, expected_peer: ?tls.PublicKey, alpn: []const u8, transport_parameters: []const u8, server_name: ?[]const u8, random: Random, cipher_suite: std.crypto.tls.CipherSuite = .AES_128_GCM_SHA256,};Source: lib/quic/src/tls/engine/model.zig:6
pub const Direction = enum { read, write };Source: lib/quic/src/tls/engine/model.zig:9
pub const FixedRandom = struct { hello: [32]u8, key_exchange: [32]u8,};Source: lib/quic/src/tls/engine/model.zig:5
pub const Level = enum { initial, handshake, one_rtt };Source: lib/quic/src/tls/engine/model.zig:61
pub const Peer = struct { public_key: tls.PublicKey, alpn: []const u8, transport_parameters: []const u8,};Source: lib/quic/src/tls/engine/model.zig:14
pub const Random = union(enum) { secure: std.Io, fixed_for_testing: FixedRandom, /// Builds a random source whose bytes the caller fixed in advance. The engine takes the hello /// random and the key exchange secret from it, so a handshake built this way repeats exactly. A /// test uses this form, and the other form of the source takes its bytes from the caller's I/O /// interface through a secure-random call. pub fn testing(value: FixedRandom) Random { return .{ .fixed_for_testing = value }; } pub fn values(self: Random) error{RandomFailed}!FixedRandom { return switch (self) { .fixed_for_testing => |value| value, .secure => |io| blk: { var value: FixedRandom = undefined; io.randomSecure(&value.hello) catch return error.RandomFailed; io.randomSecure(&value.key_exchange) catch return error.RandomFailed; break :blk value; }, }; }};Source: lib/quic/src/tls/engine/model.zig:4
pub const Role = enum { client, server };Source: lib/quic/src/tls/engine/model.zig:55
/// How far the handshake has reached: still running, complete, or confirmed. A client passes/// through completion first and reaches confirmation when the caller confirms it, which also erases/// the handshake traffic secrets. A server reports confirmation directly, once the client's/// Finished message verifies. A caller therefore treats a server's confirmation as both completion/// and confirmation.pub const Status = enum { handshaking, handshake_complete, handshake_confirmed,};Source: lib/quic/src/tls/engine/storage.zig:11
pub const Storage = struct { pub const storage_alignment: usize = 16; pub const Storage = []align(storage_alignment) u8; pub const Limits = struct { max_message: u16 }; pub const Capacity = struct { message_bytes: usize, receive_bytes: usize, initial_bytes: usize, handshake_bytes: usize, peer_bytes: usize, storage_bytes: usize, pub const DeriveError = error{ MessageLimitEmpty, CapacityOverflow }; pub fn derive(limits: Limits) DeriveError!Capacity { if (limits.max_message == 0) return error.MessageLimitEmpty; const message_bytes = std.math.add( usize, limits.max_message, 4, ) catch return error.CapacityOverflow; const handshake_bytes = std.math.mul( usize, message_bytes, server_flight_messages_max, ) catch return error.CapacityOverflow; const message_regions = std.math.mul( usize, message_bytes, 7, ) catch return error.CapacityOverflow; const storage_bytes = std.math.add( usize, message_regions, limits.max_message, ) catch return error.CapacityOverflow; return .{ .message_bytes = message_bytes, .receive_bytes = message_bytes, .initial_bytes = message_bytes, .handshake_bytes = handshake_bytes, .peer_bytes = limits.max_message, .storage_bytes = storage_bytes, }; } }; pub const InitError = Capacity.DeriveError || error{StorageTooShort}; pub const work_limits: alloc_phase.capacity.WorkLimits = .{ .transition_steps_max = 1, .cleanup_steps_per_call_max = 0, .cleanup_calls_at_capacity_max = 0, }; pub const claim: alloc_phase.capacity.Declaration = .{ .source = .{ .id = "quic.tls_storage", .kind = .phase_static, .limit_source = .caller, .storage = .{ .covered = &.{ .{ .id = "handshake_input_output_and_peer_bytes", .lifetime = .steady, .detail = "one receive message, two flights, and peer parameters", }, }, .excluded = &.{ "caller configuration and identity key storage", "QUIC CRYPTO reassembly and packet protection storage", "cryptographic primitive stack storage and external I/O state", }, }, .capacity = .{ .inputs = &.{ alloc_phase.capacity.bindInput(Limits, "max_message", "max_message"), }, .type_selectors = &.{}, .nodes = &.{ .{ .input = 0 }, .{ .constant = 4 }, .{ .add = .{ .left = 0, .right = 1 } }, .{ .scale = .{ .node = 2, .coefficient = .{ .literal = 7 } }, }, .{ .add = .{ .left = 3, .right = 0 } }, }, .assertions = &.{.{ .scope = .closure_total, .measure = .retained, .relation = .exact, .expression = 4, }}, }, .overload = .{ .kind = .reject_before_seal, .detail = "empty limits and short storage reject before activation", }, .risks = .{ .transitive = .{ .status = .witnessed, .detail = "message parsing and emission use caller storage only", }, .foreign = .{ .status = .excluded, .detail = "standard cryptography retains no dynamic engine storage", }, }, .work = .{ .equation = "initialization to handshake transition steps <= 1" }, .obligations = &.{ .{ .key = "quic_tls_capacity", .role = .capacity_model }, .{ .key = "quic_tls_boundary", .role = .overload }, .{ .key = "quic_tls_transitive", .role = .transitive_risk }, .{ .key = "quic_tls_foreign", .role = .foreign_risk }, .{ .key = "quic_tls_work", .role = .work_bound }, .{ .key = "quic_tls_root", .role = .custom }, }, }, .bindings = .{ .owner = @This(), .seal = .{ .family = alloc_phase.capacity.selector(@This().activate), .premise = .{ .class = .checked_semantic_fact, .authority = .checker, }, }, .teardown = .{ .family = alloc_phase.capacity.selector(@This().deinit), .premise = .{ .class = .checked_semantic_fact, .authority = .checker, }, }, }, }; phase: alloc_phase.capacity.Phase, capacity: Capacity, storage: []align(storage_alignment) u8, pub fn init(bytes: @This().Storage, limits: Limits) InitError!@This() { const capacity = try Capacity.derive(limits); if (bytes.len < capacity.storage_bytes) return error.StorageTooShort; return .{ .phase = .initialization, .capacity = capacity, .storage = bytes[0..capacity.storage_bytes], }; } pub fn activate(self: *@This()) void { std.debug.assert(self.phase == .initialization); self.phase = .steady; } pub fn deinit(self: *@This()) @This().Storage { std.debug.assert(self.phase == .steady); std.crypto.secureZero(u8, self.storage); self.phase = .teardown; const bytes = self.storage; self.* = undefined; return bytes; } /// A client engine writes its ClientHello into the Initial flight region during startup, before /// the storage is sealed into its steady phase. The ordinary accessor requires the steady /// phase, so this function returns that region while the storage is still in its initialization /// phase. The call returns the same bytes the steady accessor later returns. pub fn initialBytesForInitialization(self: *@This()) []u8 { std.debug.assert(self.phase == .initialization); const start = self.capacity.receive_bytes; return self.storage[start..][0..self.capacity.initial_bytes]; } pub fn receiveBytes(self: *@This()) []u8 { self.assertSteady(); return self.storage[0..self.capacity.receive_bytes]; } pub fn initialBytes(self: *@This()) []u8 { self.assertSteady(); const start = self.capacity.receive_bytes; return self.storage[start..][0..self.capacity.initial_bytes]; } pub fn handshakeBytes(self: *@This()) []u8 { self.assertSteady(); const start = self.capacity.receive_bytes + self.capacity.initial_bytes; return self.storage[start..][0..self.capacity.handshake_bytes]; } pub fn peerBytes(self: *@This()) []u8 { self.assertSteady(); const start = self.capacity.receive_bytes + self.capacity.initial_bytes + self.capacity.handshake_bytes; return self.storage[start..][0..self.capacity.peer_bytes]; } fn assertSteady(self: *const @This()) void { std.debug.assert(self.phase == .steady); std.debug.assert(self.storage.len >= self.capacity.storage_bytes); }};Source: lib/quic/src/tls/identity.zig:12
pub const Identity = struct { key_pair: Ed25519.KeyPair, pub fn fromSeed(seed: [Ed25519.KeyPair.seed_length]u8) error{InvalidSeed}!Identity { const key_pair = Ed25519.KeyPair.generateDeterministic(seed) catch { return error.InvalidSeed; }; return .{ .key_pair = key_pair }; } pub fn public(self: Identity) PublicKey { return self.key_pair.public_key.toBytes(); } pub fn sign(self: Identity, content: []const u8) error{SigningFailed}![64]u8 { const signature = self.key_pair.sign(content, null) catch { return error.SigningFailed; }; return signature.toBytes(); }};Source: lib/quic/src/tls/engine/machine.zig:19
Source: lib/quic/src/tls/identity.zig:5
pub const PublicKey = [Ed25519.PublicKey.encoded_length]u8;Source: lib/quic/src/tls/identity.zig:47
pub fn decodeSubjectPublicKeyInfo( encoded: []const u8,) error{ InvalidLength, InvalidPrefix }!PublicKey { if (encoded.len != spki_length) return error.InvalidLength; if (!std.mem.eql(u8, encoded[0..spki_prefix.len], &spki_prefix)) { return error.InvalidPrefix; } return encoded[spki_prefix.len..][0..@sizeOf(PublicKey)].*;}Source: lib/quic/src/tls/identity.zig:40
pub fn encodeSubjectPublicKeyInfo(public_key: PublicKey) [spki_length]u8 { var encoded: [spki_length]u8 = undefined; encoded[0..spki_prefix.len].* = spki_prefix; encoded[spki_prefix.len..].* = public_key; return encoded;}Source: lib/quic/src/tls/identity.zig:6
pub const spki_length: usize = 44;Source: lib/quic/src/tls/identity.zig:34
pub fn verify(public_key: PublicKey, content: []const u8, signature: [64]u8) !void { const parsed_key = try Ed25519.PublicKey.fromBytes(public_key); const parsed_signature = Ed25519.Signature.fromBytes(signature); try parsed_signature.verify(content, parsed_key);}Source: lib/quic/src/tls/engine/model.zig:7
pub const Secret = tls.schedule.Secret;Source: lib/quic/src/root.zig:50
pub const tls = @import("tls/root.zig");Source: lib/quic/src/tls/root.zig
//! The TLS 1.3 handshake a QUIC connection needs runs as a state machine over bytes the caller//! carries.//!//! Before any application data moves, the two endpoints have to agree on a shared secret, each//! prove who it is, and exchange the settings that govern the connection. Handshake messages travel//! inside the transport's own frames. The keys a handshake message travels under are selected by an//! *encryption level*: one of Initial, Handshake, and 1-RTT, so the handshake has to hand bytes out//! and take bytes in one level at a time.//!//! A message can arrive split across more than one delivery, so partial messages have to be held//! until they are whole. The frames carrying handshake bytes can be lost, so the bytes already//! handed out have to stay available to be sent again. The secrets derived along the way stop being//! needed at known points, and leaving them in memory past that point widens what a later//! compromise reaches.//!//! RFC 8446 defines TLS 1.3, and this code follows its message layouts, its key schedule from the//! shared secret to the application *traffic secret* (the 32 bytes from which one direction's//! packet keys are derived), and its Finished verification. RFC 9001 section 4 defines how TLS//! meets QUIC, and this code follows it by carrying handshake bytes per encryption level and//! turning a TLS alert into a transport error code. The values two endpoints exchange inside the//! TLS handshake are the *transport parameters*, and an extension carries them. RFC 7250 and RFC//! 8410 define raw public keys and the Ed25519 key encoding, and identities here are Ed25519 public//! keys in that 44-byte form.//!//! The accepted profile is narrow, taking X25519 for the key exchange, Ed25519 raw public keys for//! identity, and one of two cipher suites. Both endpoints authenticate, because the server asks the//! client for a certificate and rejects a client that omits it. A session ticket is parsed and//! dropped, so each handshake starts fresh. A TLS KeyUpdate message is rejected, because packet key//! updates are the transport's own business. The engine reaches for no allocator, so the caller//! derives a byte count from one maximum message size and hands over the block the engine//! partitions.const alert_mod = @import("alert.zig");const engine_mod = @import("engine/root.zig");const identity_mod = @import("identity.zig");pub const message = @import("message/root.zig");pub const schedule = @import("schedule.zig");pub const Alert = alert_mod.Alert;pub const PublicKey = identity_mod.PublicKey;pub const Identity = identity_mod.Identity;pub const spki_length = identity_mod.spki_length;pub const encodeSubjectPublicKeyInfo = identity_mod.encodeSubjectPublicKeyInfo;pub const decodeSubjectPublicKeyInfo = identity_mod.decodeSubjectPublicKeyInfo;pub const verifySignature = identity_mod.verify;pub const Role = engine_mod.Role;pub const Level = engine_mod.Level;pub const Direction = engine_mod.Direction;pub const Limits = engine_mod.Limits;pub const Capacity = engine_mod.Capacity;pub const Storage = engine_mod.Storage;pub const Config = engine_mod.Config;pub const FixedRandom = engine_mod.FixedRandom;pub const Random = engine_mod.Random;pub const Secret = engine_mod.Secret;pub const Status = engine_mod.Status;pub const Peer = engine_mod.Peer;pub const Engine = engine_mod.Engine;pub const Error = engine_mod.Error;pub const InitError = engine_mod.InitError;Audit
| Definitions | 55 |
|---|---|
| Public names | 59 |
| Members | 77 |
| Version | 26.7.0 |
| Revision | daab053ee433 |