Zig 0.17.0-dev (Split by item)

Shortcut keys
?
Show these tips
S
Focus search field
Esc
Clear focus & close tips
U
Jump to source code
↑
Move up in search results
↓
Move down in search results
↵
Go to active search result
This is an example of documentation generated by ZigDoc, an alternative to Zig's built-in Auto Doc feature. See also examples in other modes/formats. The project being documented here (as the example) is the Zig standard library. Public Struct

Bundle

Public Struct

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 of the containing certificate within bytes.

Fields

map: std.HashMapUnmanaged(der.Element.Slice, u32, MapContext, std.hash_map.default_max_load_percentage)

The key is the contents slice of the subject.

bytes: std.ArrayList(u8)

Namespaces

Structs

Values

Functions

pub fn assert(ok: bool) void

Invokes detectable illegal behavior when ok is false.

In debug and safe modes, calls to this function are always generated, and the unreachable statement triggers a panic.

In fast and small modes, calls to this function are optimized away, and in fact the optimizer is able to use the assertion in its heuristics.

Inside a test block, it is best to use the testing module rather than this function, because this function may not detect a test failure in fast and small mode. Outside of a test block, this assert function is the correct function to use.

pub fn verify(cb: Bundle, subject: Certificate.Parsed, now_sec: i64) VerifyError!void
pub fn find(cb: Bundle, subject_name: []const u8) ?u32

The returned bytes become invalid after calling any of the rescan functions or add functions.

pub fn deinit(cb: *Bundle, gpa: Allocator) void
pub fn rescan(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp) RescanError!void

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 rescanMac(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp) RescanMacError!void
fn rescanLinux(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp) RescanLinuxError!void
fn rescanWithPath(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp, cert_file_path: []const u8) RescanWithPathError!void
fn rescanWindows(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp) RescanWindowsError!void
pub fn addCertsFromDirPath(cb: *Bundle, gpa: Allocator, io: Io, dir: Io.Dir, sub_dir_path: []const u8) AddCertsFromDirPathError!void
pub fn addCertsFromDirPathAbsolute(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp, abs_dir_path: []const u8) AddCertsFromDirPathError!void
pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp, iterable_dir: Io.Dir) AddCertsFromDirError!void
pub fn addCertsFromFilePathAbsolute(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp, abs_file_path: []const u8) AddCertsFromFilePathError!void
pub fn addCertsFromFilePath(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp, dir: Io.Dir, sub_file_path: []const u8) AddCertsFromFilePathError!void
pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file_reader: *Io.File.Reader, now_sec: i64) AddCertsFromFileError!void
pub fn parseCert(cb: *Bundle, gpa: Allocator, decoded_start: u32, now_sec: i64) ParseCertError!void

File

crypto/Certificate/Bundle.zig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
//! 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 of the containing
//! certificate within `bytes`.
const Bundle = @This();
const builtin = @import("builtin");

const std = @import("../../std.zig");
const Io = std.Io;
const Dir = std.Io.Dir;
const assert = std.debug.assert;
const mem = std.mem;
const crypto = std.crypto;
const Allocator = std.mem.Allocator;
const Certificate = std.crypto.Certificate;
const der = Certificate.der;

const base64 = std.base64.standard.decoderWithIgnore(" \t\r\n");

/// The key is the contents slice of the subject.
map: std.HashMapUnmanaged(der.Element.Slice, u32, MapContext, std.hash_map.default_max_load_percentage),
bytes: std.ArrayList(u8),

pub const empty: Bundle = .{ .map = .empty, .bytes = .empty };

pub const VerifyError = Certificate.Parsed.VerifyError || error{
    CertificateIssuerNotFound,
};

pub fn verify(cb: Bundle, subject: Certificate.Parsed, now_sec: i64) VerifyError!void {
    const bytes_index = cb.find(subject.issuer()) orelse return error.CertificateIssuerNotFound;
    const issuer_cert: Certificate = .{
        .buffer = cb.bytes.items,
        .index = bytes_index,
    };
    // Every certificate in the bundle is pre-parsed before adding it, ensuring
    // that parsing will succeed here.
    const issuer = issuer_cert.parse() catch unreachable;
    try subject.verify(issuer, now_sec);
}

