authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2022-12-22 19:56:43-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-01-02 16:57:15-07:00
log7cb535d4b54a4e5627edc6b558d1f31b41651328
tree98dbf7e28271b958ab12593fa3fff2fa3da0c56c
parent862ecf23442b7b399f07400c8997c6481f329853

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.

3 files changed, 188 insertions(+), 23 deletions(-)

lib/std/crypto/Certificate.zig+160-4
...@@ -79,6 +79,12 @@ pub const Parsed = struct {...@@ -79,6 +79,12 @@ pub const Parsed = struct {
79 pub_key_algo: AlgorithmCategory,79 pub_key_algo: AlgorithmCategory,
80 pub_key_slice: Slice,80 pub_key_slice: Slice,
81 message_slice: Slice,81 message_slice: Slice,
82 validity: Validity,
83
84 pub const Validity = struct {
85 not_before: u64,
86 not_after: u64,
87 };
8288
83 pub const Slice = der.Element.Slice;89 pub const Slice = der.Element.Slice;
8490
...@@ -110,6 +116,8 @@ pub const Parsed = struct {...@@ -110,6 +116,8 @@ pub const Parsed = struct {
110 return p.slice(p.message_slice);116 return p.slice(p.message_slice);
111 }117 }
112118
119 /// This function checks the time validity for the subject only. Checking
120 /// the issuer's time validity is out of scope.
113 pub fn verify(parsed_subject: Parsed, parsed_issuer: Parsed) !void {121 pub fn verify(parsed_subject: Parsed, parsed_issuer: Parsed) !void {
114 // Check that the subject's issuer name matches the issuer's122 // Check that the subject's issuer name matches the issuer's
115 // subject name.123 // subject name.
...@@ -117,8 +125,11 @@ pub const Parsed = struct {...@@ -117,8 +125,11 @@ pub const Parsed = struct {
117 return error.CertificateIssuerMismatch;125 return error.CertificateIssuerMismatch;
118 }126 }
119127
120 // TODO check the time validity for the subject128 const now_sec = std.time.timestamp();
121 // TODO check the time validity for the issuer129 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
123 switch (parsed_subject.signature_algorithm) {134 switch (parsed_subject.signature_algorithm) {
124 inline .sha1WithRSAEncryption,135 inline .sha1WithRSAEncryption,
...@@ -157,6 +168,10 @@ pub fn parse(cert: Certificate) !Parsed {...@@ -157,6 +168,10 @@ pub fn parse(cert: Certificate) !Parsed {
157 const tbs_signature = try der.parseElement(cert_bytes, serial_number.slice.end);168 const tbs_signature = try der.parseElement(cert_bytes, serial_number.slice.end);
158 const issuer = try der.parseElement(cert_bytes, tbs_signature.slice.end);169 const issuer = try der.parseElement(cert_bytes, tbs_signature.slice.end);
159 const validity = try der.parseElement(cert_bytes, issuer.slice.end);170 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);
160 const subject = try der.parseElement(cert_bytes, validity.slice.end);175 const subject = try der.parseElement(cert_bytes, validity.slice.end);
161176
162 const pub_key_info = try der.parseElement(cert_bytes, subject.slice.end);177 const pub_key_info = try der.parseElement(cert_bytes, subject.slice.end);
...@@ -198,6 +213,10 @@ pub fn parse(cert: Certificate) !Parsed {...@@ -198,6 +213,10 @@ pub fn parse(cert: Certificate) !Parsed {
198 .message_slice = .{ .start = certificate.slice.start, .end = tbs_certificate.slice.end },213 .message_slice = .{ .start = certificate.slice.start, .end = tbs_certificate.slice.end },
199 .pub_key_algo = pub_key_algo,214 .pub_key_algo = pub_key_algo,
200 .pub_key_slice = pub_key,215 .pub_key_slice = pub_key,
216 .validity = .{
217 .not_before = not_before_utc,
218 .not_after = not_after_utc,
219 },
201 };220 };
202}221}
203222
...@@ -208,7 +227,7 @@ pub fn verify(subject: Certificate, issuer: Certificate) !void {...@@ -208,7 +227,7 @@ pub fn verify(subject: Certificate, issuer: Certificate) !void {
208}227}
209228
210pub fn contents(cert: Certificate, elem: der.Element) []const u8 {229pub 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];
212}231}
213232
214pub fn parseBitString(cert: Certificate, elem: der.Element) !der.Element.Slice {233pub 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 {...@@ -217,6 +236,133 @@ pub fn parseBitString(cert: Certificate, elem: der.Element) !der.Element.Slice {
217 return .{ .start = elem.slice.start + 1, .end = elem.slice.end };236 return .{ .start = elem.slice.start + 1, .end = elem.slice.end };
218}237}
219238
239/// Returns number of seconds since epoch.
240pub 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
279const 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
326pub 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
335test 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
346pub 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
355test 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
220pub fn parseAlgorithm(bytes: []const u8, element: der.Element) !Algorithm {366pub fn parseAlgorithm(bytes: []const u8, element: der.Element) !Algorithm {
221 if (element.identifier.tag != .object_identifier)367 if (element.identifier.tag != .object_identifier)
222 return error.CertificateFieldHasWrongDataType;368 return error.CertificateFieldHasWrongDataType;
...@@ -241,7 +387,13 @@ pub fn parseAttribute(bytes: []const u8, element: der.Element) !Attribute {...@@ -241,7 +387,13 @@ pub fn parseAttribute(bytes: []const u8, element: der.Element) !Attribute {
241 return error.CertificateHasUnrecognizedAlgorithm;387 return error.CertificateHasUnrecognizedAlgorithm;
242}388}
243389
244fn verifyRsa(comptime Hash: type, message: []const u8, sig: []const u8, pub_key_algo: AlgorithmCategory, pub_key: []const u8) !void {390fn 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 {
245 if (pub_key_algo != .rsaEncryption) return error.CertificateSignatureAlgorithmMismatch;397 if (pub_key_algo != .rsaEncryption) return error.CertificateSignatureAlgorithmMismatch;
246 const pub_key_seq = try der.parseElement(pub_key, 0);398 const pub_key_seq = try der.parseElement(pub_key, 0);
247 if (pub_key_seq.identifier.tag != .sequence) return error.CertificateFieldHasWrongDataType;399 if (pub_key_seq.identifier.tag != .sequence) return error.CertificateFieldHasWrongDataType;
...@@ -328,6 +480,10 @@ const mem = std.mem;...@@ -328,6 +480,10 @@ const mem = std.mem;
328const der = std.crypto.der;480const der = std.crypto.der;
329const Certificate = @This();481const Certificate = @This();
330482
483test {
484 _ = Bundle;
485}
486
331/// TODO: replace this with Frank's upcoming RSA implementation. the verify487/// TODO: replace this with Frank's upcoming RSA implementation. the verify
332/// function won't have the possibility of failure - it will either identify a488/// function won't have the possibility of failure - it will either identify a
333/// valid signature or an invalid signature.489/// valid signature or an invalid signature.
lib/std/crypto/Certificate/Bundle.zig+26-19
...@@ -44,12 +44,20 @@ pub fn deinit(cb: *Bundle, gpa: Allocator) void {...@@ -44,12 +44,20 @@ pub fn deinit(cb: *Bundle, gpa: Allocator) void {
44 cb.* = undefined;44 cb.* = undefined;
45}45}
4646
47/// Empties the set of certificates and then scans the host operating system47/// Clears the set of certificates and then scans the host operating system
48/// file system standard locations for certificates.48/// 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.
49pub fn rescan(cb: *Bundle, gpa: Allocator) !void {51pub fn rescan(cb: *Bundle, gpa: Allocator) !void {
50 switch (builtin.os.tag) {52 switch (builtin.os.tag) {
51 .linux => return rescanLinux(cb, gpa),53 .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 => {},
53 }61 }
54}62}
5563
...@@ -100,6 +108,8 @@ pub fn addCertsFromFile(...@@ -100,6 +108,8 @@ pub fn addCertsFromFile(
100 const begin_marker = "-----BEGIN CERTIFICATE-----";108 const begin_marker = "-----BEGIN CERTIFICATE-----";
101 const end_marker = "-----END CERTIFICATE-----";109 const end_marker = "-----END CERTIFICATE-----";
102110
111 const now_sec = std.time.timestamp();
112
103 var start_index: usize = 0;113 var start_index: usize = 0;
104 while (mem.indexOfPos(u8, encoded_bytes, start_index, begin_marker)) |begin_marker_start| {114 while (mem.indexOfPos(u8, encoded_bytes, start_index, begin_marker)) |begin_marker_start| {
105 const cert_start = begin_marker_start + begin_marker.len;115 const cert_start = begin_marker_start + begin_marker.len;
...@@ -110,8 +120,20 @@ pub fn addCertsFromFile(...@@ -110,8 +120,20 @@ pub fn addCertsFromFile(
110 const decoded_start = @intCast(u32, cb.bytes.items.len);120 const decoded_start = @intCast(u32, cb.bytes.items.len);
111 const dest_buf = cb.bytes.allocatedSlice()[decoded_start..];121 const dest_buf = cb.bytes.allocatedSlice()[decoded_start..];
112 cb.bytes.items.len += try base64.decode(dest_buf, encoded_cert);122 cb.bytes.items.len += try base64.decode(dest_buf, encoded_cert);
113 const k = try cb.key(decoded_start);123 // Even though we could only partially parse the certificate to find
114 const gop = try cb.map.getOrPutContext(gpa, k, .{ .cb = cb });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 });
115 if (gop.found_existing) {137 if (gop.found_existing) {
116 cb.bytes.items.len = decoded_start;138 cb.bytes.items.len = decoded_start;
117 } else {139 } else {
...@@ -120,21 +142,6 @@ pub fn addCertsFromFile(...@@ -120,21 +142,6 @@ pub fn addCertsFromFile(
120 }142 }
121}143}
122144
123pub 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
138const builtin = @import("builtin");145const builtin = @import("builtin");
139const std = @import("../../std.zig");146const std = @import("../../std.zig");
140const fs = std.fs;147const fs = std.fs;
lib/std/crypto/der.zig+2
...@@ -24,6 +24,8 @@ pub const Tag = enum(u5) {...@@ -24,6 +24,8 @@ pub const Tag = enum(u5) {
24 object_identifier = 6,24 object_identifier = 6,
25 sequence = 16,25 sequence = 16,
26 sequence_of = 17,26 sequence_of = 17,
27 utc_time = 23,
28 generalized_time = 24,
27 _,29 _,
28};30};
2931