Skip to content

Commit

Permalink
introduce std.crypto.CertificateBundle
Browse files Browse the repository at this point in the history
for reading root certificate authority bundles from standard
installation locations on the file system. So far only Linux logic is
added.
  • Loading branch information
andrewrk committed Dec 20, 2022
1 parent 3089041 commit 8664743
Show file tree
Hide file tree
Showing 6 changed files with 369 additions and 173 deletions.
5 changes: 5 additions & 0 deletions lib/std/crypto.zig
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ const std = @import("std.zig");
pub const errors = @import("crypto/errors.zig");

pub const tls = @import("crypto/tls.zig");
pub const Der = @import("crypto/Der.zig");
pub const CertificateBundle = @import("crypto/CertificateBundle.zig");

test {
_ = aead.aegis.Aegis128L;
Expand Down Expand Up @@ -266,6 +268,9 @@ test {
_ = utils;
_ = random;
_ = errors;
_ = tls;
_ = Der;
_ = CertificateBundle;
}

test "CSPRNG" {
Expand Down
173 changes: 173 additions & 0 deletions lib/std/crypto/CertificateBundle.zig
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);
}
153 changes: 153 additions & 0 deletions lib/std/crypto/Der.zig
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();
Loading

0 comments on commit 8664743

Please sign in to comment.