Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 3 additions & 2 deletions SPEC/RFC5280_CHAIN_VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#### System Root CAs
- `loadSystemCaBundle()` helper wraps `Certificate.Bundle.rescan()` for OS-native roots
- Supports macOS (Keychain), Linux (`/etc/ssl/certs/`), Windows (CertStore), FreeBSD, OpenBSD, etc.
- Callers must pass a populated `ca_bundle` when `skip_cert_verify = false`; the handshake now fails closed if no trust bundle is provided

### Configuration

Expand All @@ -39,10 +40,10 @@ const tls_config = TlsConfig{
- **Extended Key Usage** — `id-kp-serverAuth` not checked on leaf (recommended but not required by TLS 1.3)
- **Name Constraints** — RFC 5280 §4.2.1.10
- **Policy Constraints** — RFC 5280 §4.2.1.11
- **Mandatory ca_bundle enforcement** — When `skip_cert_verify=false` and no `ca_bundle` is provided, the chain's self-signed root is accepted without trust anchor verification
- **Automatic system CA loading** — callers must currently wire `loadSystemCaBundle()` into client setup themselves; the handshake does not auto-populate `ca_bundle`

### 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