mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 15:13:28 +00:00
449 lines
18 KiB
Zig
449 lines
18 KiB
Zig
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
const mem = std.mem;
|
|
const crypto = std.crypto;
|
|
const Certificate = crypto.Certificate;
|
|
|
|
const Transcript = @import("transcript.zig").Transcript;
|
|
const PrivateKey = @import("PrivateKey.zig");
|
|
const record = @import("record.zig");
|
|
const rsa = @import("rsa/rsa.zig");
|
|
const proto = @import("protocol.zig");
|
|
|
|
const X25519 = crypto.dh.X25519;
|
|
const EcdsaP256Sha256 = crypto.sign.ecdsa.EcdsaP256Sha256;
|
|
const EcdsaP384Sha384 = crypto.sign.ecdsa.EcdsaP384Sha384;
|
|
const Kyber768 = crypto.kem.kyber_d00.Kyber768;
|
|
|
|
pub const supported_signature_algorithms = &[_]proto.SignatureScheme{
|
|
.ecdsa_secp256r1_sha256,
|
|
.ecdsa_secp384r1_sha384,
|
|
.rsa_pss_rsae_sha256,
|
|
.rsa_pss_rsae_sha384,
|
|
.rsa_pss_rsae_sha512,
|
|
.ed25519,
|
|
.rsa_pkcs1_sha1,
|
|
.rsa_pkcs1_sha256,
|
|
.rsa_pkcs1_sha384,
|
|
};
|
|
|
|
pub const CertKeyPair = struct {
|
|
/// A chain of one or more certificates, leaf first.
|
|
///
|
|
/// Each X.509 certificate contains the public key of a key pair, extra
|
|
/// information (the name of the holder, the name of an issuer of the
|
|
/// certificate, validity time spans) and a signature generated using the
|
|
/// private key of the issuer of the certificate.
|
|
///
|
|
/// All certificates from the bundle are sent to the other side when creating
|
|
/// Certificate tls message.
|
|
///
|
|
/// Leaf certificate and private key are used to create signature for
|
|
/// CertifyVerify tls message.
|
|
bundle: Certificate.Bundle,
|
|
|
|
/// Private key corresponding to the public key in leaf certificate from the
|
|
/// bundle.
|
|
key: PrivateKey,
|
|
|
|
pub fn load(
|
|
allocator: std.mem.Allocator,
|
|
dir: std.fs.Dir,
|
|
cert_path: []const u8,
|
|
key_path: []const u8,
|
|
) !CertKeyPair {
|
|
var bundle: Certificate.Bundle = .{};
|
|
try bundle.addCertsFromFilePath(allocator, dir, cert_path);
|
|
|
|
const key_file = try dir.openFile(key_path, .{});
|
|
defer key_file.close();
|
|
const key = try PrivateKey.fromFile(allocator, key_file);
|
|
|
|
return .{ .bundle = bundle, .key = key };
|
|
}
|
|
|
|
pub fn deinit(c: *CertKeyPair, allocator: std.mem.Allocator) void {
|
|
c.bundle.deinit(allocator);
|
|
}
|
|
};
|
|
|
|
pub const CertBundle = struct {
|
|
// A chain of one or more certificates.
|
|
//
|
|
// They are used to verify that certificate chain sent by the other side
|
|
// forms valid trust chain.
|
|
bundle: Certificate.Bundle = .{},
|
|
|
|
pub fn fromFile(allocator: std.mem.Allocator, dir: std.fs.Dir, path: []const u8) !CertBundle {
|
|
var bundle: Certificate.Bundle = .{};
|
|
try bundle.addCertsFromFilePath(allocator, dir, path);
|
|
return .{ .bundle = bundle };
|
|
}
|
|
|
|
pub fn fromSystem(allocator: std.mem.Allocator) !CertBundle {
|
|
var bundle: Certificate.Bundle = .{};
|
|
try bundle.rescan(allocator);
|
|
return .{ .bundle = bundle };
|
|
}
|
|
|
|
pub fn deinit(cb: *CertBundle, allocator: std.mem.Allocator) void {
|
|
cb.bundle.deinit(allocator);
|
|
}
|
|
};
|
|
|
|
pub const CertificateBuilder = struct {
|
|
bundle: Certificate.Bundle,
|
|
key: PrivateKey,
|
|
transcript: *Transcript,
|
|
tls_version: proto.Version = .tls_1_3,
|
|
side: proto.Side = .client,
|
|
|
|
pub fn makeCertificate(h: CertificateBuilder, buf: []u8) ![]const u8 {
|
|
var w = record.Writer{ .buf = buf };
|
|
const certs = h.bundle.bytes.items;
|
|
const certs_count = h.bundle.map.size;
|
|
|
|
// Differences between tls 1.3 and 1.2
|
|
// TLS 1.3 has request context in header and extensions for each certificate.
|
|
// Here we use empty length for each field.
|
|
// TLS 1.2 don't have these two fields.
|
|
const request_context, const extensions = if (h.tls_version == .tls_1_3)
|
|
.{ &[_]u8{0}, &[_]u8{ 0, 0 } }
|
|
else
|
|
.{ &[_]u8{}, &[_]u8{} };
|
|
const certs_len = certs.len + (3 + extensions.len) * certs_count;
|
|
|
|
// Write handshake header
|
|
try w.writeHandshakeHeader(.certificate, certs_len + request_context.len + 3);
|
|
try w.write(request_context);
|
|
try w.writeInt(@as(u24, @intCast(certs_len)));
|
|
|
|
// Write each certificate
|
|
var index: u32 = 0;
|
|
while (index < certs.len) {
|
|
const e = try Certificate.der.Element.parse(certs, index);
|
|
const cert = certs[index..e.slice.end];
|
|
try w.writeInt(@as(u24, @intCast(cert.len))); // certificate length
|
|
try w.write(cert); // certificate
|
|
try w.write(extensions); // certificate extensions
|
|
index = e.slice.end;
|
|
}
|
|
return w.getWritten();
|
|
}
|
|
|
|
pub fn makeCertificateVerify(h: CertificateBuilder, buf: []u8) ![]const u8 {
|
|
var w = record.Writer{ .buf = buf };
|
|
const signature, const signature_scheme = try h.createSignature();
|
|
try w.writeHandshakeHeader(.certificate_verify, signature.len + 4);
|
|
try w.writeEnum(signature_scheme);
|
|
try w.writeInt(@as(u16, @intCast(signature.len)));
|
|
try w.write(signature);
|
|
return w.getWritten();
|
|
}
|
|
|
|
/// Creates signature for client certificate signature message.
|
|
/// Returns signature bytes and signature scheme.
|
|
inline fn createSignature(h: CertificateBuilder) !struct { []const u8, proto.SignatureScheme } {
|
|
switch (h.key.signature_scheme) {
|
|
inline .ecdsa_secp256r1_sha256,
|
|
.ecdsa_secp384r1_sha384,
|
|
=> |comptime_scheme| {
|
|
const Ecdsa = SchemeEcdsa(comptime_scheme);
|
|
const key = h.key.key.ecdsa;
|
|
const key_len = Ecdsa.SecretKey.encoded_length;
|
|
if (key.len < key_len) return error.InvalidEncoding;
|
|
const secret_key = try Ecdsa.SecretKey.fromBytes(key[0..key_len].*);
|
|
const key_pair = try Ecdsa.KeyPair.fromSecretKey(secret_key);
|
|
var signer = try key_pair.signer(null);
|
|
h.setSignatureVerifyBytes(&signer);
|
|
const signature = try signer.finalize();
|
|
var buf: [Ecdsa.Signature.der_encoded_length_max]u8 = undefined;
|
|
return .{ signature.toDer(&buf), comptime_scheme };
|
|
},
|
|
inline .rsa_pss_rsae_sha256,
|
|
.rsa_pss_rsae_sha384,
|
|
.rsa_pss_rsae_sha512,
|
|
=> |comptime_scheme| {
|
|
const Hash = SchemeHash(comptime_scheme);
|
|
var signer = try h.key.key.rsa.signerOaep(Hash, null);
|
|
h.setSignatureVerifyBytes(&signer);
|
|
var buf: [512]u8 = undefined;
|
|
const signature = try signer.finalize(&buf);
|
|
return .{ signature.bytes, comptime_scheme };
|
|
},
|
|
else => return error.TlsUnknownSignatureScheme,
|
|
}
|
|
}
|
|
|
|
fn setSignatureVerifyBytes(h: CertificateBuilder, signer: anytype) void {
|
|
if (h.tls_version == .tls_1_2) {
|
|
// tls 1.2 signature uses current transcript hash value.
|
|
// ref: https://datatracker.ietf.org/doc/html/rfc5246.html#section-7.4.8
|
|
const Hash = @TypeOf(signer.h);
|
|
signer.h = h.transcript.hash(Hash);
|
|
} else {
|
|
// tls 1.3 signature is computed over concatenation of 64 spaces,
|
|
// context, separator and content.
|
|
// ref: https://datatracker.ietf.org/doc/html/rfc8446#section-4.4.3
|
|
if (h.side == .server) {
|
|
signer.update(h.transcript.serverCertificateVerify());
|
|
} else {
|
|
signer.update(h.transcript.clientCertificateVerify());
|
|
}
|
|
}
|
|
}
|
|
|
|
fn SchemeEcdsa(comptime scheme: proto.SignatureScheme) type {
|
|
return switch (scheme) {
|
|
.ecdsa_secp256r1_sha256 => EcdsaP256Sha256,
|
|
.ecdsa_secp384r1_sha384 => EcdsaP384Sha384,
|
|
else => unreachable,
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const CertificateParser = struct {
|
|
pub_key_algo: Certificate.Parsed.PubKeyAlgo = undefined,
|
|
pub_key_buf: [600]u8 = undefined,
|
|
pub_key: []const u8 = undefined,
|
|
|
|
signature_scheme: proto.SignatureScheme = @enumFromInt(0),
|
|
signature_buf: [1024]u8 = undefined,
|
|
signature: []const u8 = undefined,
|
|
|
|
root_ca: Certificate.Bundle,
|
|
host: []const u8,
|
|
skip_verify: bool = false,
|
|
now_sec: i64 = 0,
|
|
|
|
pub fn parseCertificate(h: *CertificateParser, d: *record.Decoder, tls_version: proto.Version) !void {
|
|
if (h.now_sec == 0) {
|
|
h.now_sec = std.time.timestamp();
|
|
}
|
|
if (tls_version == .tls_1_3) {
|
|
const request_context = try d.decode(u8);
|
|
if (request_context != 0) return error.TlsIllegalParameter;
|
|
}
|
|
|
|
var trust_chain_established = false;
|
|
var last_cert: ?Certificate.Parsed = null;
|
|
const certs_len = try d.decode(u24);
|
|
const start_idx = d.idx;
|
|
while (d.idx - start_idx < certs_len) {
|
|
const cert_len = try d.decode(u24);
|
|
// std.debug.print("=> {} {} {} {}\n", .{ certs_len, d.idx, cert_len, d.payload.len });
|
|
const cert = try d.slice(cert_len);
|
|
if (tls_version == .tls_1_3) {
|
|
// certificate extensions present in tls 1.3
|
|
try d.skip(try d.decode(u16));
|
|
}
|
|
if (trust_chain_established)
|
|
continue;
|
|
|
|
const subject = try (Certificate{ .buffer = cert, .index = 0 }).parse();
|
|
if (last_cert) |pc| {
|
|
if (pc.verify(subject, h.now_sec)) {
|
|
last_cert = subject;
|
|
} else |err| switch (err) {
|
|
error.CertificateIssuerMismatch => {
|
|
// skip certificate which is not part of the chain
|
|
continue;
|
|
},
|
|
else => return err,
|
|
}
|
|
} else { // first certificate
|
|
if (!h.skip_verify and h.host.len > 0) {
|
|
try subject.verifyHostName(h.host);
|
|
}
|
|
h.pub_key = dupe(&h.pub_key_buf, subject.pubKey());
|
|
h.pub_key_algo = subject.pub_key_algo;
|
|
last_cert = subject;
|
|
}
|
|
if (!h.skip_verify) {
|
|
if (h.root_ca.verify(last_cert.?, h.now_sec)) |_| {
|
|
trust_chain_established = true;
|
|
} else |err| switch (err) {
|
|
error.CertificateIssuerNotFound => {},
|
|
else => return err,
|
|
}
|
|
}
|
|
}
|
|
if (!h.skip_verify and !trust_chain_established) {
|
|
return error.CertificateIssuerNotFound;
|
|
}
|
|
}
|
|
|
|
pub fn parseCertificateVerify(h: *CertificateParser, d: *record.Decoder) !void {
|
|
h.signature_scheme = try d.decode(proto.SignatureScheme);
|
|
h.signature = dupe(&h.signature_buf, try d.slice(try d.decode(u16)));
|
|
}
|
|
|
|
pub fn verifySignature(h: *CertificateParser, verify_bytes: []const u8) !void {
|
|
switch (h.signature_scheme) {
|
|
inline .ecdsa_secp256r1_sha256,
|
|
.ecdsa_secp384r1_sha384,
|
|
=> |comptime_scheme| {
|
|
if (h.pub_key_algo != .X9_62_id_ecPublicKey) return error.TlsBadSignatureScheme;
|
|
const cert_named_curve = h.pub_key_algo.X9_62_id_ecPublicKey;
|
|
switch (cert_named_curve) {
|
|
inline .secp384r1, .X9_62_prime256v1 => |comptime_cert_named_curve| {
|
|
const Ecdsa = SchemeEcdsaCert(comptime_scheme, comptime_cert_named_curve);
|
|
const key = try Ecdsa.PublicKey.fromSec1(h.pub_key);
|
|
const sig = try Ecdsa.Signature.fromDer(h.signature);
|
|
try sig.verify(verify_bytes, key);
|
|
},
|
|
else => return error.TlsUnknownSignatureScheme,
|
|
}
|
|
},
|
|
.ed25519 => {
|
|
if (h.pub_key_algo != .curveEd25519) return error.TlsBadSignatureScheme;
|
|
const Eddsa = crypto.sign.Ed25519;
|
|
if (h.signature.len != Eddsa.Signature.encoded_length) return error.InvalidEncoding;
|
|
const sig = Eddsa.Signature.fromBytes(h.signature[0..Eddsa.Signature.encoded_length].*);
|
|
if (h.pub_key.len != Eddsa.PublicKey.encoded_length) return error.InvalidEncoding;
|
|
const key = try Eddsa.PublicKey.fromBytes(h.pub_key[0..Eddsa.PublicKey.encoded_length].*);
|
|
try sig.verify(verify_bytes, key);
|
|
},
|
|
inline .rsa_pss_rsae_sha256,
|
|
.rsa_pss_rsae_sha384,
|
|
.rsa_pss_rsae_sha512,
|
|
=> |comptime_scheme| {
|
|
if (h.pub_key_algo != .rsaEncryption) return error.TlsBadSignatureScheme;
|
|
const Hash = SchemeHash(comptime_scheme);
|
|
const pk = try rsa.PublicKey.fromDer(h.pub_key);
|
|
const sig = rsa.Pss(Hash).Signature{ .bytes = h.signature };
|
|
try sig.verify(verify_bytes, pk, null);
|
|
},
|
|
inline .rsa_pkcs1_sha1,
|
|
.rsa_pkcs1_sha256,
|
|
.rsa_pkcs1_sha384,
|
|
.rsa_pkcs1_sha512,
|
|
=> |comptime_scheme| {
|
|
if (h.pub_key_algo != .rsaEncryption) return error.TlsBadSignatureScheme;
|
|
const Hash = SchemeHash(comptime_scheme);
|
|
const pk = try rsa.PublicKey.fromDer(h.pub_key);
|
|
const sig = rsa.PKCS1v1_5(Hash).Signature{ .bytes = h.signature };
|
|
try sig.verify(verify_bytes, pk);
|
|
},
|
|
else => return error.TlsUnknownSignatureScheme,
|
|
}
|
|
}
|
|
|
|
fn SchemeEcdsaCert(comptime scheme: proto.SignatureScheme, comptime cert_named_curve: Certificate.NamedCurve) type {
|
|
const Sha256 = crypto.hash.sha2.Sha256;
|
|
const Sha384 = crypto.hash.sha2.Sha384;
|
|
const Ecdsa = crypto.sign.ecdsa.Ecdsa;
|
|
|
|
return switch (scheme) {
|
|
.ecdsa_secp256r1_sha256 => Ecdsa(cert_named_curve.Curve(), Sha256),
|
|
.ecdsa_secp384r1_sha384 => Ecdsa(cert_named_curve.Curve(), Sha384),
|
|
else => @compileError("bad scheme"),
|
|
};
|
|
}
|
|
};
|
|
|
|
fn SchemeHash(comptime scheme: proto.SignatureScheme) type {
|
|
const Sha256 = crypto.hash.sha2.Sha256;
|
|
const Sha384 = crypto.hash.sha2.Sha384;
|
|
const Sha512 = crypto.hash.sha2.Sha512;
|
|
|
|
return switch (scheme) {
|
|
.rsa_pkcs1_sha1 => crypto.hash.Sha1,
|
|
.rsa_pss_rsae_sha256, .rsa_pkcs1_sha256 => Sha256,
|
|
.rsa_pss_rsae_sha384, .rsa_pkcs1_sha384 => Sha384,
|
|
.rsa_pss_rsae_sha512, .rsa_pkcs1_sha512 => Sha512,
|
|
else => @compileError("bad scheme"),
|
|
};
|
|
}
|
|
|
|
pub fn dupe(buf: []u8, data: []const u8) []u8 {
|
|
const n = @min(data.len, buf.len);
|
|
@memcpy(buf[0..n], data[0..n]);
|
|
return buf[0..n];
|
|
}
|
|
|
|
pub const DhKeyPair = struct {
|
|
x25519_kp: X25519.KeyPair = undefined,
|
|
secp256r1_kp: EcdsaP256Sha256.KeyPair = undefined,
|
|
secp384r1_kp: EcdsaP384Sha384.KeyPair = undefined,
|
|
kyber768_kp: Kyber768.KeyPair = undefined,
|
|
|
|
pub const seed_len = 32 + 32 + 48 + 64;
|
|
|
|
pub fn init(seed: [seed_len]u8, named_groups: []const proto.NamedGroup) !DhKeyPair {
|
|
var kp: DhKeyPair = .{};
|
|
for (named_groups) |ng|
|
|
switch (ng) {
|
|
.x25519 => kp.x25519_kp = try X25519.KeyPair.create(seed[0..][0..X25519.seed_length].*),
|
|
.secp256r1 => kp.secp256r1_kp = try EcdsaP256Sha256.KeyPair.create(seed[32..][0..EcdsaP256Sha256.KeyPair.seed_length].*),
|
|
.secp384r1 => kp.secp384r1_kp = try EcdsaP384Sha384.KeyPair.create(seed[32 + 32 ..][0..EcdsaP384Sha384.KeyPair.seed_length].*),
|
|
.x25519_kyber768d00 => kp.kyber768_kp = try Kyber768.KeyPair.create(seed[32 + 32 + 48 ..][0..Kyber768.seed_length].*),
|
|
else => return error.TlsIllegalParameter,
|
|
};
|
|
return kp;
|
|
}
|
|
|
|
pub inline fn sharedKey(self: DhKeyPair, named_group: proto.NamedGroup, server_pub_key: []const u8) ![]const u8 {
|
|
return switch (named_group) {
|
|
.x25519 => brk: {
|
|
if (server_pub_key.len != X25519.public_length)
|
|
return error.TlsIllegalParameter;
|
|
break :brk &(try X25519.scalarmult(
|
|
self.x25519_kp.secret_key,
|
|
server_pub_key[0..X25519.public_length].*,
|
|
));
|
|
},
|
|
.secp256r1 => brk: {
|
|
const pk = try EcdsaP256Sha256.PublicKey.fromSec1(server_pub_key);
|
|
const mul = try pk.p.mulPublic(self.secp256r1_kp.secret_key.bytes, .big);
|
|
break :brk &mul.affineCoordinates().x.toBytes(.big);
|
|
},
|
|
.secp384r1 => brk: {
|
|
const pk = try EcdsaP384Sha384.PublicKey.fromSec1(server_pub_key);
|
|
const mul = try pk.p.mulPublic(self.secp384r1_kp.secret_key.bytes, .big);
|
|
break :brk &mul.affineCoordinates().x.toBytes(.big);
|
|
},
|
|
.x25519_kyber768d00 => brk: {
|
|
const xksl = crypto.dh.X25519.public_length;
|
|
const hksl = xksl + Kyber768.ciphertext_length;
|
|
if (server_pub_key.len != hksl)
|
|
return error.TlsIllegalParameter;
|
|
|
|
break :brk &((crypto.dh.X25519.scalarmult(
|
|
self.x25519_kp.secret_key,
|
|
server_pub_key[0..xksl].*,
|
|
) catch return error.TlsDecryptFailure) ++ (self.kyber768_kp.secret_key.decaps(
|
|
server_pub_key[xksl..hksl],
|
|
) catch return error.TlsDecryptFailure));
|
|
},
|
|
else => return error.TlsIllegalParameter,
|
|
};
|
|
}
|
|
|
|
// Returns 32, 65, 97 or 1216 bytes
|
|
pub inline fn publicKey(self: DhKeyPair, named_group: proto.NamedGroup) ![]const u8 {
|
|
return switch (named_group) {
|
|
.x25519 => &self.x25519_kp.public_key,
|
|
.secp256r1 => &self.secp256r1_kp.public_key.toUncompressedSec1(),
|
|
.secp384r1 => &self.secp384r1_kp.public_key.toUncompressedSec1(),
|
|
.x25519_kyber768d00 => &self.x25519_kp.public_key ++ self.kyber768_kp.public_key.toBytes(),
|
|
else => return error.TlsIllegalParameter,
|
|
};
|
|
}
|
|
};
|
|
|
|
const testing = std.testing;
|
|
const testu = @import("testu.zig");
|
|
|
|
test "DhKeyPair.x25519" {
|
|
var seed: [DhKeyPair.seed_len]u8 = undefined;
|
|
testu.fill(&seed);
|
|
const server_pub_key = &testu.hexToBytes("3303486548531f08d91e675caf666c2dc924ac16f47a861a7f4d05919d143637");
|
|
const expected = &testu.hexToBytes(
|
|
\\ F1 67 FB 4A 49 B2 91 77 08 29 45 A1 F7 08 5A 21
|
|
\\ AF FE 9E 78 C2 03 9B 81 92 40 72 73 74 7A 46 1E
|
|
);
|
|
const kp = try DhKeyPair.init(seed, &.{.x25519});
|
|
try testing.expectEqualSlices(u8, expected, try kp.sharedKey(.x25519, server_pub_key));
|
|
}
|