| ... | ... | @@ -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 | 121 | pub const Parsed = struct { |
| 85 | 122 | certificate: Certificate, |
| 86 | 123 | issuer_slice: Slice, |
| ... | ... | @@ -91,6 +128,7 @@ pub const Parsed = struct { |
| 91 | 128 | pub_key_algo: PubKeyAlgo, |
| 92 | 129 | pub_key_slice: Slice, |
| 93 | 130 | message_slice: Slice, |
| 131 | subject_alt_name_slice: Slice, |
| 94 | 132 | validity: Validity, |
| 95 | 133 | |
| 96 | 134 | pub const PubKeyAlgo = union(AlgorithmCategory) { |
| ... | ... | @@ -137,6 +175,10 @@ pub const Parsed = struct { |
| 137 | 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 | 182 | pub const VerifyError = error{ |
| 141 | 183 | CertificateIssuerMismatch, |
| 142 | 184 | CertificateNotYetValid, |
| ... | ... | @@ -152,8 +194,10 @@ pub const Parsed = struct { |
| 152 | 194 | CertificateSignatureNamedCurveUnsupported, |
| 153 | 195 | }; |
| 154 | 196 | |
| 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. |
| 157 | 201 | pub fn verify(parsed_subject: Parsed, parsed_issuer: Parsed) VerifyError!void { |
| 158 | 202 | // Check that the subject's issuer name matches the issuer's |
| 159 | 203 | // subject name. |
| ... | ... | @@ -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 | 299 | pub fn parse(cert: Certificate) !Parsed { |
| ... | ... | @@ -268,6 +368,39 @@ pub fn parse(cert: Certificate) !Parsed { |
| 268 | 368 | const sig_elem = try der.Element.parse(cert_bytes, sig_algo.slice.end); |
| 269 | 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 | 404 | return .{ |
| 272 | 405 | .certificate = cert, |
| 273 | 406 | .common_name_slice = common_name, |
| ... | ... | @@ -282,6 +415,7 @@ pub fn parse(cert: Certificate) !Parsed { |
| 282 | 415 | .not_before = not_before_utc, |
| 283 | 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 | 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 | 585 | fn parseEnum(comptime E: type, bytes: []const u8, element: der.Element) !E { |
| 448 | 586 | if (element.identifier.tag != .object_identifier) |
| 449 | 587 | return error.CertificateFieldHasWrongDataType; |
| ... | ... | @@ -604,6 +742,7 @@ pub const der = struct { |
| 604 | 742 | boolean = 1, |
| 605 | 743 | integer = 2, |
| 606 | 744 | bitstring = 3, |
| 745 | octetstring = 4, |
| 607 | 746 | null = 5, |
| 608 | 747 | object_identifier = 6, |
| 609 | 748 | sequence = 16, |