-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Amortize std.crypto.Certificate.Bundle
rescanning in std.http.Client
#25264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 |
---|---|---|
|
@@ -5,15 +5,58 @@ | |
//! index from the DER-encoded subject name to the index of the containing | ||
//! certificate within `bytes`. | ||
|
||
source: Source, | ||
|
||
/// The key is the contents slice of the subject. | ||
map: std.HashMapUnmanaged(der.Element.Slice, u32, MapContext, std.hash_map.default_max_load_percentage) = .empty, | ||
bytes: std.ArrayListUnmanaged(u8) = .empty, | ||
|
||
pub const default: Bundle = .{ | ||
.source = .system, | ||
}; | ||
|
||
/// No ca verification is performed, which prevents a trusted connection from | ||
/// being established. | ||
pub const no_verification = .{ | ||
.source = .{ .callback = &noVerification }, | ||
}; | ||
|
||
fn noVerification(_: *Bundle, _: Certificate.Parsed, _: i64) VerifyError!void {} | ||
|
||
/// Verify that the server certificate is a valid self-signed certificate. This | ||
/// provides no authorization guarantees, as anyone can create a self-signed | ||
/// certificate. | ||
pub const self_signed = .{ | ||
.source = .{ .callback = &selfSigned }, | ||
}; | ||
|
||
fn selfSigned(_: *Bundle, subject: Certificate.Parsed, now_sec: i64) VerifyError!void { | ||
try subject.verify(subject, now_sec); | ||
} | ||
|
||
pub const Source = union(enum) { | ||
system, | ||
file: []const u8, | ||
bytes: []const u8, | ||
callback: *const fn (cb: *Bundle, subject: Certificate.Parsed, now_sec: i64) VerifyError!void, | ||
}; | ||
|
||
pub fn init(allocator: Allocator, source: Source) !Bundle { | ||
var bundle: Bundle = .{ | ||
.source = source, | ||
}; | ||
try bundle.rescan(allocator); | ||
return bundle; | ||
} | ||
|
||
pub const VerifyError = Certificate.Parsed.VerifyError || error{ | ||
CertificateIssuerNotFound, | ||
}; | ||
|
||
pub fn verify(cb: Bundle, subject: Certificate.Parsed, now_sec: i64) VerifyError!void { | ||
pub fn verify(cb: *Bundle, subject: Certificate.Parsed, now_sec: i64) VerifyError!void { | ||
if (cb.source == .callback) | ||
return cb.source.callback(cb, subject, now_sec); | ||
|
||
const bytes_index = cb.find(subject.issuer()) orelse return error.CertificateIssuerNotFound; | ||
const issuer_cert: Certificate = .{ | ||
.buffer = cb.bytes.items, | ||
|
@@ -25,6 +68,14 @@ pub fn verify(cb: Bundle, subject: Certificate.Parsed, now_sec: i64) VerifyError | |
try subject.verify(issuer, now_sec); | ||
} | ||
|
||
pub const VerifyRescanError = VerifyError || RescanError; | ||
|
||
pub fn verifyRescan(cb: *Bundle, allocator: Allocator, subject: Certificate.Parsed, now_sec: i64) VerifyRescanError!void { | ||
_ = cb.find(subject.issuer()) orelse try cb.rescan(allocator); | ||
|
||
return cb.verify(subject, 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 { | ||
|
@@ -57,18 +108,23 @@ pub const RescanError = RescanLinuxError || RescanMacError || RescanWithPathErro | |
/// 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) RescanError!void { | ||
switch (builtin.os.tag) { | ||
.linux => return rescanLinux(cb, gpa), | ||
.macos => return rescanMac(cb, gpa), | ||
.freebsd, .openbsd => return rescanWithPath(cb, gpa, "/etc/ssl/cert.pem"), | ||
.netbsd => return rescanWithPath(cb, gpa, "/etc/openssl/certs/ca-certificates.crt"), | ||
.dragonfly => return rescanWithPath(cb, gpa, "/usr/local/etc/ssl/cert.pem"), | ||
.solaris, .illumos => return rescanWithPath(cb, gpa, "/etc/ssl/cacert.pem"), | ||
.haiku => return rescanWithPath(cb, gpa, "/boot/system/data/ssl/CARootCertificates.pem"), | ||
// https://github.com/SerenityOS/serenity/blob/222acc9d389bc6b490d4c39539761b043a4bfcb0/Ports/ca-certificates/package.sh#L19 | ||
.serenity => return rescanWithPath(cb, gpa, "/etc/ssl/certs/ca-certificates.crt"), | ||
.windows => return rescanWindows(cb, gpa), | ||
else => {}, | ||
switch (cb.source) { | ||
.system => switch (builtin.os.tag) { | ||
.linux => return rescanLinux(cb, gpa), | ||
.macos => return rescanMac(cb, gpa), | ||
.freebsd, .openbsd => return rescanWithPath(cb, gpa, "/etc/ssl/cert.pem"), | ||
.netbsd => return rescanWithPath(cb, gpa, "/etc/openssl/certs/ca-certificates.crt"), | ||
.dragonfly => return rescanWithPath(cb, gpa, "/usr/local/etc/ssl/cert.pem"), | ||
.solaris, .illumos => return rescanWithPath(cb, gpa, "/etc/ssl/cacert.pem"), | ||
.haiku => return rescanWithPath(cb, gpa, "/boot/system/data/ssl/CARootCertificates.pem"), | ||
// https://github.com/SerenityOS/serenity/blob/222acc9d389bc6b490d4c39539761b043a4bfcb0/Ports/ca-certificates/package.sh#L19 | ||
.serenity => return rescanWithPath(cb, gpa, "/etc/ssl/certs/ca-certificates.crt"), | ||
.windows => return rescanWindows(cb, gpa), | ||
else => {}, | ||
}, | ||
.file => |path| return rescanWithPath(cb, gpa, path), | ||
.bytes => |buffer| return rescanWithBytes(cb, gpa, buffer), | ||
.callback => {}, | ||
} | ||
} | ||
|
||
|
@@ -155,6 +211,13 @@ fn rescanWindows(cb: *Bundle, gpa: Allocator) RescanWindowsError!void { | |
cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len); | ||
} | ||
|
||
fn rescanWithBytes(cb: *Bundle, gpa: Allocator, buffer: []const u8) AddCertsFromBytesError!void { | ||
cb.bytes.clearRetainingCapacity(); | ||
cb.map.clearRetainingCapacity(); | ||
try addCertsFromBytes(cb, gpa, buffer); | ||
cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len); | ||
} | ||
|
||
pub const AddCertsFromDirPathError = fs.File.OpenError || AddCertsFromDirError; | ||
|
||
pub fn addCertsFromDirPath( | ||
|
@@ -220,30 +283,45 @@ pub fn addCertsFromFilePath( | |
pub const AddCertsFromFileError = Allocator.Error || | ||
fs.File.GetSeekPosError || | ||
fs.File.ReadError || | ||
ParseCertError || | ||
std.base64.Error || | ||
error{ CertificateAuthorityBundleTooBig, MissingEndCertificateMarker }; | ||
AddCertsFromBytesError; | ||
|
||
pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file: fs.File) AddCertsFromFileError!void { | ||
const size = try file.getEndPos(); | ||
const size = std.math.cast(usize, try file.getEndPos()) orelse | ||
return error.CertificateAuthorityBundleTooBig; | ||
|
||
// 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; | ||
// We borrow `bytes` as a temporary buffer for both the decoded certificate | ||
// data and the encoded data from `file`. This is possible by placing the | ||
// file data after the decoded data filled by addCertsFromBytes. | ||
const decoded_size_upper_bound = try std.base64.standard.Decoder.calcSizeUpperBound(size); | ||
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 = try file.readAll(buffer); | ||
const encoded_bytes = buffer[0..end_index]; | ||
|
||
return cb.addCertsFromBytes(gpa, encoded_bytes); | ||
} | ||
|
||
pub const AddCertsFromBytesError = Allocator.Error || | ||
ParseCertError || | ||
std.base64.Error || | ||
error{ CertificateAuthorityBundleTooBig, MissingEndCertificateMarker }; | ||
|
||
pub fn addCertsFromBytes(cb: *Bundle, gpa: Allocator, encoded_bytes: []const u8) AddCertsFromBytesError!void { | ||
const begin_marker = "-----BEGIN CERTIFICATE-----"; | ||
const end_marker = "-----END CERTIFICATE-----"; | ||
|
||
const now_sec = std.time.timestamp(); | ||
|
||
// We borrow `bytes` as a temporary buffer for the decoded certificate data. | ||
const decoded_size_upper_bound = try std.base64.standard.Decoder.calcSizeUpperBound(encoded_bytes.len); | ||
const needed_capacity = std.math.cast(u32, decoded_size_upper_bound) orelse | ||
return error.CertificateAuthorityBundleTooBig; | ||
try cb.bytes.ensureUnusedCapacity(gpa, needed_capacity); | ||
|
||
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; | ||
|
@@ -318,11 +396,56 @@ const MapContext = struct { | |
} | ||
}; | ||
|
||
test "scan for OS-provided certificates" { | ||
if (builtin.os.tag == .wasi) return error.SkipZigTest; | ||
test "load certificate bundle" { | ||
// load from a file | ||
|
||
{ | ||
var tmp_dir = std.testing.tmpDir(.{}); | ||
defer tmp_dir.cleanup(); | ||
var dir = tmp_dir.dir; | ||
|
||
try dir.writeFile(.{ .sub_path = "cacert.pem", .data = cacert_pem }); | ||
const cacert_path = try dir.realpathAlloc(std.testing.allocator, "cacert.pem"); | ||
defer std.testing.allocator.free(cacert_path); | ||
|
||
var bundle: Bundle = try .init(std.testing.allocator, .{ | ||
.file = cacert_path, | ||
}); | ||
defer bundle.deinit(std.testing.allocator); | ||
} | ||
|
||
// load from a byte buffer | ||
|
||
{ | ||
var bundle: Bundle = try .init(std.testing.allocator, .{ | ||
.bytes = cacert_pem, | ||
}); | ||
defer bundle.deinit(std.testing.allocator); | ||
} | ||
|
||
// load from the system | ||
|
||
var bundle: Bundle = .{}; | ||
defer bundle.deinit(std.testing.allocator); | ||
{ | ||
if (builtin.os.tag == .wasi) return error.SkipZigTest; | ||
|
||
try bundle.rescan(std.testing.allocator); | ||
var bundle: Bundle = try .init(std.testing.allocator, .system); | ||
defer bundle.deinit(std.testing.allocator); | ||
} | ||
} | ||
|
||
// go run github.com/jsha/[email protected] -domains localhost | ||
const cacert_pem = | ||
\\-----BEGIN CERTIFICATE----- | ||
\\MIIB/DCCAYKgAwIBAgIIYcxXcUcpGuYwCgYIKoZIzj0EAwMwIDEeMBwGA1UEAxMV | ||
\\bWluaWNhIHJvb3QgY2EgNjFjYzU3MCAXDTI1MDkxNzAzNDAyNVoYDzIxMjUwOTE3 | ||
\\MDM0MDI1WjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSA2MWNjNTcwdjAQBgcq | ||
\\hkjOPQIBBgUrgQQAIgNiAARubFYDHoLNmM68GulcjVxxGxmqpNvosnDHpbBbU3wq | ||
\\pzwYN5FXK2QdSy3MBHvNfyu2VZVYiNGyaIWz66vOh0f6dVPLXlo1ghRvMwnaP+qy | ||
\\Xj8dWcedNoT2mxybVwxLOiKjgYYwgYMwDgYDVR0PAQH/BAQDAgKEMB0GA1UdJQQW | ||
\\MBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1Ud | ||
\\DgQWBBSmzZRaHagE1f+5ADaaNlpYLmyV/jAfBgNVHSMEGDAWgBSmzZRaHagE1f+5 | ||
\\ADaaNlpYLmyV/jAKBggqhkjOPQQDAwNoADBlAjEA9kXs6mZXpu1MQz0GKv0aHEdo | ||
\\swy3hE/7Y/UDy/bo71G3Qss1AjsS/flvfMNPIOecAjAx1wFTjdXS58CB02dTNXRv | ||
\\BPPkWAiU7avvE1RsEQU2fvudhnoiVa8PDs0TJODFiR4= | ||
\\-----END CERTIFICATE----- | ||
; |
This file contains hidden or 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 hidden or 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
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's two ways to go here: a) return the
VerifyError
orCertificateIssuerNotFound
for any rescan error, or b) add the rescan errors toVerifyError
's set. I'm not sure what's preferable.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
3rd option: add
error.CertificateBundleLoadFailure
toVerifyError
, and return that here instead ofCertificateIssuerNotFound
.