Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion SPEC/RFC5280_CHAIN_VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ const tls_config = TlsConfig{

### Caveats

- `skip_cert_verify` defaults to `true` for backward compatibility
- `skip_cert_verify` defaults to `false`; callers must opt out explicitly for test-only/self-signed scenarios
- V1 certificates (no extensions) are accepted as CAs when no basicConstraints is present — this matches common practice but is less strict than RFC 5280's recommendation
- The interop client always uses `skip_cert_verify=true` since interop test peers use various self-signed certs
27 changes: 21 additions & 6 deletions src/quic/tls13.zig
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ pub const TlsConfig = struct {
private_key_bytes: []const u8, // Raw ECDSA P-256 private key (32 bytes)
alpn: []const []const u8,
server_name: ?[]const u8 = null, // SNI (client only)
skip_cert_verify: bool = true, // Skip X.509 chain + CertificateVerify validation
skip_cert_verify: bool = false, // Skip X.509 chain + CertificateVerify validation
ca_bundle: ?*Certificate.Bundle = null, // Caller-owned CA bundle for trust anchor verification
session_ticket: ?*const SessionTicket = null, // Stored ticket from previous connection (client)
ticket_key: ?[16]u8 = null, // AES-128-GCM key for encrypting/decrypting tickets (server)
Expand Down Expand Up @@ -1119,6 +1119,8 @@ pub const Tls13Handshake = struct {
@memcpy(self.leaf_pub_key_buf[0..pub_key.len], pub_key);
self.leaf_pub_key_len = @intCast(pub_key.len);
self.leaf_pub_key_algo = std.meta.activeTag(parsed.pub_key_algo);
} else {
return error.BadCertificate;
}

// Verify hostname if SNI was set
Expand Down Expand Up @@ -1150,10 +1152,9 @@ pub const Tls13Handshake = struct {

// If this is the last cert, verify against CA bundle
if (pos >= cert_list_end) {
if (self.config.ca_bundle) |bundle| {
const now_sec = std.time.timestamp();
bundle.verify(parsed, now_sec) catch return error.BadCertificate;
}
const bundle = self.config.ca_bundle orelse return error.BadCertificate;
const now_sec = std.time.timestamp();
bundle.verify(parsed, now_sec) catch return error.BadCertificate;
}

prev_parsed = parsed;
Expand All @@ -1172,7 +1173,9 @@ pub const Tls13Handshake = struct {

if (msg[0] != @intFromEnum(MsgType.certificate_verify)) return error.UnexpectedMessage;

if (!self.config.skip_cert_verify and self.leaf_pub_key_len > 0) {
if (!self.config.skip_cert_verify) {
if (self.leaf_pub_key_len == 0) return error.BadCertificateVerify;

// Get transcript hash BEFORE updating with CertificateVerify
const transcript_hash = self.transcript.current();

Expand Down Expand Up @@ -2794,6 +2797,16 @@ test "buildServerHello: produces valid message" {
try std.testing.expectEqual(msg.len - 4, body_len);
}

test "TlsConfig defaults to certificate verification enabled" {
const config = TlsConfig{
.cert_chain_der = &.{},
.private_key_bytes = &.{},
.alpn = &.{},
};

try std.testing.expect(!config.skip_cert_verify);
}

test "loopback handshake: client and server complete" {
// Generate an ECDSA P-256 key pair for the server
const server_key_pair = EcdsaP256Sha256.KeyPair.generate();
Expand All @@ -2815,6 +2828,7 @@ test "loopback handshake: client and server complete" {
.private_key_bytes = &.{},
.alpn = &[_][]const u8{"h3"},
.server_name = "localhost",
.skip_cert_verify = true,
};

const server_tp = transport_params.TransportParams{
Expand Down Expand Up @@ -2961,6 +2975,7 @@ test "loopback PSK resumption: two handshakes with session ticket" {
.private_key_bytes = &.{},
.alpn = &[_][]const u8{"h3"},
.server_name = "localhost",
.skip_cert_verify = true,
};

const tp = transport_params.TransportParams{
Expand Down
Loading