/// The returned bytes become invalid after calling any of the rescan functions
/// or add functions.
pub fn find(cb: Bundle, subject_name: []const u8) ?u32 {
    const Adapter = struct {
        cb: Bundle,

        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: der.Element.Slice) bool {
            const b = ctx.cb.bytes.items[b_key.start..b_key.end];
            return mem.eql(u8, a, b);
        }
    };
    return cb.map.getAdapted(subject_name, Adapter{ .cb = cb });
}

pub fn deinit(cb: *Bundle, gpa: Allocator) void {
    cb.map.deinit(gpa);
    cb.bytes.deinit(gpa);
    cb.* = undefined;
}

pub const RescanError = RescanLinuxError || RescanMacError || RescanWithPathError || RescanWindowsError;

/// 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, io: Io, now: Io.Timestamp) RescanError!void {
    switch (builtin.os.tag) {
        .linux => return rescanLinux(cb, gpa, io, now),
        .maccatalyst, .macos => return rescanMac(cb, gpa, io, now),
        .freebsd, .openbsd => return rescanWithPath(cb, gpa, io, now, "/etc/ssl/cert.pem"),
        .netbsd => return rescanWithPath(cb, gpa, io, now, "/etc/openssl/certs/ca-certificates.crt"),
        .dragonfly => return rescanWithPath(cb, gpa, io, now, "/usr/local/etc/ssl/cert.pem"),
        .illumos => return rescanWithPath(cb, gpa, io, now, "/etc/ssl/cacert.pem"),
        .haiku => return rescanWithPath(cb, gpa, io, now, "/boot/system/data/ssl/CARootCertificates.pem"),
        // https://github.com/SerenityOS/serenity/blob/222acc9d389bc6b490d4c39539761b043a4bfcb0/Ports/ca-certificates/package.sh#L19
        .serenity => return rescanWithPath(cb, gpa, io, now, "/etc/ssl/certs/ca-certificates.crt"),
        .windows => return rescanWindows(cb, gpa, io, now),
        else => {},
    }
}

const rescanMac = @import("Bundle/macos.zig").rescanMac;
const RescanMacError = @import("Bundle/macos.zig").RescanMacError;

const RescanLinuxError = AddCertsFromFilePathError || AddCertsFromDirPathError;

fn rescanLinux(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp) RescanLinuxError!void {
    // Possible certificate files; stop after finding one.
    const cert_file_paths = [_][]const u8{
        "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc.
        "/etc/pki/tls/certs/ca-bundle.crt", // Fedora/RHEL 6
        "/etc/ssl/ca-bundle.pem", // OpenSUSE
        "/etc/pki/tls/cacert.pem", // OpenELEC
        "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // CentOS/RHEL 7
        "/etc/ssl/cert.pem", // Alpine Linux
    };

    // Possible directories with certificate files; all will be read.
    const cert_dir_paths = [_][]const u8{
        "/etc/ssl/certs", // SLES10/SLES11
        "/etc/pki/tls/certs", // Fedora/RHEL
        "/system/etc/security/cacerts", // Android
    };

    cb.bytes.clearRetainingCapacity();
    cb.map.clearRetainingCapacity();

    scan: {
        for (cert_file_paths) |cert_file_path| {
            if (addCertsFromFilePathAbsolute(cb, gpa, io, now, cert_file_path)) |_| {
                break :scan;
            } else |err| switch (err) {
                error.FileNotFound => continue,
                else => |e| return e,
            }
        }

        for (cert_dir_paths) |cert_dir_path| {
            addCertsFromDirPathAbsolute(cb, gpa, io, now, cert_dir_path) catch |err| switch (err) {
                error.FileNotFound => continue,
                else => |e| return e,
            };
        }
    }

    cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len);
}

const RescanWithPathError = AddCertsFromFilePathError;

