Commit 7cb535d4b5
Changed files (3)
lib
std
crypto
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;
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.
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,
_,
};