zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

commit 16f936b4202d352a6a8cf91a265fdd4bc64dde5d (tree)
parent 29475b45185f90c2437d160567e67a4b141f5845
Author: Andrew Kelley <andrew@ziglang.org>
Date:   Wed, 21 Dec 2022 18:54:17 -0700

std.crypto.tls: handle the certificate_verify message

Diffstat:
Mlib/std/crypto/Certificate.zig | 28+++++++++++++++++++++++-----
Mlib/std/crypto/tls/Client.zig | 138++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
2 files changed, 131 insertions(+), 35 deletions(-)

diff --git a/lib/std/crypto/Certificate.zig b/lib/std/crypto/Certificate.zig @@ -9,6 +9,10 @@ pub const Algorithm = enum { sha256WithRSAEncryption, sha384WithRSAEncryption, sha512WithRSAEncryption, + ecdsa_with_SHA224, + ecdsa_with_SHA256, + ecdsa_with_SHA384, + ecdsa_with_SHA512, pub const map = std.ComptimeStringMap(Algorithm, .{ .{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x05 }, .sha1WithRSAEncryption }, @@ -16,15 +20,19 @@ pub const Algorithm = enum { .{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0C }, .sha384WithRSAEncryption }, .{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0D }, .sha512WithRSAEncryption }, .{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0E }, .sha224WithRSAEncryption }, + .{ &[_]u8{ 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x01 }, .ecdsa_with_SHA224 }, + .{ &[_]u8{ 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x02 }, .ecdsa_with_SHA256 }, + .{ &[_]u8{ 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x03 }, .ecdsa_with_SHA384 }, + .{ &[_]u8{ 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x04 }, .ecdsa_with_SHA512 }, }); pub fn Hash(comptime algorithm: Algorithm) type { return switch (algorithm) { .sha1WithRSAEncryption => crypto.hash.Sha1, - .sha224WithRSAEncryption => crypto.hash.sha2.Sha224, - .sha256WithRSAEncryption => crypto.hash.sha2.Sha256, - .sha384WithRSAEncryption => crypto.hash.sha2.Sha384, - .sha512WithRSAEncryption => crypto.hash.sha2.Sha512, + .ecdsa_with_SHA224, .sha224WithRSAEncryption => crypto.hash.sha2.Sha224, + .ecdsa_with_SHA256, .sha256WithRSAEncryption => crypto.hash.sha2.Sha256, + .ecdsa_with_SHA384, .sha384WithRSAEncryption => crypto.hash.sha2.Sha384, + .ecdsa_with_SHA512, .sha512WithRSAEncryption => crypto.hash.sha2.Sha512, }; } }; @@ -125,6 +133,13 @@ pub const Parsed = struct { parsed_issuer.pub_key_algo, parsed_issuer.pubKey(), ), + .ecdsa_with_SHA224, + .ecdsa_with_SHA256, + .ecdsa_with_SHA384, + .ecdsa_with_SHA512, + => { + return error.CertificateSignatureAlgorithmUnsupported; + }, } } }; @@ -205,8 +220,11 @@ pub fn parseBitString(cert: Certificate, elem: der.Element) !der.Element.Slice { pub fn parseAlgorithm(bytes: []const u8, element: der.Element) !Algorithm { if (element.identifier.tag != .object_identifier) return error.CertificateFieldHasWrongDataType; - return Algorithm.map.get(bytes[element.slice.start..element.slice.end]) orelse + const oid_bytes = bytes[element.slice.start..element.slice.end]; + return Algorithm.map.get(oid_bytes) orelse { + //std.debug.print("oid bytes: {}\n", .{std.fmt.fmtSliceHexLower(oid_bytes)}); return error.CertificateHasUnrecognizedAlgorithm; + }; } pub fn parseAlgorithmCategory(bytes: []const u8, element: der.Element) !AlgorithmCategory { diff --git a/lib/std/crypto/tls/Client.zig b/lib/std/crypto/tls/Client.zig @@ -308,8 +308,23 @@ pub fn init(stream: net.Stream, ca_bundle: Certificate.Bundle, host: []const u8) var prev_cert: Certificate.Parsed = undefined; // Set to true once a trust chain has been established from the first // certificate to a root CA. - var cert_verification_done = false; + const HandshakeState = enum { + /// In this state we expect only an encrypted_extensions message. + encrypted_extensions, + /// In this state we expect certificate messages. + certificate, + /// In this state we expect certificate or certificate_verify messages. + /// certificate messages are ignored since the trust chain is already + /// established. + trust_chain_established, + /// In this state, we expect only the finished message. + finished, + }; + var handshake_state: HandshakeState = .encrypted_extensions; var cleartext_bufs: [2][8000]u8 = undefined; + var main_cert_pub_key_algo: Certificate.AlgorithmCategory = undefined; + var main_cert_pub_key_buf: [128]u8 = undefined; + var main_cert_pub_key_len: u8 = undefined; while (true) { const end_hdr = i + 5; @@ -376,6 +391,8 @@ pub fn init(stream: net.Stream, ca_bundle: Certificate.Bundle, host: []const u8) const handshake = cleartext[ct_i..next_handshake_i]; switch (handshake_type) { @enumToInt(HandshakeType.encrypted_extensions) => { + if (handshake_state != .encrypted_extensions) return error.TlsUnexpectedMessage; + handshake_state = .certificate; switch (handshake_cipher) { inline else => |*p| p.transcript_hash.update(wrapped_handshake), } @@ -403,7 +420,11 @@ pub fn init(stream: net.Stream, ca_bundle: Certificate.Bundle, host: []const u8) switch (handshake_cipher) { inline else => |*p| p.transcript_hash.update(wrapped_handshake), } - if (cert_verification_done) break :cert; + switch (handshake_state) { + .certificate => {}, + .trust_chain_established => break :cert, + else => return error.TlsUnexpectedMessage, + } var hs_i: u32 = 0; const cert_req_ctx_len = handshake[hs_i]; hs_i += 1; @@ -421,38 +442,41 @@ pub fn init(stream: net.Stream, ca_bundle: Certificate.Bundle, host: []const u8) .index = hs_i, }; const subject = try subject_cert.parse(); - if (cert_index > 0) { - if (prev_cert.verify(subject)) |_| { - std.debug.print("previous certificate verified\n", .{}); - } else |err| { + if (cert_index == 0) { + // Verify the host on the first certificate. + if (!hostMatchesCommonName(host, subject.commonName())) { + return error.TlsCertificateHostMismatch; + } + + // Keep track of the public key for + // the certificate_verify message + // later. + main_cert_pub_key_algo = subject.pub_key_algo; + const pub_key = subject.pubKey(); + if (pub_key.len > main_cert_pub_key_buf.len) + return error.CertificatePublicKeyInvalid; + @memcpy(&main_cert_pub_key_buf, pub_key.ptr, pub_key.len); + main_cert_pub_key_len = @intCast(@TypeOf(main_cert_pub_key_len), pub_key.len); + } else { + prev_cert.verify(subject) catch |err| { std.debug.print("unable to validate previous cert: {s}\n", .{ @errorName(err), }); - } - } else { - // Verify the host on the first certificate. - const common_name = subject.commonName(); - if (mem.eql(u8, common_name, host)) { - std.debug.print("exact host match\n", .{}); - } else if (mem.startsWith(u8, common_name, "*.") and - (mem.endsWith(u8, host, common_name[1..]) or - mem.eql(u8, common_name[2..], host))) - { - std.debug.print("wildcard host match\n", .{}); - } else { - std.debug.print("host does not match\n", .{}); - return error.TlsCertificateInvalidHost; - } + return err; + }; } if (ca_bundle.verify(subject)) |_| { - std.debug.print("found a root CA cert matching issuer. verification success!\n", .{}); - cert_verification_done = true; + handshake_state = .trust_chain_established; break :cert; - } else |err| { - std.debug.print("unable to validate cert against system root CAs: {s}\n", .{ - @errorName(err), - }); + } else |err| switch (err) { + error.IssuerNotFound => {}, + else => |e| { + std.debug.print("unable to validate cert against system root CAs: {s}\n", .{ + @errorName(e), + }); + return e; + }, } prev_cert = subject; @@ -465,12 +489,46 @@ pub fn init(stream: net.Stream, ca_bundle: Certificate.Bundle, host: []const u8) } }, @enumToInt(HandshakeType.certificate_verify) => { - switch (handshake_cipher) { - inline else => |*p| p.transcript_hash.update(wrapped_handshake), + switch (handshake_state) { + .trust_chain_established => handshake_state = .finished, + .certificate => return error.TlsCertificateNotVerified, + else => return error.TlsUnexpectedMessage, + } + + const algorithm = @intToEnum(tls.SignatureScheme, mem.readIntBig(u16, handshake[0..2])); + const sig_len = mem.readIntBig(u16, handshake[2..4]); + if (4 + sig_len > handshake.len) return error.TlsBadLength; + const encoded_sig = handshake[4..][0..sig_len]; + const max_digest_len = 64; + var verify_buffer = + ([1]u8{0x20} ** 64) ++ + "TLS 1.3, server CertificateVerify\x00".* ++ + ([1]u8{undefined} ** max_digest_len); + + const verify_bytes = switch (handshake_cipher) { + inline else => |*p| v: { + const transcript_digest = p.transcript_hash.peek(); + verify_buffer[verify_buffer.len - max_digest_len ..][0..transcript_digest.len].* = transcript_digest; + p.transcript_hash.update(wrapped_handshake); + break :v verify_buffer[0 .. verify_buffer.len - max_digest_len + transcript_digest.len]; + }, + }; + const main_cert_pub_key = main_cert_pub_key_buf[0..main_cert_pub_key_len]; + + switch (algorithm) { + .ecdsa_secp256r1_sha256 => { + if (main_cert_pub_key_algo != .X9_62_id_ecPublicKey) + return error.TlsBadSignatureAlgorithm; + const P256 = std.crypto.sign.ecdsa.EcdsaP256Sha256; + const sig = try P256.Signature.fromDer(encoded_sig); + const key = try P256.PublicKey.fromSec1(main_cert_pub_key); + try sig.verify(verify_bytes, key); + }, + else => return error.TlsBadSignatureAlgorithm, } - std.debug.print("ignoring certificate_verify\n", .{}); }, @enumToInt(HandshakeType.finished) => { + if (handshake_state != .finished) return error.TlsUnexpectedMessage; // This message is to trick buggy proxies into behaving correctly. const client_change_cipher_spec_msg = [_]u8{ @enumToInt(ContentType.change_cipher_spec), @@ -762,6 +820,26 @@ fn finishRead(c: *Client, frag: []const u8, in: usize, out: usize) usize { return out; } +fn hostMatchesCommonName(host: []const u8, common_name: []const u8) bool { + if (mem.eql(u8, common_name, host)) { + return true; // exact match + } + + if (mem.startsWith(u8, common_name, "*.")) { + // wildcard certificate, matches any subdomain + if (mem.endsWith(u8, host, common_name[1..])) { + // The host has a subdomain, but the important part matches. + return true; + } + if (mem.eql(u8, common_name[2..], host)) { + // The host has no subdomain and matches exactly. + return true; + } + } + + return false; +} + const builtin = @import("builtin"); const native_endian = builtin.cpu.arch.endian();