Skip to content

Commit c1c910b

Browse files
committed
std.crypto.tls: verify via Subject Alt Name
Previously, the code only checked Common Name, leading to unable to validate valid certificates which relied on the subject_alt_name extension for host name verification. This commit also adds rsa_pss_rsae_* back to the signature algorithms list in the ClientHello.
1 parent 91a1302 commit c1c910b

File tree

2 files changed

+145
-25
lines changed

2 files changed

+145
-25
lines changed

lib/std/crypto/Certificate.zig

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,43 @@ pub const NamedCurve = enum {
8181
});
8282
};
8383

84+
pub const ExtensionId = enum {
85+
subject_key_identifier,
86+
key_usage,
87+
private_key_usage_period,
88+
subject_alt_name,
89+
issuer_alt_name,
90+
basic_constraints,
91+
crl_number,
92+
certificate_policies,
93+
authority_key_identifier,
94+
95+
pub const map = std.ComptimeStringMap(ExtensionId, .{
96+
.{ &[_]u8{ 0x55, 0x1D, 0x0E }, .subject_key_identifier },
97+
.{ &[_]u8{ 0x55, 0x1D, 0x0F }, .key_usage },
98+
.{ &[_]u8{ 0x55, 0x1D, 0x10 }, .private_key_usage_period },
99+
.{ &[_]u8{ 0x55, 0x1D, 0x11 }, .subject_alt_name },
100+
.{ &[_]u8{ 0x55, 0x1D, 0x12 }, .issuer_alt_name },
101+
.{ &[_]u8{ 0x55, 0x1D, 0x13 }, .basic_constraints },
102+
.{ &[_]u8{ 0x55, 0x1D, 0x14 }, .crl_number },
103+
.{ &[_]u8{ 0x55, 0x1D, 0x20 }, .certificate_policies },
104+
.{ &[_]u8{ 0x55, 0x1D, 0x23 }, .authority_key_identifier },
105+
});
106+
};
107+
108+
pub const GeneralNameTag = enum(u5) {
109+
otherName = 0,
110+
rfc822Name = 1,
111+
dNSName = 2,
112+
x400Address = 3,
113+
directoryName = 4,
114+
ediPartyName = 5,
115+
uniformResourceIdentifier = 6,
116+
iPAddress = 7,
117+
registeredID = 8,
118+
_,
119+
};
120+
84121
pub const Parsed = struct {
85122
certificate: Certificate,
86123
issuer_slice: Slice,
@@ -91,6 +128,7 @@ pub const Parsed = struct {
91128
pub_key_algo: PubKeyAlgo,
92129
pub_key_slice: Slice,
93130
message_slice: Slice,
131+
subject_alt_name_slice: Slice,
94132
validity: Validity,
95133

96134
pub const PubKeyAlgo = union(AlgorithmCategory) {
@@ -137,6 +175,10 @@ pub const Parsed = struct {
137175
return p.slice(p.message_slice);
138176
}
139177

178+
pub fn subjectAltName(p: Parsed) []const u8 {
179+
return p.slice(p.subject_alt_name_slice);
180+
}
181+
140182
pub const VerifyError = error{
141183
CertificateIssuerMismatch,
142184
CertificateNotYetValid,
@@ -152,8 +194,10 @@ pub const Parsed = struct {
152194
CertificateSignatureNamedCurveUnsupported,
153195
};
154196

155-
/// This function checks the time validity for the subject only. Checking
156-
/// the issuer's time validity is out of scope.
197+
/// This function verifies:
198+
/// * That the subject's issuer is indeed the provided issuer.
199+
/// * The time validity of the subject.
200+
/// * The signature.
157201
pub fn verify(parsed_subject: Parsed, parsed_issuer: Parsed) VerifyError!void {
158202
// Check that the subject's issuer name matches the issuer's
159203
// subject name.
@@ -194,6 +238,62 @@ pub const Parsed = struct {
194238
),
195239
}
196240
}
241+
242+
pub const VerifyHostNameError = error{
243+
CertificateHostMismatch,
244+
CertificateFieldHasInvalidLength,
245+
};
246+
247+
pub fn verifyHostName(parsed_subject: Parsed, host_name: []const u8) VerifyHostNameError!void {
248+
// If the Subject Alternative Names extension is present, this is
249+
// what to check. Otherwise, only the common name is checked.
250+
const subject_alt_name = parsed_subject.subjectAltName();
251+
if (subject_alt_name.len == 0) {
252+
if (checkHostName(host_name, parsed_subject.commonName())) {
253+
return;
254+
} else {
255+
return error.CertificateHostMismatch;
256+
}
257+
}
258+
259+
const general_names = try der.Element.parse(subject_alt_name, 0);
260+
var name_i = general_names.slice.start;
261+
while (name_i < general_names.slice.end) {
262+
const general_name = try der.Element.parse(subject_alt_name, name_i);
263+
name_i = general_name.slice.end;
264+
switch (@intToEnum(GeneralNameTag, @enumToInt(general_name.identifier.tag))) {
265+
.dNSName => {
266+
const dns_name = subject_alt_name[general_name.slice.start..general_name.slice.end];
267+
if (checkHostName(host_name, dns_name)) return;
268+
},
269+
else => {},
270+
}
271+
}
272+
273+
return error.CertificateHostMismatch;
274+
}
275+
276+
fn checkHostName(host_name: []const u8, dns_name: []const u8) bool {
277+
if (mem.eql(u8, dns_name, host_name)) {
278+
return true; // exact match
279+
}
280+
281+
if (mem.startsWith(u8, dns_name, "*.")) {
282+
// wildcard certificate, matches any subdomain
283+
// TODO: I think wildcards are not supposed to match any prefix but
284+
// only match exactly one subdomain.
285+
if (mem.endsWith(u8, host_name, dns_name[1..])) {
286+
// The host_name has a subdomain, but the important part matches.
287+
return true;
288+
}
289+
if (mem.eql(u8, dns_name[2..], host_name)) {
290+
// The host_name has no subdomain and matches exactly.
291+
return true;
292+
}
293+
}
294+
295+
return false;
296+
}
197297
};
198298

199299
pub fn parse(cert: Certificate) !Parsed {
@@ -268,6 +368,39 @@ pub fn parse(cert: Certificate) !Parsed {
268368
const sig_elem = try der.Element.parse(cert_bytes, sig_algo.slice.end);
269369
const signature = try parseBitString(cert, sig_elem);
270370

371+
// Extensions
372+
var subject_alt_name_slice = der.Element.Slice.empty;
373+
ext: {
374+
if (pub_key_info.slice.end >= tbs_certificate.slice.end)
375+
break :ext;
376+
377+
const outer_extensions = try der.Element.parse(cert_bytes, pub_key_info.slice.end);
378+
if (outer_extensions.identifier.tag != .bitstring)
379+
break :ext;
380+
381+
const extensions = try der.Element.parse(cert_bytes, outer_extensions.slice.start);
382+
383+
var ext_i = extensions.slice.start;
384+
while (ext_i < extensions.slice.end) {
385+
const extension = try der.Element.parse(cert_bytes, ext_i);
386+
ext_i = extension.slice.end;
387+
const oid_elem = try der.Element.parse(cert_bytes, extension.slice.start);
388+
const ext_id = parseExtensionId(cert_bytes, oid_elem) catch |err| switch (err) {
389+
error.CertificateHasUnrecognizedObjectId => continue,
390+
else => |e| return e,
391+
};
392+
const critical_elem = try der.Element.parse(cert_bytes, oid_elem.slice.end);
393+
const ext_bytes_elem = if (critical_elem.identifier.tag != .boolean)
394+
critical_elem
395+
else
396+
try der.Element.parse(cert_bytes, critical_elem.slice.end);
397+
switch (ext_id) {
398+
.subject_alt_name => subject_alt_name_slice = ext_bytes_elem.slice,
399+
else => continue,
400+
}
401+
}
402+
}
403+
271404
return .{
272405
.certificate = cert,
273406
.common_name_slice = common_name,
@@ -282,6 +415,7 @@ pub fn parse(cert: Certificate) !Parsed {
282415
.not_before = not_before_utc,
283416
.not_after = not_after_utc,
284417
},
418+
.subject_alt_name_slice = subject_alt_name_slice,
285419
};
286420
}
287421

@@ -444,6 +578,10 @@ pub fn parseNamedCurve(bytes: []const u8, element: der.Element) !NamedCurve {
444578
return parseEnum(NamedCurve, bytes, element);
445579
}
446580

581+
pub fn parseExtensionId(bytes: []const u8, element: der.Element) !ExtensionId {
582+
return parseEnum(ExtensionId, bytes, element);
583+
}
584+
447585
fn parseEnum(comptime E: type, bytes: []const u8, element: der.Element) !E {
448586
if (element.identifier.tag != .object_identifier)
449587
return error.CertificateFieldHasWrongDataType;
@@ -604,6 +742,7 @@ pub const der = struct {
604742
boolean = 1,
605743
integer = 2,
606744
bitstring = 3,
745+
octetstring = 4,
607746
null = 5,
608747
object_identifier = 6,
609748
sequence = 16,

lib/std/crypto/tls/Client.zig

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) !C
111111
.ecdsa_secp256r1_sha256,
112112
.ecdsa_secp384r1_sha384,
113113
.ecdsa_secp521r1_sha512,
114+
.rsa_pss_rsae_sha256,
115+
.rsa_pss_rsae_sha384,
116+
.rsa_pss_rsae_sha512,
114117
.rsa_pkcs1_sha256,
115118
.rsa_pkcs1_sha384,
116119
.rsa_pkcs1_sha512,
@@ -444,9 +447,7 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) !C
444447
const subject = try subject_cert.parse();
445448
if (cert_index == 0) {
446449
// Verify the host on the first certificate.
447-
if (!hostMatchesCommonName(host, subject.commonName())) {
448-
return error.TlsCertificateHostMismatch;
449-
}
450+
try subject.verifyHostName(host);
450451

451452
// Keep track of the public key for the
452453
// certificate_verify message later.
@@ -1162,26 +1163,6 @@ fn straddleByte(s1: []const u8, s2: []const u8, index: usize) u8 {
11621163
}
11631164
}
11641165

1165-
fn hostMatchesCommonName(host: []const u8, common_name: []const u8) bool {
1166-
if (mem.eql(u8, common_name, host)) {
1167-
return true; // exact match
1168-
}
1169-
1170-
if (mem.startsWith(u8, common_name, "*.")) {
1171-
// wildcard certificate, matches any subdomain
1172-
if (mem.endsWith(u8, host, common_name[1..])) {
1173-
// The host has a subdomain, but the important part matches.
1174-
return true;
1175-
}
1176-
if (mem.eql(u8, common_name[2..], host)) {
1177-
// The host has no subdomain and matches exactly.
1178-
return true;
1179-
}
1180-
}
1181-
1182-
return false;
1183-
}
1184-
11851166
const builtin = @import("builtin");
11861167
const native_endian = builtin.cpu.arch.endian();
11871168

0 commit comments

Comments
 (0)