Skip to content

Commit 05257ac

Browse files
committed
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.
1 parent 955dbca commit 05257ac

File tree

3 files changed

+188
-23
lines changed

3 files changed

+188
-23
lines changed

lib/std/crypto/Certificate.zig

Lines changed: 160 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ pub const Parsed = struct {
7979
pub_key_algo: AlgorithmCategory,
8080
pub_key_slice: Slice,
8181
message_slice: Slice,
82+
validity: Validity,
83+
84+
pub const Validity = struct {
85+
not_before: u64,
86+
not_after: u64,
87+
};
8288

8389
pub const Slice = der.Element.Slice;
8490

@@ -110,15 +116,20 @@ pub const Parsed = struct {
110116
return p.slice(p.message_slice);
111117
}
112118

119+
/// This function checks the time validity for the subject only. Checking
120+
/// the issuer's time validity is out of scope.
113121
pub fn verify(parsed_subject: Parsed, parsed_issuer: Parsed) !void {
114122
// Check that the subject's issuer name matches the issuer's
115123
// subject name.
116124
if (!mem.eql(u8, parsed_subject.issuer(), parsed_issuer.subject())) {
117125
return error.CertificateIssuerMismatch;
118126
}
119127

120-
// TODO check the time validity for the subject
121-
// TODO check the time validity for the issuer
128+
const now_sec = std.time.timestamp();
129+
if (now_sec < parsed_subject.validity.not_before)
130+
return error.CertificateNotYetValid;
131+
if (now_sec > parsed_subject.validity.not_after)
132+
return error.CertificateExpired;
122133

123134
switch (parsed_subject.signature_algorithm) {
124135
inline .sha1WithRSAEncryption,
@@ -157,6 +168,10 @@ pub fn parse(cert: Certificate) !Parsed {
157168
const tbs_signature = try der.parseElement(cert_bytes, serial_number.slice.end);
158169
const issuer = try der.parseElement(cert_bytes, tbs_signature.slice.end);
159170
const validity = try der.parseElement(cert_bytes, issuer.slice.end);
171+
const not_before = try der.parseElement(cert_bytes, validity.slice.start);
172+
const not_before_utc = try parseTime(cert, not_before);
173+
const not_after = try der.parseElement(cert_bytes, not_before.slice.end);
174+
const not_after_utc = try parseTime(cert, not_after);
160175
const subject = try der.parseElement(cert_bytes, validity.slice.end);
161176

162177
const pub_key_info = try der.parseElement(cert_bytes, subject.slice.end);
@@ -198,6 +213,10 @@ pub fn parse(cert: Certificate) !Parsed {
198213
.message_slice = .{ .start = certificate.slice.start, .end = tbs_certificate.slice.end },
199214
.pub_key_algo = pub_key_algo,
200215
.pub_key_slice = pub_key,
216+
.validity = .{
217+
.not_before = not_before_utc,
218+
.not_after = not_after_utc,
219+
},
201220
};
202221
}
203222

@@ -208,7 +227,7 @@ pub fn verify(subject: Certificate, issuer: Certificate) !void {
208227
}
209228

210229
pub fn contents(cert: Certificate, elem: der.Element) []const u8 {
211-
return cert.buffer[elem.start..elem.end];
230+
return cert.buffer[elem.slice.start..elem.slice.end];
212231
}
213232

214233
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 {
217236
return .{ .start = elem.slice.start + 1, .end = elem.slice.end };
218237
}
219238

239+
/// Returns number of seconds since epoch.
240+
pub fn parseTime(cert: Certificate, elem: der.Element) !u64 {
241+
const bytes = cert.contents(elem);
242+
switch (elem.identifier.tag) {
243+
.utc_time => {
244+
// Example: "YYMMDD000000Z"
245+
if (bytes.len != 13)
246+
return error.CertificateTimeInvalid;
247+
if (bytes[12] != 'Z')
248+
return error.CertificateTimeInvalid;
249+
250+
return Date.toSeconds(.{
251+
.year = @as(u16, 2000) + try parseTimeDigits(bytes[0..2].*, 0, 99),
252+
.month = try parseTimeDigits(bytes[2..4].*, 1, 12),
253+
.day = try parseTimeDigits(bytes[4..6].*, 1, 31),
254+
.hour = try parseTimeDigits(bytes[6..8].*, 0, 23),
255+
.minute = try parseTimeDigits(bytes[8..10].*, 0, 59),
256+
.second = try parseTimeDigits(bytes[10..12].*, 0, 59),
257+
});
258+
},
259+
.generalized_time => {
260+
// Examples:
261+
// "19920521000000Z"
262+
// "19920622123421Z"
263+
// "19920722132100.3Z"
264+
if (bytes.len < 15)
265+
return error.CertificateTimeInvalid;
266+
return Date.toSeconds(.{
267+
.year = try parseYear4(bytes[0..4]),
268+
.month = try parseTimeDigits(bytes[4..6].*, 1, 12),
269+
.day = try parseTimeDigits(bytes[6..8].*, 1, 31),
270+
.hour = try parseTimeDigits(bytes[8..10].*, 0, 23),
271+
.minute = try parseTimeDigits(bytes[10..12].*, 0, 59),
272+
.second = try parseTimeDigits(bytes[12..14].*, 0, 59),
273+
});
274+
},
275+
else => return error.CertificateFieldHasWrongDataType,
276+
}
277+
}
278+
279+
const Date = struct {
280+
/// example: 1999
281+
year: u16,
282+
/// range: 1 to 12
283+
month: u8,
284+
/// range: 1 to 31
285+
day: u8,
286+
/// range: 0 to 59
287+
hour: u8,
288+
/// range: 0 to 59
289+
minute: u8,
290+
/// range: 0 to 59
291+
second: u8,
292+
293+
/// Convert to number of seconds since epoch.
294+
pub fn toSeconds(date: Date) u64 {
295+
var sec: u64 = 0;
296+
297+
{
298+
var year: u16 = 1970;
299+
while (year < date.year) : (year += 1) {
300+
const days: u64 = std.time.epoch.getDaysInYear(year);
301+
sec += days * std.time.epoch.secs_per_day;
302+
}
303+
}
304+
305+
{
306+
const is_leap = std.time.epoch.isLeapYear(date.year);
307+
var month: u4 = 1;
308+
while (month < date.month) : (month += 1) {
309+
const days: u64 = std.time.epoch.getDaysInMonth(
310+
@intToEnum(std.time.epoch.YearLeapKind, @boolToInt(is_leap)),
311+
@intToEnum(std.time.epoch.Month, month),
312+
);
313+
sec += days * std.time.epoch.secs_per_day;
314+
}
315+
}
316+
317+
sec += (date.day - 1) * @as(u64, std.time.epoch.secs_per_day);
318+
sec += date.hour * @as(u64, 60 * 60);
319+
sec += date.minute * @as(u64, 60);
320+
sec += date.second;
321+
322+
return sec;
323+
}
324+
};
325+
326+
pub fn parseTimeDigits(nn: @Vector(2, u8), min: u8, max: u8) !u8 {
327+
const zero: @Vector(2, u8) = .{ '0', '0' };
328+
const mm: @Vector(2, u8) = .{ 10, 1 };
329+
const result = @reduce(.Add, (nn -% zero) *% mm);
330+
if (result < min) return error.CertificateTimeInvalid;
331+
if (result > max) return error.CertificateTimeInvalid;
332+
return result;
333+
}
334+
335+
test parseTimeDigits {
336+
const expectEqual = std.testing.expectEqual;
337+
try expectEqual(@as(u8, 0), try parseTimeDigits("00".*, 0, 99));
338+
try expectEqual(@as(u8, 99), try parseTimeDigits("99".*, 0, 99));
339+
try expectEqual(@as(u8, 42), try parseTimeDigits("42".*, 0, 99));
340+
341+
const expectError = std.testing.expectError;
342+
try expectError(error.CertificateTimeInvalid, parseTimeDigits("13".*, 1, 12));
343+
try expectError(error.CertificateTimeInvalid, parseTimeDigits("00".*, 1, 12));
344+
}
345+
346+
pub fn parseYear4(text: *const [4]u8) !u16 {
347+
const nnnn: @Vector(4, u16) = .{ text[0], text[1], text[2], text[3] };
348+
const zero: @Vector(4, u16) = .{ '0', '0', '0', '0' };
349+
const mmmm: @Vector(4, u16) = .{ 1000, 100, 10, 1 };
350+
const result = @reduce(.Add, (nnnn -% zero) *% mmmm);
351+
if (result > 9999) return error.CertificateTimeInvalid;
352+
return result;
353+
}
354+
355+
test parseYear4 {
356+
const expectEqual = std.testing.expectEqual;
357+
try expectEqual(@as(u16, 0), try parseYear4("0000"));
358+
try expectEqual(@as(u16, 9999), try parseYear4("9999"));
359+
try expectEqual(@as(u16, 1988), try parseYear4("1988"));
360+
361+
const expectError = std.testing.expectError;
362+
try expectError(error.CertificateTimeInvalid, parseYear4("999b"));
363+
try expectError(error.CertificateTimeInvalid, parseYear4("crap"));
364+
}
365+
220366
pub fn parseAlgorithm(bytes: []const u8, element: der.Element) !Algorithm {
221367
if (element.identifier.tag != .object_identifier)
222368
return error.CertificateFieldHasWrongDataType;
@@ -241,7 +387,13 @@ pub fn parseAttribute(bytes: []const u8, element: der.Element) !Attribute {
241387
return error.CertificateHasUnrecognizedAlgorithm;
242388
}
243389

244-
fn verifyRsa(comptime Hash: type, message: []const u8, sig: []const u8, pub_key_algo: AlgorithmCategory, pub_key: []const u8) !void {
390+
fn verifyRsa(
391+
comptime Hash: type,
392+
message: []const u8,
393+
sig: []const u8,
394+
pub_key_algo: AlgorithmCategory,
395+
pub_key: []const u8,
396+
) !void {
245397
if (pub_key_algo != .rsaEncryption) return error.CertificateSignatureAlgorithmMismatch;
246398
const pub_key_seq = try der.parseElement(pub_key, 0);
247399
if (pub_key_seq.identifier.tag != .sequence) return error.CertificateFieldHasWrongDataType;
@@ -328,6 +480,10 @@ const mem = std.mem;
328480
const der = std.crypto.der;
329481
const Certificate = @This();
330482

483+
test {
484+
_ = Bundle;
485+
}
486+
331487
/// TODO: replace this with Frank's upcoming RSA implementation. the verify
332488
/// function won't have the possibility of failure - it will either identify a
333489
/// valid signature or an invalid signature.

lib/std/crypto/Certificate/Bundle.zig

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,20 @@ pub fn deinit(cb: *Bundle, gpa: Allocator) void {
4444
cb.* = undefined;
4545
}
4646

47-
/// Empties the set of certificates and then scans the host operating system
47+
/// Clears the set of certificates and then scans the host operating system
4848
/// file system standard locations for certificates.
49+
/// For operating systems that do not have standard CA installations to be
50+
/// found, this function clears the set of certificates.
4951
pub fn rescan(cb: *Bundle, gpa: Allocator) !void {
5052
switch (builtin.os.tag) {
5153
.linux => return rescanLinux(cb, gpa),
52-
else => @compileError("it is unknown where the root CA certificates live on this OS"),
54+
.windows => {
55+
// TODO
56+
},
57+
.macos => {
58+
// TODO
59+
},
60+
else => {},
5361
}
5462
}
5563

@@ -100,6 +108,8 @@ pub fn addCertsFromFile(
100108
const begin_marker = "-----BEGIN CERTIFICATE-----";
101109
const end_marker = "-----END CERTIFICATE-----";
102110

111+
const now_sec = std.time.timestamp();
112+
103113
var start_index: usize = 0;
104114
while (mem.indexOfPos(u8, encoded_bytes, start_index, begin_marker)) |begin_marker_start| {
105115
const cert_start = begin_marker_start + begin_marker.len;
@@ -110,8 +120,20 @@ pub fn addCertsFromFile(
110120
const decoded_start = @intCast(u32, cb.bytes.items.len);
111121
const dest_buf = cb.bytes.allocatedSlice()[decoded_start..];
112122
cb.bytes.items.len += try base64.decode(dest_buf, encoded_cert);
113-
const k = try cb.key(decoded_start);
114-
const gop = try cb.map.getOrPutContext(gpa, k, .{ .cb = cb });
123+
// Even though we could only partially parse the certificate to find
124+
// the subject name, we pre-parse all of them to make sure and only
125+
// include in the bundle ones that we know will parse. This way we can
126+
// use `catch unreachable` later.
127+
const parsed_cert = try Certificate.parse(.{
128+
.buffer = cb.bytes.items,
129+
.index = decoded_start,
130+
});
131+
if (now_sec > parsed_cert.validity.not_after) {
132+
// Ignore expired cert.
133+
cb.bytes.items.len = decoded_start;
134+
continue;
135+
}
136+
const gop = try cb.map.getOrPutContext(gpa, parsed_cert.subject_slice, .{ .cb = cb });
115137
if (gop.found_existing) {
116138
cb.bytes.items.len = decoded_start;
117139
} else {
@@ -120,21 +142,6 @@ pub fn addCertsFromFile(
120142
}
121143
}
122144

123-
pub fn key(cb: Bundle, bytes_index: u32) !der.Element.Slice {
124-
const bytes = cb.bytes.items;
125-
const certificate = try der.parseElement(bytes, bytes_index);
126-
const tbs_certificate = try der.parseElement(bytes, certificate.slice.start);
127-
const version = try der.parseElement(bytes, tbs_certificate.slice.start);
128-
try Certificate.checkVersion(bytes, version);
129-
const serial_number = try der.parseElement(bytes, version.slice.end);
130-
const signature = try der.parseElement(bytes, serial_number.slice.end);
131-
const issuer = try der.parseElement(bytes, signature.slice.end);
132-
const validity = try der.parseElement(bytes, issuer.slice.end);
133-
const subject = try der.parseElement(bytes, validity.slice.end);
134-
135-
return subject.slice;
136-
}
137-
138145
const builtin = @import("builtin");
139146
const std = @import("../../std.zig");
140147
const fs = std.fs;

lib/std/crypto/der.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ pub const Tag = enum(u5) {
2424
object_identifier = 6,
2525
sequence = 16,
2626
sequence_of = 17,
27+
utc_time = 23,
28+
generalized_time = 24,
2729
_,
2830
};
2931

0 commit comments

Comments
 (0)