also looking at this
Harden encrypted mail storage, recovery, and forwarding
#8
50 files
+1701
-9001
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -33,7 +33,10 @@ | ||
| 33 | 33 | // internet connectivity. |
| 34 | 34 | .dependencies = .{ |
| 35 | 35 | .tls = .{ |
| 36 | .path = "vendor/zig-tls", | |
| 36 | // Keep TLS sourced from the canonical zig-utils/zig-tls repository | |
| 37 | // instead of carrying a divergent snapshot under vendor/. | |
| 38 | .url = "git+https://github.com/zig-utils/zig-tls.git#42f4ea2398868c6a1d45b99a2b1a518e13fb10f2", | |
| 39 | .hash = "zig_tls-0.1.1-CwcE5Jb4JwAOOgutzm4ZFMftuFwc3Bjdfc6ceyQRSXuq", | |
| 37 | 40 | }, |
| 38 | 41 | .search_engine = .{ |
| 39 | 42 | .path = "vendor/zig-search-engine", |
| @@ -176,13 +176,13 @@ fn hasReverseDns(allocator: std.mem.Allocator, ip: []const u8) bool { renamed throughout | ||
| 176 | 176 | }); |
| 177 | 177 | const rc1 = std.c.getaddrinfo(ip_z.ptr, null, &hints, &res); |
| 178 | 178 | defer if (res) |r| std.c.freeaddrinfo(r); |
| 179 | if (@intFromEnum(rc1) != 0) return true; // unparseable -> don't penalize | |
| 179 | if (@backingInt(rc1) != 0) return true; // unparseable -> don't penalize | |
| 180 | 180 | const node = res orelse return true; |
| 181 | 181 | const sa = node.addr orelse return true; |
| 182 | 182 | |
| 183 | 183 | var host_buf: [256]u8 = undefined; |
| 184 | 184 | const rc2 = std.c.getnameinfo(sa, node.addrlen, &host_buf, @intCast(host_buf.len), null, 0, .{}); |
| 185 | if (@intFromEnum(rc2) != 0) return true; // lookup error -> don't penalize | |
| 185 | if (@backingInt(rc2) != 0) return true; // lookup error -> don't penalize | |
| 186 | 186 | const host = std.mem.sliceTo(&host_buf, 0); |
| 187 | 187 | return !std.mem.eql(u8, host, ip); // numeric echo means no PTR |
| 188 | 188 | } |
| @@ -680,7 +680,7 @@ pub const Session = struct { | ||
| 680 | 680 | self.conn_wrapper.deinitTls(); |
| 681 | 681 | // Clean up TLS reader/writer - use stored alignment |
| 682 | 682 | if (self.tls_reader_info) |info| { |
| 683 | const log2_align: std.mem.Alignment = @enumFromInt(std.math.log2(info.alignment)); | |
| 683 | const log2_align: std.mem.Alignment = @fromBackingInt(@intCast(std.math.log2(info.alignment))); | |
| 684 | 684 | const ptr_bytes: [*]u8 = @ptrCast(info.ptr); |
| 685 | 685 | switch (info.alignment) { |
| 686 | 686 | 1 => self.allocator.rawFree(ptr_bytes[0..info.size], log2_align, @returnAddress()), |
| @@ -692,7 +692,7 @@ pub const Session = struct { | ||
| 692 | 692 | } |
| 693 | 693 | } |
| 694 | 694 | if (self.tls_writer_info) |info| { |
| 695 | const log2_align: std.mem.Alignment = @enumFromInt(std.math.log2(info.alignment)); | |
| 695 | const log2_align: std.mem.Alignment = @fromBackingInt(@intCast(std.math.log2(info.alignment))); | |
| 696 | 696 | const ptr_bytes: [*]u8 = @ptrCast(info.ptr); |
| 697 | 697 | switch (info.alignment) { |
| 698 | 698 | 1 => self.allocator.rawFree(ptr_bytes[0..info.size], log2_align, @returnAddress()), |
| @@ -1,22 +0,0 @@ | ||
| 1 | MIT License | |
| 2 | ||
| 3 | Copyright (c) 2024 zig-tls contributors | |
| 4 | Based on tls.zig by Igor Anić (https://github.com/ianic/tls.zig) | |
| 5 | ||
| 6 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 7 | of this software and associated documentation files (the "Software"), to deal | |
| 8 | in the Software without restriction, including without limitation the rights | |
| 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 10 | copies of the Software, and to permit persons to whom the Software is | |
| 11 | furnished to do so, subject to the following conditions: | |
| 12 | ||
| 13 | The above copyright notice and this permission notice shall be included in all | |
| 14 | copies or substantial portions of the Software. | |
| 15 | ||
| 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 22 | SOFTWARE. | |
| @@ -1,62 +0,0 @@ | ||
| 1 | # zig-tls | |
| 2 | ||
| 3 | Pure Zig TLS 1.3 implementation for SMTP and other protocols. | |
| 4 | ||
| 5 | ## Features | |
| 6 | ||
| 7 | - TLS 1.3 client and server support | |
| 8 | - Pure Zig implementation (no C dependencies) | |
| 9 | - Non-blocking I/O | |
| 10 | - STARTTLS support for protocol upgrades | |
| 11 | - Certificate and private key management | |
| 12 | ||
| 13 | ## Installation | |
| 14 | ||
| 15 | Add to your `build.zig.zon`: | |
| 16 | ||
| 17 | ```zig | |
| 18 | .dependencies = .{ | |
| 19 | .tls = .{ | |
| 20 | .path = "../zig-tls", | |
| 21 | }, | |
| 22 | }, | |
| 23 | ``` | |
| 24 | ||
| 25 | Add to your `build.zig`: | |
| 26 | ||
| 27 | ```zig | |
| 28 | const tls = b.dependency("tls", .{ | |
| 29 | .target = target, | |
| 30 | .optimize = optimize, | |
| 31 | }); | |
| 32 | exe.root_module.addImport("tls", tls.module("tls")); | |
| 33 | ``` | |
| 34 | ||
| 35 | ## Usage | |
| 36 | ||
| 37 | ### Server Example | |
| 38 | ||
| 39 | ```zig | |
| 40 | const tls = @import("tls"); | |
| 41 | ||
| 42 | // Load certificate and key | |
| 43 | var cert_key = try tls.config.CertKeyPair.fromFilePathAbsolute( | |
| 44 | allocator, | |
| 45 | "/path/to/cert.pem", | |
| 46 | "/path/to/key.pem", | |
| 47 | ); | |
| 48 | defer cert_key.deinit(allocator); | |
| 49 | ||
| 50 | // Create TLS connection from stream | |
| 51 | const tls_conn = try tls.serverFromStream(stream, .{ | |
| 52 | .auth = &cert_key, | |
| 53 | }); | |
| 54 | ||
| 55 | // Read/write through TLS | |
| 56 | const n = try tls_conn.read(buffer); | |
| 57 | try tls_conn.write(data); | |
| 58 | ``` | |
| 59 | ||
| 60 | ## License | |
| 61 | ||
| 62 | MIT - Based on https://github.com/ianic/tls.zig | |
| @@ -1,27 +0,0 @@ | ||
| 1 | const std = ; | |
| 2 | ||
| 3 | pub fn build(b: *std.Build) void { | |
| 4 | const target = b.standardTargetOptions(.{}); | |
| 5 | const optimize = b.standardOptimizeOption(.{}); | |
| 6 | ||
| 7 | // Main TLS module | |
| 8 | const tls_module = b.addModule("tls", .{ | |
| 9 | .root_source_file = b.path("src/root.zig"), | |
| 10 | .target = target, | |
| 11 | .optimize = optimize, | |
| 12 | }); | |
| 13 | _ = tls_module; | |
| 14 | ||
| 15 | // Unit tests | |
| 16 | const lib_mod = b.createModule(.{ | |
| 17 | .root_source_file = b.path("src/root.zig"), | |
| 18 | .target = target, | |
| 19 | .optimize = optimize, | |
| 20 | }); | |
| 21 | const unit_tests = b.addTest(.{ | |
| 22 | .root_module = lib_mod, | |
| 23 | }); | |
| 24 | const run_unit_tests = b.addRunArtifact(unit_tests); | |
| 25 | const test_step = b.step("test", "Run unit tests"); | |
| 26 | test_step.dependOn(&run_unit_tests.step); | |
| 27 | } | |
| @@ -1,141 +0,0 @@ | ||
| 1 | //! Kernel structs and constants from: /usr/include/linux/tls.h or | |
| 2 | //! https://github.com/torvalds/linux/blob/master/include/uapi/linux/tls.h | |
| 3 | ||
| 4 | const std = ; | |
| 5 | const mem = std.mem; | |
| 6 | const Cipher = .Cipher; | |
| 7 | ||
| 8 | const Ktls = @This(); | |
| 9 | ||
| 10 | const U = union(enum) { | |
| 11 | aes_gcm_128: AesGcm128, | |
| 12 | aes_gcm_256: AesGcm256, | |
| 13 | chacha20_poly1305: Chacha20Poly1305, | |
| 14 | }; | |
| 15 | ||
| 16 | tx: U, | |
| 17 | rx: U, | |
| 18 | ||
| 19 | pub fn txBytes(k: *Ktls) []const u8 { | |
| 20 | return switch (k.tx) { | |
| 21 | inline else => |*v| mem.asBytes(v), | |
| 22 | }; | |
| 23 | } | |
| 24 | ||
| 25 | pub fn rxBytes(k: *Ktls) []const u8 { | |
| 26 | return switch (k.rx) { | |
| 27 | inline else => |*v| mem.asBytes(v), | |
| 28 | }; | |
| 29 | } | |
| 30 | ||
| 31 | pub const VERSION_1_2 = 0x0303; | |
| 32 | pub const VERSION_1_3 = 0x0304; | |
| 33 | pub const TX = 1; | |
| 34 | pub const RX = 2; | |
| 35 | ||
| 36 | pub const AES_GCM_128 = 51; | |
| 37 | pub const AES_GCM_256 = 52; | |
| 38 | pub const CHACHA20_POLY1305 = 54; | |
| 39 | ||
| 40 | pub const Info = extern struct { | |
| 41 | version: u16 = VERSION_1_3, | |
| 42 | cipher_type: u16 = mem.zeroes(u16), | |
| 43 | }; | |
| 44 | pub const AesGcm128 = extern struct { | |
| 45 | info: Info = mem.zeroes(Info), | |
| 46 | iv: [8]u8 = mem.zeroes([8]u8), | |
| 47 | key: [16]u8 = mem.zeroes([16]u8), | |
| 48 | salt: [4]u8 = mem.zeroes([4]u8), | |
| 49 | rec_seq: [8]u8 = mem.zeroes([8]u8), | |
| 50 | }; | |
| 51 | pub const AesGcm256 = extern struct { | |
| 52 | info: Info = mem.zeroes(Info), | |
| 53 | iv: [8]u8 = mem.zeroes([8]u8), | |
| 54 | key: [32]u8 = mem.zeroes([32]u8), | |
| 55 | salt: [4]u8 = mem.zeroes([4]u8), | |
| 56 | rec_seq: [8]u8 = mem.zeroes([8]u8), | |
| 57 | }; | |
| 58 | pub const Chacha20Poly1305 = extern struct { | |
| 59 | info: Info = mem.zeroes(Info), | |
| 60 | iv: [12]u8 = mem.zeroes([12]u8), | |
| 61 | key: [32]u8 = mem.zeroes([32]u8), | |
| 62 | salt: [0]u8 = mem.zeroes([0]u8), | |
| 63 | rec_seq: [8]u8 = mem.zeroes([8]u8), | |
| 64 | }; | |
| 65 | ||
| 66 | pub fn init(cipher: Cipher) Ktls { | |
| 67 | switch (cipher) { | |
| 68 | // TLS 1.3 ciphers | |
| 69 | .AES_128_GCM_SHA256 => |keys| { | |
| 70 | const k = makeKeys(AesGcm128, .{ .cipher_type = AES_GCM_128 }, keys); | |
| 71 | return .{ | |
| 72 | .tx = .{ .aes_gcm_128 = k.tx }, | |
| 73 | .rx = .{ .aes_gcm_128 = k.rx }, | |
| 74 | }; | |
| 75 | }, | |
| 76 | .AES_256_GCM_SHA384 => |keys| { | |
| 77 | const k = makeKeys(AesGcm256, .{ .cipher_type = AES_GCM_256 }, keys); | |
| 78 | return .{ | |
| 79 | .tx = .{ .aes_gcm_256 = k.tx }, | |
| 80 | .rx = .{ .aes_gcm_256 = k.rx }, | |
| 81 | }; | |
| 82 | }, | |
| 83 | .CHACHA20_POLY1305_SHA256 => |keys| { | |
| 84 | const k = makeKeys(Chacha20Poly1305, .{ .cipher_type = CHACHA20_POLY1305 }, keys); | |
| 85 | return .{ | |
| 86 | .tx = .{ .chacha20_poly1305 = k.tx }, | |
| 87 | .rx = .{ .chacha20_poly1305 = k.rx }, | |
| 88 | }; | |
| 89 | }, | |
| 90 | ||
| 91 | // TLS 1.2 ciphers | |
| 92 | .ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, | |
| 93 | .ECDHE_RSA_WITH_AES_128_GCM_SHA256, | |
| 94 | => |keys| { | |
| 95 | const k = makeKeys(AesGcm128, .{ .cipher_type = AES_GCM_128, .version = VERSION_1_2 }, keys); | |
| 96 | return .{ | |
| 97 | .tx = .{ .aes_gcm_128 = k.tx }, | |
| 98 | .rx = .{ .aes_gcm_128 = k.rx }, | |
| 99 | }; | |
| 100 | }, | |
| 101 | .ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, | |
| 102 | .ECDHE_RSA_WITH_AES_256_GCM_SHA384, | |
| 103 | => |keys| { | |
| 104 | const k = makeKeys(AesGcm256, .{ .cipher_type = AES_GCM_256, .version = VERSION_1_2 }, keys); | |
| 105 | return .{ | |
| 106 | .tx = .{ .aes_gcm_256 = k.tx }, | |
| 107 | .rx = .{ .aes_gcm_256 = k.rx }, | |
| 108 | }; | |
| 109 | }, | |
| 110 | .ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, | |
| 111 | .ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, | |
| 112 | => |keys| { | |
| 113 | const k = makeKeys(Chacha20Poly1305, .{ .cipher_type = CHACHA20_POLY1305, .version = VERSION_1_2 }, keys); | |
| 114 | return .{ | |
| 115 | .tx = .{ .chacha20_poly1305 = k.tx }, | |
| 116 | .rx = .{ .chacha20_poly1305 = k.rx }, | |
| 117 | }; | |
| 118 | }, | |
| 119 | ||
| 120 | else => unreachable, | |
| 121 | } | |
| 122 | } | |
| 123 | ||
| 124 | fn makeKeys(comptime T: type, info: Info, keys: anytype) struct { tx: T, rx: T } { | |
| 125 | return .{ | |
| 126 | .tx = makeKey(T, info, keys.encrypt_iv, keys.encrypt_key, keys.encrypt_seq), | |
| 127 | .rx = makeKey(T, info, keys.decrypt_iv, keys.decrypt_key, keys.decrypt_seq), | |
| 128 | }; | |
| 129 | } | |
| 130 | ||
| 131 | fn makeKey(comptime T: type, info: Info, iv: anytype, key: anytype, seq: u64) T { | |
| 132 | const salt_size = @sizeOf(@FieldType(T, "salt")); | |
| 133 | var t: T = .{ | |
| 134 | .info = info, | |
| 135 | .salt = iv[0..salt_size].*, | |
| 136 | .iv = if (iv.len > salt_size) iv[salt_size..].* else @splat(0), | |
| 137 | .key = key, | |
| 138 | }; | |
| 139 | mem.writeInt(u64, &t.rec_seq, seq, .big); | |
| 140 | return t; | |
| 141 | } | |
| @@ -1,267 +0,0 @@ | ||
| 1 | const std = ; | |
| 2 | const Allocator = std.mem.Allocator; | |
| 3 | const Certificate = std.crypto.Certificate; | |
| 4 | const der = Certificate.der; | |
| 5 | const rsa = ; | |
| 6 | const base64 = std.base64.standard.decoderWithIgnore(" \t\r\n"); | |
| 7 | const proto = ; | |
| 8 | ||
| 9 | const max_ecdsa_key_len = 66; | |
| 10 | ||
| 11 | signature_scheme: proto.SignatureScheme, | |
| 12 | ||
| 13 | key: union { | |
| 14 | rsa: rsa.KeyPair, | |
| 15 | ecdsa: [max_ecdsa_key_len]u8, | |
| 16 | }, | |
| 17 | ||
| 18 | const PrivateKey = @This(); | |
| 19 | ||
| 20 | pub fn fromFile(_: Allocator, file: std.Io.File) !PrivateKey { | |
| 21 | // Read file using posix since Zig 0.16 File.reader requires Io context | |
| 22 | const fd = file.handle; | |
| 23 | var buf: [8192]u8 = undefined; | |
| 24 | var total: usize = 0; | |
| 25 | while (total < buf.len) { | |
| 26 | const n = std.posix.read(fd, buf[total..]) catch break; | |
| 27 | if (n == 0) break; | |
| 28 | total += n; | |
| 29 | } | |
| 30 | return try parsePem(buf[0..total]); | |
| 31 | } | |
| 32 | ||
| 33 | pub fn parsePem(buf: []const u8) !PrivateKey { | |
| 34 | const key_start, const key_end, const marker_version = try findKey(buf); | |
| 35 | const encoded = std.mem.trim(u8, buf[key_start..key_end], " \t\r\n"); | |
| 36 | ||
| 37 | // required bytes: | |
| 38 | // 2412, 1821, 1236 for rsa 4096, 3072, 2048 bits size keys | |
| 39 | var decoded: [4096]u8 = undefined; | |
| 40 | const n = try base64.decode(&decoded, encoded); | |
| 41 | ||
| 42 | if (marker_version == 2) { | |
| 43 | return try parseEcDer(decoded[0..n]); | |
| 44 | } | |
| 45 | return try parseDer(decoded[0..n]); | |
| 46 | } | |
| 47 | ||
| 48 | fn findKey(buf: []const u8) !struct { usize, usize, usize } { | |
| 49 | const markers = [_]struct { | |
| 50 | begin: []const u8, | |
| 51 | end: []const u8, | |
| 52 | }{ | |
| 53 | .{ .begin = "-----BEGIN PRIVATE KEY-----", .end = "-----END PRIVATE KEY-----" }, | |
| 54 | .{ .begin = "-----BEGIN EC PRIVATE KEY-----", .end = "-----END EC PRIVATE KEY-----" }, | |
| 55 | }; | |
| 56 | ||
| 57 | for (markers, 1..) |marker, ver| { | |
| 58 | const begin_marker_start = std.mem.indexOfPos(u8, buf, 0, marker.begin) orelse continue; | |
| 59 | const key_start = begin_marker_start + marker.begin.len; | |
| 60 | const key_end = std.mem.indexOfPos(u8, buf, key_start, marker.end) orelse continue; | |
| 61 | ||
| 62 | return .{ key_start, key_end, ver }; | |
| 63 | } | |
| 64 | return error.MissingEndMarker; | |
| 65 | } | |
| 66 | ||
| 67 | // ref: https://asn1js.eu/#MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBKFkVJCtU9FR6egz3yNxKBwXd86cFzMYqyGb8hRc1zVvLdw-So_2FBtITp6jzYmFShZANiAAQ-CH3a1R0V6dFlTK8Rs4M4egrpPtdta0osysO0Zl8mkBiDsTlvJNqeAp7L2ItHgFW8k_CfhgQT6iLDacNMhKC4XOV07r_ePD-mmkvqvRmzfOowHUoVRhCKrOTmF_J9Syc | |
| 68 | pub fn parseDer(buf: []const u8) !PrivateKey { | |
| 69 | const info = try der.Element.parse(buf, 0); | |
| 70 | const version = try der.Element.parse(buf, info.slice.start); | |
| 71 | ||
| 72 | const algo_seq = try der.Element.parse(buf, version.slice.end); | |
| 73 | const algo_cat = try der.Element.parse(buf, algo_seq.slice.start); | |
| 74 | ||
| 75 | const key_str = try der.Element.parse(buf, algo_seq.slice.end); | |
| 76 | const key_seq = try der.Element.parse(buf, key_str.slice.start); | |
| 77 | const key_int = try der.Element.parse(buf, key_seq.slice.start); | |
| 78 | ||
| 79 | const category = try Certificate.parseAlgorithmCategory(buf, algo_cat); | |
| 80 | switch (category) { | |
| 81 | .rsaEncryption => { | |
| 82 | const modulus = try der.Element.parse(buf, key_int.slice.end); | |
| 83 | const public_exponent = try der.Element.parse(buf, modulus.slice.end); | |
| 84 | const private_exponent = try der.Element.parse(buf, public_exponent.slice.end); | |
| 85 | ||
| 86 | const public_key = try rsa.PublicKey.fromBytes(content(buf, modulus), content(buf, public_exponent)); | |
| 87 | const secret_key = try rsa.SecretKey.fromBytes(public_key.modulus, content(buf, private_exponent)); | |
| 88 | const key_pair = rsa.KeyPair{ .public = public_key, .secret = secret_key }; | |
| 89 | ||
| 90 | return .{ | |
| 91 | .signature_scheme = switch (key_pair.public.modulus.bits()) { | |
| 92 | 4096 => .rsa_pss_rsae_sha512, | |
| 93 | 3072 => .rsa_pss_rsae_sha384, | |
| 94 | else => .rsa_pss_rsae_sha256, | |
| 95 | }, | |
| 96 | .key = .{ .rsa = key_pair }, | |
| 97 | }; | |
| 98 | }, | |
| 99 | .X9_62_id_ecPublicKey => { | |
| 100 | const key = try der.Element.parse(buf, key_int.slice.end); | |
| 101 | const algo_param = try der.Element.parse(buf, algo_cat.slice.end); | |
| 102 | const named_curve = try Certificate.parseNamedCurve(buf, algo_param); | |
| 103 | return .{ | |
| 104 | .signature_scheme = signatureScheme(named_curve), | |
| 105 | .key = .{ .ecdsa = ecdsaKey(buf, key) }, | |
| 106 | }; | |
| 107 | }, | |
| 108 | else => unreachable, | |
| 109 | } | |
| 110 | } | |
| 111 | ||
| 112 | // References: | |
| 113 | // https://asn1js.eu/#MHcCAQEEINJSRKv8kSKEzLHptfAlg-LGh4_pHHlq0XLf30Q9pcztoAoGCCqGSM49AwEHoUQDQgAEJpmLyp8aGCgyMcFIJaIq_-4V1K6nPpeoih3bT2npeplF9eyXj7rm8eW9Ua6VLhq71mqtMC-YLm-IkORBVq1cuA | |
| 114 | // https://www.rfc-editor.org/rfc/rfc5915 | |
| 115 | pub fn parseEcDer(bytes: []const u8) !PrivateKey { | |
| 116 | const pki_msg = try der.Element.parse(bytes, 0); | |
| 117 | const version = try der.Element.parse(bytes, pki_msg.slice.start); | |
| 118 | const key = try der.Element.parse(bytes, version.slice.end); | |
| 119 | const parameters = try der.Element.parse(bytes, key.slice.end); | |
| 120 | const curve = try der.Element.parse(bytes, parameters.slice.start); | |
| 121 | const named_curve = try Certificate.parseNamedCurve(bytes, curve); | |
| 122 | return .{ | |
| 123 | .signature_scheme = signatureScheme(named_curve), | |
| 124 | .key = .{ .ecdsa = ecdsaKey(bytes, key) }, | |
| 125 | }; | |
| 126 | } | |
| 127 | ||
| 128 | fn signatureScheme(named_curve: Certificate.NamedCurve) proto.SignatureScheme { | |
| 129 | return switch (named_curve) { | |
| 130 | .X9_62_prime256v1 => .ecdsa_secp256r1_sha256, | |
| 131 | .secp384r1 => .ecdsa_secp384r1_sha384, | |
| 132 | .secp521r1 => .ecdsa_secp521r1_sha512, | |
| 133 | }; | |
| 134 | } | |
| 135 | ||
| 136 | fn ecdsaKey(bytes: []const u8, e: der.Element) [max_ecdsa_key_len]u8 { | |
| 137 | const data = content(bytes, e); | |
| 138 | var ecdsa_key: [max_ecdsa_key_len]u8 = undefined; | |
| 139 | @memcpy(ecdsa_key[0..data.len], data); | |
| 140 | return ecdsa_key; | |
| 141 | } | |
| 142 | ||
| 143 | fn content(bytes: []const u8, e: der.Element) []const u8 { | |
| 144 | return bytes[e.slice.start..e.slice.end]; | |
| 145 | } | |
| 146 | ||
| 147 | const testing = std.testing; | |
| 148 | const testu = ; | |
| 149 | ||
| 150 | test "parse ec pem" { | |
| 151 | const data = @embedFile("testdata/ec_private_key.pem"); | |
| 152 | var pk = try parsePem(data); | |
| 153 | const priv_key = &testu.hexToBytes( | |
| 154 | \\ 10 35 3d ca 1b 15 1d 06 aa 71 b8 ef f3 19 22 | |
| 155 | \\ 43 78 f3 20 98 1e b1 2f 2b 64 7e 71 d0 30 2a | |
| 156 | \\ 90 aa e5 eb 99 c3 90 65 3d c1 26 19 be 3f 08 | |
| 157 | \\ 20 9b 01 | |
| 158 | ); | |
| 159 | try testing.expectEqualSlices(u8, priv_key, pk.key.ecdsa[0..priv_key.len]); | |
| 160 | try testing.expectEqual(.ecdsa_secp384r1_sha384, pk.signature_scheme); | |
| 161 | } | |
| 162 | ||
| 163 | test "parse ec prime256v1" { | |
| 164 | const data = @embedFile("testdata/ec_prime256v1_private_key.pem"); | |
| 165 | var pk = try parsePem(data); | |
| 166 | const priv_key = &testu.hexToBytes( | |
| 167 | \\ d2 52 44 ab fc 91 22 84 cc b1 e9 b5 f0 25 83 | |
| 168 | \\ e2 c6 87 8f e9 1c 79 6a d1 72 df df 44 3d a5 | |
| 169 | \\ cc ed | |
| 170 | ); | |
| 171 | try testing.expectEqualSlices(u8, priv_key, pk.key.ecdsa[0..priv_key.len]); | |
| 172 | try testing.expectEqual(.ecdsa_secp256r1_sha256, pk.signature_scheme); | |
| 173 | } | |
| 174 | ||
| 175 | test "parse ec secp384r1" { | |
| 176 | const data = @embedFile("testdata/ec_secp384r1_private_key.pem"); | |
| 177 | var pk = try parsePem(data); | |
| 178 | const priv_key = &testu.hexToBytes( | |
| 179 | \\ ee 6d 8a 5e 0d d3 b0 c6 4b 32 40 80 e2 3a de | |
| 180 | \\ 8b 1e dd e2 92 db 36 1c db 91 ea ba a1 06 0d | |
| 181 | \\ 42 2d d9 a9 dc 05 43 29 f1 78 7c f9 08 af c5 | |
| 182 | \\ 03 1f 6d | |
| 183 | ); | |
| 184 | try testing.expectEqualSlices(u8, priv_key, pk.key.ecdsa[0..priv_key.len]); | |
| 185 | try testing.expectEqual(.ecdsa_secp384r1_sha384, pk.signature_scheme); | |
| 186 | } | |
| 187 | ||
| 188 | test "parse ec secp521r1" { | |
| 189 | const data = @embedFile("testdata/ec_secp521r1_private_key.pem"); | |
| 190 | var pk = try parsePem(data); | |
| 191 | const priv_key = &testu.hexToBytes( | |
| 192 | \\ 01 f0 2f 5a c7 24 18 ea 68 23 8c 2e a1 b4 b8 | |
| 193 | \\ dc f2 11 b2 96 b0 ec 87 80 42 bf de ba f4 96 | |
| 194 | \\ 83 8f 9b db c6 60 a7 4c d9 60 3a e4 ba 0b df | |
| 195 | \\ ae 24 d3 1b c2 6e 82 a0 88 c1 ed 17 20 0d 3a | |
| 196 | \\ f1 c5 7e e8 0b 27 | |
| 197 | ); | |
| 198 | try testing.expectEqualSlices(u8, priv_key, pk.key.ecdsa[0..priv_key.len]); | |
| 199 | try testing.expectEqual(.ecdsa_secp521r1_sha512, pk.signature_scheme); | |
| 200 | } | |
| 201 | ||
| 202 | test "parse rsa pem" { | |
| 203 | const data = @embedFile("testdata/rsa_private_key.pem"); | |
| 204 | const pk = try parsePem(data); | |
| 205 | ||
| 206 | // expected results from: | |
| 207 | // $ openssl pkey -in testdata/rsa_private_key.pem -text -noout | |
| 208 | const modulus = &testu.hexToBytes( | |
| 209 | \\ 00 de f7 23 e6 75 cc 6f dd d5 6e 0f 8c 09 f8 | |
| 210 | \\ 62 e3 60 1b c0 7d 8c d5 04 50 2c 36 e2 3b f7 | |
| 211 | \\ 33 9f a1 14 af be cf 1a 0f 4c f5 cb 39 70 0e | |
| 212 | \\ 3b 97 d6 21 f7 48 91 79 ca 7c 68 fc ea 62 a1 | |
| 213 | \\ 5a 72 4f 78 57 0e cc f2 a3 50 05 f1 4c ca 51 | |
| 214 | \\ 73 10 9a 18 8e 71 f5 b4 c7 3e be 4c ef 37 d4 | |
| 215 | \\ 84 4b 82 1c ec 08 a3 cc 07 3d 5c 0b e5 85 3f | |
| 216 | \\ fe b6 44 77 8f 3c 6a 2f 33 c3 5d f6 f2 29 46 | |
| 217 | \\ 04 25 7e 05 d9 f8 3b 2d a4 40 66 9f 0d 6d 1a | |
| 218 | \\ fa bc 0a c5 8b 86 43 30 ef 14 20 41 9d b5 cc | |
| 219 | \\ 3e 63 b5 48 04 27 c9 5c d3 62 28 5f f5 b6 e4 | |
| 220 | \\ 77 49 99 ac 84 4a a6 67 a5 9a 1a 37 c7 60 4c | |
| 221 | \\ ba c1 70 cf 57 64 4a 21 ea 05 53 10 ec 94 71 | |
| 222 | \\ 4a 43 04 83 00 aa 5a 28 bc f2 8c 58 14 92 d2 | |
| 223 | \\ 83 17 f4 7b 29 0f e7 87 a2 47 b2 53 19 12 23 | |
| 224 | \\ fb 4b ce 5a f8 a1 84 f9 b1 f3 bf e3 fa 10 f8 | |
| 225 | \\ ad af 87 ce 03 0e a0 2c 13 71 57 c4 55 44 48 | |
| 226 | \\ 44 cb | |
| 227 | ); | |
| 228 | const public_exponent = &testu.hexToBytes("01 00 01"); | |
| 229 | const private_exponent = &testu.hexToBytes( | |
| 230 | \\ 50 3b 80 98 aa a5 11 50 33 40 32 aa 02 e0 75 | |
| 231 | \\ bd 3a 55 62 34 0b 9c 8f bb c5 dd 4e 15 a4 03 | |
| 232 | \\ d8 9a 5f 56 4a 84 3d ed 69 95 3d 37 03 02 ac | |
| 233 | \\ 21 1c 36 06 c4 ff 4c 63 37 d7 93 c3 48 10 a5 | |
| 234 | \\ fa 62 6c 7c 6f 60 02 a4 0f e4 c3 8b 0d 76 b7 | |
| 235 | \\ c0 2e a3 4d 86 e6 92 d1 eb db 10 d6 38 31 ea | |
| 236 | \\ 15 3d d1 e8 81 c7 67 60 e7 8c 9a df 51 ce d0 | |
| 237 | \\ 7a 88 32 b9 c1 54 b8 7d 98 fc d4 23 1a 05 0e | |
| 238 | \\ f2 ea e1 72 29 28 2a 68 b7 90 18 80 1c 21 d6 | |
| 239 | \\ 36 a8 6b 4a 9c dd 14 b8 9f 85 ee 95 0b f4 c6 | |
| 240 | \\ 17 02 aa 4d ea 4d f9 39 d7 dd 9d b4 1d d2 f8 | |
| 241 | \\ 92 46 0f 18 41 80 f4 ea 27 55 29 f8 37 59 bf | |
| 242 | \\ 43 ec a3 eb 19 ba bc 13 06 95 3d 25 4b c9 72 | |
| 243 | \\ cf 41 0a 6f aa cb 79 d4 7b fa b1 09 7c e2 2f | |
| 244 | \\ 85 51 44 8b c6 97 8e 46 f9 6b ac 08 87 92 ce | |
| 245 | \\ af 0b bf 8c bd 27 51 8f 09 e4 d3 f9 04 ac fa | |
| 246 | \\ f2 04 70 3e d9 a6 28 17 c2 2d 74 e9 25 40 02 | |
| 247 | \\ 49 | |
| 248 | ); | |
| 249 | ||
| 250 | try testing.expectEqual(.rsa_pss_rsae_sha256, pk.signature_scheme); | |
| 251 | const kp = pk.key.rsa; | |
| 252 | { | |
| 253 | var bytes: [modulus.len]u8 = undefined; | |
| 254 | try kp.public.modulus.toBytes(&bytes, .big); | |
| 255 | try testing.expectEqualSlices(u8, modulus, &bytes); | |
| 256 | } | |
| 257 | { | |
| 258 | var bytes: [private_exponent.len]u8 = undefined; | |
| 259 | try kp.public.public_exponent.toBytes(&bytes, .big); | |
| 260 | try testing.expectEqualSlices(u8, public_exponent, bytes[bytes.len - public_exponent.len .. bytes.len]); | |
| 261 | } | |
| 262 | { | |
| 263 | var btytes: [private_exponent.len]u8 = undefined; | |
| 264 | try kp.secret.private_exponent.toBytes(&btytes, .big); | |
| 265 | try testing.expectEqualSlices(u8, private_exponent, &btytes); | |
| 266 | } | |
| 267 | } | |
| @@ -1,148 +0,0 @@ | ||
| 1 | // This file is originally copied from: https://github.com/jedisct1/zig-cbc. | |
| 2 | // | |
| 3 | // It is modified then to have TLS padding insead of PKCS#7 padding. | |
| 4 | // Reference: | |
| 5 | // https://datatracker.ietf.org/doc/html/rfc5246/#section-6.2.3.2 | |
| 6 | // https://crypto.stackexchange.com/questions/98917/on-the-correctness-of-the-padding-example-of-rfc-5246 | |
| 7 | // | |
| 8 | // If required padding i n bytes | |
| 9 | // PKCS#7 padding is (n...n) | |
| 10 | // TLS padding is (n-1...n-1) - n times of n-1 value | |
| 11 | // | |
| 12 | const std = ; | |
| 13 | const aes = std.crypto.core.aes; | |
| 14 | const mem = std.mem; | |
| 15 | const debug = std.debug; | |
| 16 | ||
| 17 | /// CBC mode with TLS 1.2 padding | |
| 18 | /// | |
| 19 | /// Important: the counter mode doesn't provide authenticated encryption: the ciphertext can be trivially modified without this being detected. | |
| 20 | /// If you need authenticated encryption, use anything from `std.crypto.aead` instead. | |
| 21 | /// If you really need to use CBC mode, make sure to use a MAC to authenticate the ciphertext. | |
| 22 | pub fn CBC(comptime BlockCipher: anytype) type { | |
| 23 | const EncryptCtx = aes.AesEncryptCtx(BlockCipher); | |
| 24 | const DecryptCtx = aes.AesDecryptCtx(BlockCipher); | |
| 25 | ||
| 26 | return struct { | |
| 27 | const Self = @This(); | |
| 28 | ||
| 29 | enc_ctx: EncryptCtx, | |
| 30 | dec_ctx: DecryptCtx, | |
| 31 | ||
| 32 | /// Initialize the CBC context with the given key. | |
| 33 | pub fn init(key: [BlockCipher.key_bits / 8]u8) Self { | |
| 34 | const enc_ctx = BlockCipher.initEnc(key); | |
| 35 | const dec_ctx = DecryptCtx.initFromEnc(enc_ctx); | |
| 36 | ||
| 37 | return Self{ .enc_ctx = enc_ctx, .dec_ctx = dec_ctx }; | |
| 38 | } | |
| 39 | ||
| 40 | /// Return the length of the ciphertext given the length of the plaintext. | |
| 41 | pub fn paddedLength(length: usize) usize { | |
| 42 | return (std.math.divCeil(usize, length + 1, EncryptCtx.block_length) catch unreachable) * EncryptCtx.block_length; | |
| 43 | } | |
| 44 | ||
| 45 | /// Encrypt the given plaintext for the given IV. | |
| 46 | /// The destination buffer must be large enough to hold the padded plaintext. | |
| 47 | /// Use the `paddedLength()` function to compute the ciphertext size. | |
| 48 | /// IV must be secret and unpredictable. | |
| 49 | pub fn encrypt(self: Self, dst: []u8, src: []const u8, iv: [EncryptCtx.block_length]u8) void { | |
| 50 | // Note: encryption *could* be parallelized, see https://research.kudelskisecurity.com/2022/11/17/some-aes-cbc-encryption-myth-busting/ | |
| 51 | const block_length = EncryptCtx.block_length; | |
| 52 | const padded_length = paddedLength(src.len); | |
| 53 | debug.assert(dst.len == padded_length); // destination buffer must hold the padded plaintext | |
| 54 | var cv = iv; | |
| 55 | var i: usize = 0; | |
| 56 | while (i + block_length <= src.len) : (i += block_length) { | |
| 57 | const in = src[i..][0..block_length]; | |
| 58 | for (cv[0..], in) |*x, y| x.* ^= y; | |
| 59 | self.enc_ctx.encrypt(&cv, &cv); | |
| 60 | @memcpy(dst[i..][0..block_length], &cv); | |
| 61 | } | |
| 62 | // Last block | |
| 63 | var in: [block_length]u8 = @splat(0); | |
| 64 | const padding_length: u8 = @intCast(padded_length - src.len - 1); | |
| 65 | @memset(&in, padding_length); | |
| 66 | @memcpy(in[0 .. src.len - i], src[i..]); | |
| 67 | for (cv[0..], in) |*x, y| x.* ^= y; | |
| 68 | self.enc_ctx.encrypt(&cv, &cv); | |
| 69 | @memcpy(dst[i..], cv[0 .. dst.len - i]); | |
| 70 | } | |
| 71 | ||
| 72 | /// Decrypt the given ciphertext for the given IV. | |
| 73 | /// The destination buffer must be large enough to hold the plaintext. | |
| 74 | /// IV must be secret, unpredictable and match the one used for encryption. | |
| 75 | pub fn decrypt(self: Self, dst: []u8, src: []const u8, iv: [DecryptCtx.block_length]u8) !void { | |
| 76 | const block_length = DecryptCtx.block_length; | |
| 77 | if (src.len != dst.len) { | |
| 78 | return error.EncodingError; | |
| 79 | } | |
| 80 | debug.assert(src.len % block_length == 0); | |
| 81 | var i: usize = 0; | |
| 82 | var cv = iv; | |
| 83 | var out: [block_length]u8 = undefined; | |
| 84 | // Decryption could be parallelized | |
| 85 | while (i + block_length <= dst.len) : (i += block_length) { | |
| 86 | const in = src[i..][0..block_length]; | |
| 87 | self.dec_ctx.decrypt(&out, in); | |
| 88 | for (&out, cv) |*x, y| x.* ^= y; | |
| 89 | cv = in.*; | |
| 90 | @memcpy(dst[i..][0..block_length], &out); | |
| 91 | } | |
| 92 | // Last block - We intentionally don't check the padding to mitigate timing attacks | |
| 93 | if (i < dst.len) { | |
| 94 | const in = src[i..][0..block_length]; | |
| 95 | @memset(&out, 0); | |
| 96 | self.dec_ctx.decrypt(&out, in); | |
| 97 | for (&out, cv) |*x, y| x.* ^= y; | |
| 98 | @memcpy(dst[i..], out[0 .. dst.len - i]); | |
| 99 | } | |
| 100 | } | |
| 101 | }; | |
| 102 | } | |
| 103 | ||
| 104 | test "CBC mode" { | |
| 105 | const M = CBC(aes.Aes128); | |
| 106 | const key = [_]u8{ 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c }; | |
| 107 | const iv = [_]u8{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }; | |
| 108 | const src_ = "This is a test of AES-CBC that goes on longer than a couple blocks. It is a somewhat long test case to type out!"; | |
| 109 | const expected = "\xA0\x8C\x09\x7D\xFF\x42\xB6\x65\x4D\x4B\xC6\x90\x90\x39\xDE\x3D\xC7\xCA\xEB\xF6\x9A\x4F\x09\x97\xC9\x32\xAB\x75\x88\xB7\x57\x17"; | |
| 110 | var res: [32]u8 = undefined; | |
| 111 | ||
| 112 | try comptime std.testing.expect(src_.len / M.paddedLength(1) >= 3); // Ensure that we have at least 3 blocks | |
| 113 | ||
| 114 | const z = M.init(key); | |
| 115 | ||
| 116 | // Test encryption and decryption with distinct buffers | |
| 117 | var h = std.crypto.hash.sha2.Sha256.init(.{}); | |
| 118 | inline for (0..src_.len) |len| { | |
| 119 | const src = src_[0..len]; | |
| 120 | var dst: [M.paddedLength(src.len)]u8 = @splat(0); | |
| 121 | ||
| 122 | z.encrypt(&dst, src, iv); | |
| 123 | h.update(&dst); | |
| 124 | ||
| 125 | var decrypted: [dst.len]u8 = @splat(0); | |
| 126 | try z.decrypt(&decrypted, &dst, iv); | |
| 127 | ||
| 128 | const padding = decrypted[decrypted.len - 1] + 1; | |
| 129 | try std.testing.expectEqualSlices(u8, src, decrypted[0 .. decrypted.len - padding]); | |
| 130 | } | |
| 131 | h.final(&res); | |
| 132 | try std.testing.expectEqualSlices(u8, expected, &res); | |
| 133 | ||
| 134 | // Test encryption and decryption with the same buffer | |
| 135 | h = std.crypto.hash.sha2.Sha256.init(.{}); | |
| 136 | inline for (0..src_.len) |len| { | |
| 137 | var buf: [M.paddedLength(len)]u8 = @splat(0); | |
| 138 | @memcpy(buf[0..len], src_[0..len]); | |
| 139 | z.encrypt(&buf, buf[0..len], iv); | |
| 140 | h.update(&buf); | |
| 141 | ||
| 142 | try z.decrypt(&buf, &buf, iv); | |
| 143 | ||
| 144 | try std.testing.expectEqualSlices(u8, src_[0..len], buf[0..len]); | |
| 145 | } | |
| 146 | h.final(&res); | |
| 147 | try std.testing.expectEqualSlices(u8, expected, &res); | |
| 148 | } | |
| @@ -1,1089 +0,0 @@ | ||
| 1 | const std = ; | |
| 2 | const crypto = std.crypto; | |
| 3 | const hkdfExpandLabel = crypto.tls.hkdfExpandLabel; | |
| 4 | ||
| 5 | const Sha1 = crypto.hash.Sha1; | |
| 6 | const Sha256 = crypto.hash.sha2.Sha256; | |
| 7 | const Sha384 = crypto.hash.sha2.Sha384; | |
| 8 | ||
| 9 | const record = ; | |
| 10 | const Record = record.Record; | |
| 11 | const Transcript = .Transcript; | |
| 12 | const proto = ; | |
| 13 | ||
| 14 | const csprng = .csprng; | |
| 15 | ||
| 16 | // tls 1.2 cbc cipher types | |
| 17 | const CbcAes128Sha1 = CbcType(crypto.core.aes.Aes128, Sha1); | |
| 18 | const CbcAes128Sha256 = CbcType(crypto.core.aes.Aes128, Sha256); | |
| 19 | const CbcAes256Sha384 = CbcType(crypto.core.aes.Aes256, Sha384); | |
| 20 | // tls 1.2 gcm cipher types | |
| 21 | const Aead12Aes128Gcm = Aead12Type(crypto.aead.aes_gcm.Aes128Gcm); | |
| 22 | const Aead12Aes256Gcm = Aead12Type(crypto.aead.aes_gcm.Aes256Gcm); | |
| 23 | // tls 1.2 chacha cipher type | |
| 24 | const Aead12ChaCha = Aead12ChaChaType(crypto.aead.chacha_poly.ChaCha20Poly1305); | |
| 25 | // tls 1.3 cipher types | |
| 26 | const Aes128GcmSha256 = Aead13Type(crypto.aead.aes_gcm.Aes128Gcm, Sha256); | |
| 27 | const Aes256GcmSha384 = Aead13Type(crypto.aead.aes_gcm.Aes256Gcm, Sha384); | |
| 28 | const ChaChaSha256 = Aead13Type(crypto.aead.chacha_poly.ChaCha20Poly1305, Sha256); | |
| 29 | const Aegis128Sha256 = Aead13Type(crypto.aead.aegis.Aegis128L, Sha256); | |
| 30 | ||
| 31 | pub const encrypt_overhead_tls_12: comptime_int = @max( | |
| 32 | CbcAes128Sha1.encrypt_overhead, | |
| 33 | CbcAes128Sha256.encrypt_overhead, | |
| 34 | CbcAes256Sha384.encrypt_overhead, | |
| 35 | Aead12Aes128Gcm.encrypt_overhead, | |
| 36 | Aead12Aes256Gcm.encrypt_overhead, | |
| 37 | Aead12ChaCha.encrypt_overhead, | |
| 38 | ); | |
| 39 | pub const encrypt_overhead_tls_13: comptime_int = @max( | |
| 40 | Aes128GcmSha256.encrypt_overhead, | |
| 41 | Aes256GcmSha384.encrypt_overhead, | |
| 42 | ChaChaSha256.encrypt_overhead, | |
| 43 | Aegis128Sha256.encrypt_overhead, | |
| 44 | ); | |
| 45 | ||
| 46 | // ref (length): https://www.rfc-editor.org/rfc/rfc8446#section-5.1 | |
| 47 | pub const max_cleartext_len = 1 << 14; | |
| 48 | // ref (length): https://www.rfc-editor.org/rfc/rfc8446#section-5.2 | |
| 49 | // The sum of the lengths of the content and the padding, plus one for the inner | |
| 50 | // content type, plus any expansion added by the AEAD algorithm. | |
| 51 | pub const max_ciphertext_len = max_cleartext_len + 256; | |
| 52 | pub const max_ciphertext_record_len = record.header_len + max_ciphertext_len; | |
| 53 | // max ciphertext produced with this implementation () | |
| 54 | pub const max_encrypted_record_len = max_cleartext_len + @max(encrypt_overhead_tls_13, encrypt_overhead_tls_12); | |
| 55 | ||
| 56 | test { | |
| 57 | try testing.expectEqual(16645, max_ciphertext_record_len); | |
| 58 | try testing.expectEqual(85, encrypt_overhead_tls_12); | |
| 59 | try testing.expectEqual(22, encrypt_overhead_tls_13); | |
| 60 | try testing.expectEqual(16469, max_encrypted_record_len); | |
| 61 | } | |
| 62 | ||
| 63 | /// Returns type for cipher suite tag. | |
| 64 | fn CipherType(comptime tag: CipherSuite) type { | |
| 65 | return switch (tag) { | |
| 66 | // tls 1.2 cbc | |
| 67 | .ECDHE_ECDSA_WITH_AES_128_CBC_SHA, | |
| 68 | .ECDHE_RSA_WITH_AES_128_CBC_SHA, | |
| 69 | .RSA_WITH_AES_128_CBC_SHA, | |
| 70 | => CbcAes128Sha1, | |
| 71 | .ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, | |
| 72 | .ECDHE_RSA_WITH_AES_128_CBC_SHA256, | |
| 73 | .RSA_WITH_AES_128_CBC_SHA256, | |
| 74 | => CbcAes128Sha256, | |
| 75 | .ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, | |
| 76 | .ECDHE_RSA_WITH_AES_256_CBC_SHA384, | |
| 77 | => CbcAes256Sha384, | |
| 78 | ||
| 79 | // tls 1.2 gcm | |
| 80 | .ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, | |
| 81 | .ECDHE_RSA_WITH_AES_128_GCM_SHA256, | |
| 82 | => Aead12Aes128Gcm, | |
| 83 | .ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, | |
| 84 | .ECDHE_RSA_WITH_AES_256_GCM_SHA384, | |
| 85 | => Aead12Aes256Gcm, | |
| 86 | ||
| 87 | // tls 1.2 chacha | |
| 88 | .ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, | |
| 89 | .ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, | |
| 90 | => Aead12ChaCha, | |
| 91 | ||
| 92 | // tls 1.3 | |
| 93 | .AES_128_GCM_SHA256 => Aes128GcmSha256, | |
| 94 | .AES_256_GCM_SHA384 => Aes256GcmSha384, | |
| 95 | .CHACHA20_POLY1305_SHA256 => ChaChaSha256, | |
| 96 | .AEGIS_128L_SHA256 => Aegis128Sha256, | |
| 97 | ||
| 98 | else => unreachable, | |
| 99 | }; | |
| 100 | } | |
| 101 | ||
| 102 | /// Provides initialization and common encrypt/decrypt methods for all supported | |
| 103 | /// ciphers. Tls 1.2 has only application cipher, tls 1.3 has separate cipher | |
| 104 | /// for handshake and application. | |
| 105 | pub const Cipher = union(CipherSuite) { | |
| 106 | // tls 1.2 cbc | |
| 107 | ECDHE_ECDSA_WITH_AES_128_CBC_SHA: CipherType(.ECDHE_ECDSA_WITH_AES_128_CBC_SHA), | |
| 108 | ECDHE_RSA_WITH_AES_128_CBC_SHA: CipherType(.ECDHE_RSA_WITH_AES_128_CBC_SHA), | |
| 109 | RSA_WITH_AES_128_CBC_SHA: CipherType(.RSA_WITH_AES_128_CBC_SHA), | |
| 110 | ||
| 111 | ECDHE_ECDSA_WITH_AES_128_CBC_SHA256: CipherType(.ECDHE_ECDSA_WITH_AES_128_CBC_SHA256), | |
| 112 | ECDHE_RSA_WITH_AES_128_CBC_SHA256: CipherType(.ECDHE_RSA_WITH_AES_128_CBC_SHA256), | |
| 113 | RSA_WITH_AES_128_CBC_SHA256: CipherType(.RSA_WITH_AES_128_CBC_SHA256), | |
| 114 | ||
| 115 | ECDHE_ECDSA_WITH_AES_256_CBC_SHA384: CipherType(.ECDHE_ECDSA_WITH_AES_256_CBC_SHA384), | |
| 116 | ECDHE_RSA_WITH_AES_256_CBC_SHA384: CipherType(.ECDHE_RSA_WITH_AES_256_CBC_SHA384), | |
| 117 | // tls 1.2 gcm | |
| 118 | ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: CipherType(.ECDHE_ECDSA_WITH_AES_128_GCM_SHA256), | |
| 119 | ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: CipherType(.ECDHE_RSA_WITH_AES_256_GCM_SHA384), | |
| 120 | ECDHE_RSA_WITH_AES_128_GCM_SHA256: CipherType(.ECDHE_RSA_WITH_AES_128_GCM_SHA256), | |
| 121 | ECDHE_RSA_WITH_AES_256_GCM_SHA384: CipherType(.ECDHE_RSA_WITH_AES_256_GCM_SHA384), | |
| 122 | // tls 1.2 chacha | |
| 123 | ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: CipherType(.ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256), | |
| 124 | ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256: CipherType(.ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256), | |
| 125 | // tls 1.3 | |
| 126 | AES_128_GCM_SHA256: CipherType(.AES_128_GCM_SHA256), | |
| 127 | AES_256_GCM_SHA384: CipherType(.AES_256_GCM_SHA384), | |
| 128 | CHACHA20_POLY1305_SHA256: CipherType(.CHACHA20_POLY1305_SHA256), | |
| 129 | AEGIS_128L_SHA256: CipherType(.AEGIS_128L_SHA256), | |
| 130 | ||
| 131 | // tls 1.2 application cipher | |
| 132 | pub fn initTls12(tag: CipherSuite, key_material: []const u8, side: proto.Side) !Cipher { | |
| 133 | switch (tag) { | |
| 134 | inline .ECDHE_ECDSA_WITH_AES_128_CBC_SHA, | |
| 135 | .ECDHE_RSA_WITH_AES_128_CBC_SHA, | |
| 136 | .RSA_WITH_AES_128_CBC_SHA, | |
| 137 | .ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, | |
| 138 | .ECDHE_RSA_WITH_AES_128_CBC_SHA256, | |
| 139 | .RSA_WITH_AES_128_CBC_SHA256, | |
| 140 | .ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, | |
| 141 | .ECDHE_RSA_WITH_AES_256_CBC_SHA384, | |
| 142 | .ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, | |
| 143 | .ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, | |
| 144 | .ECDHE_RSA_WITH_AES_128_GCM_SHA256, | |
| 145 | .ECDHE_RSA_WITH_AES_256_GCM_SHA384, | |
| 146 | .ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, | |
| 147 | .ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, | |
| 148 | => |comptime_tag| { | |
| 149 | return @unionInit(Cipher, @tagName(comptime_tag), CipherType(comptime_tag).init(key_material, side)); | |
| 150 | }, | |
| 151 | else => return error.TlsIllegalParameter, | |
| 152 | } | |
| 153 | } | |
| 154 | ||
| 155 | // tls 1.3 handshake or application cipher | |
| 156 | pub fn initTls13(tag: CipherSuite, secret: Transcript.Secret, side: proto.Side) !Cipher { | |
| 157 | return switch (tag) { | |
| 158 | inline .AES_128_GCM_SHA256, | |
| 159 | .AES_256_GCM_SHA384, | |
| 160 | .CHACHA20_POLY1305_SHA256, | |
| 161 | .AEGIS_128L_SHA256, | |
| 162 | => |comptime_tag| { | |
| 163 | return @unionInit(Cipher, @tagName(comptime_tag), CipherType(comptime_tag).init(secret, side)); | |
| 164 | }, | |
| 165 | else => return error.TlsIllegalParameter, | |
| 166 | }; | |
| 167 | } | |
| 168 | ||
| 169 | pub fn encrypt( | |
| 170 | c: *Cipher, | |
| 171 | buf: []u8, | |
| 172 | content_type: proto.ContentType, | |
| 173 | cleartext: []const u8, | |
| 174 | ) ![]const u8 { | |
| 175 | return switch (c.*) { | |
| 176 | inline else => |*f| try f.encrypt(buf, content_type, cleartext), | |
| 177 | }; | |
| 178 | } | |
| 179 | ||
| 180 | pub fn decrypt( | |
| 181 | c: *Cipher, | |
| 182 | buf: []u8, | |
| 183 | rec: Record, | |
| 184 | ) !struct { proto.ContentType, []u8 } { | |
| 185 | return switch (c.*) { | |
| 186 | inline else => |*f| { | |
| 187 | const content_type, const cleartext = try f.decrypt(buf, rec); | |
| 188 | if (cleartext.len > max_cleartext_len) return error.TlsRecordOverflow; | |
| 189 | return .{ content_type, cleartext }; | |
| 190 | }, | |
| 191 | }; | |
| 192 | } | |
| 193 | ||
| 194 | pub fn recordLen(c: Cipher, cleartext_len: usize) usize { | |
| 195 | return switch (c) { | |
| 196 | inline else => |f| f.recordLen(cleartext_len), | |
| 197 | }; | |
| 198 | } | |
| 199 | ||
| 200 | pub fn encryptOverhead(c: Cipher) usize { | |
| 201 | return switch (c) { | |
| 202 | inline else => |f| return @TypeOf(f).encrypt_overhead, | |
| 203 | }; | |
| 204 | } | |
| 205 | ||
| 206 | pub fn encryptSeq(c: Cipher) u64 { | |
| 207 | return switch (c) { | |
| 208 | inline else => |f| f.encrypt_seq, | |
| 209 | }; | |
| 210 | } | |
| 211 | ||
| 212 | pub fn keyUpdateEncrypt(c: *Cipher) !void { | |
| 213 | return switch (c.*) { | |
| 214 | inline .AES_128_GCM_SHA256, | |
| 215 | .AES_256_GCM_SHA384, | |
| 216 | .CHACHA20_POLY1305_SHA256, | |
| 217 | .AEGIS_128L_SHA256, | |
| 218 | => |*f| f.keyUpdateEncrypt(), | |
| 219 | // can't happen on tls 1.2 | |
| 220 | else => return error.TlsUnexpectedMessage, | |
| 221 | }; | |
| 222 | } | |
| 223 | pub fn keyUpdateDecrypt(c: *Cipher) !void { | |
| 224 | return switch (c.*) { | |
| 225 | inline .AES_128_GCM_SHA256, | |
| 226 | .AES_256_GCM_SHA384, | |
| 227 | .CHACHA20_POLY1305_SHA256, | |
| 228 | .AEGIS_128L_SHA256, | |
| 229 | => |*f| f.keyUpdateDecrypt(), | |
| 230 | // can't happen on tls 1.2 | |
| 231 | else => return error.TlsUnexpectedMessage, | |
| 232 | }; | |
| 233 | } | |
| 234 | }; | |
| 235 | ||
| 236 | fn Aead12Type(comptime AeadType: type) type { | |
| 237 | return struct { | |
| 238 | const explicit_iv_len = 8; | |
| 239 | const key_len = AeadType.key_length; | |
| 240 | const auth_tag_len = AeadType.tag_length; | |
| 241 | const nonce_len = AeadType.nonce_length; | |
| 242 | const iv_len = AeadType.nonce_length - explicit_iv_len; | |
| 243 | const encrypt_overhead = record.header_len + explicit_iv_len + auth_tag_len; | |
| 244 | ||
| 245 | encrypt_key: [key_len]u8, | |
| 246 | decrypt_key: [key_len]u8, | |
| 247 | encrypt_iv: [iv_len]u8, | |
| 248 | decrypt_iv: [iv_len]u8, | |
| 249 | encrypt_seq: u64 = 0, | |
| 250 | decrypt_seq: u64 = 0, | |
| 251 | rnd: std.Random = csprng, | |
| 252 | ||
| 253 | const Self = @This(); | |
| 254 | ||
| 255 | fn init(key_material: []const u8, side: proto.Side) Self { | |
| 256 | const client_key = key_material[0..key_len].*; | |
| 257 | const server_key = key_material[key_len..][0..key_len].*; | |
| 258 | const client_iv = key_material[2 * key_len ..][0..iv_len].*; | |
| 259 | const server_iv = key_material[2 * key_len + iv_len ..][0..iv_len].*; | |
| 260 | return .{ | |
| 261 | .encrypt_key = if (side == .client) client_key else server_key, | |
| 262 | .decrypt_key = if (side == .client) server_key else client_key, | |
| 263 | .encrypt_iv = if (side == .client) client_iv else server_iv, | |
| 264 | .decrypt_iv = if (side == .client) server_iv else client_iv, | |
| 265 | }; | |
| 266 | } | |
| 267 | ||
| 268 | /// Returns encrypted tls record in format: | |
| 269 | /// ----------------- buf ---------------------- | |
| 270 | /// header | explicit_iv | ciphertext | auth_tag | |
| 271 | /// | |
| 272 | /// tls record header: 5 bytes | |
| 273 | /// explicit_iv: 8 bytes | |
| 274 | /// ciphertext: same length as cleartext | |
| 275 | /// auth_tag: 16 bytes | |
| 276 | pub fn encrypt( | |
| 277 | self: *Self, | |
| 278 | buf: []u8, | |
| 279 | content_type: proto.ContentType, | |
| 280 | cleartext: []const u8, | |
| 281 | ) ![]const u8 { | |
| 282 | const record_len = record.header_len + explicit_iv_len + cleartext.len + auth_tag_len; | |
| 283 | if (buf.len < record_len) return error.TlsCipherNoSpaceLeft; | |
| 284 | ||
| 285 | const header = buf[0..record.header_len]; | |
| 286 | const explicit_iv = buf[record.header_len..][0..explicit_iv_len]; | |
| 287 | const ciphertext = buf[record.header_len + explicit_iv_len ..][0..cleartext.len]; | |
| 288 | const auth_tag = buf[record.header_len + explicit_iv_len + cleartext.len ..][0..auth_tag_len]; | |
| 289 | ||
| 290 | header.* = record.header(content_type, explicit_iv_len + cleartext.len + auth_tag_len); | |
| 291 | self.rnd.bytes(explicit_iv); | |
| 292 | const iv = self.encrypt_iv ++ explicit_iv.*; | |
| 293 | const ad = additionalData(self.encrypt_seq, content_type, cleartext.len); | |
| 294 | AeadType.encrypt(ciphertext, auth_tag, cleartext, &ad, iv, self.encrypt_key); | |
| 295 | self.encrypt_seq +%= 1; | |
| 296 | ||
| 297 | return buf[0..record_len]; | |
| 298 | } | |
| 299 | ||
| 300 | pub fn recordLen(_: Self, cleartext_len: usize) usize { | |
| 301 | return record.header_len + explicit_iv_len + cleartext_len + auth_tag_len; | |
| 302 | } | |
| 303 | ||
| 304 | /// Decrypts payload into cleartext. Returns tls record content type and | |
| 305 | /// cleartext. | |
| 306 | /// Accepts tls record header and payload: | |
| 307 | /// header | ----------- payload --------------- | |
| 308 | /// header | explicit_iv | ciphertext | auth_tag | |
| 309 | pub fn decrypt( | |
| 310 | self: *Self, | |
| 311 | buf: []u8, | |
| 312 | rec: Record, | |
| 313 | ) !struct { proto.ContentType, []u8 } { | |
| 314 | const overhead = explicit_iv_len + auth_tag_len; | |
| 315 | if (rec.payload.len < overhead) return error.TlsDecryptError; | |
| 316 | const cleartext_len = rec.payload.len - overhead; | |
| 317 | if (buf.len < cleartext_len) return error.TlsCipherNoSpaceLeft; | |
| 318 | ||
| 319 | const explicit_iv = rec.payload[0..explicit_iv_len]; | |
| 320 | const ciphertext = rec.payload[explicit_iv_len..][0..cleartext_len]; | |
| 321 | const auth_tag = rec.payload[explicit_iv_len + cleartext_len ..][0..auth_tag_len]; | |
| 322 | ||
| 323 | const iv = self.decrypt_iv ++ explicit_iv.*; | |
| 324 | const ad = additionalData(self.decrypt_seq, rec.content_type, cleartext_len); | |
| 325 | const cleartext = buf[0..cleartext_len]; | |
| 326 | AeadType.decrypt(cleartext, ciphertext, auth_tag.*, &ad, iv, self.decrypt_key) catch return error.TlsDecryptError; | |
| 327 | self.decrypt_seq +%= 1; | |
| 328 | return .{ rec.content_type, cleartext }; | |
| 329 | } | |
| 330 | }; | |
| 331 | } | |
| 332 | ||
| 333 | fn Aead12ChaChaType(comptime AeadType: type) type { | |
| 334 | return struct { | |
| 335 | const key_len = AeadType.key_length; | |
| 336 | const auth_tag_len = AeadType.tag_length; | |
| 337 | const nonce_len = AeadType.nonce_length; | |
| 338 | const encrypt_overhead = record.header_len + auth_tag_len; | |
| 339 | ||
| 340 | encrypt_key: [key_len]u8, | |
| 341 | decrypt_key: [key_len]u8, | |
| 342 | encrypt_iv: [nonce_len]u8, | |
| 343 | decrypt_iv: [nonce_len]u8, | |
| 344 | encrypt_seq: u64 = 0, | |
| 345 | decrypt_seq: u64 = 0, | |
| 346 | ||
| 347 | const Self = @This(); | |
| 348 | ||
| 349 | fn init(key_material: []const u8, side: proto.Side) Self { | |
| 350 | const client_key = key_material[0..key_len].*; | |
| 351 | const server_key = key_material[key_len..][0..key_len].*; | |
| 352 | const client_iv = key_material[2 * key_len ..][0..nonce_len].*; | |
| 353 | const server_iv = key_material[2 * key_len + nonce_len ..][0..nonce_len].*; | |
| 354 | return .{ | |
| 355 | .encrypt_key = if (side == .client) client_key else server_key, | |
| 356 | .decrypt_key = if (side == .client) server_key else client_key, | |
| 357 | .encrypt_iv = if (side == .client) client_iv else server_iv, | |
| 358 | .decrypt_iv = if (side == .client) server_iv else client_iv, | |
| 359 | }; | |
| 360 | } | |
| 361 | ||
| 362 | /// Returns encrypted tls record in format: | |
| 363 | /// ------------ buf ------------- | |
| 364 | /// header | ciphertext | auth_tag | |
| 365 | /// | |
| 366 | /// tls record header: 5 bytes | |
| 367 | /// ciphertext: same length as cleartext | |
| 368 | /// auth_tag: 16 bytes | |
| 369 | pub fn encrypt( | |
| 370 | self: *Self, | |
| 371 | buf: []u8, | |
| 372 | content_type: proto.ContentType, | |
| 373 | cleartext: []const u8, | |
| 374 | ) ![]const u8 { | |
| 375 | const record_len = record.header_len + cleartext.len + auth_tag_len; | |
| 376 | if (buf.len < record_len) return error.TlsCipherNoSpaceLeft; | |
| 377 | ||
| 378 | const ciphertext = buf[record.header_len..][0..cleartext.len]; | |
| 379 | const auth_tag = buf[record.header_len + ciphertext.len ..][0..auth_tag_len]; | |
| 380 | ||
| 381 | const ad = additionalData(self.encrypt_seq, content_type, cleartext.len); | |
| 382 | const iv = ivWithSeq(nonce_len, self.encrypt_iv, self.encrypt_seq); | |
| 383 | AeadType.encrypt(ciphertext, auth_tag, cleartext, &ad, iv, self.encrypt_key); | |
| 384 | self.encrypt_seq +%= 1; | |
| 385 | ||
| 386 | buf[0..record.header_len].* = record.header(content_type, ciphertext.len + auth_tag.len); | |
| 387 | return buf[0..record_len]; | |
| 388 | } | |
| 389 | ||
| 390 | pub fn recordLen(_: Self, cleartext_len: usize) usize { | |
| 391 | return record.header_len + cleartext_len + auth_tag_len; | |
| 392 | } | |
| 393 | ||
| 394 | /// Decrypts payload into cleartext. Returns tls record content type and | |
| 395 | /// cleartext. | |
| 396 | /// Accepts tls record header and payload: | |
| 397 | /// header | ----- payload ------- | |
| 398 | /// header | ciphertext | auth_tag | |
| 399 | pub fn decrypt( | |
| 400 | self: *Self, | |
| 401 | buf: []u8, | |
| 402 | rec: Record, | |
| 403 | ) !struct { proto.ContentType, []u8 } { | |
| 404 | const overhead = auth_tag_len; | |
| 405 | if (rec.payload.len < overhead) return error.TlsDecryptError; | |
| 406 | const cleartext_len = rec.payload.len - overhead; | |
| 407 | if (buf.len < cleartext_len) return error.TlsCipherNoSpaceLeft; | |
| 408 | ||
| 409 | const ciphertext = rec.payload[0..cleartext_len]; | |
| 410 | const auth_tag = rec.payload[cleartext_len..][0..auth_tag_len]; | |
| 411 | const cleartext = buf[0..cleartext_len]; | |
| 412 | ||
| 413 | const ad = additionalData(self.decrypt_seq, rec.content_type, cleartext_len); | |
| 414 | const iv = ivWithSeq(nonce_len, self.decrypt_iv, self.decrypt_seq); | |
| 415 | AeadType.decrypt(cleartext, ciphertext, auth_tag.*, &ad, iv, self.decrypt_key) catch return error.TlsDecryptError; | |
| 416 | self.decrypt_seq +%= 1; | |
| 417 | return .{ rec.content_type, cleartext }; | |
| 418 | } | |
| 419 | }; | |
| 420 | } | |
| 421 | ||
| 422 | fn Aead13Type(comptime AeadType: type, comptime Hash: type) type { | |
| 423 | return struct { | |
| 424 | const Hmac = crypto.auth.hmac.Hmac(Hash); | |
| 425 | const Hkdf = crypto.kdf.hkdf.Hkdf(Hmac); | |
| 426 | ||
| 427 | const key_len = AeadType.key_length; | |
| 428 | const auth_tag_len = AeadType.tag_length; | |
| 429 | const nonce_len = AeadType.nonce_length; | |
| 430 | const digest_len = Hash.digest_length; | |
| 431 | const encrypt_overhead = record.header_len + 1 + auth_tag_len; | |
| 432 | ||
| 433 | encrypt_secret: [digest_len]u8, | |
| 434 | decrypt_secret: [digest_len]u8, | |
| 435 | encrypt_key: [key_len]u8, | |
| 436 | decrypt_key: [key_len]u8, | |
| 437 | encrypt_iv: [nonce_len]u8, | |
| 438 | decrypt_iv: [nonce_len]u8, | |
| 439 | encrypt_seq: u64 = 0, | |
| 440 | decrypt_seq: u64 = 0, | |
| 441 | ||
| 442 | const Self = @This(); | |
| 443 | ||
| 444 | pub fn init(secret: Transcript.Secret, side: proto.Side) Self { | |
| 445 | var self = Self{ | |
| 446 | .encrypt_secret = if (side == .client) secret.client[0..digest_len].* else secret.server[0..digest_len].*, | |
| 447 | .decrypt_secret = if (side == .server) secret.client[0..digest_len].* else secret.server[0..digest_len].*, | |
| 448 | .encrypt_key = undefined, | |
| 449 | .decrypt_key = undefined, | |
| 450 | .encrypt_iv = undefined, | |
| 451 | .decrypt_iv = undefined, | |
| 452 | }; | |
| 453 | self.keyGenerate(); | |
| 454 | return self; | |
| 455 | } | |
| 456 | ||
| 457 | fn keyGenerate(self: *Self) void { | |
| 458 | self.encrypt_key = hkdfExpandLabel(Hkdf, self.encrypt_secret, "key", "", key_len); | |
| 459 | self.decrypt_key = hkdfExpandLabel(Hkdf, self.decrypt_secret, "key", "", key_len); | |
| 460 | self.encrypt_iv = hkdfExpandLabel(Hkdf, self.encrypt_secret, "iv", "", nonce_len); | |
| 461 | self.decrypt_iv = hkdfExpandLabel(Hkdf, self.decrypt_secret, "iv", "", nonce_len); | |
| 462 | } | |
| 463 | ||
| 464 | pub fn keyUpdateEncrypt(self: *Self) void { | |
| 465 | self.encrypt_secret = hkdfExpandLabel(Hkdf, self.encrypt_secret, "traffic upd", "", digest_len); | |
| 466 | self.encrypt_seq = 0; | |
| 467 | self.keyGenerate(); | |
| 468 | } | |
| 469 | ||
| 470 | pub fn keyUpdateDecrypt(self: *Self) void { | |
| 471 | self.decrypt_secret = hkdfExpandLabel(Hkdf, self.decrypt_secret, "traffic upd", "", digest_len); | |
| 472 | self.decrypt_seq = 0; | |
| 473 | self.keyGenerate(); | |
| 474 | } | |
| 475 | ||
| 476 | /// Returns encrypted tls record in format: | |
| 477 | /// ------------ buf ------------- | |
| 478 | /// header | ciphertext | auth_tag | |
| 479 | /// | |
| 480 | /// tls record header: 5 bytes | |
| 481 | /// ciphertext: cleartext len + 1 byte content type | |
| 482 | /// auth_tag: 16 bytes | |
| 483 | pub fn encrypt( | |
| 484 | self: *Self, | |
| 485 | buf: []u8, | |
| 486 | content_type: proto.ContentType, | |
| 487 | cleartext: []const u8, | |
| 488 | ) ![]const u8 { | |
| 489 | const payload_len = cleartext.len + 1 + auth_tag_len; | |
| 490 | const record_len = record.header_len + payload_len; | |
| 491 | if (buf.len < record_len) return error.TlsCipherNoSpaceLeft; | |
| 492 | ||
| 493 | const header = buf[0..record.header_len]; | |
| 494 | header.* = record.header(.application_data, payload_len); | |
| 495 | ||
| 496 | // Skip @memcpy if cleartext is already part of the buf at right position | |
| 497 | if (&cleartext[0] != &buf[record.header_len]) { | |
| 498 | @memcpy(buf[record.header_len..][0..cleartext.len], cleartext); | |
| 499 | } | |
| 500 | buf[record.header_len + cleartext.len] = @backingInt(content_type); | |
| 501 | const ciphertext = buf[record.header_len..][0 .. cleartext.len + 1]; | |
| 502 | const auth_tag = buf[record.header_len + ciphertext.len ..][0..auth_tag_len]; | |
| 503 | ||
| 504 | const iv = ivWithSeq(nonce_len, self.encrypt_iv, self.encrypt_seq); | |
| 505 | AeadType.encrypt(ciphertext, auth_tag, ciphertext, header, iv, self.encrypt_key); | |
| 506 | self.encrypt_seq +%= 1; | |
| 507 | return buf[0..record_len]; | |
| 508 | } | |
| 509 | ||
| 510 | pub fn recordLen(_: Self, cleartext_len: usize) usize { | |
| 511 | const payload_len = cleartext_len + 1 + auth_tag_len; | |
| 512 | return record.header_len + payload_len; | |
| 513 | } | |
| 514 | ||
| 515 | /// Decrypts payload into cleartext. Returns tls record content type and | |
| 516 | /// cleartext. | |
| 517 | /// Accepts tls record header and payload: | |
| 518 | /// header | ------- payload --------- | |
| 519 | /// header | ciphertext | auth_tag | |
| 520 | /// header | cleartext + ct | auth_tag | |
| 521 | /// Ciphertext after decryption contains cleartext and content type (1 byte). | |
| 522 | pub fn decrypt( | |
| 523 | self: *Self, | |
| 524 | buf: []u8, | |
| 525 | rec: Record, | |
| 526 | ) !struct { proto.ContentType, []u8 } { | |
| 527 | const overhead = auth_tag_len + 1; | |
| 528 | if (rec.payload.len < overhead) return error.TlsDecryptError; | |
| 529 | const ciphertext_len = rec.payload.len - auth_tag_len; | |
| 530 | if (ciphertext_len > max_cleartext_len + 1) return error.TlsRecordOverflow; | |
| 531 | if (buf.len < ciphertext_len) return error.TlsCipherNoSpaceLeft; | |
| 532 | ||
| 533 | const ciphertext = rec.payload[0..ciphertext_len]; | |
| 534 | const auth_tag = rec.payload[ciphertext_len..][0..auth_tag_len]; | |
| 535 | ||
| 536 | const iv = ivWithSeq(nonce_len, self.decrypt_iv, self.decrypt_seq); | |
| 537 | AeadType.decrypt(buf[0..ciphertext_len], ciphertext, auth_tag.*, rec.header, iv, self.decrypt_key) catch return error.TlsBadRecordMac; | |
| 538 | ||
| 539 | // Remove zero bytes padding | |
| 540 | var content_type_idx: usize = ciphertext_len - 1; | |
| 541 | while (buf[content_type_idx] == 0 and content_type_idx > 0) : (content_type_idx -= 1) {} | |
| 542 | ||
| 543 | const cleartext = buf[0..content_type_idx]; | |
| 544 | const content_type: proto.ContentType = @fromBackingInt(@intCast(buf[content_type_idx])); | |
| 545 | self.decrypt_seq +%= 1; | |
| 546 | return .{ content_type, cleartext }; | |
| 547 | } | |
| 548 | }; | |
| 549 | } | |
| 550 | ||
| 551 | fn CbcType(comptime BlockCipher: type, comptime HashType: type) type { | |
| 552 | const CBC = .CBC(BlockCipher); | |
| 553 | return struct { | |
| 554 | const mac_len = HashType.digest_length; // 20, 32, 48 bytes for sha1, sha256, sha384 | |
| 555 | const key_len = BlockCipher.key_bits / 8; // 16, 32 for Aes128, Aes256 | |
| 556 | const iv_len = 16; | |
| 557 | const encrypt_overhead = record.header_len + iv_len + mac_len + max_padding; | |
| 558 | ||
| 559 | const Hmac = crypto.auth.hmac.Hmac(HashType); | |
| 560 | const paddedLength = CBC.paddedLength; | |
| 561 | const max_padding = 16; | |
| 562 | ||
| 563 | encrypt_secret: [mac_len]u8, | |
| 564 | decrypt_secret: [mac_len]u8, | |
| 565 | encrypt_key: [key_len]u8, | |
| 566 | decrypt_key: [key_len]u8, | |
| 567 | encrypt_seq: u64 = 0, | |
| 568 | decrypt_seq: u64 = 0, | |
| 569 | rnd: std.Random = csprng, | |
| 570 | ||
| 571 | const Self = @This(); | |
| 572 | ||
| 573 | fn init(key_material: []const u8, side: proto.Side) Self { | |
| 574 | const client_secret = key_material[0..mac_len].*; | |
| 575 | const server_secret = key_material[mac_len..][0..mac_len].*; | |
| 576 | const client_key = key_material[2 * mac_len ..][0..key_len].*; | |
| 577 | const server_key = key_material[2 * mac_len + key_len ..][0..key_len].*; | |
| 578 | return .{ | |
| 579 | .encrypt_secret = if (side == .client) client_secret else server_secret, | |
| 580 | .decrypt_secret = if (side == .client) server_secret else client_secret, | |
| 581 | .encrypt_key = if (side == .client) client_key else server_key, | |
| 582 | .decrypt_key = if (side == .client) server_key else client_key, | |
| 583 | }; | |
| 584 | } | |
| 585 | ||
| 586 | /// Returns encrypted tls record in format: | |
| 587 | /// ----------------- buf ----------------- | |
| 588 | /// header | iv | ------ ciphertext ------- | |
| 589 | /// header | iv | cleartext | mac | padding | |
| 590 | /// | |
| 591 | /// tls record header: 5 bytes | |
| 592 | /// iv: 16 bytes | |
| 593 | /// ciphertext: cleartext length + mac + padding | |
| 594 | /// mac: 20, 32 or 48 (sha1, sha256, sha384) | |
| 595 | /// padding: 1-16 bytes | |
| 596 | /// | |
| 597 | /// Max encrypt buf overhead = iv + mac + padding (1-16) | |
| 598 | /// aes_128_cbc_sha => 16 + 20 + 16 = 52 | |
| 599 | /// aes_128_cbc_sha256 => 16 + 32 + 16 = 64 | |
| 600 | /// aes_256_cbc_sha384 => 16 + 48 + 16 = 80 | |
| 601 | pub fn encrypt( | |
| 602 | self: *Self, | |
| 603 | buf: []u8, | |
| 604 | content_type: proto.ContentType, | |
| 605 | cleartext: []const u8, | |
| 606 | ) ![]const u8 { | |
| 607 | if (buf.len < self.recordLen(cleartext.len)) return error.TlsCipherNoSpaceLeft; | |
| 608 | const cleartext_idx = record.header_len + iv_len; // position of cleartext in buf | |
| 609 | @memcpy(buf[cleartext_idx..][0..cleartext.len], cleartext); | |
| 610 | ||
| 611 | { // calculate mac from (ad + cleartext) | |
| 612 | // ... | ad | cleartext | mac | ... | |
| 613 | // | -- mac msg -- | mac | | |
| 614 | const ad = additionalData(self.encrypt_seq, content_type, cleartext.len); | |
| 615 | const mac_msg = buf[cleartext_idx - ad.len ..][0 .. ad.len + cleartext.len]; | |
| 616 | @memcpy(mac_msg[0..ad.len], &ad); | |
| 617 | const mac = buf[cleartext_idx + cleartext.len ..][0..mac_len]; | |
| 618 | Hmac.create(mac, mac_msg, &self.encrypt_secret); | |
| 619 | self.encrypt_seq +%= 1; | |
| 620 | } | |
| 621 | ||
| 622 | // ... | cleartext | mac | ... | |
| 623 | // ... | -- plaintext --- ... | |
| 624 | // ... | cleartext | mac | padding | |
| 625 | // ... | ------ ciphertext ------- | |
| 626 | const unpadded_len = cleartext.len + mac_len; | |
| 627 | const padded_len = paddedLength(unpadded_len); | |
| 628 | const plaintext = buf[cleartext_idx..][0..unpadded_len]; | |
| 629 | const ciphertext = buf[cleartext_idx..][0..padded_len]; | |
| 630 | ||
| 631 | // Add header and iv at the buf start | |
| 632 | // header | iv | ... | |
| 633 | buf[0..record.header_len].* = record.header(content_type, iv_len + ciphertext.len); | |
| 634 | const iv = buf[record.header_len..][0..iv_len]; | |
| 635 | self.rnd.bytes(iv); | |
| 636 | ||
| 637 | // encrypt plaintext into ciphertext | |
| 638 | CBC.init(self.encrypt_key).encrypt(ciphertext, plaintext, iv[0..iv_len].*); | |
| 639 | ||
| 640 | // header | iv | ------ ciphertext ------- | |
| 641 | return buf[0 .. record.header_len + iv_len + ciphertext.len]; | |
| 642 | } | |
| 643 | ||
| 644 | pub fn recordLen(_: Self, cleartext_len: usize) usize { | |
| 645 | const unpadded_len = cleartext_len + mac_len; | |
| 646 | const padded_len = paddedLength(unpadded_len); | |
| 647 | return record.header_len + iv_len + padded_len; | |
| 648 | } | |
| 649 | ||
| 650 | /// Decrypts payload into cleartext. Returns tls record content type and | |
| 651 | /// cleartext. | |
| 652 | pub fn decrypt( | |
| 653 | self: *Self, | |
| 654 | buf: []u8, | |
| 655 | rec: Record, | |
| 656 | ) !struct { proto.ContentType, []u8 } { | |
| 657 | if (rec.payload.len < iv_len + mac_len + 1) return error.TlsDecryptError; | |
| 658 | ||
| 659 | // --------- payload ------------ | |
| 660 | // iv | ------ ciphertext ------- | |
| 661 | // iv | cleartext | mac | padding | |
| 662 | const iv = rec.payload[0..iv_len]; | |
| 663 | const ciphertext = rec.payload[iv_len..]; | |
| 664 | ||
| 665 | if (buf.len < ciphertext.len + additional_data_len) return error.TlsCipherNoSpaceLeft; | |
| 666 | // ---------- buf --------------- | |
| 667 | // ad | ------ plaintext -------- | |
| 668 | // ad | cleartext | mac | padding | |
| 669 | const plaintext = buf[additional_data_len..][0..ciphertext.len]; | |
| 670 | // decrypt ciphertext -> plaintext | |
| 671 | CBC.init(self.decrypt_key).decrypt(plaintext, ciphertext, iv[0..iv_len].*) catch return error.TlsDecryptError; | |
| 672 | ||
| 673 | // get padding len from last padding byte | |
| 674 | const padding_len = plaintext[plaintext.len - 1] + 1; | |
| 675 | if (plaintext.len < mac_len + padding_len) return error.TlsDecryptError; | |
| 676 | // split plaintext into cleartext and mac | |
| 677 | const cleartext_len = plaintext.len - mac_len - padding_len; | |
| 678 | const cleartext = plaintext[0..cleartext_len]; | |
| 679 | const mac = plaintext[cleartext_len..][0..mac_len]; | |
| 680 | ||
| 681 | // write ad to the buf | |
| 682 | var ad = additionalData(self.decrypt_seq, rec.content_type, cleartext_len); | |
| 683 | @memcpy(buf[0..ad.len], &ad); | |
| 684 | const mac_msg = buf[0 .. ad.len + cleartext_len]; | |
| 685 | self.decrypt_seq +%= 1; | |
| 686 | ||
| 687 | // calculate expected mac and compare with received mac | |
| 688 | var expected_mac: [mac_len]u8 = undefined; | |
| 689 | Hmac.create(&expected_mac, mac_msg, &self.decrypt_secret); | |
| 690 | if (!std.mem.eql(u8, &expected_mac, mac)) | |
| 691 | return error.TlsBadRecordMac; | |
| 692 | ||
| 693 | return .{ rec.content_type, cleartext }; | |
| 694 | } | |
| 695 | }; | |
| 696 | } | |
| 697 | ||
| 698 | // xor lower 8 iv bytes with seq | |
| 699 | fn ivWithSeq(comptime nonce_len: usize, iv: [nonce_len]u8, seq: u64) [nonce_len]u8 { | |
| 700 | var res = iv; | |
| 701 | const buf = res[nonce_len - 8 ..]; | |
| 702 | const operand = std.mem.readInt(u64, buf, .big); | |
| 703 | std.mem.writeInt(u64, buf, operand ^ seq, .big); | |
| 704 | return res; | |
| 705 | } | |
| 706 | ||
| 707 | pub const additional_data_len = record.header_len + @sizeOf(u64); | |
| 708 | ||
| 709 | fn additionalData(seq: u64, content_type: proto.ContentType, payload_len: usize) [additional_data_len]u8 { | |
| 710 | const header = record.header(content_type, payload_len); | |
| 711 | var seq_buf: [8]u8 = undefined; | |
| 712 | std.mem.writeInt(u64, &seq_buf, seq, .big); | |
| 713 | return seq_buf ++ header; | |
| 714 | } | |
| 715 | ||
| 716 | // Cipher suites lists. In the order of preference. | |
| 717 | // For the preference using grades priority and rules from Go project. | |
| 718 | // https://ciphersuite.info/page/faq/ | |
| 719 | // https://github.com/golang/go/blob/73186ba00251b3ed8baaab36e4f5278c7681155b/src/crypto/tls/cipher_suites.go#L226 | |
| 720 | pub const cipher_suites = struct { | |
| 721 | pub const tls12_secure = if (crypto.core.aes.has_hardware_support) [_]CipherSuite{ | |
| 722 | // recommended | |
| 723 | .ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, | |
| 724 | .ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, | |
| 725 | .ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, | |
| 726 | // secure | |
| 727 | .ECDHE_RSA_WITH_AES_128_GCM_SHA256, | |
| 728 | .ECDHE_RSA_WITH_AES_256_GCM_SHA384, | |
| 729 | .ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, | |
| 730 | } else [_]CipherSuite{ | |
| 731 | // recommended | |
| 732 | .ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, | |
| 733 | .ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, | |
| 734 | .ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, | |
| 735 | ||
| 736 | // secure | |
| 737 | .ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, | |
| 738 | .ECDHE_RSA_WITH_AES_128_GCM_SHA256, | |
| 739 | .ECDHE_RSA_WITH_AES_256_GCM_SHA384, | |
| 740 | }; | |
| 741 | pub const tls12_weak = [_]CipherSuite{ | |
| 742 | // weak | |
| 743 | .ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, | |
| 744 | .ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, | |
| 745 | .ECDHE_ECDSA_WITH_AES_128_CBC_SHA, | |
| 746 | .ECDHE_RSA_WITH_AES_128_CBC_SHA256, | |
| 747 | .ECDHE_RSA_WITH_AES_256_CBC_SHA384, | |
| 748 | .ECDHE_RSA_WITH_AES_128_CBC_SHA, | |
| 749 | ||
| 750 | .RSA_WITH_AES_128_CBC_SHA256, | |
| 751 | .RSA_WITH_AES_128_CBC_SHA, | |
| 752 | }; | |
| 753 | pub const tls13_ = if (crypto.core.aes.has_hardware_support) [_]CipherSuite{ | |
| 754 | .AES_128_GCM_SHA256, | |
| 755 | .AES_256_GCM_SHA384, | |
| 756 | .CHACHA20_POLY1305_SHA256, | |
| 757 | // Excluded because didn't find server which supports it to test | |
| 758 | // .AEGIS_128L_SHA256 | |
| 759 | } else [_]CipherSuite{ | |
| 760 | .CHACHA20_POLY1305_SHA256, | |
| 761 | .AES_128_GCM_SHA256, | |
| 762 | .AES_256_GCM_SHA384, | |
| 763 | }; | |
| 764 | ||
| 765 | pub const tls13 = &tls13_; | |
| 766 | pub const tls12 = &(tls12_secure ++ tls12_weak); | |
| 767 | pub const secure = &(tls13_ ++ tls12_secure); | |
| 768 | pub const all = &(tls13_ ++ tls12_secure ++ tls12_weak); | |
| 769 | ||
| 770 | pub fn includes(list: []const CipherSuite, cs: CipherSuite) bool { | |
| 771 | for (list) |s| { | |
| 772 | if (cs == s) return true; | |
| 773 | } | |
| 774 | return false; | |
| 775 | } | |
| 776 | }; | |
| 777 | ||
| 778 | // Weak, secure, recommended grades are from https://ciphersuite.info/page/faq/ | |
| 779 | pub const CipherSuite = enum(u16) { | |
| 780 | // tls 1.2 cbc sha1 | |
| 781 | ECDHE_ECDSA_WITH_AES_128_CBC_SHA = 0xc009, // weak | |
| 782 | ECDHE_RSA_WITH_AES_128_CBC_SHA = 0xc013, // weak | |
| 783 | RSA_WITH_AES_128_CBC_SHA = 0x002F, // weak | |
| 784 | // tls 1.2 cbc sha256 | |
| 785 | ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 = 0xc023, // weak | |
| 786 | ECDHE_RSA_WITH_AES_128_CBC_SHA256 = 0xc027, // weak | |
| 787 | RSA_WITH_AES_128_CBC_SHA256 = 0x003c, // weak | |
| 788 | // tls 1.2 cbc sha384 | |
| 789 | ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 = 0xc024, // weak | |
| 790 | ECDHE_RSA_WITH_AES_256_CBC_SHA384 = 0xc028, // weak | |
| 791 | // tls 1.2 gcm | |
| 792 | ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 = 0xc02b, // recommended | |
| 793 | ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 = 0xc02c, // recommended | |
| 794 | ECDHE_RSA_WITH_AES_128_GCM_SHA256 = 0xc02f, // secure | |
| 795 | ECDHE_RSA_WITH_AES_256_GCM_SHA384 = 0xc030, // secure | |
| 796 | // tls 1.2 chacha | |
| 797 | ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 = 0xcca9, // recommended | |
| 798 | ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = 0xcca8, // secure | |
| 799 | // tls 1.3 (all are recommended) | |
| 800 | AES_128_GCM_SHA256 = 0x1301, | |
| 801 | AES_256_GCM_SHA384 = 0x1302, | |
| 802 | CHACHA20_POLY1305_SHA256 = 0x1303, | |
| 803 | AEGIS_128L_SHA256 = 0x1307, | |
| 804 | // AEGIS_256_SHA512 = 0x1306, | |
| 805 | _, | |
| 806 | ||
| 807 | pub fn validate(cs: CipherSuite) !void { | |
| 808 | if (cipher_suites.includes(cipher_suites.tls12, cs)) return; | |
| 809 | if (cipher_suites.includes(cipher_suites.tls13, cs)) return; | |
| 810 | return error.TlsIllegalParameter; | |
| 811 | } | |
| 812 | ||
| 813 | pub const Versions = enum { | |
| 814 | both, | |
| 815 | tls_1_3, | |
| 816 | tls_1_2, | |
| 817 | }; | |
| 818 | ||
| 819 | // get tls versions from list of cipher suites | |
| 820 | pub fn versions(list: []const CipherSuite) !Versions { | |
| 821 | var has_12 = false; | |
| 822 | var has_13 = false; | |
| 823 | for (list) |cs| { | |
| 824 | if (cipher_suites.includes(cipher_suites.tls12, cs)) { | |
| 825 | has_12 = true; | |
| 826 | } else { | |
| 827 | if (cipher_suites.includes(cipher_suites.tls13, cs)) has_13 = true; | |
| 828 | } | |
| 829 | } | |
| 830 | if (has_12 and has_13) return .both; | |
| 831 | if (has_12) return .tls_1_2; | |
| 832 | if (has_13) return .tls_1_3; | |
| 833 | return error.TlsIllegalParameter; | |
| 834 | } | |
| 835 | ||
| 836 | pub const KeyExchangeAlgorithm = enum { | |
| 837 | ecdhe, | |
| 838 | rsa, | |
| 839 | }; | |
| 840 | ||
| 841 | pub fn keyExchange(s: CipherSuite) KeyExchangeAlgorithm { | |
| 842 | return switch (s) { | |
| 843 | // Random premaster secret, encrypted with publich key from certificate. | |
| 844 | // No server key exchange message. | |
| 845 | .RSA_WITH_AES_128_CBC_SHA, | |
| 846 | .RSA_WITH_AES_128_CBC_SHA256, | |
| 847 | => .rsa, | |
| 848 | else => .ecdhe, | |
| 849 | }; | |
| 850 | } | |
| 851 | ||
| 852 | pub const HashTag = enum { | |
| 853 | sha256, | |
| 854 | sha384, | |
| 855 | sha512, | |
| 856 | }; | |
| 857 | ||
| 858 | pub fn hash(cs: CipherSuite) HashTag { | |
| 859 | return switch (cs) { | |
| 860 | .ECDHE_RSA_WITH_AES_256_CBC_SHA384, | |
| 861 | .ECDHE_RSA_WITH_AES_256_GCM_SHA384, | |
| 862 | .ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, | |
| 863 | .ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, | |
| 864 | .AES_256_GCM_SHA384, | |
| 865 | => .sha384, | |
| 866 | else => .sha256, | |
| 867 | }; | |
| 868 | } | |
| 869 | }; | |
| 870 | ||
| 871 | const testing = std.testing; | |
| 872 | const testu = ; | |
| 873 | ||
| 874 | test "CipherSuite validate" { | |
| 875 | { | |
| 876 | const cs: CipherSuite = .AES_256_GCM_SHA384; | |
| 877 | try cs.validate(); | |
| 878 | try testing.expectEqual(cs.hash(), .sha384); | |
| 879 | try testing.expectEqual(cs.keyExchange(), .ecdhe); | |
| 880 | } | |
| 881 | { | |
| 882 | const cs: CipherSuite = .AES_128_GCM_SHA256; | |
| 883 | try cs.validate(); | |
| 884 | try testing.expectEqual(.sha256, cs.hash()); | |
| 885 | try testing.expectEqual(.ecdhe, cs.keyExchange()); | |
| 886 | } | |
| 887 | for (cipher_suites.tls12) |cs| { | |
| 888 | try cs.validate(); | |
| 889 | _ = cs.hash(); | |
| 890 | _ = cs.keyExchange(); | |
| 891 | } | |
| 892 | } | |
| 893 | ||
| 894 | test "CipherSuite versions" { | |
| 895 | try testing.expectEqual(.tls_1_3, CipherSuite.versions(&[_]CipherSuite{.AES_128_GCM_SHA256})); | |
| 896 | try testing.expectEqual(.both, CipherSuite.versions(&[_]CipherSuite{ .AES_128_GCM_SHA256, .ECDHE_ECDSA_WITH_AES_128_CBC_SHA })); | |
| 897 | try testing.expectEqual(.tls_1_2, CipherSuite.versions(&[_]CipherSuite{.RSA_WITH_AES_128_CBC_SHA})); | |
| 898 | } | |
| 899 | ||
| 900 | test "gcm 1.2 encrypt overhead" { | |
| 901 | inline for ([_]type{ | |
| 902 | Aead12Aes128Gcm, | |
| 903 | Aead12Aes256Gcm, | |
| 904 | }) |T| { | |
| 905 | { | |
| 906 | const expected_key_len = switch (T) { | |
| 907 | Aead12Aes128Gcm => 16, | |
| 908 | Aead12Aes256Gcm => 32, | |
| 909 | else => unreachable, | |
| 910 | }; | |
| 911 | try testing.expectEqual(expected_key_len, T.key_len); | |
| 912 | try testing.expectEqual(16, T.auth_tag_len); | |
| 913 | try testing.expectEqual(12, T.nonce_len); | |
| 914 | try testing.expectEqual(4, T.iv_len); | |
| 915 | try testing.expectEqual(29, T.encrypt_overhead); | |
| 916 | } | |
| 917 | } | |
| 918 | } | |
| 919 | ||
| 920 | test "cbc 1.2 encrypt overhead" { | |
| 921 | try testing.expectEqual(85, encrypt_overhead_tls_12); | |
| 922 | ||
| 923 | inline for ([_]type{ | |
| 924 | CbcAes128Sha1, | |
| 925 | CbcAes128Sha256, | |
| 926 | CbcAes256Sha384, | |
| 927 | }) |T| { | |
| 928 | switch (T) { | |
| 929 | CbcAes128Sha1 => { | |
| 930 | try testing.expectEqual(20, T.mac_len); | |
| 931 | try testing.expectEqual(16, T.key_len); | |
| 932 | try testing.expectEqual(57, T.encrypt_overhead); | |
| 933 | }, | |
| 934 | CbcAes128Sha256 => { | |
| 935 | try testing.expectEqual(32, T.mac_len); | |
| 936 | try testing.expectEqual(16, T.key_len); | |
| 937 | try testing.expectEqual(69, T.encrypt_overhead); | |
| 938 | }, | |
| 939 | CbcAes256Sha384 => { | |
| 940 | try testing.expectEqual(48, T.mac_len); | |
| 941 | try testing.expectEqual(32, T.key_len); | |
| 942 | try testing.expectEqual(85, T.encrypt_overhead); | |
| 943 | }, | |
| 944 | else => unreachable, | |
| 945 | } | |
| 946 | try testing.expectEqual(16, T.paddedLength(1)); // cbc block padding | |
| 947 | try testing.expectEqual(16, T.iv_len); | |
| 948 | } | |
| 949 | } | |
| 950 | ||
| 951 | test "overhead tls 1.3" { | |
| 952 | try testing.expectEqual(22, encrypt_overhead_tls_13); | |
| 953 | try expectOverhead(Aes128GcmSha256, 16, 16, 12, 22); | |
| 954 | try expectOverhead(Aes256GcmSha384, 32, 16, 12, 22); | |
| 955 | try expectOverhead(ChaChaSha256, 32, 16, 12, 22); | |
| 956 | try expectOverhead(Aegis128Sha256, 16, 16, 16, 22); | |
| 957 | // and tls 1.2 chacha | |
| 958 | try expectOverhead(Aead12ChaCha, 32, 16, 12, 21); | |
| 959 | } | |
| 960 | ||
| 961 | fn expectOverhead(T: type, key_len: usize, auth_tag_len: usize, nonce_len: usize, overhead: usize) !void { | |
| 962 | try testing.expectEqual(key_len, T.key_len); | |
| 963 | try testing.expectEqual(auth_tag_len, T.auth_tag_len); | |
| 964 | try testing.expectEqual(nonce_len, T.nonce_len); | |
| 965 | try testing.expectEqual(overhead, T.encrypt_overhead); | |
| 966 | } | |
| 967 | ||
| 968 | test "client/server encryption tls 1.3" { | |
| 969 | inline for (cipher_suites.tls13) |cs| { | |
| 970 | var buf: [256]u8 = undefined; | |
| 971 | testu.fill(&buf); | |
| 972 | const secret = Transcript.Secret{ | |
| 973 | .client = buf[0..128], | |
| 974 | .server = buf[128..], | |
| 975 | }; | |
| 976 | var client_cipher = try Cipher.initTls13(cs, secret, .client); | |
| 977 | var server_cipher = try Cipher.initTls13(cs, secret, .server); | |
| 978 | try encryptDecrypt(&client_cipher, &server_cipher); | |
| 979 | ||
| 980 | try client_cipher.keyUpdateEncrypt(); | |
| 981 | try server_cipher.keyUpdateDecrypt(); | |
| 982 | try encryptDecrypt(&client_cipher, &server_cipher); | |
| 983 | ||
| 984 | try client_cipher.keyUpdateDecrypt(); | |
| 985 | try server_cipher.keyUpdateEncrypt(); | |
| 986 | try encryptDecrypt(&client_cipher, &server_cipher); | |
| 987 | } | |
| 988 | } | |
| 989 | ||
| 990 | test "client/server encryption tls 1.2" { | |
| 991 | inline for (cipher_suites.tls12) |cs| { | |
| 992 | var key_material: [256]u8 = undefined; | |
| 993 | testu.fill(&key_material); | |
| 994 | var client_cipher = try Cipher.initTls12(cs, &key_material, .client); | |
| 995 | var server_cipher = try Cipher.initTls12(cs, &key_material, .server); | |
| 996 | try encryptDecrypt(&client_cipher, &server_cipher); | |
| 997 | } | |
| 998 | } | |
| 999 | ||
| 1000 | fn encryptDecrypt(client_cipher: *Cipher, server_cipher: *Cipher) !void { | |
| 1001 | const cleartext = | |
| 1002 | \\ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do | |
| 1003 | \\ eiusmod tempor incididunt ut labore et dolore magna aliqua. | |
| 1004 | ; | |
| 1005 | var buf: [256]u8 = undefined; | |
| 1006 | ||
| 1007 | { // client to server | |
| 1008 | // encrypt | |
| 1009 | const encrypted = try client_cipher.encrypt(&buf, .application_data, cleartext); | |
| 1010 | const expected_encrypted_len = switch (client_cipher.*) { | |
| 1011 | inline else => |f| brk: { | |
| 1012 | const T = @TypeOf(f); | |
| 1013 | break :brk switch (T) { | |
| 1014 | CbcAes128Sha1, | |
| 1015 | CbcAes128Sha256, | |
| 1016 | CbcAes256Sha384, | |
| 1017 | => record.header_len + T.paddedLength(T.iv_len + cleartext.len + T.mac_len), | |
| 1018 | Aead12Aes128Gcm, | |
| 1019 | Aead12Aes256Gcm, | |
| 1020 | Aead12ChaCha, | |
| 1021 | Aes128GcmSha256, | |
| 1022 | Aes256GcmSha384, | |
| 1023 | ChaChaSha256, | |
| 1024 | Aegis128Sha256, | |
| 1025 | => cleartext.len + T.encrypt_overhead, | |
| 1026 | else => unreachable, | |
| 1027 | }; | |
| 1028 | }, | |
| 1029 | }; | |
| 1030 | try testing.expectEqual(client_cipher.recordLen(cleartext.len), encrypted.len); | |
| 1031 | try testing.expectEqual(expected_encrypted_len, encrypted.len); | |
| 1032 | // decrypt | |
| 1033 | const content_type, const decrypted = try server_cipher.decrypt(&buf, Record.init(encrypted)); | |
| 1034 | try testing.expectEqualSlices(u8, cleartext, decrypted); | |
| 1035 | try testing.expectEqual(.application_data, content_type); | |
| 1036 | } | |
| 1037 | // server to client | |
| 1038 | { | |
| 1039 | const encrypted = try server_cipher.encrypt(&buf, .application_data, cleartext); | |
| 1040 | const content_type, const decrypted = try client_cipher.decrypt(&buf, Record.init(encrypted)); | |
| 1041 | try testing.expectEqualSlices(u8, cleartext, decrypted); | |
| 1042 | try testing.expectEqual(.application_data, content_type); | |
| 1043 | } | |
| 1044 | } | |
| 1045 | ||
| 1046 | const data13 = ; | |
| 1047 | ||
| 1048 | /// Pair of ciphers based on keys from well-known key/iv pairs | |
| 1049 | pub fn testCiphers() struct { Cipher, Cipher } { | |
| 1050 | const client_cipher: Cipher = .{ .AES_256_GCM_SHA384 = .{ | |
| 1051 | .encrypt_secret = undefined, | |
| 1052 | .decrypt_secret = undefined, | |
| 1053 | .encrypt_key = data13.client_application_key, | |
| 1054 | .decrypt_key = data13.server_application_key, | |
| 1055 | .encrypt_iv = data13.client_application_iv, | |
| 1056 | .decrypt_iv = data13.server_application_iv, | |
| 1057 | } }; | |
| 1058 | const server_cipher: Cipher = .{ .AES_256_GCM_SHA384 = .{ | |
| 1059 | .encrypt_secret = undefined, | |
| 1060 | .decrypt_secret = undefined, | |
| 1061 | .encrypt_key = data13.server_application_key, | |
| 1062 | .decrypt_key = data13.client_application_key, | |
| 1063 | .encrypt_iv = data13.server_application_iv, | |
| 1064 | .decrypt_iv = data13.client_application_iv, | |
| 1065 | } }; | |
| 1066 | return .{ client_cipher, server_cipher }; | |
| 1067 | } | |
| 1068 | ||
| 1069 | test testCiphers { | |
| 1070 | var client_cipher, var server_cipher = testCiphers(); | |
| 1071 | var buffer: [1024]u8 = undefined; | |
| 1072 | { | |
| 1073 | const payload = "ping"; | |
| 1074 | const ciphertext = try client_cipher.encrypt(&buffer, .application_data, payload); | |
| 1075 | try testing.expectEqualSlices(u8, &data13.client_ping_wrapped, ciphertext); | |
| 1076 | ||
| 1077 | const content_type, const cleartext = try server_cipher.decrypt(&buffer, record.Record.init(ciphertext)); | |
| 1078 | try testing.expectEqualStrings(payload, cleartext); | |
| 1079 | try testing.expectEqual(.application_data, content_type); | |
| 1080 | } | |
| 1081 | { | |
| 1082 | const close_notify = &proto.Alert.closeNotify(); | |
| 1083 | const ciphertext = try client_cipher.encrypt(&buffer, .alert, close_notify); | |
| 1084 | ||
| 1085 | const content_type, const cleartext = try server_cipher.decrypt(&buffer, record.Record.init(ciphertext)); | |
| 1086 | try testing.expectEqualStrings(close_notify, cleartext); | |
| 1087 | try testing.expectEqual(.alert, content_type); | |
| 1088 | } | |
| 1089 | } | |
| @@ -1,663 +0,0 @@ | ||
| 1 | const std = ; | |
| 2 | const builtin = ; | |
| 3 | const mem = std.mem; | |
| 4 | const Io = std.Io; | |
| 5 | const assert = std.debug.assert; | |
| 6 | ||
| 7 | const proto = ; | |
| 8 | const record = ; | |
| 9 | const Record = record.Record; | |
| 10 | const cipher = ; | |
| 11 | const Cipher = cipher.Cipher; | |
| 12 | const SessionResumption = .Options.SessionResumption; | |
| 13 | ||
| 14 | const log = std.log.scoped(.tls); | |
| 15 | ||
| 16 | pub const Connection = struct { | |
| 17 | /// Underlying network connection stream reader/writer pair. | |
| 18 | input: *Io.Reader, // source of the encrypted (ciphertext) data | |
| 19 | output: *Io.Writer, // sink to send encrypted (ciphertext) data | |
| 20 | cipher: Cipher, | |
| 21 | ||
| 22 | max_encrypt_seq: u64 = std.math.maxInt(u64) - 1, | |
| 23 | key_update_requested: bool = false, | |
| 24 | received_close_notify: bool = false, | |
| 25 | /// Part of the cleartext record returned from next but not yet read by client. | |
| 26 | cleartext_buf: []const u8 = &.{}, | |
| 27 | ||
| 28 | session_resumption: ?*SessionResumption = null, | |
| 29 | session_resumption_secret_idx: ?usize = null, | |
| 30 | ||
| 31 | const Self = @This(); | |
| 32 | ||
| 33 | /// Encrypts and writes single tls record to the stream. | |
| 34 | fn writeRecord(c: *Self, content_type: proto.ContentType, bytes: []const u8) !void { | |
| 35 | assert(bytes.len <= cipher.max_cleartext_len); | |
| 36 | // If key update is requested send key update message and update | |
| 37 | // my encryption keys. | |
| 38 | if (c.cipher.encryptSeq() >= c.max_encrypt_seq or @atomicLoad(bool, &c.key_update_requested, .monotonic)) { | |
| 39 | @atomicStore(bool, &c.key_update_requested, false, .monotonic); | |
| 40 | ||
| 41 | // If the request_update field is set to "update_requested", | |
| 42 | // then the receiver MUST send a KeyUpdate of its own with | |
| 43 | // request_update set to "update_not_requested" prior to sending | |
| 44 | // its next Application Data record. This mechanism allows | |
| 45 | // either side to force an update to the entire connection, but | |
| 46 | // causes an implementation which receives multiple KeyUpdates | |
| 47 | // while it is silent to respond with a single update. | |
| 48 | // | |
| 49 | // rfc: https://datatracker.ietf.org/doc/html/rfc8446#autoid-57 | |
| 50 | const key_update = &record.handshakeHeader(.key_update, 1) ++ [_]u8{0}; | |
| 51 | try c.encryptWrite(.handshake, key_update); | |
| 52 | try c.cipher.keyUpdateEncrypt(); | |
| 53 | } | |
| 54 | try c.encryptWrite(content_type, bytes); | |
| 55 | } | |
| 56 | ||
| 57 | fn encryptWrite(c: *Self, content_type: proto.ContentType, bytes: []const u8) !void { | |
| 58 | const encrypted_len = c.cipher.recordLen(bytes.len); | |
| 59 | const writable = try c.output.writableSliceGreedy(encrypted_len); | |
| 60 | const rec = try c.cipher.encrypt(writable, content_type, bytes); | |
| 61 | c.output.advance(rec.len); | |
| 62 | try c.output.flush(); | |
| 63 | } | |
| 64 | ||
| 65 | /// Returns next record of cleartext data. Null on end of stream. | |
| 66 | /// Can be used in iterator like loop without memcpy to another buffer: | |
| 67 | /// while (try client.next()) |buf| { ... } | |
| 68 | pub fn next(c: *Self) anyerror!?[]const u8 { | |
| 69 | return c.nextRecord(&.{}) catch |err| { | |
| 70 | if (err == error.EndOfStream) return null; | |
| 71 | // Write alert on tls errors. | |
| 72 | // Stream errors return to the caller. | |
| 73 | if (mem.startsWith(u8, @errorName(err), "Tls")) | |
| 74 | try c.encryptWrite(.alert, &proto.alertFromError(err)); | |
| 75 | return err; | |
| 76 | }; | |
| 77 | } | |
| 78 | ||
| 79 | /// Decrypt next tls record into buffer, if buffer is not big enough reuse | |
| 80 | /// input ciphertext buffer for cleartext. Returns cleartext of the next tls | |
| 81 | /// record. | |
| 82 | fn nextRecord(c: *Self, buffer: []u8) ![]const u8 { | |
| 83 | assert(c.cleartext_buf.len == 0); | |
| 84 | if (c.received_close_notify) return error.EndOfStream; | |
| 85 | while (true) { | |
| 86 | const rec = try Record.read(c.input); | |
| 87 | if (rec.protocol_version != .tls_1_2) return error.TlsBadVersion; | |
| 88 | ||
| 89 | // If provided buffer is not big enought reuse input buffer for | |
| 90 | // cleartext. `rec.header` and `rec.payload`(ciphertext) are | |
| 91 | // pointing somewhere in this buffer. Decrypter is first reading | |
| 92 | // then writing a block, cleartext has less length then ciphertext, | |
| 93 | // cleartext starts from the beginning of the buffer, so ciphertext | |
| 94 | // is always ahead of cleartext. | |
| 95 | const cleartext_buf = if (buffer.len >= rec.payload.len) buffer else @constCast(rec.buffer); | |
| 96 | const content_type, const cleartext = try c.cipher.decrypt(cleartext_buf, rec); | |
| 97 | ||
| 98 | switch (content_type) { | |
| 99 | .application_data => {}, | |
| 100 | .handshake => { | |
| 101 | const handshake_type: proto.Handshake = @fromBackingInt(@intCast(cleartext[0])); | |
| 102 | switch (handshake_type) { | |
| 103 | .new_session_ticket => { | |
| 104 | if (c.session_resumption) |r| { | |
| 105 | r.pushTicket(cleartext, c.session_resumption_secret_idx.?) catch {}; | |
| 106 | } | |
| 107 | continue; | |
| 108 | }, | |
| 109 | .key_update => { | |
| 110 | if (cleartext.len != 5) return error.TlsDecodeError; | |
| 111 | // rfc: Upon receiving a KeyUpdate, the receiver MUST | |
| 112 | // update its receiving keys. | |
| 113 | try c.cipher.keyUpdateDecrypt(); | |
| 114 | const key: proto.KeyUpdateRequest = @fromBackingInt(@intCast(cleartext[4])); | |
| 115 | switch (key) { | |
| 116 | .update_requested => { | |
| 117 | @atomicStore(bool, &c.key_update_requested, true, .monotonic); | |
| 118 | }, | |
| 119 | .update_not_requested => {}, | |
| 120 | else => return error.TlsIllegalParameter, | |
| 121 | } | |
| 122 | // this record is handled read next | |
| 123 | continue; | |
| 124 | }, | |
| 125 | else => {}, | |
| 126 | } | |
| 127 | }, | |
| 128 | .alert => { | |
| 129 | if (cleartext.len < 2) return error.TlsUnexpectedMessage; | |
| 130 | try proto.Alert.parse(cleartext[0..2].*).toError(); | |
| 131 | // server side clean shutdown | |
| 132 | c.received_close_notify = true; | |
| 133 | return error.EndOfStream; | |
| 134 | }, | |
| 135 | else => return error.TlsUnexpectedMessage, | |
| 136 | } | |
| 137 | return cleartext; | |
| 138 | } | |
| 139 | } | |
| 140 | ||
| 141 | pub fn eof(c: *Self) bool { | |
| 142 | return c.received_close_notify and c.cleartext_buf.len == 0; | |
| 143 | } | |
| 144 | ||
| 145 | pub fn close(c: *Self) anyerror!void { | |
| 146 | if (c.received_close_notify) return; | |
| 147 | try c.writeRecord(.alert, &proto.Alert.closeNotify()); | |
| 148 | } | |
| 149 | ||
| 150 | // write/read | |
| 151 | ||
| 152 | /// Encrypts cleartext and writes it to the underlying stream as single | |
| 153 | /// tls record. Max single tls record payload length is 1<<14 (16K) | |
| 154 | /// bytes. | |
| 155 | pub fn write(c: *Self, bytes: []const u8) !usize { | |
| 156 | const encrypt_overhead = c.cipher.encryptOverhead(); | |
| 157 | assert(c.output.buffer.len > encrypt_overhead); | |
| 158 | // Find maximum number of bytes which can fit into output buffer as encrypted ciphertext | |
| 159 | const n = @min(bytes.len, cipher.max_cleartext_len, c.output.buffer.len - encrypt_overhead); | |
| 160 | try c.writeRecord(.application_data, bytes[0..n]); | |
| 161 | return n; | |
| 162 | } | |
| 163 | ||
| 164 | /// Encrypts cleartext and writes it to the underlying stream. If needed | |
| 165 | /// splits cleartext into multiple tls record. | |
| 166 | pub fn writeAll(c: *Self, bytes: []const u8) !void { | |
| 167 | var index: usize = 0; | |
| 168 | while (index < bytes.len) { | |
| 169 | index += try c.write(bytes[index..]); | |
| 170 | } | |
| 171 | } | |
| 172 | ||
| 173 | pub fn read(c: *Self, buffer: []u8) !usize { | |
| 174 | if (c.cleartext_buf.len == 0) { | |
| 175 | const cleartext = c.nextRecord(buffer) catch |err| { | |
| 176 | if (err == error.EndOfStream) return 0; | |
| 177 | if (mem.startsWith(u8, @errorName(err), "Tls")) | |
| 178 | try c.encryptWrite(.alert, &proto.alertFromError(err)); | |
| 179 | return err; | |
| 180 | }; | |
| 181 | if (cleartext.ptr == buffer.ptr) { | |
| 182 | // provided buffer is used for cleartext | |
| 183 | return cleartext.len; | |
| 184 | } | |
| 185 | // Buffer was too small input ciphertext buffer was used for cleartext | |
| 186 | // Store referenct to the cleartext | |
| 187 | c.cleartext_buf = cleartext; | |
| 188 | } | |
| 189 | // move part of the cleartext_buf into provided buffer | |
| 190 | const n = @min(c.cleartext_buf.len, buffer.len); | |
| 191 | @memcpy(buffer[0..n], c.cleartext_buf[0..n]); | |
| 192 | c.cleartext_buf = c.cleartext_buf[n..]; | |
| 193 | return n; | |
| 194 | } | |
| 195 | ||
| 196 | /// Returns the number of bytes read. If the number read is smaller than | |
| 197 | /// `buffer.len`, it means the stream reached the end. | |
| 198 | pub fn readAll(c: *Self, buffer: []u8) !usize { | |
| 199 | return c.readAtLeast(buffer, buffer.len); | |
| 200 | } | |
| 201 | ||
| 202 | /// Returns the number of bytes read, calling the underlying read function | |
| 203 | /// the minimal number of times until the buffer has at least `len` bytes | |
| 204 | /// filled. If the number read is less than `len` it means the stream | |
| 205 | /// reached the end. | |
| 206 | pub fn readAtLeast(c: *Self, buffer: []u8, len: usize) !usize { | |
| 207 | assert(len <= buffer.len); | |
| 208 | var index: usize = 0; | |
| 209 | while (index < len) { | |
| 210 | const amt = try c.read(buffer[index..]); | |
| 211 | if (amt == 0) break; | |
| 212 | index += amt; | |
| 213 | } | |
| 214 | return index; | |
| 215 | } | |
| 216 | ||
| 217 | /// Returns the number of bytes read. If the number read is less than | |
| 218 | /// the space provided it means the stream reached the end. | |
| 219 | pub fn readv(c: *Self, iovecs: []std.posix.iovec) !usize { | |
| 220 | var vp: VecPut = .{ .iovecs = iovecs }; | |
| 221 | while (true) { | |
| 222 | if (c.cleartext_buf.len == 0) { | |
| 223 | c.cleartext_buf = try c.next() orelse break; | |
| 224 | } | |
| 225 | const n = vp.put(c.cleartext_buf); | |
| 226 | const read_buf_len = c.cleartext_buf.len; | |
| 227 | c.cleartext_buf = c.cleartext_buf[n..]; | |
| 228 | if (n < read_buf_len) break; | |
| 229 | } | |
| 230 | return vp.total; | |
| 231 | } | |
| 232 | ||
| 233 | // Io.Reader interface | |
| 234 | ||
| 235 | pub const Reader = struct { | |
| 236 | conn: *Connection, | |
| 237 | interface: Io.Reader, | |
| 238 | err: ?anyerror = null, | |
| 239 | ||
| 240 | pub fn init(c: *Connection, buffer: []u8) Reader { | |
| 241 | return .{ | |
| 242 | .conn = c, | |
| 243 | .interface = .{ | |
| 244 | .vtable = &.{ | |
| 245 | .stream = stream, | |
| 246 | }, | |
| 247 | .buffer = buffer, | |
| 248 | .seek = 0, | |
| 249 | .end = 0, | |
| 250 | }, | |
| 251 | }; | |
| 252 | } | |
| 253 | ||
| 254 | fn stream(r: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize { | |
| 255 | const self: *Reader = @fieldParentPtr("interface", r); | |
| 256 | const n = self.conn.read(limit.slice(w.unusedCapacitySlice())) catch |err| { | |
| 257 | self.err = err; | |
| 258 | if (err == error.EndOfStream) return error.EndOfStream; | |
| 259 | return error.ReadFailed; | |
| 260 | }; | |
| 261 | if (n == 0) return error.EndOfStream; | |
| 262 | w.advance(n); | |
| 263 | return n; | |
| 264 | } | |
| 265 | }; | |
| 266 | ||
| 267 | /// There is no strict requirement on buffer size. | |
| 268 | /// If the buffer is big enough tls record will be decrypted directly into | |
| 269 | /// provided buffer. If not input buffer will be used for record decryption | |
| 270 | /// and than cleartext will be copied to the reader buffer when needed. | |
| 271 | pub fn reader(c: *Self, buffer: []u8) Reader { | |
| 272 | return .init(c, buffer); | |
| 273 | } | |
| 274 | ||
| 275 | // Io.Writer interface | |
| 276 | ||
| 277 | pub const Writer = struct { | |
| 278 | conn: *Connection, | |
| 279 | interface: Io.Writer, | |
| 280 | err: ?anyerror = null, | |
| 281 | ||
| 282 | pub fn init(c: *Connection, buffer: []u8) Writer { | |
| 283 | return .{ | |
| 284 | .conn = c, | |
| 285 | .interface = .{ | |
| 286 | .vtable = &.{ | |
| 287 | .drain = drain, | |
| 288 | }, | |
| 289 | .buffer = buffer, | |
| 290 | .end = 0, | |
| 291 | }, | |
| 292 | }; | |
| 293 | } | |
| 294 | ||
| 295 | fn drain(w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { | |
| 296 | const self: *Writer = @fieldParentPtr("interface", w); | |
| 297 | // w.buffer is consumed first | |
| 298 | try self.writeAll(w.buffered()); | |
| 299 | w.end = 0; | |
| 300 | ||
| 301 | if (data.len == 0) return 0; | |
| 302 | // Followed by each slice of `data` in order | |
| 303 | var n: usize = 0; | |
| 304 | for (data[0 .. data.len - 1]) |bytes| { | |
| 305 | try self.writeAll(bytes); | |
| 306 | n += bytes.len; | |
| 307 | } | |
| 308 | ||
| 309 | // Last element of `data` is repeated as necessary so that it is | |
| 310 | // written `splat` number of times, which may be zero. | |
| 311 | const pattern = data[data.len - 1]; | |
| 312 | for (0..splat) |_| { | |
| 313 | try self.writeAll(pattern); | |
| 314 | n += pattern.len; | |
| 315 | } | |
| 316 | ||
| 317 | // Number of bytes consumed from `data` is returned, excluding bytes | |
| 318 | // from w.buffer. | |
| 319 | return n; | |
| 320 | } | |
| 321 | ||
| 322 | fn writeAll(self: *Writer, bytes: []const u8) Io.Writer.Error!void { | |
| 323 | self.conn.writeAll(bytes) catch |err| { | |
| 324 | self.err = err; | |
| 325 | return error.WriteFailed; | |
| 326 | }; | |
| 327 | } | |
| 328 | }; | |
| 329 | ||
| 330 | pub fn writer(c: *Self, buffer: []u8) Writer { | |
| 331 | return .init(c, buffer); | |
| 332 | } | |
| 333 | }; | |
| 334 | ||
| 335 | const testing = std.testing; | |
| 336 | const data12 = ; | |
| 337 | const testu = ; | |
| 338 | ||
| 339 | test "encrypt decrypt" { | |
| 340 | var output_buf: [1024]u8 = undefined; | |
| 341 | const server_pongs = data12.server_pong ++ data12.server_pong ++ | |
| 342 | data12.server_pong ++ data12.server_pong; | |
| 343 | var stream_reader: Io.Reader = .fixed(&server_pongs); | |
| 344 | var stream_writer: Io.Writer = .fixed(&output_buf); | |
| 345 | var conn: Connection = .{ | |
| 346 | .input = &stream_reader, | |
| 347 | .output = &stream_writer, | |
| 348 | .cipher = try Cipher.initTls12(.ECDHE_RSA_WITH_AES_128_CBC_SHA, &data12.key_material, .client), | |
| 349 | }; | |
| 350 | conn.cipher.ECDHE_RSA_WITH_AES_128_CBC_SHA.rnd = testu.random(0); // use fixed rng | |
| 351 | ||
| 352 | { // encrypt verify data from example | |
| 353 | _ = testu.random(0x40); // sets iv to 40, 41, ... 4f | |
| 354 | try conn.writeRecord(.handshake, &data12.client_finished); | |
| 355 | try testing.expectEqualSlices(u8, &data12.verify_data_encrypted_msg, conn.output.buffered()); | |
| 356 | } | |
| 357 | _ = conn.output.consumeAll(); // reset writer buffer | |
| 358 | { // encrypt ping | |
| 359 | const cleartext = "ping"; | |
| 360 | _ = testu.random(0); // sets iv to 00, 01, ... 0f | |
| 361 | ||
| 362 | try conn.writeAll(cleartext); | |
| 363 | try testing.expectEqualSlices(u8, &data12.encrypted_ping_msg, conn.output.buffered()); | |
| 364 | } | |
| 365 | _ = conn.output.consumeAll(); | |
| 366 | { // writer interface | |
| 367 | const cleartext = "ping"; | |
| 368 | _ = testu.random(0); // sets iv to 00, 01, ... 0f | |
| 369 | conn.cipher.ECDHE_RSA_WITH_AES_128_CBC_SHA.encrypt_seq = 1; // reset sequence | |
| 370 | ||
| 371 | var writer = conn.writer(&.{}); | |
| 372 | var w = &writer.interface; | |
| 373 | try w.writeAll(cleartext); | |
| 374 | try testing.expectEqualSlices(u8, &data12.encrypted_ping_msg, conn.output.buffered()); | |
| 375 | } | |
| 376 | { // decrypt server pong message | |
| 377 | conn.cipher.ECDHE_RSA_WITH_AES_128_CBC_SHA.decrypt_seq = 1; | |
| 378 | try testing.expectEqualStrings("pong", (try conn.next()).?); | |
| 379 | } | |
| 380 | { // test reader interface | |
| 381 | conn.cipher.ECDHE_RSA_WITH_AES_128_CBC_SHA.decrypt_seq = 1; | |
| 382 | var buffer: [2]u8 = undefined; | |
| 383 | var reader = conn.reader(&buffer); | |
| 384 | var rdr = &reader.interface; | |
| 385 | try testing.expectEqualStrings("", conn.cleartext_buf); | |
| 386 | try testing.expectEqualStrings("po", try rdr.take(rdr.buffer.len)); | |
| 387 | // cleartext record part which didn't fit into reader buffer | |
| 388 | try testing.expectEqualStrings("ng", conn.cleartext_buf); | |
| 389 | try testing.expectEqualStrings("n", try rdr.take(1)); | |
| 390 | try testing.expectEqualStrings("", conn.cleartext_buf); | |
| 391 | try testing.expectEqualStrings("g", try rdr.take(1)); | |
| 392 | } | |
| 393 | { // reader discard | |
| 394 | conn.cipher.ECDHE_RSA_WITH_AES_128_CBC_SHA.decrypt_seq = 1; | |
| 395 | var buffer: [5]u8 = undefined; | |
| 396 | var reader = conn.reader(&buffer); | |
| 397 | var rdr = &reader.interface; | |
| 398 | try testing.expectEqual(1, try rdr.discard(.limited(1))); | |
| 399 | try testing.expectEqualStrings("ong", conn.cleartext_buf); | |
| 400 | try testing.expectEqual(3, try rdr.discard(.limited(3))); | |
| 401 | } | |
| 402 | { // test readv interface | |
| 403 | conn.cipher.ECDHE_RSA_WITH_AES_128_CBC_SHA.decrypt_seq = 1; | |
| 404 | var buffer: [9]u8 = undefined; | |
| 405 | var iovecs = [_]std.posix.iovec{ | |
| 406 | .{ .base = &buffer, .len = 3 }, | |
| 407 | .{ .base = buffer[3..], .len = 3 }, | |
| 408 | .{ .base = buffer[6..], .len = 3 }, | |
| 409 | }; | |
| 410 | const n = try conn.readv(iovecs[0..]); | |
| 411 | try testing.expectEqual(4, n); | |
| 412 | try testing.expectEqualStrings("pong", buffer[0..n]); | |
| 413 | } | |
| 414 | } | |
| 415 | ||
| 416 | // Copied from: https://github.com/ziglang/zig/blob/455899668b620dfda40252501c748c0a983555bd/lib/std/crypto/tls/Client.zig#L1354 | |
| 417 | /// Abstraction for sending multiple byte buffers to a slice of iovecs. | |
| 418 | pub const VecPut = struct { | |
| 419 | iovecs: []const std.posix.iovec, | |
| 420 | idx: usize = 0, | |
| 421 | off: usize = 0, | |
| 422 | total: usize = 0, | |
| 423 | ||
| 424 | /// Returns the amount actually put which is always equal to bytes.len | |
| 425 | /// unless the vectors ran out of space. | |
| 426 | pub fn put(vp: *VecPut, bytes: []const u8) usize { | |
| 427 | if (vp.idx >= vp.iovecs.len) return 0; | |
| 428 | var bytes_i: usize = 0; | |
| 429 | while (true) { | |
| 430 | const v = vp.iovecs[vp.idx]; | |
| 431 | const dest = v.base[vp.off..v.len]; | |
| 432 | const src = bytes[bytes_i..][0..@min(dest.len, bytes.len - bytes_i)]; | |
| 433 | @memcpy(dest[0..src.len], src); | |
| 434 | bytes_i += src.len; | |
| 435 | vp.off += src.len; | |
| 436 | if (vp.off >= v.len) { | |
| 437 | vp.off = 0; | |
| 438 | vp.idx += 1; | |
| 439 | if (vp.idx >= vp.iovecs.len) { | |
| 440 | vp.total += bytes_i; | |
| 441 | return bytes_i; | |
| 442 | } | |
| 443 | } | |
| 444 | if (bytes_i >= bytes.len) { | |
| 445 | vp.total += bytes_i; | |
| 446 | return bytes_i; | |
| 447 | } | |
| 448 | } | |
| 449 | } | |
| 450 | }; | |
| 451 | ||
| 452 | test "client/server connection" { | |
| 453 | // create ciphers pair | |
| 454 | const cipher_client, const cipher_server = brk: { | |
| 455 | const Transcript = .Transcript; | |
| 456 | const CipherSuite = .CipherSuite; | |
| 457 | const cipher_suite: CipherSuite = .AES_256_GCM_SHA384; | |
| 458 | ||
| 459 | var rnd: [128]u8 = undefined; | |
| 460 | .fill(&rnd); | |
| 461 | const secret = Transcript.Secret{ | |
| 462 | .client = rnd[0..64], | |
| 463 | .server = rnd[64..], | |
| 464 | }; | |
| 465 | ||
| 466 | break :brk .{ | |
| 467 | try Cipher.initTls13(cipher_suite, secret, .client), | |
| 468 | try Cipher.initTls13(cipher_suite, secret, .server), | |
| 469 | }; | |
| 470 | }; | |
| 471 | ||
| 472 | var client_conn: Connection = .{ | |
| 473 | .input = undefined, | |
| 474 | .output = undefined, | |
| 475 | .cipher = cipher_client, | |
| 476 | }; | |
| 477 | var server_conn: Connection = .{ | |
| 478 | .input = undefined, | |
| 479 | .output = undefined, | |
| 480 | .cipher = cipher_server, | |
| 481 | }; | |
| 482 | ||
| 483 | // big enough cleartext to produce multiple tls records | |
| 484 | var cleartext_buf: [cipher.max_cleartext_len * 5]u8 = undefined; | |
| 485 | // fill cleartext_buf with random bytes | |
| 486 | var prng = std.Random.DefaultPrng.init(0); | |
| 487 | const random = prng.random(); | |
| 488 | random.bytes(&cleartext_buf); | |
| 489 | ||
| 490 | // cleartext -> client_conn -> ciphertext -> server_conn -> cleartext | |
| 491 | for (0..16) |_| { | |
| 492 | // use random part of clartext_buf | |
| 493 | const n = random.intRangeAtMost(usize, cipher.max_cleartext_len + 1, cleartext_buf.len); | |
| 494 | const client_cleartext = cleartext_buf[0..n]; | |
| 495 | ||
| 496 | // prepare ciphertext buffer | |
| 497 | var ciphertext_buf: [cleartext_buf.len]u8 = undefined; | |
| 498 | var w: Io.Writer = .fixed(&ciphertext_buf); | |
| 499 | client_conn.output = &w; | |
| 500 | ||
| 501 | // write cleartext to the server side | |
| 502 | try client_conn.writeAll(client_cleartext); | |
| 503 | const ciphertext_len = NonBlock.init(cipher_client).encryptedLength(n); | |
| 504 | try testing.expectEqual(ciphertext_len, w.buffered().len); | |
| 505 | ||
| 506 | // feed ciphertext from client to the server | |
| 507 | var r: Io.Reader = .fixed(w.buffered()); | |
| 508 | server_conn.input = &r; | |
| 509 | var server_cleartext_buf: [cleartext_buf.len]u8 = undefined; | |
| 510 | // read cleartext from the server connection | |
| 511 | const nr = try server_conn.readAll(&server_cleartext_buf); | |
| 512 | const server_cleartext = server_cleartext_buf[0..nr]; | |
| 513 | ||
| 514 | try testing.expectEqual(n, nr); | |
| 515 | try testing.expectEqualSlices(u8, client_cleartext, server_cleartext); | |
| 516 | } | |
| 517 | } | |
| 518 | ||
| 519 | pub const NonBlock = struct { | |
| 520 | const Self = @This(); | |
| 521 | ||
| 522 | inner: Connection, | |
| 523 | ||
| 524 | pub fn init(c: Cipher) Self { | |
| 525 | return .{ .inner = .{ .cipher = c, .input = undefined, .output = undefined } }; | |
| 526 | } | |
| 527 | ||
| 528 | /// Required ciphertext buffer length for the given cleartext length. | |
| 529 | pub fn encryptedLength(self: Self, cleartext_len: usize) usize { | |
| 530 | const c = self.inner.cipher; | |
| 531 | const records_count = cleartext_len / cipher.max_cleartext_len; | |
| 532 | if (records_count == 0) return c.recordLen(cleartext_len); | |
| 533 | const last_chunk_len = cleartext_len - cipher.max_cleartext_len * records_count; | |
| 534 | return c.recordLen(cipher.max_cleartext_len) * records_count + | |
| 535 | if (last_chunk_len == 0) 0 else c.recordLen(last_chunk_len); | |
| 536 | } | |
| 537 | ||
| 538 | fn reset(self: *Self) void { | |
| 539 | self.inner.input = undefined; | |
| 540 | self.inner.output = undefined; | |
| 541 | } | |
| 542 | ||
| 543 | /// Encrypts cleartext into ciphertext. | |
| 544 | /// If ciphertext.len is >= encryptedLength(cleartext.len) whole | |
| 545 | /// cleartext will be consumed. | |
| 546 | pub fn encrypt( | |
| 547 | self: *Self, | |
| 548 | /// Cleartext data to encrypt | |
| 549 | cleartext: []const u8, | |
| 550 | /// Write buffer for ciphertext; encrypted data | |
| 551 | ciphertext: []u8, | |
| 552 | ) !struct { | |
| 553 | /// Number of bytes consumed from cleartext | |
| 554 | cleartext_pos: usize = 0, | |
| 555 | /// Unused part of the provided cleartext buffer | |
| 556 | unused_cleartext: []const u8, | |
| 557 | /// Encrypted ciphertext data | |
| 558 | ciphertext: []u8, | |
| 559 | } { | |
| 560 | defer self.reset(); | |
| 561 | var output: Io.Writer = .fixed(ciphertext); | |
| 562 | self.inner.output = &output; | |
| 563 | ||
| 564 | var n: usize = 0; | |
| 565 | while (n < cleartext.len and output.end < output.buffer.len) { | |
| 566 | n += try self.inner.write(cleartext[n..]); | |
| 567 | } | |
| 568 | return .{ | |
| 569 | .cleartext_pos = n, | |
| 570 | .unused_cleartext = cleartext[n..], | |
| 571 | .ciphertext = output.buffered(), | |
| 572 | }; | |
| 573 | } | |
| 574 | ||
| 575 | /// Decrypts ciphertext into cleartext. | |
| 576 | /// NOTE: It is safe to reuses ciphertext buffer for cleartext data. | |
| 577 | pub fn decrypt( | |
| 578 | self: *Self, | |
| 579 | /// Ciphertext data recieved from the other side of the tls connection | |
| 580 | ciphertext: []const u8, | |
| 581 | /// Write buffer for cleartext; decrypted data | |
| 582 | cleartext: []u8, | |
| 583 | ) !struct { | |
| 584 | /// Number of bytes consumed from provided ciphertext buffer | |
| 585 | ciphertext_pos: usize, | |
| 586 | /// Unconsumed part of the provided ciphertext buffer | |
| 587 | unused_ciphertext: []const u8, | |
| 588 | /// Decrypted cleartext data | |
| 589 | cleartext: []u8, | |
| 590 | /// Is clear notify alert received | |
| 591 | closed: bool = false, | |
| 592 | } { | |
| 593 | defer self.reset(); | |
| 594 | var input: Io.Reader = .fixed(ciphertext); | |
| 595 | self.inner.input = &input; | |
| 596 | ||
| 597 | var n: usize = 0; | |
| 598 | while (n < cleartext.len) { | |
| 599 | const nn = self.inner.read(cleartext[n..]) catch |err| switch (err) { | |
| 600 | error.InputBufferUndersize => break, | |
| 601 | else => return err, | |
| 602 | }; | |
| 603 | if (nn == 0) break; | |
| 604 | n += nn; | |
| 605 | } | |
| 606 | ||
| 607 | return .{ | |
| 608 | .ciphertext_pos = input.seek, | |
| 609 | .unused_ciphertext = input.buffered(), | |
| 610 | .cleartext = cleartext[0..n], | |
| 611 | .closed = self.inner.received_close_notify, | |
| 612 | }; | |
| 613 | } | |
| 614 | ||
| 615 | pub fn close(self: *Self, ciphertext: []u8) ![]const u8 { | |
| 616 | return try self.inner.cipher.encrypt(ciphertext, .alert, &proto.Alert.closeNotify()); | |
| 617 | } | |
| 618 | }; | |
| 619 | ||
| 620 | test "nonblock encrypt" { | |
| 621 | const data13 = ; | |
| 622 | const client_cipher, _ = cipher.testCiphers(); | |
| 623 | var conn = NonBlock.init(client_cipher); | |
| 624 | ||
| 625 | const cleartext = "ping"; | |
| 626 | try testing.expectEqual(26, conn.encryptedLength(cleartext.len)); | |
| 627 | var ciphertext: [32]u8 = undefined; | |
| 628 | ||
| 629 | // with enough big buffer | |
| 630 | { | |
| 631 | const res = try conn.encrypt(cleartext, &ciphertext); | |
| 632 | try testing.expectEqual(cleartext.len, res.cleartext_pos); | |
| 633 | try testing.expectEqual(26, res.ciphertext.len); | |
| 634 | try testing.expectEqualSlices(u8, &data13.client_ping_wrapped, res.ciphertext); | |
| 635 | } | |
| 636 | ||
| 637 | // while ciphertext buffer is not enough for cleartext record | |
| 638 | for (conn.inner.cipher.encryptOverhead() + 1..ciphertext.len) |i| { | |
| 639 | const res = try conn.encrypt(cleartext, ciphertext[0..i]); | |
| 640 | const expected_used_cleartext = @min(cleartext.len, i - conn.inner.cipher.encryptOverhead()); | |
| 641 | try testing.expectEqual(expected_used_cleartext, res.cleartext_pos); | |
| 642 | try testing.expectEqual(expected_used_cleartext + conn.inner.cipher.encryptOverhead(), res.ciphertext.len); | |
| 643 | } | |
| 644 | } | |
| 645 | ||
| 646 | test "nonblock decrypt" { | |
| 647 | const data13 = ; | |
| 648 | _, const server_cipher = cipher.testCiphers(); | |
| 649 | var conn = NonBlock.init(server_cipher); | |
| 650 | ||
| 651 | const ciphertext = data13.client_ping_wrapped; | |
| 652 | var cleartext_buf: [32]u8 = undefined; | |
| 653 | ||
| 654 | for (1..ciphertext.len - 1) |i| { | |
| 655 | const res = try conn.decrypt(ciphertext[0..i], &cleartext_buf); | |
| 656 | try testing.expectEqual(0, res.ciphertext_pos); | |
| 657 | try testing.expectEqual(0, res.cleartext.len); | |
| 658 | try testing.expectEqual(i, res.unused_ciphertext.len); | |
| 659 | } | |
| 660 | ||
| 661 | const res = try conn.decrypt(&ciphertext, &cleartext_buf); | |
| 662 | try testing.expectEqualSlices(u8, "ping", res.cleartext); | |
| 663 | } | |
| @@ -1,1396 +0,0 @@ | ||
| 1 | const std = ; | |
| 2 | const assert = std.debug.assert; | |
| 3 | const crypto = std.crypto; | |
| 4 | const mem = std.mem; | |
| 5 | const posix = std.posix; | |
| 6 | const Io = std.Io; | |
| 7 | const Certificate = crypto.Certificate; | |
| 8 | ||
| 9 | // Helper function for Zig 0.16 compatibility | |
| 10 | fn getMilliTimestamp() i64 { | |
| 11 | var ts: std.c.timespec = undefined; | |
| 12 | if (std.c.clock_gettime(.REALTIME, &ts) != 0) return 0; | |
| 13 | return ts.sec * 1000 + @divFloor(ts.nsec, std.time.ns_per_ms); | |
| 14 | } | |
| 15 | ||
| 16 | const rng = ; | |
| 17 | const Cipher = .Cipher; | |
| 18 | const CipherSuite = .CipherSuite; | |
| 19 | const cipher_suites = .cipher_suites; | |
| 20 | const max_cleartext_len = .max_cleartext_len; | |
| 21 | ||
| 22 | const Transcript = .Transcript; | |
| 23 | const record = ; | |
| 24 | const Record = record.Record; | |
| 25 | const rsa = ; | |
| 26 | const key_log = ; | |
| 27 | const PrivateKey = ; | |
| 28 | const proto = ; | |
| 29 | ||
| 30 | const common = ; | |
| 31 | const CertificateBuilder = common.CertificateBuilder; | |
| 32 | const CertificateParser = common.CertificateParser; | |
| 33 | const DhKeyPair = common.DhKeyPair; | |
| 34 | const CertKeyPair = common.CertKeyPair; | |
| 35 | const cert = common.cert; | |
| 36 | ||
| 37 | const log = std.log.scoped(.tls); | |
| 38 | ||
| 39 | pub const Options = struct { | |
| 40 | host: []const u8, | |
| 41 | /// Set of root certificate authorities that clients use when verifying | |
| 42 | /// server certificates. | |
| 43 | root_ca: cert.Bundle, | |
| 44 | ||
| 45 | /// Controls whether a client verifies the server's certificate chain and | |
| 46 | /// host name. | |
| 47 | insecure_skip_verify: bool = false, | |
| 48 | ||
| 49 | /// List of cipher suites to use. | |
| 50 | /// To use just tls 1.3 cipher suites: | |
| 51 | /// .cipher_suites = &tls.CipherSuite.tls13, | |
| 52 | /// To select particular cipher suite: | |
| 53 | /// .cipher_suites = &[_]tls.CipherSuite{tls.CipherSuite.CHACHA20_POLY1305_SHA256}, | |
| 54 | cipher_suites: []const CipherSuite = cipher_suites.all, | |
| 55 | ||
| 56 | /// List of named groups to use. | |
| 57 | /// To use specific named group: | |
| 58 | /// .named_groups = &[_]tls.NamedGroup{.secp384r1}, | |
| 59 | named_groups: []const proto.NamedGroup = &[_]proto.NamedGroup{ | |
| 60 | .x25519, | |
| 61 | .secp256r1, | |
| 62 | .secp384r1, | |
| 63 | .x25519_ml_kem768, | |
| 64 | }, | |
| 65 | ||
| 66 | /// Client authentication certificates and private key. | |
| 67 | auth: ?*CertKeyPair = null, | |
| 68 | ||
| 69 | /// If this structure is provided it will be filled with handshake attributes | |
| 70 | /// at the end of the handshake process. | |
| 71 | diagnostic: ?*Diagnostic = null, | |
| 72 | ||
| 73 | /// For logging current connection tls keys, so we can share them with | |
| 74 | /// Wireshark and analyze decrypted traffic there. | |
| 75 | key_log_callback: ?key_log.Callback = null, | |
| 76 | ||
| 77 | session_resumption: ?*SessionResumption = null, | |
| 78 | ||
| 79 | pub const Diagnostic = struct { | |
| 80 | tls_version: proto.Version = @fromBackingInt(@intCast(0)), | |
| 81 | cipher_suite_tag: CipherSuite = @fromBackingInt(@intCast(0)), | |
| 82 | named_group: proto.NamedGroup = @fromBackingInt(@intCast(0)), | |
| 83 | signature_scheme: proto.SignatureScheme = @fromBackingInt(@intCast(0)), | |
| 84 | client_signature_scheme: proto.SignatureScheme = @fromBackingInt(@intCast(0)), | |
| 85 | is_session_resumption: bool = false, | |
| 86 | max_server_record_len: usize = 0, | |
| 87 | max_server_cleartext_len: usize = 0, | |
| 88 | max_client_record_len: usize = 0, | |
| 89 | }; | |
| 90 | ||
| 91 | /// Collects and stores session resumption tickets. | |
| 92 | pub const SessionResumption = struct { | |
| 93 | const Self = @This(); | |
| 94 | ||
| 95 | pub const Ticket = struct { | |
| 96 | // in seconds | |
| 97 | lifetime: u32, | |
| 98 | // in milliseconds | |
| 99 | created_at: i64, | |
| 100 | // value to add to the milliseconds age | |
| 101 | age_add: u32, | |
| 102 | // nonce and identity are pointes to the part of the payload | |
| 103 | nonce: []const u8, // <0..255> | |
| 104 | identity: []const u8, // <1..2^16-1> | |
| 105 | // payload is allocated and should be freed | |
| 106 | payload: []const u8, | |
| 107 | // pointer to the externaly allocated secret | |
| 108 | secret: []const u8, | |
| 109 | ||
| 110 | pub fn init(payload: []const u8, secret: []const u8) !Ticket { | |
| 111 | var d = record.Decoder.init(.handshake, payload); | |
| 112 | const handshake_type: proto.Handshake = @fromBackingInt(@intCast(try d.decode(u8))); | |
| 113 | if (handshake_type != proto.Handshake.new_session_ticket) return error.InvalidType; | |
| 114 | const length = try d.decode(u24); | |
| 115 | if (d.rest().len != length) return error.InvalidLength; | |
| 116 | ||
| 117 | return .{ | |
| 118 | .lifetime = try d.decode(u32), | |
| 119 | .age_add = try d.decode(u32), | |
| 120 | .nonce = try d.slice(try d.decode(u8)), | |
| 121 | .identity = try d.slice(try d.decode(u16)), | |
| 122 | .payload = payload, | |
| 123 | .secret = secret, | |
| 124 | .created_at = getMilliTimestamp(), | |
| 125 | }; | |
| 126 | } | |
| 127 | ||
| 128 | /// Obfuscated age for pre shared extension in client hello message when | |
| 129 | /// using this ticket. | |
| 130 | pub fn obfuscatedAge(t: Ticket) u32 { | |
| 131 | const age: u32 = @intCast(getMilliTimestamp() - t.created_at); | |
| 132 | return t.age_add +% age; | |
| 133 | } | |
| 134 | }; | |
| 135 | ||
| 136 | allocator: std.mem.Allocator, | |
| 137 | hash_tag: CipherSuite.HashTag = .sha256, | |
| 138 | tickets: std.ArrayListUnmanaged(Ticket) = .empty, | |
| 139 | secrets: std.ArrayListUnmanaged([]const u8) = .empty, | |
| 140 | used_tickets: usize = 0, | |
| 141 | ||
| 142 | pub fn init(allocator: mem.Allocator) Self { | |
| 143 | return .{ .allocator = allocator }; | |
| 144 | } | |
| 145 | ||
| 146 | /// Set resumption master secret (known at then end of the handshake) to | |
| 147 | /// be used by new session ticket's sent on that connection. Returns | |
| 148 | /// index of the appended secret in the secrets list. Connection should | |
| 149 | /// use that index in pushTicket to connect ticket and the secret. | |
| 150 | pub fn appendSecret(self: *Self, tag: CipherSuite.HashTag, secret: []const u8) !usize { | |
| 151 | if (self.secrets.items.len == 0) self.hash_tag = tag; | |
| 152 | if (tag != self.hash_tag) return error.InvalidHash; | |
| 153 | try self.secrets.ensureUnusedCapacity(self.allocator, 1); | |
| 154 | self.secrets.appendAssumeCapacity(try self.allocator.dupe(u8, secret)); | |
| 155 | return self.secrets.items.len - 1; | |
| 156 | } | |
| 157 | ||
| 158 | /// When new session ticket message is recived connection should push ticket. | |
| 159 | pub fn pushTicket(self: *Self, data: []const u8, secret_idx: usize) !void { | |
| 160 | assert(self.secrets.items.len > secret_idx); | |
| 161 | try self.tickets.ensureUnusedCapacity(self.allocator, 1); | |
| 162 | const payload = try self.allocator.dupe(u8, data); | |
| 163 | errdefer self.allocator.free(payload); | |
| 164 | const ticket = try Ticket.init(payload, self.secrets.items[secret_idx]); | |
| 165 | self.tickets.appendAssumeCapacity(ticket); | |
| 166 | } | |
| 167 | ||
| 168 | pub fn popTicket(self: *Self) ?Ticket { | |
| 169 | if (self.used_tickets >= self.tickets.items.len) return null; | |
| 170 | defer self.used_tickets += 1; | |
| 171 | return self.tickets.items[self.used_tickets]; | |
| 172 | } | |
| 173 | ||
| 174 | pub fn deinit(self: *Self) void { | |
| 175 | for (self.tickets.items) |ticket| | |
| 176 | self.allocator.free(ticket.payload); | |
| 177 | self.tickets.deinit(self.allocator); | |
| 178 | for (self.secrets.items) |secret| | |
| 179 | self.allocator.free(secret); | |
| 180 | self.secrets.deinit(self.allocator); | |
| 181 | } | |
| 182 | ||
| 183 | pub fn print(self: Self) !void { | |
| 184 | std.debug.print( | |
| 185 | "resumption tag: {}, tickets: {} used_tickets: {}\n", | |
| 186 | .{ self.hash_tag, self.tickets.items.len, self.used_tickets }, | |
| 187 | ); | |
| 188 | for (self.tickets.items) |ticket| { | |
| 189 | std.debug.print( | |
| 190 | " lifetime: {}, ticket age add: {}, nonce: {x}\n ticket: {x}\n secret: {x}\n\n", | |
| 191 | .{ ticket.lifetime, ticket.age_add, ticket.nonce, ticket.identity, ticket.secret }, | |
| 192 | ); | |
| 193 | } | |
| 194 | } | |
| 195 | }; | |
| 196 | }; | |
| 197 | ||
| 198 | const ResumptionTicket = Options.SessionResumption.Ticket; | |
| 199 | ||
| 200 | const supported_named_groups = &[_]proto.NamedGroup{ | |
| 201 | .x25519, | |
| 202 | .secp256r1, | |
| 203 | .secp384r1, | |
| 204 | .x25519_ml_kem768, | |
| 205 | }; | |
| 206 | ||
| 207 | /// Handshake parses tls server message and creates client messages. Collects | |
| 208 | /// tls attributes: server random, cipher suite and so on. Client messages are | |
| 209 | /// created using provided buffer. Provided record reader is used to get tls | |
| 210 | /// record when needed. | |
| 211 | pub const Handshake = struct { | |
| 212 | /// Underlying network connection stream reader/writer pair. | |
| 213 | input: *Io.Reader, | |
| 214 | output: *Io.Writer, | |
| 215 | ||
| 216 | client_random: [32]u8 = undefined, | |
| 217 | server_random: [32]u8 = undefined, | |
| 218 | master_secret: [48]u8 = undefined, | |
| 219 | key_material: [48 * 4]u8 = undefined, // for sha256 32 * 4 is filled, for sha384 48 * 4 | |
| 220 | ||
| 221 | transcript: Transcript = .{}, | |
| 222 | cipher_suite: CipherSuite = @fromBackingInt(@intCast(0)), | |
| 223 | named_group: ?proto.NamedGroup = null, | |
| 224 | dh_kp: DhKeyPair = undefined, | |
| 225 | rsa_secret: RsaSecret = undefined, | |
| 226 | tls_version: proto.Version = .tls_1_2, | |
| 227 | cipher: Cipher = undefined, | |
| 228 | cert: CertificateParser = undefined, | |
| 229 | client_certificate_requested: bool = false, | |
| 230 | // public key len: x25519 = 32, secp256r1 = 65, secp384r1 = 97, x25519_ml_kem768 = 1120 | |
| 231 | server_pub_key_buf: [1120]u8 = undefined, | |
| 232 | server_pub_key: []const u8 = undefined, | |
| 233 | pre_shared_selected_identity: ?u16 = null, | |
| 234 | // statistics | |
| 235 | max_server_record_len: usize = 0, | |
| 236 | max_server_cleartext_len: usize = 0, | |
| 237 | max_client_record_len: usize = 0, | |
| 238 | ||
| 239 | const Self = @This(); | |
| 240 | ||
| 241 | fn initKeys(h: *Self, opt: Options) !void { | |
| 242 | const init_keys_buf_len = 32 + 46 + DhKeyPair.seed_len; | |
| 243 | var buf: [init_keys_buf_len]u8 = undefined; | |
| 244 | rng.fill(&buf); | |
| 245 | ||
| 246 | h.client_random = buf[0..32].*; | |
| 247 | h.rsa_secret = RsaSecret.init(buf[32..][0..46].*); | |
| 248 | h.dh_kp = try DhKeyPair.init(buf[32 + 46 ..][0..DhKeyPair.seed_len].*, opt.named_groups); | |
| 249 | ||
| 250 | h.cert = .{ | |
| 251 | .host = opt.host, | |
| 252 | .root_ca = opt.root_ca, | |
| 253 | .skip_verify = opt.insecure_skip_verify, | |
| 254 | }; | |
| 255 | } | |
| 256 | ||
| 257 | /// Handshake exchanges messages with server to get agreement about | |
| 258 | /// cryptographic parameters. That upgrades existing client-server | |
| 259 | /// connection to TLS connection. Returns cipher used in application for | |
| 260 | /// encrypted message exchange. | |
| 261 | /// | |
| 262 | /// Handles TLS 1.2 and TLS 1.3 connections. After initial client hello | |
| 263 | /// server chooses in its server hello which TLS version will be used. | |
| 264 | /// | |
| 265 | /// TLS 1.2 handshake messages exchange: | |
| 266 | /// Client Server | |
| 267 | /// -------------------------------------------------------------- | |
| 268 | /// ClientHello client flight 1 ---> | |
| 269 | /// ServerHello | |
| 270 | /// Certificate | |
| 271 | /// ServerKeyExchange | |
| 272 | /// CertificateRequest* | |
| 273 | /// <--- server flight 1 ServerHelloDone | |
| 274 | /// Certificate* | |
| 275 | /// ClientKeyExchange | |
| 276 | /// CertificateVerify* | |
| 277 | /// ChangeCipherSpec | |
| 278 | /// Finished client flight 2 ---> | |
| 279 | /// ChangeCipherSpec | |
| 280 | /// <--- server flight 2 Finished | |
| 281 | /// | |
| 282 | /// TLS 1.3 handshake messages exchange: | |
| 283 | /// Client Server | |
| 284 | /// -------------------------------------------------------------- | |
| 285 | /// ClientHello client flight 1 ---> | |
| 286 | /// ServerHello | |
| 287 | /// {EncryptedExtensions} | |
| 288 | /// {CertificateRequest*} | |
| 289 | /// {Certificate} | |
| 290 | /// {CertificateVerify} | |
| 291 | /// <--- server flight 1 {Finished} | |
| 292 | /// ChangeCipherSpec | |
| 293 | /// {Certificate*} | |
| 294 | /// {CertificateVerify*} | |
| 295 | /// Finished client flight 2 ---> | |
| 296 | /// | |
| 297 | /// * - optional | |
| 298 | /// {} - encrypted | |
| 299 | /// | |
| 300 | /// References: | |
| 301 | /// https://datatracker.ietf.org/doc/html/rfc5246#section-7.3 | |
| 302 | /// https://datatracker.ietf.org/doc/html/rfc8446#section-2 | |
| 303 | /// | |
| 304 | pub fn handshake(h: *Self, opt: Options) !struct { Cipher, ?usize } { | |
| 305 | var resumption_ticket: ?ResumptionTicket = | |
| 306 | if (opt.session_resumption) |r| | |
| 307 | r.popTicket() | |
| 308 | else | |
| 309 | null; | |
| 310 | ||
| 311 | defer h.updateDiagnostic(opt, resumption_ticket != null); | |
| 312 | try h.initKeys(opt); | |
| 313 | ||
| 314 | if (resumption_ticket) |ticket| { | |
| 315 | h.transcript.use(opt.session_resumption.?.hash_tag); | |
| 316 | h.transcript.setPreSharedSecret(ticket.secret, ticket.nonce); | |
| 317 | } | |
| 318 | ||
| 319 | try h.makeClientHello(opt, resumption_ticket); // client flight 1 | |
| 320 | h.max_client_record_len = h.output.end; | |
| 321 | try h.output.flush(); | |
| 322 | ||
| 323 | try h.readServerFlight1(); // server flight 1 | |
| 324 | ||
| 325 | if (resumption_ticket != null and h.pre_shared_selected_identity == null) { | |
| 326 | // server didn't accept resuption | |
| 327 | resumption_ticket = null; | |
| 328 | h.transcript.clearPreSharedSecret(); | |
| 329 | } | |
| 330 | ||
| 331 | if (resumption_ticket != null and h.cipher_suite.hash() != h.transcript.tag) | |
| 332 | return error.TlsAlertIllegalParameter; // session resumption must use same hash as previous session | |
| 333 | h.transcript.use(h.cipher_suite.hash()); | |
| 334 | ||
| 335 | // tls 1.3 specific handshake part | |
| 336 | if (h.tls_version == .tls_1_3) { | |
| 337 | try h.generateHandshakeCipher(opt.key_log_callback); | |
| 338 | try h.readEncryptedServerFlight1(resumption_ticket != null); // server flight 1 | |
| 339 | const app_cipher = try h.generateApplicationCipher(opt.key_log_callback); | |
| 340 | try h.makeClientFlight2Tls13(opt.auth); // client flight 2 | |
| 341 | h.max_client_record_len = @max(h.max_client_record_len, h.output.end); | |
| 342 | try h.output.flush(); | |
| 343 | ||
| 344 | const secret_idx: ?usize = if (opt.session_resumption) |r| | |
| 345 | try r.appendSecret(h.transcript.tag, h.transcript.resumptionSecret()) | |
| 346 | else | |
| 347 | null; | |
| 348 | ||
| 349 | return .{ app_cipher, secret_idx }; | |
| 350 | } | |
| 351 | ||
| 352 | // tls 1.2 specific handshake part | |
| 353 | try h.generateCipher(opt.key_log_callback); | |
| 354 | try h.makeClientFlight2Tls12(opt.auth); // client flight 2 | |
| 355 | h.max_client_record_len = @max(h.max_client_record_len, h.output.end); | |
| 356 | try h.output.flush(); | |
| 357 | try h.readServerFlight2(); // server flight 2 | |
| 358 | return .{ h.cipher, null }; | |
| 359 | } | |
| 360 | ||
| 361 | fn clientFlight1(h: *Self, opt: Options) !void { | |
| 362 | try h.makeClientHello(opt, null); | |
| 363 | } | |
| 364 | ||
| 365 | fn serverFlight1(h: *Self, opt: Options) !void { | |
| 366 | try h.readServerFlight1(); | |
| 367 | h.transcript.use(h.cipher_suite.hash()); | |
| 368 | if (h.tls_version == .tls_1_3) { | |
| 369 | try h.generateHandshakeCipher(opt.key_log_callback); | |
| 370 | try h.readEncryptedServerFlight1(false); | |
| 371 | } | |
| 372 | } | |
| 373 | ||
| 374 | fn clientFlight2(h: *Self, opt: Options) !void { | |
| 375 | if (h.tls_version == .tls_1_3) { | |
| 376 | const app_cipher = try h.generateApplicationCipher(opt.key_log_callback); | |
| 377 | try h.makeClientFlight2Tls13(opt.auth); | |
| 378 | h.cipher = app_cipher; | |
| 379 | } else { | |
| 380 | // tls 1.2 specific handshake part | |
| 381 | try h.generateCipher(opt.key_log_callback); | |
| 382 | try h.makeClientFlight2Tls12(opt.auth); | |
| 383 | } | |
| 384 | } | |
| 385 | ||
| 386 | fn serverFlight2(h: *Self, _: Options) !void { | |
| 387 | if (h.tls_version == .tls_1_3) return; | |
| 388 | try h.readServerFlight2(); | |
| 389 | } | |
| 390 | ||
| 391 | /// Prepare key material and generate cipher for TLS 1.2 | |
| 392 | fn generateCipher(h: *Self, key_log_callback: ?key_log.Callback) !void { | |
| 393 | try h.verifyCertificateSignatureTls12(); | |
| 394 | try h.generateKeyMaterial(key_log_callback); | |
| 395 | h.cipher = try Cipher.initTls12(h.cipher_suite, &h.key_material, .client); | |
| 396 | } | |
| 397 | ||
| 398 | /// Generate TLS 1.2 pre master secret, master secret and key material. | |
| 399 | fn generateKeyMaterial(h: *Self, key_log_callback: ?key_log.Callback) !void { | |
| 400 | const pre_master_secret = if (h.named_group) |named_group| | |
| 401 | try h.dh_kp.sharedKey(named_group, h.server_pub_key) | |
| 402 | else | |
| 403 | &h.rsa_secret.secret; | |
| 404 | ||
| 405 | h.transcript.masterSecret(&h.master_secret, pre_master_secret, h.client_random, h.server_random); | |
| 406 | h.transcript.keyMaterial(&h.key_material, &h.master_secret, h.client_random, h.server_random); | |
| 407 | ||
| 408 | if (key_log_callback) |cb| { | |
| 409 | cb(key_log.label.client_random, &h.client_random, &h.master_secret); | |
| 410 | } | |
| 411 | } | |
| 412 | ||
| 413 | /// TLS 1.3 cipher used during handshake | |
| 414 | fn generateHandshakeCipher(h: *Self, key_log_callback: ?key_log.Callback) !void { | |
| 415 | const shared_key = try h.dh_kp.sharedKey(h.named_group.?, h.server_pub_key); | |
| 416 | const handshake_secret = h.transcript.handshakeSecret(shared_key); | |
| 417 | if (key_log_callback) |cb| { | |
| 418 | cb(key_log.label.server_handshake_traffic_secret, &h.client_random, handshake_secret.server); | |
| 419 | cb(key_log.label.client_handshake_traffic_secret, &h.client_random, handshake_secret.client); | |
| 420 | } | |
| 421 | h.cipher = try Cipher.initTls13(h.cipher_suite, handshake_secret, .client); | |
| 422 | } | |
| 423 | ||
| 424 | /// TLS 1.3 application (client) cipher | |
| 425 | fn generateApplicationCipher(h: *Self, key_log_callback: ?key_log.Callback) !Cipher { | |
| 426 | const application_secret = h.transcript.applicationSecret(); | |
| 427 | if (key_log_callback) |cb| { | |
| 428 | cb(key_log.label.server_traffic_secret_0, &h.client_random, application_secret.server); | |
| 429 | cb(key_log.label.client_traffic_secret_0, &h.client_random, application_secret.client); | |
| 430 | } | |
| 431 | return try Cipher.initTls13(h.cipher_suite, application_secret, .client); | |
| 432 | } | |
| 433 | ||
| 434 | fn makeClientHello(h: *Self, opt: Options, resumption_ticket: ?ResumptionTicket) !void { | |
| 435 | // Prepare data | |
| 436 | const tls_versions = try CipherSuite.versions(opt.cipher_suites); | |
| 437 | const shared_keys: []const []const u8 = if (tls_versions != .tls_1_2) brk: { | |
| 438 | var keys: [supported_named_groups.len][]const u8 = undefined; | |
| 439 | for (opt.named_groups, 0..) |ng, i| { | |
| 440 | keys[i] = try h.dh_kp.publicKey(ng); | |
| 441 | } | |
| 442 | break :brk keys[0..opt.named_groups.len]; | |
| 443 | } else &.{}; | |
| 444 | const supported_versions = switch (tls_versions) { | |
| 445 | .both => &[_]proto.Version{ .tls_1_3, .tls_1_2 }, | |
| 446 | .tls_1_3 => &[_]proto.Version{.tls_1_3}, | |
| 447 | .tls_1_2 => &[_]proto.Version{.tls_1_2}, | |
| 448 | }; | |
| 449 | const psk_binder_len = if (resumption_ticket == null) 0 else h.transcript.hashLength() + 3; | |
| 450 | ||
| 451 | var w: record.Writer = .initFromIo(h.output); | |
| 452 | ||
| 453 | // Placeholder for tls record header (5 bytes) and handshake header (4 bytes). | |
| 454 | // We will write it when we know payload length. | |
| 455 | const header_pos = try w.skip(9); | |
| 456 | ||
| 457 | try w.enumValue(proto.Version.tls_1_2); | |
| 458 | try w.slice(&h.client_random); | |
| 459 | try w.byte(0); // no session id | |
| 460 | try w.enumList(CipherSuite, opt.cipher_suites); | |
| 461 | try w.slice(&[_]u8{ 0x01, 0x00 }); // no compression | |
| 462 | // Remeber position to write extensions length | |
| 463 | const ext_len_pos = try w.skip(2); | |
| 464 | try w.extension(.supported_versions, supported_versions); | |
| 465 | try w.extension(.signature_algorithms, common.supported_signature_algorithms); | |
| 466 | try w.extension(.supported_groups, opt.named_groups); | |
| 467 | try w.keyShare(opt.named_groups, shared_keys); | |
| 468 | try w.serverName(opt.host); | |
| 469 | // binder key placeholder | |
| 470 | const binder_pos: ?usize = if (resumption_ticket) |ticket| brk: { | |
| 471 | // Add pre shared key extension if this is session resumption. Must be last extension. | |
| 472 | try w.extension(.psk_key_exchange_modes, &[_]proto.KeyExchangeModes{.psk_dhe_ke}); | |
| 473 | try w.preSharedKey(ticket.identity, ticket.obfuscatedAge(), psk_binder_len); | |
| 474 | // Retruns placeholder for binder, we can write binder when we have | |
| 475 | // hash up to thise point with valid extnsions length and | |
| 476 | // record header written. | |
| 477 | break :brk try w.skip(psk_binder_len); | |
| 478 | } else null; | |
| 479 | ||
| 480 | // Write extensions length to the placeholder | |
| 481 | var ew = w.writerAt(ext_len_pos); | |
| 482 | try ew.int(u16, w.pos() - ext_len_pos - 2); | |
| 483 | // Writer header to the placeholder | |
| 484 | var hw = w.writerAt(header_pos); | |
| 485 | try hw.recordHeader(.handshake, w.pos() - 5); | |
| 486 | try hw.handshakeRecordHeader(.client_hello, w.pos() - 9); | |
| 487 | ||
| 488 | const pre_binder_hash_buf = w.buffered()[record.header_len .. w.pos() - psk_binder_len]; | |
| 489 | h.transcript.update(pre_binder_hash_buf); | |
| 490 | ||
| 491 | if (binder_pos) |pos| { | |
| 492 | // Write binder to the placeholder and update hash with binder extenstion | |
| 493 | const binder = h.transcript.pskBinder(); | |
| 494 | var bw = w.writerAt(pos); | |
| 495 | try bw.preSharedKeyBinder(binder); | |
| 496 | h.transcript.update(w.buffered()[w.buffered().len - psk_binder_len ..]); | |
| 497 | } | |
| 498 | ||
| 499 | h.output.advance(w.buffered().len); | |
| 500 | } | |
| 501 | ||
| 502 | /// Process first flight of the messages from the server. | |
| 503 | /// Read server hello message. If TLS 1.3 is chosen in server hello | |
| 504 | /// return. For TLS 1.2 continue and read certificate, key_exchange | |
| 505 | /// eventual certificate request and hello done messages. | |
| 506 | fn readServerFlight1(h: *Self) !void { | |
| 507 | var handshake_states: []const proto.Handshake = &.{.server_hello}; | |
| 508 | ||
| 509 | while (true) { | |
| 510 | var d = try Record.decoder(h.input); | |
| 511 | try d.expectContentType(.handshake); | |
| 512 | h.transcript.update(d.payload); | |
| 513 | h.max_server_record_len = @max(h.max_server_record_len, d.payload.len + record.header_len); | |
| 514 | ||
| 515 | // Multiple handshake messages can be packed in single tls record. | |
| 516 | while (!d.eof()) { | |
| 517 | const handshake_type = try d.decode(proto.Handshake); | |
| 518 | ||
| 519 | const length = try d.decode(u24); | |
| 520 | if (length > max_cleartext_len) | |
| 521 | return error.TlsUnsupportedFragmentedHandshakeMessage; | |
| 522 | ||
| 523 | brk: { | |
| 524 | for (handshake_states) |state| | |
| 525 | if (state == handshake_type) break :brk; | |
| 526 | return error.TlsUnexpectedMessage; | |
| 527 | } | |
| 528 | switch (handshake_type) { | |
| 529 | .server_hello => { // server hello, ref: https://datatracker.ietf.org/doc/html/rfc5246#section-7.4.1.3 | |
| 530 | try h.parseServerHello(&d, length); | |
| 531 | if (h.tls_version == .tls_1_3) { | |
| 532 | if (!d.eof()) return error.TlsIllegalParameter; | |
| 533 | return; // end of tls 1.3 server flight 1 | |
| 534 | } | |
| 535 | handshake_states = if (h.cert.skip_verify) | |
| 536 | &.{ .certificate, .server_key_exchange, .server_hello_done } | |
| 537 | else | |
| 538 | &.{.certificate}; | |
| 539 | }, | |
| 540 | .certificate => { | |
| 541 | try h.cert.parseCertificate(&d, h.tls_version); | |
| 542 | handshake_states = if (h.cipher_suite.keyExchange() == .rsa) | |
| 543 | &.{.server_hello_done} | |
| 544 | else | |
| 545 | &.{.server_key_exchange}; | |
| 546 | }, | |
| 547 | .server_key_exchange => { | |
| 548 | try h.parseServerKeyExchange(&d); | |
| 549 | handshake_states = &.{ .certificate_request, .server_hello_done }; | |
| 550 | }, | |
| 551 | .certificate_request => { | |
| 552 | h.client_certificate_requested = true; | |
| 553 | try d.skip(length); | |
| 554 | handshake_states = &.{.server_hello_done}; | |
| 555 | }, | |
| 556 | .server_hello_done => { | |
| 557 | if (length != 0) return error.TlsIllegalParameter; | |
| 558 | return; | |
| 559 | }, | |
| 560 | else => return error.TlsUnexpectedMessage, | |
| 561 | } | |
| 562 | } | |
| 563 | } | |
| 564 | } | |
| 565 | ||
| 566 | /// Parse server hello message. | |
| 567 | fn parseServerHello(h: *Self, d: *record.Decoder, length: u24) !void { | |
| 568 | if (try d.decode(proto.Version) != proto.Version.tls_1_2) | |
| 569 | return error.TlsBadVersion; | |
| 570 | h.server_random = try d.array(32); | |
| 571 | if (isServerHelloRetryRequest(&h.server_random)) | |
| 572 | return error.TlsServerHelloRetryRequest; | |
| 573 | ||
| 574 | const session_id_len = try d.decode(u8); | |
| 575 | if (session_id_len > 32) return error.TlsIllegalParameter; | |
| 576 | try d.skip(session_id_len); | |
| 577 | ||
| 578 | h.cipher_suite = try d.decode(CipherSuite); | |
| 579 | try h.cipher_suite.validate(); | |
| 580 | try d.skip(1); // skip compression method | |
| 581 | ||
| 582 | const extensions_present = length > 2 + 32 + 1 + session_id_len + 2 + 1; | |
| 583 | if (extensions_present) { | |
| 584 | const exs_len = try d.decode(u16); | |
| 585 | var l: usize = 0; | |
| 586 | while (l < exs_len) { | |
| 587 | const typ = try d.decode(proto.Extension); | |
| 588 | const len = try d.decode(u16); | |
| 589 | defer l += len + 4; | |
| 590 | ||
| 591 | switch (typ) { | |
| 592 | .supported_versions => { | |
| 593 | switch (try d.decode(proto.Version)) { | |
| 594 | .tls_1_2, .tls_1_3 => |v| h.tls_version = v, | |
| 595 | else => return error.TlsIllegalParameter, | |
| 596 | } | |
| 597 | if (len != 2) return error.TlsIllegalParameter; | |
| 598 | }, | |
| 599 | .key_share => { | |
| 600 | h.named_group = try d.decode(proto.NamedGroup); | |
| 601 | h.server_pub_key = try common.dupe(&h.server_pub_key_buf, try d.slice(try d.decode(u16))); | |
| 602 | if (len != h.server_pub_key.len + 4) return error.TlsIllegalParameter; | |
| 603 | }, | |
| 604 | .pre_shared_key => { | |
| 605 | h.pre_shared_selected_identity = try d.decode(u16); | |
| 606 | }, | |
| 607 | else => { | |
| 608 | try d.skip(len); | |
| 609 | }, | |
| 610 | } | |
| 611 | } | |
| 612 | } | |
| 613 | } | |
| 614 | ||
| 615 | fn isServerHelloRetryRequest(server_random: []const u8) bool { | |
| 616 | // Ref: https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.3 | |
| 617 | const hello_retry_request_magic = [32]u8{ | |
| 618 | 0xCF, 0x21, 0xAD, 0x74, 0xE5, 0x9A, 0x61, 0x11, 0xBE, 0x1D, 0x8C, 0x02, 0x1E, 0x65, 0xB8, 0x91, | |
| 619 | 0xC2, 0xA2, 0x11, 0x16, 0x7A, 0xBB, 0x8C, 0x5E, 0x07, 0x9E, 0x09, 0xE2, 0xC8, 0xA8, 0x33, 0x9C, | |
| 620 | }; | |
| 621 | return mem.eql(u8, server_random, &hello_retry_request_magic); | |
| 622 | } | |
| 623 | ||
| 624 | fn parseServerKeyExchange(h: *Self, d: *record.Decoder) !void { | |
| 625 | const curve_type = try d.decode(proto.Curve); | |
| 626 | h.named_group = try d.decode(proto.NamedGroup); | |
| 627 | h.server_pub_key = try common.dupe(&h.server_pub_key_buf, try d.slice(try d.decode(u8))); | |
| 628 | h.cert.signature_scheme = try d.decode(proto.SignatureScheme); | |
| 629 | h.cert.signature = try common.dupe(&h.cert.signature_buf, try d.slice(try d.decode(u16))); | |
| 630 | if (curve_type != .named_curve) return error.TlsIllegalParameter; | |
| 631 | } | |
| 632 | ||
| 633 | /// Read encrypted part (after server hello) of the server first flight | |
| 634 | /// for TLS 1.3: change cipher spec, eventual certificate request, | |
| 635 | /// certificate, certificate verify and handshake finished messages. | |
| 636 | fn readEncryptedServerFlight1(h: *Self, is_session_resumption: bool) !void { | |
| 637 | // buffer for decrypted handshake records | |
| 638 | var cleartext_buffer: [max_cleartext_len]u8 = undefined; | |
| 639 | // cleartext writer | |
| 640 | var cw = Io.Writer.fixed(&cleartext_buffer); | |
| 641 | // valid handshake types for the next record | |
| 642 | var handshake_states: []const proto.Handshake = &.{.encrypted_extensions}; | |
| 643 | ||
| 644 | outer: while (true) { | |
| 645 | // wrapped record decoder | |
| 646 | const rec = try Record.read(h.input); | |
| 647 | if (rec.protocol_version != .tls_1_2) return error.TlsBadVersion; | |
| 648 | h.max_server_record_len = @max(h.max_server_record_len, rec.buffer.len); | |
| 649 | switch (rec.content_type) { | |
| 650 | .change_cipher_spec => {}, | |
| 651 | .application_data => { | |
| 652 | // `decrypt` will return TlsCipherNoSpaceLeft | |
| 653 | // unusedCapacitySlice is too small to hold cleartext | |
| 654 | const content_type, const cleartext = try h.cipher.decrypt(cw.unusedCapacitySlice(), rec); | |
| 655 | cw.advance(cleartext.len); | |
| 656 | h.max_server_cleartext_len = @max(h.max_server_cleartext_len, cw.end); | |
| 657 | ||
| 658 | var d = record.Decoder.init(content_type, cw.buffered()); | |
| 659 | try d.expectContentType(.handshake); | |
| 660 | while (!d.eof()) { | |
| 661 | const handshake_type = try d.decode(proto.Handshake); | |
| 662 | const length = try d.decode(u24); | |
| 663 | ||
| 664 | if (length > max_cleartext_len) | |
| 665 | return error.TlsUnsupportedFragmentedHandshakeMessage; | |
| 666 | if (length > d.rest().len) | |
| 667 | continue :outer; // handshake fragmented into multiple records | |
| 668 | // jump to outer must be before this defer to presrve buffers | |
| 669 | defer { | |
| 670 | // 'd.idx' data is consumed | |
| 671 | h.transcript.update(d.payload[0..d.idx]); | |
| 672 | _ = cw.consume(d.idx); | |
| 673 | d = record.Decoder.init(content_type, cw.buffered()); | |
| 674 | } | |
| 675 | // check valid handshake types in this state | |
| 676 | brk: { | |
| 677 | for (handshake_states) |state| | |
| 678 | if (state == handshake_type) break :brk; | |
| 679 | return error.TlsUnexpectedMessage; | |
| 680 | } | |
| 681 | switch (handshake_type) { | |
| 682 | .encrypted_extensions => { | |
| 683 | try d.skip(length); | |
| 684 | handshake_states = if (is_session_resumption) | |
| 685 | &.{ .certificate_request, .certificate, .finished } | |
| 686 | else if (h.cert.skip_verify) | |
| 687 | &.{ .certificate_request, .certificate, .finished } | |
| 688 | else | |
| 689 | &.{ .certificate_request, .certificate }; | |
| 690 | }, | |
| 691 | .certificate_request => { | |
| 692 | h.client_certificate_requested = true; | |
| 693 | try d.skip(length); | |
| 694 | handshake_states = if (h.cert.skip_verify) | |
| 695 | &.{ .certificate, .finished } | |
| 696 | else | |
| 697 | &.{.certificate}; | |
| 698 | }, | |
| 699 | .certificate => { | |
| 700 | try h.cert.parseCertificate(&d, h.tls_version); | |
| 701 | handshake_states = &.{.certificate_verify}; | |
| 702 | }, | |
| 703 | .certificate_verify => { | |
| 704 | try h.cert.parseCertificateVerify(&d); | |
| 705 | try h.cert.verifySignature(h.transcript.serverCertificateVerify()); | |
| 706 | handshake_states = &.{.finished}; | |
| 707 | }, | |
| 708 | .finished => { | |
| 709 | const actual = try d.slice(length); | |
| 710 | const expected = h.transcript.serverFinishedTls13(); | |
| 711 | if (!mem.eql(u8, expected, actual)) | |
| 712 | return error.TlsDecryptError; | |
| 713 | return; | |
| 714 | }, | |
| 715 | else => return error.TlsUnexpectedMessage, | |
| 716 | } | |
| 717 | } | |
| 718 | }, | |
| 719 | else => return error.TlsUnexpectedMessage, | |
| 720 | } | |
| 721 | } | |
| 722 | } | |
| 723 | ||
| 724 | fn verifyCertificateSignatureTls12(h: *Self) !void { | |
| 725 | if (h.cipher_suite.keyExchange() != .ecdhe) return; | |
| 726 | const verify_bytes = brk: { | |
| 727 | var buffer: [h.server_pub_key_buf.len + 128]u8 = undefined; | |
| 728 | var w = Io.Writer.fixed(&buffer); | |
| 729 | try w.writeAll(&h.client_random); | |
| 730 | try w.writeAll(&h.server_random); | |
| 731 | try w.writeByte(@backingInt(proto.Curve.named_curve)); | |
| 732 | try w.writeInt(u16, @backingInt(h.named_group.?), .big); | |
| 733 | try w.writeByte(@intCast(h.server_pub_key.len)); | |
| 734 | try w.writeAll(h.server_pub_key); | |
| 735 | break :brk w.buffered(); | |
| 736 | }; | |
| 737 | try h.cert.verifySignature(verify_bytes); | |
| 738 | } | |
| 739 | ||
| 740 | /// Create client key exchange, change cipher spec and handshake | |
| 741 | /// finished messages for tls 1.2. | |
| 742 | /// If client certificate is requested also adds client certificate and | |
| 743 | /// certificate verify messages. | |
| 744 | fn makeClientFlight2Tls12(h: *Self, auth: ?*CertKeyPair) !void { | |
| 745 | var w: record.Writer = .initFromIo(h.output); | |
| 746 | var cert_builder: ?CertificateBuilder = null; | |
| 747 | ||
| 748 | // Client certificate | |
| 749 | if (h.client_certificate_requested) { | |
| 750 | if (auth) |a| { | |
| 751 | const cb = h.certificateBuilder(a); | |
| 752 | cert_builder = cb; | |
| 753 | var hw = try w.writerAdvance(record.header_len); | |
| 754 | try cb.makeCertificate(&hw); | |
| 755 | h.transcript.update(hw.buffered()); | |
| 756 | try w.record(.handshake, hw.buffered()); | |
| 757 | } else { | |
| 758 | var hw = try w.writerAdvance(record.header_len); | |
| 759 | try hw.handshakeRecord(.certificate, &[_]u8{ 0, 0, 0 }); | |
| 760 | h.transcript.update(hw.buffered()); | |
| 761 | try w.record(.handshake, hw.buffered()); | |
| 762 | } | |
| 763 | } | |
| 764 | ||
| 765 | { // Client key exchange | |
| 766 | var hw = try w.writerAdvance(record.header_len); | |
| 767 | if (h.named_group) |named_group| { | |
| 768 | const key = try h.dh_kp.publicKey(named_group); | |
| 769 | try hw.handshakeRecordHeader(.client_key_exchange, 1 + key.len); | |
| 770 | try hw.int(u8, key.len); | |
| 771 | try hw.slice(key); | |
| 772 | } else { | |
| 773 | var key_buf: [512]u8 = undefined; | |
| 774 | const key = try h.rsa_secret.encrypted(&key_buf, h.cert.pub_key_algo, h.cert.pub_key); | |
| 775 | try hw.handshakeRecordHeader(.client_key_exchange, 2 + key.len); | |
| 776 | try hw.int(u16, key.len); | |
| 777 | try hw.slice(key); | |
| 778 | } | |
| 779 | h.transcript.update(hw.buffered()); | |
| 780 | try w.record(.handshake, hw.buffered()); | |
| 781 | } | |
| 782 | ||
| 783 | // Client certificate verify | |
| 784 | if (cert_builder) |cb| { | |
| 785 | var hw = try w.writerAdvance(record.header_len); | |
| 786 | try cb.makeCertificateVerify(&hw); | |
| 787 | h.transcript.update(hw.buffered()); | |
| 788 | try w.record(.handshake, hw.buffered()); | |
| 789 | } | |
| 790 | ||
| 791 | // Client change cipher spec | |
| 792 | try w.record(.change_cipher_spec, &[_]u8{1}); | |
| 793 | ||
| 794 | { // Client handshake finished | |
| 795 | const client_finished = &record.handshakeHeader(.finished, 12) ++ | |
| 796 | h.transcript.clientFinishedTls12(&h.master_secret); | |
| 797 | h.transcript.update(client_finished); | |
| 798 | try h.writeEncrypted(&w, client_finished); | |
| 799 | } | |
| 800 | ||
| 801 | h.output.advance(w.buffered().len); | |
| 802 | } | |
| 803 | ||
| 804 | /// Create client change cipher spec and handshake finished messages for | |
| 805 | /// tls 1.3. | |
| 806 | /// If the client certificate is requested by the server and client is | |
| 807 | /// configured with certificates and private key then client certificate | |
| 808 | /// and client certificate verify messages are also created. If the | |
| 809 | /// server has requested certificate but the client is not configured | |
| 810 | /// empty certificate message is sent, as is required by rfc. | |
| 811 | fn makeClientFlight2Tls13(h: *Self, auth: ?*CertKeyPair) !void { | |
| 812 | var w: record.Writer = .initFromIo(h.output); | |
| 813 | ||
| 814 | // Client change cipher spec | |
| 815 | try w.record(.change_cipher_spec, &[_]u8{1}); | |
| 816 | ||
| 817 | if (h.client_certificate_requested) { | |
| 818 | if (auth) |a| { | |
| 819 | const cb = h.certificateBuilder(a); | |
| 820 | { // Certificate | |
| 821 | var hw = try w.writerAdvance(record.header_len); | |
| 822 | try cb.makeCertificate(&hw); | |
| 823 | h.transcript.update(hw.buffered()); | |
| 824 | try h.writeEncrypted(&w, hw.buffered()); | |
| 825 | } | |
| 826 | { // Client certificate | |
| 827 | var hw = try w.writerAdvance(record.header_len); | |
| 828 | try cb.makeCertificateVerify(&hw); | |
| 829 | h.transcript.update(hw.buffered()); | |
| 830 | try h.writeEncrypted(&w, hw.buffered()); | |
| 831 | } | |
| 832 | } else { | |
| 833 | // Empty certificate message and no certificate verify message | |
| 834 | var hw = try w.writerAdvance(record.header_len); | |
| 835 | try hw.handshakeRecord(.certificate, &[_]u8{ 0, 0, 0, 0 }); | |
| 836 | h.transcript.update(hw.buffered()); | |
| 837 | try h.writeEncrypted(&w, hw.buffered()); | |
| 838 | } | |
| 839 | } | |
| 840 | ||
| 841 | { // Finished | |
| 842 | var hw = try w.writerAdvance(record.header_len); | |
| 843 | try hw.handshakeRecord(.finished, h.transcript.clientFinishedTls13()); | |
| 844 | h.transcript.update(hw.buffered()); | |
| 845 | try h.writeEncrypted(&w, hw.buffered()); | |
| 846 | } | |
| 847 | ||
| 848 | h.output.advance(w.buffered().len); | |
| 849 | } | |
| 850 | ||
| 851 | fn certificateBuilder(h: *Self, auth: *CertKeyPair) CertificateBuilder { | |
| 852 | return .{ | |
| 853 | .cert_key_pair = auth, | |
| 854 | .transcript = &h.transcript, | |
| 855 | .tls_version = h.tls_version, | |
| 856 | .side = .client, | |
| 857 | }; | |
| 858 | } | |
| 859 | ||
| 860 | fn readServerFlight2(h: *Self) !void { | |
| 861 | // Read server change cipher spec message. | |
| 862 | { | |
| 863 | var d = try Record.decoder(h.input); | |
| 864 | try d.expectContentType(.change_cipher_spec); | |
| 865 | h.max_server_record_len = @max(h.max_server_record_len, d.payload.len + record.header_len); | |
| 866 | } | |
| 867 | // Read encrypted server handshake finished message. Verify that | |
| 868 | // content of the server finished message is based on transcript | |
| 869 | // hash and master secret. | |
| 870 | { | |
| 871 | const rec = try Record.read(h.input); | |
| 872 | h.max_server_record_len = @max(h.max_server_record_len, rec.buffer.len); | |
| 873 | const content_type, const server_finished = try h.cipher.decrypt(@constCast(rec.buffer), rec); | |
| 874 | if (content_type != .handshake) | |
| 875 | return error.TlsUnexpectedMessage; | |
| 876 | const expected = record.handshakeHeader(.finished, 12) ++ h.transcript.serverFinishedTls12(&h.master_secret); | |
| 877 | if (!mem.eql(u8, server_finished, &expected)) | |
| 878 | return error.TlsBadRecordMac; | |
| 879 | } | |
| 880 | } | |
| 881 | ||
| 882 | /// Write encrypted handshake message into `w` | |
| 883 | fn writeEncrypted(h: *Self, w: *record.Writer, cleartext: []const u8) !void { | |
| 884 | const ciphertext = try h.cipher.encrypt(w.unused(), .handshake, cleartext); | |
| 885 | w.advance(ciphertext.len); | |
| 886 | } | |
| 887 | ||
| 888 | // Copy handshake parameters to opt.diagnostic | |
| 889 | fn updateDiagnostic(h: *Self, opt: Options, is_session_resumption: bool) void { | |
| 890 | if (opt.diagnostic) |d| { | |
| 891 | d.tls_version = h.tls_version; | |
| 892 | d.cipher_suite_tag = h.cipher_suite; | |
| 893 | d.named_group = h.named_group orelse @as(proto.NamedGroup, @fromBackingInt(@intCast(0x0000))); | |
| 894 | d.signature_scheme = h.cert.signature_scheme; | |
| 895 | d.is_session_resumption = is_session_resumption; | |
| 896 | d.max_server_record_len = h.max_server_record_len; | |
| 897 | d.max_server_cleartext_len = h.max_server_cleartext_len; | |
| 898 | d.max_client_record_len = h.max_client_record_len; | |
| 899 | if (opt.auth) |a| | |
| 900 | d.client_signature_scheme = a.key.signature_scheme; | |
| 901 | } | |
| 902 | } | |
| 903 | }; | |
| 904 | ||
| 905 | const RsaSecret = struct { | |
| 906 | secret: [48]u8, | |
| 907 | ||
| 908 | fn init(rand: [46]u8) RsaSecret { | |
| 909 | return .{ .secret = [_]u8{ 0x03, 0x03 } ++ rand }; | |
| 910 | } | |
| 911 | ||
| 912 | // Pre master secret encrypted with certificate public key. | |
| 913 | fn encrypted( | |
| 914 | self: RsaSecret, | |
| 915 | out: *[512]u8, | |
| 916 | cert_pub_key_algo: Certificate.Parsed.PubKeyAlgo, | |
| 917 | cert_pub_key: []const u8, | |
| 918 | ) ![]const u8 { | |
| 919 | if (cert_pub_key_algo != .rsaEncryption) return error.TlsBadSignatureScheme; | |
| 920 | const pk = try rsa.PublicKey.fromDer(cert_pub_key); | |
| 921 | return try pk.encryptPkcsv1_5(&self.secret, out); | |
| 922 | } | |
| 923 | }; | |
| 924 | ||
| 925 | const testing = std.testing; | |
| 926 | const data12 = ; | |
| 927 | const data13 = ; | |
| 928 | const testu = ; | |
| 929 | ||
| 930 | test "parse tls 1.2 server hello" { | |
| 931 | var buffer: [1024]u8 = undefined; | |
| 932 | var writer: Io.Writer = .fixed(&buffer); | |
| 933 | var reader: Io.Reader = .fixed(&data12.server_hello_responses); | |
| 934 | var h: Handshake = .{ | |
| 935 | .output = &writer, | |
| 936 | .input = &reader, | |
| 937 | .client_random = data12.client_random, | |
| 938 | }; | |
| 939 | // Set to known instead of random | |
| 940 | h.dh_kp.x25519_kp.secret_key = data12.client_secret; | |
| 941 | ||
| 942 | // Parse server hello, certificate and key exchange messages. | |
| 943 | // Read cipher suite, named group, signature scheme, server random certificate public key | |
| 944 | // Verify host name, signature | |
| 945 | // Calculate key material | |
| 946 | h.cert = .{ .host = "example.ulfheim.net", .skip_verify = true, .root_ca = .empty }; | |
| 947 | try h.readServerFlight1(); | |
| 948 | try testing.expectEqual(.ECDHE_RSA_WITH_AES_128_CBC_SHA, h.cipher_suite); | |
| 949 | try testing.expectEqual(.x25519, h.named_group.?); | |
| 950 | try testing.expectEqual(.rsa_pkcs1_sha256, h.cert.signature_scheme); | |
| 951 | try testing.expectEqualSlices(u8, &data12.server_random, &h.server_random); | |
| 952 | try testing.expectEqualSlices(u8, &data12.server_pub_key, h.server_pub_key); | |
| 953 | try testing.expectEqualSlices(u8, &data12.signature, h.cert.signature); | |
| 954 | try testing.expectEqualSlices(u8, &data12.cert_pub_key, h.cert.pub_key); | |
| 955 | ||
| 956 | try h.verifyCertificateSignatureTls12(); | |
| 957 | try h.generateKeyMaterial(null); | |
| 958 | ||
| 959 | try testing.expectEqualSlices(u8, &data12.key_material, h.key_material[0..data12.key_material.len]); | |
| 960 | } | |
| 961 | ||
| 962 | test "verify google.com certificate" { | |
| 963 | var buffer: [1024]u8 = undefined; | |
| 964 | var writer: Io.Writer = .fixed(&buffer); | |
| 965 | var reader: Io.Reader = .fixed(@embedFile("testdata/google.com/server_hello")); | |
| 966 | var h: Handshake = .{ | |
| 967 | .output = &writer, | |
| 968 | .input = &reader, | |
| 969 | .client_random = @embedFile("testdata/google.com/client_random").*, | |
| 970 | }; | |
| 971 | ||
| 972 | var ca_bundle: Certificate.Bundle = .empty; | |
| 973 | try ca_bundle.rescan(testing.allocator); | |
| 974 | defer ca_bundle.deinit(testing.allocator); | |
| 975 | ||
| 976 | h.cert = .{ .host = "google.com", .skip_verify = true, .root_ca = .empty, .now_sec = 1714846451 }; | |
| 977 | try h.readServerFlight1(); | |
| 978 | try h.verifyCertificateSignatureTls12(); | |
| 979 | } | |
| 980 | ||
| 981 | test "parse tls 1.3 server hello" { | |
| 982 | var reader: Io.Reader = .fixed(&data13.server_hello); | |
| 983 | var d = try Record.decoder(&reader); | |
| 984 | ||
| 985 | const handshake_type = try d.decode(proto.Handshake); | |
| 986 | const length = try d.decode(u24); | |
| 987 | try testing.expectEqual(0x000076, length); | |
| 988 | try testing.expectEqual(.server_hello, handshake_type); | |
| 989 | ||
| 990 | var h: Handshake = undefined; | |
| 991 | try h.parseServerHello(&d, length); | |
| 992 | ||
| 993 | try testing.expectEqual(.AES_256_GCM_SHA384, h.cipher_suite); | |
| 994 | try testing.expectEqualSlices(u8, &data13.server_random, &h.server_random); | |
| 995 | try testing.expectEqual(.tls_1_3, h.tls_version); | |
| 996 | try testing.expectEqual(.x25519, h.named_group); | |
| 997 | try testing.expectEqualSlices(u8, &data13.server_pub_key, h.server_pub_key); | |
| 998 | } | |
| 999 | ||
| 1000 | test "init tls 1.3 handshake cipher" { | |
| 1001 | const cipher_suite_tag: CipherSuite = .AES_256_GCM_SHA384; | |
| 1002 | ||
| 1003 | var transcript = Transcript{}; | |
| 1004 | transcript.use(cipher_suite_tag.hash()); | |
| 1005 | transcript.update(data13.client_hello[record.header_len..]); | |
| 1006 | transcript.update(data13.server_hello[record.header_len..]); | |
| 1007 | ||
| 1008 | var dh_kp = DhKeyPair{ | |
| 1009 | .x25519_kp = .{ | |
| 1010 | .public_key = data13.client_public_key, | |
| 1011 | .secret_key = data13.client_private_key, | |
| 1012 | }, | |
| 1013 | }; | |
| 1014 | const shared_key = try dh_kp.sharedKey(.x25519, &data13.server_pub_key); | |
| 1015 | try testing.expectEqualSlices(u8, &data13.shared_key, shared_key); | |
| 1016 | ||
| 1017 | const cipher = try Cipher.initTls13(cipher_suite_tag, transcript.handshakeSecret(shared_key), .client); | |
| 1018 | ||
| 1019 | const c = &cipher.AES_256_GCM_SHA384; | |
| 1020 | try testing.expectEqualSlices(u8, &data13.server_handshake_key, &c.decrypt_key); | |
| 1021 | try testing.expectEqualSlices(u8, &data13.client_handshake_key, &c.encrypt_key); | |
| 1022 | try testing.expectEqualSlices(u8, &data13.server_handshake_iv, &c.decrypt_iv); | |
| 1023 | try testing.expectEqualSlices(u8, &data13.client_handshake_iv, &c.encrypt_iv); | |
| 1024 | } | |
| 1025 | ||
| 1026 | fn initExampleHandshake(h: *Handshake) !void { | |
| 1027 | h.cipher_suite = .AES_256_GCM_SHA384; | |
| 1028 | h.transcript.use(h.cipher_suite.hash()); | |
| 1029 | h.transcript.update(data13.client_hello[record.header_len..]); | |
| 1030 | h.transcript.update(data13.server_hello[record.header_len..]); | |
| 1031 | h.cipher = try Cipher.initTls13(h.cipher_suite, h.transcript.handshakeSecret(&data13.shared_key), .client); | |
| 1032 | h.tls_version = .tls_1_3; | |
| 1033 | h.cert.now_sec = 1714846451; | |
| 1034 | h.server_pub_key = &data13.server_pub_key; | |
| 1035 | } | |
| 1036 | ||
| 1037 | test "tls 1.3 decrypt wrapped record" { | |
| 1038 | var cipher = brk: { | |
| 1039 | var h: Handshake = .{ .input = undefined, .output = undefined }; | |
| 1040 | try initExampleHandshake(&h); | |
| 1041 | break :brk h.cipher; | |
| 1042 | }; | |
| 1043 | ||
| 1044 | var cleartext_buf: [1024]u8 = undefined; | |
| 1045 | { | |
| 1046 | const rec = record.Record.init(&data13.server_encrypted_extensions_wrapped); | |
| 1047 | ||
| 1048 | const content_type, const cleartext = try cipher.decrypt(&cleartext_buf, rec); | |
| 1049 | try testing.expectEqual(.handshake, content_type); | |
| 1050 | try testing.expectEqualSlices(u8, &data13.server_encrypted_extensions, cleartext); | |
| 1051 | } | |
| 1052 | { | |
| 1053 | const rec = record.Record.init(&data13.server_certificate_wrapped); | |
| 1054 | ||
| 1055 | const content_type, const cleartext = try cipher.decrypt(&cleartext_buf, rec); | |
| 1056 | try testing.expectEqual(.handshake, content_type); | |
| 1057 | try testing.expectEqualSlices(u8, &data13.server_certificate, cleartext); | |
| 1058 | } | |
| 1059 | } | |
| 1060 | ||
| 1061 | test "tls 1.3 process server flight" { | |
| 1062 | var h: Handshake = brk: { | |
| 1063 | var buffer: [1024]u8 = undefined; | |
| 1064 | var writer: Io.Writer = .fixed(&buffer); | |
| 1065 | var reader: Io.Reader = .fixed(&data13.server_flight); | |
| 1066 | break :brk .{ .input = &reader, .output = &writer }; | |
| 1067 | }; | |
| 1068 | try initExampleHandshake(&h); | |
| 1069 | ||
| 1070 | h.cert = .{ .host = "example.ulfheim.net", .skip_verify = true, .root_ca = .empty }; | |
| 1071 | try h.readEncryptedServerFlight1(false); | |
| 1072 | ||
| 1073 | { // application cipher keys calculation | |
| 1074 | try testing.expectEqualSlices(u8, &data13.handshake_hash, &h.transcript.sha384.hash.peek()); | |
| 1075 | ||
| 1076 | var cipher = try Cipher.initTls13(h.cipher_suite, h.transcript.applicationSecret(), .client); | |
| 1077 | const c = &cipher.AES_256_GCM_SHA384; | |
| 1078 | try testing.expectEqualSlices(u8, &data13.server_application_key, &c.decrypt_key); | |
| 1079 | try testing.expectEqualSlices(u8, &data13.client_application_key, &c.encrypt_key); | |
| 1080 | try testing.expectEqualSlices(u8, &data13.server_application_iv, &c.decrypt_iv); | |
| 1081 | try testing.expectEqualSlices(u8, &data13.client_application_iv, &c.encrypt_iv); | |
| 1082 | ||
| 1083 | var buffer: [128]u8 = undefined; | |
| 1084 | const encrypted = try cipher.encrypt(&buffer, .application_data, "ping"); | |
| 1085 | try testing.expectEqualSlices(u8, &data13.client_ping_wrapped, encrypted); | |
| 1086 | } | |
| 1087 | { // client finished message | |
| 1088 | const verify_data = h.transcript.clientFinishedTls13(); | |
| 1089 | try testing.expectEqualSlices(u8, &data13.client_finished_verify_data, verify_data); | |
| 1090 | ||
| 1091 | var buffer: [128]u8 = undefined; | |
| 1092 | var w: Io.Writer = .fixed(&buffer); | |
| 1093 | h.output = &w; | |
| 1094 | try h.makeClientFlight2Tls13(null); | |
| 1095 | try testing.expectEqualSlices(u8, &testu.hexToBytes("140303000101"), w.buffered()[0..6]); | |
| 1096 | try testing.expectEqualSlices(u8, &data13.client_finished_wrapped, w.buffered()[6..]); | |
| 1097 | } | |
| 1098 | } | |
| 1099 | ||
| 1100 | test "create client hello" { | |
| 1101 | const expected = testu.hexToBytes( | |
| 1102 | "16 03 03 00 6d " ++ // record header | |
| 1103 | "01 00 00 69 " ++ // handshake header | |
| 1104 | "03 03 " ++ // protocol version | |
| 1105 | "00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f " ++ // client random | |
| 1106 | "00 " ++ // no session id | |
| 1107 | "00 02 c0 2b " ++ // cipher suites | |
| 1108 | "01 00 " ++ // compression methods | |
| 1109 | "00 3e " ++ // extensions length | |
| 1110 | "00 2b 00 03 02 03 03 " ++ // supported versions extension | |
| 1111 | "00 0d 00 14 00 12 04 03 05 03 08 04 08 05 08 06 08 07 02 01 04 01 05 01 " ++ // signature algorithms extension | |
| 1112 | "00 0a 00 08 00 06 00 1d 00 17 00 18 " ++ // named groups extension | |
| 1113 | "00 00 00 0f 00 0d 00 00 0a 67 6f 6f 67 6c 65 2e 63 6f 6d ", // server name extension | |
| 1114 | ); | |
| 1115 | ||
| 1116 | var h = brk: { | |
| 1117 | var buffer: [256 + 10]u8 = undefined; // reserved + host name len | |
| 1118 | var stream_writer: Io.Writer = .fixed(&buffer); | |
| 1119 | break :brk Handshake{ | |
| 1120 | .output = &stream_writer, | |
| 1121 | .input = undefined, | |
| 1122 | .client_random = testu.hexToBytes( | |
| 1123 | \\ 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f | |
| 1124 | ), | |
| 1125 | }; | |
| 1126 | }; | |
| 1127 | ||
| 1128 | try h.makeClientHello(.{ | |
| 1129 | .host = "google.com", | |
| 1130 | .root_ca = .empty, | |
| 1131 | .cipher_suites = &[_]CipherSuite{CipherSuite.ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}, | |
| 1132 | .named_groups = &[_]proto.NamedGroup{ .x25519, .secp256r1, .secp384r1 }, | |
| 1133 | }, null); | |
| 1134 | ||
| 1135 | const actual = h.output.buffered(); | |
| 1136 | try testing.expectEqualSlices(u8, &expected, actual); | |
| 1137 | try testing.expectEqual(152, h.output.unusedCapacityLen()); | |
| 1138 | try testing.expectEqual(114, expected.len); | |
| 1139 | } | |
| 1140 | ||
| 1141 | test "client hello size" { | |
| 1142 | const opt: Options = .{ | |
| 1143 | .host = "some-host-name.net", | |
| 1144 | .root_ca = .empty, | |
| 1145 | .cipher_suites = cipher_suites.all, | |
| 1146 | //.named_groups = supported_named_groups, | |
| 1147 | }; | |
| 1148 | ||
| 1149 | var buffer: [max_cleartext_len]u8 = undefined; | |
| 1150 | var stream_writer: Io.Writer = .fixed(&buffer); | |
| 1151 | var h = Handshake{ | |
| 1152 | .output = &stream_writer, | |
| 1153 | .input = undefined, | |
| 1154 | }; | |
| 1155 | try h.initKeys(opt); | |
| 1156 | try h.makeClientHello(opt, null); | |
| 1157 | try testing.expectEqual(1572 + opt.host.len, h.output.end); | |
| 1158 | //try testing.expectEqual(2794 + opt.host.len, h.stream_writer.end); | |
| 1159 | } | |
| 1160 | ||
| 1161 | test "handshake verify server finished message" { | |
| 1162 | var h: Handshake = brk: { | |
| 1163 | var buffer: [1024]u8 = undefined; | |
| 1164 | var writer: Io.Writer = .fixed(&buffer); | |
| 1165 | var reader: Io.Reader = .fixed(&data12.server_handshake_finished_msgs); | |
| 1166 | break :brk .{ .input = &reader, .output = &writer }; | |
| 1167 | }; | |
| 1168 | ||
| 1169 | h.cipher_suite = .ECDHE_ECDSA_WITH_AES_128_CBC_SHA; | |
| 1170 | h.master_secret = data12.master_secret; | |
| 1171 | ||
| 1172 | // add handshake messages to the transcript | |
| 1173 | for (data12.handshake_messages) |msg| { | |
| 1174 | h.transcript.update(msg[record.header_len..]); | |
| 1175 | } | |
| 1176 | ||
| 1177 | // expect verify data | |
| 1178 | const client_finished = h.transcript.clientFinishedTls12(&h.master_secret); | |
| 1179 | try testing.expectEqualSlices(u8, &data12.client_finished, &record.handshakeHeader(.finished, 12) ++ client_finished); | |
| 1180 | ||
| 1181 | // init client with prepared key_material | |
| 1182 | h.cipher = try Cipher.initTls12(.ECDHE_RSA_WITH_AES_128_CBC_SHA, &data12.key_material, .client); | |
| 1183 | ||
| 1184 | // check that server verify data matches calculates from hashes of all handshake messages | |
| 1185 | h.transcript.update(&data12.client_finished); | |
| 1186 | try h.readServerFlight2(); | |
| 1187 | } | |
| 1188 | ||
| 1189 | pub const NonBlock = struct { | |
| 1190 | const Self = @This(); | |
| 1191 | ||
| 1192 | // inner sync handshake | |
| 1193 | inner: Handshake = undefined, | |
| 1194 | opt: Options = undefined, | |
| 1195 | state: State = .none, | |
| 1196 | ||
| 1197 | const State = enum { | |
| 1198 | none, | |
| 1199 | init, | |
| 1200 | client_flight_1, | |
| 1201 | server_flight_1, | |
| 1202 | client_flight_2, | |
| 1203 | server_flight_2, | |
| 1204 | ||
| 1205 | fn next(self: *State) void { | |
| 1206 | self.* = @fromBackingInt(@intCast(@backingInt(self.*) + 1)); | |
| 1207 | } | |
| 1208 | }; | |
| 1209 | ||
| 1210 | pub fn init(opt: Options) Self { | |
| 1211 | var inner = Handshake{ | |
| 1212 | .input = undefined, | |
| 1213 | .output = undefined, | |
| 1214 | }; | |
| 1215 | inner.initKeys(opt) catch unreachable; | |
| 1216 | return .{ | |
| 1217 | .inner = inner, | |
| 1218 | .opt = opt, | |
| 1219 | .state = .init, | |
| 1220 | }; | |
| 1221 | } | |
| 1222 | ||
| 1223 | fn send(self: *Self) !void { | |
| 1224 | switch (self.state) { | |
| 1225 | .init => { | |
| 1226 | try self.inner.clientFlight1(self.opt); | |
| 1227 | self.state.next(); | |
| 1228 | }, | |
| 1229 | .server_flight_1 => { | |
| 1230 | try self.inner.clientFlight2(self.opt); | |
| 1231 | self.state.next(); | |
| 1232 | if (self.done()) self.inner.updateDiagnostic(self.opt, false); | |
| 1233 | }, | |
| 1234 | else => return, | |
| 1235 | } | |
| 1236 | } | |
| 1237 | ||
| 1238 | fn recv(self: *Self) !void { | |
| 1239 | const prev: Transcript = self.inner.transcript; | |
| 1240 | errdefer self.inner.transcript = prev; | |
| 1241 | ||
| 1242 | switch (self.state) { | |
| 1243 | .client_flight_1 => { | |
| 1244 | try self.inner.serverFlight1(self.opt); | |
| 1245 | self.state.next(); | |
| 1246 | }, | |
| 1247 | .client_flight_2 => { | |
| 1248 | try self.inner.serverFlight2(self.opt); | |
| 1249 | self.state.next(); | |
| 1250 | if (self.done()) self.inner.updateDiagnostic(self.opt, false); | |
| 1251 | }, | |
| 1252 | else => return, | |
| 1253 | } | |
| 1254 | } | |
| 1255 | ||
| 1256 | /// True when handshake is successfully finished | |
| 1257 | pub fn done(self: Self) bool { | |
| 1258 | return self.state == .server_flight_2 or | |
| 1259 | (self.inner.tls_version == .tls_1_3 and self.state == .client_flight_2); | |
| 1260 | } | |
| 1261 | ||
| 1262 | /// Runs next handshake step. | |
| 1263 | pub fn run( | |
| 1264 | self: *Self, | |
| 1265 | /// Data received from the peer | |
| 1266 | recv_buf: []const u8, | |
| 1267 | /// Scratch buffer where data to be sent to the peer will be prepared | |
| 1268 | send_buf: []u8, | |
| 1269 | ) !struct { | |
| 1270 | /// Number of bytes consumed from recv_buf | |
| 1271 | recv_pos: usize, | |
| 1272 | /// Number of bytes prepared in send_buf | |
| 1273 | send_pos: usize, | |
| 1274 | /// Unused part of the recv_buf, | |
| 1275 | unused_recv: []const u8, | |
| 1276 | /// Part of the send_buf that should be sent to the peer | |
| 1277 | send: []const u8, | |
| 1278 | } { | |
| 1279 | if (self.done()) return .{ | |
| 1280 | .recv_pos = 0, | |
| 1281 | .send_pos = 0, | |
| 1282 | .unused_recv = recv_buf, | |
| 1283 | .send = &.{}, | |
| 1284 | }; | |
| 1285 | ||
| 1286 | var input: Io.Reader = .fixed(recv_buf); | |
| 1287 | var output: Io.Writer = .fixed(send_buf); | |
| 1288 | self.inner.input = &input; | |
| 1289 | self.inner.output = &output; | |
| 1290 | ||
| 1291 | const recv_pos: usize = if (recv_buf.len > 0) brk: { | |
| 1292 | self.recv() catch |err| switch (err) { | |
| 1293 | error.EndOfStream, error.InputBufferUndersize => break :brk 0, | |
| 1294 | else => return err, | |
| 1295 | }; | |
| 1296 | break :brk input.seek; | |
| 1297 | } else 0; | |
| 1298 | ||
| 1299 | try self.send(); | |
| 1300 | ||
| 1301 | return .{ | |
| 1302 | .recv_pos = recv_pos, | |
| 1303 | .send_pos = output.end, | |
| 1304 | .unused_recv = recv_buf[recv_pos..], | |
| 1305 | .send = output.buffered(), | |
| 1306 | }; | |
| 1307 | } | |
| 1308 | ||
| 1309 | /// Cipher produced in handshake, null until successful handshake. | |
| 1310 | pub fn cipher(self: Self) ?Cipher { | |
| 1311 | return if (self.done()) self.inner.cipher else null; | |
| 1312 | } | |
| 1313 | }; | |
| 1314 | ||
| 1315 | test "nonblock handshake" { | |
| 1316 | var buffer: [1024]u8 = undefined; | |
| 1317 | var h = NonBlock.init(.{ | |
| 1318 | .host = "example.ulfheim.net", | |
| 1319 | .insecure_skip_verify = true, | |
| 1320 | .root_ca = .empty, | |
| 1321 | .cipher_suites = &[_]CipherSuite{CipherSuite.AES_256_GCM_SHA384}, | |
| 1322 | .named_groups = &[_]proto.NamedGroup{.x25519}, | |
| 1323 | }); | |
| 1324 | { // update secrets to well known from example | |
| 1325 | h.inner.client_random = data13.client_random; | |
| 1326 | h.inner.dh_kp.x25519_kp = .{ | |
| 1327 | .public_key = data13.client_public_key, | |
| 1328 | .secret_key = data13.client_private_key, | |
| 1329 | }; | |
| 1330 | } | |
| 1331 | ||
| 1332 | const expected_client_flight_1 = testu.hexToBytes( | |
| 1333 | \\ 16 03 03 00 9c | |
| 1334 | \\ 01 00 00 98 | |
| 1335 | \\ 03 03 | |
| 1336 | \\ 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f | |
| 1337 | \\ 00 | |
| 1338 | \\ 00 02 13 02 | |
| 1339 | \\ 01 00 | |
| 1340 | \\ 00 6d | |
| 1341 | \\ 00 2b 00 03 02 03 04 | |
| 1342 | \\ 00 0d 00 14 00 12 04 03 05 03 08 04 08 05 08 06 08 07 02 01 04 01 05 01 | |
| 1343 | \\ 00 0a 00 04 00 02 00 1d 00 33 00 26 00 24 00 1d 00 20 | |
| 1344 | \\ 35 80 72 d6 36 58 80 d1 ae ea 32 9a df 91 21 38 38 51 ed 21 a2 8e 3b 75 e9 65 d0 d2 cd 16 62 54 | |
| 1345 | \\ 00 00 00 18 00 16 00 00 13 65 78 61 6d 70 6c 65 2e 75 6c 66 68 65 69 6d 2e 6e 65 74 | |
| 1346 | ); | |
| 1347 | ||
| 1348 | var res = try h.run(&.{}, &buffer); | |
| 1349 | try testing.expectEqualSlices(u8, &expected_client_flight_1, res.send); | |
| 1350 | ||
| 1351 | { // update transcript to well known from example | |
| 1352 | h.inner.transcript = .{}; | |
| 1353 | h.inner.transcript.update(data13.client_hello[record.header_len..]); | |
| 1354 | } | |
| 1355 | ||
| 1356 | // Parsing partial server flight consumes nothing | |
| 1357 | var i: usize = 1; | |
| 1358 | while (i < data13.server_flight_1.len) : (i += 16) { | |
| 1359 | //for (1..data13.server_flight_1.len - 1) |i| { | |
| 1360 | res = try h.run(data13.server_flight_1[0..i], &.{}); | |
| 1361 | try testing.expectEqual(0, res.recv_pos); | |
| 1362 | try testing.expectEqual(i, res.unused_recv.len); | |
| 1363 | } | |
| 1364 | ||
| 1365 | res = try h.run(&(data13.server_flight_1 ++ "dummy footer".*), &buffer); | |
| 1366 | { | |
| 1367 | try testing.expectEqual(data13.server_flight_1.len, res.recv_pos); // footer is not touched | |
| 1368 | try testing.expectEqual(.tls_1_3, h.inner.tls_version); | |
| 1369 | try testing.expectEqual(.x25519, h.inner.named_group); | |
| 1370 | try testing.expectEqualSlices(u8, &data13.server_random, &h.inner.server_random); | |
| 1371 | try testing.expectEqual(.AES_256_GCM_SHA384, h.inner.cipher_suite); | |
| 1372 | try testing.expectEqualSlices(u8, &data13.server_pub_key, h.inner.server_pub_key); | |
| 1373 | try testing.expect(h.done()); | |
| 1374 | try testing.expectEqualSlices(u8, &data13.client_flight_2, res.send); | |
| 1375 | } | |
| 1376 | ||
| 1377 | // When done pushing more data is noop | |
| 1378 | res = try h.run(&("dummy footer".*), &buffer); | |
| 1379 | try testing.expectEqual(0, res.recv_pos); | |
| 1380 | try testing.expectEqual(0, res.send_pos); | |
| 1381 | try testing.expect(h.done()); | |
| 1382 | } | |
| 1383 | ||
| 1384 | test "note about sizes" { | |
| 1385 | // values valid only for 64 bit platform, so skip in ci | |
| 1386 | if (true) return error.SkipZigTest; | |
| 1387 | ||
| 1388 | try testing.expectEqual(18928, @sizeOf(Handshake)); | |
| 1389 | try testing.expectEqual(19072, @sizeOf(NonBlock)); | |
| 1390 | try testing.expectEqual(14384, @sizeOf(DhKeyPair)); | |
| 1391 | try testing.expectEqual(136, @sizeOf(Options)); | |
| 1392 | try testing.expectEqual(2792, @sizeOf(CertKeyPair)); | |
| 1393 | try testing.expectEqual(1736, @sizeOf(CertificateParser)); | |
| 1394 | try testing.expectEqual(48, @sizeOf(cert.Bundle)); | |
| 1395 | try testing.expectEqual(208, @sizeOf(Cipher)); | |
| 1396 | } | |
| @@ -1,594 +0,0 @@ | ||
| 1 | const std = ; | |
| 2 | const assert = std.debug.assert; | |
| 3 | const mem = std.mem; | |
| 4 | const crypto = std.crypto; | |
| 5 | const Certificate = crypto.Certificate; | |
| 6 | const Io = std.Io; | |
| 7 | ||
| 8 | const Transcript = .Transcript; | |
| 9 | const PrivateKey = ; | |
| 10 | const record = ; | |
| 11 | const rsa = ; | |
| 12 | const proto = ; | |
| 13 | ||
| 14 | const X25519 = crypto.dh.X25519; | |
| 15 | const EcdsaP256Sha256 = crypto.sign.ecdsa.EcdsaP256Sha256; | |
| 16 | const EcdsaP384Sha384 = crypto.sign.ecdsa.EcdsaP384Sha384; | |
| 17 | const MLKem768 = crypto.kem.ml_kem.MLKem768; | |
| 18 | ||
| 19 | pub const supported_signature_algorithms = &[_]proto.SignatureScheme{ | |
| 20 | .ecdsa_secp256r1_sha256, | |
| 21 | .ecdsa_secp384r1_sha384, | |
| 22 | .rsa_pss_rsae_sha256, | |
| 23 | .rsa_pss_rsae_sha384, | |
| 24 | .rsa_pss_rsae_sha512, | |
| 25 | .ed25519, | |
| 26 | .rsa_pkcs1_sha1, | |
| 27 | .rsa_pkcs1_sha256, | |
| 28 | .rsa_pkcs1_sha384, | |
| 29 | }; | |
| 30 | ||
| 31 | pub const CertKeyPair = struct { | |
| 32 | /// A chain of one or more certificates, leaf first. | |
| 33 | /// | |
| 34 | /// Each X.509 certificate contains the public key of a key pair, extra | |
| 35 | /// information (the name of the holder, the name of an issuer of the | |
| 36 | /// certificate, validity time spans) and a signature generated using the | |
| 37 | /// private key of the issuer of the certificate. | |
| 38 | /// | |
| 39 | /// All certificates from the bundle are sent to the other side when creating | |
| 40 | /// Certificate tls message. | |
| 41 | /// | |
| 42 | /// Leaf certificate and private key are used to create signature for | |
| 43 | /// CertifyVerify tls message. | |
| 44 | bundle: Certificate.Bundle, | |
| 45 | ||
| 46 | /// Private key corresponding to the public key in leaf certificate from the | |
| 47 | /// bundle. | |
| 48 | key: PrivateKey, | |
| 49 | ||
| 50 | /// Ecdsa key pair derived from key. Computed on init and cached because it | |
| 51 | /// is costly operation. Important for server which is creating many | |
| 52 | /// signatures with the same key to not repeat that operation. | |
| 53 | ecdsa_key_pair: ?EcdsaKeyPair = null, | |
| 54 | ||
| 55 | pub fn fromFilePath( | |
| 56 | allocator: mem.Allocator, | |
| 57 | io: Io, | |
| 58 | dir: std.Io.Dir, | |
| 59 | cert_path: []const u8, | |
| 60 | key_path: []const u8, | |
| 61 | ) !CertKeyPair { | |
| 62 | var bundle: cert.Bundle = .empty; | |
| 63 | try bundle.addCertsFromFilePath(allocator, dir, cert_path); | |
| 64 | ||
| 65 | const key_file = try dir.openFile(io, key_path, .{}); | |
| 66 | defer key_file.close(io); | |
| 67 | const key = try PrivateKey.fromFile(allocator, key_file); | |
| 68 | ||
| 69 | return .{ .bundle = bundle, .key = key, .ecdsa_key_pair = try EcdsaKeyPair.init(key) }; | |
| 70 | } | |
| 71 | ||
| 72 | pub fn fromFilePathAbsolute( | |
| 73 | allocator: mem.Allocator, | |
| 74 | io: Io, | |
| 75 | cert_path: []const u8, | |
| 76 | key_path: []const u8, | |
| 77 | ) !CertKeyPair { | |
| 78 | var bundle: cert.Bundle = .empty; | |
| 79 | const now = Io.Clock.real.now(io); | |
| 80 | try bundle.addCertsFromFilePathAbsolute(allocator, io, now, cert_path); | |
| 81 | ||
| 82 | const key_file = try std.Io.Dir.openFileAbsolute(io, key_path, .{}); | |
| 83 | defer key_file.close(io); | |
| 84 | const key = try PrivateKey.fromFile(allocator, key_file); | |
| 85 | ||
| 86 | return .{ .bundle = bundle, .key = key, .ecdsa_key_pair = try EcdsaKeyPair.init(key) }; | |
| 87 | } | |
| 88 | ||
| 89 | /// Reads certs directly from PEM files (delegates to fromFilePathAbsolute) | |
| 90 | pub fn fromFilePathAbsoluteSync( | |
| 91 | allocator: mem.Allocator, | |
| 92 | cert_path: []const u8, | |
| 93 | key_path: []const u8, | |
| 94 | ) !CertKeyPair { | |
| 95 | // This function is called from contexts that have io_compat initialized. | |
| 96 | // We use the same approach - read cert/key via Io. | |
| 97 | // Callers should use fromFilePathAbsolute directly when they have Io. | |
| 98 | ||
| 99 | // Read key file using posix fd operations (no Io needed) | |
| 100 | // For cert, manually parse PEM | |
| 101 | var cert_buf: [32768]u8 = undefined; | |
| 102 | var cert_len: usize = 0; | |
| 103 | ||
| 104 | // Use posix read since we may not have Io | |
| 105 | const cert_path_z = @as([*:0]const u8, @ptrCast(cert_path.ptr)); | |
| 106 | const cert_fd = std.c.open(cert_path_z, .{}, @as(std.c.mode_t, 0)); | |
| 107 | if (cert_fd < 0) return error.FileNotFound; | |
| 108 | defer _ = std.c.close(@intCast(cert_fd)); | |
| 109 | ||
| 110 | while (cert_len < cert_buf.len) { | |
| 111 | const n = std.posix.read(@intCast(cert_fd), cert_buf[cert_len..]) catch break; | |
| 112 | if (n == 0) break; | |
| 113 | cert_len += n; | |
| 114 | } | |
| 115 | ||
| 116 | // Parse PEM certificates manually | |
| 117 | var bundle: cert.Bundle = .empty; | |
| 118 | const pem_data = cert_buf[0..cert_len]; | |
| 119 | ||
| 120 | // Find and decode PEM blocks - add DER bytes directly to bundle | |
| 121 | const base64_decoder = std.base64.standard.decoderWithIgnore(" \t\r\n"); | |
| 122 | var start: usize = 0; | |
| 123 | while (std.mem.indexOfPos(u8, pem_data, start, "-----BEGIN CERTIFICATE-----")) |begin| { | |
| 124 | const content_start = begin + 27; // Length of "-----BEGIN CERTIFICATE-----" | |
| 125 | if (std.mem.indexOfPos(u8, pem_data, content_start, "-----END CERTIFICATE-----")) |end| { | |
| 126 | const base64_data = pem_data[content_start..end]; | |
| 127 | const decoded_start: u32 = @intCast(bundle.bytes.items.len); | |
| 128 | const dest_buf = bundle.bytes.addManyAsSlice(allocator, base64_decoder.calcSizeUpperBound(base64_data.len)) catch { | |
| 129 | start = end + 25; | |
| 130 | continue; | |
| 131 | }; | |
| 132 | const decoded_len = base64_decoder.decode(dest_buf, base64_data) catch { | |
| 133 | bundle.bytes.items.len = decoded_start; | |
| 134 | start = end + 25; | |
| 135 | continue; | |
| 136 | }; | |
| 137 | bundle.bytes.items.len = decoded_start + decoded_len; | |
| 138 | // Parse and add to map | |
| 139 | bundle.parseCert(allocator, decoded_start, 0) catch { | |
| 140 | bundle.bytes.items.len = decoded_start; | |
| 141 | }; | |
| 142 | start = end + 25; | |
| 143 | } else break; | |
| 144 | } | |
| 145 | ||
| 146 | // Read key file using posix fd | |
| 147 | const key_path_z = @as([*:0]const u8, @ptrCast(key_path.ptr)); | |
| 148 | const key_fd = std.c.open(key_path_z, .{}, @as(std.c.mode_t, 0)); | |
| 149 | if (key_fd < 0) return error.FileNotFound; | |
| 150 | defer _ = std.c.close(@intCast(key_fd)); | |
| 151 | ||
| 152 | var key_buf: [8192]u8 = undefined; | |
| 153 | var key_len: usize = 0; | |
| 154 | while (key_len < key_buf.len) { | |
| 155 | const n = std.posix.read(@intCast(key_fd), key_buf[key_len..]) catch break; | |
| 156 | if (n == 0) break; | |
| 157 | key_len += n; | |
| 158 | } | |
| 159 | const key = try PrivateKey.parsePem(key_buf[0..key_len]); | |
| 160 | ||
| 161 | return .{ .bundle = bundle, .key = key, .ecdsa_key_pair = try EcdsaKeyPair.init(key) }; | |
| 162 | } | |
| 163 | ||
| 164 | pub fn deinit(c: *CertKeyPair, allocator: mem.Allocator) void { | |
| 165 | c.bundle.deinit(allocator); | |
| 166 | } | |
| 167 | ||
| 168 | const EcdsaKeyPair = union(enum) { | |
| 169 | ecdsa_secp256r1_sha256: EcdsaP256Sha256.KeyPair, | |
| 170 | ecdsa_secp384r1_sha384: EcdsaP384Sha384.KeyPair, | |
| 171 | ||
| 172 | fn init(pk: PrivateKey) !?EcdsaKeyPair { | |
| 173 | switch (pk.signature_scheme) { | |
| 174 | inline .ecdsa_secp256r1_sha256, | |
| 175 | .ecdsa_secp384r1_sha384, | |
| 176 | => |comptime_scheme| { | |
| 177 | const Ecdsa = SchemeEcdsa(comptime_scheme); | |
| 178 | const key = pk.key.ecdsa; | |
| 179 | const key_len = Ecdsa.SecretKey.encoded_length; | |
| 180 | if (key.len < key_len) return error.InvalidEncoding; | |
| 181 | const secret_key = try Ecdsa.SecretKey.fromBytes(key[0..key_len].*); | |
| 182 | const key_pair = try Ecdsa.KeyPair.fromSecretKey(secret_key); | |
| 183 | return switch (comptime_scheme) { | |
| 184 | .ecdsa_secp256r1_sha256 => .{ .ecdsa_secp256r1_sha256 = key_pair }, | |
| 185 | .ecdsa_secp384r1_sha384 => .{ .ecdsa_secp384r1_sha384 = key_pair }, | |
| 186 | else => unreachable, | |
| 187 | }; | |
| 188 | }, | |
| 189 | else => return null, | |
| 190 | } | |
| 191 | } | |
| 192 | }; | |
| 193 | }; | |
| 194 | ||
| 195 | pub const cert = struct { | |
| 196 | // A chain of one or more certificates. | |
| 197 | // | |
| 198 | // They are used to verify that certificate chain sent by the other side | |
| 199 | // forms valid trust chain. | |
| 200 | pub const Bundle = crypto.Certificate.Bundle; | |
| 201 | ||
| 202 | pub fn fromFilePath(allocator: mem.Allocator, dir: std.fs.Dir, path: []const u8) !Bundle { | |
| 203 | var bundle: Bundle = .empty; | |
| 204 | try bundle.addCertsFromFilePath(allocator, dir, path); | |
| 205 | return bundle; | |
| 206 | } | |
| 207 | ||
| 208 | pub fn fromFilePathAbsolute(allocator: mem.Allocator, path: []const u8) !Bundle { | |
| 209 | var bundle: Bundle = .empty; | |
| 210 | try bundle.addCertsFromFilePathAbsolute(allocator, path); | |
| 211 | return bundle; | |
| 212 | } | |
| 213 | ||
| 214 | pub fn fromSystem(allocator: mem.Allocator) !Bundle { | |
| 215 | var bundle: Bundle = .empty; | |
| 216 | try bundle.rescan(allocator); | |
| 217 | return bundle; | |
| 218 | } | |
| 219 | }; | |
| 220 | ||
| 221 | pub const CertificateBuilder = struct { | |
| 222 | cert_key_pair: *CertKeyPair, | |
| 223 | transcript: *Transcript, | |
| 224 | tls_version: proto.Version = .tls_1_3, | |
| 225 | side: proto.Side = .client, | |
| 226 | ||
| 227 | pub fn makeCertificate(h: CertificateBuilder, w: *record.Writer) !void { | |
| 228 | const certs = h.cert_key_pair.bundle.bytes.items; | |
| 229 | const certs_count = h.cert_key_pair.bundle.map.size; | |
| 230 | ||
| 231 | // Differences between tls 1.3 and 1.2 | |
| 232 | // TLS 1.3 has request context in header and extensions for each certificate. | |
| 233 | // Here we use empty length for each field. | |
| 234 | // TLS 1.2 don't have these two fields. | |
| 235 | const request_context, const extensions = if (h.tls_version == .tls_1_3) | |
| 236 | .{ &[_]u8{0}, &[_]u8{ 0, 0 } } | |
| 237 | else | |
| 238 | .{ &[_]u8{}, &[_]u8{} }; | |
| 239 | const certs_len = certs.len + (3 + extensions.len) * certs_count; | |
| 240 | ||
| 241 | // Write handshake header | |
| 242 | try w.handshakeRecordHeader(.certificate, certs_len + request_context.len + 3); | |
| 243 | try w.slice(request_context); | |
| 244 | try w.int(u24, certs_len); | |
| 245 | ||
| 246 | // Write each certificate | |
| 247 | var index: u32 = 0; | |
| 248 | while (index < certs.len) { | |
| 249 | const e = try Certificate.der.Element.parse(certs, index); | |
| 250 | const crt = certs[index..e.slice.end]; | |
| 251 | try w.int(u24, crt.len); // certificate length | |
| 252 | try w.slice(crt); // certificate | |
| 253 | try w.slice(extensions); // certificate extensions | |
| 254 | index = e.slice.end; | |
| 255 | } | |
| 256 | } | |
| 257 | ||
| 258 | pub fn makeCertificateVerify(h: CertificateBuilder, w: *record.Writer) !void { | |
| 259 | // Creates signature for client certificate signature message. | |
| 260 | // Returns signature bytes and signature scheme. | |
| 261 | const signature, const signature_scheme = switch (h.cert_key_pair.key.signature_scheme) { | |
| 262 | inline .ecdsa_secp256r1_sha256, | |
| 263 | .ecdsa_secp384r1_sha384, | |
| 264 | => |comptime_scheme| brk: { | |
| 265 | const Ecdsa = SchemeEcdsa(comptime_scheme); | |
| 266 | const key_pair = switch (comptime_scheme) { | |
| 267 | .ecdsa_secp256r1_sha256 => h.cert_key_pair.ecdsa_key_pair.?.ecdsa_secp256r1_sha256, | |
| 268 | .ecdsa_secp384r1_sha384 => h.cert_key_pair.ecdsa_key_pair.?.ecdsa_secp384r1_sha384, | |
| 269 | else => unreachable, | |
| 270 | }; | |
| 271 | var signer = try key_pair.signer(null); | |
| 272 | h.setSignatureVerifyBytes(&signer); | |
| 273 | const signature = try signer.finalize(); | |
| 274 | var buf: [Ecdsa.Signature.der_encoded_length_max]u8 = undefined; | |
| 275 | break :brk .{ signature.toDer(&buf), comptime_scheme }; | |
| 276 | }, | |
| 277 | inline .rsa_pss_rsae_sha256, | |
| 278 | .rsa_pss_rsae_sha384, | |
| 279 | .rsa_pss_rsae_sha512, | |
| 280 | => |comptime_scheme| brk: { | |
| 281 | const Hash = SchemeHash(comptime_scheme); | |
| 282 | var signer = try h.cert_key_pair.key.key.rsa.signerOaep(Hash, null); | |
| 283 | h.setSignatureVerifyBytes(&signer); | |
| 284 | var buf: [512]u8 = undefined; | |
| 285 | const signature = try signer.finalize(&buf); | |
| 286 | break :brk .{ signature.bytes, comptime_scheme }; | |
| 287 | }, | |
| 288 | else => return error.TlsUnknownSignatureScheme, | |
| 289 | }; | |
| 290 | ||
| 291 | try w.handshakeRecordHeader(.certificate_verify, signature.len + 4); | |
| 292 | try w.enumValue(signature_scheme); | |
| 293 | try w.int(u16, signature.len); | |
| 294 | try w.slice(signature); | |
| 295 | } | |
| 296 | ||
| 297 | fn setSignatureVerifyBytes(h: CertificateBuilder, signer: anytype) void { | |
| 298 | if (h.tls_version == .tls_1_2) { | |
| 299 | // tls 1.2 signature uses current transcript hash value. | |
| 300 | // ref: https://datatracker.ietf.org/doc/html/rfc5246.html#section-7.4.8 | |
| 301 | const Hash = @TypeOf(signer.h); | |
| 302 | signer.h = h.transcript.hash(Hash); | |
| 303 | } else { | |
| 304 | // tls 1.3 signature is computed over concatenation of 64 spaces, | |
| 305 | // context, separator and content. | |
| 306 | // ref: https://datatracker.ietf.org/doc/html/rfc8446#section-4.4.3 | |
| 307 | if (h.side == .server) { | |
| 308 | signer.update(h.transcript.serverCertificateVerify()); | |
| 309 | } else { | |
| 310 | signer.update(h.transcript.clientCertificateVerify()); | |
| 311 | } | |
| 312 | } | |
| 313 | } | |
| 314 | }; | |
| 315 | ||
| 316 | pub fn SchemeEcdsa(comptime scheme: proto.SignatureScheme) type { | |
| 317 | return switch (scheme) { | |
| 318 | .ecdsa_secp256r1_sha256 => EcdsaP256Sha256, | |
| 319 | .ecdsa_secp384r1_sha384 => EcdsaP384Sha384, | |
| 320 | else => unreachable, | |
| 321 | }; | |
| 322 | } | |
| 323 | ||
| 324 | pub const CertificateParser = struct { | |
| 325 | pub_key_algo: Certificate.Parsed.PubKeyAlgo = undefined, | |
| 326 | pub_key_buf: [1038]u8 = undefined, | |
| 327 | pub_key: []const u8 = undefined, | |
| 328 | ||
| 329 | signature_scheme: proto.SignatureScheme = @fromBackingInt(@intCast(0)), | |
| 330 | signature_buf: [1024]u8 = undefined, | |
| 331 | signature: []const u8 = undefined, | |
| 332 | ||
| 333 | root_ca: Certificate.Bundle, | |
| 334 | host: []const u8, | |
| 335 | skip_verify: bool = false, | |
| 336 | now_sec: i64 = 0, | |
| 337 | ||
| 338 | pub fn parseCertificate(h: *CertificateParser, d: *record.Decoder, tls_version: proto.Version) !void { | |
| 339 | if (h.now_sec == 0) { | |
| 340 | var ts: std.c.timespec = undefined; | |
| 341 | if (std.c.clock_gettime(.REALTIME, &ts) == 0) { | |
| 342 | h.now_sec = ts.sec; | |
| 343 | } | |
| 344 | } | |
| 345 | if (tls_version == .tls_1_3) { | |
| 346 | const request_context = try d.decode(u8); | |
| 347 | if (request_context != 0) return error.TlsIllegalParameter; | |
| 348 | } | |
| 349 | ||
| 350 | var trust_chain_established = false; | |
| 351 | var last_cert: ?Certificate.Parsed = null; | |
| 352 | const certs_len = try d.decode(u24); | |
| 353 | const start_idx = d.idx; | |
| 354 | while (d.idx - start_idx < certs_len) { | |
| 355 | const crt_len = try d.decode(u24); | |
| 356 | const crt = try d.slice(crt_len); | |
| 357 | if (tls_version == .tls_1_3) { | |
| 358 | // certificate extensions present in tls 1.3 | |
| 359 | try d.skip(try d.decode(u16)); | |
| 360 | } | |
| 361 | if (trust_chain_established) | |
| 362 | continue; | |
| 363 | ||
| 364 | const subject = try (Certificate{ .buffer = crt, .index = 0 }).parse(); | |
| 365 | if (last_cert) |pc| { | |
| 366 | if (pc.verify(subject, h.now_sec)) { | |
| 367 | last_cert = subject; | |
| 368 | } else |err| switch (err) { | |
| 369 | error.CertificateIssuerMismatch => { | |
| 370 | // skip certificate which is not part of the chain | |
| 371 | continue; | |
| 372 | }, | |
| 373 | else => return err, | |
| 374 | } | |
| 375 | } else { // first certificate | |
| 376 | if (!h.skip_verify and h.host.len > 0) { | |
| 377 | try subject.verifyHostName(h.host); | |
| 378 | } | |
| 379 | h.pub_key = try dupe(&h.pub_key_buf, subject.pubKey()); | |
| 380 | h.pub_key_algo = subject.pub_key_algo; | |
| 381 | last_cert = subject; | |
| 382 | } | |
| 383 | if (!h.skip_verify) { | |
| 384 | if (h.root_ca.verify(last_cert.?, h.now_sec)) |_| { | |
| 385 | trust_chain_established = true; | |
| 386 | } else |err| switch (err) { | |
| 387 | error.CertificateIssuerNotFound => {}, | |
| 388 | else => return err, | |
| 389 | } | |
| 390 | } | |
| 391 | } | |
| 392 | if (!h.skip_verify and !trust_chain_established) { | |
| 393 | return error.CertificateIssuerNotFound; | |
| 394 | } | |
| 395 | } | |
| 396 | ||
| 397 | pub fn parseCertificateVerify(h: *CertificateParser, d: *record.Decoder) !void { | |
| 398 | h.signature_scheme = try d.decode(proto.SignatureScheme); | |
| 399 | h.signature = try dupe(&h.signature_buf, try d.slice(try d.decode(u16))); | |
| 400 | } | |
| 401 | ||
| 402 | pub fn verifySignature(h: *CertificateParser, verify_bytes: []const u8) !void { | |
| 403 | switch (h.signature_scheme) { | |
| 404 | inline .ecdsa_secp256r1_sha256, | |
| 405 | .ecdsa_secp384r1_sha384, | |
| 406 | => |comptime_scheme| { | |
| 407 | if (h.pub_key_algo != .X9_62_id_ecPublicKey) return error.TlsBadSignatureScheme; | |
| 408 | const cert_named_curve = h.pub_key_algo.X9_62_id_ecPublicKey; | |
| 409 | switch (cert_named_curve) { | |
| 410 | inline .secp384r1, .X9_62_prime256v1 => |comptime_cert_named_curve| { | |
| 411 | const Ecdsa = SchemeEcdsaCert(comptime_scheme, comptime_cert_named_curve); | |
| 412 | const key = try Ecdsa.PublicKey.fromSec1(h.pub_key); | |
| 413 | const sig = try Ecdsa.Signature.fromDer(h.signature); | |
| 414 | try sig.verify(verify_bytes, key); | |
| 415 | }, | |
| 416 | else => return error.TlsUnknownSignatureScheme, | |
| 417 | } | |
| 418 | }, | |
| 419 | .ed25519 => { | |
| 420 | if (h.pub_key_algo != .curveEd25519) return error.TlsBadSignatureScheme; | |
| 421 | const Eddsa = crypto.sign.Ed25519; | |
| 422 | if (h.signature.len != Eddsa.Signature.encoded_length) return error.InvalidEncoding; | |
| 423 | const sig = Eddsa.Signature.fromBytes(h.signature[0..Eddsa.Signature.encoded_length].*); | |
| 424 | if (h.pub_key.len != Eddsa.PublicKey.encoded_length) return error.InvalidEncoding; | |
| 425 | const key = try Eddsa.PublicKey.fromBytes(h.pub_key[0..Eddsa.PublicKey.encoded_length].*); | |
| 426 | try sig.verify(verify_bytes, key); | |
| 427 | }, | |
| 428 | inline .rsa_pss_rsae_sha256, | |
| 429 | .rsa_pss_rsae_sha384, | |
| 430 | .rsa_pss_rsae_sha512, | |
| 431 | => |comptime_scheme| { | |
| 432 | if (h.pub_key_algo != .rsaEncryption) return error.TlsBadSignatureScheme; | |
| 433 | const Hash = SchemeHash(comptime_scheme); | |
| 434 | const pk = try rsa.PublicKey.fromDer(h.pub_key); | |
| 435 | const sig = rsa.Pss(Hash).Signature{ .bytes = h.signature }; | |
| 436 | try sig.verify(verify_bytes, pk, null); | |
| 437 | }, | |
| 438 | inline .rsa_pkcs1_sha1, | |
| 439 | .rsa_pkcs1_sha256, | |
| 440 | .rsa_pkcs1_sha384, | |
| 441 | .rsa_pkcs1_sha512, | |
| 442 | => |comptime_scheme| { | |
| 443 | if (h.pub_key_algo != .rsaEncryption) return error.TlsBadSignatureScheme; | |
| 444 | const Hash = SchemeHash(comptime_scheme); | |
| 445 | const pk = try rsa.PublicKey.fromDer(h.pub_key); | |
| 446 | const sig = rsa.PKCS1v1_5(Hash).Signature{ .bytes = h.signature }; | |
| 447 | try sig.verify(verify_bytes, pk); | |
| 448 | }, | |
| 449 | else => return error.TlsUnknownSignatureScheme, | |
| 450 | } | |
| 451 | } | |
| 452 | ||
| 453 | fn SchemeEcdsaCert(comptime scheme: proto.SignatureScheme, comptime cert_named_curve: Certificate.NamedCurve) type { | |
| 454 | const Sha256 = crypto.hash.sha2.Sha256; | |
| 455 | const Sha384 = crypto.hash.sha2.Sha384; | |
| 456 | const Ecdsa = crypto.sign.ecdsa.Ecdsa; | |
| 457 | ||
| 458 | return switch (scheme) { | |
| 459 | .ecdsa_secp256r1_sha256 => Ecdsa(cert_named_curve.Curve(), Sha256), | |
| 460 | .ecdsa_secp384r1_sha384 => Ecdsa(cert_named_curve.Curve(), Sha384), | |
| 461 | else => @compileError("bad scheme"), | |
| 462 | }; | |
| 463 | } | |
| 464 | }; | |
| 465 | ||
| 466 | pub fn SchemeHash(comptime scheme: proto.SignatureScheme) type { | |
| 467 | const Sha256 = crypto.hash.sha2.Sha256; | |
| 468 | const Sha384 = crypto.hash.sha2.Sha384; | |
| 469 | const Sha512 = crypto.hash.sha2.Sha512; | |
| 470 | ||
| 471 | return switch (scheme) { | |
| 472 | .rsa_pkcs1_sha1 => crypto.hash.Sha1, | |
| 473 | .rsa_pss_rsae_sha256, .rsa_pkcs1_sha256 => Sha256, | |
| 474 | .rsa_pss_rsae_sha384, .rsa_pkcs1_sha384 => Sha384, | |
| 475 | .rsa_pss_rsae_sha512, .rsa_pkcs1_sha512 => Sha512, | |
| 476 | else => @compileError("bad scheme"), | |
| 477 | }; | |
| 478 | } | |
| 479 | ||
| 480 | pub fn dupe(buf: []u8, data: []const u8) ![]u8 { | |
| 481 | if (buf.len < data.len) { | |
| 482 | return error.BufferUndersize; | |
| 483 | } | |
| 484 | @memcpy(buf[0..data.len], data); | |
| 485 | return buf[0..data.len]; | |
| 486 | } | |
| 487 | ||
| 488 | pub fn dupeMin(buf: []u8, data: []const u8) []u8 { | |
| 489 | const n = @min(data.len, buf.len); | |
| 490 | @memcpy(buf[0..n], data[0..n]); | |
| 491 | return buf[0..n]; | |
| 492 | } | |
| 493 | ||
| 494 | pub const DhKeyPair = struct { | |
| 495 | x25519_kp: X25519.KeyPair = undefined, | |
| 496 | secp256r1_kp: EcdsaP256Sha256.KeyPair = undefined, | |
| 497 | secp384r1_kp: EcdsaP384Sha384.KeyPair = undefined, | |
| 498 | ml_kem768: MLKem768.KeyPair = undefined, | |
| 499 | ||
| 500 | secp256r1_pk_buf: [EcdsaP256Sha256.PublicKey.uncompressed_sec1_encoded_length]u8 = undefined, //65 bytes | |
| 501 | secp384r1_pk_buf: [EcdsaP384Sha384.PublicKey.uncompressed_sec1_encoded_length]u8 = undefined, //97 | |
| 502 | ml_kem768_pk_buf: [MLKem768.PublicKey.encoded_length + X25519.public_length]u8 = undefined, // 1216 | |
| 503 | shared_key_buf: [64]u8 = undefined, | |
| 504 | ||
| 505 | pub const seed_len = 32 + 32 + 48 + 64 + 64; | |
| 506 | ||
| 507 | pub fn init(seed: [seed_len]u8, named_groups: []const proto.NamedGroup) !DhKeyPair { | |
| 508 | var kp: DhKeyPair = .{}; | |
| 509 | for (named_groups) |ng| | |
| 510 | switch (ng) { | |
| 511 | .x25519 => kp.x25519_kp = try X25519.KeyPair.generateDeterministic(seed[0..][0..X25519.seed_length].*), | |
| 512 | .secp256r1 => kp.secp256r1_kp = try EcdsaP256Sha256.KeyPair.generateDeterministic(seed[32..][0..EcdsaP256Sha256.KeyPair.seed_length].*), | |
| 513 | .secp384r1 => kp.secp384r1_kp = try EcdsaP384Sha384.KeyPair.generateDeterministic(seed[32 + 32 ..][0..EcdsaP384Sha384.KeyPair.seed_length].*), | |
| 514 | .x25519_ml_kem768 => kp.ml_kem768 = try MLKem768.KeyPair.generateDeterministic(seed[32 + 32 + 48 + 64 ..][0..MLKem768.seed_length].*), | |
| 515 | else => return error.TlsIllegalParameter, | |
| 516 | }; | |
| 517 | return kp; | |
| 518 | } | |
| 519 | ||
| 520 | // x25519: 32, secp256r1: 32, secp384r1: 48, x25519_ml_kem768: 64 | |
| 521 | pub fn sharedKey(self: *DhKeyPair, named_group: proto.NamedGroup, server_pub_key: []const u8) ![]const u8 { | |
| 522 | return switch (named_group) { | |
| 523 | .x25519 => { | |
| 524 | if (server_pub_key.len != X25519.public_length) | |
| 525 | return error.TlsIllegalParameter; | |
| 526 | self.shared_key_buf[0..32].* = try X25519.scalarmult( | |
| 527 | self.x25519_kp.secret_key, | |
| 528 | server_pub_key[0..X25519.public_length].*, | |
| 529 | ); | |
| 530 | return self.shared_key_buf[0..32]; | |
| 531 | }, | |
| 532 | .secp256r1 => { | |
| 533 | const pk = try EcdsaP256Sha256.PublicKey.fromSec1(server_pub_key); | |
| 534 | const mul = try pk.p.mulPublic(self.secp256r1_kp.secret_key.bytes, .big); | |
| 535 | self.shared_key_buf[0..32].* = mul.affineCoordinates().x.toBytes(.big); | |
| 536 | return self.shared_key_buf[0..32]; | |
| 537 | }, | |
| 538 | .secp384r1 => { | |
| 539 | const pk = try EcdsaP384Sha384.PublicKey.fromSec1(server_pub_key); | |
| 540 | const mul = try pk.p.mulPublic(self.secp384r1_kp.secret_key.bytes, .big); | |
| 541 | self.shared_key_buf[0..48].* = mul.affineCoordinates().x.toBytes(.big); | |
| 542 | return self.shared_key_buf[0..48]; | |
| 543 | }, | |
| 544 | .x25519_ml_kem768 => { | |
| 545 | const hksl = crypto.kem.ml_kem.MLKem768.ciphertext_length; | |
| 546 | const xksl = hksl + crypto.dh.X25519.public_length; | |
| 547 | if (server_pub_key.len != xksl) return error.TlsIllegalParameter; | |
| 548 | ||
| 549 | const hsk = self.ml_kem768.secret_key.decaps(server_pub_key[0..hksl]) catch | |
| 550 | return error.TlsDecryptFailure; | |
| 551 | const xsk = crypto.dh.X25519.scalarmult(self.x25519_kp.secret_key, server_pub_key[hksl..xksl].*) catch | |
| 552 | return error.TlsDecryptFailure; | |
| 553 | self.shared_key_buf = (hsk ++ xsk); | |
| 554 | return &self.shared_key_buf; | |
| 555 | }, | |
| 556 | else => return error.TlsIllegalParameter, | |
| 557 | }; | |
| 558 | } | |
| 559 | ||
| 560 | // Returns 32, 65, 97 or 1216 bytes ml_kem | |
| 561 | pub fn publicKey(self: *DhKeyPair, named_group: proto.NamedGroup) ![]const u8 { | |
| 562 | return switch (named_group) { | |
| 563 | .x25519 => &self.x25519_kp.public_key, | |
| 564 | .secp256r1 => { | |
| 565 | self.secp256r1_pk_buf = self.secp256r1_kp.public_key.toUncompressedSec1(); | |
| 566 | return &self.secp256r1_pk_buf; | |
| 567 | }, | |
| 568 | .secp384r1 => { | |
| 569 | self.secp384r1_pk_buf = self.secp384r1_kp.public_key.toUncompressedSec1(); | |
| 570 | return &self.secp384r1_pk_buf; | |
| 571 | }, | |
| 572 | .x25519_ml_kem768 => { | |
| 573 | self.ml_kem768_pk_buf = self.ml_kem768.public_key.toBytes() ++ self.x25519_kp.public_key; | |
| 574 | return &self.ml_kem768_pk_buf; | |
| 575 | }, | |
| 576 | else => return error.TlsIllegalParameter, | |
| 577 | }; | |
| 578 | } | |
| 579 | }; | |
| 580 | ||
| 581 | const testing = std.testing; | |
| 582 | const testu = ; | |
| 583 | ||
| 584 | test "DhKeyPair.x25519" { | |
| 585 | var seed: [DhKeyPair.seed_len]u8 = undefined; | |
| 586 | testu.fill(&seed); | |
| 587 | const server_pub_key = &testu.hexToBytes("3303486548531f08d91e675caf666c2dc924ac16f47a861a7f4d05919d143637"); | |
| 588 | const expected = &testu.hexToBytes( | |
| 589 | \\ F1 67 FB 4A 49 B2 91 77 08 29 45 A1 F7 08 5A 21 | |
| 590 | \\ AF FE 9E 78 C2 03 9B 81 92 40 72 73 74 7A 46 1E | |
| 591 | ); | |
| 592 | var kp = try DhKeyPair.init(seed, &.{.x25519}); | |
| 593 | try testing.expectEqualSlices(u8, expected, try kp.sharedKey(.x25519, server_pub_key)); | |
| 594 | } | |
| @@ -1,1069 +0,0 @@ | ||
| 1 | const std = ; | |
| 2 | const assert = std.debug.assert; | |
| 3 | const crypto = std.crypto; | |
| 4 | const mem = std.mem; | |
| 5 | const Io = std.Io; | |
| 6 | const Certificate = crypto.Certificate; | |
| 7 | ||
| 8 | const Cipher = .Cipher; | |
| 9 | const CipherSuite = .CipherSuite; | |
| 10 | const cipher_suites = .cipher_suites; | |
| 11 | const max_cleartext_len = .max_cleartext_len; | |
| 12 | ||
| 13 | const Transcript = .Transcript; | |
| 14 | const record = ; | |
| 15 | const Record = record.Record; | |
| 16 | const PrivateKey = ; | |
| 17 | const proto = ; | |
| 18 | ||
| 19 | const common = ; | |
| 20 | const CertificateBuilder = common.CertificateBuilder; | |
| 21 | const CertificateParser = common.CertificateParser; | |
| 22 | const DhKeyPair = common.DhKeyPair; | |
| 23 | const CertKeyPair = common.CertKeyPair; | |
| 24 | const cert = common.cert; | |
| 25 | ||
| 26 | const rng = ; | |
| 27 | const log = std.log.scoped(.tls); | |
| 28 | ||
| 29 | pub const Options = struct { | |
| 30 | /// Server authentication. If null server will not send Certificate and | |
| 31 | /// CertificateVerify message. | |
| 32 | auth: ?*CertKeyPair, | |
| 33 | ||
| 34 | /// If not null server will request client certificate. If auth_type is | |
| 35 | /// .request empty client certificate message will be accepted. | |
| 36 | /// Client certificate will be verified with root_ca certificates. | |
| 37 | client_auth: ?ClientAuth = null, | |
| 38 | ||
| 39 | /// List of supported cipher suites (TLS 1.3 and/or TLS 1.2) | |
| 40 | /// Default includes both TLS 1.3 and TLS 1.2 secure ciphers for maximum compatibility. | |
| 41 | cipher_suites: []const CipherSuite = cipher_suites.all, | |
| 42 | ||
| 43 | /// Named groups (elliptic curves) to support for key exchange | |
| 44 | named_groups: []const proto.NamedGroup = &[_]proto.NamedGroup{ .x25519, .secp256r1, .secp384r1 }, | |
| 45 | }; | |
| 46 | ||
| 47 | pub const ClientAuth = struct { | |
| 48 | /// Set of root certificate authorities that server use when verifying | |
| 49 | /// client certificates. | |
| 50 | root_ca: cert.Bundle, | |
| 51 | ||
| 52 | auth_type: Type = .require, | |
| 53 | ||
| 54 | pub const Type = enum { | |
| 55 | /// Client certificate will be requested during the handshake, but does | |
| 56 | /// not require that the client send any certificates. | |
| 57 | request, | |
| 58 | /// Client certificate will be requested during the handshake, and client | |
| 59 | /// has to send valid certificate. | |
| 60 | require, | |
| 61 | }; | |
| 62 | }; | |
| 63 | ||
| 64 | pub const Handshake = struct { | |
| 65 | // public key len: x25519 = 32, secp256r1 = 65, secp384r1 = 97 | |
| 66 | const max_pub_key_len = 98; | |
| 67 | const supported_named_groups = &[_]proto.NamedGroup{ .x25519, .secp256r1, .secp384r1 }; | |
| 68 | ||
| 69 | /// Underlying network connection stream reader/writer pair. | |
| 70 | input: *Io.Reader, | |
| 71 | output: *Io.Writer, | |
| 72 | ||
| 73 | server_random: [32]u8 = undefined, | |
| 74 | client_random: [32]u8 = undefined, | |
| 75 | legacy_session_id_buf: [32]u8 = undefined, | |
| 76 | legacy_session_id: []u8 = &.{}, | |
| 77 | cipher_suite: CipherSuite = @fromBackingInt(@intCast(0)), | |
| 78 | signature_scheme: proto.SignatureScheme = @fromBackingInt(@intCast(0)), | |
| 79 | named_group: proto.NamedGroup = @fromBackingInt(@intCast(0)), | |
| 80 | client_pub_key_buf: [max_pub_key_len]u8 = undefined, | |
| 81 | client_pub_key: []u8 = &.{}, | |
| 82 | server_pub_key_buf: [max_pub_key_len]u8 = undefined, | |
| 83 | server_pub_key: []u8 = &.{}, | |
| 84 | ||
| 85 | cipher: Cipher = undefined, | |
| 86 | transcript: Transcript = .{}, | |
| 87 | ||
| 88 | // TLS 1.2 specific fields | |
| 89 | tls_version: proto.Version = .tls_1_3, | |
| 90 | master_secret: [48]u8 = undefined, | |
| 91 | key_material: [48 * 4]u8 = undefined, | |
| 92 | dh_kp: DhKeyPair = undefined, | |
| 93 | ||
| 94 | const Self = @This(); | |
| 95 | ||
| 96 | fn writeAlert(h: *Self, cph: ?*Cipher, err: anyerror) !void { | |
| 97 | if (cph) |c| { | |
| 98 | const cleartext = proto.alertFromError(err); | |
| 99 | const ciphertext = try c.encrypt(h.output.unusedCapacitySlice(), .alert, &cleartext); | |
| 100 | h.output.advance(ciphertext.len); | |
| 101 | } else { | |
| 102 | const alert = record.header(.alert, 2) ++ proto.alertFromError(err); | |
| 103 | try h.output.writeAll(&alert); | |
| 104 | } | |
| 105 | try h.output.flush(); | |
| 106 | } | |
| 107 | ||
| 108 | pub fn handshake(h: *Self, opt: Options) !Cipher { | |
| 109 | h.initKeys(opt); | |
| 110 | ||
| 111 | h.readClientHello(opt.cipher_suites, opt.named_groups) catch |err| { | |
| 112 | try h.writeAlert(null, err); | |
| 113 | return err; | |
| 114 | }; | |
| 115 | h.transcript.use(h.cipher_suite.hash()); | |
| 116 | ||
| 117 | if (h.tls_version == .tls_1_3) { | |
| 118 | // TLS 1.3 handshake | |
| 119 | h.serverFlight(opt) catch |err| { | |
| 120 | try h.writeAlert(null, err); | |
| 121 | return err; | |
| 122 | }; | |
| 123 | try h.output.flush(); | |
| 124 | ||
| 125 | h.clientFlight2(opt) catch |err| { | |
| 126 | // Alert received from client | |
| 127 | if (!mem.startsWith(u8, @errorName(err), "TlsAlert")) { | |
| 128 | try h.writeAlert(&h.cipher, err); | |
| 129 | } | |
| 130 | return err; | |
| 131 | }; | |
| 132 | } else { | |
| 133 | // TLS 1.2 handshake | |
| 134 | h.serverFlightTls12(opt) catch |err| { | |
| 135 | try h.writeAlert(null, err); | |
| 136 | return err; | |
| 137 | }; | |
| 138 | try h.output.flush(); | |
| 139 | ||
| 140 | h.clientFlight2Tls12(opt) catch |err| { | |
| 141 | if (!mem.startsWith(u8, @errorName(err), "TlsAlert")) { | |
| 142 | try h.writeAlert(if (h.cipher_suite.validate() == null) &h.cipher else null, err); | |
| 143 | } | |
| 144 | return err; | |
| 145 | }; | |
| 146 | } | |
| 147 | return h.cipher; | |
| 148 | } | |
| 149 | ||
| 150 | fn initKeys(h: *Self, opt: Options) void { | |
| 151 | rng.fill(&h.server_random); | |
| 152 | if (opt.auth) |a| { | |
| 153 | // required signature scheme in client hello | |
| 154 | h.signature_scheme = a.key.signature_scheme; | |
| 155 | } | |
| 156 | // Initialize DH key pair for key exchange (used in TLS 1.2 ECDHE) | |
| 157 | var seed: [DhKeyPair.seed_len]u8 = undefined; | |
| 158 | rng.fill(&seed); | |
| 159 | h.dh_kp = DhKeyPair.init(seed, opt.named_groups) catch unreachable; | |
| 160 | } | |
| 161 | ||
| 162 | fn clientFlight1(h: *Self, opt: Options) !void { | |
| 163 | try h.readClientHello(opt.cipher_suites, opt.named_groups); | |
| 164 | h.transcript.use(h.cipher_suite.hash()); | |
| 165 | } | |
| 166 | ||
| 167 | /// TLS 1.2 server flight: ServerHello, Certificate, ServerKeyExchange, ServerHelloDone | |
| 168 | fn serverFlightTls12(h: *Self, opt: Options) !void { | |
| 169 | var w: record.Writer = .initFromIo(h.output); | |
| 170 | ||
| 171 | // Debug: log the selected named_group (use info level to show in production) | |
| 172 | log.info("TLS 1.2 serverFlightTls12: named_group={x}, cipher_suite={x}", .{ @backingInt(h.named_group), @backingInt(h.cipher_suite) }); | |
| 173 | ||
| 174 | // Generate server's DH public key | |
| 175 | log.info("TLS 1.2: generating DH public key", .{}); | |
| 176 | h.server_pub_key = try common.dupe(&h.server_pub_key_buf, try h.dh_kp.publicKey(h.named_group)); | |
| 177 | log.info("TLS 1.2: DH public key generated, len={d}", .{h.server_pub_key.len}); | |
| 178 | ||
| 179 | // ServerHello | |
| 180 | { | |
| 181 | log.info("TLS 1.2: making ServerHello", .{}); | |
| 182 | const hello = try h.makeServerHelloTls12(&w); | |
| 183 | h.transcript.update(hello[record.header_len..]); | |
| 184 | log.info("TLS 1.2: ServerHello done, pos={d}", .{w.pos()}); | |
| 185 | } | |
| 186 | ||
| 187 | // Certificate (if auth is enabled) | |
| 188 | if (opt.auth) |auth| { | |
| 189 | log.info("TLS 1.2: making Certificate at pos={d}", .{w.pos()}); | |
| 190 | const cb = CertificateBuilder{ | |
| 191 | .cert_key_pair = auth, | |
| 192 | .transcript = &h.transcript, | |
| 193 | .tls_version = .tls_1_2, | |
| 194 | .side = .server, | |
| 195 | }; | |
| 196 | // Skip space for record header (5 bytes) | |
| 197 | const header_pos = try w.skip(record.header_len); | |
| 198 | const content_start = w.pos(); | |
| 199 | try cb.makeCertificate(&w); | |
| 200 | const content_len = w.pos() - content_start; | |
| 201 | h.transcript.update(w.buffered()[content_start..]); | |
| 202 | // Go back and write the record header | |
| 203 | var hw = w.writerAt(header_pos); | |
| 204 | try hw.recordHeader(.handshake, content_len); | |
| 205 | log.info("TLS 1.2: Certificate done, content_len={d}, pos={d}", .{ content_len, w.pos() }); | |
| 206 | } | |
| 207 | ||
| 208 | // ServerKeyExchange (for ECDHE cipher suites) | |
| 209 | if (h.cipher_suite.keyExchange() == .ecdhe) { | |
| 210 | if (opt.auth) |auth| { | |
| 211 | log.info("TLS 1.2: making ServerKeyExchange at pos={d}", .{w.pos()}); | |
| 212 | // Skip space for record header (5 bytes) | |
| 213 | const header_pos = try w.skip(record.header_len); | |
| 214 | const content_start = w.pos(); | |
| 215 | try h.makeServerKeyExchange(&w, auth); | |
| 216 | const content_len = w.pos() - content_start; | |
| 217 | h.transcript.update(w.buffered()[content_start..]); | |
| 218 | // Go back and write the record header | |
| 219 | var hw = w.writerAt(header_pos); | |
| 220 | try hw.recordHeader(.handshake, content_len); | |
| 221 | log.info("TLS 1.2: ServerKeyExchange done, content_len={d}, pos={d}", .{ content_len, w.pos() }); | |
| 222 | } | |
| 223 | } | |
| 224 | ||
| 225 | // ServerHelloDone | |
| 226 | { | |
| 227 | log.info("TLS 1.2: making ServerHelloDone at pos={d}", .{w.pos()}); | |
| 228 | // Skip space for record header (5 bytes) | |
| 229 | const header_pos = try w.skip(record.header_len); | |
| 230 | const content_start = w.pos(); | |
| 231 | // ServerHelloDone is just a handshake header with 0 length body | |
| 232 | try w.handshakeRecordHeader(.server_hello_done, 0); | |
| 233 | const content_len = w.pos() - content_start; | |
| 234 | h.transcript.update(w.buffered()[content_start..]); | |
| 235 | // Go back and write the record header | |
| 236 | var hw = w.writerAt(header_pos); | |
| 237 | try hw.recordHeader(.handshake, content_len); | |
| 238 | log.info("TLS 1.2: ServerHelloDone done, content_len={d}, pos={d}", .{ content_len, w.pos() }); | |
| 239 | } | |
| 240 | ||
| 241 | log.info("TLS 1.2: serverFlightTls12 complete, total bytes={d}", .{w.buffered().len}); | |
| 242 | log.info("TLS 1.2: h.output.end before advance={d}", .{h.output.end}); | |
| 243 | h.output.advance(w.buffered().len); | |
| 244 | log.info("TLS 1.2: h.output.end after advance={d}", .{h.output.end}); | |
| 245 | } | |
| 246 | ||
| 247 | fn makeServerHelloTls12(h: *Self, w: *record.Writer) ![]const u8 { | |
| 248 | const header_pos = try w.skip(9); | |
| 249 | ||
| 250 | try w.enumValue(proto.Version.tls_1_2); | |
| 251 | try w.slice(&h.server_random); | |
| 252 | { | |
| 253 | try w.int(u8, h.legacy_session_id.len); | |
| 254 | if (h.legacy_session_id.len > 0) try w.slice(h.legacy_session_id); | |
| 255 | } | |
| 256 | try w.enumValue(h.cipher_suite); | |
| 257 | try w.slice(&[_]u8{0}); // compression method (null) | |
| 258 | ||
| 259 | // Extensions for TLS 1.2 ServerHello | |
| 260 | // RFC 5746: renegotiation_info extension is required by modern clients | |
| 261 | // Extension structure: type (2) + length (2) + data | |
| 262 | // renegotiation_info: type=0xff01, length=1, renegotiated_connection_length=0 | |
| 263 | try w.int(u16, 5); // Extensions total length | |
| 264 | try w.int(u16, 0xff01); // Extension type: renegotiation_info | |
| 265 | try w.int(u16, 1); // Extension data length | |
| 266 | try w.int(u8, 0); // renegotiated_connection length (0 for initial handshake) | |
| 267 | ||
| 268 | var hw = w.writerAt(header_pos); | |
| 269 | try hw.recordHeader(.handshake, w.pos() - 5); | |
| 270 | try hw.handshakeRecordHeader(.server_hello, w.pos() - 9); | |
| 271 | ||
| 272 | return w.buffered(); | |
| 273 | } | |
| 274 | ||
| 275 | fn makeServerKeyExchange(h: *Self, hw: *record.Writer, auth: *CertKeyPair) !void { | |
| 276 | const content_start = hw.pos(); | |
| 277 | // Skip handshake header, write it at end | |
| 278 | _ = try hw.skip(4); | |
| 279 | ||
| 280 | // ECParameters - named_curve type + curve ID | |
| 281 | try hw.int(u8, @backingInt(proto.Curve.named_curve)); | |
| 282 | try hw.enumValue(h.named_group); | |
| 283 | ||
| 284 | // ECPoint - public key | |
| 285 | const pub_key = h.server_pub_key; | |
| 286 | try hw.int(u8, pub_key.len); | |
| 287 | try hw.slice(pub_key); | |
| 288 | ||
| 289 | // Signature - sign (client_random + server_random + ec_params + ec_point) | |
| 290 | const params_end = hw.pos(); | |
| 291 | const params = hw.buffered()[content_start + 4 .. params_end]; | |
| 292 | ||
| 293 | // Build data to sign: client_random || server_random || ec_params | |
| 294 | var sign_buf: [32 + 32 + 256]u8 = undefined; | |
| 295 | @memcpy(sign_buf[0..32], &h.client_random); | |
| 296 | @memcpy(sign_buf[32..64], &h.server_random); | |
| 297 | @memcpy(sign_buf[64 .. 64 + params.len], params); | |
| 298 | const sign_data = sign_buf[0 .. 64 + params.len]; | |
| 299 | ||
| 300 | // Sign the data using the appropriate signing method based on key type | |
| 301 | const signature_scheme = auth.key.signature_scheme; | |
| 302 | log.info("TLS 1.2 ServerKeyExchange: signature_scheme={x}, ecdsa_key_pair_present={}", .{ @backingInt(signature_scheme), auth.ecdsa_key_pair != null }); | |
| 303 | try hw.enumValue(signature_scheme); | |
| 304 | ||
| 305 | const signature = switch (signature_scheme) { | |
| 306 | inline .ecdsa_secp256r1_sha256, | |
| 307 | .ecdsa_secp384r1_sha384, | |
| 308 | => |comptime_scheme| brk: { | |
| 309 | log.info("TLS 1.2 ServerKeyExchange: entering ECDSA branch", .{}); | |
| 310 | const Ecdsa = common.SchemeEcdsa(comptime_scheme); | |
| 311 | const key_pair = switch (comptime_scheme) { | |
| 312 | .ecdsa_secp256r1_sha256 => auth.ecdsa_key_pair.?.ecdsa_secp256r1_sha256, | |
| 313 | .ecdsa_secp384r1_sha384 => auth.ecdsa_key_pair.?.ecdsa_secp384r1_sha384, | |
| 314 | else => unreachable, | |
| 315 | }; | |
| 316 | log.info("TLS 1.2 ServerKeyExchange: got key_pair, creating signer", .{}); | |
| 317 | var signer = try key_pair.signer(null); | |
| 318 | log.info("TLS 1.2 ServerKeyExchange: signer created, updating", .{}); | |
| 319 | signer.update(sign_data); | |
| 320 | log.info("TLS 1.2 ServerKeyExchange: finalizing signature", .{}); | |
| 321 | const sig = try signer.finalize(); | |
| 322 | log.info("TLS 1.2 ServerKeyExchange: converting to DER", .{}); | |
| 323 | var buf: [Ecdsa.Signature.der_encoded_length_max]u8 = undefined; | |
| 324 | break :brk sig.toDer(&buf); | |
| 325 | }, | |
| 326 | inline .rsa_pss_rsae_sha256, | |
| 327 | .rsa_pss_rsae_sha384, | |
| 328 | .rsa_pss_rsae_sha512, | |
| 329 | => |comptime_scheme| brk: { | |
| 330 | const Hash = common.SchemeHash(comptime_scheme); | |
| 331 | var signer = try auth.key.key.rsa.signerOaep(Hash, null); | |
| 332 | signer.update(sign_data); | |
| 333 | var buf: [512]u8 = undefined; | |
| 334 | const sig = try signer.finalize(&buf); | |
| 335 | break :brk sig.bytes; | |
| 336 | }, | |
| 337 | else => return error.TlsUnknownSignatureScheme, | |
| 338 | }; | |
| 339 | ||
| 340 | try hw.int(u16, signature.len); | |
| 341 | try hw.slice(signature); | |
| 342 | ||
| 343 | // Write handshake header | |
| 344 | var header_w = hw.writerAt(content_start); | |
| 345 | try header_w.handshakeRecordHeader(.server_key_exchange, hw.pos() - content_start - 4); | |
| 346 | } | |
| 347 | ||
| 348 | /// TLS 1.2 client flight 2: ClientKeyExchange, ChangeCipherSpec, Finished | |
| 349 | fn clientFlight2Tls12(h: *Self, _: Options) !void { | |
| 350 | log.info("TLS 1.2 clientFlight2Tls12: starting", .{}); | |
| 351 | // Read ClientKeyExchange | |
| 352 | { | |
| 353 | var d = Record.decoder(h.input) catch |err| { | |
| 354 | log.err("TLS 1.2 clientFlight2Tls12: Record.decoder failed: {}", .{err}); | |
| 355 | return err; | |
| 356 | }; | |
| 357 | log.info("TLS 1.2 clientFlight2Tls12: got record, content_type={x}", .{@backingInt(d.content_type)}); | |
| 358 | d.expectContentType(.handshake) catch |err| { | |
| 359 | log.err("TLS 1.2 clientFlight2Tls12: expected handshake but got content_type={x}, payload_len={d}", .{ @backingInt(d.content_type), d.payload.len }); | |
| 360 | if (d.content_type == .alert and d.payload.len >= 2) { | |
| 361 | log.err("TLS 1.2 clientFlight2Tls12: client sent alert: level={d}, desc={d}", .{ d.payload[0], d.payload[1] }); | |
| 362 | } | |
| 363 | return err; | |
| 364 | }; | |
| 365 | h.transcript.update(d.payload); | |
| 366 | ||
| 367 | const handshake_type = try d.decode(proto.Handshake); | |
| 368 | log.info("TLS 1.2 clientFlight2Tls12: handshake_type={x} (decimal={d}), expected client_key_exchange={x}", .{ @backingInt(handshake_type), @backingInt(handshake_type), @backingInt(proto.Handshake.client_key_exchange) }); | |
| 369 | if (handshake_type != .client_key_exchange) { | |
| 370 | log.err("TLS 1.2 clientFlight2Tls12: handshake type mismatch! got={s}, expected=client_key_exchange", .{@tagName(handshake_type)}); | |
| 371 | return error.TlsUnexpectedMessage; | |
| 372 | } | |
| 373 | log.info("TLS 1.2 clientFlight2Tls12: handshake type check passed", .{}); | |
| 374 | const length = try d.decode(u24); | |
| 375 | log.info("TLS 1.2 clientFlight2Tls12: ClientKeyExchange length={d}", .{length}); | |
| 376 | ||
| 377 | // For ECDHE, client sends its public key | |
| 378 | if (h.cipher_suite.keyExchange() == .ecdhe) { | |
| 379 | const client_pub_key_len = try d.decode(u8); | |
| 380 | log.info("TLS 1.2 clientFlight2Tls12: client pub key len={d}, expected={d}", .{ client_pub_key_len, length - 1 }); | |
| 381 | if (client_pub_key_len != length - 1) return error.TlsDecodeError; | |
| 382 | h.client_pub_key = try common.dupe(&h.client_pub_key_buf, try d.slice(client_pub_key_len)); | |
| 383 | log.info("TLS 1.2 clientFlight2Tls12: got client pub key, len={d}", .{h.client_pub_key.len}); | |
| 384 | } else { | |
| 385 | // RSA key exchange - skip for now | |
| 386 | try d.skip(length); | |
| 387 | } | |
| 388 | } | |
| 389 | log.info("TLS 1.2 clientFlight2Tls12: finished reading ClientKeyExchange", .{}); | |
| 390 | ||
| 391 | // Generate pre-master secret and derive keys | |
| 392 | if (h.cipher_suite.keyExchange() == .ecdhe) { | |
| 393 | log.info("TLS 1.2 clientFlight2Tls12: generating shared key, named_group={x}", .{@backingInt(h.named_group)}); | |
| 394 | const pre_master_secret = try h.dh_kp.sharedKey(h.named_group, h.client_pub_key); | |
| 395 | log.info("TLS 1.2 clientFlight2Tls12: got pre_master_secret, len={d}", .{pre_master_secret.len}); | |
| 396 | h.transcript.masterSecret(&h.master_secret, pre_master_secret, h.client_random, h.server_random); | |
| 397 | h.transcript.keyMaterial(&h.key_material, &h.master_secret, h.client_random, h.server_random); | |
| 398 | log.info("TLS 1.2 clientFlight2Tls12: derived keys", .{}); | |
| 399 | } | |
| 400 | ||
| 401 | // Initialize cipher for decryption | |
| 402 | log.info("TLS 1.2 clientFlight2Tls12: initializing cipher", .{}); | |
| 403 | h.cipher = try Cipher.initTls12(h.cipher_suite, &h.key_material, .server); | |
| 404 | log.info("TLS 1.2 clientFlight2Tls12: cipher initialized", .{}); | |
| 405 | ||
| 406 | // Read ChangeCipherSpec | |
| 407 | log.info("TLS 1.2 clientFlight2Tls12: reading ChangeCipherSpec", .{}); | |
| 408 | { | |
| 409 | var d = try Record.decoder(h.input); | |
| 410 | log.info("TLS 1.2 clientFlight2Tls12: got record, content_type={x}", .{@backingInt(d.content_type)}); | |
| 411 | try d.expectContentType(.change_cipher_spec); | |
| 412 | log.info("TLS 1.2 clientFlight2Tls12: ChangeCipherSpec payload_len={d}, payload[0]={d}", .{ d.payload.len, if (d.payload.len > 0) d.payload[0] else 255 }); | |
| 413 | if (d.payload.len != 1 or d.payload[0] != 1) return error.TlsUnexpectedMessage; | |
| 414 | } | |
| 415 | log.info("TLS 1.2 clientFlight2Tls12: ChangeCipherSpec OK", .{}); | |
| 416 | ||
| 417 | // Read encrypted Finished | |
| 418 | // In TLS 1.2, the encrypted Finished message uses content_type=handshake (0x16) | |
| 419 | // The encryption is applied to the payload, but the record header type stays as handshake | |
| 420 | log.info("TLS 1.2 clientFlight2Tls12: reading encrypted Finished", .{}); | |
| 421 | { | |
| 422 | const rec = try Record.read(h.input); | |
| 423 | log.info("TLS 1.2 clientFlight2Tls12: got record, content_type={x}", .{@backingInt(rec.content_type)}); | |
| 424 | // TLS 1.2 encrypted Finished uses handshake (0x16) content type, NOT application_data | |
| 425 | if (rec.content_type != .handshake) { | |
| 426 | log.err("TLS 1.2 clientFlight2Tls12: expected handshake, got {x}", .{@backingInt(rec.content_type)}); | |
| 427 | return error.TlsUnexpectedMessage; | |
| 428 | } | |
| 429 | ||
| 430 | var cleartext_buf: [128]u8 = undefined; | |
| 431 | // For TLS 1.2, decrypt returns the record's content_type (which is handshake) | |
| 432 | const content_type, const cleartext = try h.cipher.decrypt(&cleartext_buf, rec); | |
| 433 | log.info("TLS 1.2 clientFlight2Tls12: decrypted, content_type={x}, cleartext_len={d}", .{ @backingInt(content_type), cleartext.len }); | |
| 434 | if (content_type != .handshake) return error.TlsUnexpectedMessage; | |
| 435 | ||
| 436 | // Parse Finished message | |
| 437 | var d = record.Decoder.init(content_type, cleartext); | |
| 438 | const handshake_type = try d.decode(proto.Handshake); | |
| 439 | if (handshake_type != .finished) return error.TlsUnexpectedMessage; | |
| 440 | const length = try d.decode(u24); | |
| 441 | if (length != 12) return error.TlsDecodeError; | |
| 442 | ||
| 443 | const client_verify_data = try d.slice(12); | |
| 444 | const expected_verify_data = h.transcript.clientFinishedTls12(&h.master_secret); | |
| 445 | if (!mem.eql(u8, client_verify_data, &expected_verify_data)) | |
| 446 | return error.TlsDecryptError; | |
| 447 | ||
| 448 | // Update transcript with Finished message | |
| 449 | h.transcript.update(cleartext); | |
| 450 | } | |
| 451 | ||
| 452 | // Send ChangeCipherSpec and Finished | |
| 453 | { | |
| 454 | var w: record.Writer = .initFromIo(h.output); | |
| 455 | ||
| 456 | // ChangeCipherSpec | |
| 457 | try w.record(.change_cipher_spec, &[_]u8{1}); | |
| 458 | ||
| 459 | // Finished (encrypted) | |
| 460 | const server_finished = &record.handshakeHeader(.finished, 12) ++ | |
| 461 | h.transcript.serverFinishedTls12(&h.master_secret); | |
| 462 | const ciphertext = try h.cipher.encrypt(w.unused(), .handshake, server_finished); | |
| 463 | w.advance(ciphertext.len); | |
| 464 | ||
| 465 | h.output.advance(w.buffered().len); | |
| 466 | try h.output.flush(); | |
| 467 | } | |
| 468 | } | |
| 469 | ||
| 470 | fn clientFlight2(h: *Self, opt: Options) !void { | |
| 471 | // calculate application cipher before updating transcript in readClientFlight2 | |
| 472 | const application_secret = h.transcript.applicationSecret(); | |
| 473 | const app_cipher = try Cipher.initTls13(h.cipher_suite, application_secret, .server); | |
| 474 | // set application cipher instead of EndOfStream error | |
| 475 | h.readClientFlight2(opt) catch |err| { | |
| 476 | if (err != error.EndOfStream and err != error.InputBufferUndersize) { | |
| 477 | // don't change on short reads: https://github.com/ianic/tls.zig/commit/2f3f23485e01e4be8219c4a1ceda01ed961da61d | |
| 478 | h.cipher = app_cipher; | |
| 479 | } | |
| 480 | return err; | |
| 481 | }; | |
| 482 | h.cipher = app_cipher; | |
| 483 | } | |
| 484 | ||
| 485 | fn serverFlight(h: *Self, opt: Options) !void { | |
| 486 | var w: record.Writer = .initFromIo(h.output); | |
| 487 | ||
| 488 | const shared_key = brk: { | |
| 489 | var seed: [DhKeyPair.seed_len]u8 = undefined; | |
| 490 | rng.fill(&seed); | |
| 491 | var kp = try DhKeyPair.init(seed, &[_]proto.NamedGroup{h.named_group}); | |
| 492 | h.server_pub_key = try common.dupe(&h.server_pub_key_buf, try kp.publicKey(h.named_group)); | |
| 493 | break :brk try kp.sharedKey(h.named_group, h.client_pub_key); | |
| 494 | }; | |
| 495 | { | |
| 496 | const hello = try h.makeServerHello(&w); | |
| 497 | h.transcript.update(hello[record.header_len..]); | |
| 498 | } | |
| 499 | { | |
| 500 | const handshake_secret = h.transcript.handshakeSecret(shared_key); | |
| 501 | h.cipher = try Cipher.initTls13(h.cipher_suite, handshake_secret, .server); | |
| 502 | } | |
| 503 | try w.record(.change_cipher_spec, &[_]u8{1}); | |
| 504 | { | |
| 505 | var hw = try w.writerAdvance(record.header_len); | |
| 506 | try hw.handshakeRecord(.encrypted_extensions, &[_]u8{ 0, 0 }); | |
| 507 | h.transcript.update(hw.buffered()); | |
| 508 | try h.writeEncrypted(&w, hw.buffered()); | |
| 509 | } | |
| 510 | if (opt.client_auth) |_| { // Certificate request | |
| 511 | var hw = try w.writerAdvance(record.header_len); | |
| 512 | try makeCertificateRequest(&hw); | |
| 513 | h.transcript.update(hw.buffered()); | |
| 514 | try h.writeEncrypted(&w, hw.buffered()); | |
| 515 | } | |
| 516 | if (opt.auth) |auth| { | |
| 517 | const cb = CertificateBuilder{ | |
| 518 | .cert_key_pair = auth, | |
| 519 | .transcript = &h.transcript, | |
| 520 | .side = .server, | |
| 521 | }; | |
| 522 | { // Certificate | |
| 523 | var hw = try w.writerAdvance(record.header_len); | |
| 524 | try cb.makeCertificate(&hw); | |
| 525 | h.transcript.update(hw.buffered()); | |
| 526 | try h.writeEncrypted(&w, hw.buffered()); | |
| 527 | } | |
| 528 | { // Certificate verify | |
| 529 | var hw = try w.writerAdvance(record.header_len); | |
| 530 | try cb.makeCertificateVerify(&hw); | |
| 531 | h.transcript.update(hw.buffered()); | |
| 532 | try h.writeEncrypted(&w, hw.buffered()); | |
| 533 | } | |
| 534 | } | |
| 535 | { // Finished | |
| 536 | var hw = try w.writerAdvance(record.header_len); | |
| 537 | try hw.handshakeRecord(.finished, h.transcript.serverFinishedTls13()); | |
| 538 | h.transcript.update(hw.buffered()); | |
| 539 | try h.writeEncrypted(&w, hw.buffered()); | |
| 540 | } | |
| 541 | ||
| 542 | h.output.advance(w.buffered().len); | |
| 543 | } | |
| 544 | ||
| 545 | fn readClientFlight2(h: *Self, opt: Options) !void { | |
| 546 | // buffer for decrypted handshake records | |
| 547 | var cleartext_buffer: [max_cleartext_len]u8 = undefined; | |
| 548 | // cleartext writer | |
| 549 | var cw = Io.Writer.fixed(&cleartext_buffer); | |
| 550 | ||
| 551 | var handshake_state: proto.Handshake = .finished; | |
| 552 | var crt_parser: CertificateParser = undefined; | |
| 553 | if (opt.client_auth) |client_auth| { | |
| 554 | crt_parser = .{ .root_ca = client_auth.root_ca, .host = "" }; | |
| 555 | handshake_state = .certificate; | |
| 556 | } | |
| 557 | ||
| 558 | outer: while (true) { | |
| 559 | const rec = try Record.read(h.input); | |
| 560 | if (rec.protocol_version != .tls_1_2 and rec.content_type != .alert) | |
| 561 | return error.TlsProtocolVersion; | |
| 562 | ||
| 563 | switch (rec.content_type) { | |
| 564 | .change_cipher_spec => { | |
| 565 | if (rec.payload.len != 1) return error.TlsUnexpectedMessage; | |
| 566 | }, | |
| 567 | .application_data => { | |
| 568 | const content_type, const cleartext = try h.cipher.decrypt(cw.unusedCapacitySlice(), rec); | |
| 569 | cw.advance(cleartext.len); | |
| 570 | ||
| 571 | var d = record.Decoder.init(content_type, cw.buffered()); | |
| 572 | try d.expectContentType(.handshake); | |
| 573 | while (!d.eof()) { | |
| 574 | const handshake_type = try d.decode(proto.Handshake); | |
| 575 | const length = try d.decode(u24); | |
| 576 | ||
| 577 | if (length > max_cleartext_len) | |
| 578 | return error.TlsRecordOverflow; | |
| 579 | if (length > d.rest().len) | |
| 580 | continue :outer; // fragmented handshake into multiple records | |
| 581 | ||
| 582 | defer { | |
| 583 | h.transcript.update(d.payload[0..d.idx]); | |
| 584 | _ = cw.consume(d.idx); | |
| 585 | d = record.Decoder.init(content_type, cw.buffered()); | |
| 586 | } | |
| 587 | ||
| 588 | if (handshake_state != handshake_type) | |
| 589 | return error.TlsUnexpectedMessage; | |
| 590 | ||
| 591 | switch (handshake_type) { | |
| 592 | .certificate => { | |
| 593 | if (length == 4) { | |
| 594 | // got empty certificate message | |
| 595 | if (opt.client_auth.?.auth_type == .require) | |
| 596 | return error.TlsCertificateRequired; | |
| 597 | try d.skip(length); | |
| 598 | handshake_state = .finished; | |
| 599 | } else { | |
| 600 | try crt_parser.parseCertificate(&d, .tls_1_3); | |
| 601 | handshake_state = .certificate_verify; | |
| 602 | } | |
| 603 | }, | |
| 604 | .certificate_verify => { | |
| 605 | try crt_parser.parseCertificateVerify(&d); | |
| 606 | crt_parser.verifySignature(h.transcript.clientCertificateVerify()) catch |err| return switch (err) { | |
| 607 | error.TlsUnknownSignatureScheme => error.TlsIllegalParameter, | |
| 608 | else => error.TlsDecryptError, | |
| 609 | }; | |
| 610 | handshake_state = .finished; | |
| 611 | }, | |
| 612 | .finished => { | |
| 613 | const actual = try d.slice(length); | |
| 614 | const expected = h.transcript.clientFinishedTls13(); | |
| 615 | if (!mem.eql(u8, expected, actual)) | |
| 616 | return if (expected.len == actual.len) | |
| 617 | error.TlsDecryptError | |
| 618 | else | |
| 619 | error.TlsDecodeError; | |
| 620 | return; | |
| 621 | }, | |
| 622 | else => return error.TlsUnexpectedMessage, | |
| 623 | } | |
| 624 | } | |
| 625 | }, | |
| 626 | .alert => { | |
| 627 | var d = record.Decoder.init(rec.content_type, rec.payload); | |
| 628 | return d.raiseAlert(); | |
| 629 | }, | |
| 630 | else => return error.TlsUnexpectedMessage, | |
| 631 | } | |
| 632 | } | |
| 633 | } | |
| 634 | ||
| 635 | /// Write encrypted handshake message into `w` Cleartext and write buffer | |
| 636 | /// `w.unused()` are reusing same buffer. Cleartext is written 5 bytes ahead | |
| 637 | /// (record header len) from w.unused() position to avoid memcopy in the | |
| 638 | /// encrypt. Encrypt will add tls record head in first 5 bytes, encrypt | |
| 639 | /// cleartext and add hmac at end. | |
| 640 | fn writeEncrypted(h: *Self, w: *record.Writer, cleartext: []const u8) !void { | |
| 641 | const ciphertext = try h.cipher.encrypt(w.unused(), .handshake, cleartext); | |
| 642 | w.advance(ciphertext.len); | |
| 643 | } | |
| 644 | ||
| 645 | fn makeServerHello(h: *Self, w: *record.Writer) ![]const u8 { | |
| 646 | const header_pos = try w.skip(9); | |
| 647 | ||
| 648 | try w.enumValue(proto.Version.tls_1_2); | |
| 649 | try w.slice(&h.server_random); | |
| 650 | { | |
| 651 | try w.int(u8, h.legacy_session_id.len); | |
| 652 | if (h.legacy_session_id.len > 0) try w.slice(h.legacy_session_id); | |
| 653 | } | |
| 654 | try w.enumValue(h.cipher_suite); | |
| 655 | try w.slice(&[_]u8{0}); // compression method | |
| 656 | ||
| 657 | const ext_len_pos = try w.skip(2); // extensions length placeholder writer | |
| 658 | { // supported versions extension | |
| 659 | try w.enumValue(proto.Extension.supported_versions); | |
| 660 | try w.int(u16, 2); | |
| 661 | try w.enumValue(proto.Version.tls_1_3); | |
| 662 | } | |
| 663 | { // key share extension | |
| 664 | const key_len: u16 = @intCast(h.server_pub_key.len); | |
| 665 | try w.enumValue(proto.Extension.key_share); | |
| 666 | try w.int(u16, key_len + 4); | |
| 667 | try w.enumValue(h.named_group); | |
| 668 | try w.int(u16, key_len); | |
| 669 | try w.slice(h.server_pub_key); | |
| 670 | } | |
| 671 | var ew = w.writerAt(ext_len_pos); | |
| 672 | try ew.int(u16, w.pos() - ext_len_pos - 2); | |
| 673 | var hw = w.writerAt(header_pos); | |
| 674 | try hw.recordHeader(.handshake, w.pos() - 5); | |
| 675 | try hw.handshakeRecordHeader(.server_hello, w.pos() - 9); | |
| 676 | ||
| 677 | return w.buffered(); | |
| 678 | } | |
| 679 | ||
| 680 | fn makeCertificateRequest(w: *record.Writer) !void { | |
| 681 | const header_pos = try w.skip(4 + 1 + 2); | |
| 682 | const ext_head = w.pos(); | |
| 683 | try w.extension(.signature_algorithms, common.supported_signature_algorithms); | |
| 684 | const ext_len = w.pos() - ext_head; | |
| 685 | var hw = w.writerAt(header_pos); | |
| 686 | try hw.handshakeRecordHeader(.certificate_request, ext_len + 3); | |
| 687 | try hw.int(u8, 0); // certificate request context length = 0 | |
| 688 | try hw.int(u16, ext_len); // extensions length | |
| 689 | } | |
| 690 | ||
| 691 | fn readClientHello(h: *Self, supported_cipher_suites: []const CipherSuite, server_named_groups: []const proto.NamedGroup) !void { | |
| 692 | log.info("readClientHello: starting to parse ClientHello", .{}); | |
| 693 | var d = try Record.decoder(h.input); | |
| 694 | if (d.payload.len > max_cleartext_len) return error.TlsRecordOverflow; | |
| 695 | try d.expectContentType(.handshake); | |
| 696 | h.transcript.update(d.payload); | |
| 697 | ||
| 698 | const handshake_type = try d.decode(proto.Handshake); | |
| 699 | if (handshake_type != .client_hello) return error.TlsUnexpectedMessage; | |
| 700 | _ = try d.decode(u24); // handshake length | |
| 701 | if (try d.decode(proto.Version) != .tls_1_2) return error.TlsProtocolVersion; | |
| 702 | ||
| 703 | h.client_random = try d.array(32); | |
| 704 | { // legacy session id | |
| 705 | const len = try d.decode(u8); | |
| 706 | h.legacy_session_id = try common.dupe(&h.legacy_session_id_buf, try d.slice(len)); | |
| 707 | } | |
| 708 | // Track cipher suite candidates for both TLS versions separately | |
| 709 | // This is needed because we don't know which TLS version will be used yet | |
| 710 | var tls13_cipher: CipherSuite = @fromBackingInt(@intCast(0)); | |
| 711 | var tls12_cipher: CipherSuite = @fromBackingInt(@intCast(0)); | |
| 712 | { // cipher suites | |
| 713 | const end_idx = try d.decode(u16) + d.idx; | |
| 714 | ||
| 715 | while (d.idx < end_idx) { | |
| 716 | const cipher_suite = try d.decode(CipherSuite); | |
| 717 | if (cipher_suites.includes(supported_cipher_suites, cipher_suite)) { | |
| 718 | // Track TLS 1.3 and TLS 1.2 ciphers separately | |
| 719 | if (cipher_suites.includes(cipher_suites.tls13, cipher_suite)) { | |
| 720 | if (@backingInt(tls13_cipher) == 0) { | |
| 721 | tls13_cipher = cipher_suite; | |
| 722 | } | |
| 723 | } else if (cipher_suites.includes(cipher_suites.tls12, cipher_suite)) { | |
| 724 | if (@backingInt(tls12_cipher) == 0) { | |
| 725 | tls12_cipher = cipher_suite; | |
| 726 | } | |
| 727 | } | |
| 728 | } | |
| 729 | } | |
| 730 | // Must have at least one supported cipher | |
| 731 | if (@backingInt(tls13_cipher) == 0 and @backingInt(tls12_cipher) == 0) | |
| 732 | return error.TlsNoSupportedCiphers; | |
| 733 | } | |
| 734 | log.info("readClientHello: tls13_cipher={x}, tls12_cipher={x}", .{ @backingInt(tls13_cipher), @backingInt(tls12_cipher) }); | |
| 735 | try d.skip(2); // compression methods | |
| 736 | ||
| 737 | var key_share_received = false; | |
| 738 | var tls_1_3_supported = false; | |
| 739 | var supported_groups_buf: [16]proto.NamedGroup = undefined; | |
| 740 | var supported_groups_len: usize = 0; | |
| 741 | ||
| 742 | // extensions | |
| 743 | const extensions_end_idx = try d.decode(u16) + d.idx; | |
| 744 | while (d.idx < extensions_end_idx) { | |
| 745 | const extension_type = try d.decode(proto.Extension); | |
| 746 | const extension_len = try d.decode(u16); | |
| 747 | ||
| 748 | switch (extension_type) { | |
| 749 | .supported_versions => { | |
| 750 | const end_idx = try d.decode(u8) + d.idx; | |
| 751 | while (d.idx < end_idx) { | |
| 752 | const version = try d.decode(proto.Version); | |
| 753 | if (version == proto.Version.tls_1_3) { | |
| 754 | tls_1_3_supported = true; | |
| 755 | } | |
| 756 | } | |
| 757 | }, | |
| 758 | .key_share => { | |
| 759 | if (extension_len == 0) return error.TlsDecodeError; | |
| 760 | key_share_received = true; | |
| 761 | var selected_named_group_idx = supported_named_groups.len; | |
| 762 | const end_idx = try d.decode(u16) + d.idx; | |
| 763 | while (d.idx < end_idx) { | |
| 764 | const named_group = try d.decode(proto.NamedGroup); | |
| 765 | switch (@backingInt(named_group)) { | |
| 766 | 0x0001...0x0016, | |
| 767 | 0x001a...0x001c, | |
| 768 | 0xff01...0xff02, | |
| 769 | => return error.TlsIllegalParameter, | |
| 770 | else => {}, | |
| 771 | } | |
| 772 | const client_pub_key = try d.slice(try d.decode(u16)); | |
| 773 | for (supported_named_groups, 0..) |supported, idx| { | |
| 774 | if (named_group == supported and idx < selected_named_group_idx) { | |
| 775 | h.named_group = named_group; | |
| 776 | h.client_pub_key = try common.dupe(&h.client_pub_key_buf, client_pub_key); | |
| 777 | selected_named_group_idx = idx; | |
| 778 | } | |
| 779 | } | |
| 780 | } | |
| 781 | if (@backingInt(h.named_group) == 0) | |
| 782 | return error.TlsIllegalParameter; | |
| 783 | }, | |
| 784 | .supported_groups => { | |
| 785 | const end_idx = try d.decode(u16) + d.idx; | |
| 786 | while (d.idx < end_idx) { | |
| 787 | const named_group = try d.decode(proto.NamedGroup); | |
| 788 | switch (@backingInt(named_group)) { | |
| 789 | // Deprecated/insecure curves - skip silently for TLS 1.2 compatibility | |
| 790 | // Some clients (including Apple SecureTransport) may include these | |
| 791 | 0x0001...0x0016, | |
| 792 | 0x001a...0x001c, | |
| 793 | 0xff01...0xff02, | |
| 794 | => {}, | |
| 795 | else => { | |
| 796 | // Store supported groups for TLS 1.2 fallback | |
| 797 | if (supported_groups_len < supported_groups_buf.len) { | |
| 798 | supported_groups_buf[supported_groups_len] = named_group; | |
| 799 | supported_groups_len += 1; | |
| 800 | } | |
| 801 | }, | |
| 802 | } | |
| 803 | } | |
| 804 | }, | |
| 805 | .signature_algorithms => { | |
| 806 | if (@backingInt(h.signature_scheme) == 0) { | |
| 807 | try d.skip(extension_len); | |
| 808 | } else { | |
| 809 | var found = false; | |
| 810 | const list_len = try d.decode(u16); | |
| 811 | if (list_len == 0) return error.TlsDecodeError; | |
| 812 | const end_idx = list_len + d.idx; | |
| 813 | while (d.idx < end_idx) { | |
| 814 | const signature_scheme = try d.decode(proto.SignatureScheme); | |
| 815 | if (signature_scheme == h.signature_scheme) found = true; | |
| 816 | } | |
| 817 | if (!found) return error.TlsHandshakeFailure; | |
| 818 | } | |
| 819 | }, | |
| 820 | else => { | |
| 821 | try d.skip(extension_len); | |
| 822 | }, | |
| 823 | } | |
| 824 | } | |
| 825 | ||
| 826 | // Determine TLS version based on client capabilities | |
| 827 | log.info("readClientHello: tls_1_3_supported={}, key_share_received={}, supported_groups_len={}", .{ tls_1_3_supported, key_share_received, supported_groups_len }); | |
| 828 | if (tls_1_3_supported and key_share_received and @backingInt(tls13_cipher) != 0) { | |
| 829 | // TLS 1.3 handshake - requires TLS 1.3 cipher | |
| 830 | h.tls_version = .tls_1_3; | |
| 831 | h.cipher_suite = tls13_cipher; | |
| 832 | if (@backingInt(h.named_group) == 0) return error.TlsIllegalParameter; | |
| 833 | } else if (@backingInt(tls12_cipher) != 0) { | |
| 834 | // TLS 1.2 handshake - need to select named group from supported_groups | |
| 835 | h.tls_version = .tls_1_2; | |
| 836 | h.cipher_suite = tls12_cipher; | |
| 837 | log.info("readClientHello: TLS 1.2 path, cipher keyExchange={}", .{h.cipher_suite.keyExchange()}); | |
| 838 | ||
| 839 | // For TLS 1.2 ECDHE, select a named group from supported_groups extension | |
| 840 | if (h.cipher_suite.keyExchange() == .ecdhe) { | |
| 841 | // If no supported_groups extension was received, default to common curves | |
| 842 | // per RFC 8422 - most TLS 1.2 clients support secp256r1/secp384r1 | |
| 843 | const client_groups = if (supported_groups_len > 0) | |
| 844 | supported_groups_buf[0..supported_groups_len] | |
| 845 | else | |
| 846 | &[_]proto.NamedGroup{ .secp256r1, .secp384r1 }; | |
| 847 | ||
| 848 | log.info("readClientHello: client_groups count={}, server_named_groups count={}", .{ client_groups.len, server_named_groups.len }); | |
| 849 | ||
| 850 | // Find first matching named group between server and client | |
| 851 | for (server_named_groups) |server_ng| { | |
| 852 | for (client_groups) |client_ng| { | |
| 853 | if (server_ng == client_ng) { | |
| 854 | h.named_group = server_ng; | |
| 855 | break; | |
| 856 | } | |
| 857 | } | |
| 858 | if (@backingInt(h.named_group) != 0) break; | |
| 859 | } | |
| 860 | if (@backingInt(h.named_group) == 0) return error.TlsIllegalParameter; | |
| 861 | } | |
| 862 | log.info("TLS 1.2 readClientHello: selected named_group={x}", .{@backingInt(h.named_group)}); | |
| 863 | } else { | |
| 864 | // No compatible cipher suite found | |
| 865 | return error.TlsNoSupportedCiphers; | |
| 866 | } | |
| 867 | log.info("readClientHello complete: tls_version={}, cipher_suite={x}, named_group={x}", .{ h.tls_version, @backingInt(h.cipher_suite), @backingInt(h.named_group) }); | |
| 868 | } | |
| 869 | }; | |
| 870 | ||
| 871 | const testing = std.testing; | |
| 872 | const data13 = ; | |
| 873 | const testu = ; | |
| 874 | ||
| 875 | test "read client hello" { | |
| 876 | var reader: Io.Reader = .fixed(&data13.client_hello); | |
| 877 | var h: Handshake = .{ | |
| 878 | .input = &reader, | |
| 879 | .output = undefined, | |
| 880 | }; | |
| 881 | h.signature_scheme = .ecdsa_secp521r1_sha512; // this must be supported in signature_algorithms extension | |
| 882 | try h.readClientHello(cipher_suites.tls13, &[_]proto.NamedGroup{ .x25519, .secp256r1, .secp384r1 }); | |
| 883 | ||
| 884 | try testing.expectEqual(CipherSuite.AES_256_GCM_SHA384, h.cipher_suite); | |
| 885 | try testing.expectEqual(.x25519, h.named_group); | |
| 886 | try testing.expectEqualSlices(u8, &data13.client_random, &h.client_random); | |
| 887 | try testing.expectEqualSlices(u8, &data13.client_public_key, h.client_pub_key); | |
| 888 | } | |
| 889 | ||
| 890 | test "make server hello" { | |
| 891 | var h: Handshake = .{ .input = undefined, .output = undefined }; | |
| 892 | ||
| 893 | h.cipher_suite = .AES_256_GCM_SHA384; | |
| 894 | testu.fillFrom(&h.server_random, 0); | |
| 895 | testu.fillFrom(&h.server_pub_key_buf, 0x20); | |
| 896 | h.named_group = .x25519; | |
| 897 | h.server_pub_key = h.server_pub_key_buf[0..32]; | |
| 898 | ||
| 899 | const expected = &testu.hexToBytes( | |
| 900 | \\ 16 03 03 00 5a 02 00 00 56 | |
| 901 | \\ 03 03 | |
| 902 | \\ 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f | |
| 903 | \\ 00 | |
| 904 | \\ 13 02 00 | |
| 905 | \\ 00 2e 00 2b 00 02 03 04 | |
| 906 | \\ 00 33 00 24 00 1d 00 20 | |
| 907 | \\ 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f | |
| 908 | ); | |
| 909 | ||
| 910 | var buffer: [128]u8 = undefined; | |
| 911 | var w: record.Writer = .init(&buffer); | |
| 912 | const actual = try h.makeServerHello(&w); | |
| 913 | try testing.expectEqual(95, actual.len); | |
| 914 | try testing.expectEqualSlices(u8, expected, actual); | |
| 915 | } | |
| 916 | ||
| 917 | test "make certificate request" { | |
| 918 | var buffer: [32]u8 = undefined; | |
| 919 | ||
| 920 | const expected = testu.hexToBytes("0d 00 00 1b" ++ // handshake header | |
| 921 | "00 00 18" ++ // extension length | |
| 922 | "00 0d" ++ // signature algorithms extension | |
| 923 | "00 14" ++ // extension length | |
| 924 | "00 12" ++ // list length 6 * 2 bytes | |
| 925 | "04 03 05 03 08 04 08 05 08 06 08 07 02 01 04 01 05 01" // signature schemes | |
| 926 | ); | |
| 927 | ||
| 928 | var w: record.Writer = .init(&buffer); | |
| 929 | try Handshake.makeCertificateRequest(&w); | |
| 930 | try testing.expectEqualSlices(u8, &expected, w.buffered()); | |
| 931 | } | |
| 932 | ||
| 933 | pub const NonBlock = struct { | |
| 934 | const Self = @This(); | |
| 935 | ||
| 936 | // inner sync handshake | |
| 937 | inner: Handshake = undefined, | |
| 938 | opt: Options = undefined, | |
| 939 | state: State = undefined, | |
| 940 | ||
| 941 | const State = enum { | |
| 942 | init, | |
| 943 | client_flight_1, | |
| 944 | server_flight, | |
| 945 | client_flight_2, | |
| 946 | ||
| 947 | fn next(self: *State) void { | |
| 948 | self.* = @fromBackingInt(@intCast(@backingInt(self.*) + 1)); | |
| 949 | } | |
| 950 | }; | |
| 951 | ||
| 952 | pub fn init(opt: Options) Self { | |
| 953 | var inner: Handshake = .{ | |
| 954 | .input = undefined, | |
| 955 | .output = undefined, | |
| 956 | }; | |
| 957 | inner.initKeys(opt); | |
| 958 | return .{ | |
| 959 | .opt = opt, | |
| 960 | .inner = inner, | |
| 961 | .state = .init, | |
| 962 | }; | |
| 963 | } | |
| 964 | ||
| 965 | fn recv(self: *Self) !void { | |
| 966 | const prev: Transcript = self.inner.transcript; | |
| 967 | errdefer self.inner.transcript = prev; | |
| 968 | ||
| 969 | switch (self.state) { | |
| 970 | .init => { | |
| 971 | try self.inner.clientFlight1(self.opt); | |
| 972 | self.state.next(); | |
| 973 | }, | |
| 974 | .server_flight => { | |
| 975 | // Use appropriate client flight based on TLS version | |
| 976 | if (self.inner.tls_version == .tls_1_2) { | |
| 977 | try self.inner.clientFlight2Tls12(self.opt); | |
| 978 | } else { | |
| 979 | try self.inner.clientFlight2(self.opt); | |
| 980 | } | |
| 981 | self.state.next(); | |
| 982 | }, | |
| 983 | else => return, | |
| 984 | } | |
| 985 | } | |
| 986 | ||
| 987 | /// True when handshake is successfully finished | |
| 988 | pub fn done(self: Self) bool { | |
| 989 | return self.state == .client_flight_2; | |
| 990 | } | |
| 991 | ||
| 992 | /// Runs next handshake step. | |
| 993 | pub fn run( | |
| 994 | self: *Self, | |
| 995 | /// Data received from the peer | |
| 996 | recv_buf: []const u8, | |
| 997 | /// Scratch buffer where data to be sent to the peer will be prepared | |
| 998 | send_buf: []u8, | |
| 999 | ) !struct { | |
| 1000 | /// Number of bytes consumed from recv_buf | |
| 1001 | recv_pos: usize, | |
| 1002 | /// Number of bytes prepared in send_buf | |
| 1003 | send_pos: usize, | |
| 1004 | /// Unused part of the recv_buf, | |
| 1005 | unused_recv: []const u8, | |
| 1006 | /// Part of the send_buf that should be sent to the peer | |
| 1007 | send: []const u8, | |
| 1008 | } { | |
| 1009 | if (self.done()) return .{ | |
| 1010 | .recv_pos = 0, | |
| 1011 | .send_pos = 0, | |
| 1012 | .unused_recv = &.{}, | |
| 1013 | .send = &.{}, | |
| 1014 | }; | |
| 1015 | ||
| 1016 | var reader: Io.Reader = .fixed(recv_buf); | |
| 1017 | self.inner.input = &reader; | |
| 1018 | var writer: Io.Writer = .fixed(send_buf); | |
| 1019 | self.inner.output = &writer; | |
| 1020 | ||
| 1021 | var recv_pos: usize = 0; | |
| 1022 | out: switch (self.state) { | |
| 1023 | .init, .server_flight => { | |
| 1024 | self.recv() catch |err| switch (err) { | |
| 1025 | error.EndOfStream, error.InputBufferUndersize => { | |
| 1026 | return .{ | |
| 1027 | .recv_pos = 0, | |
| 1028 | .send_pos = 0, | |
| 1029 | .unused_recv = recv_buf, | |
| 1030 | .send = &.{}, | |
| 1031 | }; | |
| 1032 | }, | |
| 1033 | else => return err, | |
| 1034 | }; | |
| 1035 | recv_pos = reader.seek; | |
| 1036 | continue :out self.state; | |
| 1037 | }, | |
| 1038 | .client_flight_1 => { | |
| 1039 | if (recv_buf.ptr == send_buf.ptr and recv_pos != recv_buf.len) { | |
| 1040 | // recv buffer is fully consumed, same buffer can be used for write | |
| 1041 | return error.TlsUnexpectedMessage; | |
| 1042 | } | |
| 1043 | // Use appropriate server flight based on TLS version | |
| 1044 | if (self.inner.tls_version == .tls_1_2) { | |
| 1045 | try self.inner.serverFlightTls12(self.opt); | |
| 1046 | } else { | |
| 1047 | try self.inner.serverFlight(self.opt); | |
| 1048 | } | |
| 1049 | self.state.next(); | |
| 1050 | }, | |
| 1051 | .client_flight_2 => { | |
| 1052 | // done | |
| 1053 | }, | |
| 1054 | } | |
| 1055 | ||
| 1056 | log.info("NonBlock run() returning: recv_pos={d}, writer.end={d}, send.len={d}", .{ recv_pos, writer.end, writer.buffered().len }); | |
| 1057 | return .{ | |
| 1058 | .recv_pos = recv_pos, | |
| 1059 | .send_pos = writer.end, | |
| 1060 | .unused_recv = recv_buf[recv_pos..], | |
| 1061 | .send = writer.buffered(), | |
| 1062 | }; | |
| 1063 | } | |
| 1064 | ||
| 1065 | /// Cipher produced in handshake, null until successful handshake. | |
| 1066 | pub fn cipher(self: Self) ?Cipher { | |
| 1067 | return if (self.done()) self.inner.cipher else null; | |
| 1068 | } | |
| 1069 | }; | |
| @@ -1,67 +0,0 @@ | ||
| 1 | //! Exporting tls key so we can share them with Wireshark and analyze decrypted | |
| 2 | //! traffic in Wireshark. | |
| 3 | //! To configure Wireshark to use exprted keys see curl reference. | |
| 4 | //! | |
| 5 | //! References: | |
| 6 | //! curl: https://everything.curl.dev/usingcurl/tls/sslkeylogfile.html | |
| 7 | //! openssl: https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_keylog_callback.html | |
| 8 | //! https://udn.realityripple.com/docs/Mozilla/Projects/NSS/Key_Log_Format | |
| 9 | ||
| 10 | const std = ; | |
| 11 | ||
| 12 | const key_log_file_env = "SSLKEYLOGFILE"; | |
| 13 | ||
| 14 | pub const label = struct { | |
| 15 | // tls 1.3 | |
| 16 | pub const client_handshake_traffic_secret: []const u8 = "CLIENT_HANDSHAKE_TRAFFIC_SECRET"; | |
| 17 | pub const server_handshake_traffic_secret: []const u8 = "SERVER_HANDSHAKE_TRAFFIC_SECRET"; | |
| 18 | pub const client_traffic_secret_0: []const u8 = "CLIENT_TRAFFIC_SECRET_0"; | |
| 19 | pub const server_traffic_secret_0: []const u8 = "SERVER_TRAFFIC_SECRET_0"; | |
| 20 | // tls 1.2 | |
| 21 | pub const client_random: []const u8 = "CLIENT_RANDOM"; | |
| 22 | }; | |
| 23 | ||
| 24 | pub const Callback = *const fn (label: []const u8, client_random: []const u8, secret: []const u8) void; | |
| 25 | ||
| 26 | /// Writes tls keys to the file pointed by SSLKEYLOGFILE environment variable. | |
| 27 | pub fn callback(label_: []const u8, client_random: []const u8, secret: []const u8) void { | |
| 28 | const builtin = ; | |
| 29 | const native_os = builtin.os.tag; | |
| 30 | const file_name: []const u8 = if (native_os == .windows) | |
| 31 | return | |
| 32 | else if (native_os == .wasi and !builtin.link_libc) | |
| 33 | return | |
| 34 | else | |
| 35 | std.posix.getenv(key_log_file_env) orelse return; | |
| 36 | fileAppend(file_name, label_, client_random, secret) catch return; | |
| 37 | } | |
| 38 | ||
| 39 | pub fn fileAppend(file_name: []const u8, label_: []const u8, client_random: []const u8, secret: []const u8) !void { | |
| 40 | var buf: [1024]u8 = undefined; | |
| 41 | const line = try formatLine(&buf, label_, client_random, secret); | |
| 42 | try fileWrite(file_name, line); | |
| 43 | } | |
| 44 | ||
| 45 | fn fileWrite(file_name: []const u8, line: []const u8) !void { | |
| 46 | var path_buf: [std.posix.PATH_MAX]u8 = undefined; | |
| 47 | if (file_name.len >= path_buf.len) return error.NameTooLong; | |
| 48 | @memcpy(path_buf[0..file_name.len], file_name); | |
| 49 | path_buf[file_name.len] = 0; | |
| 50 | const fd = try std.posix.open(path_buf[0..file_name.len :0], .{ .ACCMODE = .WRONLY, .CREAT = true, .APPEND = true }, 0o600); | |
| 51 | defer _ = std.c.close(fd); | |
| 52 | _ = try std.posix.write(fd, line); | |
| 53 | } | |
| 54 | ||
| 55 | pub fn formatLine(buf: []u8, label_: []const u8, client_random: []const u8, secret: []const u8) ![]const u8 { | |
| 56 | var w = std.Io.Writer.fixed(buf); | |
| 57 | try w.print("{s} ", .{label_}); | |
| 58 | for (client_random) |b| { | |
| 59 | try w.print("{x:0>2}", .{b}); | |
| 60 | } | |
| 61 | try w.writeByte(' '); | |
| 62 | for (secret) |b| { | |
| 63 | try w.print("{x:0>2}", .{b}); | |
| 64 | } | |
| 65 | try w.writeByte('\n'); | |
| 66 | return w.buffered(); | |
| 67 | } | |
| @@ -1,309 +0,0 @@ | ||
| 1 | pub const Version = enum(u16) { | |
| 2 | tls_1_2 = 0x0303, | |
| 3 | tls_1_3 = 0x0304, | |
| 4 | _, | |
| 5 | }; | |
| 6 | ||
| 7 | pub const ContentType = enum(u8) { | |
| 8 | invalid = 0, | |
| 9 | change_cipher_spec = 20, | |
| 10 | alert = 21, | |
| 11 | handshake = 22, | |
| 12 | application_data = 23, | |
| 13 | _, | |
| 14 | }; | |
| 15 | ||
| 16 | pub const Handshake = enum(u8) { | |
| 17 | client_hello = 1, | |
| 18 | server_hello = 2, | |
| 19 | new_session_ticket = 4, | |
| 20 | end_of_early_data = 5, | |
| 21 | encrypted_extensions = 8, | |
| 22 | certificate = 11, | |
| 23 | server_key_exchange = 12, | |
| 24 | certificate_request = 13, | |
| 25 | server_hello_done = 14, | |
| 26 | certificate_verify = 15, | |
| 27 | client_key_exchange = 16, | |
| 28 | finished = 20, | |
| 29 | key_update = 24, | |
| 30 | message_hash = 254, | |
| 31 | _, | |
| 32 | }; | |
| 33 | ||
| 34 | pub const Curve = enum(u8) { | |
| 35 | named_curve = 0x03, | |
| 36 | _, | |
| 37 | }; | |
| 38 | ||
| 39 | pub const KeyExchangeModes = enum(u8) { | |
| 40 | psk_ke = 0, | |
| 41 | psk_dhe_ke = 1, | |
| 42 | }; | |
| 43 | ||
| 44 | pub const Extension = enum(u16) { | |
| 45 | /// RFC 6066 | |
| 46 | server_name = 0, | |
| 47 | /// RFC 6066 | |
| 48 | max_fragment_length = 1, | |
| 49 | /// RFC 6066 | |
| 50 | status_request = 5, | |
| 51 | /// RFC 8422, 7919 | |
| 52 | supported_groups = 10, | |
| 53 | /// RFC 8446 | |
| 54 | signature_algorithms = 13, | |
| 55 | /// RFC 5764 | |
| 56 | use_srtp = 14, | |
| 57 | /// RFC 6520 | |
| 58 | heartbeat = 15, | |
| 59 | /// RFC 7301 | |
| 60 | application_layer_protocol_negotiation = 16, | |
| 61 | /// RFC 6962 | |
| 62 | signed_certificate_timestamp = 18, | |
| 63 | /// RFC 7250 | |
| 64 | client_certificate_type = 19, | |
| 65 | /// RFC 7250 | |
| 66 | server_certificate_type = 20, | |
| 67 | /// RFC 7685 | |
| 68 | padding = 21, | |
| 69 | /// RFC 8446 | |
| 70 | pre_shared_key = 41, | |
| 71 | /// RFC 8446 | |
| 72 | early_data = 42, | |
| 73 | /// RFC 8446 | |
| 74 | supported_versions = 43, | |
| 75 | /// RFC 8446 | |
| 76 | cookie = 44, | |
| 77 | /// RFC 8446 | |
| 78 | psk_key_exchange_modes = 45, | |
| 79 | /// RFC 8446 | |
| 80 | certificate_authorities = 47, | |
| 81 | /// RFC 8446 | |
| 82 | oid_filters = 48, | |
| 83 | /// RFC 8446 | |
| 84 | post_handshake_auth = 49, | |
| 85 | /// RFC 8446 | |
| 86 | signature_algorithms_cert = 50, | |
| 87 | /// RFC 8446 | |
| 88 | key_share = 51, | |
| 89 | ||
| 90 | _, | |
| 91 | }; | |
| 92 | ||
| 93 | pub fn alertFromError(err: anyerror) [2]u8 { | |
| 94 | return [2]u8{ @backingInt(Alert.Level.fatal), @backingInt(Alert.fromError(err)) }; | |
| 95 | } | |
| 96 | ||
| 97 | pub const Alert = enum(u8) { | |
| 98 | pub const Level = enum(u8) { | |
| 99 | warning = 1, | |
| 100 | fatal = 2, | |
| 101 | _, | |
| 102 | }; | |
| 103 | ||
| 104 | pub const Error = error{ | |
| 105 | TlsAlertUnexpectedMessage, | |
| 106 | TlsAlertBadRecordMac, | |
| 107 | TlsAlertRecordOverflow, | |
| 108 | TlsAlertHandshakeFailure, | |
| 109 | TlsAlertBadCertificate, | |
| 110 | TlsAlertUnsupportedCertificate, | |
| 111 | TlsAlertCertificateRevoked, | |
| 112 | TlsAlertCertificateExpired, | |
| 113 | TlsAlertCertificateUnknown, | |
| 114 | TlsAlertIllegalParameter, | |
| 115 | TlsAlertUnknownCa, | |
| 116 | TlsAlertAccessDenied, | |
| 117 | TlsAlertDecodeError, | |
| 118 | TlsAlertDecryptError, | |
| 119 | TlsAlertProtocolVersion, | |
| 120 | TlsAlertInsufficientSecurity, | |
| 121 | TlsAlertInternalError, | |
| 122 | TlsAlertInappropriateFallback, | |
| 123 | TlsAlertMissingExtension, | |
| 124 | TlsAlertUnsupportedExtension, | |
| 125 | TlsAlertUnrecognizedName, | |
| 126 | TlsAlertBadCertificateStatusResponse, | |
| 127 | TlsAlertUnknownPskIdentity, | |
| 128 | TlsAlertCertificateRequired, | |
| 129 | TlsAlertNoApplicationProtocol, | |
| 130 | TlsAlertUnknown, | |
| 131 | }; | |
| 132 | ||
| 133 | close_notify = 0, | |
| 134 | unexpected_message = 10, | |
| 135 | bad_record_mac = 20, | |
| 136 | record_overflow = 22, | |
| 137 | handshake_failure = 40, | |
| 138 | bad_certificate = 42, | |
| 139 | unsupported_certificate = 43, | |
| 140 | certificate_revoked = 44, | |
| 141 | certificate_expired = 45, | |
| 142 | certificate_unknown = 46, | |
| 143 | illegal_parameter = 47, | |
| 144 | unknown_ca = 48, | |
| 145 | access_denied = 49, | |
| 146 | decode_error = 50, | |
| 147 | decrypt_error = 51, | |
| 148 | protocol_version = 70, | |
| 149 | insufficient_security = 71, | |
| 150 | internal_error = 80, | |
| 151 | inappropriate_fallback = 86, | |
| 152 | user_canceled = 90, | |
| 153 | missing_extension = 109, | |
| 154 | unsupported_extension = 110, | |
| 155 | unrecognized_name = 112, | |
| 156 | bad_certificate_status_response = 113, | |
| 157 | unknown_psk_identity = 115, | |
| 158 | certificate_required = 116, | |
| 159 | no_application_protocol = 120, | |
| 160 | _, | |
| 161 | ||
| 162 | pub fn toError(alert: Alert) Error!void { | |
| 163 | return switch (alert) { | |
| 164 | .close_notify => {}, // not an error | |
| 165 | .unexpected_message => error.TlsAlertUnexpectedMessage, | |
| 166 | .bad_record_mac => error.TlsAlertBadRecordMac, | |
| 167 | .record_overflow => error.TlsAlertRecordOverflow, | |
| 168 | .handshake_failure => error.TlsAlertHandshakeFailure, | |
| 169 | .bad_certificate => error.TlsAlertBadCertificate, | |
| 170 | .unsupported_certificate => error.TlsAlertUnsupportedCertificate, | |
| 171 | .certificate_revoked => error.TlsAlertCertificateRevoked, | |
| 172 | .certificate_expired => error.TlsAlertCertificateExpired, | |
| 173 | .certificate_unknown => error.TlsAlertCertificateUnknown, | |
| 174 | .illegal_parameter => error.TlsAlertIllegalParameter, | |
| 175 | .unknown_ca => error.TlsAlertUnknownCa, | |
| 176 | .access_denied => error.TlsAlertAccessDenied, | |
| 177 | .decode_error => error.TlsAlertDecodeError, | |
| 178 | .decrypt_error => error.TlsAlertDecryptError, | |
| 179 | .protocol_version => error.TlsAlertProtocolVersion, | |
| 180 | .insufficient_security => error.TlsAlertInsufficientSecurity, | |
| 181 | .internal_error => error.TlsAlertInternalError, | |
| 182 | .inappropriate_fallback => error.TlsAlertInappropriateFallback, | |
| 183 | .user_canceled => {}, // not an error | |
| 184 | .missing_extension => error.TlsAlertMissingExtension, | |
| 185 | .unsupported_extension => error.TlsAlertUnsupportedExtension, | |
| 186 | .unrecognized_name => error.TlsAlertUnrecognizedName, | |
| 187 | .bad_certificate_status_response => error.TlsAlertBadCertificateStatusResponse, | |
| 188 | .unknown_psk_identity => error.TlsAlertUnknownPskIdentity, | |
| 189 | .certificate_required => error.TlsAlertCertificateRequired, | |
| 190 | .no_application_protocol => error.TlsAlertNoApplicationProtocol, | |
| 191 | _ => error.TlsAlertUnknown, | |
| 192 | }; | |
| 193 | } | |
| 194 | ||
| 195 | pub fn fromError(err: anyerror) Alert { | |
| 196 | return switch (err) { | |
| 197 | error.TlsUnexpectedMessage => .unexpected_message, | |
| 198 | error.TlsBadRecordMac => .bad_record_mac, | |
| 199 | error.TlsRecordOverflow => .record_overflow, | |
| 200 | error.TlsHandshakeFailure => .handshake_failure, | |
| 201 | error.TlsBadCertificate => .bad_certificate, | |
| 202 | error.TlsUnsupportedCertificate => .unsupported_certificate, | |
| 203 | error.TlsCertificateRevoked => .certificate_revoked, | |
| 204 | error.TlsCertificateExpired => .certificate_expired, | |
| 205 | error.TlsCertificateUnknown => .certificate_unknown, | |
| 206 | error.TlsIllegalParameter, | |
| 207 | error.IdentityElement, | |
| 208 | error.InvalidEncoding, | |
| 209 | error.TlsNoSupportedCiphers, | |
| 210 | => .illegal_parameter, | |
| 211 | error.TlsUnknownCa => .unknown_ca, | |
| 212 | error.TlsAccessDenied => .access_denied, | |
| 213 | error.TlsDecodeError => .decode_error, | |
| 214 | error.TlsDecryptError => .decrypt_error, | |
| 215 | error.TlsProtocolVersion => .protocol_version, | |
| 216 | error.TlsInsufficientSecurity => .insufficient_security, | |
| 217 | error.TlsInternalError => .internal_error, | |
| 218 | error.TlsInappropriateFallback => .inappropriate_fallback, | |
| 219 | error.TlsMissingExtension => .missing_extension, | |
| 220 | error.TlsUnsupportedExtension => .unsupported_extension, | |
| 221 | error.TlsUnrecognizedName => .unrecognized_name, | |
| 222 | error.TlsBadCertificateStatusResponse => .bad_certificate_status_response, | |
| 223 | error.TlsUnknownPskIdentity => .unknown_psk_identity, | |
| 224 | error.TlsCertificateRequired => .certificate_required, | |
| 225 | error.TlsNoApplicationProtocol => .no_application_protocol, | |
| 226 | else => .internal_error, | |
| 227 | }; | |
| 228 | } | |
| 229 | ||
| 230 | pub fn parse(buf: [2]u8) Alert { | |
| 231 | const level: Alert.Level = @fromBackingInt(@intCast(buf[0])); | |
| 232 | const alert: Alert = @fromBackingInt(@intCast(buf[1])); | |
| 233 | _ = level; | |
| 234 | return alert; | |
| 235 | } | |
| 236 | ||
| 237 | pub fn closeNotify() [2]u8 { | |
| 238 | return [2]u8{ | |
| 239 | @backingInt(Alert.Level.warning), | |
| 240 | @backingInt(Alert.close_notify), | |
| 241 | }; | |
| 242 | } | |
| 243 | }; | |
| 244 | ||
| 245 | pub const SignatureScheme = enum(u16) { | |
| 246 | // RSASSA-PKCS1-v1_5 algorithms | |
| 247 | rsa_pkcs1_sha256 = 0x0401, | |
| 248 | rsa_pkcs1_sha384 = 0x0501, | |
| 249 | rsa_pkcs1_sha512 = 0x0601, | |
| 250 | ||
| 251 | // ECDSA algorithms | |
| 252 | ecdsa_secp256r1_sha256 = 0x0403, | |
| 253 | ecdsa_secp384r1_sha384 = 0x0503, | |
| 254 | ecdsa_secp521r1_sha512 = 0x0603, | |
| 255 | ||
| 256 | // RSASSA-PSS algorithms with public key OID rsaEncryption | |
| 257 | rsa_pss_rsae_sha256 = 0x0804, | |
| 258 | rsa_pss_rsae_sha384 = 0x0805, | |
| 259 | rsa_pss_rsae_sha512 = 0x0806, | |
| 260 | ||
| 261 | // EdDSA algorithms | |
| 262 | ed25519 = 0x0807, | |
| 263 | ed448 = 0x0808, | |
| 264 | ||
| 265 | // RSASSA-PSS algorithms with public key OID RSASSA-PSS | |
| 266 | rsa_pss_pss_sha256 = 0x0809, | |
| 267 | rsa_pss_pss_sha384 = 0x080a, | |
| 268 | rsa_pss_pss_sha512 = 0x080b, | |
| 269 | ||
| 270 | // Legacy algorithms | |
| 271 | rsa_pkcs1_sha1 = 0x0201, | |
| 272 | ecdsa_sha1 = 0x0203, | |
| 273 | ||
| 274 | _, | |
| 275 | }; | |
| 276 | ||
| 277 | pub const NamedGroup = enum(u16) { | |
| 278 | // Elliptic Curve Groups (ECDHE) | |
| 279 | secp256r1 = 0x0017, | |
| 280 | secp384r1 = 0x0018, | |
| 281 | secp521r1 = 0x0019, | |
| 282 | x25519 = 0x001D, | |
| 283 | x448 = 0x001E, | |
| 284 | ||
| 285 | // Finite Field Groups (DHE) | |
| 286 | ffdhe2048 = 0x0100, | |
| 287 | ffdhe3072 = 0x0101, | |
| 288 | ffdhe4096 = 0x0102, | |
| 289 | ffdhe6144 = 0x0103, | |
| 290 | ffdhe8192 = 0x0104, | |
| 291 | ||
| 292 | // Hybrid post-quantum key agreements | |
| 293 | x25519_kyber512d00 = 0xFE30, | |
| 294 | x25519_kyber768d00 = 0x6399, | |
| 295 | x25519_ml_kem768 = 0x11EC, | |
| 296 | ||
| 297 | _, | |
| 298 | }; | |
| 299 | ||
| 300 | pub const KeyUpdateRequest = enum(u8) { | |
| 301 | update_not_requested = 0, | |
| 302 | update_requested = 1, | |
| 303 | _, | |
| 304 | }; | |
| 305 | ||
| 306 | pub const Side = enum { | |
| 307 | client, | |
| 308 | server, | |
| 309 | }; | |
| @@ -1,47 +0,0 @@ | ||
| 1 | /// Cross-platform CSPRNG that works on macOS, Linux, and other targets. | |
| 2 | const std = ; | |
| 3 | const builtin = ; | |
| 4 | ||
| 5 | /// Fill buffer with cryptographically secure random bytes. | |
| 6 | pub fn fill(buf: []u8) void { | |
| 7 | switch (builtin.os.tag) { | |
| 8 | .macos, .ios, .freebsd, .netbsd, .openbsd => { | |
| 9 | std.c.arc4random_buf(buf.ptr, buf.len); | |
| 10 | }, | |
| 11 | .linux => { | |
| 12 | // Use getrandom syscall directly (always available via kernel >= 3.17) | |
| 13 | const rc = std.os.linux.getrandom(buf.ptr, buf.len, 0); | |
| 14 | // Check for error: on Linux syscalls, error is indicated by | |
| 15 | // return value > max_usize - 4096 (i.e., negative errno) | |
| 16 | if (rc > std.math.maxInt(usize) - 4096) { | |
| 17 | // Fallback: read from /dev/urandom | |
| 18 | urandomFill(buf); | |
| 19 | } | |
| 20 | }, | |
| 21 | else => { | |
| 22 | // Fallback for other platforms | |
| 23 | urandomFill(buf); | |
| 24 | }, | |
| 25 | } | |
| 26 | } | |
| 27 | ||
| 28 | fn urandomFill(buf: []u8) void { | |
| 29 | const fd = std.posix.openat(std.posix.AT.FDCWD, "/dev/urandom", .{ .ACCMODE = .RDONLY }, 0) catch return; | |
| 30 | defer _ = std.c.close(fd); | |
| 31 | var pos: usize = 0; | |
| 32 | while (pos < buf.len) { | |
| 33 | const n = std.posix.read(fd, buf[pos..]) catch return; | |
| 34 | if (n == 0) return; | |
| 35 | pos += n; | |
| 36 | } | |
| 37 | } | |
| 38 | ||
| 39 | /// std.Random interface backed by this CSPRNG | |
| 40 | pub const csprng: std.Random = .{ | |
| 41 | .ptr = undefined, | |
| 42 | .fillFn = struct { | |
| 43 | fn f(_: *anyopaque, b: []u8) void { | |
| 44 | fill(b); | |
| 45 | } | |
| 46 | }.f, | |
| 47 | }; | |
| @@ -1,462 +0,0 @@ | ||
| 1 | const std = ; | |
| 2 | const builtin = ; | |
| 3 | const assert = std.debug.assert; | |
| 4 | const mem = std.mem; | |
| 5 | const Io = std.Io; | |
| 6 | const proto = ; | |
| 7 | const max_ciphertext_len = .max_ciphertext_len; | |
| 8 | ||
| 9 | const log = std.log.scoped(.tls); | |
| 10 | ||
| 11 | pub const header_len = 5; | |
| 12 | ||
| 13 | pub const Record = struct { | |
| 14 | content_type: proto.ContentType, | |
| 15 | protocol_version: proto.Version = .tls_1_2, | |
| 16 | buffer: []const u8, | |
| 17 | header: []const u8, | |
| 18 | payload: []const u8, | |
| 19 | ||
| 20 | pub fn init(buffer: []const u8) Record { | |
| 21 | return .{ | |
| 22 | .content_type = @fromBackingInt(@intCast(buffer[0])), | |
| 23 | .protocol_version = @fromBackingInt(@intCast(mem.readInt(u16, buffer[1..3], .big))), | |
| 24 | .buffer = buffer, | |
| 25 | .header = buffer[0..header_len], | |
| 26 | .payload = buffer[header_len..], | |
| 27 | }; | |
| 28 | } | |
| 29 | ||
| 30 | const Error = error{ | |
| 31 | EndOfStream, // clean close of the stream | |
| 32 | ReadFailed, // all other stream close | |
| 33 | InputBufferUndersize, // input can't fit tls record | |
| 34 | TlsRecordOverflow, // incorrect tls record | |
| 35 | }; | |
| 36 | ||
| 37 | pub fn read(rdr: *Io.Reader) Error!Record { | |
| 38 | if (header_len > rdr.buffer.len) { | |
| 39 | @branchHint(.cold); | |
| 40 | return error.InputBufferUndersize; | |
| 41 | } | |
| 42 | const hdr = try rdr.peek(header_len); | |
| 43 | const payload_len = mem.readInt(u16, hdr[3..5], .big); | |
| 44 | if (payload_len > max_ciphertext_len) { | |
| 45 | @branchHint(.cold); | |
| 46 | return error.TlsRecordOverflow; | |
| 47 | } | |
| 48 | const record_len = header_len + payload_len; | |
| 49 | if (record_len > rdr.buffer.len) { | |
| 50 | @branchHint(.cold); | |
| 51 | return error.InputBufferUndersize; | |
| 52 | } | |
| 53 | return .init(try rdr.take(record_len)); | |
| 54 | } | |
| 55 | ||
| 56 | pub fn decoder(rdr: *Io.Reader) !Decoder { | |
| 57 | const rec = try Record.read(rdr); | |
| 58 | if (@backingInt(rec.protocol_version) != 0x0300 and | |
| 59 | @backingInt(rec.protocol_version) != 0x0301 and | |
| 60 | rec.protocol_version != .tls_1_2) | |
| 61 | return error.TlsBadVersion; | |
| 62 | return .{ | |
| 63 | .content_type = rec.content_type, | |
| 64 | .payload = rec.payload, | |
| 65 | }; | |
| 66 | } | |
| 67 | }; | |
| 68 | ||
| 69 | pub const Decoder = struct { | |
| 70 | content_type: proto.ContentType, | |
| 71 | payload: []const u8, | |
| 72 | idx: usize = 0, | |
| 73 | ||
| 74 | pub fn init(content_type: proto.ContentType, payload: []const u8) Decoder { | |
| 75 | return .{ | |
| 76 | .content_type = content_type, | |
| 77 | .payload = payload, | |
| 78 | }; | |
| 79 | } | |
| 80 | ||
| 81 | pub fn decode(d: *Decoder, comptime T: type) !T { | |
| 82 | switch (@typeInfo(T)) { | |
| 83 | .int => |info| switch (info.bits) { | |
| 84 | 8 => { | |
| 85 | try skip(d, 1); | |
| 86 | return d.payload[d.idx - 1]; | |
| 87 | }, | |
| 88 | 16 => { | |
| 89 | try skip(d, 2); | |
| 90 | const b0: u16 = d.payload[d.idx - 2]; | |
| 91 | const b1: u16 = d.payload[d.idx - 1]; | |
| 92 | return (b0 << 8) | b1; | |
| 93 | }, | |
| 94 | 24 => { | |
| 95 | try skip(d, 3); | |
| 96 | const b0: u24 = d.payload[d.idx - 3]; | |
| 97 | const b1: u24 = d.payload[d.idx - 2]; | |
| 98 | const b2: u24 = d.payload[d.idx - 1]; | |
| 99 | return (b0 << 16) | (b1 << 8) | b2; | |
| 100 | }, | |
| 101 | 32 => { | |
| 102 | try skip(d, 4); | |
| 103 | const b0: u32 = d.payload[d.idx - 4]; | |
| 104 | const b1: u32 = d.payload[d.idx - 3]; | |
| 105 | const b2: u32 = d.payload[d.idx - 2]; | |
| 106 | const b3: u32 = d.payload[d.idx - 1]; | |
| 107 | return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3; | |
| 108 | }, | |
| 109 | else => @compileError("unsupported int type: " ++ @typeName(T)), | |
| 110 | }, | |
| 111 | .@"enum" => |info| { | |
| 112 | const int = try d.decode(info.tag_type); | |
| 113 | if (info.mode == .exhaustive) @compileError("exhaustive enum cannot be used"); | |
| 114 | return @as(T, @fromBackingInt(@intCast(int))); | |
| 115 | }, | |
| 116 | else => @compileError("unsupported type: " ++ @typeName(T)), | |
| 117 | } | |
| 118 | } | |
| 119 | ||
| 120 | pub fn array(d: *Decoder, comptime len: usize) ![len]u8 { | |
| 121 | try d.skip(len); | |
| 122 | return d.payload[d.idx - len ..][0..len].*; | |
| 123 | } | |
| 124 | ||
| 125 | pub fn slice(d: *Decoder, len: usize) ![]const u8 { | |
| 126 | try d.skip(len); | |
| 127 | return d.payload[d.idx - len ..][0..len]; | |
| 128 | } | |
| 129 | ||
| 130 | pub fn skip(d: *Decoder, amt: usize) !void { | |
| 131 | if (d.idx + amt > d.payload.len) return error.TlsDecodeError; | |
| 132 | d.idx += amt; | |
| 133 | } | |
| 134 | ||
| 135 | pub fn rest(d: Decoder) []const u8 { | |
| 136 | return d.payload[d.idx..]; | |
| 137 | } | |
| 138 | ||
| 139 | pub fn eof(d: Decoder) bool { | |
| 140 | return d.idx == d.payload.len; | |
| 141 | } | |
| 142 | ||
| 143 | pub fn expectContentType(d: *Decoder, content_type: proto.ContentType) !void { | |
| 144 | if (d.content_type == content_type) return; | |
| 145 | ||
| 146 | switch (d.content_type) { | |
| 147 | .alert => try d.raiseAlert(), | |
| 148 | else => return error.TlsUnexpectedMessage, | |
| 149 | } | |
| 150 | } | |
| 151 | ||
| 152 | pub fn raiseAlert(d: *Decoder) !void { | |
| 153 | if (d.payload.len < 2) return error.TlsUnexpectedMessage; | |
| 154 | try proto.Alert.parse(try d.array(2)).toError(); | |
| 155 | return error.TlsAlertCloseNotify; | |
| 156 | } | |
| 157 | }; | |
| 158 | ||
| 159 | const testing = std.testing; | |
| 160 | const data12 = ; | |
| 161 | const testu = ; | |
| 162 | const CipherSuite = .CipherSuite; | |
| 163 | ||
| 164 | test "record.read" { | |
| 165 | // expected records in the data12.server_responses | |
| 166 | const expected = [_]struct { | |
| 167 | content_type: proto.ContentType, | |
| 168 | payload_len: usize, | |
| 169 | }{ | |
| 170 | .{ .content_type = .handshake, .payload_len = 49 }, | |
| 171 | .{ .content_type = .handshake, .payload_len = 815 }, | |
| 172 | .{ .content_type = .handshake, .payload_len = 300 }, | |
| 173 | .{ .content_type = .handshake, .payload_len = 4 }, | |
| 174 | .{ .content_type = .change_cipher_spec, .payload_len = 1 }, | |
| 175 | .{ .content_type = .handshake, .payload_len = 64 }, | |
| 176 | }; | |
| 177 | var reader: Io.Reader = .fixed(&data12.server_responses); | |
| 178 | ||
| 179 | { | |
| 180 | var n: usize = 0; | |
| 181 | for (expected) |e| { | |
| 182 | const rec = try Record.read(&reader); | |
| 183 | try testing.expectEqual(e.content_type, rec.content_type); | |
| 184 | try testing.expectEqual(e.payload_len, rec.payload.len); | |
| 185 | try testing.expectEqual(.tls_1_2, rec.protocol_version); | |
| 186 | try testing.expectEqual(e.payload_len + header_len, rec.buffer.len); | |
| 187 | ||
| 188 | n += rec.payload.len + header_len; | |
| 189 | try testing.expectEqual(n, reader.seek); | |
| 190 | } | |
| 191 | try testing.expectEqual(data12.server_responses.len, reader.seek); | |
| 192 | try testing.expectError(error.EndOfStream, Record.read(&reader)); | |
| 193 | } | |
| 194 | ||
| 195 | reader.seek = 0; // rewind | |
| 196 | { // use smaller buffer to force fill, buffer must fit largest record 815 + 5 (header) | |
| 197 | var buffer: [820]u8 = undefined; | |
| 198 | var limited = reader.limited(.unlimited, &buffer); | |
| 199 | for (expected) |e| { | |
| 200 | const rec = try Record.read(&limited.interface); | |
| 201 | try testing.expectEqual(e.content_type, rec.content_type); | |
| 202 | try testing.expectEqual(e.payload_len, rec.payload.len); | |
| 203 | try testing.expectEqual(.tls_1_2, rec.protocol_version); | |
| 204 | } | |
| 205 | try testing.expectError(error.EndOfStream, Record.read(&limited.interface)); | |
| 206 | } | |
| 207 | } | |
| 208 | ||
| 209 | test Decoder { | |
| 210 | var reader: Io.Reader = .fixed(&data12.server_responses); | |
| 211 | var d = (try Record.decoder(&reader)); | |
| 212 | try testing.expectEqual(.handshake, d.content_type); | |
| 213 | ||
| 214 | try testing.expectEqual(.server_hello, try d.decode(proto.Handshake)); | |
| 215 | try testing.expectEqual(45, try d.decode(u24)); // length | |
| 216 | try testing.expectEqual(.tls_1_2, try d.decode(proto.Version)); | |
| 217 | try testing.expectEqualStrings( | |
| 218 | &testu.hexToBytes("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f"), | |
| 219 | try d.slice(32), | |
| 220 | ); // server random | |
| 221 | try testing.expectEqual(0, try d.decode(u8)); // session id len | |
| 222 | try testing.expectEqual(.ECDHE_RSA_WITH_AES_128_CBC_SHA, try d.decode(CipherSuite)); | |
| 223 | try testing.expectEqual(0, try d.decode(u8)); // compression method | |
| 224 | try testing.expectEqual(5, try d.decode(u16)); // extension length | |
| 225 | try testing.expectEqual(5, d.rest().len); | |
| 226 | try d.skip(5); | |
| 227 | try testing.expect(d.eof()); | |
| 228 | } | |
| 229 | ||
| 230 | pub const Writer = struct { | |
| 231 | inner: Io.Writer, | |
| 232 | ||
| 233 | pub fn initFromIo(io_w: *Io.Writer) Writer { | |
| 234 | return .{ .inner = Io.Writer.fixed(io_w.unusedCapacitySlice()) }; | |
| 235 | } | |
| 236 | ||
| 237 | pub fn init(buffer: []u8) Writer { | |
| 238 | return .{ .inner = Io.Writer.fixed(buffer) }; | |
| 239 | } | |
| 240 | ||
| 241 | fn ensureCapacity(w: Writer, n: usize) !void { | |
| 242 | if (w.inner.unusedCapacityLen() < n) | |
| 243 | return error.OutputBufferUndersize; | |
| 244 | } | |
| 245 | ||
| 246 | pub fn buffered(w: Writer) []const u8 { | |
| 247 | return w.inner.buffered(); | |
| 248 | } | |
| 249 | ||
| 250 | pub fn bytesWritten(w: Writer) usize { | |
| 251 | return w.inner.end; | |
| 252 | } | |
| 253 | ||
| 254 | pub fn pos(w: Writer) usize { | |
| 255 | return w.inner.end; | |
| 256 | } | |
| 257 | ||
| 258 | pub fn byte(w: *Writer, b: u8) !void { | |
| 259 | try w.ensureCapacity(1); | |
| 260 | try w.inner.writeByte(b); | |
| 261 | } | |
| 262 | ||
| 263 | pub fn slice(w: *Writer, bytes: []const u8) !void { | |
| 264 | try w.ensureCapacity(bytes.len); | |
| 265 | try w.inner.writeAll(bytes); | |
| 266 | } | |
| 267 | ||
| 268 | pub fn int(w: *Writer, comptime T: type, value: anytype) !void { | |
| 269 | try w.ensureCapacity(@divExact(@typeInfo(T).int.bits, 8)); | |
| 270 | try w.inner.writeInt(T, @intCast(value), .big); | |
| 271 | } | |
| 272 | ||
| 273 | pub fn writableArray(w: *Writer, comptime len: usize) !*[len]u8 { | |
| 274 | try w.ensureCapacity(len); | |
| 275 | return try w.inner.writableArray(len); | |
| 276 | } | |
| 277 | ||
| 278 | pub fn enumValue(w: *Writer, value: anytype) !void { | |
| 279 | const i = @backingInt(value); | |
| 280 | try w.int(@TypeOf(i), i); | |
| 281 | } | |
| 282 | ||
| 283 | pub fn enumList(w: *Writer, comptime E: type, tags: []const E) !void { | |
| 284 | assert(@sizeOf(E) == 2); | |
| 285 | try w.int(u16, tags.len * 2); | |
| 286 | for (tags) |t| { | |
| 287 | try w.enumValue(t); | |
| 288 | } | |
| 289 | } | |
| 290 | ||
| 291 | /// Default extension writer, writes extension type and list of tags | |
| 292 | pub fn extension(w: *Writer, ex: proto.Extension, tags: anytype) !void { | |
| 293 | try w.enumValue(ex); | |
| 294 | if (ex == .supported_versions) { | |
| 295 | try w.int(u16, tags.len * 2 + 1); | |
| 296 | try w.int(u8, tags.len * 2); | |
| 297 | } else if (ex == .psk_key_exchange_modes) { | |
| 298 | try w.int(u16, tags.len + 1); | |
| 299 | try w.int(u8, tags.len); | |
| 300 | } else { | |
| 301 | try w.int(u16, tags.len * 2 + 2); | |
| 302 | try w.int(u16, tags.len * 2); | |
| 303 | } | |
| 304 | for (tags) |t| { | |
| 305 | try w.enumValue(t); | |
| 306 | } | |
| 307 | } | |
| 308 | ||
| 309 | /// Key share extension | |
| 310 | pub fn keyShare(w: *Writer, named_groups: []const proto.NamedGroup, keys: []const []const u8) !void { | |
| 311 | if (keys.len == 0) return; | |
| 312 | assert(named_groups.len == keys.len); | |
| 313 | try w.enumValue(proto.Extension.key_share); | |
| 314 | var l: usize = 0; | |
| 315 | for (keys) |key| { | |
| 316 | l += key.len + 4; | |
| 317 | } | |
| 318 | try w.int(u16, l + 2); | |
| 319 | try w.int(u16, l); | |
| 320 | for (named_groups, 0..) |ng, i| { | |
| 321 | const key = keys[i]; | |
| 322 | try w.enumValue(ng); | |
| 323 | try w.int(u16, key.len); | |
| 324 | try w.inner.writeAll(key); | |
| 325 | } | |
| 326 | } | |
| 327 | ||
| 328 | /// Server name extension | |
| 329 | pub fn serverName(w: *Writer, host: []const u8) !void { | |
| 330 | const host_len: u16 = @intCast(host.len); | |
| 331 | try w.enumValue(proto.Extension.server_name); | |
| 332 | try w.int(u16, host_len + 5); // byte length of extension payload | |
| 333 | try w.int(u16, host_len + 3); // server_name_list byte count | |
| 334 | try w.inner.writeByte(0); // name type | |
| 335 | try w.int(u16, host_len); | |
| 336 | try w.inner.writeAll(host); | |
| 337 | } | |
| 338 | ||
| 339 | /// Writes header of the pre shared key extension, without binders | |
| 340 | pub fn preSharedKey( | |
| 341 | w: *Writer, | |
| 342 | identity: []const u8, | |
| 343 | obfuscated_age: u32, | |
| 344 | binder_len: u8, | |
| 345 | ) !void { | |
| 346 | try w.enumValue(proto.Extension.pre_shared_key); | |
| 347 | try w.int(u16, identity.len + binder_len + 4 + 2 + 2); | |
| 348 | try w.int(u16, identity.len + 4 + 2); | |
| 349 | try w.int(u16, identity.len); | |
| 350 | try w.inner.writeAll(identity); | |
| 351 | try w.int(u32, obfuscated_age); | |
| 352 | } | |
| 353 | ||
| 354 | /// Writes the rest of the pre shared keys extension | |
| 355 | pub fn preSharedKeyBinder(w: *Writer, binder: []const u8) !void { | |
| 356 | try w.int(u16, binder.len + 1); | |
| 357 | try w.int(u8, binder.len); | |
| 358 | try w.inner.writeAll(binder); | |
| 359 | } | |
| 360 | ||
| 361 | /// tls record | |
| 362 | pub fn record(w: *Writer, content_type: proto.ContentType, payload: []const u8) !void { | |
| 363 | try w.enumValue(content_type); | |
| 364 | try w.enumValue(proto.Version.tls_1_2); | |
| 365 | try w.int(u16, payload.len); | |
| 366 | if (w.unused().ptr == payload.ptr) { | |
| 367 | w.inner.advance(payload.len); | |
| 368 | } else { | |
| 369 | try w.slice(payload); | |
| 370 | } | |
| 371 | } | |
| 372 | ||
| 373 | pub fn recordHeader(w: *Writer, content_type: proto.ContentType, payload_len: usize) !void { | |
| 374 | try w.enumValue(content_type); | |
| 375 | try w.enumValue(proto.Version.tls_1_2); | |
| 376 | try w.int(u16, payload_len); | |
| 377 | } | |
| 378 | ||
| 379 | /// tls handshake record | |
| 380 | pub fn handshakeRecord(w: *Writer, handshake_type: proto.Handshake, payload: []const u8) !void { | |
| 381 | try w.enumValue(handshake_type); | |
| 382 | try w.int(u24, payload.len); | |
| 383 | if (w.unused().ptr == payload.ptr) { | |
| 384 | w.inner.advance(payload.len); | |
| 385 | } else { | |
| 386 | try w.slice(payload); | |
| 387 | } | |
| 388 | } | |
| 389 | ||
| 390 | pub fn handshakeRecordHeader(w: *Writer, handshake_type: proto.Handshake, payload_len: usize) !void { | |
| 391 | try w.enumValue(handshake_type); | |
| 392 | try w.int(u24, payload_len); | |
| 393 | } | |
| 394 | ||
| 395 | pub fn unused(w: *Writer) []u8 { | |
| 396 | return w.inner.unusedCapacitySlice(); | |
| 397 | } | |
| 398 | ||
| 399 | pub fn advance(w: *Writer, n: usize) void { | |
| 400 | w.inner.advance(n); | |
| 401 | } | |
| 402 | ||
| 403 | /// Skip `n` bytes from current position and return position before skip. | |
| 404 | /// Used with writerAt to later return to that point and write skipped | |
| 405 | /// bytes. | |
| 406 | pub fn skip(w: *Writer, n: usize) !usize { | |
| 407 | try w.ensureCapacity(n); | |
| 408 | const p = w.pos(); | |
| 409 | w.inner.advance(n); | |
| 410 | return p; | |
| 411 | } | |
| 412 | ||
| 413 | /// Returns writer at some previous position | |
| 414 | /// Parent buffer position is not changed. | |
| 415 | pub fn writerAt(w: *Writer, p: usize) Writer { | |
| 416 | assert(p < w.inner.end); | |
| 417 | return Writer{ .inner = Io.Writer.fixed(w.inner.buffered()[p..]) }; | |
| 418 | } | |
| 419 | ||
| 420 | /// Returns new writer in unused buffer part advancing `n` bytes. | |
| 421 | /// Parent buffer position is not changed. | |
| 422 | pub fn writerAdvance(w: *Writer, n: usize) !Writer { | |
| 423 | try w.ensureCapacity(n); | |
| 424 | return Writer{ .inner = Io.Writer.fixed(w.inner.unusedCapacitySlice()[n..]) }; | |
| 425 | } | |
| 426 | }; | |
| 427 | ||
| 428 | test "Writer" { | |
| 429 | var buf: [16]u8 = undefined; | |
| 430 | var w = Writer{ .inner = Io.Writer.fixed(&buf) }; | |
| 431 | ||
| 432 | try w.slice("ab"); | |
| 433 | try w.enumValue(proto.Curve.named_curve); | |
| 434 | try w.enumValue(proto.NamedGroup.x25519); | |
| 435 | try w.int(u16, 0x1234); | |
| 436 | try testing.expectEqualSlices(u8, &[_]u8{ 'a', 'b', 0x03, 0x00, 0x1d, 0x12, 0x34 }, w.buffered()); | |
| 437 | } | |
| 438 | ||
| 439 | fn int2(int: u16) [2]u8 { | |
| 440 | var arr: [2]u8 = undefined; | |
| 441 | std.mem.writeInt(u16, &arr, int, .big); | |
| 442 | return arr; | |
| 443 | } | |
| 444 | ||
| 445 | fn int3(int: u24) [3]u8 { | |
| 446 | var arr: [3]u8 = undefined; | |
| 447 | std.mem.writeInt(u24, &arr, int, .big); | |
| 448 | return arr; | |
| 449 | } | |
| 450 | ||
| 451 | pub fn header(content_type: proto.ContentType, payload_len: usize) [header_len]u8 { | |
| 452 | return [1]u8{@backingInt(content_type)} ++ | |
| 453 | int2(@backingInt(proto.Version.tls_1_2)) ++ | |
| 454 | int2(@intCast(payload_len)); | |
| 455 | } | |
| 456 | ||
| 457 | pub fn handshakeHeader(handshake_type: proto.Handshake, payload_len: usize) [4]u8 { | |
| 458 | var ret: [4]u8 = undefined; | |
| 459 | ret[0] = @backingInt(handshake_type); | |
| 460 | std.mem.writeInt(u24, ret[1..4], @intCast(payload_len), .big); | |
| 461 | return ret; | |
| 462 | } | |
| @@ -1,208 +0,0 @@ | ||
| 1 | const std = ; | |
| 2 | const assert = std.debug.assert; | |
| 3 | const Io = std.Io; | |
| 4 | ||
| 5 | pub const max_ciphertext_record_len = .max_ciphertext_record_len; | |
| 6 | ||
| 7 | /// Buffer of this size will fit any tls ciphertext record sent by other side. | |
| 8 | /// To decrytp we need full record, smalled buffer will not work in general | |
| 9 | /// case. Bigger can be used for performance reason. | |
| 10 | pub const input_buffer_len = max_ciphertext_record_len; // 16645 bytes | |
| 11 | ||
| 12 | /// Needed output buffer during handshake is the size of the tls hello message, | |
| 13 | /// which is (when client authentication is not used) ~1600 bytes. After | |
| 14 | /// handshake it limits how big tls record can be produced. This suggested value | |
| 15 | /// can hold max ciphertext record produced with this implementation. | |
| 16 | pub const output_buffer_len = .max_encrypted_record_len; // 16469 bytes | |
| 17 | ||
| 18 | pub const Connection = .Connection; | |
| 19 | pub const rsa = ; | |
| 20 | ||
| 21 | const handshake = struct { | |
| 22 | const Client = .Handshake; | |
| 23 | const Server = .Handshake; | |
| 24 | }; | |
| 25 | ||
| 26 | /// Upgrades existing stream to the tls connection by the client tls handshake. | |
| 27 | pub inline fn clientFromStream(stream: anytype, opt: config.Client) !Connection { | |
| 28 | const input, const output = streamToRaderWriter(stream); | |
| 29 | return try client(input, output, opt); | |
| 30 | } | |
| 31 | ||
| 32 | pub fn client(input: *Io.Reader, output: *Io.Writer, opt: config.Client) !Connection { | |
| 33 | assert(input.buffer.len >= input_buffer_len); | |
| 34 | assert(output.buffer.len >= 2048); // client hello requires: 1572 + opt.host.len | |
| 35 | ||
| 36 | var hc: handshake.Client = .{ .input = input, .output = output }; | |
| 37 | const cipher, const session_resumption_secret_idx = try hc.handshake(opt); | |
| 38 | return .{ | |
| 39 | .cipher = cipher, | |
| 40 | .input = input, | |
| 41 | .output = output, | |
| 42 | .session_resumption_secret_idx = session_resumption_secret_idx, | |
| 43 | .session_resumption = opt.session_resumption, | |
| 44 | }; | |
| 45 | } | |
| 46 | ||
| 47 | /// Upgrades existing stream to the tls connection by the server side tls handshake. | |
| 48 | pub inline fn serverFromStream(stream: anytype, opt: config.Server) !Connection { | |
| 49 | const input, const output = streamToRaderWriter(stream); | |
| 50 | return try server(input, output, opt); | |
| 51 | } | |
| 52 | ||
| 53 | pub fn server(input: *Io.Reader, output: *Io.Writer, opt: config.Server) !Connection { | |
| 54 | var hs: handshake.Server = .{ .input = input, .output = output }; | |
| 55 | const cipher = try hs.handshake(opt); | |
| 56 | return .{ | |
| 57 | .cipher = cipher, | |
| 58 | .input = input, | |
| 59 | .output = output, | |
| 60 | }; | |
| 61 | } | |
| 62 | ||
| 63 | /// With default buffer sizes | |
| 64 | inline fn streamToRaderWriter(stream: anytype) struct { *Io.Reader, *Io.Writer } { | |
| 65 | var input_buf: [input_buffer_len]u8 = undefined; | |
| 66 | var output_buf: [output_buffer_len]u8 = undefined; | |
| 67 | var reader = stream.reader(&input_buf); | |
| 68 | var writer = stream.writer(&output_buf); | |
| 69 | const input = if (@hasField(@TypeOf(reader), "interface")) &reader.interface else reader.interface(); | |
| 70 | const output = &writer.interface; | |
| 71 | return .{ input, output }; | |
| 72 | } | |
| 73 | ||
| 74 | pub const Cipher = .Cipher; | |
| 75 | pub const config = struct { | |
| 76 | pub const proto = ; | |
| 77 | const common = ; | |
| 78 | ||
| 79 | pub const CipherSuite = .CipherSuite; | |
| 80 | pub const PrivateKey = ; | |
| 81 | pub const NamedGroup = proto.NamedGroup; | |
| 82 | pub const Version = proto.Version; | |
| 83 | pub const ContentType = proto.ContentType; | |
| 84 | pub const Record = .Record; | |
| 85 | pub const cert = common.cert; | |
| 86 | pub const CertKeyPair = common.CertKeyPair; | |
| 87 | ||
| 88 | pub const cipher_suites = .cipher_suites; | |
| 89 | pub const key_log = ; | |
| 90 | ||
| 91 | pub const Client = .Options; | |
| 92 | pub const Server = .Options; | |
| 93 | }; | |
| 94 | ||
| 95 | /// Non-blocking client/server handshake and connection. Handshake produces | |
| 96 | /// cipher used in connection to encrypt data for sending and decrypt received | |
| 97 | /// data. | |
| 98 | pub const nonblock = struct { | |
| 99 | pub const Client = .NonBlock; | |
| 100 | pub const Server = .NonBlock; | |
| 101 | pub const Connection = .NonBlock; | |
| 102 | }; | |
| 103 | ||
| 104 | pub const Ktls = ; | |
| 105 | ||
| 106 | test "nonblock handshake and connection" { | |
| 107 | const testing = .testing; | |
| 108 | ||
| 109 | // data from server to the client | |
| 110 | var sc_buf: [max_ciphertext_record_len]u8 = undefined; | |
| 111 | // data from client to the server | |
| 112 | var cs_buf: [max_ciphertext_record_len]u8 = undefined; | |
| 113 | ||
| 114 | // client/server handshake produces ciphers | |
| 115 | const cli_cipher, const srv_cipher = brk: { | |
| 116 | var cli = nonblock.Client.init(.{ | |
| 117 | .root_ca = .empty, | |
| 118 | .host = &.{}, | |
| 119 | .insecure_skip_verify = true, | |
| 120 | }); | |
| 121 | var srv = nonblock.Server.init(.{ .auth = null }); | |
| 122 | ||
| 123 | // client flight1; client hello is in buf1 | |
| 124 | var cr = try cli.run(&sc_buf, &cs_buf); | |
| 125 | try testing.expectEqual(0, cr.recv_pos); | |
| 126 | try testing.expect(cr.send.len > 0); | |
| 127 | try testing.expect(!cli.done()); | |
| 128 | ||
| 129 | { // short read, partial buffer received | |
| 130 | for (0..cr.send_pos) |i| { | |
| 131 | const sr = try srv.run(cs_buf[0..i], &sc_buf); | |
| 132 | try testing.expectEqual(0, sr.recv_pos); | |
| 133 | try testing.expectEqual(0, sr.send_pos); | |
| 134 | } | |
| 135 | } | |
| 136 | ||
| 137 | // server flight 1; server parses client hello from buf2 and writes server hello into buf1 | |
| 138 | var sr = try srv.run(&cs_buf, &sc_buf); | |
| 139 | try testing.expectEqual(sr.recv_pos, cr.send_pos); | |
| 140 | try testing.expect(sr.send.len > 0); | |
| 141 | try testing.expect(!srv.done()); | |
| 142 | ||
| 143 | { // short read, partial buffer received | |
| 144 | for (0..sr.send_pos) |i| { | |
| 145 | cr = try cli.run(sc_buf[0..i], &cs_buf); | |
| 146 | try testing.expectEqual(0, cr.recv_pos); | |
| 147 | try testing.expectEqual(0, cr.send_pos); | |
| 148 | } | |
| 149 | } | |
| 150 | ||
| 151 | // client flight 2; client parses server hello from buf1 and writes finished into buf2 | |
| 152 | cr = try cli.run(&sc_buf, &cs_buf); | |
| 153 | try testing.expectEqual(sr.send_pos, cr.recv_pos); | |
| 154 | try testing.expect(cr.send.len > 0); | |
| 155 | try testing.expect(cli.done()); // client is done | |
| 156 | try testing.expect(cli.cipher() != null); | |
| 157 | ||
| 158 | // server parses client finished | |
| 159 | sr = try srv.run(&cs_buf, &sc_buf); | |
| 160 | try testing.expectEqual(sr.recv_pos, cr.send_pos); | |
| 161 | try testing.expectEqual(0, sr.send.len); | |
| 162 | try testing.expect(srv.done()); // server is done | |
| 163 | try testing.expect(srv.cipher() != null); | |
| 164 | ||
| 165 | break :brk .{ cli.cipher().?, srv.cipher().? }; | |
| 166 | }; | |
| 167 | { // use ciphers in connection | |
| 168 | var cli = nonblock.Connection.init(cli_cipher); | |
| 169 | var srv = nonblock.Connection.init(srv_cipher); | |
| 170 | ||
| 171 | const cleartext = "Lorem ipsum dolor sit amet"; | |
| 172 | { // client to server | |
| 173 | const e = try cli.encrypt(cleartext, &cs_buf); | |
| 174 | try testing.expectEqual(cleartext.len, e.cleartext_pos); | |
| 175 | try testing.expect(e.ciphertext.len > cleartext.len); | |
| 176 | try testing.expect(e.unused_cleartext.len == 0); | |
| 177 | ||
| 178 | const d = try srv.decrypt(e.ciphertext, &sc_buf); | |
| 179 | try testing.expectEqualSlices(u8, cleartext, d.cleartext); | |
| 180 | try testing.expectEqual(e.ciphertext.len, d.ciphertext_pos); | |
| 181 | try testing.expectEqual(0, d.unused_ciphertext.len); | |
| 182 | } | |
| 183 | { // server to client | |
| 184 | const e = try srv.encrypt(cleartext, &sc_buf); | |
| 185 | const d = try cli.decrypt(e.ciphertext, &cs_buf); | |
| 186 | try testing.expectEqualSlices(u8, cleartext, d.cleartext); | |
| 187 | } | |
| 188 | { // server sends close | |
| 189 | const close_buf = try srv.close(&sc_buf); | |
| 190 | const d = try cli.decrypt(close_buf, &cs_buf); | |
| 191 | try testing.expectEqual(close_buf.len, d.ciphertext_pos); | |
| 192 | try testing.expectEqual(0, d.unused_ciphertext.len); | |
| 193 | try testing.expect(d.closed); | |
| 194 | } | |
| 195 | } | |
| 196 | } | |
| 197 | ||
| 198 | test { | |
| 199 | _ = ; | |
| 200 | _ = ; | |
| 201 | _ = ; | |
| 202 | ||
| 203 | _ = ; | |
| 204 | _ = ; | |
| 205 | _ = ; | |
| 206 | _ = ; | |
| 207 | _ = ; | |
| 208 | } | |
| @@ -1,467 +0,0 @@ | ||
| 1 | //! An encoding of ASN.1. | |
| 2 | //! | |
| 3 | //! Distinguised Encoding Rules as defined in X.690 and X.691. | |
| 4 | //! | |
| 5 | //! A version of Basic Encoding Rules (BER) where there is exactly ONE way to | |
| 6 | //! represent non-constructed elements. This is useful for cryptographic signatures. | |
| 7 | //! | |
| 8 | //! Currently an implementation detail of the standard library not fit for public | |
| 9 | //! use since it's missing an encoder. | |
| 10 | ||
| 11 | const std = ; | |
| 12 | const builtin = ; | |
| 13 | ||
| 14 | pub const Index = usize; | |
| 15 | const log = std.log.scoped(.der); | |
| 16 | ||
| 17 | /// A secure DER parser that: | |
| 18 | /// - Does NOT read memory outside `bytes`. | |
| 19 | /// - Does NOT return elements with slices outside `bytes`. | |
| 20 | /// - Errors on values that do NOT follow DER rules. | |
| 21 | /// - Lengths that could be represented in a shorter form. | |
| 22 | /// - Booleans that are not 0xff or 0x00. | |
| 23 | pub const Parser = struct { | |
| 24 | bytes: []const u8, | |
| 25 | index: Index = 0, | |
| 26 | ||
| 27 | pub const Error = Element.Error || error{ | |
| 28 | UnexpectedElement, | |
| 29 | InvalidIntegerEncoding, | |
| 30 | Overflow, | |
| 31 | NonCanonical, | |
| 32 | InvalidBitString, | |
| 33 | }; | |
| 34 | ||
| 35 | pub fn expectBool(self: *Parser) Error!bool { | |
| 36 | const ele = try self.expect(.universal, false, .boolean); | |
| 37 | if (ele.slice.len() != 1) return error.InvalidBool; | |
| 38 | ||
| 39 | return switch (self.view(ele)[0]) { | |
| 40 | 0x00 => false, | |
| 41 | 0xff => true, | |
| 42 | else => error.InvalidBool, | |
| 43 | }; | |
| 44 | } | |
| 45 | ||
| 46 | pub fn expectBitstring(self: *Parser) Error!BitString { | |
| 47 | const ele = try self.expect(.universal, false, .bitstring); | |
| 48 | const bytes = self.view(ele); | |
| 49 | const right_padding = bytes[0]; | |
| 50 | if (right_padding >= 8) return error.InvalidBitString; | |
| 51 | return .{ | |
| 52 | .bytes = bytes[1..], | |
| 53 | .right_padding = @intCast(right_padding), | |
| 54 | }; | |
| 55 | } | |
| 56 | ||
| 57 | // TODO: return high resolution date time type instead of epoch seconds | |
| 58 | pub fn expectDateTime(self: *Parser) Error!i64 { | |
| 59 | const ele = try self.expect(.universal, false, null); | |
| 60 | const bytes = self.view(ele); | |
| 61 | switch (ele.identifier.tag) { | |
| 62 | .utc_time => { | |
| 63 | // Example: "YYMMDD000000Z" | |
| 64 | if (bytes.len != 13) | |
| 65 | return error.InvalidDateTime; | |
| 66 | if (bytes[12] != 'Z') | |
| 67 | return error.InvalidDateTime; | |
| 68 | ||
| 69 | var date: Date = undefined; | |
| 70 | date.year = try parseTimeDigits(bytes[0..2], 0, 99); | |
| 71 | date.year += if (date.year >= 50) 1900 else 2000; | |
| 72 | date.month = try parseTimeDigits(bytes[2..4], 1, 12); | |
| 73 | date.day = try parseTimeDigits(bytes[4..6], 1, 31); | |
| 74 | const time = try parseTime(bytes[6..12]); | |
| 75 | ||
| 76 | return date.toEpochSeconds() + time.toSec(); | |
| 77 | }, | |
| 78 | .generalized_time => { | |
| 79 | // Examples: | |
| 80 | // "19920622123421Z" | |
| 81 | // "19920722132100.3Z" | |
| 82 | if (bytes.len < 15) | |
| 83 | return error.InvalidDateTime; | |
| 84 | ||
| 85 | var date: Date = undefined; | |
| 86 | date.year = try parseYear4(bytes[0..4]); | |
| 87 | date.month = try parseTimeDigits(bytes[4..6], 1, 12); | |
| 88 | date.day = try parseTimeDigits(bytes[6..8], 1, 31); | |
| 89 | const time = try parseTime(bytes[8..14]); | |
| 90 | ||
| 91 | return date.toEpochSeconds() + time.toSec(); | |
| 92 | }, | |
| 93 | else => return error.InvalidDateTime, | |
| 94 | } | |
| 95 | } | |
| 96 | ||
| 97 | pub fn expectOid(self: *Parser) Error![]const u8 { | |
| 98 | const oid = try self.expect(.universal, false, .object_identifier); | |
| 99 | return self.view(oid); | |
| 100 | } | |
| 101 | ||
| 102 | pub fn expectEnum(self: *Parser, comptime Enum: type) Error!Enum { | |
| 103 | const oid = try self.expectOid(); | |
| 104 | return Enum.oids.get(oid) orelse { | |
| 105 | if (builtin.mode == .Debug) { | |
| 106 | var buf: [256]u8 = undefined; | |
| 107 | var stream = std.io.fixedBufferStream(&buf); | |
| 108 | try .decode(oid, stream.writer()); | |
| 109 | log.warn("unknown oid {s} for enum {s}\n", .{ stream.getWritten(), @typeName(Enum) }); | |
| 110 | } | |
| 111 | return error.UnknownObjectId; | |
| 112 | }; | |
| 113 | } | |
| 114 | ||
| 115 | pub fn expectInt(self: *Parser, comptime T: type) Error!T { | |
| 116 | const ele = try self.expectPrimitive(.integer); | |
| 117 | const bytes = self.view(ele); | |
| 118 | ||
| 119 | const info = @typeInfo(T); | |
| 120 | if (info != .int) @compileError(@typeName(T) ++ " is not an int type"); | |
| 121 | const Shift = std.math.Log2Int(u8); | |
| 122 | ||
| 123 | var result: std.meta.Int(.unsigned, info.int.bits) = 0; | |
| 124 | for (bytes, 0..) |b, index| { | |
| 125 | const shifted = @shlWithOverflow(b, @as(Shift, @intCast(index * 8))); | |
| 126 | if (shifted[1] == 1) return error.Overflow; | |
| 127 | ||
| 128 | result |= shifted[0]; | |
| 129 | } | |
| 130 | ||
| 131 | return @bitCast(result); | |
| 132 | } | |
| 133 | ||
| 134 | pub fn expectString(self: *Parser, allowed: std.EnumSet(String.Tag)) Error!String { | |
| 135 | const ele = try self.expect(.universal, false, null); | |
| 136 | switch (ele.identifier.tag) { | |
| 137 | inline .string_utf8, | |
| 138 | .string_numeric, | |
| 139 | .string_printable, | |
| 140 | .string_teletex, | |
| 141 | .string_videotex, | |
| 142 | .string_ia5, | |
| 143 | .string_visible, | |
| 144 | .string_universal, | |
| 145 | .string_bmp, | |
| 146 | => |t| { | |
| 147 | const tagname = @tagName(t)["string_".len..]; | |
| 148 | const tag = std.meta.stringToEnum(String.Tag, tagname) orelse unreachable; | |
| 149 | if (allowed.contains(tag)) { | |
| 150 | return String{ .tag = tag, .data = self.view(ele) }; | |
| 151 | } | |
| 152 | }, | |
| 153 | else => {}, | |
| 154 | } | |
| 155 | return error.UnexpectedElement; | |
| 156 | } | |
| 157 | ||
| 158 | pub fn expectPrimitive(self: *Parser, tag: ?Identifier.Tag) Error!Element { | |
| 159 | var elem = try self.expect(.universal, false, tag); | |
| 160 | if (tag == .integer and elem.slice.len() > 0) { | |
| 161 | if (self.view(elem)[0] == 0) elem.slice.start += 1; | |
| 162 | if (elem.slice.len() > 0 and self.view(elem)[0] == 0) return error.InvalidIntegerEncoding; | |
| 163 | } | |
| 164 | return elem; | |
| 165 | } | |
| 166 | ||
| 167 | /// Remember to call `expectEnd` | |
| 168 | pub fn expectSequence(self: *Parser) Error!Element { | |
| 169 | return try self.expect(.universal, true, .sequence); | |
| 170 | } | |
| 171 | ||
| 172 | /// Remember to call `expectEnd` | |
| 173 | pub fn expectSequenceOf(self: *Parser) Error!Element { | |
| 174 | return try self.expect(.universal, true, .sequence_of); | |
| 175 | } | |
| 176 | ||
| 177 | pub fn expectEnd(self: *Parser, val: usize) Error!void { | |
| 178 | if (self.index != val) return error.NonCanonical; // either forgot to parse end OR an attacker | |
| 179 | } | |
| 180 | ||
| 181 | pub fn expect( | |
| 182 | self: *Parser, | |
| 183 | class: ?Identifier.Class, | |
| 184 | constructed: ?bool, | |
| 185 | tag: ?Identifier.Tag, | |
| 186 | ) Error!Element { | |
| 187 | if (self.index >= self.bytes.len) return error.EndOfStream; | |
| 188 | ||
| 189 | const res = try Element.init(self.bytes, self.index); | |
| 190 | if (tag) |e| { | |
| 191 | if (res.identifier.tag != e) return error.UnexpectedElement; | |
| 192 | } | |
| 193 | if (constructed) |e| { | |
| 194 | if (res.identifier.constructed != e) return error.UnexpectedElement; | |
| 195 | } | |
| 196 | if (class) |e| { | |
| 197 | if (res.identifier.class != e) return error.UnexpectedElement; | |
| 198 | } | |
| 199 | self.index = if (res.identifier.constructed) res.slice.start else res.slice.end; | |
| 200 | return res; | |
| 201 | } | |
| 202 | ||
| 203 | pub fn view(self: Parser, elem: Element) []const u8 { | |
| 204 | return elem.slice.view(self.bytes); | |
| 205 | } | |
| 206 | ||
| 207 | pub fn seek(self: *Parser, index: usize) void { | |
| 208 | self.index = index; | |
| 209 | } | |
| 210 | ||
| 211 | pub fn eof(self: *Parser) bool { | |
| 212 | return self.index == self.bytes.len; | |
| 213 | } | |
| 214 | }; | |
| 215 | ||
| 216 | pub const Element = struct { | |
| 217 | identifier: Identifier, | |
| 218 | slice: Slice, | |
| 219 | ||
| 220 | pub const Slice = struct { | |
| 221 | start: Index, | |
| 222 | end: Index, | |
| 223 | ||
| 224 | pub fn len(self: Slice) Index { | |
| 225 | return self.end - self.start; | |
| 226 | } | |
| 227 | ||
| 228 | pub fn view(self: Slice, bytes: []const u8) []const u8 { | |
| 229 | return bytes[self.start..self.end]; | |
| 230 | } | |
| 231 | }; | |
| 232 | ||
| 233 | pub const Error = error{ InvalidLength, EndOfStream, ReadFailed }; | |
| 234 | ||
| 235 | pub fn init(bytes: []const u8, index: Index) Error!Element { | |
| 236 | var reader = std.Io.Reader.fixed(bytes[index..]); | |
| 237 | ||
| 238 | const identifier = @as(Identifier, @bitCast(try reader.takeByte())); | |
| 239 | const size_or_len_size = try reader.takeByte(); | |
| 240 | ||
| 241 | var start = index + 2; | |
| 242 | // short form between 0-127 | |
| 243 | if (size_or_len_size < 128) { | |
| 244 | const end = start + size_or_len_size; | |
| 245 | if (end > bytes.len) return error.InvalidLength; | |
| 246 | ||
| 247 | return .{ .identifier = identifier, .slice = .{ .start = start, .end = end } }; | |
| 248 | } | |
| 249 | ||
| 250 | // long form between 0 and std.math.maxInt(u1024) | |
| 251 | const len_size: u7 = @truncate(size_or_len_size); | |
| 252 | start += len_size; | |
| 253 | if (len_size > @sizeOf(Index)) return error.InvalidLength; | |
| 254 | const len = try reader.takeVarInt(Index, .big, len_size); | |
| 255 | if (len < 128) return error.InvalidLength; // should have used short form | |
| 256 | ||
| 257 | const end = std.math.add(Index, start, len) catch return error.InvalidLength; | |
| 258 | if (end > bytes.len) return error.InvalidLength; | |
| 259 | ||
| 260 | return .{ .identifier = identifier, .slice = .{ .start = start, .end = end } }; | |
| 261 | } | |
| 262 | }; | |
| 263 | ||
| 264 | test Element { | |
| 265 | const short_form = [_]u8{ 0x30, 0x03, 0x02, 0x01, 0x09 }; | |
| 266 | try std.testing.expectEqual(Element{ | |
| 267 | .identifier = Identifier{ .tag = .sequence, .constructed = true, .class = .universal }, | |
| 268 | .slice = .{ .start = 2, .end = short_form.len }, | |
| 269 | }, Element.init(&short_form, 0)); | |
| 270 | ||
| 271 | const long_form = [_]u8{ 0x30, 129, 129 } ++ @as([129]u8, @splat(0)); | |
| 272 | try std.testing.expectEqual(Element{ | |
| 273 | .identifier = Identifier{ .tag = .sequence, .constructed = true, .class = .universal }, | |
| 274 | .slice = .{ .start = 3, .end = long_form.len }, | |
| 275 | }, Element.init(&long_form, 0)); | |
| 276 | } | |
| 277 | ||
| 278 | test "parser.expectInt" { | |
| 279 | const one = [_]u8{ 2, 1, 1 }; | |
| 280 | var parser = Parser{ .bytes = &one }; | |
| 281 | try std.testing.expectEqual(@as(u8, 1), try parser.expectInt(u8)); | |
| 282 | } | |
| 283 | ||
| 284 | pub const Identifier = packed struct(u8) { | |
| 285 | tag: Tag, | |
| 286 | constructed: bool, | |
| 287 | class: Class, | |
| 288 | ||
| 289 | pub const Class = enum(u2) { | |
| 290 | universal, | |
| 291 | application, | |
| 292 | context_specific, | |
| 293 | private, | |
| 294 | }; | |
| 295 | ||
| 296 | // https://www.oss.com/asn1/resources/asn1-made-simple/asn1-quick-reference/asn1-tags.html | |
| 297 | pub const Tag = enum(u5) { | |
| 298 | boolean = 1, | |
| 299 | integer = 2, | |
| 300 | bitstring = 3, | |
| 301 | octetstring = 4, | |
| 302 | null = 5, | |
| 303 | object_identifier = 6, | |
| 304 | real = 9, | |
| 305 | enumerated = 10, | |
| 306 | string_utf8 = 12, | |
| 307 | sequence = 16, | |
| 308 | sequence_of = 17, | |
| 309 | string_numeric = 18, | |
| 310 | string_printable = 19, | |
| 311 | string_teletex = 20, | |
| 312 | string_videotex = 21, | |
| 313 | string_ia5 = 22, | |
| 314 | utc_time = 23, | |
| 315 | generalized_time = 24, | |
| 316 | string_visible = 26, | |
| 317 | string_universal = 28, | |
| 318 | string_bmp = 30, | |
| 319 | _, | |
| 320 | }; | |
| 321 | }; | |
| 322 | ||
| 323 | pub const BitString = struct { | |
| 324 | bytes: []const u8, | |
| 325 | right_padding: u3, | |
| 326 | ||
| 327 | pub fn bitLen(self: BitString) usize { | |
| 328 | return self.bytes.len * 8 + self.right_padding; | |
| 329 | } | |
| 330 | }; | |
| 331 | ||
| 332 | pub const String = struct { | |
| 333 | tag: Tag, | |
| 334 | data: []const u8, | |
| 335 | ||
| 336 | pub const Tag = enum { | |
| 337 | /// Blessed. | |
| 338 | utf8, | |
| 339 | /// us-ascii ([-][0-9][eE][.])* | |
| 340 | numeric, | |
| 341 | /// us-ascii ([A-Z][a-z][0-9][.?!,][ \t])* | |
| 342 | printable, | |
| 343 | /// iso-8859-1 with escaping into different character sets. | |
| 344 | /// Cursed. | |
| 345 | teletex, | |
| 346 | /// iso-8859-1 | |
| 347 | videotex, | |
| 348 | /// us-ascii first 128 characters. | |
| 349 | ia5, | |
| 350 | /// us-ascii without control characters. | |
| 351 | visible, | |
| 352 | /// utf-32-be | |
| 353 | universal, | |
| 354 | /// utf-16-be | |
| 355 | bmp, | |
| 356 | }; | |
| 357 | ||
| 358 | pub const all = [_]Tag{ | |
| 359 | .utf8, | |
| 360 | .numeric, | |
| 361 | .printable, | |
| 362 | .teletex, | |
| 363 | .videotex, | |
| 364 | .ia5, | |
| 365 | .visible, | |
| 366 | .universal, | |
| 367 | .bmp, | |
| 368 | }; | |
| 369 | }; | |
| 370 | ||
| 371 | const Date = struct { | |
| 372 | year: Year, | |
| 373 | month: u8, | |
| 374 | day: u8, | |
| 375 | ||
| 376 | const Year = std.time.epoch.Year; | |
| 377 | ||
| 378 | fn toEpochSeconds(date: Date) i64 { | |
| 379 | // Euclidean Affine Transform by Cassio and Neri. | |
| 380 | // Shift and correction constants for 1970-01-01. | |
| 381 | const s = 82; | |
| 382 | const K = 719468 + 146097 * s; | |
| 383 | const L = 400 * s; | |
| 384 | ||
| 385 | const Y_G: u32 = date.year; | |
| 386 | const M_G: u32 = date.month; | |
| 387 | const D_G: u32 = date.day; | |
| 388 | // Map to computational calendar. | |
| 389 | const J: u32 = if (M_G <= 2) 1 else 0; | |
| 390 | const Y: u32 = Y_G + L - J; | |
| 391 | const M: u32 = if (J != 0) M_G + 12 else M_G; | |
| 392 | const D: u32 = D_G - 1; | |
| 393 | const C: u32 = Y / 100; | |
| 394 | ||
| 395 | // Rata die. | |
| 396 | const y_star: u32 = 1461 * Y / 4 - C + C / 4; | |
| 397 | const m_star: u32 = (979 * M - 2919) / 32; | |
| 398 | const N: u32 = y_star + m_star + D; | |
| 399 | const days: i32 = @intCast(N - K); | |
| 400 | ||
| 401 | return @as(i64, days) * std.time.epoch.secs_per_day; | |
| 402 | } | |
| 403 | }; | |
| 404 | ||
| 405 | const Time = struct { | |
| 406 | hour: std.math.IntFittingRange(0, 24), | |
| 407 | minute: std.math.IntFittingRange(0, 60), | |
| 408 | second: std.math.IntFittingRange(0, 60), | |
| 409 | ||
| 410 | fn toSec(t: Time) i64 { | |
| 411 | var sec: i64 = 0; | |
| 412 | sec += @as(i64, t.hour) * 60 * 60; | |
| 413 | sec += @as(i64, t.minute) * 60; | |
| 414 | sec += t.second; | |
| 415 | return sec; | |
| 416 | } | |
| 417 | }; | |
| 418 | ||
| 419 | fn parseTimeDigits( | |
| 420 | text: *const [2]u8, | |
| 421 | min: comptime_int, | |
| 422 | max: comptime_int, | |
| 423 | ) !std.math.IntFittingRange(min, max) { | |
| 424 | const result = std.fmt.parseInt(std.math.IntFittingRange(min, max), text, 10) catch | |
| 425 | return error.InvalidTime; | |
| 426 | if (result < min) return error.InvalidTime; | |
| 427 | if (result > max) return error.InvalidTime; | |
| 428 | return result; | |
| 429 | } | |
| 430 | ||
| 431 | test parseTimeDigits { | |
| 432 | const expectEqual = std.testing.expectEqual; | |
| 433 | try expectEqual(@as(u8, 0), try parseTimeDigits("00", 0, 99)); | |
| 434 | try expectEqual(@as(u8, 99), try parseTimeDigits("99", 0, 99)); | |
| 435 | try expectEqual(@as(u8, 42), try parseTimeDigits("42", 0, 99)); | |
| 436 | ||
| 437 | const expectError = std.testing.expectError; | |
| 438 | try expectError(error.InvalidTime, parseTimeDigits("13", 1, 12)); | |
| 439 | try expectError(error.InvalidTime, parseTimeDigits("00", 1, 12)); | |
| 440 | try expectError(error.InvalidTime, parseTimeDigits("Di", 0, 99)); | |
| 441 | } | |
| 442 | ||
| 443 | fn parseYear4(text: *const [4]u8) !Date.Year { | |
| 444 | const result = std.fmt.parseInt(Date.Year, text, 10) catch return error.InvalidYear; | |
| 445 | if (result > 9999) return error.InvalidYear; | |
| 446 | return result; | |
| 447 | } | |
| 448 | ||
| 449 | test parseYear4 { | |
| 450 | const expectEqual = std.testing.expectEqual; | |
| 451 | try expectEqual(@as(Date.Year, 0), try parseYear4("0000")); | |
| 452 | try expectEqual(@as(Date.Year, 9999), try parseYear4("9999")); | |
| 453 | try expectEqual(@as(Date.Year, 1988), try parseYear4("1988")); | |
| 454 | ||
| 455 | const expectError = std.testing.expectError; | |
| 456 | try expectError(error.InvalidYear, parseYear4("999b")); | |
| 457 | try expectError(error.InvalidYear, parseYear4("crap")); | |
| 458 | try expectError(error.InvalidYear, parseYear4("r:bQ")); | |
| 459 | } | |
| 460 | ||
| 461 | fn parseTime(bytes: *const [6]u8) !Time { | |
| 462 | return .{ | |
| 463 | .hour = try parseTimeDigits(bytes[0..2], 0, 23), | |
| 464 | .minute = try parseTimeDigits(bytes[2..4], 0, 59), | |
| 465 | .second = try parseTimeDigits(bytes[4..6], 0, 59), | |
| 466 | }; | |
| 467 | } | |
| @@ -1,132 +0,0 @@ | ||
| 1 | //! Developed by ITU-U and ISO/IEC for naming objects. Used in DER. | |
| 2 | //! | |
| 3 | //! This implementation supports any number of `u32` arcs. | |
| 4 | ||
| 5 | const Arc = u32; | |
| 6 | const encoding_base = 128; | |
| 7 | ||
| 8 | /// Returns encoded length. | |
| 9 | pub fn encodeLen(dot_notation: []const u8) !usize { | |
| 10 | var split = std.mem.splitScalar(u8, dot_notation, '.'); | |
| 11 | if (split.next() == null) return 0; | |
| 12 | if (split.next() == null) return 1; | |
| 13 | ||
| 14 | var res: usize = 1; | |
| 15 | while (split.next()) |s| { | |
| 16 | const parsed = try std.fmt.parseUnsigned(Arc, s, 10); | |
| 17 | const n_bytes = if (parsed == 0) 0 else std.math.log(Arc, encoding_base, parsed); | |
| 18 | ||
| 19 | res += n_bytes; | |
| 20 | res += 1; | |
| 21 | } | |
| 22 | ||
| 23 | return res; | |
| 24 | } | |
| 25 | ||
| 26 | pub const EncodeError = std.fmt.ParseIntError || error{ | |
| 27 | MissingPrefix, | |
| 28 | BufferTooSmall, | |
| 29 | }; | |
| 30 | ||
| 31 | pub fn encode(dot_notation: []const u8, buf: []u8) EncodeError![]const u8 { | |
| 32 | if (buf.len < try encodeLen(dot_notation)) return error.BufferTooSmall; | |
| 33 | ||
| 34 | var split = std.mem.splitScalar(u8, dot_notation, '.'); | |
| 35 | const first_str = split.next() orelse return error.MissingPrefix; | |
| 36 | const second_str = split.next() orelse return error.MissingPrefix; | |
| 37 | ||
| 38 | const first = try std.fmt.parseInt(u8, first_str, 10); | |
| 39 | const second = try std.fmt.parseInt(u8, second_str, 10); | |
| 40 | ||
| 41 | buf[0] = first * 40 + second; | |
| 42 | ||
| 43 | var i: usize = 1; | |
| 44 | while (split.next()) |s| { | |
| 45 | var parsed = try std.fmt.parseUnsigned(Arc, s, 10); | |
| 46 | const n_bytes = if (parsed == 0) 0 else std.math.log(Arc, encoding_base, parsed); | |
| 47 | ||
| 48 | for (0..n_bytes) |j| { | |
| 49 | const place = std.math.pow(Arc, encoding_base, n_bytes - @as(Arc, @intCast(j))); | |
| 50 | const digit: u8 = @intCast(@divFloor(parsed, place)); | |
| 51 | ||
| 52 | buf[i] = digit | 0x80; | |
| 53 | parsed -= digit * place; | |
| 54 | ||
| 55 | i += 1; | |
| 56 | } | |
| 57 | buf[i] = @intCast(parsed); | |
| 58 | i += 1; | |
| 59 | } | |
| 60 | ||
| 61 | return buf[0..i]; | |
| 62 | } | |
| 63 | ||
| 64 | pub fn decode(encoded: []const u8, writer: anytype) @TypeOf(writer).Error!void { | |
| 65 | const first = @divTrunc(encoded[0], 40); | |
| 66 | const second = encoded[0] - first * 40; | |
| 67 | try writer.print("{d}.{d}", .{ first, second }); | |
| 68 | ||
| 69 | var i: usize = 1; | |
| 70 | while (i != encoded.len) { | |
| 71 | const n_bytes: usize = brk: { | |
| 72 | var res: usize = 1; | |
| 73 | var j: usize = i; | |
| 74 | while (encoded[j] & 0x80 != 0) { | |
| 75 | res += 1; | |
| 76 | j += 1; | |
| 77 | } | |
| 78 | break :brk res; | |
| 79 | }; | |
| 80 | ||
| 81 | var n: usize = 0; | |
| 82 | for (0..n_bytes) |j| { | |
| 83 | const place = std.math.pow(usize, encoding_base, n_bytes - j - 1); | |
| 84 | n += place * (encoded[i] & 0b01111111); | |
| 85 | i += 1; | |
| 86 | } | |
| 87 | try writer.print(".{d}", .{n}); | |
| 88 | } | |
| 89 | } | |
| 90 | ||
| 91 | pub fn encodeComptime(comptime dot_notation: []const u8) [encodeLen(dot_notation) catch unreachable]u8 { | |
| 92 | @setEvalBranchQuota(10_000); | |
| 93 | var buf: [encodeLen(dot_notation) catch unreachable]u8 = undefined; | |
| 94 | _ = encode(dot_notation, &buf) catch unreachable; | |
| 95 | return buf; | |
| 96 | } | |
| 97 | ||
| 98 | const std = ; | |
| 99 | ||
| 100 | fn testOid(expected_encoded: []const u8, expected_dot_notation: []const u8) !void { | |
| 101 | var buf: [256]u8 = undefined; | |
| 102 | const encoded = try encode(expected_dot_notation, &buf); | |
| 103 | try std.testing.expectEqualSlices(u8, expected_encoded, encoded); | |
| 104 | ||
| 105 | var stream = std.io.fixedBufferStream(&buf); | |
| 106 | try decode(expected_encoded, stream.writer()); | |
| 107 | try std.testing.expectEqualStrings(expected_dot_notation, stream.getWritten()); | |
| 108 | } | |
| 109 | ||
| 110 | test "encode and decode" { | |
| 111 | // https://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-object-identifier | |
| 112 | try testOid( | |
| 113 | &[_]u8{ 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x15, 0x14 }, | |
| 114 | "1.3.6.1.4.1.311.21.20", | |
| 115 | ); | |
| 116 | // https://luca.ntop.org/Teaching/Appunti/asn1.html | |
| 117 | try testOid(&[_]u8{ 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d }, "1.2.840.113549"); | |
| 118 | // https://www.sysadmins.lv/blog-en/how-to-encode-object-identifier-to-an-asn1-der-encoded-string.aspx | |
| 119 | try testOid(&[_]u8{ 0x2a, 0x86, 0x8d, 0x20 }, "1.2.100000"); | |
| 120 | try testOid( | |
| 121 | &[_]u8{ 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b }, | |
| 122 | "1.2.840.113549.1.1.11", | |
| 123 | ); | |
| 124 | try testOid(&[_]u8{ 0x2b, 0x65, 0x70 }, "1.3.101.112"); | |
| 125 | } | |
| 126 | ||
| 127 | test encodeComptime { | |
| 128 | try std.testing.expectEqual( | |
| 129 | [_]u8{ 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x15, 0x14 }, | |
| 130 | encodeComptime("1.3.6.1.4.1.311.21.20"), | |
| 131 | ); | |
| 132 | } | |
| @@ -1,900 +0,0 @@ | ||
| 1 | //! RFC8017: Public Key Cryptography Standards #1 v2.2 (PKCS1) | |
| 2 | const std = ; | |
| 3 | const der = ; | |
| 4 | const ff = std.crypto.ff; | |
| 5 | const rng = ; | |
| 6 | ||
| 7 | pub const max_modulus_bits = 4096; | |
| 8 | const max_modulus_len = max_modulus_bits / 8; | |
| 9 | ||
| 10 | const Modulus = std.crypto.ff.Modulus(max_modulus_bits); | |
| 11 | const Fe = Modulus.Fe; | |
| 12 | ||
| 13 | pub const ValueError = error{ | |
| 14 | Modulus, | |
| 15 | Exponent, | |
| 16 | }; | |
| 17 | ||
| 18 | pub const PublicKey = struct { | |
| 19 | /// `n` | |
| 20 | modulus: Modulus, | |
| 21 | /// `e` | |
| 22 | public_exponent: Fe, | |
| 23 | ||
| 24 | pub const FromBytesError = ValueError || ff.OverflowError || ff.FieldElementError || ff.InvalidModulusError || error{InsecureBitCount}; | |
| 25 | ||
| 26 | pub fn fromBytes(mod: []const u8, exp: []const u8) FromBytesError!PublicKey { | |
| 27 | const modulus = try Modulus.fromBytes(mod, .big); | |
| 28 | if (modulus.bits() <= 512) return error.InsecureBitCount; | |
| 29 | const public_exponent = try Fe.fromBytes(modulus, exp, .big); | |
| 30 | ||
| 31 | if (std.debug.runtime_safety) { | |
| 32 | // > the RSA public exponent e is an integer between 3 and n - 1 satisfying | |
| 33 | // > GCD(e,\lambda(n)) = 1, where \lambda(n) = LCM(r_1 - 1, ..., r_u - 1) | |
| 34 | const e_v = public_exponent.toPrimitive(u32) catch return error.Exponent; | |
| 35 | if (!public_exponent.isOdd()) return error.Exponent; | |
| 36 | if (e_v < 3) return error.Exponent; | |
| 37 | if (modulus.v.compare(public_exponent.v) == .lt) return error.Exponent; | |
| 38 | } | |
| 39 | ||
| 40 | return .{ .modulus = modulus, .public_exponent = public_exponent }; | |
| 41 | } | |
| 42 | ||
| 43 | pub fn fromDer(bytes: []const u8) (der.Parser.Error || FromBytesError)!PublicKey { | |
| 44 | var parser = der.Parser{ .bytes = bytes }; | |
| 45 | ||
| 46 | const seq = try parser.expectSequence(); | |
| 47 | defer parser.seek(seq.slice.end); | |
| 48 | ||
| 49 | const modulus = try parser.expectPrimitive(.integer); | |
| 50 | const pub_exp = try parser.expectPrimitive(.integer); | |
| 51 | ||
| 52 | try parser.expectEnd(seq.slice.end); | |
| 53 | try parser.expectEnd(bytes.len); | |
| 54 | ||
| 55 | return try fromBytes(parser.view(modulus), parser.view(pub_exp)); | |
| 56 | } | |
| 57 | ||
| 58 | /// Parse an RSA public key from a DER-encoded `SubjectPublicKeyInfo` | |
| 59 | /// (X.509 / OpenSSL `-pubout` / DKIM `p=` format): unwrap the | |
| 60 | /// AlgorithmIdentifier + BIT STRING and parse the inner PKCS#1 RSAPublicKey. | |
| 61 | pub fn fromSpki(bytes: []const u8) (der.Parser.Error || FromBytesError)!PublicKey { | |
| 62 | var parser = der.Parser{ .bytes = bytes }; | |
| 63 | _ = try parser.expectSequence(); // SubjectPublicKeyInfo | |
| 64 | const algorithm = try parser.expectSequence(); // AlgorithmIdentifier | |
| 65 | parser.seek(algorithm.slice.end); | |
| 66 | const subject_public_key = try parser.expectBitstring(); | |
| 67 | return try fromDer(subject_public_key.bytes); | |
| 68 | } | |
| 69 | ||
| 70 | /// Deprecated. | |
| 71 | /// | |
| 72 | /// Encrypt a short message using RSAES-PKCS1-v1_5. | |
| 73 | /// The use of this scheme for encrypting an arbitrary message, as opposed to a | |
| 74 | /// randomly generated key, is NOT RECOMMENDED. | |
| 75 | pub fn encryptPkcsv1_5(pk: PublicKey, msg: []const u8, out: []u8) ![]const u8 { | |
| 76 | // align variable names with spec | |
| 77 | const k = byteLen(pk.modulus.bits()); | |
| 78 | if (out.len < k) return error.BufferTooSmall; | |
| 79 | if (msg.len > k - 11) return error.MessageTooLong; | |
| 80 | ||
| 81 | // EM = 0x00 || 0x02 || PS || 0x00 || M. | |
| 82 | var em = out[0..k]; | |
| 83 | em[0] = 0; | |
| 84 | em[1] = 2; | |
| 85 | ||
| 86 | const ps = em[2..][0 .. k - msg.len - 3]; | |
| 87 | // Section: 7.2.1 | |
| 88 | // PS consists of pseudo-randomly generated nonzero octets. | |
| 89 | for (ps) |*v| { | |
| 90 | var rnd_byte: [1]u8 = undefined; | |
| 91 | rng.fill(&rnd_byte); | |
| 92 | v.* = if (rnd_byte[0] == 0) 1 else rnd_byte[0]; | |
| 93 | } | |
| 94 | ||
| 95 | em[em.len - msg.len - 1] = 0; | |
| 96 | @memcpy(em[em.len - msg.len ..][0..msg.len], msg); | |
| 97 | ||
| 98 | const m = try Fe.fromBytes(pk.modulus, em, .big); | |
| 99 | const e = try pk.modulus.powPublic(m, pk.public_exponent); | |
| 100 | try e.toBytes(em, .big); | |
| 101 | return em; | |
| 102 | } | |
| 103 | ||
| 104 | /// Encrypt a short message using Optimal Asymmetric Encryption Padding (RSAES-OAEP). | |
| 105 | pub fn encryptOaep( | |
| 106 | pk: PublicKey, | |
| 107 | comptime Hash: type, | |
| 108 | msg: []const u8, | |
| 109 | label: []const u8, | |
| 110 | out: []u8, | |
| 111 | ) ![]const u8 { | |
| 112 | // align variable names with spec | |
| 113 | const k = byteLen(pk.modulus.bits()); | |
| 114 | if (out.len < k) return error.BufferTooSmall; | |
| 115 | ||
| 116 | if (msg.len > k - 2 * Hash.digest_length - 2) return error.MessageTooLong; | |
| 117 | ||
| 118 | // EM = 0x00 || maskedSeed || maskedDB. | |
| 119 | var em = out[0..k]; | |
| 120 | em[0] = 0; | |
| 121 | const seed = em[1..][0..Hash.digest_length]; | |
| 122 | rng.fill(seed); | |
| 123 | ||
| 124 | // DB = lHash || PS || 0x01 || M. | |
| 125 | var db = em[1 + seed.len ..]; | |
| 126 | const lHash = labelHash(Hash, label); | |
| 127 | @memcpy(db[0..lHash.len], &lHash); | |
| 128 | @memset(db[lHash.len .. db.len - msg.len - 2], 0); | |
| 129 | db[db.len - msg.len - 1] = 1; | |
| 130 | @memcpy(db[db.len - msg.len ..], msg); | |
| 131 | ||
| 132 | var mgf_buf: [max_modulus_len]u8 = undefined; | |
| 133 | ||
| 134 | const db_mask = mgf1(Hash, seed, mgf_buf[0..db.len]); | |
| 135 | for (db, db_mask) |*v, m| v.* ^= m; | |
| 136 | ||
| 137 | const seed_mask = mgf1(Hash, db, mgf_buf[0..seed.len]); | |
| 138 | for (seed, seed_mask) |*v, m| v.* ^= m; | |
| 139 | ||
| 140 | const m = try Fe.fromBytes(pk.modulus, em, .big); | |
| 141 | const e = try pk.modulus.powPublic(m, pk.public_exponent); | |
| 142 | try e.toBytes(em, .big); | |
| 143 | return em; | |
| 144 | } | |
| 145 | }; | |
| 146 | ||
| 147 | pub fn byteLen(bits: usize) usize { | |
| 148 | return std.math.divCeil(usize, bits, 8) catch unreachable; | |
| 149 | } | |
| 150 | ||
| 151 | pub const SecretKey = struct { | |
| 152 | /// `d` | |
| 153 | private_exponent: Fe, | |
| 154 | ||
| 155 | pub const FromBytesError = ValueError || ff.OverflowError || ff.FieldElementError; | |
| 156 | ||
| 157 | pub fn fromBytes(n: Modulus, exp: []const u8) FromBytesError!SecretKey { | |
| 158 | const d = try Fe.fromBytes(n, exp, .big); | |
| 159 | if (std.debug.runtime_safety) { | |
| 160 | // > The RSA private exponent d is a positive integer less than n | |
| 161 | // > satisfying e * d == 1 (mod \lambda(n)), | |
| 162 | if (!d.isOdd()) return error.Exponent; | |
| 163 | if (d.v.compare(n.v) != .lt) return error.Exponent; | |
| 164 | } | |
| 165 | ||
| 166 | return .{ .private_exponent = d }; | |
| 167 | } | |
| 168 | }; | |
| 169 | ||
| 170 | pub const KeyPair = struct { | |
| 171 | public: PublicKey, | |
| 172 | secret: SecretKey, | |
| 173 | ||
| 174 | pub const FromDerError = PublicKey.FromBytesError || SecretKey.FromBytesError || der.Parser.Error || error{ KeyMismatch, InvalidVersion }; | |
| 175 | ||
| 176 | pub fn fromDer(bytes: []const u8) FromDerError!KeyPair { | |
| 177 | var parser = der.Parser{ .bytes = bytes }; | |
| 178 | const seq = try parser.expectSequence(); | |
| 179 | const version = try parser.expectInt(u8); | |
| 180 | ||
| 181 | const mod = try parser.expectPrimitive(.integer); | |
| 182 | const pub_exp = try parser.expectPrimitive(.integer); | |
| 183 | const sec_exp = try parser.expectPrimitive(.integer); | |
| 184 | ||
| 185 | const public = try PublicKey.fromBytes(parser.view(mod), parser.view(pub_exp)); | |
| 186 | const secret = try SecretKey.fromBytes(public.modulus, parser.view(sec_exp)); | |
| 187 | ||
| 188 | const prime1 = try parser.expectPrimitive(.integer); | |
| 189 | const prime2 = try parser.expectPrimitive(.integer); | |
| 190 | const exp1 = try parser.expectPrimitive(.integer); | |
| 191 | const exp2 = try parser.expectPrimitive(.integer); | |
| 192 | const coeff = try parser.expectPrimitive(.integer); | |
| 193 | _ = .{ exp1, exp2, coeff }; | |
| 194 | ||
| 195 | switch (version) { | |
| 196 | 0 => {}, | |
| 197 | 1 => { | |
| 198 | _ = try parser.expectSequenceOf(); | |
| 199 | while (!parser.eof()) { | |
| 200 | _ = try parser.expectSequence(); | |
| 201 | const ri = try parser.expectPrimitive(.integer); | |
| 202 | const di = try parser.expectPrimitive(.integer); | |
| 203 | const ti = try parser.expectPrimitive(.integer); | |
| 204 | _ = .{ ri, di, ti }; | |
| 205 | } | |
| 206 | }, | |
| 207 | else => return error.InvalidVersion, | |
| 208 | } | |
| 209 | ||
| 210 | try parser.expectEnd(seq.slice.end); | |
| 211 | try parser.expectEnd(bytes.len); | |
| 212 | ||
| 213 | if (std.debug.runtime_safety) { | |
| 214 | const p = try Fe.fromBytes(public.modulus, parser.view(prime1), .big); | |
| 215 | const q = try Fe.fromBytes(public.modulus, parser.view(prime2), .big); | |
| 216 | ||
| 217 | // check that n = p * q | |
| 218 | const expected_zero = public.modulus.mul(p, q); | |
| 219 | if (!expected_zero.isZero()) return error.KeyMismatch; | |
| 220 | ||
| 221 | // TODO: check that d * e is one mod p-1 and mod q-1. Note d and e were bound | |
| 222 | // const de = secret.private_exponent.mul(public.public_exponent); | |
| 223 | // const one = public.modulus.one(); | |
| 224 | ||
| 225 | // if (public.modulus.mul(de, p).compare(one) != .eq) return error.KeyMismatch; | |
| 226 | // if (public.modulus.mul(de, q).compare(one) != .eq) return error.KeyMismatch; | |
| 227 | } | |
| 228 | ||
| 229 | return .{ .public = public, .secret = secret }; | |
| 230 | } | |
| 231 | ||
| 232 | /// Deprecated. | |
| 233 | pub fn signPkcsv1_5(kp: KeyPair, comptime Hash: type, msg: []const u8, out: []u8) !PKCS1v1_5(Hash).Signature { | |
| 234 | var st = try signerPkcsv1_5(kp, Hash); | |
| 235 | st.update(msg); | |
| 236 | return try st.finalize(out); | |
| 237 | } | |
| 238 | ||
| 239 | /// Deprecated. | |
| 240 | pub fn signerPkcsv1_5(kp: KeyPair, comptime Hash: type) !PKCS1v1_5(Hash).Signer { | |
| 241 | return PKCS1v1_5(Hash).Signer.init(kp); | |
| 242 | } | |
| 243 | ||
| 244 | /// Deprecated. | |
| 245 | pub fn decryptPkcsv1_5(kp: KeyPair, ciphertext: []const u8, out: []u8) ![]const u8 { | |
| 246 | const k = byteLen(kp.public.modulus.bits()); | |
| 247 | if (out.len < k) return error.BufferTooSmall; | |
| 248 | ||
| 249 | const em = out[0..k]; | |
| 250 | ||
| 251 | const m = try Fe.fromBytes(kp.public.modulus, ciphertext, .big); | |
| 252 | const e = try kp.public.modulus.pow(m, kp.secret.private_exponent); | |
| 253 | try e.toBytes(em, .big); | |
| 254 | ||
| 255 | // Care shall be taken to ensure that an opponent cannot | |
| 256 | // distinguish these error conditions, whether by error | |
| 257 | // message or timing. | |
| 258 | const msg_start = ct.lastIndexOfScalar(em, 0) orelse em.len; | |
| 259 | const ps_len = em.len - msg_start; | |
| 260 | if (ct.@"or"(em[0] != 0, ct.@"or"(em[1] != 2, ps_len < 8))) { | |
| 261 | return error.Inconsistent; | |
| 262 | } | |
| 263 | ||
| 264 | return em[msg_start + 1 ..]; | |
| 265 | } | |
| 266 | ||
| 267 | pub fn signOaep( | |
| 268 | kp: KeyPair, | |
| 269 | comptime Hash: type, | |
| 270 | msg: []const u8, | |
| 271 | salt: ?[]const u8, | |
| 272 | out: []u8, | |
| 273 | ) !Pss(Hash).Signature { | |
| 274 | var st = try signerOaep(kp, Hash, salt); | |
| 275 | st.update(msg); | |
| 276 | return try st.finalize(out); | |
| 277 | } | |
| 278 | ||
| 279 | /// Salt must outlive returned `PSS.Signer`. | |
| 280 | pub fn signerOaep(kp: KeyPair, comptime Hash: type, salt: ?[]const u8) !Pss(Hash).Signer { | |
| 281 | return Pss(Hash).Signer.init(kp, salt); | |
| 282 | } | |
| 283 | ||
| 284 | pub fn decryptOaep( | |
| 285 | kp: KeyPair, | |
| 286 | comptime Hash: type, | |
| 287 | ciphertext: []const u8, | |
| 288 | label: []const u8, | |
| 289 | out: []u8, | |
| 290 | ) ![]u8 { | |
| 291 | // align variable names with spec | |
| 292 | const k = byteLen(kp.public.modulus.bits()); | |
| 293 | if (out.len < k) return error.BufferTooSmall; | |
| 294 | ||
| 295 | const mod = try Fe.fromBytes(kp.public.modulus, ciphertext, .big); | |
| 296 | const exp = kp.public.modulus.pow(mod, kp.secret.private_exponent) catch unreachable; | |
| 297 | const em = out[0..k]; | |
| 298 | try exp.toBytes(em, .big); | |
| 299 | ||
| 300 | const y = em[0]; | |
| 301 | const seed = em[1..][0..Hash.digest_length]; | |
| 302 | const db = em[1 + Hash.digest_length ..]; | |
| 303 | ||
| 304 | var mgf_buf: [max_modulus_len]u8 = undefined; | |
| 305 | ||
| 306 | const seed_mask = mgf1(Hash, db, mgf_buf[0..seed.len]); | |
| 307 | for (seed, seed_mask) |*v, m| v.* ^= m; | |
| 308 | ||
| 309 | const db_mask = mgf1(Hash, seed, mgf_buf[0..db.len]); | |
| 310 | for (db, db_mask) |*v, m| v.* ^= m; | |
| 311 | ||
| 312 | const expected_hash = labelHash(Hash, label); | |
| 313 | const actual_hash = db[0..expected_hash.len]; | |
| 314 | ||
| 315 | // Care shall be taken to ensure that an opponent cannot | |
| 316 | // distinguish these error conditions, whether by error | |
| 317 | // message or timing. | |
| 318 | const msg_start = ct.indexOfScalarPos(em, expected_hash.len + 1, 1) orelse 0; | |
| 319 | if (ct.@"or"(y != 0, ct.@"or"(msg_start == 0, !ct.memEql(&expected_hash, actual_hash)))) { | |
| 320 | return error.Inconsistent; | |
| 321 | } | |
| 322 | ||
| 323 | return em[msg_start + 1 ..]; | |
| 324 | } | |
| 325 | ||
| 326 | /// Encrypt short plaintext with secret key. | |
| 327 | pub fn encrypt(kp: KeyPair, plaintext: []const u8, out: []u8) !void { | |
| 328 | const n = kp.public.modulus; | |
| 329 | const k = byteLen(n.bits()); | |
| 330 | if (plaintext.len > k) return error.MessageTooLong; | |
| 331 | ||
| 332 | const msg_as_int = try Fe.fromBytes(n, plaintext, .big); | |
| 333 | const enc_as_int = try n.pow(msg_as_int, kp.secret.private_exponent); | |
| 334 | try enc_as_int.toBytes(out, .big); | |
| 335 | } | |
| 336 | }; | |
| 337 | ||
| 338 | /// Deprecated. | |
| 339 | /// | |
| 340 | /// Signature Scheme with Appendix v1.5 (RSASSA-PKCS1-v1_5) | |
| 341 | /// | |
| 342 | /// This standard has been superceded by PSS which is formally proven secure | |
| 343 | /// and has fewer footguns. | |
| 344 | pub fn PKCS1v1_5(comptime Hash: type) type { | |
| 345 | return struct { | |
| 346 | const PkcsT = @This(); | |
| 347 | pub const Signature = struct { | |
| 348 | bytes: []const u8, | |
| 349 | ||
| 350 | const Self = @This(); | |
| 351 | ||
| 352 | pub fn verifier(self: Self, public_key: PublicKey) !Verifier { | |
| 353 | return Verifier.init(self, public_key); | |
| 354 | } | |
| 355 | ||
| 356 | pub fn verify(self: Self, msg: []const u8, public_key: PublicKey) !void { | |
| 357 | var st = Verifier.init(self, public_key); | |
| 358 | st.update(msg); | |
| 359 | return st.verify(); | |
| 360 | } | |
| 361 | }; | |
| 362 | ||
| 363 | pub const Signer = struct { | |
| 364 | h: Hash, | |
| 365 | key_pair: KeyPair, | |
| 366 | ||
| 367 | fn init(key_pair: KeyPair) Signer { | |
| 368 | return .{ | |
| 369 | .h = Hash.init(.{}), | |
| 370 | .key_pair = key_pair, | |
| 371 | }; | |
| 372 | } | |
| 373 | ||
| 374 | pub fn update(self: *Signer, data: []const u8) void { | |
| 375 | self.h.update(data); | |
| 376 | } | |
| 377 | ||
| 378 | pub fn finalize(self: *Signer, out: []u8) !PkcsT.Signature { | |
| 379 | const k = byteLen(self.key_pair.public.modulus.bits()); | |
| 380 | if (out.len < k) return error.BufferTooSmall; | |
| 381 | ||
| 382 | var hash: [Hash.digest_length]u8 = undefined; | |
| 383 | self.h.final(&hash); | |
| 384 | ||
| 385 | const em = try emsaEncode(hash, out[0..k]); | |
| 386 | try self.key_pair.encrypt(em, em); | |
| 387 | return .{ .bytes = em }; | |
| 388 | } | |
| 389 | }; | |
| 390 | ||
| 391 | pub const Verifier = struct { | |
| 392 | h: Hash, | |
| 393 | sig: PkcsT.Signature, | |
| 394 | public_key: PublicKey, | |
| 395 | ||
| 396 | fn init(sig: PkcsT.Signature, public_key: PublicKey) Verifier { | |
| 397 | return Verifier{ | |
| 398 | .h = Hash.init(.{}), | |
| 399 | .sig = sig, | |
| 400 | .public_key = public_key, | |
| 401 | }; | |
| 402 | } | |
| 403 | ||
| 404 | pub fn update(self: *Verifier, data: []const u8) void { | |
| 405 | self.h.update(data); | |
| 406 | } | |
| 407 | ||
| 408 | pub fn verify(self: *Verifier) !void { | |
| 409 | const pk = self.public_key; | |
| 410 | const s = try Fe.fromBytes(pk.modulus, self.sig.bytes, .big); | |
| 411 | const emm = try pk.modulus.powPublic(s, pk.public_exponent); | |
| 412 | ||
| 413 | var em_buf: [max_modulus_len]u8 = undefined; | |
| 414 | const em = em_buf[0..byteLen(pk.modulus.bits())]; | |
| 415 | try emm.toBytes(em, .big); | |
| 416 | ||
| 417 | var hash: [Hash.digest_length]u8 = undefined; | |
| 418 | self.h.final(&hash); | |
| 419 | ||
| 420 | var em_buf2: [max_modulus_len]u8 = undefined; | |
| 421 | const expected_em = try emsaEncode(hash, em_buf2[0..byteLen(pk.modulus.bits())]); | |
| 422 | if (!std.mem.eql(u8, expected_em, em)) return error.Inconsistent; | |
| 423 | } | |
| 424 | }; | |
| 425 | ||
| 426 | /// PKCS Encrypted Message Signature Appendix | |
| 427 | fn emsaEncode(hash: [Hash.digest_length]u8, out: []u8) ![]u8 { | |
| 428 | const digest_header = comptime digestHeader(); | |
| 429 | const tLen = digest_header.len + Hash.digest_length; | |
| 430 | const emLen = out.len; | |
| 431 | if (emLen < tLen + 11) return error.ModulusTooShort; | |
| 432 | if (out.len < emLen) return error.BufferTooSmall; | |
| 433 | ||
| 434 | var res = out[0..emLen]; | |
| 435 | res[0] = 0; | |
| 436 | res[1] = 1; | |
| 437 | const padding_len = emLen - tLen - 3; | |
| 438 | @memset(res[2..][0..padding_len], 0xff); | |
| 439 | res[2 + padding_len] = 0; | |
| 440 | @memcpy(res[2 + padding_len + 1 ..][0..digest_header.len], digest_header); | |
| 441 | @memcpy(res[res.len - hash.len ..], &hash); | |
| 442 | ||
| 443 | return res; | |
| 444 | } | |
| 445 | ||
| 446 | /// DER encoded header. Sequence of digest algo + digest. | |
| 447 | /// TODO: use a DER encoder instead | |
| 448 | fn digestHeader() []const u8 { | |
| 449 | const sha2 = std.crypto.hash.sha2; | |
| 450 | // Section 9.2 Notes 1. | |
| 451 | return switch (Hash) { | |
| 452 | std.crypto.hash.Sha1 => &hexToBytes( | |
| 453 | \\30 21 30 09 06 05 2b 0e 03 02 1a 05 00 04 14 | |
| 454 | ), | |
| 455 | sha2.Sha224 => &hexToBytes( | |
| 456 | \\30 2d 30 0d 06 09 60 86 48 01 65 03 04 02 04 | |
| 457 | \\05 00 04 1c | |
| 458 | ), | |
| 459 | sha2.Sha256 => &hexToBytes( | |
| 460 | \\30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 | |
| 461 | \\04 20 | |
| 462 | ), | |
| 463 | sha2.Sha384 => &hexToBytes( | |
| 464 | \\30 41 30 0d 06 09 60 86 48 01 65 03 04 02 02 05 00 | |
| 465 | \\04 30 | |
| 466 | ), | |
| 467 | sha2.Sha512 => &hexToBytes( | |
| 468 | \\30 51 30 0d 06 09 60 86 48 01 65 03 04 02 03 05 00 | |
| 469 | \\04 40 | |
| 470 | ), | |
| 471 | // sha2.Sha512224 => &hexToBytes( | |
| 472 | // \\30 2d 30 0d 06 09 60 86 48 01 65 03 04 02 05 | |
| 473 | // \\05 00 04 1c | |
| 474 | // ), | |
| 475 | // sha2.Sha512256 => &hexToBytes( | |
| 476 | // \\30 31 30 0d 06 09 60 86 48 01 65 03 04 02 06 | |
| 477 | // \\05 00 04 20 | |
| 478 | // ), | |
| 479 | else => @compileError("unknown Hash " ++ @typeName(Hash)), | |
| 480 | }; | |
| 481 | } | |
| 482 | }; | |
| 483 | } | |
| 484 | ||
| 485 | /// Probabilistic Signature Scheme (RSASSA-PSS) | |
| 486 | pub fn Pss(comptime Hash: type) type { | |
| 487 | // RFC 4055 S3.1 | |
| 488 | const default_salt_len = Hash.digest_length; | |
| 489 | return struct { | |
| 490 | pub const Signature = struct { | |
| 491 | bytes: []const u8, | |
| 492 | ||
| 493 | const Self = @This(); | |
| 494 | ||
| 495 | pub fn verifier(self: Self, public_key: PublicKey) !Verifier { | |
| 496 | return Verifier.init(self, public_key); | |
| 497 | } | |
| 498 | ||
| 499 | pub fn verify(self: Self, msg: []const u8, public_key: PublicKey, salt_len: ?usize) !void { | |
| 500 | var st = Verifier.init(self, public_key, salt_len orelse default_salt_len); | |
| 501 | st.update(msg); | |
| 502 | return st.verify(); | |
| 503 | } | |
| 504 | }; | |
| 505 | ||
| 506 | const PssT = @This(); | |
| 507 | ||
| 508 | pub const Signer = struct { | |
| 509 | h: Hash, | |
| 510 | key_pair: KeyPair, | |
| 511 | salt: ?[]const u8, | |
| 512 | ||
| 513 | fn init(key_pair: KeyPair, salt: ?[]const u8) Signer { | |
| 514 | return .{ | |
| 515 | .h = Hash.init(.{}), | |
| 516 | .key_pair = key_pair, | |
| 517 | .salt = salt, | |
| 518 | }; | |
| 519 | } | |
| 520 | ||
| 521 | pub fn update(self: *Signer, data: []const u8) void { | |
| 522 | self.h.update(data); | |
| 523 | } | |
| 524 | ||
| 525 | pub fn finalize(self: *Signer, out: []u8) !PssT.Signature { | |
| 526 | var hashed: [Hash.digest_length]u8 = undefined; | |
| 527 | self.h.final(&hashed); | |
| 528 | ||
| 529 | const salt = if (self.salt) |s| s else brk: { | |
| 530 | var res: [default_salt_len]u8 = undefined; | |
| 531 | rng.fill(&res); | |
| 532 | break :brk &res; | |
| 533 | }; | |
| 534 | ||
| 535 | const em_bits = self.key_pair.public.modulus.bits() - 1; | |
| 536 | const em = try emsaEncode(hashed, salt, em_bits, out); | |
| 537 | try self.key_pair.encrypt(em, em); | |
| 538 | return .{ .bytes = em }; | |
| 539 | } | |
| 540 | }; | |
| 541 | ||
| 542 | pub const Verifier = struct { | |
| 543 | h: Hash, | |
| 544 | sig: PssT.Signature, | |
| 545 | public_key: PublicKey, | |
| 546 | salt_len: usize, | |
| 547 | ||
| 548 | fn init(sig: PssT.Signature, public_key: PublicKey, salt_len: usize) Verifier { | |
| 549 | return Verifier{ | |
| 550 | .h = Hash.init(.{}), | |
| 551 | .sig = sig, | |
| 552 | .public_key = public_key, | |
| 553 | .salt_len = salt_len, | |
| 554 | }; | |
| 555 | } | |
| 556 | ||
| 557 | pub fn update(self: *Verifier, data: []const u8) void { | |
| 558 | self.h.update(data); | |
| 559 | } | |
| 560 | ||
| 561 | pub fn verify(self: *Verifier) !void { | |
| 562 | const pk = self.public_key; | |
| 563 | const s = try Fe.fromBytes(pk.modulus, self.sig.bytes, .big); | |
| 564 | const emm = try pk.modulus.powPublic(s, pk.public_exponent); | |
| 565 | ||
| 566 | var em_buf: [max_modulus_len]u8 = undefined; | |
| 567 | const em_bits = pk.modulus.bits() - 1; | |
| 568 | const em_len = std.math.divCeil(usize, em_bits, 8) catch unreachable; | |
| 569 | var em = em_buf[0..em_len]; | |
| 570 | try emm.toBytes(em, .big); | |
| 571 | ||
| 572 | if (em.len < Hash.digest_length + self.salt_len + 2) return error.Inconsistent; | |
| 573 | if (em[em.len - 1] != 0xbc) return error.Inconsistent; | |
| 574 | ||
| 575 | const db = em[0 .. em.len - Hash.digest_length - 1]; | |
| 576 | if (@clz(db[0]) < em.len * 8 - em_bits) return error.Inconsistent; | |
| 577 | ||
| 578 | const expected_hash = em[db.len..][0..Hash.digest_length]; | |
| 579 | var mgf_buf: [max_modulus_len]u8 = undefined; | |
| 580 | const db_mask = mgf1(Hash, expected_hash, mgf_buf[0..db.len]); | |
| 581 | for (db, db_mask) |*v, m| v.* ^= m; | |
| 582 | ||
| 583 | for (1..db.len - self.salt_len - 1) |i| { | |
| 584 | if (db[i] != 0) return error.Inconsistent; | |
| 585 | } | |
| 586 | if (db[db.len - self.salt_len - 1] != 1) return error.Inconsistent; | |
| 587 | const salt = db[db.len - self.salt_len ..]; | |
| 588 | var mp_buf: [max_modulus_len]u8 = undefined; | |
| 589 | var mp = mp_buf[0 .. 8 + Hash.digest_length + self.salt_len]; | |
| 590 | @memset(mp[0..8], 0); | |
| 591 | self.h.final(mp[8..][0..Hash.digest_length]); | |
| 592 | @memcpy(mp[8 + Hash.digest_length ..][0..salt.len], salt); | |
| 593 | ||
| 594 | var actual_hash: [Hash.digest_length]u8 = undefined; | |
| 595 | Hash.hash(mp, &actual_hash, .{}); | |
| 596 | ||
| 597 | if (!std.mem.eql(u8, expected_hash, &actual_hash)) return error.Inconsistent; | |
| 598 | } | |
| 599 | }; | |
| 600 | ||
| 601 | /// PSS Encrypted Message Signature Appendix | |
| 602 | fn emsaEncode(msg_hash: [Hash.digest_length]u8, salt: []const u8, em_bits: usize, out: []u8) ![]u8 { | |
| 603 | const em_len = std.math.divCeil(usize, em_bits, 8) catch unreachable; | |
| 604 | ||
| 605 | if (em_len < Hash.digest_length + salt.len + 2) return error.Encoding; | |
| 606 | ||
| 607 | // EM = maskedDB || H || 0xbc | |
| 608 | var em = out[0..em_len]; | |
| 609 | em[em.len - 1] = 0xbc; | |
| 610 | ||
| 611 | var mp_buf: [max_modulus_len]u8 = undefined; | |
| 612 | // M' = (0x)00 00 00 00 00 00 00 00 || mHash || salt; | |
| 613 | const mp = mp_buf[0 .. 8 + Hash.digest_length + salt.len]; | |
| 614 | @memset(mp[0..8], 0); | |
| 615 | @memcpy(mp[8..][0..Hash.digest_length], &msg_hash); | |
| 616 | @memcpy(mp[8 + Hash.digest_length ..][0..salt.len], salt); | |
| 617 | ||
| 618 | // H = Hash(M') | |
| 619 | const hash = em[em.len - 1 - Hash.digest_length ..][0..Hash.digest_length]; | |
| 620 | Hash.hash(mp, hash, .{}); | |
| 621 | ||
| 622 | // DB = PS || 0x01 || salt | |
| 623 | var db = em[0 .. em_len - Hash.digest_length - 1]; | |
| 624 | @memset(db[0 .. db.len - salt.len - 1], 0); | |
| 625 | db[db.len - salt.len - 1] = 1; | |
| 626 | @memcpy(db[db.len - salt.len ..], salt); | |
| 627 | ||
| 628 | var mgf_buf: [max_modulus_len]u8 = undefined; | |
| 629 | const db_mask = mgf1(Hash, hash, mgf_buf[0..db.len]); | |
| 630 | for (db, db_mask) |*v, m| v.* ^= m; | |
| 631 | ||
| 632 | // Set the leftmost 8emLen - emBits bits of the leftmost octet | |
| 633 | // in maskedDB to zero. | |
| 634 | const shift = std.math.comptimeMod(8 * em_len - em_bits, 8); | |
| 635 | const mask = @as(u8, 0xff) >> shift; | |
| 636 | db[0] &= mask; | |
| 637 | ||
| 638 | return em; | |
| 639 | } | |
| 640 | }; | |
| 641 | } | |
| 642 | ||
| 643 | /// Mask generation function. Currently the only one defined. | |
| 644 | fn mgf1(comptime Hash: type, seed: []const u8, out: []u8) []u8 { | |
| 645 | var c: [@sizeOf(u32)]u8 = undefined; | |
| 646 | var tmp: [Hash.digest_length]u8 = undefined; | |
| 647 | ||
| 648 | var i: usize = 0; | |
| 649 | var counter: u32 = 0; | |
| 650 | while (i < out.len) : (counter += 1) { | |
| 651 | var hasher = Hash.init(.{}); | |
| 652 | hasher.update(seed); | |
| 653 | std.mem.writeInt(u32, &c, counter, .big); | |
| 654 | hasher.update(&c); | |
| 655 | ||
| 656 | const left = out.len - i; | |
| 657 | if (left >= Hash.digest_length) { | |
| 658 | // optimization: write straight to `out` | |
| 659 | hasher.final(out[i..][0..Hash.digest_length]); | |
| 660 | i += Hash.digest_length; | |
| 661 | } else { | |
| 662 | hasher.final(&tmp); | |
| 663 | @memcpy(out[i..][0..left], tmp[0..left]); | |
| 664 | i += left; | |
| 665 | } | |
| 666 | } | |
| 667 | ||
| 668 | return out; | |
| 669 | } | |
| 670 | ||
| 671 | test mgf1 { | |
| 672 | const Hash = std.crypto.hash.sha2.Sha256; | |
| 673 | var out: [Hash.digest_length * 2 + 1]u8 = undefined; | |
| 674 | try std.testing.expectEqualSlices( | |
| 675 | u8, | |
| 676 | &hexToBytes( | |
| 677 | \\ed 1b 84 6b b9 26 39 00 c8 17 82 ad 08 eb 17 01 | |
| 678 | \\fa 8c 72 21 c6 57 63 77 31 7f 5c e8 09 89 9f | |
| 679 | ), | |
| 680 | mgf1(Hash, "asdf", out[0 .. Hash.digest_length - 1]), | |
| 681 | ); | |
| 682 | try std.testing.expectEqualSlices( | |
| 683 | u8, | |
| 684 | &hexToBytes( | |
| 685 | \\ed 1b 84 6b b9 26 39 00 c8 17 82 ad 08 eb 17 01 | |
| 686 | \\fa 8c 72 21 c6 57 63 77 31 7f 5c e8 09 89 9f 5a | |
| 687 | \\22 F2 80 D5 28 08 F4 93 83 76 00 DE 09 E4 EC 92 | |
| 688 | \\4A 2C 7C EF 0D F7 7B BE 8F 7F 12 CB 8F 33 A6 65 | |
| 689 | \\AB | |
| 690 | ), | |
| 691 | mgf1(Hash, "asdf", &out), | |
| 692 | ); | |
| 693 | } | |
| 694 | ||
| 695 | /// For OAEP. | |
| 696 | fn labelHash(comptime Hash: type, label: []const u8) [Hash.digest_length]u8 { | |
| 697 | if (label.len == 0) { | |
| 698 | // magic constants from NIST | |
| 699 | const sha2 = std.crypto.hash.sha2; | |
| 700 | switch (Hash) { | |
| 701 | std.crypto.hash.Sha1 => return hexToBytes( | |
| 702 | \\da39a3ee 5e6b4b0d 3255bfef 95601890 | |
| 703 | \\afd80709 | |
| 704 | ), | |
| 705 | sha2.Sha256 => return hexToBytes( | |
| 706 | \\e3b0c442 98fc1c14 9afbf4c8 996fb924 | |
| 707 | \\27ae41e4 649b934c a495991b 7852b855 | |
| 708 | ), | |
| 709 | sha2.Sha384 => return hexToBytes( | |
| 710 | \\38b060a7 51ac9638 4cd9327e b1b1e36a | |
| 711 | \\21fdb711 14be0743 4c0cc7bf 63f6e1da | |
| 712 | \\274edebf e76f65fb d51ad2f1 4898b95b | |
| 713 | ), | |
| 714 | sha2.Sha512 => return hexToBytes( | |
| 715 | \\cf83e135 7eefb8bd f1542850 d66d8007 | |
| 716 | \\d620e405 0b5715dc 83f4a921 d36ce9ce | |
| 717 | \\47d0d13c 5d85f2b0 ff8318d2 877eec2f | |
| 718 | \\63b931bd 47417a81 a538327a f927da3e | |
| 719 | ), | |
| 720 | // just use the empty hash... | |
| 721 | else => {}, | |
| 722 | } | |
| 723 | } | |
| 724 | var res: [Hash.digest_length]u8 = undefined; | |
| 725 | Hash.hash(label, &res, .{}); | |
| 726 | return res; | |
| 727 | } | |
| 728 | ||
| 729 | const ct = if (std.options.side_channels_mitigations == .none) ct_unprotected else ct_protected; | |
| 730 | ||
| 731 | const ct_unprotected = struct { | |
| 732 | fn lastIndexOfScalar(slice: []const u8, value: u8) ?usize { | |
| 733 | return std.mem.lastIndexOfScalar(u8, slice, value); | |
| 734 | } | |
| 735 | ||
| 736 | fn indexOfScalarPos(slice: []const u8, start_index: usize, value: u8) ?usize { | |
| 737 | return std.mem.indexOfScalarPos(u8, slice, start_index, value); | |
| 738 | } | |
| 739 | ||
| 740 | fn memEql(a: []const u8, b: []const u8) bool { | |
| 741 | return std.mem.eql(u8, a, b); | |
| 742 | } | |
| 743 | ||
| 744 | fn @"and"(a: bool, b: bool) bool { | |
| 745 | return a and b; | |
| 746 | } | |
| 747 | ||
| 748 | fn @"or"(a: bool, b: bool) bool { | |
| 749 | return a or b; | |
| 750 | } | |
| 751 | }; | |
| 752 | ||
| 753 | const ct_protected = struct { | |
| 754 | fn lastIndexOfScalar(slice: []const u8, value: u8) ?usize { | |
| 755 | var res: ?usize = null; | |
| 756 | var i: usize = slice.len; | |
| 757 | while (i != 0) { | |
| 758 | i -= 1; | |
| 759 | if (@intFromBool(res == null) & @intFromBool(slice[i] == value) == 1) res = i; | |
| 760 | } | |
| 761 | return res; | |
| 762 | } | |
| 763 | ||
| 764 | fn indexOfScalarPos(slice: []const u8, start_index: usize, value: u8) ?usize { | |
| 765 | var res: ?usize = null; | |
| 766 | for (slice[start_index..], start_index..) |c, j| { | |
| 767 | if (c == value) res = j; | |
| 768 | } | |
| 769 | return res; | |
| 770 | } | |
| 771 | ||
| 772 | fn memEql(a: []const u8, b: []const u8) bool { | |
| 773 | var res: u1 = 1; | |
| 774 | for (a, b) |a_elem, b_elem| { | |
| 775 | res &= @intFromBool(a_elem == b_elem); | |
| 776 | } | |
| 777 | return res == 1; | |
| 778 | } | |
| 779 | ||
| 780 | fn @"and"(a: bool, b: bool) bool { | |
| 781 | return (@intFromBool(a) & @intFromBool(b)) == 1; | |
| 782 | } | |
| 783 | ||
| 784 | fn @"or"(a: bool, b: bool) bool { | |
| 785 | return (@intFromBool(a) | @intFromBool(b)) == 1; | |
| 786 | } | |
| 787 | }; | |
| 788 | ||
| 789 | test ct { | |
| 790 | const c = ct_unprotected; | |
| 791 | try std.testing.expectEqual(true, c.@"or"(true, false)); | |
| 792 | try std.testing.expectEqual(true, c.@"and"(true, true)); | |
| 793 | try std.testing.expectEqual(true, c.memEql("Asdf", "Asdf")); | |
| 794 | try std.testing.expectEqual(false, c.memEql("asdf", "Asdf")); | |
| 795 | try std.testing.expectEqual(3, c.indexOfScalarPos("asdff", 1, 'f')); | |
| 796 | try std.testing.expectEqual(4, c.lastIndexOfScalar("asdff", 'f')); | |
| 797 | } | |
| 798 | ||
| 799 | fn removeNonHex(comptime hex: []const u8) []const u8 { | |
| 800 | var res: [hex.len]u8 = undefined; | |
| 801 | var i: usize = 0; | |
| 802 | for (hex) |c| { | |
| 803 | if (std.ascii.isHex(c)) { | |
| 804 | res[i] = c; | |
| 805 | i += 1; | |
| 806 | } | |
| 807 | } | |
| 808 | return res[0..i]; | |
| 809 | } | |
| 810 | ||
| 811 | /// For readable copy/pasting from hex viewers. | |
| 812 | fn hexToBytes(comptime hex: []const u8) [removeNonHex(hex).len / 2]u8 { | |
| 813 | const hex2 = comptime removeNonHex(hex); | |
| 814 | comptime var res: [hex2.len / 2]u8 = undefined; | |
| 815 | _ = comptime std.fmt.hexToBytes(&res, hex2) catch unreachable; | |
| 816 | return res; | |
| 817 | } | |
| 818 | ||
| 819 | test hexToBytes { | |
| 820 | const hex = | |
| 821 | \\e3b0c442 98fc1c14 9afbf4c8 996fb924 | |
| 822 | \\27ae41e4 649b934c a495991b 7852b855 | |
| 823 | ; | |
| 824 | try std.testing.expectEqual( | |
| 825 | [_]u8{ | |
| 826 | 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, | |
| 827 | 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, | |
| 828 | 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, | |
| 829 | 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55, | |
| 830 | }, | |
| 831 | hexToBytes(hex), | |
| 832 | ); | |
| 833 | } | |
| 834 | ||
| 835 | const TestHash = std.crypto.hash.sha2.Sha256; | |
| 836 | fn testKeypair() !KeyPair { | |
| 837 | const keypair_bytes = @embedFile("testdata/id_rsa.der"); | |
| 838 | const kp = try KeyPair.fromDer(keypair_bytes); | |
| 839 | try std.testing.expectEqual(2048, kp.public.modulus.bits()); | |
| 840 | return kp; | |
| 841 | } | |
| 842 | ||
| 843 | const skip_slow_tests = false; | |
| 844 | ||
| 845 | test "rsa PKCS1-v1_5 encrypt and decrypt" { | |
| 846 | if (skip_slow_tests) return error.SkipZigTest; | |
| 847 | const kp = try testKeypair(); | |
| 848 | ||
| 849 | const msg = "rsa PKCS1-v1_5 encrypt and decrypt"; | |
| 850 | var out: [max_modulus_len]u8 = undefined; | |
| 851 | const enc = try kp.public.encryptPkcsv1_5(msg, &out); | |
| 852 | ||
| 853 | var out2: [max_modulus_len]u8 = undefined; | |
| 854 | const dec = try kp.decryptPkcsv1_5(enc, &out2); | |
| 855 | ||
| 856 | try std.testing.expectEqualSlices(u8, msg, dec); | |
| 857 | } | |
| 858 | ||
| 859 | test "rsa OAEP encrypt and decrypt" { | |
| 860 | if (skip_slow_tests) return error.SkipZigTest; | |
| 861 | const kp = try testKeypair(); | |
| 862 | ||
| 863 | const msg = "rsa OAEP encrypt and decrypt"; | |
| 864 | const label = ""; | |
| 865 | var out: [max_modulus_len]u8 = undefined; | |
| 866 | const enc = try kp.public.encryptOaep(TestHash, msg, label, &out); | |
| 867 | ||
| 868 | var out2: [max_modulus_len]u8 = undefined; | |
| 869 | const dec = try kp.decryptOaep(TestHash, enc, label, &out2); | |
| 870 | ||
| 871 | try std.testing.expectEqualSlices(u8, msg, dec); | |
| 872 | } | |
| 873 | ||
| 874 | test "rsa PKCS1-v1_5 signature" { | |
| 875 | if (skip_slow_tests) return error.SkipZigTest; | |
| 876 | const kp = try testKeypair(); | |
| 877 | ||
| 878 | const msg = "rsa PKCS1-v1_5 signature"; | |
| 879 | var out: [max_modulus_len]u8 = undefined; | |
| 880 | ||
| 881 | const signature = try kp.signPkcsv1_5(TestHash, msg, &out); | |
| 882 | try signature.verify(msg, kp.public); | |
| 883 | } | |
| 884 | ||
| 885 | test "rsa PSS signature" { | |
| 886 | if (skip_slow_tests) return error.SkipZigTest; | |
| 887 | const kp = try testKeypair(); | |
| 888 | ||
| 889 | const msg = "rsa PSS signature"; | |
| 890 | var out: [max_modulus_len]u8 = undefined; | |
| 891 | ||
| 892 | const salts = [_][]const u8{ "asdf", "" }; | |
| 893 | for (salts) |salt| { | |
| 894 | const signature = try kp.signOaep(TestHash, msg, salt, &out); | |
| 895 | try signature.verify(msg, kp.public, salt.len); | |
| 896 | } | |
| 897 | ||
| 898 | const signature = try kp.signOaep(TestHash, msg, null, &out); // random salt | |
| 899 | try signature.verify(msg, kp.public, null); | |
| 900 | } | |
Binary file. Nothing to show line by line.
| @@ -1,1 +0,0 @@ | ||
| 1 | '���qp0x�0)쩖�~�+�`��tY4��D_ | |
Binary file. Nothing to show line by line.
| @@ -1,244 +0,0 @@ | ||
| 1 | /// Messages from The Illustrated TLS 1.2 Connection | |
| 2 | /// https://tls12.xargs.org/ | |
| 3 | const hexToBytes = .hexToBytes; | |
| 4 | ||
| 5 | pub const client_hello = hexToBytes( | |
| 6 | \\ 16 03 01 00 a5 01 00 00 a1 03 03 00 01 02 03 04 | |
| 7 | \\ 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 | |
| 8 | \\ 15 16 17 18 19 1a 1b 1c 1d 1e 1f 00 00 20 cc a8 | |
| 9 | \\ cc a9 c0 2f c0 30 c0 2b c0 2c c0 13 c0 09 c0 14 | |
| 10 | \\ c0 0a 00 9c 00 9d 00 2f 00 35 c0 12 00 0a 01 00 | |
| 11 | \\ 00 58 00 00 00 18 00 16 00 00 13 65 78 61 6d 70 | |
| 12 | \\ 6c 65 2e 75 6c 66 68 65 69 6d 2e 6e 65 74 00 05 | |
| 13 | \\ 00 05 01 00 00 00 00 00 0a 00 0a 00 08 00 1d 00 | |
| 14 | \\ 17 00 18 00 19 00 0b 00 02 01 00 00 0d 00 12 00 | |
| 15 | \\ 10 04 01 04 03 05 01 05 03 06 01 06 03 02 01 02 | |
| 16 | \\ 03 ff 01 00 01 00 00 12 00 00 | |
| 17 | ); | |
| 18 | pub const server_hello = hexToBytes( | |
| 19 | \\ 16 03 03 00 31 02 00 00 2d 03 03 70 71 72 73 74 | |
| 20 | \\ 75 76 77 78 79 7a 7b 7c 7d 7e 7f 80 81 82 83 84 | |
| 21 | \\ 85 86 87 88 89 8a 8b 8c 8d 8e 8f 00 c0 13 00 00 | |
| 22 | \\ 05 ff 01 00 01 00 | |
| 23 | ); | |
| 24 | pub const server_certificate = hexToBytes( | |
| 25 | \\ 16 03 03 03 2f 0b 00 03 2b 00 03 28 00 03 25 30 | |
| 26 | \\ 82 03 21 30 82 02 09 a0 03 02 01 02 02 08 15 5a | |
| 27 | \\ 92 ad c2 04 8f 90 30 0d 06 09 2a 86 48 86 f7 0d | |
| 28 | \\ 01 01 0b 05 00 30 22 31 0b 30 09 06 03 55 04 06 | |
| 29 | \\ 13 02 55 53 31 13 30 11 06 03 55 04 0a 13 0a 45 | |
| 30 | \\ 78 61 6d 70 6c 65 20 43 41 30 1e 17 0d 31 38 31 | |
| 31 | \\ 30 30 35 30 31 33 38 31 37 5a 17 0d 31 39 31 30 | |
| 32 | \\ 30 35 30 31 33 38 31 37 5a 30 2b 31 0b 30 09 06 | |
| 33 | \\ 03 55 04 06 13 02 55 53 31 1c 30 1a 06 03 55 04 | |
| 34 | \\ 03 13 13 65 78 61 6d 70 6c 65 2e 75 6c 66 68 65 | |
| 35 | \\ 69 6d 2e 6e 65 74 30 82 01 22 30 0d 06 09 2a 86 | |
| 36 | \\ 48 86 f7 0d 01 01 01 05 00 03 82 01 0f 00 30 82 | |
| 37 | \\ 01 0a 02 82 01 01 00 c4 80 36 06 ba e7 47 6b 08 | |
| 38 | \\ 94 04 ec a7 b6 91 04 3f f7 92 bc 19 ee fb 7d 74 | |
| 39 | \\ d7 a8 0d 00 1e 7b 4b 3a 4a e6 0f e8 c0 71 fc 73 | |
| 40 | \\ e7 02 4c 0d bc f4 bd d1 1d 39 6b ba 70 46 4a 13 | |
| 41 | \\ e9 4a f8 3d f3 e1 09 59 54 7b c9 55 fb 41 2d a3 | |
| 42 | \\ 76 52 11 e1 f3 dc 77 6c aa 53 37 6e ca 3a ec be | |
| 43 | \\ c3 aa b7 3b 31 d5 6c b6 52 9c 80 98 bc c9 e0 28 | |
| 44 | \\ 18 e2 0b f7 f8 a0 3a fd 17 04 50 9e ce 79 bd 9f | |
| 45 | \\ 39 f1 ea 69 ec 47 97 2e 83 0f b5 ca 95 de 95 a1 | |
| 46 | \\ e6 04 22 d5 ee be 52 79 54 a1 e7 bf 8a 86 f6 46 | |
| 47 | \\ 6d 0d 9f 16 95 1a 4c f7 a0 46 92 59 5c 13 52 f2 | |
| 48 | \\ 54 9e 5a fb 4e bf d7 7a 37 95 01 44 e4 c0 26 87 | |
| 49 | \\ 4c 65 3e 40 7d 7d 23 07 44 01 f4 84 ff d0 8f 7a | |
| 50 | \\ 1f a0 52 10 d1 f4 f0 d5 ce 79 70 29 32 e2 ca be | |
| 51 | \\ 70 1f df ad 6b 4b b7 11 01 f4 4b ad 66 6a 11 13 | |
| 52 | \\ 0f e2 ee 82 9e 4d 02 9d c9 1c dd 67 16 db b9 06 | |
| 53 | \\ 18 86 ed c1 ba 94 21 02 03 01 00 01 a3 52 30 50 | |
| 54 | \\ 30 0e 06 03 55 1d 0f 01 01 ff 04 04 03 02 05 a0 | |
| 55 | \\ 30 1d 06 03 55 1d 25 04 16 30 14 06 08 2b 06 01 | |
| 56 | \\ 05 05 07 03 02 06 08 2b 06 01 05 05 07 03 01 30 | |
| 57 | \\ 1f 06 03 55 1d 23 04 18 30 16 80 14 89 4f de 5b | |
| 58 | \\ cc 69 e2 52 cf 3e a3 00 df b1 97 b8 1d e1 c1 46 | |
| 59 | \\ 30 0d 06 09 2a 86 48 86 f7 0d 01 01 0b 05 00 03 | |
| 60 | \\ 82 01 01 00 59 16 45 a6 9a 2e 37 79 e4 f6 dd 27 | |
| 61 | \\ 1a ba 1c 0b fd 6c d7 55 99 b5 e7 c3 6e 53 3e ff | |
| 62 | \\ 36 59 08 43 24 c9 e7 a5 04 07 9d 39 e0 d4 29 87 | |
| 63 | \\ ff e3 eb dd 09 c1 cf 1d 91 44 55 87 0b 57 1d d1 | |
| 64 | \\ 9b df 1d 24 f8 bb 9a 11 fe 80 fd 59 2b a0 39 8c | |
| 65 | \\ de 11 e2 65 1e 61 8c e5 98 fa 96 e5 37 2e ef 3d | |
| 66 | \\ 24 8a fd e1 74 63 eb bf ab b8 e4 d1 ab 50 2a 54 | |
| 67 | \\ ec 00 64 e9 2f 78 19 66 0d 3f 27 cf 20 9e 66 7f | |
| 68 | \\ ce 5a e2 e4 ac 99 c7 c9 38 18 f8 b2 51 07 22 df | |
| 69 | \\ ed 97 f3 2e 3e 93 49 d4 c6 6c 9e a6 39 6d 74 44 | |
| 70 | \\ 62 a0 6b 42 c6 d5 ba 68 8e ac 3a 01 7b dd fc 8e | |
| 71 | \\ 2c fc ad 27 cb 69 d3 cc dc a2 80 41 44 65 d3 ae | |
| 72 | \\ 34 8c e0 f3 4a b2 fb 9c 61 83 71 31 2b 19 10 41 | |
| 73 | \\ 64 1c 23 7f 11 a5 d6 5c 84 4f 04 04 84 99 38 71 | |
| 74 | \\ 2b 95 9e d6 85 bc 5c 5d d6 45 ed 19 90 94 73 40 | |
| 75 | \\ 29 26 dc b4 0e 34 69 a1 59 41 e8 e2 cc a8 4b b6 | |
| 76 | \\ 08 46 36 a0 | |
| 77 | ); | |
| 78 | pub const server_key_exchange = hexToBytes( | |
| 79 | \\ 16 03 03 01 2c 0c 00 01 28 03 00 1d 20 9f d7 ad | |
| 80 | \\ 6d cf f4 29 8d d3 f9 6d 5b 1b 2a f9 10 a0 53 5b | |
| 81 | \\ 14 88 d7 f8 fa bb 34 9a 98 28 80 b6 15 04 01 01 | |
| 82 | \\ 00 04 02 b6 61 f7 c1 91 ee 59 be 45 37 66 39 bd | |
| 83 | \\ c3 d4 bb 81 e1 15 ca 73 c8 34 8b 52 5b 0d 23 38 | |
| 84 | \\ aa 14 46 67 ed 94 31 02 14 12 cd 9b 84 4c ba 29 | |
| 85 | \\ 93 4a aa cc e8 73 41 4e c1 1c b0 2e 27 2d 0a d8 | |
| 86 | \\ 1f 76 7d 33 07 67 21 f1 3b f3 60 20 cf 0b 1f d0 | |
| 87 | \\ ec b0 78 de 11 28 be ba 09 49 eb ec e1 a1 f9 6e | |
| 88 | \\ 20 9d c3 6e 4f ff d3 6b 67 3a 7d dc 15 97 ad 44 | |
| 89 | \\ 08 e4 85 c4 ad b2 c8 73 84 12 49 37 25 23 80 9e | |
| 90 | \\ 43 12 d0 c7 b3 52 2e f9 83 ca c1 e0 39 35 ff 13 | |
| 91 | \\ a8 e9 6b a6 81 a6 2e 40 d3 e7 0a 7f f3 58 66 d3 | |
| 92 | \\ d9 99 3f 9e 26 a6 34 c8 1b 4e 71 38 0f cd d6 f4 | |
| 93 | \\ e8 35 f7 5a 64 09 c7 dc 2c 07 41 0e 6f 87 85 8c | |
| 94 | \\ 7b 94 c0 1c 2e 32 f2 91 76 9e ac ca 71 64 3b 8b | |
| 95 | \\ 98 a9 63 df 0a 32 9b ea 4e d6 39 7e 8c d0 1a 11 | |
| 96 | \\ 0a b3 61 ac 5b ad 1c cd 84 0a 6c 8a 6e aa 00 1a | |
| 97 | \\ 9d 7d 87 dc 33 18 64 35 71 22 6c 4d d2 c2 ac 41 | |
| 98 | \\ fb | |
| 99 | ); | |
| 100 | pub const server_hello_done = hexToBytes("16 03 03 00 04 0e 00 00 00 "); | |
| 101 | pub const server_change_cipher_spec = hexToBytes("14 03 03 00 01 01 "); | |
| 102 | ||
| 103 | pub const server_handshake_finished = hexToBytes( | |
| 104 | \\ 16 03 03 00 40 51 52 53 54 55 56 57 58 59 5a 5b | |
| 105 | \\ 5c 5d 5e 5f 60 18 e0 75 31 7b 10 03 15 f6 08 1f | |
| 106 | \\ cb f3 13 78 1a ac 73 ef e1 9f e2 5b a1 af 59 c2 | |
| 107 | \\ 0b e9 4f c0 1b da 2d 68 00 29 8b 73 a7 e8 49 d7 | |
| 108 | \\ 4b d4 94 cf 7d | |
| 109 | ); | |
| 110 | pub const client_key_exchange_for_transcript = hexToBytes( | |
| 111 | \\ 16 03 03 00 25 10 00 00 21 20 35 80 72 d6 36 58 | |
| 112 | \\ 80 d1 ae ea 32 9a df 91 21 38 38 51 ed 21 a2 8e | |
| 113 | \\ 3b 75 e9 65 d0 d2 cd 16 62 54 | |
| 114 | ); | |
| 115 | ||
| 116 | pub const server_hello_responses = server_hello ++ server_certificate ++ server_key_exchange ++ server_hello_done; | |
| 117 | ||
| 118 | pub const server_responses = server_hello_responses ++ server_change_cipher_spec ++ server_handshake_finished; | |
| 119 | ||
| 120 | pub const server_handshake_finished_msgs = server_change_cipher_spec ++ server_handshake_finished; | |
| 121 | ||
| 122 | pub const master_secret = hexToBytes( | |
| 123 | \\ 91 6a bf 9d a5 59 73 e1 36 14 ae 0a 3f 5d 3f 37 | |
| 124 | \\ b0 23 ba 12 9a ee 02 cc 91 34 33 81 27 cd 70 49 | |
| 125 | \\ 78 1c 8e 19 fc 1e b2 a7 38 7a c0 6a e2 37 34 4c | |
| 126 | ); | |
| 127 | ||
| 128 | pub const client_key_exchange = hexToBytes( | |
| 129 | \\ 16 03 03 00 25 10 00 00 21 20 00 01 02 03 04 05 | |
| 130 | \\ 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 | |
| 131 | \\ 16 17 18 19 1a 1b 1c 1d 1e 1f | |
| 132 | ); | |
| 133 | pub const client_change_cyper_spec = hexToBytes("14 03 03 00 01 01 "); | |
| 134 | ||
| 135 | pub const client_handshake_finished = hexToBytes( | |
| 136 | \\ 16 03 03 00 40 20 21 22 23 24 25 26 27 28 29 2a | |
| 137 | \\ 2b 2c 2d 2e 2f a9 ac f5 5a f3 7a 90 17 63 ff 91 | |
| 138 | \\ 68 9a b7 ee a0 d4 0c 1c ca 62 44 ef f3 0b a3 6d | |
| 139 | \\ d0 df 86 3f 7d e3 98 d3 1a cc 37 6a e6 7a 00 6d | |
| 140 | \\ 8c 08 bc 8a 5a | |
| 141 | ); | |
| 142 | ||
| 143 | pub const handshake_messages = [_][]const u8{ | |
| 144 | &client_hello, | |
| 145 | &server_hello, | |
| 146 | &server_certificate, | |
| 147 | &server_key_exchange, | |
| 148 | &server_hello_done, | |
| 149 | &client_key_exchange_for_transcript, | |
| 150 | }; | |
| 151 | ||
| 152 | pub const client_finished = hexToBytes("14 00 00 0c cf 91 96 26 f1 36 0c 53 6a aa d7 3a "); | |
| 153 | ||
| 154 | // with iv 40 " ++ 41 ... 4f | |
| 155 | // client_sequence = 0 | |
| 156 | pub const verify_data_encrypted_msg = hexToBytes( | |
| 157 | \\ 16 03 03 00 40 40 41 42 43 44 45 46 47 48 49 4a | |
| 158 | \\ 4b 4c 4d 4e 4f 22 7b c9 ba 81 ef 30 f2 a8 a7 8f | |
| 159 | \\ f1 df 50 84 4d 58 04 b7 ee b2 e2 14 c3 2b 68 92 | |
| 160 | \\ ac a3 db 7b 78 07 7f dd 90 06 7c 51 6b ac b3 ba | |
| 161 | \\ 90 de df 72 0f | |
| 162 | ); | |
| 163 | ||
| 164 | // with iv 00 " ++ 01 ... 1f | |
| 165 | // client_sequence = 1 | |
| 166 | pub const encrypted_ping_msg = hexToBytes( | |
| 167 | \\ 17 03 03 00 30 00 01 02 03 04 05 06 07 08 09 0a | |
| 168 | \\ 0b 0c 0d 0e 0f 6c 42 1c 71 c4 2b 18 3b fa 06 19 | |
| 169 | \\ 5d 13 3d 0a 09 d0 0f c7 cb 4e 0f 5d 1c da 59 d1 | |
| 170 | \\ 47 ec 79 0c 99 | |
| 171 | ); | |
| 172 | ||
| 173 | pub const key_material = hexToBytes( | |
| 174 | \\ 1b 7d 11 7c 7d 5f 69 0b c2 63 ca e8 ef 60 af 0f | |
| 175 | \\ 18 78 ac c2 2a d8 bd d8 c6 01 a6 17 12 6f 63 54 | |
| 176 | \\ 0e b2 09 06 f7 81 fa d2 f6 56 d0 37 b1 73 ef 3e | |
| 177 | \\ 11 16 9f 27 23 1a 84 b6 75 2a 18 e7 a9 fc b7 cb | |
| 178 | \\ cd d8 f9 8d d8 f7 69 eb a0 d2 55 0c 92 38 ee bf | |
| 179 | \\ ef 5c 32 25 1a bb 67 d6 43 45 28 db 49 37 d5 40 | |
| 180 | \\ d3 93 13 5e 06 a1 1b b8 0e 45 ea eb e3 2c ac 72 | |
| 181 | \\ 75 74 38 fb b3 df 64 5c bd a4 06 7c df a0 f8 48 | |
| 182 | ); | |
| 183 | ||
| 184 | pub const server_pong = hexToBytes( | |
| 185 | \\ 17 03 03 00 30 61 62 63 64 65 66 67 68 69 6a 6b | |
| 186 | \\ 6c 6d 6e 6f 70 97 83 48 8a f5 fa 20 bf 7a 2e f6 | |
| 187 | \\ 9d eb b5 34 db 9f b0 7a 8c 27 21 de e5 40 9f 77 | |
| 188 | \\ af 0c 3d de 56 | |
| 189 | ); | |
| 190 | ||
| 191 | pub const client_random = hexToBytes( | |
| 192 | \\ 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f | |
| 193 | ); | |
| 194 | ||
| 195 | pub const server_random = hexToBytes( | |
| 196 | \\ 70 71 72 73 74 75 76 77 78 79 7a 7b 7c 7d 7e 7f 80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f | |
| 197 | ); | |
| 198 | ||
| 199 | pub const client_secret = hexToBytes( | |
| 200 | \\ 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f | |
| 201 | ); | |
| 202 | ||
| 203 | pub const server_pub_key = hexToBytes( | |
| 204 | \\ 9f d7 ad 6d cf f4 29 8d d3 f9 6d 5b 1b 2a f9 10 a0 53 5b 14 88 d7 f8 fa bb 34 9a 98 28 80 b6 15 | |
| 205 | ); | |
| 206 | ||
| 207 | pub const signature = hexToBytes( | |
| 208 | \\ 04 02 b6 61 f7 c1 91 ee 59 be 45 37 66 39 bd c3 | |
| 209 | \\ d4 bb 81 e1 15 ca 73 c8 34 8b 52 5b 0d 23 38 aa | |
| 210 | \\ 14 46 67 ed 94 31 02 14 12 cd 9b 84 4c ba 29 93 | |
| 211 | \\ 4a aa cc e8 73 41 4e c1 1c b0 2e 27 2d 0a d8 1f | |
| 212 | \\ 76 7d 33 07 67 21 f1 3b f3 60 20 cf 0b 1f d0 ec | |
| 213 | \\ b0 78 de 11 28 be ba 09 49 eb ec e1 a1 f9 6e 20 | |
| 214 | \\ 9d c3 6e 4f ff d3 6b 67 3a 7d dc 15 97 ad 44 08 | |
| 215 | \\ e4 85 c4 ad b2 c8 73 84 12 49 37 25 23 80 9e 43 | |
| 216 | \\ 12 d0 c7 b3 52 2e f9 83 ca c1 e0 39 35 ff 13 a8 | |
| 217 | \\ e9 6b a6 81 a6 2e 40 d3 e7 0a 7f f3 58 66 d3 d9 | |
| 218 | \\ 99 3f 9e 26 a6 34 c8 1b 4e 71 38 0f cd d6 f4 e8 | |
| 219 | \\ 35 f7 5a 64 09 c7 dc 2c 07 41 0e 6f 87 85 8c 7b | |
| 220 | \\ 94 c0 1c 2e 32 f2 91 76 9e ac ca 71 64 3b 8b 98 | |
| 221 | \\ a9 63 df 0a 32 9b ea 4e d6 39 7e 8c d0 1a 11 0a | |
| 222 | \\ b3 61 ac 5b ad 1c cd 84 0a 6c 8a 6e aa 00 1a 9d | |
| 223 | \\ 7d 87 dc 33 18 64 35 71 22 6c 4d d2 c2 ac 41 fb | |
| 224 | ); | |
| 225 | ||
| 226 | pub const cert_pub_key = hexToBytes( | |
| 227 | \\ 30 82 01 0a 02 82 01 01 00 c4 80 36 06 ba e7 47 | |
| 228 | \\ 6b 08 94 04 ec a7 b6 91 04 3f f7 92 bc 19 ee fb | |
| 229 | \\ 7d 74 d7 a8 0d 00 1e 7b 4b 3a 4a e6 0f e8 c0 71 | |
| 230 | \\ fc 73 e7 02 4c 0d bc f4 bd d1 1d 39 6b ba 70 46 | |
| 231 | \\ 4a 13 e9 4a f8 3d f3 e1 09 59 54 7b c9 55 fb 41 | |
| 232 | \\ 2d a3 76 52 11 e1 f3 dc 77 6c aa 53 37 6e ca 3a | |
| 233 | \\ ec be c3 aa b7 3b 31 d5 6c b6 52 9c 80 98 bc c9 | |
| 234 | \\ e0 28 18 e2 0b f7 f8 a0 3a fd 17 04 50 9e ce 79 | |
| 235 | \\ bd 9f 39 f1 ea 69 ec 47 97 2e 83 0f b5 ca 95 de | |
| 236 | \\ 95 a1 e6 04 22 d5 ee be 52 79 54 a1 e7 bf 8a 86 | |
| 237 | \\ f6 46 6d 0d 9f 16 95 1a 4c f7 a0 46 92 59 5c 13 | |
| 238 | \\ 52 f2 54 9e 5a fb 4e bf d7 7a 37 95 01 44 e4 c0 | |
| 239 | \\ 26 87 4c 65 3e 40 7d 7d 23 07 44 01 f4 84 ff d0 | |
| 240 | \\ 8f 7a 1f a0 52 10 d1 f4 f0 d5 ce 79 70 29 32 e2 | |
| 241 | \\ ca be 70 1f df ad 6b 4b b7 11 01 f4 4b ad 66 6a | |
| 242 | \\ 11 13 0f e2 ee 82 9e 4d 02 9d c9 1c dd 67 16 db | |
| 243 | \\ b9 06 18 86 ed c1 ba 94 21 02 03 01 00 01 | |
| 244 | ); | |
| @@ -1,69 +0,0 @@ | ||
| 1 | const hexToBytes = .hexToBytes; | |
| 2 | ||
| 3 | pub const client_hello = | |
| 4 | hexToBytes("16030100f8010000f40303000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20e0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000813021303130100ff010000a30000001800160000136578616d706c652e756c666865696d2e6e6574000b000403000102000a00160014001d0017001e0019001801000101010201030104002300000016000000170000000d001e001c040305030603080708080809080a080b080408050806040105010601002b0003020304002d00020101003300260024001d0020358072d6365880d1aeea329adf9121383851ed21a28e3b75e965d0d2cd166254"); | |
| 5 | ||
| 6 | pub const server_hello = | |
| 7 | hexToBytes("160303007a") ++ // record header | |
| 8 | hexToBytes("020000760303") ++ // handshake header, server version | |
| 9 | hexToBytes("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f") ++ // server_random | |
| 10 | hexToBytes("20e0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff") ++ // session id | |
| 11 | hexToBytes("130200") ++ // cipher suite, compression method | |
| 12 | hexToBytes("002e002b00020304") ++ // extensions, supported version | |
| 13 | hexToBytes("00330024001d00209fd7ad6dcff4298dd3f96d5b1b2af910a0535b1488d7f8fabb349a982880b615"); // extension key share | |
| 14 | ||
| 15 | pub const client_random = hexToBytes( | |
| 16 | \\ 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f | |
| 17 | ); | |
| 18 | ||
| 19 | pub const server_random = | |
| 20 | hexToBytes("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f"); | |
| 21 | pub const server_pub_key = | |
| 22 | hexToBytes("9fd7ad6dcff4298dd3f96d5b1b2af910a0535b1488d7f8fabb349a982880b615"); | |
| 23 | pub const client_private_key = | |
| 24 | hexToBytes("202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f"); | |
| 25 | pub const client_public_key = | |
| 26 | hexToBytes("358072d6365880d1aeea329adf9121383851ed21a28e3b75e965d0d2cd166254"); | |
| 27 | ||
| 28 | pub const shared_key = hexToBytes("df4a291baa1eb7cfa6934b29b474baad2697e29f1f920dcc77c8a0a088447624"); | |
| 29 | pub const server_handshake_key = hexToBytes("9f13575ce3f8cfc1df64a77ceaffe89700b492ad31b4fab01c4792be1b266b7f"); | |
| 30 | pub const server_handshake_iv = hexToBytes("9563bc8b590f671f488d2da3"); | |
| 31 | pub const client_handshake_key = hexToBytes("1135b4826a9a70257e5a391ad93093dfd7c4214812f493b3e3daae1eb2b1ac69"); | |
| 32 | pub const client_handshake_iv = hexToBytes("4256d2e0e88babdd05eb2f27"); | |
| 33 | ||
| 34 | pub const server_application_key = hexToBytes("01f78623f17e3edcc09e944027ba3218d57c8e0db93cd3ac419309274700ac27"); | |
| 35 | pub const server_application_iv = hexToBytes("196a750b0c5049c0cc51a541"); | |
| 36 | pub const client_application_key = hexToBytes("de2f4c7672723a692319873e5c227606691a32d1c59d8b9f51dbb9352e9ca9cc"); | |
| 37 | pub const client_application_iv = hexToBytes("bb007956f474b25de902432f"); | |
| 38 | ||
| 39 | pub const server_encrypted_extensions_wrapped = | |
| 40 | hexToBytes("17030300176be02f9da7c2dc9ddef56f2468b90adfa25101ab0344ae"); | |
| 41 | pub const server_encrypted_extensions = | |
| 42 | hexToBytes("080000020000"); | |
| 43 | ||
| 44 | pub const server_certificate_wrapped = | |
| 45 | hexToBytes("1703030343baf00a9be50f3f2307e726edcbdacbe4b18616449d46c6207af6e9953ee5d2411ba65d31feaf4f78764f2d693987186cc01329c187a5e4608e8d27b318e98dd94769f7739ce6768392caca8dcc597d77ec0d1272233785f6e69d6f43effa8e7905edfdc4037eee5933e990a7972f206913a31e8d04931366d3d8bcd6a4a4d647dd4bd80b0ff863ce3554833d744cf0e0b9c07cae726dd23f9953df1f1ce3aceb3b7230871e92310cfb2b098486f43538f8e82d8404e5c6c25f66a62ebe3c5f26232640e20a769175ef83483cd81e6cb16e78dfad4c1b714b04b45f6ac8d1065ad18c13451c9055c47da300f93536ea56f531986d6492775393c4ccb095467092a0ec0b43ed7a0687cb470ce350917b0ac30c6e5c24725a78c45f9f5f29b6626867f6f79ce054273547b36df030bd24af10d632dba54fc4e890bd0586928c0206ca2e28e44e227a2d5063195935df38da8936092eef01e84cad2e49d62e470a6c7745f625ec39e4fc23329c79d1172876807c36d736ba42bb69b004ff55f93850dc33c1f98abb92858324c76ff1eb085db3c1fc50f74ec04442e622973ea70743418794c388140bb492d6294a0540e5a59cfae60ba0f14899fca71333315ea083a68e1d7c1e4cdc2f56bcd6119681a4adbc1bbf42afd806c3cbd42a076f545dee4e118d0b396754be2b042a685dd4727e89c0386a94d3cd6ecb9820e9d49afeed66c47e6fc243eabebbcb0b02453877f5ac5dbfbdf8db1052a3c994b224cd9aaaf56b026bb9efa2e01302b36401ab6494e7018d6e5b573bd38bcef023b1fc92946bbca0209ca5fa926b4970b1009103645cb1fcfe552311ff730558984370038fd2cce2a91fc74d6f3e3ea9f843eed356f6f82d35d03bc24b81b58ceb1a43ec9437e6f1e50eb6f555e321fd67c8332eb1b832aa8d795a27d479c6e27d5a61034683891903f66421d094e1b00a9a138d861e6f78a20ad3e1580054d2e305253c713a02fe1e28deee7336246f6ae34331806b46b47b833c39b9d31cd300c2a6ed831399776d07f570eaf0059a2c68a5f3ae16b617404af7b7231a4d942758fc020b3f23ee8c15e36044cfd67cd640993b16207597fbf385ea7a4d99e8d456ff83d41f7b8b4f069b028a2a63a919a70e3a10e3084158faa5bafa30186c6b2f238eb530c73e"); | |
| 46 | pub const server_certificate = | |
| 47 | hexToBytes("0b00032e0000032a0003253082032130820209a0030201020208155a92adc2048f90300d06092a864886f70d01010b05003022310b300906035504061302555331133011060355040a130a4578616d706c65204341301e170d3138313030353031333831375a170d3139313030353031333831375a302b310b3009060355040613025553311c301a060355040313136578616d706c652e756c666865696d2e6e657430820122300d06092a864886f70d01010105000382010f003082010a0282010100c4803606bae7476b089404eca7b691043ff792bc19eefb7d74d7a80d001e7b4b3a4ae60fe8c071fc73e7024c0dbcf4bdd11d396bba70464a13e94af83df3e10959547bc955fb412da3765211e1f3dc776caa53376eca3aecbec3aab73b31d56cb6529c8098bcc9e02818e20bf7f8a03afd1704509ece79bd9f39f1ea69ec47972e830fb5ca95de95a1e60422d5eebe527954a1e7bf8a86f6466d0d9f16951a4cf7a04692595c1352f2549e5afb4ebfd77a37950144e4c026874c653e407d7d23074401f484ffd08f7a1fa05210d1f4f0d5ce79702932e2cabe701fdfad6b4bb71101f44bad666a11130fe2ee829e4d029dc91cdd6716dbb9061886edc1ba94210203010001a3523050300e0603551d0f0101ff0404030205a0301d0603551d250416301406082b0601050507030206082b06010505070301301f0603551d23041830168014894fde5bcc69e252cf3ea300dfb197b81de1c146300d06092a864886f70d01010b05000382010100591645a69a2e3779e4f6dd271aba1c0bfd6cd75599b5e7c36e533eff3659084324c9e7a504079d39e0d42987ffe3ebdd09c1cf1d914455870b571dd19bdf1d24f8bb9a11fe80fd592ba0398cde11e2651e618ce598fa96e5372eef3d248afde17463ebbfabb8e4d1ab502a54ec0064e92f7819660d3f27cf209e667fce5ae2e4ac99c7c93818f8b2510722dfed97f32e3e9349d4c66c9ea6396d744462a06b42c6d5ba688eac3a017bddfc8e2cfcad27cb69d3ccdca280414465d3ae348ce0f34ab2fb9c618371312b191041641c237f11a5d65c844f0404849938712b959ed685bc5c5dd645ed19909473402926dcb40e3469a15941e8e2cca84bb6084636a00000"); | |
| 48 | ||
| 49 | pub const server_certificate_verify_wrapped = hexToBytes("170303011973719fce07ec2f6d3bba0292a0d40b2770c06a271799a53314f6f77fc95c5fe7b9a4329fd9548c670ebeea2f2d5c351dd9356ef2dcd52eb137bd3a676522f8cd0fb7560789ad7b0e3caba2e37e6b4199c6793b3346ed46cf740a9fa1fec414dc715c415c60e575703ce6a34b70b5191aa6a61a18faff216c687ad8d17e12a7e99915a611bfc1a2befc15e6e94d784642e682fd17382a348c301056b940c9847200408bec56c81ea3d7217ab8e85a88715395899c90587f72e8ddd74b26d8edc1c7c837d9f2ebbc260962219038b05654a63a0b12999b4a8306a3ddcc0e17c53ba8f9c80363f7841354d291b4ace0c0f330c0fcd5aa9deef969ae8ab2d98da88ebb6ea80a3a11f00ea296a3232367ff075e1c66dd9cbedc4713"); | |
| 50 | pub const server_finished_wrapped = hexToBytes("17030300451061de27e51c2c9f342911806f282b710c10632ca5006755880dbf7006002d0e84fed9adf27a43b5192303e4df5c285d58e3c76224078440c0742374744aecf28cf3182fd0"); | |
| 51 | ||
| 52 | pub const handshake_hash = hexToBytes("fa6800169a6baac19159524fa7b9721b41be3c9db6f3f93fa5ff7e3db3ece204d2b456c51046e40ec5312c55a86126f5"); | |
| 53 | ||
| 54 | pub const client_finished_verify_data = hexToBytes("bff56a671b6c659d0a7c5dd18428f58bdd38b184a3ce342d9fde95cbd5056f7da7918ee320eab7a93abd8f1c02454d27"); | |
| 55 | ||
| 56 | pub const client_finished_wrapped = hexToBytes("17030300459ff9b063175177322a46dd9896f3c3bb820ab51743ebc25fdadd53454b73deb54cc7248d411a18bccf657a960824e9a19364837c350a69a88d4bf635c85eb874aebc9dfde8"); | |
| 57 | ||
| 58 | pub const client_ping_wrapped = hexToBytes("1703030015828139cb7b73aaabf5b82fbf9a2961bcde10038a32"); | |
| 59 | pub const server_flight = | |
| 60 | hexToBytes("140303000101") ++ | |
| 61 | server_encrypted_extensions_wrapped ++ | |
| 62 | server_certificate_wrapped ++ | |
| 63 | server_certificate_verify_wrapped ++ | |
| 64 | server_finished_wrapped; | |
| 65 | ||
| 66 | pub const server_flight_1 = server_hello ++ server_flight; | |
| 67 | ||
| 68 | pub const client_flight_2 = | |
| 69 | hexToBytes("140303000101") ++ client_finished_wrapped; | |
| @@ -1,117 +0,0 @@ | ||
| 1 | const std = ; | |
| 2 | ||
| 3 | pub fn bufPrint(var_name: []const u8, buf: []const u8) void { | |
| 4 | // std.debug.print("\nconst {s} = [_]u8;\n", .{}); | |
| 11 | ||
| 12 | std.debug.print("const {s} = \"", .{var_name}); | |
| 13 | const charset = "0123456789abcdef"; | |
| 14 | for (buf) |b| { | |
| 15 | const x = charset[b >> 4]; | |
| 16 | const y = charset[b & 15]; | |
| 17 | std.debug.print("{c}{c} ", .{ x, y }); | |
| 18 | } | |
| 19 | std.debug.print("\"\n", .{}); | |
| 20 | } | |
| 21 | ||
| 22 | const random_instance = std.Random{ .ptr = undefined, .fillFn = randomFillFn }; | |
| 23 | var random_seed: u8 = 0; | |
| 24 | ||
| 25 | pub fn randomFillFn(_: *anyopaque, buf: []u8) void { | |
| 26 | for (buf) |*v| { | |
| 27 | v.* = random_seed; | |
| 28 | random_seed +%= 1; | |
| 29 | } | |
| 30 | } | |
| 31 | ||
| 32 | pub fn random(seed: u8) std.Random { | |
| 33 | random_seed = seed; | |
| 34 | return random_instance; | |
| 35 | } | |
| 36 | ||
| 37 | // Fill buf with 0,1,..ff,0,... | |
| 38 | pub fn fill(buf: []u8) void { | |
| 39 | fillFrom(buf, 0); | |
| 40 | } | |
| 41 | ||
| 42 | pub fn fillFrom(buf: []u8, start: u8) void { | |
| 43 | var i: u8 = start; | |
| 44 | for (buf) |*v| { | |
| 45 | v.* = i; | |
| 46 | i +%= 1; | |
| 47 | } | |
| 48 | } | |
| 49 | ||
| 50 | pub const Stream = struct { | |
| 51 | output: std.io.FixedBufferStream([]u8) = undefined, | |
| 52 | input: std.io.FixedBufferStream([]const u8) = undefined, | |
| 53 | ||
| 54 | pub fn init(input: []const u8, output: []u8) Stream { | |
| 55 | return .{ | |
| 56 | .input = std.io.fixedBufferStream(input), | |
| 57 | .output = std.io.fixedBufferStream(output), | |
| 58 | }; | |
| 59 | } | |
| 60 | ||
| 61 | pub const ReadError = error{}; | |
| 62 | pub const WriteError = error{NoSpaceLeft}; | |
| 63 | ||
| 64 | pub fn write(self: *Stream, buf: []const u8) !usize { | |
| 65 | return try self.output.writer().write(buf); | |
| 66 | } | |
| 67 | ||
| 68 | pub fn writeAll(self: *Stream, buffer: []const u8) !void { | |
| 69 | var n: usize = 0; | |
| 70 | while (n < buffer.len) { | |
| 71 | n += try self.write(buffer[n..]); | |
| 72 | } | |
| 73 | } | |
| 74 | ||
| 75 | pub fn read(self: *Stream, buffer: []u8) !usize { | |
| 76 | return self.input.read(buffer); | |
| 77 | } | |
| 78 | }; | |
| 79 | ||
| 80 | // Copied from: https://github.com/clickingbuttons/zig/blob/f1cea91624fd2deae28bfb2414a4fd9c7e246883/lib/std/crypto/rsa.zig#L791 | |
| 81 | /// For readable copy/pasting from hex viewers. | |
| 82 | pub fn hexToBytes(comptime hex: []const u8) [removeNonHex(hex).len / 2]u8 { | |
| 83 | @setEvalBranchQuota(1000 * 100); | |
| 84 | const hex2 = comptime removeNonHex(hex); | |
| 85 | comptime var res: [hex2.len / 2]u8 = undefined; | |
| 86 | _ = comptime std.fmt.hexToBytes(&res, hex2) catch unreachable; | |
| 87 | return res; | |
| 88 | } | |
| 89 | ||
| 90 | fn removeNonHex(comptime hex: []const u8) []const u8 { | |
| 91 | @setEvalBranchQuota(1000 * 100); | |
| 92 | var res: [hex.len]u8 = undefined; | |
| 93 | var i: usize = 0; | |
| 94 | for (hex) |c| { | |
| 95 | if (std.ascii.isHex(c)) { | |
| 96 | res[i] = c; | |
| 97 | i += 1; | |
| 98 | } | |
| 99 | } | |
| 100 | return res[0..i]; | |
| 101 | } | |
| 102 | ||
| 103 | test hexToBytes { | |
| 104 | const hex = | |
| 105 | \\e3b0c442 98fc1c14 9afbf4c8 996fb924 | |
| 106 | \\27ae41e4 649b934c a495991b 7852b855 | |
| 107 | ; | |
| 108 | try std.testing.expectEqual( | |
| 109 | [_]u8{ | |
| 110 | 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, | |
| 111 | 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, | |
| 112 | 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, | |
| 113 | 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55, | |
| 114 | }, | |
| 115 | hexToBytes(hex), | |
| 116 | ); | |
| 117 | } | |
| @@ -1,454 +0,0 @@ | ||
| 1 | const std = ; | |
| 2 | const crypto = std.crypto; | |
| 3 | const tls = crypto.tls; | |
| 4 | const hkdfExpandLabel = tls.hkdfExpandLabel; | |
| 5 | ||
| 6 | const Sha256 = crypto.hash.sha2.Sha256; | |
| 7 | const Sha384 = crypto.hash.sha2.Sha384; | |
| 8 | const Sha512 = crypto.hash.sha2.Sha512; | |
| 9 | ||
| 10 | const HashTag = .CipherSuite.HashTag; | |
| 11 | ||
| 12 | // Transcript holds hash of all handshake message. | |
| 13 | // | |
| 14 | // Until the server hello is parsed we don't know which hash (sha256, sha384, | |
| 15 | // sha512) will be used so we update all of them. Handshake process will set | |
| 16 | // `selected` field once cipher suite is known. Other function will use that | |
| 17 | // selected hash. We continue to calculate all hashes because client certificate | |
| 18 | // message could use different hash than the other part of the handshake. | |
| 19 | // Handshake hash is dictated by the server selected cipher. Client certificate | |
| 20 | // hash is dictated by the private key used. | |
| 21 | // | |
| 22 | // Most of the functions are inlined because they are returning pointers. | |
| 23 | // | |
| 24 | pub const Transcript = struct { | |
| 25 | sha256: Type(.sha256) = .{ .hash = Sha256.init(.{}) }, | |
| 26 | sha384: Type(.sha384) = .{ .hash = Sha384.init(.{}) }, | |
| 27 | sha512: Type(.sha512) = .{ .hash = Sha512.init(.{}) }, | |
| 28 | ||
| 29 | tag: HashTag = .sha256, | |
| 30 | ||
| 31 | // Transcript Type from hash tag | |
| 32 | fn Type(h: HashTag) type { | |
| 33 | return switch (h) { | |
| 34 | .sha256 => TranscriptT(Sha256), | |
| 35 | .sha384 => TranscriptT(Sha384), | |
| 36 | .sha512 => TranscriptT(Sha512), | |
| 37 | }; | |
| 38 | } | |
| 39 | ||
| 40 | /// Set hash to use in all following function calls. | |
| 41 | pub fn use(t: *Transcript, tag: HashTag) void { | |
| 42 | t.tag = tag; | |
| 43 | } | |
| 44 | ||
| 45 | pub fn update(t: *Transcript, buf: []const u8) void { | |
| 46 | t.sha256.hash.update(buf); | |
| 47 | t.sha384.hash.update(buf); | |
| 48 | t.sha512.hash.update(buf); | |
| 49 | } | |
| 50 | ||
| 51 | // tls 1.2 handshake specific | |
| 52 | ||
| 53 | pub fn masterSecret( | |
| 54 | t: *Transcript, | |
| 55 | out: []u8, | |
| 56 | pre_master_secret: []const u8, | |
| 57 | client_random: [32]u8, | |
| 58 | server_random: [32]u8, | |
| 59 | ) void { | |
| 60 | switch (t.tag) { | |
| 61 | inline else => |h| @field(t, @tagName(h)).masterSecret( | |
| 62 | out, | |
| 63 | pre_master_secret, | |
| 64 | client_random, | |
| 65 | server_random, | |
| 66 | ), | |
| 67 | } | |
| 68 | } | |
| 69 | ||
| 70 | pub fn keyMaterial( | |
| 71 | t: *Transcript, | |
| 72 | out: []u8, | |
| 73 | master_secret: []const u8, | |
| 74 | client_random: [32]u8, | |
| 75 | server_random: [32]u8, | |
| 76 | ) void { | |
| 77 | switch (t.tag) { | |
| 78 | inline else => |h| @field(t, @tagName(h)).keyExpansion( | |
| 79 | out, | |
| 80 | master_secret, | |
| 81 | client_random, | |
| 82 | server_random, | |
| 83 | ), | |
| 84 | } | |
| 85 | } | |
| 86 | ||
| 87 | pub fn clientFinishedTls12(t: *Transcript, master_secret: []const u8) [12]u8 { | |
| 88 | return switch (t.tag) { | |
| 89 | inline else => |h| @field(t, @tagName(h)).clientFinishedTls12(master_secret), | |
| 90 | }; | |
| 91 | } | |
| 92 | ||
| 93 | pub fn serverFinishedTls12(t: *Transcript, master_secret: []const u8) [12]u8 { | |
| 94 | return switch (t.tag) { | |
| 95 | inline else => |h| @field(t, @tagName(h)).serverFinishedTls12(master_secret), | |
| 96 | }; | |
| 97 | } | |
| 98 | ||
| 99 | // tls 1.3 handshake specific | |
| 100 | ||
| 101 | pub fn setPreSharedSecret( | |
| 102 | t: *Transcript, | |
| 103 | resumption_master_secret: []const u8, | |
| 104 | ticket_nonce: []const u8, | |
| 105 | ) void { | |
| 106 | switch (t.tag) { | |
| 107 | inline else => |h| @field(t, @tagName(h)).setPreSharedSecret(resumption_master_secret, ticket_nonce), | |
| 108 | } | |
| 109 | } | |
| 110 | ||
| 111 | pub fn clearPreSharedSecret(t: *Transcript) void { | |
| 112 | switch (t.tag) { | |
| 113 | inline else => |h| @field(t, @tagName(h)).clearPreSharedSecret(), | |
| 114 | } | |
| 115 | } | |
| 116 | ||
| 117 | pub fn serverCertificateVerify(t: *Transcript) []const u8 { | |
| 118 | return switch (t.tag) { | |
| 119 | inline else => |h| @field(t, @tagName(h)).serverCertificateVerify(), | |
| 120 | }; | |
| 121 | } | |
| 122 | ||
| 123 | pub fn clientCertificateVerify(t: *Transcript) []const u8 { | |
| 124 | return switch (t.tag) { | |
| 125 | inline else => |h| @field(t, @tagName(h)).clientCertificateVerify(), | |
| 126 | }; | |
| 127 | } | |
| 128 | ||
| 129 | pub fn serverFinishedTls13(t: *Transcript) []const u8 { | |
| 130 | return switch (t.tag) { | |
| 131 | inline else => |h| @field(t, @tagName(h)).serverFinishedTls13(), | |
| 132 | }; | |
| 133 | } | |
| 134 | ||
| 135 | pub fn clientFinishedTls13(t: *Transcript) []const u8 { | |
| 136 | return switch (t.tag) { | |
| 137 | inline else => |h| @field(t, @tagName(h)).clientFinishedTls13(), | |
| 138 | }; | |
| 139 | } | |
| 140 | ||
| 141 | pub const Secret = struct { | |
| 142 | client: []const u8, | |
| 143 | server: []const u8, | |
| 144 | }; | |
| 145 | ||
| 146 | pub fn handshakeSecret(t: *Transcript, shared_key: []const u8) Secret { | |
| 147 | return switch (t.tag) { | |
| 148 | inline else => |h| @field(t, @tagName(h)).handshakeSecret(shared_key), | |
| 149 | }; | |
| 150 | } | |
| 151 | ||
| 152 | pub fn applicationSecret(t: *Transcript) Secret { | |
| 153 | return switch (t.tag) { | |
| 154 | inline else => |h| @field(t, @tagName(h)).applicationSecret(), | |
| 155 | }; | |
| 156 | } | |
| 157 | ||
| 158 | pub fn resumptionSecret(t: *Transcript) []const u8 { | |
| 159 | return switch (t.tag) { | |
| 160 | inline else => |h| @field(t, @tagName(h)).resumptionSecret(), | |
| 161 | }; | |
| 162 | } | |
| 163 | ||
| 164 | pub fn pskBinder(t: *Transcript) []const u8 { | |
| 165 | return switch (t.tag) { | |
| 166 | inline else => |h| @field(t, @tagName(h)).pskBinder(), | |
| 167 | }; | |
| 168 | } | |
| 169 | ||
| 170 | pub fn hashLength(t: *Transcript) u8 { | |
| 171 | return switch (t.tag) { | |
| 172 | inline else => |h| @TypeOf(@field(t, @tagName(h))).hash_length, | |
| 173 | }; | |
| 174 | } | |
| 175 | ||
| 176 | // other | |
| 177 | ||
| 178 | pub fn Hkdf(h: HashTag) type { | |
| 179 | return Type(h).Hkdf; | |
| 180 | } | |
| 181 | ||
| 182 | /// Copy of the current hash value | |
| 183 | pub fn hash(t: *Transcript, comptime Hash: type) Hash { | |
| 184 | return switch (Hash) { | |
| 185 | Sha256 => t.sha256.hash, | |
| 186 | Sha384 => t.sha384.hash, | |
| 187 | Sha512 => t.sha512.hash, | |
| 188 | else => @compileError("unimplemented"), | |
| 189 | }; | |
| 190 | } | |
| 191 | }; | |
| 192 | ||
| 193 | fn TranscriptT(comptime Hash: type) type { | |
| 194 | return struct { | |
| 195 | const Hmac = crypto.auth.hmac.Hmac(Hash); | |
| 196 | const Hkdf = crypto.kdf.hkdf.Hkdf(Hmac); | |
| 197 | const hash_length = Hash.digest_length; // Hmac.mac_length == Hmac.key_length == Hash.digest_length | |
| 198 | ||
| 199 | hash: Hash, | |
| 200 | handshake_secret: ?[hash_length]u8 = null, | |
| 201 | server_finished_key: [hash_length]u8 = undefined, | |
| 202 | client_finished_key: [hash_length]u8 = undefined, | |
| 203 | buffer: [hash_length + 64 + 34]u8 = undefined, | |
| 204 | ||
| 205 | const Self = @This(); | |
| 206 | ||
| 207 | fn init(transcript: Hash) Self { | |
| 208 | return .{ .transcript = transcript }; | |
| 209 | } | |
| 210 | ||
| 211 | fn serverCertificateVerify(self: *Self) []const u8 { | |
| 212 | self.buffer = @as([64]u8, @splat(0x20)) ++ | |
| 213 | "TLS 1.3, server CertificateVerify\x00".* ++ | |
| 214 | self.hash.peek(); | |
| 215 | return &self.buffer; | |
| 216 | } | |
| 217 | ||
| 218 | // ref: https://www.rfc-editor.org/rfc/rfc8446#section-4.4.3 | |
| 219 | fn clientCertificateVerify(self: *Self) []u8 { | |
| 220 | self.buffer = @as([64]u8, @splat(0x20)) ++ | |
| 221 | "TLS 1.3, client CertificateVerify\x00".* ++ | |
| 222 | self.hash.peek(); | |
| 223 | return &self.buffer; | |
| 224 | } | |
| 225 | ||
| 226 | fn masterSecret( | |
| 227 | _: *Self, | |
| 228 | out: []u8, | |
| 229 | pre_master_secret: []const u8, | |
| 230 | client_random: [32]u8, | |
| 231 | server_random: [32]u8, | |
| 232 | ) void { | |
| 233 | const seed = "master secret" ++ client_random ++ server_random; | |
| 234 | ||
| 235 | var a1: [hash_length]u8 = undefined; | |
| 236 | var a2: [hash_length]u8 = undefined; | |
| 237 | Hmac.create(&a1, seed, pre_master_secret); | |
| 238 | Hmac.create(&a2, &a1, pre_master_secret); | |
| 239 | ||
| 240 | var p1: [hash_length]u8 = undefined; | |
| 241 | var p2: [hash_length]u8 = undefined; | |
| 242 | Hmac.create(&p1, a1 ++ seed, pre_master_secret); | |
| 243 | Hmac.create(&p2, a2 ++ seed, pre_master_secret); | |
| 244 | ||
| 245 | const master_secret = p1 ++ p2; | |
| 246 | const len = @min(out.len, master_secret.len); | |
| 247 | @memcpy(out[0..len], master_secret[0..len]); | |
| 248 | } | |
| 249 | ||
| 250 | fn keyExpansion( | |
| 251 | _: *Self, | |
| 252 | out: []u8, | |
| 253 | master_secret: []const u8, | |
| 254 | client_random: [32]u8, | |
| 255 | server_random: [32]u8, | |
| 256 | ) void { | |
| 257 | const seed = "key expansion" ++ server_random ++ client_random; | |
| 258 | ||
| 259 | const a0 = seed; | |
| 260 | var a1: [hash_length]u8 = undefined; | |
| 261 | var a2: [hash_length]u8 = undefined; | |
| 262 | var a3: [hash_length]u8 = undefined; | |
| 263 | var a4: [hash_length]u8 = undefined; | |
| 264 | Hmac.create(&a1, a0, master_secret); | |
| 265 | Hmac.create(&a2, &a1, master_secret); | |
| 266 | Hmac.create(&a3, &a2, master_secret); | |
| 267 | Hmac.create(&a4, &a3, master_secret); | |
| 268 | ||
| 269 | var key_material: [hash_length * 4]u8 = undefined; | |
| 270 | Hmac.create(key_material[0..hash_length], a1 ++ seed, master_secret); | |
| 271 | Hmac.create(key_material[hash_length .. hash_length * 2], a2 ++ seed, master_secret); | |
| 272 | Hmac.create(key_material[hash_length * 2 .. hash_length * 3], a3 ++ seed, master_secret); | |
| 273 | Hmac.create(key_material[hash_length * 3 ..], a4 ++ seed, master_secret); | |
| 274 | ||
| 275 | const len = @min(out.len, key_material.len); | |
| 276 | @memcpy(out[0..len], key_material[0..len]); | |
| 277 | } | |
| 278 | ||
| 279 | fn clientFinishedTls12(self: *Self, master_secret: []const u8) [12]u8 { | |
| 280 | const seed = "client finished" ++ self.hash.peek(); | |
| 281 | var a1: [hash_length]u8 = undefined; | |
| 282 | var p1: [hash_length]u8 = undefined; | |
| 283 | Hmac.create(&a1, seed, master_secret); | |
| 284 | Hmac.create(&p1, a1 ++ seed, master_secret); | |
| 285 | return p1[0..12].*; | |
| 286 | } | |
| 287 | ||
| 288 | fn serverFinishedTls12(self: *Self, master_secret: []const u8) [12]u8 { | |
| 289 | const seed = "server finished" ++ self.hash.peek(); | |
| 290 | var a1: [hash_length]u8 = undefined; | |
| 291 | var p1: [hash_length]u8 = undefined; | |
| 292 | Hmac.create(&a1, seed, master_secret); | |
| 293 | Hmac.create(&p1, a1 ++ seed, master_secret); | |
| 294 | return p1[0..12].*; | |
| 295 | } | |
| 296 | ||
| 297 | // tls 1.3 | |
| 298 | ||
| 299 | fn setPreSharedSecret( | |
| 300 | self: *Self, | |
| 301 | resumption_secret: []const u8, | |
| 302 | ticket_nonce: []const u8, | |
| 303 | ) void { | |
| 304 | const ikm = hkdfExpandLabel( | |
| 305 | Hkdf, | |
| 306 | resumption_secret[0..hash_length].*, | |
| 307 | "resumption", | |
| 308 | ticket_nonce, | |
| 309 | hash_length, | |
| 310 | ); | |
| 311 | self.handshake_secret = Hkdf.extract(&[1]u8{0}, &ikm); | |
| 312 | } | |
| 313 | ||
| 314 | fn clearPreSharedSecret(self: *Self) void { | |
| 315 | self.handshake_secret = null; | |
| 316 | } | |
| 317 | ||
| 318 | fn handshakeSecret(self: *Self, shared_key: []const u8) Transcript.Secret { | |
| 319 | const hello_hash = self.hash.peek(); | |
| 320 | ||
| 321 | const empty_hash = tls.emptyHash(Hash); | |
| 322 | const zeroes: [hash_length]u8 = @splat(0); | |
| 323 | const early_secret = if (self.handshake_secret) |hs| hs else Hkdf.extract(&[1]u8{0}, &zeroes); | |
| 324 | const hs_derived_secret = hkdfExpandLabel(Hkdf, early_secret, "derived", &empty_hash, hash_length); | |
| 325 | ||
| 326 | const secret = Hkdf.extract(&hs_derived_secret, shared_key); | |
| 327 | self.handshake_secret = secret; | |
| 328 | const client_secret = hkdfExpandLabel(Hkdf, secret, "c hs traffic", &hello_hash, hash_length); | |
| 329 | const server_secret = hkdfExpandLabel(Hkdf, secret, "s hs traffic", &hello_hash, hash_length); | |
| 330 | ||
| 331 | self.server_finished_key = hkdfExpandLabel(Hkdf, server_secret, "finished", "", hash_length); | |
| 332 | self.client_finished_key = hkdfExpandLabel(Hkdf, client_secret, "finished", "", hash_length); | |
| 333 | ||
| 334 | self.buffer[0..hash_length].* = client_secret; | |
| 335 | self.buffer[hash_length .. 2 * hash_length].* = server_secret; | |
| 336 | return .{ | |
| 337 | .client = self.buffer[0..hash_length], | |
| 338 | .server = self.buffer[hash_length .. 2 * hash_length], | |
| 339 | }; | |
| 340 | } | |
| 341 | ||
| 342 | fn applicationSecret(self: *Self) Transcript.Secret { | |
| 343 | const handshake_hash = self.hash.peek(); | |
| 344 | ||
| 345 | const empty_hash = tls.emptyHash(Hash); | |
| 346 | const zeroes: [hash_length]u8 = @splat(0); | |
| 347 | const ap_derived_secret = hkdfExpandLabel(Hkdf, self.handshake_secret.?, "derived", &empty_hash, hash_length); | |
| 348 | const master_secret = Hkdf.extract(&ap_derived_secret, &zeroes); | |
| 349 | ||
| 350 | self.buffer[0..hash_length].* = hkdfExpandLabel(Hkdf, master_secret, "c ap traffic", &handshake_hash, hash_length); | |
| 351 | self.buffer[hash_length .. 2 * hash_length].* = hkdfExpandLabel(Hkdf, master_secret, "s ap traffic", &handshake_hash, hash_length); | |
| 352 | return .{ | |
| 353 | .client = self.buffer[0..hash_length], | |
| 354 | .server = self.buffer[hash_length .. 2 * hash_length], | |
| 355 | }; | |
| 356 | } | |
| 357 | ||
| 358 | fn resumptionSecret(self: *Self) []const u8 { | |
| 359 | const handshake_hash = self.hash.peek(); | |
| 360 | const empty_hash = tls.emptyHash(Hash); | |
| 361 | const zeroes: [hash_length]u8 = @splat(0); | |
| 362 | const ap_derived_secret = hkdfExpandLabel(Hkdf, self.handshake_secret.?, "derived", &empty_hash, hash_length); | |
| 363 | const master_secret = Hkdf.extract(&ap_derived_secret, &zeroes); | |
| 364 | self.buffer[0..hash_length].* = hkdfExpandLabel(Hkdf, master_secret, "res master", &handshake_hash, hash_length); | |
| 365 | return self.buffer[0..hash_length]; | |
| 366 | } | |
| 367 | ||
| 368 | fn pskBinder(self: *Self) []const u8 { | |
| 369 | const secret = self.handshake_secret.?; | |
| 370 | const prk = hkdfExpandLabel(Hkdf, secret, "res binder", &tls.emptyHash(Hash), hash_length); | |
| 371 | const expanded = hkdfExpandLabel(Hkdf, prk, "finished", "", hash_length); | |
| 372 | Hmac.create(self.buffer[0..hash_length], &self.hash.peek(), &expanded); | |
| 373 | return self.buffer[0..hash_length]; | |
| 374 | } | |
| 375 | ||
| 376 | fn serverFinishedTls13(self: *Self) []const u8 { | |
| 377 | Hmac.create(self.buffer[0..hash_length], &self.hash.peek(), &self.server_finished_key); | |
| 378 | return self.buffer[0..hash_length]; | |
| 379 | } | |
| 380 | ||
| 381 | // client finished message with header | |
| 382 | fn clientFinishedTls13(self: *Self) []const u8 { | |
| 383 | Hmac.create(self.buffer[0..hash_length], &self.hash.peek(), &self.client_finished_key); | |
| 384 | return self.buffer[0..hash_length]; | |
| 385 | } | |
| 386 | }; | |
| 387 | } | |
| 388 | ||
| 389 | const hexToBytes = .hexToBytes; | |
| 390 | const testing = std.testing; | |
| 391 | ||
| 392 | fn pskBinder_( | |
| 393 | comptime Hash: type, | |
| 394 | resumption_master_secret: [Hash.digest_length]u8, | |
| 395 | binder: *[Hash.digest_length]u8, | |
| 396 | binder_hash: [Hash.digest_length]u8, | |
| 397 | ticket_nonce: []const u8, | |
| 398 | ) void { | |
| 399 | const Hmac = crypto.auth.hmac.Hmac(Hash); | |
| 400 | const Hkdf = crypto.kdf.hkdf.Hkdf(Hmac); | |
| 401 | ||
| 402 | const ikm = hkdfExpandLabel(Hkdf, resumption_master_secret, "resumption", ticket_nonce, Hash.digest_length); | |
| 403 | const secret = Hkdf.extract(&[1]u8{0}, &ikm); | |
| 404 | const prk = hkdfExpandLabel(Hkdf, secret, "res binder", &tls.emptyHash(Hash), Hash.digest_length); | |
| 405 | const expanded = hkdfExpandLabel(Hkdf, prk, "finished", "", Hash.digest_length); | |
| 406 | Hmac.create(binder, &binder_hash, &expanded); | |
| 407 | } | |
| 408 | ||
| 409 | // Example from: https://datatracker.ietf.org/doc/html/rfc8448#autoid-4 | |
| 410 | test pskBinder_ { | |
| 411 | // input values from example | |
| 412 | const resumption_master_secret = hexToBytes("7d f2 35 f2 03 1d 2a 05 12 87 d0 2b 02 41 b0 bf da f8 6c c8 56 23 1f 2d 5a ba 46 c4 34 ec 19 6c"); | |
| 413 | const binder_hash = hexToBytes("63 22 4b 2e 45 73 f2 d3 45 4c a8 4b 9d 00 9a 04 f6 be 9e 05 71 1a 83 96 47 3a ef a0 1e 92 4a 14"); | |
| 414 | const ticket_nonce = hexToBytes("00 00"); | |
| 415 | // expected intermediate and resulting finished from example | |
| 416 | const expected_ikm = hexToBytes("4e cd 0e b6 ec 3b 4d 87 f5 d6 02 8f 92 2c a4 c5 85 1a 27 7f d4 13 11 c9 e6 2d 2c 94 92 e1 c4 f3"); | |
| 417 | const expected_secret = hexToBytes("9b 21 88 e9 b2 fc 6d 64 d7 1d c3 29 90 0e 20 bb 41 91 50 00 f6 78 aa 83 9c bb 79 7c b7 d8 33 2c"); | |
| 418 | const expected_prk = hexToBytes("69 fe 13 1a 3b ba d5 d6 3c 64 ee bc c3 0e 39 5b 9d 81 07 72 6a 13 d0 74 e3 89 db c8 a4 e4 72 56"); | |
| 419 | const expected_expanded = hexToBytes("55 88 67 3e 72 cb 59 c8 7d 22 0c af fe 94 f2 de a9 a3 b1 60 9f 7d 50 e9 0a 48 22 7d b9 ed 7e aa"); | |
| 420 | const expected_binder = hexToBytes("3a dd 4f b2 d8 fd f8 22 a0 ca 3c f7 67 8e f5 e8 8d ae 99 01 41 c5 92 4d 57 bb 6f a3 1b 9e 5f 9d"); | |
| 421 | ||
| 422 | const Hash = Sha256; | |
| 423 | const Hmac = crypto.auth.hmac.Hmac(Hash); | |
| 424 | const Hkdf = crypto.kdf.hkdf.Hkdf(Hmac); | |
| 425 | ||
| 426 | const ikm = hkdfExpandLabel(Hkdf, resumption_master_secret, "resumption", &ticket_nonce, Hash.digest_length); | |
| 427 | const secret = Hkdf.extract(&[1]u8{0}, &ikm); | |
| 428 | const prk = hkdfExpandLabel(Hkdf, secret, "res binder", &tls.emptyHash(Hash), Hash.digest_length); | |
| 429 | const expanded = hkdfExpandLabel(Hkdf, prk, "finished", "", Hash.digest_length); | |
| 430 | var binder: [Hash.digest_length]u8 = undefined; | |
| 431 | Hmac.create(&binder, &binder_hash, &expanded); | |
| 432 | ||
| 433 | if (false) { | |
| 434 | std.debug.print("ikm : {x}\n", .{ikm}); | |
| 435 | std.debug.print("secret : {x}\n", .{secret}); | |
| 436 | std.debug.print("prk : {x}\n", .{prk}); | |
| 437 | std.debug.print("expanded : {x}\n", .{expanded}); | |
| 438 | std.debug.print("binder : {x}\n", .{binder}); | |
| 439 | } | |
| 440 | ||
| 441 | try testing.expectEqualSlices(u8, &expected_ikm, &ikm); | |
| 442 | try testing.expectEqualSlices(u8, &expected_secret, &secret); | |
| 443 | try testing.expectEqualSlices(u8, &expected_prk, &prk); | |
| 444 | try testing.expectEqualSlices(u8, &expected_expanded, &expanded); | |
| 445 | try testing.expectEqualSlices(u8, &expected_binder, &binder); | |
| 446 | ||
| 447 | pskBinder_(Hash, resumption_master_secret, &binder, binder_hash, &ticket_nonce); | |
| 448 | // test pskBinder function | |
| 449 | try testing.expectEqualSlices( | |
| 450 | u8, | |
| 451 | &expected_binder, | |
| 452 | &binder, | |
| 453 | ); | |
| 454 | } | |