| ... | @@ -81,6 +81,43 @@ pub const NamedCurve = enum { | ... | @@ -81,6 +81,43 @@ pub const NamedCurve = enum { |
| 81 | }); | 81 | }); |
| 82 | }; | 82 | }; |
| 83 | | 83 | |
| | 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 | |
| 84 | pub const Parsed = struct { | 121 | pub 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, |
| 95 | | 133 | |
| 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 | } |
| 139 | | 177 | |
| | 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 | }; |
| 154 | | 196 | |
| 155 | /// This function checks the time validity for the subject only. Checking | 197 | /// 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's | 202 | // 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 | }; |
| 198 | | 298 | |
| 199 | pub fn parse(cert: Certificate) !Parsed { | 299 | pub 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); |
| 270 | | 370 | |
| | 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 | } |
| 287 | | 421 | |
| ... | @@ -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 | } |
| 446 | | 580 | |
| | 581 | pub fn parseExtensionId(bytes: []const u8, element: der.Element) !ExtensionId { |
| | 582 | return parseEnum(ExtensionId, bytes, element); |
| | 583 | } |
| | 584 | |
| 447 | fn parseEnum(comptime E: type, bytes: []const u8, element: der.Element) !E { | 585 | fn 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, |