diff options
| author | Andrew Kelley <andrew@ziglang.org> | 2022-12-22 19:56:43 -0700 |
|---|---|---|
| committer | Andrew Kelley <andrew@ziglang.org> | 2023-01-02 16:57:15 -0700 |
| commit | 7cb535d4b54a4e5627edc6b558d1f31b41651328 (patch) | |
| tree | 98dbf7e28271b958ab12593fa3fff2fa3da0c56c /lib | |
| parent | 862ecf23442b7b399f07400c8997c6481f329853 (diff) | |
| download | zig-7cb535d4b54a4e5627edc6b558d1f31b41651328.tar.gz zig-7cb535d4b54a4e5627edc6b558d1f31b41651328.zip | |
std.crypto.tls.Certificate: verify time validity
When scanning the file system for root certificates, expired
certificates are skipped and therefore not used for verification in TLS
sessions. There is only this one check, however, so a long-running
server will need to periodically rescan for a new Certificate.Bundle
and strategically start using it for new sessions. In this commit I made
the judgement call that applications would like to opt-in to root
certificate rescanning at a point in time that makes sense for that
application, as opposed to having the system clock potentially start
causing connections to fail.
Certificate verification checks the subject only, as opposed to both the
subject and the issuer. The idea is that the trust chain analysis will
always check the subject, leading to every certificate in the chain's
validity being checked exactly once, with the root certificate's
validity checked upon scanning.
Furthermore, this commit adjusts the scanning logic to fully parse
certificates, even though only the subject is technically needed. This
allows relying on parsing to succeed later on.
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/std/crypto/Certificate.zig | 164 | ||||
| -rw-r--r-- | lib/std/crypto/Certificate/Bundle.zig | 45 | ||||
| -rw-r--r-- | lib/std/crypto/der.zig | 2 |
3 files changed, 188 insertions, 23 deletions
diff --git a/lib/std/crypto/Certificate.zig b/lib/std/crypto/Certificate.zig index 0cdafa7ade..ba36958ae8 100644 --- a/lib/std/crypto/Certificate.zig +++ b/lib/std/crypto/Certificate.zig @@ -79,6 +79,12 @@ pub const Parsed = struct { pub_key_algo: AlgorithmCategory, pub_key_slice: Slice, message_slice: Slice, + validity: Validity, + + pub const Validity = struct { + not_before: u64, + not_after: u64, + }; pub const Slice = der.Element.Slice; @@ -110,6 +116,8 @@ pub const Parsed = struct { return p.slice(p.message_slice); } + /// This function checks the time validity for the subject only. Checking + /// the issuer's time validity is out of scope. pub fn verify(parsed_subject: Parsed, parsed_issuer: Parsed) !void { // Check that the subject's issuer name matches the issuer's // subject name. @@ -117,8 +125,11 @@ pub const Parsed = struct { return error.CertificateIssuerMismatch; } - // TODO check the time validity for the subject - // TODO check the time validity for the issuer + const now_sec = std.time.timestamp(); + if (now_sec < parsed_subject.validity.not_before) + return error.CertificateNotYetValid; + if (now_sec > parsed_subject.validity.not_after) + return error.CertificateExpired; switch (parsed_subject.signature_algorithm) { inline .sha1WithRSAEncryption, @@ -157,6 +168,10 @@ pub fn parse(cert: Certificate) !Parsed { const tbs_signature = try der.parseElement(cert_bytes, serial_number.slice.end); const issuer = try der.parseElement(cert_bytes, tbs_signature.slice.end); const validity = try der.parseElement(cert_bytes, issuer.slice.end); + const not_before = try der.parseElement(cert_bytes, validity.slice.start); + const not_before_utc = try parseTime(cert, not_before); + const not_after = try der.parseElement(cert_bytes, not_before.slice.end); + const not_after_utc = try parseTime(cert, not_after); const subject = try der.parseElement(cert_bytes, validity.slice.end); const pub_key_info = try der.parseElement(cert_bytes, subject.slice.end); @@ -198,6 +213,10 @@ pub fn parse(cert: Certificate) !Parsed { .message_slice = .{ .start = certificate.slice.start, .end = tbs_certificate.slice.end }, .pub_key_algo = pub_key_algo, .pub_key_slice = pub_key, + .validity = .{ + .not_before = not_before_utc, + .not_after = not_after_utc, + }, }; } @@ -208,7 +227,7 @@ pub fn verify(subject: Certificate, issuer: Certificate) !void { } pub fn contents(cert: Certificate, elem: der.Element) []const u8 { - return cert.buffer[elem.start..elem.end]; + return cert.buffer[elem.slice.start..elem.slice.end]; } pub fn parseBitString(cert: Certificate, elem: der.Element) !der.Element.Slice { @@ -217,6 +236,133 @@ pub fn parseBitString(cert: Certificate, elem: der.Element) !der.Element.Slice { return .{ .start = elem.slice.start + 1, .end = elem.slice.end }; } +/// Returns number of seconds since epoch. +pub fn parseTime(cert: Certificate, elem: der.Element) !u64 { + const bytes = cert.contents(elem); + switch (elem.identifier.tag) { + .utc_time => { + // Example: "YYMMDD000000Z" + if (bytes.len != 13) + return error.CertificateTimeInvalid; + if (bytes[12] != 'Z') + return error.CertificateTimeInvalid; + + return Date.toSeconds(.{ + .year = @as(u16, 2000) + try parseTimeDigits(bytes[0..2].*, 0, 99), + .month = try parseTimeDigits(bytes[2..4].*, 1, 12), + .day = try parseTimeDigits(bytes[4..6].*, 1, 31), + .hour = try parseTimeDigits(bytes[6..8].*, 0, 23), + .minute = try parseTimeDigits(bytes[8..10].*, 0, 59), + .second = try parseTimeDigits(bytes[10..12].*, 0, 59), + }); + }, + .generalized_time => { + // Examples: + // "19920521000000Z" + // "19920622123421Z" + // "19920722132100.3Z" + if (bytes.len < 15) + return error.CertificateTimeInvalid; + return Date.toSeconds(.{ + .year = try parseYear4(bytes[0..4]), + .month = try parseTimeDigits(bytes[4..6].*, 1, 12), + .day = try parseTimeDigits(bytes[6..8].*, 1, 31), + .hour = try parseTimeDigits(bytes[8..10].*, 0, 23), + .minute = try parseTimeDigits(bytes[10..12].*, 0, 59), + .second = try parseTimeDigits(bytes[12..14].*, 0, 59), + }); + }, + else => return error.CertificateFieldHasWrongDataType, + } +} + +const Date = struct { + /// example: 1999 + year: u16, + /// range: 1 to 12 + month: u8, + /// range: 1 to 31 + day: u8, + /// range: 0 to 59 + hour: u8, + /// range: 0 to 59 + minute: u8, + /// range: 0 to 59 + second: u8, + + /// Convert to number of seconds since epoch. + pub fn toSeconds(date: Date) u64 { + var sec: u64 = 0; + + { + var year: u16 = 1970; + while (year < date.year) : (year += 1) { + const days: u64 = std.time.epoch.getDaysInYear(year); + sec += days * std.time.epoch.secs_per_day; + } + } + + { + const is_leap = std.time.epoch.isLeapYear(date.year); + var month: u4 = 1; + while (month < date.month) : (month += 1) { + const days: u64 = std.time.epoch.getDaysInMonth( + @intToEnum(std.time.epoch.YearLeapKind, @boolToInt(is_leap)), + @intToEnum(std.time.epoch.Month, month), + ); + sec += days * std.time.epoch.secs_per_day; + } + } + + sec += (date.day - 1) * @as(u64, std.time.epoch.secs_per_day); + sec += date.hour * @as(u64, 60 * 60); + sec += date.minute * @as(u64, 60); + sec += date.second; + + return sec; + } +}; + +pub fn parseTimeDigits(nn: @Vector(2, u8), min: u8, max: u8) !u8 { + const zero: @Vector(2, u8) = .{ '0', '0' }; + const mm: @Vector(2, u8) = .{ 10, 1 }; + const result = @reduce(.Add, (nn -% zero) *% mm); + if (result < min) return error.CertificateTimeInvalid; + if (result > max) return error.CertificateTimeInvalid; + return result; +} + +test parseTimeDigits { + const expectEqual = std.testing.expectEqual; + try expectEqual(@as(u8, 0), try parseTimeDigits("00".*, 0, 99)); + try expectEqual(@as(u8, 99), try parseTimeDigits("99".*, 0, 99)); + try expectEqual(@as(u8, 42), try parseTimeDigits("42".*, 0, 99)); + + const expectError = std.testing.expectError; + try expectError(error.CertificateTimeInvalid, parseTimeDigits("13".*, 1, 12)); + try expectError(error.CertificateTimeInvalid, parseTimeDigits("00".*, 1, 12)); +} + +pub fn parseYear4(text: *const [4]u8) !u16 { + const nnnn: @Vector(4, u16) = .{ text[0], text[1], text[2], text[3] }; + const zero: @Vector(4, u16) = .{ '0', '0', '0', '0' }; + const mmmm: @Vector(4, u16) = .{ 1000, 100, 10, 1 }; + const result = @reduce(.Add, (nnnn -% zero) *% mmmm); + if (result > 9999) return error.CertificateTimeInvalid; + return result; +} + +test parseYear4 { + const expectEqual = std.testing.expectEqual; + try expectEqual(@as(u16, 0), try parseYear4("0000")); + try expectEqual(@as(u16, 9999), try parseYear4("9999")); + try expectEqual(@as(u16, 1988), try parseYear4("1988")); + + const expectError = std.testing.expectError; + try expectError(error.CertificateTimeInvalid, parseYear4("999b")); + try expectError(error.CertificateTimeInvalid, parseYear4("crap")); +} + pub fn parseAlgorithm(bytes: []const u8, element: der.Element) !Algorithm { if (element.identifier.tag != .object_identifier) return error.CertificateFieldHasWrongDataType; @@ -241,7 +387,13 @@ pub fn parseAttribute(bytes: []const u8, element: der.Element) !Attribute { return error.CertificateHasUnrecognizedAlgorithm; } -fn verifyRsa(comptime Hash: type, message: []const u8, sig: []const u8, pub_key_algo: AlgorithmCategory, pub_key: []const u8) !void { +fn verifyRsa( + comptime Hash: type, + message: []const u8, + sig: []const u8, + pub_key_algo: AlgorithmCategory, + pub_key: []const u8, +) !void { if (pub_key_algo != .rsaEncryption) return error.CertificateSignatureAlgorithmMismatch; const pub_key_seq = try der.parseElement(pub_key, 0); if (pub_key_seq.identifier.tag != .sequence) return error.CertificateFieldHasWrongDataType; @@ -328,6 +480,10 @@ const mem = std.mem; const der = std.crypto.der; const Certificate = @This(); +test { + _ = Bundle; +} + /// TODO: replace this with Frank's upcoming RSA implementation. the verify /// function won't have the possibility of failure - it will either identify a /// valid signature or an invalid signature. diff --git a/lib/std/crypto/Certificate/Bundle.zig b/lib/std/crypto/Certificate/Bundle.zig index c2c18552a7..68b2967d10 100644 --- a/lib/std/crypto/Certificate/Bundle.zig +++ b/lib/std/crypto/Certificate/Bundle.zig @@ -44,12 +44,20 @@ pub fn deinit(cb: *Bundle, gpa: Allocator) void { cb.* = undefined; } -/// Empties the set of certificates and then scans the host operating system +/// Clears the set of certificates and then scans the host operating system /// file system standard locations for certificates. +/// For operating systems that do not have standard CA installations to be +/// found, this function clears the set of certificates. pub fn rescan(cb: *Bundle, gpa: Allocator) !void { switch (builtin.os.tag) { .linux => return rescanLinux(cb, gpa), - else => @compileError("it is unknown where the root CA certificates live on this OS"), + .windows => { + // TODO + }, + .macos => { + // TODO + }, + else => {}, } } @@ -100,6 +108,8 @@ pub fn addCertsFromFile( const begin_marker = "-----BEGIN CERTIFICATE-----"; const end_marker = "-----END CERTIFICATE-----"; + const now_sec = std.time.timestamp(); + var start_index: usize = 0; while (mem.indexOfPos(u8, encoded_bytes, start_index, begin_marker)) |begin_marker_start| { const cert_start = begin_marker_start + begin_marker.len; @@ -110,8 +120,20 @@ pub fn addCertsFromFile( const decoded_start = @intCast(u32, cb.bytes.items.len); const dest_buf = cb.bytes.allocatedSlice()[decoded_start..]; cb.bytes.items.len += try base64.decode(dest_buf, encoded_cert); - const k = try cb.key(decoded_start); - const gop = try cb.map.getOrPutContext(gpa, k, .{ .cb = cb }); + // Even though we could only partially parse the certificate to find + // the subject name, we pre-parse all of them to make sure and only + // include in the bundle ones that we know will parse. This way we can + // use `catch unreachable` later. + const parsed_cert = try Certificate.parse(.{ + .buffer = cb.bytes.items, + .index = decoded_start, + }); + if (now_sec > parsed_cert.validity.not_after) { + // Ignore expired cert. + cb.bytes.items.len = decoded_start; + continue; + } + const gop = try cb.map.getOrPutContext(gpa, parsed_cert.subject_slice, .{ .cb = cb }); if (gop.found_existing) { cb.bytes.items.len = decoded_start; } else { @@ -120,21 +142,6 @@ pub fn addCertsFromFile( } } -pub fn key(cb: Bundle, bytes_index: u32) !der.Element.Slice { - const bytes = cb.bytes.items; - const certificate = try der.parseElement(bytes, bytes_index); - const tbs_certificate = try der.parseElement(bytes, certificate.slice.start); - const version = try der.parseElement(bytes, tbs_certificate.slice.start); - try Certificate.checkVersion(bytes, version); - const serial_number = try der.parseElement(bytes, version.slice.end); - const signature = try der.parseElement(bytes, serial_number.slice.end); - const issuer = try der.parseElement(bytes, signature.slice.end); - const validity = try der.parseElement(bytes, issuer.slice.end); - const subject = try der.parseElement(bytes, validity.slice.end); - - return subject.slice; -} - const builtin = @import("builtin"); const std = @import("../../std.zig"); const fs = std.fs; diff --git a/lib/std/crypto/der.zig b/lib/std/crypto/der.zig index 82f75421ea..27c8049758 100644 --- a/lib/std/crypto/der.zig +++ b/lib/std/crypto/der.zig @@ -24,6 +24,8 @@ pub const Tag = enum(u5) { object_identifier = 6, sequence = 16, sequence_of = 17, + utc_time = 23, + generalized_time = 24, _, }; |