fn rescanWithPath(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp, cert_file_path: []const u8) RescanWithPathError!void {
    cb.bytes.clearRetainingCapacity();
    cb.map.clearRetainingCapacity();
    try addCertsFromFilePathAbsolute(cb, gpa, io, now, cert_file_path);
    cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len);
}

const RescanWindowsError = Allocator.Error || ParseCertError || std.posix.UnexpectedError || error{FileNotFound};

fn rescanWindows(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp) RescanWindowsError!void {
    cb.bytes.clearRetainingCapacity();
    cb.map.clearRetainingCapacity();

    _ = io;

    const w = std.os.windows;
    const GetLastError = w.GetLastError;
    const root = [4:0]u16{ 'R', 'O', 'O', 'T' };
    const store = w.crypt32.CertOpenSystemStoreW(.NULL, &root) orelse switch (GetLastError()) {
        .FILE_NOT_FOUND => return error.FileNotFound,
        else => |err| return w.unexpectedError(err),
    };
    defer assert(w.crypt32.CertCloseStore(store, .{ .CHECK = std.debug.runtime_safety }).toBool());

    const now_sec = now.toSeconds();

    var ctx = w.crypt32.CertEnumCertificatesInStore(store, null);
    while (ctx) |context| : (ctx = w.crypt32.CertEnumCertificatesInStore(store, ctx)) {
        const decoded_start = @as(u32, @intCast(cb.bytes.items.len));
        const encoded_cert = context.pbCertEncoded[0..context.cbCertEncoded];
        try cb.bytes.appendSlice(gpa, encoded_cert);
        try cb.parseCert(gpa, decoded_start, now_sec);
    }
    cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len);
}

pub const AddCertsFromDirPathError = Io.File.OpenError || AddCertsFromDirError;

pub fn addCertsFromDirPath(
    cb: *Bundle,
    gpa: Allocator,
    io: Io,
    dir: Io.Dir,
    sub_dir_path: []const u8,
) AddCertsFromDirPathError!void {
    var iterable_dir = try dir.openDir(io, sub_dir_path, .{ .iterate = true });
    defer iterable_dir.close(io);
    const now = Io.Clock.real.now(io);
    return addCertsFromDir(cb, gpa, io, now, iterable_dir);
}

pub fn addCertsFromDirPathAbsolute(
    cb: *Bundle,
    gpa: Allocator,
    io: Io,
    now: Io.Timestamp,
    abs_dir_path: []const u8,
) AddCertsFromDirPathError!void {
    assert(Dir.path.isAbsolute(abs_dir_path));
    var iterable_dir = try Dir.openDirAbsolute(io, abs_dir_path, .{ .iterate = true });
    defer iterable_dir.close(io);
    return addCertsFromDir(cb, gpa, io, now, iterable_dir);
}

pub const AddCertsFromDirError = AddCertsFromFilePathError;

pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp, iterable_dir: Io.Dir) AddCertsFromDirError!void {
    var it = iterable_dir.iterate();
    while (try it.next(io)) |entry| {
        switch (entry.kind) {
            .file, .sym_link => {},
            else => continue,
        }

        try addCertsFromFilePath(cb, gpa, io, now, iterable_dir, entry.name);
    }
}

pub const AddCertsFromFilePathError = Io.File.OpenError || AddCertsFromFileError;

pub fn addCertsFromFilePathAbsolute(
    cb: *Bundle,
    gpa: Allocator,
    io: Io,
    now: Io.Timestamp,
    abs_file_path: []const u8,
) AddCertsFromFilePathError!void {
    var file = try Io.Dir.openFileAbsolute(io, abs_file_path, .{});
    defer file.close(io);
    var file_reader = file.reader(io, &.{});
    return addCertsFromFile(cb, gpa, &file_reader, now.toSeconds());
}

pub fn addCertsFromFilePath(
    cb: *Bundle,
    gpa: Allocator,
    io: Io,
    now: Io.Timestamp,
    dir: Io.Dir,
    sub_file_path: []const u8,
) AddCertsFromFilePathError!void {
    var file = try dir.openFile(io, sub_file_path, .{});
    defer file.close(io);
    var file_reader = file.reader(io, &.{});
    return addCertsFromFile(cb, gpa, &file_reader, now.toSeconds());
}

