-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
introduce std.crypto.CertificateBundle
for reading root certificate authority bundles from standard installation locations on the file system. So far only Linux logic is added.
- Loading branch information
Showing
6 changed files
with
369 additions
and
173 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
//! A set of certificates. Typically pre-installed on every operating system, | ||
//! these are "Certificate Authorities" used to validate SSL certificates. | ||
//! This data structure stores certificates in DER-encoded form, all of them | ||
//! concatenated together in the `bytes` array. The `map` field contains an | ||
//! index from the DER-encoded subject name to the index within `bytes`. | ||
|
||
map: std.HashMapUnmanaged(Key, u32, MapContext, std.hash_map.default_max_load_percentage) = .{}, | ||
bytes: std.ArrayListUnmanaged(u8) = .{}, | ||
|
||
pub const Key = struct { | ||
subject_start: u32, | ||
subject_end: u32, | ||
}; | ||
|
||
/// The returned bytes become invalid after calling any of the rescan functions | ||
/// or add functions. | ||
pub fn find(cb: CertificateBundle, subject_name: []const u8) ?[]const u8 { | ||
const Adapter = struct { | ||
cb: CertificateBundle, | ||
|
||
pub fn hash(ctx: @This(), k: []const u8) u64 { | ||
_ = ctx; | ||
return std.hash_map.hashString(k); | ||
} | ||
|
||
pub fn eql(ctx: @This(), a: []const u8, b_key: Key) bool { | ||
const b = ctx.cb.bytes.items[b_key.subject_start..b_key.subject_end]; | ||
return mem.eql(u8, a, b); | ||
} | ||
}; | ||
const index = cb.map.getAdapted(subject_name, Adapter{ .cb = cb }) orelse return null; | ||
return cb.bytes.items[index..]; | ||
} | ||
|
||
pub fn deinit(cb: *CertificateBundle, gpa: Allocator) void { | ||
cb.map.deinit(gpa); | ||
cb.bytes.deinit(gpa); | ||
cb.* = undefined; | ||
} | ||
|
||
/// Empties the set of certificates and then scans the host operating system | ||
/// file system standard locations for certificates. | ||
pub fn rescan(cb: *CertificateBundle, 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"), | ||
} | ||
} | ||
|
||
pub fn rescanLinux(cb: *CertificateBundle, gpa: Allocator) !void { | ||
var dir = fs.openIterableDirAbsolute("/etc/ssl/certs", .{}) catch |err| switch (err) { | ||
error.FileNotFound => return, | ||
else => |e| return e, | ||
}; | ||
defer dir.close(); | ||
|
||
cb.bytes.clearRetainingCapacity(); | ||
cb.map.clearRetainingCapacity(); | ||
|
||
var it = dir.iterate(); | ||
while (try it.next()) |entry| { | ||
switch (entry.kind) { | ||
.File, .SymLink => {}, | ||
else => continue, | ||
} | ||
|
||
try addCertsFromFile(cb, gpa, dir.dir, entry.name); | ||
} | ||
|
||
cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len); | ||
} | ||
|
||
pub fn addCertsFromFile( | ||
cb: *CertificateBundle, | ||
gpa: Allocator, | ||
dir: fs.Dir, | ||
sub_file_path: []const u8, | ||
) !void { | ||
var file = try dir.openFile(sub_file_path, .{}); | ||
defer file.close(); | ||
|
||
const size = try file.getEndPos(); | ||
|
||
// We borrow `bytes` as a temporary buffer for the base64-encoded data. | ||
// This is possible by computing the decoded length and reserving the space | ||
// for the decoded bytes first. | ||
const decoded_size_upper_bound = size / 4 * 3; | ||
try cb.bytes.ensureUnusedCapacity(gpa, decoded_size_upper_bound + size); | ||
const end_reserved = cb.bytes.items.len + decoded_size_upper_bound; | ||
const buffer = cb.bytes.allocatedSlice()[end_reserved..]; | ||
const end_index = try file.readAll(buffer); | ||
const encoded_bytes = buffer[0..end_index]; | ||
|
||
const begin_marker = "-----BEGIN CERTIFICATE-----"; | ||
const end_marker = "-----END CERTIFICATE-----"; | ||
|
||
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; | ||
const cert_end = mem.indexOfPos(u8, encoded_bytes, cert_start, end_marker) orelse | ||
return error.MissingEndCertificateMarker; | ||
start_index = cert_end + end_marker.len; | ||
const encoded_cert = mem.trim(u8, encoded_bytes[cert_start..cert_end], " \t\r\n"); | ||
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 key(cb, decoded_start); | ||
try cb.map.putContext(gpa, k, decoded_start, .{ .cb = cb }); | ||
} | ||
} | ||
|
||
pub fn key(cb: *CertificateBundle, bytes_index: u32) !Key { | ||
const bytes = cb.bytes.items; | ||
const certificate = try Der.parseElement(bytes, bytes_index); | ||
const tbs_certificate = try Der.parseElement(bytes, certificate.start); | ||
const version = try Der.parseElement(bytes, tbs_certificate.start); | ||
if (@bitCast(u8, version.identifier) != 0xa0 or | ||
!mem.eql(u8, bytes[version.start..version.end], "\x02\x01\x02")) | ||
{ | ||
return error.UnsupportedCertificateVersion; | ||
} | ||
|
||
const serial_number = try Der.parseElement(bytes, version.end); | ||
|
||
// RFC 5280, section 4.1.2.3: | ||
// "This field MUST contain the same algorithm identifier as | ||
// the signatureAlgorithm field in the sequence Certificate." | ||
const signature = try Der.parseElement(bytes, serial_number.end); | ||
const issuer = try Der.parseElement(bytes, signature.end); | ||
const validity = try Der.parseElement(bytes, issuer.end); | ||
const subject = try Der.parseElement(bytes, validity.end); | ||
//const subject_pub_key = try Der.parseElement(bytes, subject.end); | ||
//const extensions = try Der.parseElement(bytes, subject_pub_key.end); | ||
|
||
return .{ | ||
.subject_start = subject.start, | ||
.subject_end = subject.end, | ||
}; | ||
} | ||
|
||
const builtin = @import("builtin"); | ||
const std = @import("../std.zig"); | ||
const fs = std.fs; | ||
const mem = std.mem; | ||
const Allocator = std.mem.Allocator; | ||
const Der = std.crypto.Der; | ||
const CertificateBundle = @This(); | ||
|
||
const base64 = std.base64.standard.decoderWithIgnore(" \t\r\n"); | ||
|
||
const MapContext = struct { | ||
cb: *const CertificateBundle, | ||
|
||
pub fn hash(ctx: MapContext, k: Key) u64 { | ||
return std.hash_map.hashString(ctx.cb.bytes.items[k.subject_start..k.subject_end]); | ||
} | ||
|
||
pub fn eql(ctx: MapContext, a: Key, b: Key) bool { | ||
const bytes = ctx.cb.bytes.items; | ||
return mem.eql( | ||
u8, | ||
bytes[a.subject_start..a.subject_end], | ||
bytes[b.subject_start..b.subject_end], | ||
); | ||
} | ||
}; | ||
|
||
test { | ||
var bundle: CertificateBundle = .{}; | ||
defer bundle.deinit(std.testing.allocator); | ||
|
||
try bundle.rescan(std.testing.allocator); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
pub const Class = enum(u2) { | ||
universal, | ||
application, | ||
context_specific, | ||
private, | ||
}; | ||
|
||
pub const PC = enum(u1) { | ||
primitive, | ||
constructed, | ||
}; | ||
|
||
pub const Identifier = packed struct(u8) { | ||
tag: Tag, | ||
pc: PC, | ||
class: Class, | ||
}; | ||
|
||
pub const Tag = enum(u5) { | ||
boolean = 1, | ||
integer = 2, | ||
bitstring = 3, | ||
null = 5, | ||
object_identifier = 6, | ||
sequence = 16, | ||
sequence_of = 17, | ||
_, | ||
}; | ||
|
||
pub const Oid = enum { | ||
rsadsi, | ||
pkcs, | ||
rsaEncryption, | ||
md2WithRSAEncryption, | ||
md5WithRSAEncryption, | ||
sha1WithRSAEncryption, | ||
sha256WithRSAEncryption, | ||
sha384WithRSAEncryption, | ||
sha512WithRSAEncryption, | ||
sha224WithRSAEncryption, | ||
pbeWithMD2AndDES_CBC, | ||
pbeWithMD5AndDES_CBC, | ||
pkcs9_emailAddress, | ||
md2, | ||
md5, | ||
rc4, | ||
ecdsa_with_Recommended, | ||
ecdsa_with_Specified, | ||
ecdsa_with_SHA224, | ||
ecdsa_with_SHA256, | ||
ecdsa_with_SHA384, | ||
ecdsa_with_SHA512, | ||
X500, | ||
X509, | ||
commonName, | ||
serialNumber, | ||
countryName, | ||
localityName, | ||
stateOrProvinceName, | ||
organizationName, | ||
organizationalUnitName, | ||
organizationIdentifier, | ||
|
||
pub const map = std.ComptimeStringMap(Oid, .{ | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D }, .rsadsi }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01 }, .pkcs }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01 }, .rsaEncryption }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x02 }, .md2WithRSAEncryption }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x04 }, .md5WithRSAEncryption }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x05 }, .sha1WithRSAEncryption }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B }, .sha256WithRSAEncryption }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0C }, .sha384WithRSAEncryption }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0D }, .sha512WithRSAEncryption }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0E }, .sha224WithRSAEncryption }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x05, 0x01 }, .pbeWithMD2AndDES_CBC }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x05, 0x03 }, .pbeWithMD5AndDES_CBC }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x09, 0x01 }, .pkcs9_emailAddress }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x02, 0x02 }, .md2 }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x02, 0x05 }, .md5 }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x03, 0x04 }, .rc4 }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x02 }, .ecdsa_with_Recommended }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03 }, .ecdsa_with_Specified }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x01 }, .ecdsa_with_SHA224 }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x02 }, .ecdsa_with_SHA256 }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x03 }, .ecdsa_with_SHA384 }, | ||
.{ &[_]u8{ 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x04 }, .ecdsa_with_SHA512 }, | ||
.{ &[_]u8{0x55}, .X500 }, | ||
.{ &[_]u8{ 0x55, 0x04 }, .X509 }, | ||
.{ &[_]u8{ 0x55, 0x04, 0x03 }, .commonName }, | ||
.{ &[_]u8{ 0x55, 0x04, 0x05 }, .serialNumber }, | ||
.{ &[_]u8{ 0x55, 0x04, 0x06 }, .countryName }, | ||
.{ &[_]u8{ 0x55, 0x04, 0x07 }, .localityName }, | ||
.{ &[_]u8{ 0x55, 0x04, 0x08 }, .stateOrProvinceName }, | ||
.{ &[_]u8{ 0x55, 0x04, 0x0A }, .organizationName }, | ||
.{ &[_]u8{ 0x55, 0x04, 0x0B }, .organizationalUnitName }, | ||
.{ &[_]u8{ 0x55, 0x04, 0x61 }, .organizationIdentifier }, | ||
}); | ||
}; | ||
|
||
pub const Element = struct { | ||
identifier: Identifier, | ||
start: u32, | ||
end: u32, | ||
}; | ||
|
||
pub const ParseElementError = error{CertificateHasFieldWithInvalidLength}; | ||
|
||
pub fn parseElement(bytes: []const u8, index: u32) ParseElementError!Element { | ||
var i = index; | ||
const identifier = @bitCast(Identifier, bytes[i]); | ||
i += 1; | ||
const size_byte = bytes[i]; | ||
i += 1; | ||
if ((size_byte >> 7) == 0) { | ||
return .{ | ||
.identifier = identifier, | ||
.start = i, | ||
.end = i + size_byte, | ||
}; | ||
} | ||
|
||
const len_size = @truncate(u7, size_byte); | ||
if (len_size > @sizeOf(u32)) { | ||
return error.CertificateHasFieldWithInvalidLength; | ||
} | ||
|
||
const end_i = i + len_size; | ||
var long_form_size: u32 = 0; | ||
while (i < end_i) : (i += 1) { | ||
long_form_size = (long_form_size << 8) | bytes[i]; | ||
} | ||
|
||
return .{ | ||
.identifier = identifier, | ||
.start = i, | ||
.end = i + long_form_size, | ||
}; | ||
} | ||
|
||
pub const ParseObjectIdError = error{ | ||
CertificateHasUnrecognizedObjectId, | ||
CertificateFieldHasWrongDataType, | ||
} || ParseElementError; | ||
|
||
pub fn parseObjectId(bytes: []const u8, element: Element) ParseObjectIdError!Oid { | ||
if (element.identifier.tag != .object_identifier) | ||
return error.CertificateFieldHasWrongDataType; | ||
return Oid.map.get(bytes[element.start..element.end]) orelse | ||
return error.CertificateHasUnrecognizedObjectId; | ||
} | ||
|
||
const std = @import("../std.zig"); | ||
const Der = @This(); |
Oops, something went wrong.