authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-01-01 17:52:28-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-01-02 16:57:16-07:00
log97acdeeca86af4111972aeb57dd9c792e7f1f419
tree7fb5e5a511235a920089beaa39340dbcbfd9c0d8
parent3127bd79fb8106c3fdf487486c686a0551c5efa8

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.

2 files changed, 145 insertions(+), 25 deletions(-)

lib/std/crypto/Certificate.zig+141-2
...@@ -81,6 +81,43 @@ pub const NamedCurve = enum {...@@ -81,6 +81,43 @@ pub const NamedCurve = enum {
81 });81 });
82};82};
8383
84pub 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
108pub 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
84pub const Parsed = struct {121pub const Parsed = struct {
85 certificate: Certificate,122 certificate: Certificate,
86 issuer_slice: Slice,123 issuer_slice: Slice,
...@@ -91,6 +128,7 @@ pub const Parsed = struct {...@@ -91,6 +128,7 @@ pub const Parsed = struct {
91 pub_key_algo: PubKeyAlgo,128 pub_key_algo: PubKeyAlgo,
92 pub_key_slice: Slice,129 pub_key_slice: Slice,
93 message_slice: Slice,130 message_slice: Slice,
131 subject_alt_name_slice: Slice,
94 validity: Validity,132 validity: Validity,
95133
96 pub const PubKeyAlgo = union(AlgorithmCategory) {134 pub const PubKeyAlgo = union(AlgorithmCategory) {
...@@ -137,6 +175,10 @@ pub const Parsed = struct {...@@ -137,6 +175,10 @@ pub const Parsed = struct {
137 return p.slice(p.message_slice);175 return p.slice(p.message_slice);
138 }176 }
139177
178 pub fn subjectAltName(p: Parsed) []const u8 {
179 return p.slice(p.subject_alt_name_slice);
180 }
181
140 pub const VerifyError = error{182 pub const VerifyError = error{
141 CertificateIssuerMismatch,183 CertificateIssuerMismatch,
142 CertificateNotYetValid,184 CertificateNotYetValid,
...@@ -152,8 +194,10 @@ pub const Parsed = struct {...@@ -152,8 +194,10 @@ pub const Parsed = struct {
152 CertificateSignatureNamedCurveUnsupported,194 CertificateSignatureNamedCurveUnsupported,
153 };195 };
154196
155 /// This function checks the time validity for the subject only. Checking197 /// This function verifies:
156 /// the issuer's time validity is out of scope.198 /// * That the subject's issuer is indeed the provided issuer.
199 /// * The time validity of the subject.
200 /// * The signature.
157 pub fn verify(parsed_subject: Parsed, parsed_issuer: Parsed) VerifyError!void {201 pub fn verify(parsed_subject: Parsed, parsed_issuer: Parsed) VerifyError!void {
158 // Check that the subject's issuer name matches the issuer's202 // Check that the subject's issuer name matches the issuer's
159 // subject name.203 // subject name.
...@@ -194,6 +238,62 @@ pub const Parsed = struct {...@@ -194,6 +238,62 @@ pub const Parsed = struct {
194 ),238 ),
195 }239 }
196 }240 }
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 }
197};297};
198298
199pub fn parse(cert: Certificate) !Parsed {299pub fn parse(cert: Certificate) !Parsed {
...@@ -268,6 +368,39 @@ pub fn parse(cert: Certificate) !Parsed {...@@ -268,6 +368,39 @@ pub fn parse(cert: Certificate) !Parsed {
268 const sig_elem = try der.Element.parse(cert_bytes, sig_algo.slice.end);368 const sig_elem = try der.Element.parse(cert_bytes, sig_algo.slice.end);
269 const signature = try parseBitString(cert, sig_elem);369 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
271 return .{404 return .{
272 .certificate = cert,405 .certificate = cert,
273 .common_name_slice = common_name,406 .common_name_slice = common_name,
...@@ -282,6 +415,7 @@ pub fn parse(cert: Certificate) !Parsed {...@@ -282,6 +415,7 @@ pub fn parse(cert: Certificate) !Parsed {
282 .not_before = not_before_utc,415 .not_before = not_before_utc,
283 .not_after = not_after_utc,416 .not_after = not_after_utc,
284 },417 },
418 .subject_alt_name_slice = subject_alt_name_slice,
285 };419 };
286}420}
287421
...@@ -444,6 +578,10 @@ pub fn parseNamedCurve(bytes: []const u8, element: der.Element) !NamedCurve {...@@ -444,6 +578,10 @@ pub fn parseNamedCurve(bytes: []const u8, element: der.Element) !NamedCurve {
444 return parseEnum(NamedCurve, bytes, element);578 return parseEnum(NamedCurve, bytes, element);
445}579}
446580
581pub fn parseExtensionId(bytes: []const u8, element: der.Element) !ExtensionId {
582 return parseEnum(ExtensionId, bytes, element);
583}
584
447fn parseEnum(comptime E: type, bytes: []const u8, element: der.Element) !E {585fn parseEnum(comptime E: type, bytes: []const u8, element: der.Element) !E {
448 if (element.identifier.tag != .object_identifier)586 if (element.identifier.tag != .object_identifier)
449 return error.CertificateFieldHasWrongDataType;587 return error.CertificateFieldHasWrongDataType;
...@@ -604,6 +742,7 @@ pub const der = struct {...@@ -604,6 +742,7 @@ pub const der = struct {
604 boolean = 1,742 boolean = 1,
605 integer = 2,743 integer = 2,
606 bitstring = 3,744 bitstring = 3,
745 octetstring = 4,
607 null = 5,746 null = 5,
608 object_identifier = 6,747 object_identifier = 6,
609 sequence = 16,748 sequence = 16,
lib/std/crypto/tls/Client.zig+4-23
...@@ -111,6 +111,9 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) !C...@@ -111,6 +111,9 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) !C
111 .ecdsa_secp256r1_sha256,111 .ecdsa_secp256r1_sha256,
112 .ecdsa_secp384r1_sha384,112 .ecdsa_secp384r1_sha384,
113 .ecdsa_secp521r1_sha512,113 .ecdsa_secp521r1_sha512,
114 .rsa_pss_rsae_sha256,
115 .rsa_pss_rsae_sha384,
116 .rsa_pss_rsae_sha512,
114 .rsa_pkcs1_sha256,117 .rsa_pkcs1_sha256,
115 .rsa_pkcs1_sha384,118 .rsa_pkcs1_sha384,
116 .rsa_pkcs1_sha512,119 .rsa_pkcs1_sha512,
...@@ -444,9 +447,7 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) !C...@@ -444,9 +447,7 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) !C
444 const subject = try subject_cert.parse();447 const subject = try subject_cert.parse();
445 if (cert_index == 0) {448 if (cert_index == 0) {
446 // Verify the host on the first certificate.449 // Verify the host on the first certificate.
447 if (!hostMatchesCommonName(host, subject.commonName())) {450 try subject.verifyHostName(host);
448 return error.TlsCertificateHostMismatch;
449 }
450451
451 // Keep track of the public key for the452 // Keep track of the public key for the
452 // certificate_verify message later.453 // certificate_verify message later.
...@@ -1162,26 +1163,6 @@ fn straddleByte(s1: []const u8, s2: []const u8, index: usize) u8 {...@@ -1162,26 +1163,6 @@ fn straddleByte(s1: []const u8, s2: []const u8, index: usize) u8 {
1162 }1163 }
1163}1164}
11641165
1165fn 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
1185const builtin = @import("builtin");1166const builtin = @import("builtin");
1186const native_endian = builtin.cpu.arch.endian();1167const native_endian = builtin.cpu.arch.endian();
11871168