pub const AddCertsFromFileError = Allocator.Error ||
    Io.File.Reader.Error ||
    Io.File.Reader.SizeError ||
    ParseCertError ||
    std.base64.Error ||
    error{ CertificateAuthorityBundleTooBig, MissingEndCertificateMarker, Streaming };

pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file_reader: *Io.File.Reader, now_sec: i64) AddCertsFromFileError!void {
    const size = try file_reader.getSize();

    // 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;
    const needed_capacity = std.math.cast(u32, decoded_size_upper_bound + size) orelse
        return error.CertificateAuthorityBundleTooBig;
    try cb.bytes.ensureUnusedCapacity(gpa, needed_capacity);
    const end_reserved: u32 = @intCast(cb.bytes.items.len + decoded_size_upper_bound);
    const buffer = cb.bytes.allocatedSlice()[end_reserved..];
    const end_index = file_reader.interface.readSliceShort(buffer) catch |err| switch (err) {
        error.ReadFailed => return file_reader.err.?,
    };
    const encoded_bytes = buffer[0..end_index];

    const begin_marker = "-----BEGIN CERTIFICATE-----";
    const end_marker = "-----END CERTIFICATE-----";

    var start_index: usize = 0;
    while (mem.findPos(u8, encoded_bytes, start_index, begin_marker)) |begin_marker_start| {
        const cert_start = begin_marker_start + begin_marker.len;
        const cert_end = mem.findPos(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: u32 = @intCast(cb.bytes.items.len);
        const dest_buf = cb.bytes.allocatedSlice()[decoded_start..];
        cb.bytes.items.len += try base64.decode(dest_buf, encoded_cert);
        try cb.parseCert(gpa, decoded_start, now_sec);
    }
}

pub const ParseCertError = Allocator.Error || Certificate.ParseError;

pub fn parseCert(cb: *Bundle, gpa: Allocator, decoded_start: u32, now_sec: i64) ParseCertError!void {
    // 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 = Certificate.parse(.{
        .buffer = cb.bytes.items,
        .index = decoded_start,
    }) catch |err| switch (err) {
        error.CertificateHasUnrecognizedObjectId => {
            cb.bytes.items.len = decoded_start;
            return;
        },
        else => |e| return e,
    };
    if (now_sec > parsed_cert.validity.not_after) {
        // Ignore expired cert.
        cb.bytes.items.len = decoded_start;
        return;
    }
    const gop = try cb.map.getOrPutContext(gpa, parsed_cert.subject_slice, .{ .cb = cb });
    if (gop.found_existing) {
        cb.bytes.items.len = decoded_start;
    } else {
        gop.value_ptr.* = decoded_start;
    }
}

const MapContext = struct {
    cb: *const Bundle,

    pub fn hash(ctx: MapContext, k: der.Element.Slice) u64 {
        return std.hash_map.hashString(ctx.cb.bytes.items[k.start..k.end]);
    }

    pub fn eql(ctx: MapContext, a: der.Element.Slice, b: der.Element.Slice) bool {
        const bytes = ctx.cb.bytes.items;
        return mem.eql(
            u8,
            bytes[a.start..a.end],
            bytes[b.start..b.end],
        );
    }
};

test "addCertsFromDirPath compiles and accepts an empty directory" {
    const io = std.testing.io;
    const gpa = std.testing.allocator;

    var tmp = std.testing.tmpDir(.{ .iterate = true });
    defer tmp.cleanup();

    var bundle: Bundle = .empty;
    defer bundle.deinit(gpa);

    try bundle.addCertsFromDirPath(gpa, io, tmp.dir, ".");
}

test "scan for OS-provided certificates" {
    if (builtin.os.tag == .wasi) return error.SkipZigTest;

    const io = std.testing.io;
    const gpa = std.testing.allocator;

    var bundle: Bundle = .empty;
    defer bundle.deinit(gpa);

    const now = Io.Clock.real.now(io);

    try bundle.rescan(gpa, io, now);
}