diff --git a/libs/iguanatls/bench/bench.zig b/libs/iguanatls/bench/bench.zig new file mode 100644 index 0000000000000000000000000000000000000000..77dd2ebd945cfcd9754239e3c8122623f9784077 --- /dev/null +++ b/libs/iguanatls/bench/bench.zig @@ -0,0 +1,425 @@ +const std = @import("std"); +const tls = @import("tls"); +const use_gpa = @import("build_options").use_gpa; + +pub const log_level = .debug; + +const RecordingAllocator = struct { + const Stats = struct { + peak_allocated: usize = 0, + total_allocated: usize = 0, + total_deallocated: usize = 0, + total_allocations: usize = 0, + }; + + allocator: std.mem.Allocator = .{ + .allocFn = allocFn, + .resizeFn = resizeFn, + }, + base_allocator: *std.mem.Allocator, + stats: Stats = .{}, + + fn allocFn( + a: *std.mem.Allocator, + len: usize, + ptr_align: u29, + len_align: u29, + ret_addr: usize, + ) ![]u8 { + const self = @fieldParentPtr(RecordingAllocator, "allocator", a); + const mem = try self.base_allocator.allocFn( + self.base_allocator, + len, + ptr_align, + len_align, + ret_addr, + ); + + self.stats.total_allocations += 1; + self.stats.total_allocated += mem.len; + self.stats.peak_allocated = std.math.max( + self.stats.peak_allocated, + self.stats.total_allocated - self.stats.total_deallocated, + ); + return mem; + } + + fn resizeFn(a: *std.mem.Allocator, buf: []u8, buf_align: u29, new_len: usize, len_align: u29, ret_addr: usize) !usize { + const self = @fieldParentPtr(RecordingAllocator, "allocator", a); + const actual_len = try self.base_allocator.resizeFn( + self.base_allocator, + buf, + buf_align, + new_len, + len_align, + ret_addr, + ); + + if (actual_len == 0) { + std.debug.assert(new_len == 0); + self.stats.total_deallocated += buf.len; + } else if (actual_len > buf.len) { + self.stats.total_allocated += actual_len - buf.len; + self.stats.peak_allocated = std.math.max( + self.stats.peak_allocated, + self.stats.total_allocated - self.stats.total_deallocated, + ); + } else { + self.stats.total_deallocated += buf.len - actual_len; + } + return actual_len; + } +}; + +const SinkWriter = blk: { + const S = struct {}; + break :blk std.io.Writer(S, error{}, struct { + fn f(_: S, buffer: []const u8) !usize { + return buffer.len; + } + }.f); +}; + +const ReplayingReaderState = struct { + data: []const u8, +}; +const ReplayingReader = std.io.Reader(*ReplayingReaderState, error{}, struct { + fn f(self: *ReplayingReaderState, buffer: []u8) !usize { + if (self.data.len < buffer.len) + @panic("Not enoguh reader data!"); + std.mem.copy(u8, buffer, self.data[0..buffer.len]); + self.data = self.data[buffer.len..]; + return buffer.len; + } +}.f); + +const ReplayingRandom = struct { + rand: std.rand.Random = .{ .fillFn = fillFn }, + data: []const u8, + + fn fillFn(r: *std.rand.Random, buf: []u8) void { + const self = @fieldParentPtr(ReplayingRandom, "rand", r); + if (self.data.len < buf.len) + @panic("Not enough random data!"); + std.mem.copy(u8, buf, self.data[0..buf.len]); + self.data = self.data[buf.len..]; + } +}; + +fn benchmark_run( + comptime ciphersuites: anytype, + comptime curves: anytype, + gpa: *std.mem.Allocator, + allocator: *std.mem.Allocator, + running_time: f32, + hostname: []const u8, + port: u16, + trust_anchors: tls.x509.TrustAnchorChain, + reader_recording: []const u8, + random_recording: []const u8, +) !void { + { + const warmup_time_secs = std.math.max(0.5, running_time / 20); + std.debug.print("Warming up for {d:.2} seconds...\n", .{warmup_time_secs}); + const warmup_time_ns = @floatToInt(i128, warmup_time_secs * std.time.ns_per_s); + + var warmup_time_passed: i128 = 0; + var timer = try std.time.Timer.start(); + while (warmup_time_passed < warmup_time_ns) { + var rand = ReplayingRandom{ + .data = random_recording, + }; + var reader_state = ReplayingReaderState{ + .data = reader_recording, + }; + const reader = ReplayingReader{ .context = &reader_state }; + const writer = SinkWriter{ .context = .{} }; + + timer.reset(); + _ = try tls.client_connect(.{ + .rand = &rand.rand, + .reader = reader, + .writer = writer, + .ciphersuites = ciphersuites, + .curves = curves, + .cert_verifier = .default, + .temp_allocator = allocator, + .trusted_certificates = trust_anchors.data.items, + }, hostname); + warmup_time_passed += timer.read(); + } + } + { + std.debug.print("Benchmarking for {d:.2} seconds...\n", .{running_time}); + + const RunRecording = struct { + time: i128, + mem_stats: RecordingAllocator.Stats, + }; + var run_recordings = std.ArrayList(RunRecording).init(gpa); + + defer run_recordings.deinit(); + const bench_time_ns = @floatToInt(i128, running_time * std.time.ns_per_s); + + var total_time_passed: i128 = 0; + var iterations: usize = 0; + var timer = try std.time.Timer.start(); + while (total_time_passed < bench_time_ns) : (iterations += 1) { + var rand = ReplayingRandom{ + .data = random_recording, + }; + var reader_state = ReplayingReaderState{ + .data = reader_recording, + }; + const reader = ReplayingReader{ .context = &reader_state }; + const writer = SinkWriter{ .context = .{} }; + var recording_allocator = RecordingAllocator{ .base_allocator = allocator }; + + timer.reset(); + _ = try tls.client_connect(.{ + .rand = &rand.rand, + .reader = reader, + .writer = writer, + .ciphersuites = ciphersuites, + .curves = curves, + .cert_verifier = .default, + .temp_allocator = &recording_allocator.allocator, + .trusted_certificates = trust_anchors.data.items, + }, hostname); + const runtime = timer.read(); + total_time_passed += runtime; + + (try run_recordings.addOne()).* = .{ + .mem_stats = recording_allocator.stats, + .time = runtime, + }; + } + + const total_time_secs = @intToFloat(f64, total_time_passed) / std.time.ns_per_s; + const mean_time_ns = @divTrunc(total_time_passed, iterations); + const mean_time_ms = @intToFloat(f64, mean_time_ns) * std.time.ms_per_s / std.time.ns_per_s; + + const std_dev_ns = blk: { + var acc: i128 = 0; + for (run_recordings.items) |rec| { + const dt = rec.time - mean_time_ns; + acc += dt * dt; + } + break :blk std.math.sqrt(@divTrunc(acc, iterations)); + }; + const std_dev_ms = @intToFloat(f64, std_dev_ns) * std.time.ms_per_s / std.time.ns_per_s; + + std.debug.print( + \\Finished benchmarking. + \\Total runtime: {d:.2} sec + \\Iterations: {} ({d:.2} iterations/sec) + \\Mean iteration time: {d:.2} ms + \\Standard deviation: {d:.2} ms + \\ + , .{ + total_time_secs, + iterations, + @intToFloat(f64, iterations) / total_time_secs, + mean_time_ms, + std_dev_ms, + }); + + // (percentile/100) * (total number n + 1) + std.sort.sort(RunRecording, run_recordings.items, {}, struct { + fn f(_: void, lhs: RunRecording, rhs: RunRecording) bool { + return lhs.time < rhs.time; + } + }.f); + const percentiles = .{ 99.0, 90.0, 75.0, 50.0 }; + inline for (percentiles) |percentile| { + if (percentile < iterations) { + const idx = @floatToInt(usize, @intToFloat(f64, iterations + 1) * percentile / 100.0); + std.debug.print( + "{d:.0}th percentile value: {d:.2} ms\n", + .{ + percentile, + @intToFloat(f64, run_recordings.items[idx].time) * std.time.ms_per_s / std.time.ns_per_s, + }, + ); + } + } + + const first_mem_stats = run_recordings.items[0].mem_stats; + for (run_recordings.items[1..]) |rec| { + std.debug.assert(std.meta.eql(first_mem_stats, rec.mem_stats)); + } + + std.debug.print( + \\Peak allocated memory: {Bi:.2}, + \\Total allocated memory: {Bi:.2}, + \\Number of allocations: {d}, + \\ + , .{ + first_mem_stats.peak_allocated, + first_mem_stats.total_allocated, + first_mem_stats.total_allocations, + }); + } +} + +fn benchmark_run_with_ciphersuite( + comptime ciphersuites: anytype, + curve_str: []const u8, + gpa: *std.mem.Allocator, + allocator: *std.mem.Allocator, + running_time: f32, + hostname: []const u8, + port: u16, + trust_anchors: tls.x509.TrustAnchorChain, + reader_recording: []const u8, + random_recording: []const u8, +) !void { + if (std.mem.eql(u8, curve_str, "all")) { + return try benchmark_run( + ciphersuites, + tls.curves.all, + gpa, + allocator, + running_time, + hostname, + port, + trust_anchors, + reader_recording, + random_recording, + ); + } + inline for (tls.curves.all) |curve| { + if (std.mem.eql(u8, curve_str, curve.name)) { + return try benchmark_run( + ciphersuites, + .{curve}, + gpa, + allocator, + running_time, + hostname, + port, + trust_anchors, + reader_recording, + random_recording, + ); + } + } + return error.InvalidCurve; +} + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = &gpa.allocator; + + var args = std.process.args(); + std.debug.assert(args.skip()); + + const running_time = blk: { + const maybe_arg = args.next(allocator) orelse return error.NoArguments; + const arg = try maybe_arg; + break :blk std.fmt.parseFloat(f32, arg) catch { + std.log.crit("Running time is not a floating point number...", .{}); + return error.InvalidArg; + }; + }; + + // Loop over all files, swap gpa with a fixed buffer allocator for the handhsake + arg_loop: while (args.next(allocator)) |recorded_file_path_or_err| { + const recorded_file_path = try recorded_file_path_or_err; + defer allocator.free(recorded_file_path); + + std.debug.print( + \\============================================================ + \\{s} + \\============================================================ + \\ + , .{std.fs.path.basename(recorded_file_path)}); + + const recorded_file = try std.fs.cwd().openFile(recorded_file_path, .{}); + defer recorded_file.close(); + + const ciphersuite_str_len = try recorded_file.reader().readByte(); + const ciphersuite_str = try allocator.alloc(u8, ciphersuite_str_len); + defer allocator.free(ciphersuite_str); + try recorded_file.reader().readNoEof(ciphersuite_str); + + const curve_str_len = try recorded_file.reader().readByte(); + const curve_str = try allocator.alloc(u8, curve_str_len); + defer allocator.free(curve_str); + try recorded_file.reader().readNoEof(curve_str); + + const hostname_len = try recorded_file.reader().readIntLittle(usize); + const hostname = try allocator.alloc(u8, hostname_len); + defer allocator.free(hostname); + try recorded_file.reader().readNoEof(hostname); + + const port = try recorded_file.reader().readIntLittle(u16); + + const trust_anchors = blk: { + const pem_file_path_len = try recorded_file.reader().readIntLittle(usize); + const pem_file_path = try allocator.alloc(u8, pem_file_path_len); + defer allocator.free(pem_file_path); + try recorded_file.reader().readNoEof(pem_file_path); + + const pem_file = try std.fs.cwd().openFile(pem_file_path, .{}); + defer pem_file.close(); + + const tas = try tls.x509.TrustAnchorChain.from_pem(allocator, pem_file.reader()); + std.debug.print("Read {} certificates.\n", .{tas.data.items.len}); + break :blk tas; + }; + defer trust_anchors.deinit(); + + const reader_recording_len = try recorded_file.reader().readIntLittle(usize); + const reader_recording = try allocator.alloc(u8, reader_recording_len); + defer allocator.free(reader_recording); + try recorded_file.reader().readNoEof(reader_recording); + + const random_recording_len = try recorded_file.reader().readIntLittle(usize); + const random_recording = try allocator.alloc(u8, random_recording_len); + defer allocator.free(random_recording); + try recorded_file.reader().readNoEof(random_recording); + + const handshake_allocator = if (use_gpa) + &gpa.allocator + else + &std.heap.ArenaAllocator.init(std.heap.page_allocator).allocator; + + defer if (!use_gpa) + @fieldParentPtr(std.heap.ArenaAllocator, "allocator", handshake_allocator).deinit(); + + if (std.mem.eql(u8, ciphersuite_str, "all")) { + try benchmark_run_with_ciphersuite( + tls.ciphersuites.all, + curve_str, + allocator, + handshake_allocator, + running_time, + hostname, + port, + trust_anchors, + reader_recording, + random_recording, + ); + continue :arg_loop; + } + inline for (tls.ciphersuites.all) |ciphersuite| { + if (std.mem.eql(u8, ciphersuite_str, ciphersuite.name)) { + try benchmark_run_with_ciphersuite( + .{ciphersuite}, + curve_str, + allocator, + handshake_allocator, + running_time, + hostname, + port, + trust_anchors, + reader_recording, + random_recording, + ); + continue :arg_loop; + } + } + return error.InvalidCiphersuite; + } +} diff --git a/libs/iguanatls/bench/build.zig b/libs/iguanatls/bench/build.zig new file mode 100644 index 0000000000000000000000000000000000000000..ab36f63ff0ea3af0947cb30c3303656cf3a734ba --- /dev/null +++ b/libs/iguanatls/bench/build.zig @@ -0,0 +1,33 @@ +const Builder = @import("std").build.Builder; + +pub fn build(b: *Builder) void { + const record_build = b.addExecutable("record_handshake", "record_handshake.zig"); + record_build.addPackagePath("tls", "../src/main.zig"); + record_build.setBuildMode(.Debug); + record_build.install(); + + const use_gpa = b.option( + bool, + "use-gpa", + "Use the general purpose allocator instead of an arena allocator", + ) orelse false; + const bench_build = b.addExecutable("bench", "bench.zig"); + bench_build.addPackagePath("tls", "../src/main.zig"); + bench_build.setBuildMode(.ReleaseFast); + bench_build.addBuildOption(bool, "use_gpa", use_gpa); + bench_build.install(); + + const record_run_cmd = record_build.run(); + const bench_run_cmd = bench_build.run(); + record_run_cmd.step.dependOn(b.getInstallStep()); + bench_run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + record_run_cmd.addArgs(args); + bench_run_cmd.addArgs(args); + } + + const record_run_step = b.step("record-handshake", "Record a TLS handshake"); + const bench_run_step = b.step("bench", "Run the benchmark"); + record_run_step.dependOn(&record_run_cmd.step); + bench_run_step.dependOn(&bench_run_cmd.step); +} diff --git a/libs/iguanatls/bench/record_handshake.zig b/libs/iguanatls/bench/record_handshake.zig new file mode 100644 index 0000000000000000000000000000000000000000..4d275fa5b8f55c9fdcb41d4ff0a04c21669fe382 --- /dev/null +++ b/libs/iguanatls/bench/record_handshake.zig @@ -0,0 +1,249 @@ +const std = @import("std"); +const tls = @import("tls"); + +const RecordingRandom = struct { + rand: std.rand.Random = .{ + .fillFn = fillFn, + }, + base: *std.rand.Random, + recorded: std.ArrayList(u8), + + fn fillFn(r: *std.rand.Random, buf: []u8) void { + const self = @fieldParentPtr(@This(), "rand", r); + self.base.bytes(buf); + self.recorded.writer().writeAll(buf) catch unreachable; + } +}; + +fn RecordingReaderState(comptime Base: type) type { + return struct { + base: Base, + recorded: std.ArrayList(u8), + + fn read(self: *@This(), buffer: []u8) !usize { + var read_bytes = try self.base.read(buffer); + if (read_bytes != 0) { + try self.recorded.writer().writeAll(buffer[0..read_bytes]); + } + return read_bytes; + } + }; +} + +fn RecordingReader(comptime Base: type) type { + return std.io.Reader( + *RecordingReaderState(Base), + Base.Error || error{OutOfMemory}, + RecordingReaderState(Base).read, + ); +} + +fn record_handshake( + comptime ciphersuites: anytype, + comptime curves: anytype, + allocator: *std.mem.Allocator, + out_name: []const u8, + hostname: []const u8, + port: u16, + pem_file_path: []const u8, +) !void { + // Read PEM file + const pem_file = try std.fs.cwd().openFile(pem_file_path, .{}); + defer pem_file.close(); + + const trust_anchors = try tls.x509.TrustAnchorChain.from_pem(allocator, pem_file.reader()); + defer trust_anchors.deinit(); + std.log.info("Read {} certificates.", .{trust_anchors.data.items.len}); + + const sock = try std.net.tcpConnectToHost(allocator, hostname, port); + defer sock.close(); + + var recording_reader_state = RecordingReaderState(@TypeOf(sock).Reader){ + .base = sock.reader(), + .recorded = std.ArrayList(u8).init(allocator), + }; + defer recording_reader_state.recorded.deinit(); + + var recording_random = RecordingRandom{ + .base = std.crypto.random, + .recorded = std.ArrayList(u8).init(allocator), + }; + defer recording_random.recorded.deinit(); + + const reader = RecordingReader(@TypeOf(sock).Reader){ + .context = &recording_reader_state, + }; + std.log.info("Recording session `{s}`...", .{out_name}); + var client = try tls.client_connect(.{ + .rand = &recording_random.rand, + .reader = reader, + .writer = sock.writer(), + .ciphersuites = ciphersuites, + .curves = curves, + .cert_verifier = .default, + .temp_allocator = allocator, + .trusted_certificates = trust_anchors.data.items, + }, hostname); + defer client.close_notify() catch {}; + + const out_file = try std.fs.cwd().createFile(out_name, .{}); + defer out_file.close(); + + if (ciphersuites.len > 1) { + try out_file.writeAll(&[_]u8{ 0x3, 'a', 'l', 'l' }); + } else { + try out_file.writer().writeIntLittle(u8, ciphersuites[0].name.len); + try out_file.writeAll(ciphersuites[0].name); + } + if (curves.len > 1) { + try out_file.writeAll(&[_]u8{ 0x3, 'a', 'l', 'l' }); + } else { + try out_file.writer().writeIntLittle(u8, curves[0].name.len); + try out_file.writeAll(curves[0].name); + } + try out_file.writer().writeIntLittle(usize, hostname.len); + try out_file.writeAll(hostname); + try out_file.writer().writeIntLittle(u16, port); + try out_file.writer().writeIntLittle(usize, pem_file_path.len); + try out_file.writeAll(pem_file_path); + try out_file.writer().writeIntLittle(usize, recording_reader_state.recorded.items.len); + try out_file.writeAll(recording_reader_state.recorded.items); + try out_file.writer().writeIntLittle(usize, recording_random.recorded.items.len); + try out_file.writeAll(recording_random.recorded.items); + std.log.info("Session recorded.\n", .{}); +} + +fn record_with_ciphersuite( + comptime ciphersuites: anytype, + allocator: *std.mem.Allocator, + out_name: []const u8, + curve_str: []const u8, + hostname: []const u8, + port: u16, + pem_file_path: []const u8, +) !void { + if (std.mem.eql(u8, curve_str, "all")) { + return try record_handshake( + ciphersuites, + tls.curves.all, + allocator, + out_name, + hostname, + port, + pem_file_path, + ); + } + inline for (tls.curves.all) |curve| { + if (std.mem.eql(u8, curve_str, curve.name)) { + return try record_handshake( + ciphersuites, + .{curve}, + allocator, + out_name, + hostname, + port, + pem_file_path, + ); + } + } + std.log.crit("Invalid curve `{s}`", .{curve_str}); + std.debug.warn("Available options:\n- all\n", .{}); + inline for (tls.curves.all) |curve| { + std.debug.warn("- {s}\n", .{curve.name}); + } + return error.InvalidArg; +} + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +pub fn main() !void { + const allocator = &gpa.allocator; + + var args = std.process.args(); + std.debug.assert(args.skip()); + + const pem_file_path = try (args.next(allocator) orelse { + std.log.crit("Need PEM file path as first argument", .{}); + return error.NotEnoughArgs; + }); + defer allocator.free(pem_file_path); + + const ciphersuite_str = try (args.next(allocator) orelse { + std.log.crit("Need ciphersuite as second argument", .{}); + return error.NotEnoughArgs; + }); + defer allocator.free(ciphersuite_str); + + const curve_str = try (args.next(allocator) orelse { + std.log.crit("Need curve as third argument", .{}); + return error.NotEnoughArgs; + }); + defer allocator.free(curve_str); + + const hostname_port = try (args.next(allocator) orelse { + std.log.crit("Need hostname:port as fourth argument", .{}); + return error.NotEnoughArgs; + }); + defer allocator.free(hostname_port); + + if (args.skip()) { + std.log.crit("Need exactly four arguments", .{}); + return error.TooManyArgs; + } + + var hostname_parts = std.mem.split(hostname_port, ":"); + const hostname = hostname_parts.next().?; + const port = std.fmt.parseUnsigned( + u16, + hostname_parts.next() orelse { + std.log.crit("Hostname and port should be in `hostname:port` format", .{}); + return error.InvalidArg; + }, + 10, + ) catch { + std.log.crit("Port is not a base 10 unsigned integer...", .{}); + return error.InvalidArg; + }; + if (hostname_parts.next() != null) { + std.log.crit("Hostname and port should be in `hostname:port` format", .{}); + return error.InvalidArg; + } + + const out_name = try std.fmt.allocPrint(allocator, "{s}-{s}-{s}-{}.handshake", .{ + hostname, + ciphersuite_str, + curve_str, + std.time.timestamp(), + }); + defer allocator.free(out_name); + + if (std.mem.eql(u8, ciphersuite_str, "all")) { + return try record_with_ciphersuite( + tls.ciphersuites.all, + allocator, + out_name, + curve_str, + hostname, + port, + pem_file_path, + ); + } + inline for (tls.ciphersuites.all) |ciphersuite| { + if (std.mem.eql(u8, ciphersuite_str, ciphersuite.name)) { + return try record_with_ciphersuite( + .{ciphersuite}, + allocator, + out_name, + curve_str, + hostname, + port, + pem_file_path, + ); + } + } + std.log.crit("Invalid ciphersuite `{s}`", .{ciphersuite_str}); + std.debug.warn("Available options:\n- all\n", .{}); + inline for (tls.ciphersuites.all) |ciphersuite| { + std.debug.warn("- {s}\n", .{ciphersuite.name}); + } + return error.InvalidArg; +} diff --git a/libs/iguanatls/src/ciphersuites.zig b/libs/iguanatls/src/ciphersuites.zig index 87d65ea3f0705a199469bedad9bc5e82962e3032..38c0c77915ed9eda33162e979bbbae0832a78b2a 100644 --- a/libs/iguanatls/src/ciphersuites.zig +++ b/libs/iguanatls/src/ciphersuites.zig @@ -4,7 +4,11 @@ const mem = std.mem; usingnamespace @import("crypto.zig"); const Chacha20Poly1305 = std.crypto.aead.chacha_poly.ChaCha20Poly1305; const Aes128Gcm = std.crypto.aead.aes_gcm.Aes128Gcm; -const record_length = @import("main.zig").record_length; + +const main = @import("main.zig"); +const alert_byte_to_error = main.alert_byte_to_error; +const record_tag_length = main.record_tag_length; +const record_length = main.record_length; pub const suites = struct { pub const ECDHE_RSA_Chacha20_Poly1305 = struct { @@ -106,12 +110,24 @@ pub const suites = struct { ) !usize { switch (state.*) { .none => { - const len = (record_length(0x17, reader) catch |err| switch (err) { + const tag_length = record_tag_length(reader) catch |err| switch (err) { error.EndOfStream => return 0, else => |e| return e, - }) - 16; + }; + if (tag_length.length < 16) + return error.ServerMalformedResponse; + const len = tag_length.length - 16; - const curr_bytes = std.math.min(std.math.min(len, buf_size), buffer.len); + if ((tag_length.tag != 0x17 and tag_length.tag != 0x15) or + (tag_length.tag == 0x15 and len != 2)) + { + return error.ServerMalformedResponse; + } + + const curr_bytes = if (tag_length.tag == 0x15) + 2 + else + std.math.min(std.math.min(len, buf_size), buffer.len); var nonce: [12]u8 = ([1]u8{0} ** 4) ++ ([1]u8{undefined} ** 8); mem.writeIntBig(u64, nonce[4..12], server_seq.*); @@ -119,10 +135,6 @@ pub const suites = struct { n.* ^= key_data.server_iv(@This())[i]; } - // Partially decrypt the data. - var encrypted: [buf_size]u8 = undefined; - const actually_read = try reader.read(encrypted[0..curr_bytes]); - var c: [4]u32 = undefined; c[0] = 1; c[1] = mem.readIntLittle(u32, nonce[0..4]); @@ -132,32 +144,63 @@ pub const suites = struct { var context = ChaCha20Stream.initContext(server_key, c); var idx: usize = 0; var buf: [64]u8 = undefined; - ChaCha20Stream.chacha20Xor( - buffer[0..actually_read], - encrypted[0..actually_read], - server_key, - &context, - &idx, - &buf, - ); - if (actually_read < len) { - state.* = .{ - .in_record = .{ - .left = len - actually_read, - .context = context, - .idx = idx, - .buf = buf, - }, + + if (tag_length.tag == 0x15) { + var encrypted: [2]u8 = undefined; + reader.readNoEof(&encrypted) catch |err| switch (err) { + error.EndOfStream => return error.ServerMalformedResponse, + else => |e| return e, }; - } else { - // @TODO Verify Poly1305. + var result: [2] u8 = undefined; + ChaCha20Stream.chacha20Xor( + &result, + &encrypted, + server_key, + &context, + &idx, + &buf, + ); reader.skipBytes(16, .{}) catch |err| switch (err) { - error.EndOfStream => return 0, + error.EndOfStream => return error.ServerMalformedResponse, else => |e| return e, }; server_seq.* += 1; - } - return actually_read; + // CloseNotify + if (result[1] == 0) + return 0; + return alert_byte_to_error(result[1]); + } else if (tag_length.tag == 0x17) { + // Partially decrypt the data. + var encrypted: [buf_size]u8 = undefined; + const actually_read = try reader.read(encrypted[0..curr_bytes]); + + ChaCha20Stream.chacha20Xor( + buffer[0..actually_read], + encrypted[0..actually_read], + server_key, + &context, + &idx, + &buf, + ); + if (actually_read < len) { + state.* = .{ + .in_record = .{ + .left = len - actually_read, + .context = context, + .idx = idx, + .buf = buf, + }, + }; + } else { + // @TODO Verify Poly1305. + reader.skipBytes(16, .{}) catch |err| switch (err) { + error.EndOfStream => return error.ServerMalformedResponse, + else => |e| return e, + }; + server_seq.* += 1; + } + return actually_read; + } else unreachable; }, .in_record => |*record_info| { const curr_bytes = std.math.min(std.math.min(buf_size, buffer.len), record_info.left); @@ -177,7 +220,7 @@ pub const suites = struct { if (record_info.left == 0) { // @TODO Verify Poly1305. reader.skipBytes(16, .{}) catch |err| switch (err) { - error.EndOfStream => return 0, + error.EndOfStream => return error.ServerMalformedResponse, else => |e| return e, }; state.* = .none; @@ -293,12 +336,24 @@ pub const suites = struct { ) !usize { switch (state.*) { .none => { - const len = (record_length(0x17, reader) catch |err| switch (err) { + const tag_length = record_tag_length(reader) catch |err| switch (err) { error.EndOfStream => return 0, else => |e| return e, - }) - 24; + }; + if (tag_length.length < 24) + return error.ServerMalformedResponse; + const len = tag_length.length - 24; - const curr_bytes = std.math.min(std.math.min(len, buf_size), buffer.len); + if ((tag_length.tag != 0x17 and tag_length.tag != 0x15) or + (tag_length.tag == 0x15 and len != 2)) + { + return error.ServerMalformedResponse; + } + + const curr_bytes = if (tag_length.tag == 0x15) + 2 + else + std.math.min(std.math.min(len, buf_size), buffer.len); var iv: [12]u8 = undefined; iv[0..4].* = key_data.server_iv(@This()).*; @@ -307,10 +362,6 @@ pub const suites = struct { else => |e| return e, }; - // Partially decrypt the data. - var encrypted: [buf_size]u8 = undefined; - const actually_read = try reader.read(encrypted[0..curr_bytes]); - const aes = Aes.initEnc(key_data.server_key(@This()).*); var j: [16]u8 = undefined; @@ -320,34 +371,66 @@ pub const suites = struct { var counterInt = mem.readInt(u128, &j, .Big); var idx: usize = 0; - ctr( - @TypeOf(aes), - aes, - buffer[0..actually_read], - encrypted[0..actually_read], - &counterInt, - &idx, - .Big, - ); - - if (actually_read < len) { - state.* = .{ - .in_record = .{ - .left = len - actually_read, - .aes = aes, - .counterInt = counterInt, - .idx = idx, - }, + if (tag_length.tag == 0x15) { + var encrypted: [2]u8 = undefined; + reader.readNoEof(&encrypted) catch |err| switch (err) { + error.EndOfStream => return error.ServerMalformedResponse, + else => |e| return e, }; - } else { - // @TODO Verify the message + + var result: [2]u8 = undefined; + ctr( + @TypeOf(aes), + aes, + &result, + &encrypted, + &counterInt, + &idx, + .Big, + ); reader.skipBytes(16, .{}) catch |err| switch (err) { - error.EndOfStream => return 0, + error.EndOfStream => return error.ServerMalformedResponse, else => |e| return e, }; server_seq.* += 1; - } - return actually_read; + // CloseNotify + if (result[1] == 0) + return 0; + return alert_byte_to_error(result[1]); + } else if (tag_length.tag == 0x17) { + // Partially decrypt the data. + var encrypted: [buf_size]u8 = undefined; + const actually_read = try reader.read(encrypted[0..curr_bytes]); + + ctr( + @TypeOf(aes), + aes, + buffer[0..actually_read], + encrypted[0..actually_read], + &counterInt, + &idx, + .Big, + ); + + if (actually_read < len) { + state.* = .{ + .in_record = .{ + .left = len - actually_read, + .aes = aes, + .counterInt = counterInt, + .idx = idx, + }, + }; + } else { + // @TODO Verify the message + reader.skipBytes(16, .{}) catch |err| switch (err) { + error.EndOfStream => return error.ServerMalformedResponse, + else => |e| return e, + }; + server_seq.* += 1; + } + return actually_read; + } else unreachable; }, .in_record => |*record_info| { const curr_bytes = std.math.min(std.math.min(buf_size, buffer.len), record_info.left); @@ -368,7 +451,7 @@ pub const suites = struct { if (record_info.left == 0) { // @TODO Verify Poly1305. reader.skipBytes(16, .{}) catch |err| switch (err) { - error.EndOfStream => return 0, + error.EndOfStream => return error.ServerMalformedResponse, else => |e| return e, }; state.* = .none; @@ -394,7 +477,7 @@ fn key_field_width(comptime T: type, comptime field: anytype) ?usize { return @typeInfo(field_info.field_type).Array.len; } -pub fn key_data_size(comptime ciphersuites: []const type) usize { +pub fn key_data_size(comptime ciphersuites: anytype) usize { var max: usize = 0; for (ciphersuites) |cs| { const curr = (key_field_width(cs.Keys, .client_mac) orelse 0) + @@ -409,7 +492,7 @@ pub fn key_data_size(comptime ciphersuites: []const type) usize { return max; } -pub fn KeyData(comptime ciphersuites: []const type) type { +pub fn KeyData(comptime ciphersuites: anytype) type { return struct { data: [key_data_size(ciphersuites)]u8, @@ -455,7 +538,7 @@ pub fn KeyData(comptime ciphersuites: []const type) type { } pub fn key_expansion( - comptime ciphersuites: []const type, + comptime ciphersuites: anytype, tag: u16, context: anytype, comptime next_32_bytes: anytype, @@ -505,7 +588,7 @@ pub fn key_expansion( unreachable; } -pub fn ClientState(comptime ciphersuites: []const type) type { +pub fn ClientState(comptime ciphersuites: anytype) type { var fields: [ciphersuites.len]std.builtin.TypeInfo.UnionField = undefined; for (ciphersuites) |cs, i| { fields[i] = .{ @@ -524,7 +607,7 @@ pub fn ClientState(comptime ciphersuites: []const type) type { }); } -pub fn client_state_default(comptime ciphersuites: []const type, tag: u16) ClientState(ciphersuites) { +pub fn client_state_default(comptime ciphersuites: anytype, tag: u16) ClientState(ciphersuites) { inline for (ciphersuites) |cs| { if (cs.tag == tag) { return @unionInit(ClientState(ciphersuites), cs.name, cs.default_state); diff --git a/libs/iguanatls/src/crypto.zig b/libs/iguanatls/src/crypto.zig index 29e2d2f4f6d7d6a5f603d52c9957385ea21d3416..25553c318157a594a8cf46f3f36e947e48479c42 100644 --- a/libs/iguanatls/src/crypto.zig +++ b/libs/iguanatls/src/crypto.zig @@ -243,6 +243,54 @@ pub const ecc = struct { } }; + pub const SECP256R1 = struct { + pub const point_len = 64; + + const order = [point_len / 2]u8{ + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xBC, 0xE6, 0xFA, 0xAD, 0xA7, 0x17, 0x9E, 0x84, + 0xF3, 0xB9, 0xCA, 0xC2, 0xFC, 0x63, 0x25, 0x51, + }; + + const P = [_]u32{ + 0x00000108, 0x7FFFFFFF, + 0x7FFFFFFF, 0x7FFFFFFF, + 0x00000007, 0x00000000, + 0x00000000, 0x00000040, + 0x7FFFFF80, 0x000000FF, + }; + const R2 = [_]u32{ + 0x00000108, 0x00014000, + 0x00018000, 0x00000000, + 0x7FF40000, 0x7FEFFFFF, + 0x7FF7FFFF, 0x7FAFFFFF, + 0x005FFFFF, 0x00000000, + }; + const B = [_]u32{ + 0x00000108, 0x6FEE1803, + 0x6229C4BD, 0x21B139BE, + 0x327150AA, 0x3567802E, + 0x3F7212ED, 0x012E4355, + 0x782DD38D, 0x0000000E, + }; + + const base_point = [point_len]u8{ + 0x6B, 0x17, 0xD1, 0xF2, 0xE1, 0x2C, 0x42, 0x47, + 0xF8, 0xBC, 0xE6, 0xE5, 0x63, 0xA4, 0x40, 0xF2, + 0x77, 0x03, 0x7D, 0x81, 0x2D, 0xEB, 0x33, 0xA0, + 0xF4, 0xA1, 0x39, 0x45, 0xD8, 0x98, 0xC2, 0x96, + 0x4F, 0xE3, 0x42, 0xE2, 0xFE, 0x1A, 0x7F, 0x9B, + 0x8E, 0xE7, 0xEB, 0x4A, 0x7C, 0x0F, 0x9E, 0x16, + 0x2B, 0xCE, 0x33, 0x57, 0x6B, 0x31, 0x5E, 0xCE, + 0xCB, 0xB6, 0x40, 0x68, 0x37, 0xBF, 0x51, 0xF5, + }; + + comptime { + std.debug.assert((P[0] - (P[0] >> 5) + 7) >> 2 == point_len + 1); + } + }; + fn jacobian_len(comptime Curve: type) usize { return @divTrunc(Curve.order.len * 8 + 61, 31); } diff --git a/libs/iguanatls/src/main.zig b/libs/iguanatls/src/main.zig index 7b7297efba1a153cbf4f86cd3059a68bb1fead10..d57bfbb2acd85dba8376e3654d9f2fb5c89e0884 100644 --- a/libs/iguanatls/src/main.zig +++ b/libs/iguanatls/src/main.zig @@ -23,6 +23,26 @@ fn handshake_record_length(reader: anytype) !usize { return try record_length(0x16, reader); } +pub const RecordTagLength = struct { + tag: u8, + length: u16, +}; +pub fn record_tag_length(reader: anytype) !RecordTagLength { + const record_tag = try reader.readByte(); + + var record_header: [4]u8 = undefined; + try reader.readNoEof(&record_header); + + if (!mem.eql(u8, record_header[0..2], "\x03\x03") and !mem.eql(u8, record_header[0..2], "\x03\x01")) + return error.ServerInvalidVersion; + + const len = mem.readIntSliceBig(u16, record_header[2..4]); + return RecordTagLength{ + .tag = record_tag, + .length = len, + }; +} + pub fn record_length(t: u8, reader: anytype) !usize { try check_record_type(t, reader); var record_header: [4]u8 = undefined; @@ -72,39 +92,43 @@ fn check_record_type( const severity = try reader.readByte(); const err_num = try reader.readByte(); - return switch (err_num) { - 0 => error.AlertCloseNotify, - 10 => error.AlertUnexpectedMessage, - 20 => error.AlertBadRecordMAC, - 21 => error.AlertDecryptionFailed, - 22 => error.AlertRecordOverflow, - 30 => error.AlertDecompressionFailure, - 40 => error.AlertHandshakeFailure, - 41 => error.AlertNoCertificate, - 42 => error.AlertBadCertificate, - 43 => error.AlertUnsupportedCertificate, - 44 => error.AlertCertificateRevoked, - 45 => error.AlertCertificateExpired, - 46 => error.AlertCertificateUnknown, - 47 => error.AlertIllegalParameter, - 48 => error.AlertUnknownCA, - 49 => error.AlertAccessDenied, - 50 => error.AlertDecodeError, - 51 => error.AlertDecryptError, - 60 => error.AlertExportRestriction, - 70 => error.AlertProtocolVersion, - 71 => error.AlertInsufficientSecurity, - 80 => error.AlertInternalError, - 90 => error.AlertUserCanceled, - 100 => error.AlertNoRenegotiation, - 110 => error.AlertUnsupportedExtension, - else => error.ServerMalformedResponse, - }; + return alert_byte_to_error(err_num); } if (record_type != expected) return error.ServerMalformedResponse; } +pub fn alert_byte_to_error(b: u8) (ServerAlert || error{ServerMalformedResponse}) { + return switch (b) { + 0 => error.AlertCloseNotify, + 10 => error.AlertUnexpectedMessage, + 20 => error.AlertBadRecordMAC, + 21 => error.AlertDecryptionFailed, + 22 => error.AlertRecordOverflow, + 30 => error.AlertDecompressionFailure, + 40 => error.AlertHandshakeFailure, + 41 => error.AlertNoCertificate, + 42 => error.AlertBadCertificate, + 43 => error.AlertUnsupportedCertificate, + 44 => error.AlertCertificateRevoked, + 45 => error.AlertCertificateExpired, + 46 => error.AlertCertificateUnknown, + 47 => error.AlertIllegalParameter, + 48 => error.AlertUnknownCA, + 49 => error.AlertAccessDenied, + 50 => error.AlertDecodeError, + 51 => error.AlertDecryptError, + 60 => error.AlertExportRestriction, + 70 => error.AlertProtocolVersion, + 71 => error.AlertInsufficientSecurity, + 80 => error.AlertInternalError, + 90 => error.AlertUserCanceled, + 100 => error.AlertNoRenegotiation, + 110 => error.AlertUnsupportedExtension, + else => error.ServerMalformedResponse, + }; +} + fn Sha256Reader(comptime Reader: anytype) type { const State = struct { sha256: *Sha256, @@ -284,8 +308,39 @@ fn check_cert_timestamp(time: i64, tag_byte: u8, length: usize, reader: anytype) return error.CertificateVerificationFailed; } -fn add_cert_subject_dn(state: *VerifierCaptureState, _: u8, length: usize, reader: anytype) !void { +fn add_dn_field(state: *VerifierCaptureState, tag: u8, length: usize, reader: anytype) !void { + const seq_tag = try reader.readByte(); + if (seq_tag != 0x30) + return error.CertificateVerificationFailed; + const seq_length = try asn1.der.parse_length(reader); + + const oid_tag = try reader.readByte(); + if (oid_tag != 0x06) + return error.CertificateVerificationFailed; + + const oid_length = try asn1.der.parse_length(reader); + if (oid_length == 3 and (try reader.isBytes("\x55\x04\x03"))) { + // Common name + const common_name_tag = try reader.readByte(); + if (common_name_tag != 0x04 and common_name_tag != 0x0c and common_name_tag != 0x13 and common_name_tag != 0x16) + return error.CertificateVerificationFailed; + const common_name_len = try asn1.der.parse_length(reader); + state.list.items[state.list.items.len - 1].common_name = state.fbs.buffer[state.fbs.pos .. state.fbs.pos + common_name_len]; + } +} + +fn add_cert_subject_dn(state: *VerifierCaptureState, tag: u8, length: usize, reader: anytype) !void { state.list.items[state.list.items.len - 1].dn = state.fbs.buffer[state.fbs.pos .. state.fbs.pos + length]; + const schema = .{ + .sequence_of, + .{ + .capture, 0, .set, + }, + }; + const captures = .{ + state, add_dn_field, + }; + try asn1.der.parse_schema_tag_len(tag, length, schema, captures, reader); } fn add_cert_public_key(state: *VerifierCaptureState, _: u8, length: usize, reader: anytype) !void { @@ -302,8 +357,9 @@ fn add_server_cert(state: *VerifierCaptureState, tag_byte: u8, length: usize, re const is_ca = state.list.items.len != 0; const encoded_length = asn1.der.encode_length(length).slice(); + // This is not errdefered since default_cert_verifier call takes care of cleaning up all the certificate data. + // Same for the signature.data const cert_bytes = try state.allocator.alloc(u8, length + 1 + encoded_length.len); - errdefer state.allocator.free(cert_bytes); cert_bytes[0] = tag_byte; mem.copy(u8, cert_bytes[1 .. 1 + encoded_length.len], encoded_length); @@ -312,11 +368,11 @@ fn add_server_cert(state: *VerifierCaptureState, tag_byte: u8, length: usize, re .is_ca = is_ca, .bytes = cert_bytes, .dn = undefined, - .public_key = undefined, + .common_name = &[0]u8{}, + .public_key = x509.PublicKey.empty, .signature = asn1.BitString{ .data = &[0]u8{}, .bit_len = 0 }, .signature_algorithm = undefined, }; - errdefer state.allocator.free(state.list.items[state.list.items.len - 1].signature.data); const schema = .{ .sequence, @@ -474,7 +530,6 @@ fn verify_signature( try llmod(&curr_base, modulus); try curr_exponent.shiftRight(curr_exponent, 1); } - try llmod(&encrypted_signature, modulus); } // EMSA-PKCS1-V1_5-ENCODE if (encrypted_signature.limbs.len * @sizeOf(usize) < signature.data.len) @@ -553,6 +608,7 @@ const SignatureAlgorithm = enum { const ServerCertificate = struct { bytes: []const u8, dn: []const u8, + common_name: []const u8, public_key: x509.PublicKey, signature: asn1.BitString, signature_algorithm: SignatureAlgorithm, @@ -566,8 +622,36 @@ const VerifierCaptureState = struct { fbs: *std.io.FixedBufferStream([]const u8), }; +// @TODO Move out of here +const ReverseSplitIterator = struct { + buffer: []const u8, + index: ?usize, + delimiter: []const u8, + + pub fn next(self: *ReverseSplitIterator) ?[]const u8 { + const end = self.index orelse return null; + const start = if (mem.lastIndexOfLinear(u8, self.buffer[0..end], self.delimiter)) |delim_start| blk: { + self.index = delim_start; + break :blk delim_start + self.delimiter.len; + } else blk: { + self.index = null; + break :blk 0; + }; + return self.buffer[start..end]; + } +}; + +fn reverse_split(buffer: []const u8, delimiter: []const u8) ReverseSplitIterator { + std.debug.assert(delimiter.len != 0); + return .{ + .index = buffer.len, + .buffer = buffer, + .delimiter = delimiter, + }; +} + pub fn default_cert_verifier( - allocator: *std.mem.Allocator, + allocator: *mem.Allocator, reader: anytype, certs_bytes: usize, trusted_certificates: []const x509.TrustAnchor, @@ -622,6 +706,31 @@ pub fn default_cert_verifier( return error.CertificateVerificationFailed; const chain = capture_state.list.items; + if (chain.len == 0) return error.CertificateVerificationFailed; + // Check if the hostname matches the leaf certificate's common name + { + var common_name_split = reverse_split(chain[0].common_name, "."); + var hostname_split = reverse_split(hostname, "."); + while (true) { + const cn_part = common_name_split.next(); + const hn_part = hostname_split.next(); + + if (cn_part) |cnp| { + if (hn_part == null and common_name_split.index == null and mem.eql(u8, cnp, "www")) + break + else if (hn_part) |hnp| { + if (mem.eql(u8, cnp, "*")) + continue; + if (!mem.eql(u8, cnp, hnp)) + return error.CertificateVerificationFailed; + } + } else if (hn_part != null) + return error.CertificateVerificationFailed + else + break; + } + } + var i: usize = 0; while (i < chain.len - 1) : (i += 1) { if (!try certificate_verify_signature( @@ -642,12 +751,7 @@ pub fn default_cert_verifier( cert.public_key.eql(trusted.public_key)) { const key = chain[0].public_key; - chain[0].public_key = x509.PublicKey{ - .ec = .{ - .id = undefined, - .curve_point = &[0]u8{}, - }, - }; + chain[0].public_key = x509.PublicKey.empty; return key; } @@ -662,12 +766,7 @@ pub fn default_cert_verifier( trusted.public_key, )) { const key = chain[0].public_key; - chain[0].public_key = x509.PublicKey{ - .ec = .{ - .id = undefined, - .curve_point = &[0]u8{}, - }, - }; + chain[0].public_key = x509.PublicKey.empty; return key; } } @@ -736,6 +835,155 @@ pub fn extract_cert_public_key(allocator: *Allocator, reader: anytype, length: u return capture_state.pub_key; } +pub const curves = struct { + pub const x25519 = struct { + pub const name = "x25519"; + const tag = 0x001D; + const pub_key_len = 32; + const Keys = std.crypto.dh.X25519.KeyPair; + + inline fn make_key_pair(rand: *std.rand.Random) Keys { + while (true) { + var seed: [32]u8 = undefined; + rand.bytes(&seed); + return std.crypto.dh.X25519.KeyPair.create(seed) catch continue; + } else unreachable; + } + + inline fn make_pre_master_secret( + key_pair: Keys, + pre_master_secret_buf: []u8, + server_public_key: *const [32]u8, + ) ![]const u8 { + pre_master_secret_buf[0..32].* = std.crypto.dh.X25519.scalarmult( + key_pair.secret_key, + server_public_key.*, + ) catch return error.PreMasterGenerationFailed; + return pre_master_secret_buf[0..32]; + } + }; + + pub const secp384r1 = struct { + pub const name = "secp384r1"; + const tag = 0x0018; + const pub_key_len = 97; + const Keys = crypto.ecc.KeyPair(crypto.ecc.SECP384R1); + + inline fn make_key_pair(rand: *std.rand.Random) Keys { + var seed: [48]u8 = undefined; + rand.bytes(&seed); + return crypto.ecc.make_key_pair(crypto.ecc.SECP384R1, seed); + } + + inline fn make_pre_master_secret( + key_pair: Keys, + pre_master_secret_buf: []u8, + server_public_key: *const [97]u8, + ) ![]const u8 { + pre_master_secret_buf[0..96].* = crypto.ecc.scalarmult( + crypto.ecc.SECP384R1, + server_public_key[1..].*, + &key_pair.secret_key, + ) catch return error.PreMasterGenerationFailed; + return pre_master_secret_buf[0..48]; + } + }; + + pub const secp256r1 = struct { + pub const name = "secp256r1"; + const tag = 0x0017; + const pub_key_len = 65; + const Keys = crypto.ecc.KeyPair(crypto.ecc.SECP256R1); + + inline fn make_key_pair(rand: *std.rand.Random) Keys { + var seed: [32]u8 = undefined; + rand.bytes(&seed); + return crypto.ecc.make_key_pair(crypto.ecc.SECP256R1, seed); + } + + inline fn make_pre_master_secret( + key_pair: Keys, + pre_master_secret_buf: []u8, + server_public_key: *const [65]u8, + ) ![]const u8 { + pre_master_secret_buf[0..64].* = crypto.ecc.scalarmult( + crypto.ecc.SECP256R1, + server_public_key[1..].*, + &key_pair.secret_key, + ) catch return error.PreMasterGenerationFailed; + return pre_master_secret_buf[0..32]; + } + }; + + pub const all = &[_]type{ x25519, secp384r1, secp256r1 }; + + fn max_pub_key_len(comptime list: anytype) usize { + var max: usize = 0; + for (list) |curve| { + if (curve.pub_key_len > max) + max = curve.pub_key_len; + } + return max; + } + + fn max_pre_master_secret_len(comptime list: anytype) usize { + var max: usize = 0; + for (list) |curve| { + const curr = @typeInfo(std.meta.fieldInfo(curve.Keys, .public_key).field_type).Array.len; + if (curr > max) + max = curr; + } + return max; + } + + fn KeyPair(comptime list: anytype) type { + var fields: [list.len]std.builtin.TypeInfo.UnionField = undefined; + for (list) |curve, i| { + fields[i] = .{ + .name = curve.name, + .field_type = curve.Keys, + .alignment = @alignOf(curve.Keys), + }; + } + return @Type(.{ + .Union = .{ + .layout = .Extern, + .tag_type = null, + .fields = &fields, + .decls = &[0]std.builtin.TypeInfo.Declaration{}, + }, + }); + } + + inline fn make_key_pair(comptime list: anytype, curve_id: u16, rand: *std.rand.Random) KeyPair(list) { + inline for (list) |curve| { + if (curve.tag == curve_id) { + return @unionInit(KeyPair(list), curve.name, curve.make_key_pair(rand)); + } + } + unreachable; + } + + inline fn make_pre_master_secret( + comptime list: anytype, + curve_id: u16, + key_pair: KeyPair(list), + pre_master_secret_buf: *[max_pre_master_secret_len(list)]u8, + server_public_key: [max_pub_key_len(list)]u8, + ) ![]const u8 { + inline for (list) |curve| { + if (curve.tag == curve_id) { + return try curve.make_pre_master_secret( + @field(key_pair, curve.name), + pre_master_secret_buf, + server_public_key[0..curve.pub_key_len], + ); + } + } + unreachable; + } +}; + pub fn client_connect( options: anytype, hostname: []const u8, @@ -746,7 +994,10 @@ pub fn client_connect( )!Client( @TypeOf(options.reader), @TypeOf(options.writer), - options.ciphersuites, + if (@hasField(@TypeOf(options), "ciphersuites")) + options.ciphersuites + else + ciphersuites.all, @hasField(@TypeOf(options), "protocols"), ) { const Options = @TypeOf(options); @@ -761,6 +1012,20 @@ pub fn client_connect( @compileError("Option tuple is missing field 'trusted_certificates' for .default cert_verifier"); } + const suites = if (!@hasField(Options, "ciphersuites")) + ciphersuites.all + else + options.ciphersuites; + if (suites.len == 0) + @compileError("Must provide at least one ciphersuite type."); + + const curvelist = if (!@hasField(Options, "curves")) + curves.all + else + options.curves; + if (curvelist.len == 0) + @compileError("Must provide at least one curve type."); + const has_alpn = comptime @hasField(Options, "protocols"); var handshake_record_hash = Sha256.init(.{}); const reader = options.reader; @@ -777,11 +1042,7 @@ pub fn client_connect( rand.bytes(&client_random); var server_random: [32]u8 = undefined; - - if (options.ciphersuites.len == 0) - @compileError("Must provide at least one ciphersuite."); - const ciphersuite_bytes = 2 * options.ciphersuites.len + 2; - // @TODO Make sure the individual lengths are u16s + const ciphersuite_bytes = 2 * suites.len + 2; const alpn_bytes = if (has_alpn) blk: { var sum: usize = 0; for (options.protocols) |proto| { @@ -789,6 +1050,7 @@ pub fn client_connect( } break :blk 6 + options.protocols.len + sum; } else 0; + const curvelist_bytes = 2 * curvelist.len; var protocol: if (has_alpn) []const u8 else void = undefined; { const client_hello_start = comptime blk: { @@ -810,7 +1072,7 @@ pub fn client_connect( // Same as above, couldnt achieve this with a single buffer. // TLS_EMPTY_RENEGOTIATION_INFO_SCSV var ciphersuite_buf: []const u8 = &[2]u8{ 0x00, 0x0f }; - for (options.ciphersuites) |cs, i| { + for (suites) |cs, i| { // Also check for properties of the ciphersuites here if (cs.key_exchange != .ecdhe) @compileError("Non ECDHE key exchange is not supported yet."); @@ -838,10 +1100,10 @@ pub fn client_connect( }; var msg_buf = client_hello_start.ptr[0..client_hello_start.len].*; - mem.writeIntBig(u16, msg_buf[3..5], @intCast(u16, alpn_bytes + hostname.len + 0x59 + ciphersuite_bytes)); - mem.writeIntBig(u24, msg_buf[6..9], @intCast(u24, alpn_bytes + hostname.len + 0x55 + ciphersuite_bytes)); + mem.writeIntBig(u16, msg_buf[3..5], @intCast(u16, alpn_bytes + hostname.len + 0x55 + ciphersuite_bytes + curvelist_bytes)); + mem.writeIntBig(u24, msg_buf[6..9], @intCast(u24, alpn_bytes + hostname.len + 0x51 + ciphersuite_bytes + curvelist_bytes)); mem.copy(u8, msg_buf[11..43], &client_random); - mem.writeIntBig(u16, msg_buf[48 + ciphersuite_bytes ..][0..2], @intCast(u16, alpn_bytes + hostname.len + 0x2C)); + mem.writeIntBig(u16, msg_buf[48 + ciphersuite_bytes ..][0..2], @intCast(u16, alpn_bytes + hostname.len + 0x28 + curvelist_bytes)); mem.writeIntBig(u16, msg_buf[52 + ciphersuite_bytes ..][0..2], @intCast(u16, hostname.len + 5)); mem.writeIntBig(u16, msg_buf[54 + ciphersuite_bytes ..][0..2], @intCast(u16, hostname.len + 3)); mem.writeIntBig(u16, msg_buf[57 + ciphersuite_bytes ..][0..2], @intCast(u16, hostname.len)); @@ -849,7 +1111,6 @@ pub fn client_connect( try hashing_writer.writeAll(msg_buf[5..]); } try hashing_writer.writeAll(hostname); - // @TODO Fix this with wikipedia test, add secp384r1 support (then app options.curves but default to all when not there (also do this for ciphersuites)) if (has_alpn) { var msg_buf = [6]u8{ 0x00, 0x10, undefined, undefined, undefined, undefined }; mem.writeIntBig(u16, msg_buf[2..4], @intCast(u16, alpn_bytes - 4)); @@ -860,20 +1121,37 @@ pub fn client_connect( try hashing_writer.writeAll(proto); } } - try hashing_writer.writeAll(&[35]u8{ - // Extension: supported groups, for now just x25519 (00 1D) and secp384r1 (00 0x18) - 0x00, 0x0A, 0x00, 0x06, 0x00, 0x04, 0x00, 0x1D, 0x00, 0x18, + + // Extension: supported groups + { + var msg_buf = [6]u8{ + 0x00, 0x0A, + undefined, undefined, + undefined, undefined, + }; + + mem.writeIntBig(u16, msg_buf[2..4], @intCast(u16, curvelist_bytes + 2)); + mem.writeIntBig(u16, msg_buf[4..6], @intCast(u16, curvelist_bytes)); + try hashing_writer.writeAll(&msg_buf); + + inline for (curvelist) |curve| { + try hashing_writer.writeIntBig(u16, curve.tag); + } + } + + try hashing_writer.writeAll(&[25]u8{ // Extension: EC point formats => uncompressed point format 0x00, 0x0B, 0x00, 0x02, 0x01, 0x00, // Extension: Signature algorithms // RSA/PKCS1/SHA256, RSA/PKCS1/SHA512 - 0x00, 0x0D, 0x00, 0x06, - 0x00, 0x04, 0x04, 0x01, 0x06, 0x01, + 0x00, 0x0D, 0x00, 0x06, 0x00, 0x04, + 0x04, 0x01, 0x06, 0x01, // Extension: Renegotiation Info => new connection - 0xFF, 0x01, 0x00, 0x01, - 0x00, + 0xFF, 0x01, + 0x00, 0x01, 0x00, // Extension: SCT (signed certificate timestamp) - 0x00, 0x12, 0x00, 0x00, + 0x00, 0x12, 0x00, + 0x00, }); // Read server hello @@ -900,7 +1178,7 @@ pub fn client_connect( { ciphersuite = try hashing_reader.readIntBig(u16); var found = false; - inline for (options.ciphersuites) |cs| { + inline for (suites) |cs| { if (ciphersuite == cs.tag) { found = true; // TODO This segfaults stage1 @@ -1002,8 +1280,10 @@ pub fn client_connect( } errdefer certificate_public_key.deinit(options.temp_allocator); // Read server ephemeral public key - var server_public_key_buf: [97]u8 = undefined; - var curve_id: enum { x25519, secp384r1 } = undefined; + var server_public_key_buf: [curves.max_pub_key_len(curvelist)]u8 = undefined; + var curve_id: u16 = undefined; + var curve_id_buf: [3]u8 = undefined; + var pub_key_len: u8 = undefined; { const length = try handshake_record_length(reader); { @@ -1012,24 +1292,38 @@ pub fn client_connect( if (handshake_header[0] != 0x0c) return error.ServerMalformedResponse; - // Only x25519 and secp384r1 supported for now. - var curve_bytes: [3]u8 = undefined; - try hashing_reader.readNoEof(&curve_bytes); - curve_id = if (mem.eql(u8, &curve_bytes, "\x03\x00\x1D")) - .x25519 - else if (mem.eql(u8, &curve_bytes, "\x03\x00\x18")) - .secp384r1 - else + try hashing_reader.readNoEof(&curve_id_buf); + if (curve_id_buf[0] != 0x03) + return error.ServerMalformedResponse; + + curve_id = mem.readIntBig(u16, curve_id_buf[1..]); + var found = false; + inline for (curvelist) |curve| { + if (curve.tag == curve_id) { + found = true; + // @TODO This break segfaults stage1 + // break; + } + } + if (!found) return error.ServerInvalidCurve; } - const pub_key_len = try hashing_reader.readByte(); - if ((curve_id == .x25519 and pub_key_len != 32) or - (curve_id == .secp384r1 and pub_key_len != 97)) - return error.ServerMalformedResponse; + pub_key_len = try hashing_reader.readByte(); + inline for (curvelist) |curve| { + if (curve.tag == curve_id) { + if (curve.pub_key_len != pub_key_len) + return error.ServerMalformedResponse; + // @TODO This break segfaults stage1 + // break; + } + } + try hashing_reader.readNoEof(server_public_key_buf[0..pub_key_len]); - if (curve_id == .secp384r1 and server_public_key_buf[0] != 0x04) - return error.ServerMalformedResponse; + if (curve_id != curves.x25519.tag) { + if (server_public_key_buf[0] != 0x04) + return error.ServerMalformedResponse; + } // Signed public key const signature_id = try hashing_reader.readIntBig(u16); @@ -1043,10 +1337,9 @@ pub fn client_connect( var sha256 = Sha256.init(.{}); sha256.update(&client_random); sha256.update(&server_random); - switch (curve_id) { - .x25519 => sha256.update("\x03\x00\x1D\x20"), - .secp384r1 => sha256.update("\x03\x00\x18\x20"), - } + sha256.update(&curve_id_buf); + // @TODO Should this always be \x20 instead? + sha256.update(&[1]u8{pub_key_len}); sha256.update(server_public_key_buf[0..pub_key_len]); sha256.final(hash_buf[0..32]); hash = hash_buf[0..32]; @@ -1057,10 +1350,7 @@ pub fn client_connect( var sha512 = Sha512.init(.{}); sha512.update(&client_random); sha512.update(&server_random); - switch (curve_id) { - .x25519 => sha512.update("\x03\x00\x1D"), - .secp384r1 => sha512.update("\x03\x00\x18"), - } + sha512.update(&curve_id_buf); sha512.update(&[1]u8{pub_key_len}); sha512.update(server_public_key_buf[0..pub_key_len]); sha512.final(hash_buf[0..64]); @@ -1083,7 +1373,7 @@ pub fn client_connect( return error.ServerInvalidSignature; certificate_public_key.deinit(options.temp_allocator); - certificate_public_key = x509.PublicKey{ .ec = .{ .id = undefined, .curve_point = &[0]u8{} } }; + certificate_public_key = x509.PublicKey.empty; } // Read server hello done { @@ -1094,60 +1384,38 @@ pub fn client_connect( } // Generate keys for the session - const client_key_pair: extern union { - x25519: std.crypto.dh.X25519.KeyPair, - secp384r1: crypto.ecc.KeyPair(crypto.ecc.SECP384R1), - } = switch (curve_id) { - .x25519 => while (true) { - var seed: [32]u8 = undefined; - rand.bytes(&seed); - const tmp = std.crypto.dh.X25519.KeyPair.create(seed) catch continue; - break .{ .x25519 = tmp }; - } else unreachable, - .secp384r1 => blk: { - var seed: [48]u8 = undefined; - rand.bytes(&seed); - break :blk .{ .secp384r1 = crypto.ecc.make_key_pair(crypto.ecc.SECP384R1, seed) }; - }, - }; + const client_key_pair = curves.make_key_pair(curvelist, curve_id, rand); // Client key exchange - switch (curve_id) { - .x25519 => { - try writer.writeAll(&[5]u8{ 0x16, 0x03, 0x03, 0x00, 0x25 }); - try hashing_writer.writeAll(&[5]u8{ 0x10, 0x00, 0x00, 0x21, 0x20 }); - try hashing_writer.writeAll(&client_key_pair.x25519.public_key); - }, - .secp384r1 => { - try writer.writeAll(&[5]u8{ 0x16, 0x03, 0x03, 0x00, 0x66 }); - try hashing_writer.writeAll(&[6]u8{ 0x10, 0x00, 0x00, 0x62, 0x61, 0x04 }); - try hashing_writer.writeAll(&client_key_pair.secp384r1.public_key); - }, + try writer.writeAll(&[3]u8{ 0x16, 0x03, 0x03 }); + try writer.writeIntBig(u16, pub_key_len + 5); + try hashing_writer.writeAll(&[5]u8{ 0x10, 0x00, 0x00, pub_key_len + 1, pub_key_len }); + + inline for (curvelist) |curve| { + if (curve.tag == curve_id) { + const actual_len = @typeInfo(std.meta.fieldInfo(curve.Keys, .public_key).field_type).Array.len; + if (pub_key_len == actual_len + 1) { + try hashing_writer.writeByte(0x04); + } else { + std.debug.assert(pub_key_len == actual_len); + } + try hashing_writer.writeAll(&@field(client_key_pair, curve.name).public_key); + break; + } } + // Client encryption keys calculation for ECDHE_RSA cipher suites with SHA256 hash var master_secret: [48]u8 = undefined; - var key_data: ciphers.KeyData(options.ciphersuites) = undefined; + var key_data: ciphers.KeyData(suites) = undefined; { - var pre_master_secret_buf: [96]u8 = undefined; - const pre_master_secret = switch (curve_id) { - .x25519 => blk: { - pre_master_secret_buf[0..32].* = std.crypto.dh.X25519.scalarmult( - client_key_pair.x25519.secret_key, - server_public_key_buf[0..32].*, - ) catch - return error.PreMasterGenerationFailed; - break :blk pre_master_secret_buf[0..32]; - }, - .secp384r1 => blk: { - pre_master_secret_buf = crypto.ecc.scalarmult( - crypto.ecc.SECP384R1, - server_public_key_buf[1..].*, - &client_key_pair.secp384r1.secret_key, - ) catch - return error.PreMasterGenerationFailed; - break :blk pre_master_secret_buf[0..48]; - }, - }; + var pre_master_secret_buf: [curves.max_pre_master_secret_len(curvelist)]u8 = undefined; + const pre_master_secret = try curves.make_pre_master_secret( + curvelist, + curve_id, + client_key_pair, + &pre_master_secret_buf, + server_public_key_buf, + ); var seed: [77]u8 = undefined; seed[0..13].* = "master secret".*; @@ -1209,7 +1477,7 @@ pub fn client_connect( .master_secret = &master_secret, }; - key_data = ciphers.key_expansion(options.ciphersuites, ciphersuite, &state, next_32_bytes); + key_data = ciphers.key_expansion(suites, ciphersuite, &state, next_32_bytes); } // Client change cipher spec and client handshake finished @@ -1245,7 +1513,7 @@ pub fn client_connect( } handshake_record_hash.update(&verify_message); - inline for (options.ciphersuites) |cs| { + inline for (suites) |cs| { if (cs.tag == ciphersuite) { try cs.raw_write( 256, @@ -1285,7 +1553,7 @@ pub fn client_connect( verify_message[4..16].* = p1[0..12].*; } - inline for (options.ciphersuites) |cs| { + inline for (suites) |cs| { if (cs.tag == ciphersuite) { if (!try cs.check_verify_message(&key_data, length, reader, verify_message)) return error.ServerInvalidVerifyData; @@ -1293,10 +1561,10 @@ pub fn client_connect( } } - return Client(@TypeOf(reader), @TypeOf(writer), options.ciphersuites, has_alpn){ + return Client(@TypeOf(reader), @TypeOf(writer), suites, has_alpn){ .ciphersuite = ciphersuite, .key_data = key_data, - .state = ciphers.client_state_default(options.ciphersuites, ciphersuite), + .state = ciphers.client_state_default(suites, ciphersuite), .rand = rand, .parent_reader = reader, .parent_writer = writer, @@ -1418,8 +1686,9 @@ test "HTTPS request on wikipedia main page" { .cert_verifier = .default, .temp_allocator = std.testing.allocator, .trusted_certificates = trusted_chain.data.items, - .ciphersuites = ciphersuites.all, + .ciphersuites = .{ciphersuites.ECDHE_RSA_Chacha20_Poly1305}, .protocols = &[_][]const u8{"http/1.1"}, + .curves = .{curves.x25519}, }, "en.wikipedia.org"); defer client.close_notify() catch {}; @@ -1471,9 +1740,9 @@ test "HTTPS request on twitch oath2 endpoint" { .reader = sock.reader(), .writer = sock.writer(), .cert_verifier = .none, - .ciphersuites = ciphersuites.all, .protocols = &[_][]const u8{"http/1.1"}, }, "id.twitch.tv"); + std.testing.expectEqualStrings("http/1.1", client.protocol); defer client.close_notify() catch {}; try client.writer().writeAll("GET /oauth2/validate HTTP/1.1\r\nHost: id.twitch.tv\r\nAccept: */*\r\n\r\n"); @@ -1497,3 +1766,78 @@ test "HTTPS request on twitch oath2 endpoint" { try client.reader().readNoEof(html_contents); } + +test "Connecting to expired.badssl.com returns an error" { + const sock = try std.net.tcpConnectToHost(std.testing.allocator, "expired.badssl.com", 443); + defer sock.close(); + + var fbs = std.io.fixedBufferStream(@embedFile("../test/DigiCertGlobalRootCA.crt.pem")); + var trusted_chain = try x509.TrustAnchorChain.from_pem(std.testing.allocator, fbs.reader()); + defer trusted_chain.deinit(); + + // @TODO Remove this once std.crypto.rand works in .evented mode + var rand = blk: { + var seed: [std.rand.DefaultCsprng.secret_seed_length]u8 = undefined; + try std.os.getrandom(&seed); + break :blk &std.rand.DefaultCsprng.init(seed).random; + }; + + std.testing.expectError(error.CertificateVerificationFailed, client_connect(.{ + .rand = rand, + .reader = sock.reader(), + .writer = sock.writer(), + .cert_verifier = .default, + .temp_allocator = std.testing.allocator, + .trusted_certificates = trusted_chain.data.items, + }, "expired.badssl.com")); +} + +test "Connecting to wrong.host.badssl.com returns an error" { + const sock = try std.net.tcpConnectToHost(std.testing.allocator, "wrong.host.badssl.com", 443); + defer sock.close(); + + var fbs = std.io.fixedBufferStream(@embedFile("../test/DigiCertGlobalRootCA.crt.pem")); + var trusted_chain = try x509.TrustAnchorChain.from_pem(std.testing.allocator, fbs.reader()); + defer trusted_chain.deinit(); + + // @TODO Remove this once std.crypto.rand works in .evented mode + var rand = blk: { + var seed: [std.rand.DefaultCsprng.secret_seed_length]u8 = undefined; + try std.os.getrandom(&seed); + break :blk &std.rand.DefaultCsprng.init(seed).random; + }; + + std.testing.expectError(error.CertificateVerificationFailed, client_connect(.{ + .rand = rand, + .reader = sock.reader(), + .writer = sock.writer(), + .cert_verifier = .default, + .temp_allocator = std.testing.allocator, + .trusted_certificates = trusted_chain.data.items, + }, "wrong.host.badssl.com")); +} + +test "Connecting to self-signed.badssl.com returns an error" { + const sock = try std.net.tcpConnectToHost(std.testing.allocator, "self-signed.badssl.com", 443); + defer sock.close(); + + var fbs = std.io.fixedBufferStream(@embedFile("../test/DigiCertGlobalRootCA.crt.pem")); + var trusted_chain = try x509.TrustAnchorChain.from_pem(std.testing.allocator, fbs.reader()); + defer trusted_chain.deinit(); + + // @TODO Remove this once std.crypto.rand works in .evented mode + var rand = blk: { + var seed: [std.rand.DefaultCsprng.secret_seed_length]u8 = undefined; + try std.os.getrandom(&seed); + break :blk &std.rand.DefaultCsprng.init(seed).random; + }; + + std.testing.expectError(error.CertificateVerificationFailed, client_connect(.{ + .rand = rand, + .reader = sock.reader(), + .writer = sock.writer(), + .cert_verifier = .default, + .temp_allocator = std.testing.allocator, + .trusted_certificates = trusted_chain.data.items, + }, "self-signed.badssl.com")); +} diff --git a/libs/iguanatls/src/x509.zig b/libs/iguanatls/src/x509.zig index 003622cf5aae81bb3e0ab71428ecca73a475c3a8..e5aa1589d1d5d363064e4dd759a8f037364b6c08 100644 --- a/libs/iguanatls/src/x509.zig +++ b/libs/iguanatls/src/x509.zig @@ -21,6 +21,8 @@ pub const CurveId = enum { // zig fmt: on pub const PublicKey = union(enum) { + pub const empty = PublicKey{ .ec = .{ .id = undefined, .curve_point = &[0]u8{} } }; + /// RSA public key rsa: struct { //Positive std.math.big.int.Const numbers. @@ -45,7 +47,7 @@ pub const PublicKey = union(enum) { } pub fn eql(self: @This(), other: @This()) bool { - if (@as(@TagType(@This()), self) != @as(@TagType(@This()), other)) + if (@as(std.meta.Tag(@This()), self) != @as(std.meta.Tag(@This()), other)) return false; switch (self) { .rsa => |mod_exp| return mem.eql(usize, mod_exp.exponent, other.rsa.exponent) and @@ -63,7 +65,7 @@ pub fn parse_public_key(allocator: *Allocator, reader: anytype) !PublicKey { if ((try reader.readByte()) != 0x06) return error.MalformedDER; const oid_bytes = try asn1.der.parse_length(reader); - if (oid_bytes == 9 ) { + if (oid_bytes == 9) { // @TODO This fails in async if merged with the if if (!try reader.isBytes(&[9]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1 })) return error.MalformedDER; @@ -127,16 +129,15 @@ pub fn parse_public_key(allocator: *Allocator, reader: anytype) !PublicKey { // 1.3.132.0.{34, 35} const last_byte = try reader.readByte(); if (last_byte == 0x22) - key.ec = .{ .id = .secp384r1, .curve_point = undefined } + key = .{ .ec = .{ .id = .secp384r1, .curve_point = undefined } } else if (last_byte == 0x23) - key.ec = .{ .id = .secp521r1, .curve_point = undefined } + key = .{ .ec = .{ .id = .secp521r1, .curve_point = undefined } } else return error.MalformedDER; - } else if (curve_oid_bytes == 8) - { + } else if (curve_oid_bytes == 8) { if (!try reader.isBytes(&[8]u8{ 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x3, 0x1, 0x7 })) return error.MalformedDER; - key.ec = .{ .id = .secp256r1, .curve_point = undefined }; + key = .{ .ec = .{ .id = .secp256r1, .curve_point = undefined } }; } else { return error.MalformedDER; } @@ -150,7 +151,7 @@ pub fn parse_public_key(allocator: *Allocator, reader: anytype) !PublicKey { return error.MalformedDER; const bit_memory = try allocator.alloc(u8, std.math.divCeil(usize, bit_count, 8) catch unreachable); errdefer allocator.free(bit_memory); - try reader.readNoEof(bit_memory[0.. byte_len - 1]); + try reader.readNoEof(bit_memory[0 .. byte_len - 1]); key.ec.curve_point = bit_memory; return key; @@ -199,14 +200,21 @@ pub const TrustAnchor = struct { const data = object_id.object_identifier.data; // Basic constraints extension - if (data[0] != 2 or data[1] != 5 or data[2] != 29 or data[3] != 15) + if (data[0] != 2 or data[1] != 5 or data[2] != 29 or data[3] != 19) return; const basic_constraints = try asn1.der.parse_value(state.allocator, reader); defer basic_constraints.deinit(state.allocator); - if (basic_constraints != .bool) - return error.DoesNotMatchSchema; - state.self.is_ca = basic_constraints.bool; + + switch (basic_constraints) { + .bool => |b| state.self.is_ca = true, + .octet_string => |s| { + if (s.len != 5 or s[0] != 0x30 or s[1] != 0x03 or s[2] != 0x01 or s[3] != 0x01) + return error.DoesNotMatchSchema; + state.self.is_ca = s[4] != 0x00; + }, + else => return error.DoesNotMatchSchema, + } } fn initExtensions(state: *CaptureState, tag_byte: u8, length: usize, reader: anytype) !void { @@ -344,17 +352,7 @@ pub const TrustAnchorChain = struct { var buffered = std.io.bufferedReader(cert_reader); const anchor = try TrustAnchor.create(allocator, buffered.reader()); errdefer anchor.deinit(allocator); - - // This read forces the cert reader to find the `-----END` - // @TODO Should work without this read, investigate - _ = cert_reader.readByte() catch |err| switch (err) { - error.EndOfStream => { - try self.data.append(anchor); - break; - }, - else => |e| return e, - }; - return error.MalformedDER; + try self.data.append(anchor); } return self; } @@ -367,75 +365,54 @@ pub const TrustAnchorChain = struct { }; fn PEMSectionReader(comptime Reader: type) type { - const ReadError = Reader.Error || error{MalformedPEM}; - const S = struct { - pub fn read(self: *PEMCertificateIterator(Reader), buffer: []u8) ReadError!usize { - const end = "-----END "; - var end_letters_matched: ?usize = null; - + const Error = Reader.Error || error{MalformedPEM}; + const read = struct { + fn f(it: *PEMCertificateIterator(Reader), buf: []u8) Error!usize { var out_idx: usize = 0; - if (self.waiting_chars_len > 0) { - const rest_written = std.math.min(self.waiting_chars_len, buffer.len); + if (it.waiting_chars_len > 0) { + const rest_written = std.math.min(it.waiting_chars_len, buf.len); while (out_idx < rest_written) : (out_idx += 1) { - buffer[out_idx] = self.waiting_chars[out_idx]; + buf[out_idx] = it.waiting_chars[out_idx]; } - self.waiting_chars_len -= rest_written; - if (self.waiting_chars_len != 0) { - std.mem.copy(u8, self.waiting_chars[0..], self.waiting_chars[rest_written..]); + it.waiting_chars_len -= rest_written; + if (it.waiting_chars_len != 0) { + std.mem.copy(u8, it.waiting_chars[0..], it.waiting_chars[rest_written..]); } - if (out_idx == buffer.len) { + if (out_idx == buf.len) { return out_idx; } } + if (it.state != .in_section) + return out_idx; var base64_buf: [4]u8 = undefined; var base64_idx: usize = 0; - while (true) { - var byte = self.reader.readByte() catch |err| switch (err) { - error.EndOfStream => { - if (self.skip_to_newline_exit) { - self.state = .none; - return 0; - } - return error.MalformedPEM; - }, + const byte = it.reader.readByte() catch |err| switch (err) { + error.EndOfStream => return out_idx, else => |e| return e, }; - if (self.skip_to_newline_exit) { - if (byte == '\n') { - self.skip_to_newline_exit = false; - self.state = .none; - return 0; - } - continue; - } - - if (byte == '\n' or byte == '\r') { - self.empty_line = true; - continue; - } - defer self.empty_line = false; - - if (end_letters_matched) |*matched| { - if (end[matched.*] == byte) { - matched.* += 1; - if (matched.* == end.len) { - self.skip_to_newline_exit = true; - if (out_idx > 0) - return out_idx - else - continue; - } - continue; + if (byte == '-') { + if (it.reader.isBytes("----END ") catch |err| switch (err) { + error.EndOfStream => return error.MalformedPEM, + else => |e| return e, + }) { + try it.reader.skipUntilDelimiterOrEof('\n'); + it.state = .none; + return out_idx; } else return error.MalformedPEM; - } else if (self.empty_line and end[0] == byte) { - end_letters_matched = 1; + } else if (byte == '\r') { + if ((it.reader.readByte() catch |err| switch (err) { + error.EndOfStream => return error.MalformedPEM, + else => |e| return e, + }) != '\n') + return error.MalformedPEM; + continue; + } else if (byte == '\n') continue; - } base64_buf[base64_idx] = byte; base64_idx += 1; @@ -444,8 +421,9 @@ fn PEMSectionReader(comptime Reader: type) type { const out_len = std.base64.standard_decoder.calcSize(&base64_buf) catch return error.MalformedPEM; - const rest_chars = if (out_len > buffer.len - out_idx) - out_len - (buffer.len - out_idx) + + const rest_chars = if (out_len > buf.len - out_idx) + out_len - (buf.len - out_idx) else 0; const buf_chars = out_len - rest_chars; @@ -455,21 +433,26 @@ fn PEMSectionReader(comptime Reader: type) type { var i: u3 = 0; while (i < buf_chars) : (i += 1) { - buffer[out_idx] = res_buffer[i]; + buf[out_idx] = res_buffer[i]; out_idx += 1; } if (rest_chars > 0) { - mem.copy(u8, &self.waiting_chars, res_buffer[i..]); - self.waiting_chars_len = @intCast(u2, rest_chars); + mem.copy(u8, &it.waiting_chars, res_buffer[i..]); + it.waiting_chars_len = @intCast(u2, rest_chars); } - if (out_idx == buffer.len) + if (out_idx == buf.len) return out_idx; } } } - }; - return std.io.Reader(*PEMCertificateIterator(Reader), ReadError, S.read); + }.f; + + return std.io.Reader( + *PEMCertificateIterator(Reader), + Error, + read, + ); } fn PEMCertificateIterator(comptime Reader: type) type { @@ -479,141 +462,86 @@ fn PEMCertificateIterator(comptime Reader: type) type { reader: Reader, // Internal state for the iterator and the current reader. - skip_to_newline_exit: bool = false, - empty_line: bool = false, - waiting_chars: [4]u8 = undefined, - waiting_chars_len: u2 = 0, state: enum { none, - in_section_name, - in_cert, in_other, + in_section, } = .none, + waiting_chars: [4]u8 = undefined, + waiting_chars_len: u2 = 0, + // @TODO More verification, this will accept lots of invalid PEM pub fn next(self: *@This()) NextError!?SectionReader { - const end = "-----END "; - const begin = "-----BEGIN "; - const certificate = "CERTIFICATE"; - const x509_certificate = "X.509 CERTIFICATE"; - - var line_empty = true; - var end_letters_matched: ?usize = null; - var begin_letters_matched: ?usize = null; - var certificate_letters_matched: ?usize = null; - var x509_certificate_letters_matched: ?usize = null; - var skip_to_newline = false; - var return_after_skip = false; - - var base64_buf: [4]u8 = undefined; - var base64_buf_idx: usize = 0; - - // Called next before reading all of the previous cert. - if (self.state == .in_cert) { - self.waiting_chars_len = 0; - } - + self.waiting_chars_len = 0; while (true) { - var last_byte = false; const byte = self.reader.readByte() catch |err| switch (err) { - error.EndOfStream => blk: { - if (line_empty and self.state == .none) { - return null; - } else { - last_byte = true; - break :blk '\n'; - } - }, + error.EndOfStream => if (self.state == .none) + return null + else + return error.EndOfStream, else => |e| return e, }; - if (skip_to_newline) { - if (last_byte) - return null; - if (byte == '\n') { - if (return_after_skip) { - return SectionReader{ .context = self }; - } - skip_to_newline = false; - line_empty = true; - } - continue; - } else if (byte == '\r' or byte == '\n') { - line_empty = true; - continue; - } - - defer line_empty = byte == '\n' or (line_empty and byte == ' '); - switch (self.state) { - .none => { - if (begin_letters_matched) |*matched| { - if (begin[matched.*] != byte) - return error.MalformedPEM; - - matched.* += 1; - if (matched.* == begin.len) { - self.state = .in_section_name; - line_empty = true; - begin_letters_matched = null; - } - } else if (begin[0] == byte) { - begin_letters_matched = 1; - } else if (mem.indexOfScalar(u8, &std.ascii.spaces, byte) != null) { - if (last_byte) return null; - } else return error.MalformedPEM; + .none => switch (byte) { + '#' => { + try self.reader.skipUntilDelimiterOrEof('\n'); + continue; + }, + '\r', '\n', ' ', '\t' => continue, + '-' => { + if (try self.reader.isBytes("----BEGIN ")) { + const first_section_byte = try self.reader.readByte(); + if (first_section_byte == 'C') { + const rest = "ERTIFICATE"; + var matched: usize = 0; + while ((try self.reader.readByte()) == rest[matched]) : (matched += 1) { + if (matched == rest.len - 1) { + try self.reader.skipUntilDelimiterOrEof('\n'); + self.state = .in_section; + return SectionReader{ .context = self }; + } + } + try self.reader.skipUntilDelimiterOrEof('\n'); + self.state = .in_other; + continue; + } else if (first_section_byte == 'X') { + const rest = ".509 CERTIFICATE"; + var matched: usize = 0; + while ((try self.reader.readByte()) == rest[matched]) : (matched += 1) { + if (matched == rest.len - 1) { + try self.reader.skipUntilDelimiterOrEof('\n'); + self.state = .in_section; + return SectionReader{ .context = self }; + } + } + try self.reader.skipUntilDelimiterOrEof('\n'); + self.state = .in_other; + continue; + } else { + try self.reader.skipUntilDelimiterOrEof('\n'); + self.state = .in_other; + continue; + } + } else return error.MalformedPEM; + }, + else => return error.MalformedPEM, }, - .in_section_name => { - if (certificate_letters_matched) |*matched| { - if (certificate[matched.*] != byte) { - self.state = .in_other; - skip_to_newline = true; - continue; - } - matched.* += 1; - if (matched.* == certificate.len) { - self.state = .in_cert; - certificate_letters_matched = null; - skip_to_newline = true; - return_after_skip = true; - } - } else if (x509_certificate_letters_matched) |*matched| { - if (x509_certificate[matched.*] != byte) { - self.state = .in_other; - skip_to_newline = true; - continue; - } - matched.* += 1; - if (matched.* == x509_certificate.len) { - self.state = .in_cert; - x509_certificate_letters_matched = null; - skip_to_newline = true; - return_after_skip = true; - } - } else if (line_empty and certificate[0] == byte) { - certificate_letters_matched = 1; - } else if (line_empty and x509_certificate[0] == byte) { - x509_certificate_letters_matched = 1; - } else if (line_empty) { - self.state = .in_other; - skip_to_newline = true; - } else unreachable; - }, - .in_other, .in_cert => { - if (end_letters_matched) |*matched| { - if (end[matched.*] != byte) { - end_letters_matched = null; - skip_to_newline = true; - continue; - } - matched.* += 1; - if (matched.* == end.len) { + .in_other, .in_section => switch (byte) { + '#' => { + try self.reader.skipUntilDelimiterOrEof('\n'); + continue; + }, + '\r', '\n', ' ', '\t' => continue, + '-' => { + if (try self.reader.isBytes("----END ")) { + try self.reader.skipUntilDelimiterOrEof('\n'); self.state = .none; - end_letters_matched = null; - skip_to_newline = true; - } - } else if (line_empty and end[0] == byte) { - end_letters_matched = 1; - } + continue; + } else return error.MalformedPEM; + }, + // @TODO Make sure the character is base64 + else => continue, }, } } @@ -696,9 +624,8 @@ test "pemCertificateIterator" { // Try reading byte by byte from a cert reader { - var fbs = std.io.fixedBufferStream(github_pem ++ "\n" ++ github_pem); + var fbs = std.io.fixedBufferStream(github_pem ++ "\n# Some comment\n" ++ github_pem); var it = pemCertificateIterator(fbs.reader()); - // Read a couple of bytes from the first reader, then skip to the next { const first_reader = (try it.next()) orelse return error.NoCertificate; @@ -725,7 +652,70 @@ test "pemCertificateIterator" { } test "TrustAnchorChain" { - var fbs = std.io.fixedBufferStream(github_pem); + var fbs = std.io.fixedBufferStream(github_pem ++ + \\ + \\# Hellenic Academic and Research Institutions RootCA 2011 + \\-----BEGIN CERTIFICATE----- + \\MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix + \\RDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 + \\dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p + \\YyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw + \\NjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK + \\EztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl + \\cnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl + \\c2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB + \\BQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz + \\dYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ + \\fel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns + \\bgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD + \\75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP + \\FEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV + \\HRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp + \\5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu + \\b3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA + \\A4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p + \\6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 + \\TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7 + \\dIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys + \\Nnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI + \\l7WdmplNsDz4SgCbZN2fOUvRJ9e4 + \\-----END CERTIFICATE----- + \\ + \\# ePKI Root Certification Authority + \\-----BEGIN CERTIFICATE----- + \\MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe + \\MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 + \\ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe + \\Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw + \\IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL + \\SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF + \\AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH + \\SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh + \\ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X + \\DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 + \\TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ + \\fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA + \\sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU + \\WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS + \\nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH + \\dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip + \\NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC + \\AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF + \\MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH + \\ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB + \\uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl + \\PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP + \\JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ + \\gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 + \\j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 + \\5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB + \\o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS + \\/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z + \\Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE + \\W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D + \\hNQ+IIX3Sj0rnP0qCglN6oH4EZw= + \\-----END CERTIFICATE----- + ); const chain = try TrustAnchorChain.from_pem(std.testing.allocator, fbs.reader()); defer chain.deinit(); } diff --git a/libs/iguanatls/test/DigiCertGlobalRootCA.crt.pem b/libs/iguanatls/test/DigiCertGlobalRootCA.crt.pem new file mode 100644 index 0000000000000000000000000000000000000000..fd4341df26631d749ae1bd891e5b8780dac8c8bf --- /dev/null +++ b/libs/iguanatls/test/DigiCertGlobalRootCA.crt.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE-----