std.crypto.tls: improve debuggability of encrypted connections

By default, programs built in debug mode that open a https connection
will append secrets to the file specified in the SSLKEYLOGFILE
environment variable to allow protocol debugging by external programs.
This commit is contained in:
Jacob Young 2024-11-05 01:37:12 -05:00
parent d86a8aedd5
commit de53e6e4f2
3 changed files with 174 additions and 31 deletions

View File

@ -33,7 +33,7 @@ received_close_notify: bool,
/// This makes the application vulnerable to truncation attacks unless the
/// application layer itself verifies that the amount of data received equals
/// the amount of data expected, such as HTTP with the Content-Length header.
allow_truncation_attacks: bool = false,
allow_truncation_attacks: bool,
application_cipher: tls.ApplicationCipher,
/// The size is enough to contain exactly one TLSCiphertext record.
/// This buffer is segmented into four parts:
@ -44,6 +44,24 @@ application_cipher: tls.ApplicationCipher,
/// The fields `partial_cleartext_idx`, `partial_ciphertext_idx`, and
/// `partial_ciphertext_end` describe the span of the segments.
partially_read_buffer: [tls.max_ciphertext_record_len]u8,
/// If non-null, ssl secrets are logged to a file. Creating such a log file allows other
/// programs with access to that file to decrypt all traffic over this connection.
ssl_key_log: ?struct {
client_key_seq: u64,
server_key_seq: u64,
client_random: [32]u8,
file: std.fs.File,
fn clientCounter(key_log: *@This()) u64 {
defer key_log.client_key_seq += 1;
return key_log.client_key_seq;
}
fn serverCounter(key_log: *@This()) u64 {
defer key_log.server_key_seq += 1;
return key_log.server_key_seq;
}
},
/// This is an example of the type that is needed by the read and write
/// functions. It can have any fields but it must at least have these
@ -88,6 +106,32 @@ pub const StreamInterface = struct {
}
};
pub const Options = struct {
/// How to perform host verification of server certificates.
host: union(enum) {
/// No host verification is performed, which prevents a trusted connection from
/// being established.
no_verification,
/// Verify that the server certificate was issues for a given host.
explicit: []const u8,
},
/// How to verify the authenticity of server certificates.
ca: union(enum) {
/// No ca verification is performed, which prevents a trusted connection from
/// being established.
no_verification,
/// Verify that the server certificate is a valid self-signed certificate.
/// This provides no authorization guarantees, as anyone can create a
/// self-signed certificate.
self_signed,
/// Verify that the server certificate is authorized by a given ca bundle.
bundle: Certificate.Bundle,
},
/// If non-null, ssl secrets are logged to this file. Creating such a log file allows
/// other programs with access to that file to decrypt all traffic over this connection.
ssl_key_log_file: ?std.fs.File = null,
};
pub fn InitError(comptime Stream: type) type {
return std.mem.Allocator.Error || Stream.WriteError || Stream.ReadError || tls.AlertDescription.Error || error{
InsufficientEntropy,
@ -140,12 +184,17 @@ pub fn InitError(comptime Stream: type) type {
/// must conform to `StreamInterface`.
///
/// `host` is only borrowed during this function call.
pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) InitError(@TypeOf(stream))!Client {
pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client {
const host = switch (options.host) {
.no_verification => "",
.explicit => |host| host,
};
const host_len: u16 = @intCast(host.len);
var random_buffer: [128]u8 = undefined;
crypto.random.bytes(&random_buffer);
const client_hello_rand = random_buffer[0..32].*;
var key_seq: u64 = 0;
var server_hello_rand: [32]u8 = undefined;
const legacy_session_id = random_buffer[32..64].*;
@ -179,15 +228,21 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) In
array(u16, u8, key_share.secp256r1_kp.public_key.toUncompressedSec1()) ++
int(u16, @intFromEnum(tls.NamedGroup.x25519)) ++
array(u16, u8, key_share.x25519_kp.public_key),
)) ++ int(u16, @intFromEnum(tls.ExtensionType.server_name)) ++
));
const server_name_extension = int(u16, @intFromEnum(tls.ExtensionType.server_name)) ++
int(u16, 2 + 1 + 2 + host_len) ++ // byte length of this extension payload
int(u16, 1 + 2 + host_len) ++ // server_name_list byte count
.{0x00} ++ // name_type
int(u16, host_len);
const server_name_extension_len = switch (options.host) {
.no_verification => 0,
.explicit => server_name_extension.len + host_len,
};
const extensions_header =
int(u16, @intCast(extensions_payload.len + host_len)) ++
extensions_payload;
int(u16, @intCast(extensions_payload.len + server_name_extension_len)) ++
extensions_payload ++
server_name_extension;
const client_hello =
int(u16, @intFromEnum(tls.ProtocolVersion.tls_1_2)) ++
@ -198,20 +253,24 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) In
extensions_header;
const out_handshake = .{@intFromEnum(tls.HandshakeType.client_hello)} ++
int(u24, @intCast(client_hello.len + host_len)) ++
int(u24, @intCast(client_hello.len - server_name_extension.len + server_name_extension_len)) ++
client_hello;
const cleartext_header = .{@intFromEnum(tls.ContentType.handshake)} ++
const cleartext_header_buf = .{@intFromEnum(tls.ContentType.handshake)} ++
int(u16, @intFromEnum(tls.ProtocolVersion.tls_1_0)) ++
int(u16, @intCast(out_handshake.len + host_len)) ++
int(u16, @intCast(out_handshake.len - server_name_extension.len + server_name_extension_len)) ++
out_handshake;
const cleartext_header = switch (options.host) {
.no_verification => cleartext_header_buf[0 .. cleartext_header_buf.len - server_name_extension.len],
.explicit => &cleartext_header_buf,
};
{
var iovecs = [_]std.posix.iovec_const{
.{ .base = &cleartext_header, .len = cleartext_header.len },
.{ .base = cleartext_header.ptr, .len = cleartext_header.len },
.{ .base = host.ptr, .len = host.len },
};
try stream.writevAll(&iovecs);
try stream.writevAll(iovecs[0..if (host.len == 0) 1 else 2]);
}
var tls_version: tls.ProtocolVersion = undefined;
@ -472,6 +531,12 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) In
pv.master_secret = P.Hkdf.extract(&ap_derived_secret, &zeroes);
const client_secret = hkdfExpandLabel(P.Hkdf, pv.handshake_secret, "c hs traffic", &hello_hash, P.Hash.digest_length);
const server_secret = hkdfExpandLabel(P.Hkdf, pv.handshake_secret, "s hs traffic", &hello_hash, P.Hash.digest_length);
if (options.ssl_key_log_file) |key_log_file| logSecrets(key_log_file, .{
.client_random = &client_hello_rand,
}, .{
.SERVER_HANDSHAKE_TRAFFIC_SECRET = &server_secret,
.CLIENT_HANDSHAKE_TRAFFIC_SECRET = &client_secret,
});
pv.client_finished_key = hkdfExpandLabel(P.Hkdf, client_secret, "finished", "", P.Hmac.key_length);
pv.server_finished_key = hkdfExpandLabel(P.Hkdf, server_secret, "finished", "", P.Hmac.key_length);
pv.client_handshake_key = hkdfExpandLabel(P.Hkdf, client_secret, "key", "", P.AEAD.key_length);
@ -544,6 +609,13 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) In
const cert_size = certs_decoder.decode(u24);
const certd = try certs_decoder.sub(cert_size);
if (tls_version == .tls_1_3) {
try certs_decoder.ensure(2);
const total_ext_size = certs_decoder.decode(u16);
const all_extd = try certs_decoder.sub(total_ext_size);
_ = all_extd;
}
const subject_cert: Certificate = .{
.buffer = certd.buf,
.index = @intCast(certd.idx),
@ -551,7 +623,10 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) In
const subject = try subject_cert.parse();
if (cert_index == 0) {
// Verify the host on the first certificate.
try subject.verifyHostName(host);
switch (options.host) {
.no_verification => {},
.explicit => try subject.verifyHostName(host),
}
// Keep track of the public key for the
// certificate_verify message later.
@ -560,23 +635,27 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) In
try prev_cert.verify(subject, now_sec);
}
if (ca_bundle.verify(subject, now_sec)) |_| {
handshake_state = .trust_chain_established;
break :cert;
} else |err| switch (err) {
error.CertificateIssuerNotFound => {},
else => |e| return e,
switch (options.ca) {
.no_verification => {
handshake_state = .trust_chain_established;
break :cert;
},
.self_signed => {
try subject.verify(subject, now_sec);
handshake_state = .trust_chain_established;
break :cert;
},
.bundle => |ca_bundle| if (ca_bundle.verify(subject, now_sec)) |_| {
handshake_state = .trust_chain_established;
break :cert;
} else |err| switch (err) {
error.CertificateIssuerNotFound => {},
else => |e| return e,
},
}
prev_cert = subject;
cert_index += 1;
if (tls_version == .tls_1_3) {
try certs_decoder.ensure(2);
const total_ext_size = certs_decoder.decode(u16);
const all_extd = try certs_decoder.sub(total_ext_size);
_ = all_extd;
}
}
},
.server_key_exchange => {
@ -625,6 +704,11 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) In
&client_hello_rand,
&server_hello_rand,
}, 48);
if (options.ssl_key_log_file) |key_log_file| logSecrets(key_log_file, .{
.client_random = &client_hello_rand,
}, .{
.CLIENT_RANDOM = &master_secret,
});
const key_block = hmacExpandLabel(
P.Hmac,
&master_secret,
@ -748,6 +832,14 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) In
const client_secret = hkdfExpandLabel(P.Hkdf, pv.master_secret, "c ap traffic", &handshake_hash, P.Hash.digest_length);
const server_secret = hkdfExpandLabel(P.Hkdf, pv.master_secret, "s ap traffic", &handshake_hash, P.Hash.digest_length);
if (options.ssl_key_log_file) |key_log_file| logSecrets(key_log_file, .{
.counter = key_seq,
.client_random = &client_hello_rand,
}, .{
.SERVER_TRAFFIC_SECRET = &server_secret,
.CLIENT_TRAFFIC_SECRET = &client_secret,
});
key_seq += 1;
break :app_cipher @unionInit(tls.ApplicationCipher, @tagName(tag), .{ .tls_1_3 = .{
.client_secret = client_secret,
.server_secret = server_secret,
@ -784,8 +876,15 @@ pub fn init(stream: anytype, ca_bundle: Certificate.Bundle, host: []const u8) In
.partial_ciphertext_idx = 0,
.partial_ciphertext_end = @intCast(leftover.len),
.received_close_notify = false,
.allow_truncation_attacks = false,
.application_cipher = app_cipher,
.partially_read_buffer = undefined,
.ssl_key_log = if (options.ssl_key_log_file) |key_log_file| .{
.client_key_seq = key_seq,
.server_key_seq = key_seq,
.client_random = client_hello_rand,
.file = key_log_file,
} else null,
};
@memcpy(client.partially_read_buffer[0..leftover.len], leftover);
return client;
@ -1358,6 +1457,12 @@ pub fn readvAdvanced(c: *Client, stream: anytype, iovecs: []const std.posix.iove
const pv = &p.tls_1_3;
const P = @TypeOf(p.*);
const server_secret = hkdfExpandLabel(P.Hkdf, pv.server_secret, "traffic upd", "", P.Hash.digest_length);
if (c.ssl_key_log) |*key_log| logSecrets(key_log.file, .{
.counter = key_log.serverCounter(),
.client_random = &key_log.client_random,
}, .{
.SERVER_TRAFFIC_SECRET = &server_secret,
});
pv.server_secret = server_secret;
pv.server_key = hkdfExpandLabel(P.Hkdf, server_secret, "key", "", P.AEAD.key_length);
pv.server_iv = hkdfExpandLabel(P.Hkdf, server_secret, "iv", "", P.AEAD.nonce_length);
@ -1372,6 +1477,12 @@ pub fn readvAdvanced(c: *Client, stream: anytype, iovecs: []const std.posix.iove
const pv = &p.tls_1_3;
const P = @TypeOf(p.*);
const client_secret = hkdfExpandLabel(P.Hkdf, pv.client_secret, "traffic upd", "", P.Hash.digest_length);
if (c.ssl_key_log) |*key_log| logSecrets(key_log.file, .{
.counter = key_log.clientCounter(),
.client_random = &key_log.client_random,
}, .{
.CLIENT_TRAFFIC_SECRET = &client_secret,
});
pv.client_secret = client_secret;
pv.client_key = hkdfExpandLabel(P.Hkdf, client_secret, "key", "", P.AEAD.key_length);
pv.client_iv = hkdfExpandLabel(P.Hkdf, client_secret, "iv", "", P.AEAD.nonce_length);
@ -1426,6 +1537,18 @@ pub fn readvAdvanced(c: *Client, stream: anytype, iovecs: []const std.posix.iove
}
}
fn logSecrets(key_log_file: std.fs.File, context: anytype, secrets: anytype) void {
const locked = if (key_log_file.lock(.exclusive)) |_| true else |_| false;
defer if (locked) key_log_file.unlock();
key_log_file.seekFromEnd(0) catch {};
inline for (@typeInfo(@TypeOf(secrets)).@"struct".fields) |field| key_log_file.writer().print("{s}" ++
(if (@hasField(@TypeOf(context), "counter")) "_{d}" else "") ++ " {} {}\n", .{field.name} ++
(if (@hasField(@TypeOf(context), "counter")) .{context.counter} else .{}) ++ .{
std.fmt.fmtSliceHexLower(context.client_random),
std.fmt.fmtSliceHexLower(@field(secrets, field.name)),
}) catch {};
}
fn finishRead(c: *Client, frag: []const u8, in: usize, out: usize) usize {
const saved_buf = frag[in..];
if (c.partial_ciphertext_idx > c.partial_cleartext_idx) {

View File

@ -388,6 +388,7 @@ pub const Connection = struct {
// try to cleanly close the TLS connection, for any server that cares.
_ = conn.tls_client.writeEnd(conn.stream, "", true) catch {};
if (conn.tls_client.ssl_key_log) |key_log| key_log.file.close();
allocator.destroy(conn.tls_client);
}
@ -566,7 +567,7 @@ pub const Response = struct {
.reason = undefined,
.version = undefined,
.keep_alive = false,
.parser = proto.HeadersParser.init(&header_buffer),
.parser = .init(&header_buffer),
};
@memcpy(header_buffer[0..response_bytes.len], response_bytes);
@ -610,7 +611,7 @@ pub const Response = struct {
}
pub fn iterateHeaders(r: Response) http.HeaderIterator {
return http.HeaderIterator.init(r.parser.get());
return .init(r.parser.get());
}
test iterateHeaders {
@ -628,7 +629,7 @@ pub const Response = struct {
.reason = undefined,
.version = undefined,
.keep_alive = false,
.parser = proto.HeadersParser.init(&header_buffer),
.parser = .init(&header_buffer),
};
@memcpy(header_buffer[0..response_bytes.len], response_bytes);
@ -771,7 +772,7 @@ pub const Request = struct {
req.client.connection_pool.release(req.client.allocator, req.connection.?);
req.connection = null;
var server_header = std.heap.FixedBufferAllocator.init(req.response.parser.header_bytes_buffer);
var server_header: std.heap.FixedBufferAllocator = .init(req.response.parser.header_bytes_buffer);
defer req.response.parser.header_bytes_buffer = server_header.buffer[server_header.end_index..];
const protocol, const valid_uri = try validateUri(uri, server_header.allocator());
@ -1354,7 +1355,21 @@ pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connec
conn.data.tls_client = try client.allocator.create(std.crypto.tls.Client);
errdefer client.allocator.destroy(conn.data.tls_client);
conn.data.tls_client.* = std.crypto.tls.Client.init(stream, client.ca_bundle, host) catch return error.TlsInitializationFailed;
const ssl_key_log_file: ?std.fs.File = if (std.options.http_enable_ssl_key_log_file) ssl_key_log_file: {
const ssl_key_log_path = std.process.getEnvVarOwned(client.allocator, "SSLKEYLOGFILE") catch |err| switch (err) {
error.EnvironmentVariableNotFound, error.InvalidWtf8 => break :ssl_key_log_file null,
error.OutOfMemory => return error.OutOfMemory,
};
defer client.allocator.free(ssl_key_log_path);
break :ssl_key_log_file std.fs.cwd().createFile(ssl_key_log_path, .{ .truncate = false }) catch null;
} else null;
errdefer if (ssl_key_log_file) |key_log_file| key_log_file.close();
conn.data.tls_client.* = std.crypto.tls.Client.init(stream, .{
.host = .{ .explicit = host },
.ca = .{ .bundle = client.ca_bundle },
.ssl_key_log_file = ssl_key_log_file,
}) catch return error.TlsInitializationFailed;
// This is appropriate for HTTPS because the HTTP headers contain
// the content length which is used to detect truncation attacks.
conn.data.tls_client.allow_truncation_attacks = true;
@ -1620,7 +1635,7 @@ pub fn open(
}
}
var server_header = std.heap.FixedBufferAllocator.init(options.server_header_buffer);
var server_header: std.heap.FixedBufferAllocator = .init(options.server_header_buffer);
const protocol, const valid_uri = try validateUri(uri, server_header.allocator());
if (protocol == .tls and @atomicLoad(bool, &client.next_https_rescan_certs, .acquire)) {
@ -1654,7 +1669,7 @@ pub fn open(
.status = undefined,
.reason = undefined,
.keep_alive = undefined,
.parser = proto.HeadersParser.init(server_header.buffer[server_header.end_index..]),
.parser = .init(server_header.buffer[server_header.end_index..]),
},
.headers = options.headers,
.extra_headers = options.extra_headers,

View File

@ -146,6 +146,11 @@ pub const Options = struct {
/// make a HTTPS connection.
http_disable_tls: bool = false,
/// This enables `std.http.Client` to log ssl secrets to the file specified by the SSLKEYLOGFILE
/// env var. Creating such a log file allows other programs with access to that file to decrypt
/// all `std.http.Client` traffic made by this program.
http_enable_ssl_key_log_file: bool = @import("builtin").mode == .Debug,
side_channels_mitigations: crypto.SideChannelsMitigations = crypto.default_side_channels_mitigations,
};