Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/tls/handshake.zig
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ test "buildCertificateVerify produces valid structure" {
linux_platform.randomBytes(&transcript);

const kp = EcdsaP256.KeyPair.generate(std.testing.io);
const len = try buildCertificateVerify(&buf, transcript, kp.secret_key);
const len = try buildCertificateVerify(&buf, .server, transcript, kp.secret_key);

try std.testing.expectEqual(@as(u8, 0x0F), buf[0]);
try std.testing.expectEqual(@as(u8, 0x04), buf[4]);
Expand All @@ -286,8 +286,8 @@ test "buildCertificateVerify different transcripts produce different output" {
linux_platform.randomBytes(&t2);

const kp = EcdsaP256.KeyPair.generate(std.testing.io);
const len1 = try buildCertificateVerify(&buf1, t1, kp.secret_key);
const len2 = try buildCertificateVerify(&buf2, t2, kp.secret_key);
const len1 = try buildCertificateVerify(&buf1, .server, t1, kp.secret_key);
const len2 = try buildCertificateVerify(&buf2, .server, t2, kp.secret_key);

try std.testing.expect(!std.mem.eql(u8, buf1[0..len1], buf2[0..len2]));
}
Expand All @@ -300,7 +300,7 @@ test "buildCertificateVerify buffer too small" {
const kp = EcdsaP256.KeyPair.generate(std.testing.io);
try std.testing.expectError(
HandshakeError.BufferTooSmall,
buildCertificateVerify(&buf, transcript, kp.secret_key),
buildCertificateVerify(&buf, .server, transcript, kp.secret_key),
);
}

Expand Down
183 changes: 182 additions & 1 deletion src/tls/handshake/message_build.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,176 @@
const std = @import("std");
const common = @import("common.zig");

/// build a ClientHello compatible with what the existing server parses.
/// emits the AES-256-GCM cipher only, X25519 only, TLS 1.3 only, ALPN
/// (h2 + http/1.1), signature_algorithms (ecdsa_secp256r1_sha256), and
/// optionally an SNI extension. session_id is left empty (TLS 1.3 doesn't
/// resume via session IDs).
pub fn buildClientHello(
buf: []u8,
client_random: [32]u8,
client_x25519_pub: [32]u8,
server_name: ?[]const u8,
) common.HandshakeError!usize {
// count extension bytes up front so we can write the u16 length later.
var ext_len: usize = 0;
if (server_name) |sni| ext_len += 2 + 2 + 2 + 1 + 2 + sni.len; // server_name
ext_len += 2 + 2 + 1 + 2; // supported_versions: list_len + TLS 1.3
ext_len += 2 + 2 + 2 + 2; // supported_groups: list_len + X25519
ext_len += 2 + 2 + 2 + 2 + 2 + 32; // key_share: list_len + (group + len + 32 bytes)
ext_len += 2 + 2 + 2 + 2; // signature_algorithms: list_len + scheme
ext_len += 2 + 2 + 2 + (1 + 2) + (1 + 8); // alpn: list_len + ("h2" + "http/1.1")

// body = legacy_version(2) + random(32) + session_id_len(1) +
// cipher_suites(2 len + 2) + comp(1 len + 1) + extensions(2 len + ext_len)
const body_len: usize = 2 + 32 + 1 + 2 + 2 + 1 + 1 + 2 + ext_len;
const total = 4 + body_len;
if (buf.len < total) return common.HandshakeError.BufferTooSmall;

var pos: usize = 0;
buf[pos] = 0x01;
pos += 1;
common.writeU24(buf[pos..], @intCast(body_len));
pos += 3;

// legacy_version = TLS 1.2 (real version is in supported_versions)
buf[pos] = 0x03;
buf[pos + 1] = 0x03;
pos += 2;

@memcpy(buf[pos .. pos + 32], &client_random);
pos += 32;

// empty session_id
buf[pos] = 0;
pos += 1;

// cipher_suites: one entry, AES-256-GCM
common.writeU16(buf[pos..], 2);
pos += 2;
common.writeU16(buf[pos..], common.cipher_suite_aes_256_gcm);
pos += 2;

// legacy_compression_methods: [null]
buf[pos] = 1;
buf[pos + 1] = 0;
pos += 2;

// extensions length
common.writeU16(buf[pos..], @intCast(ext_len));
pos += 2;

if (server_name) |sni| {
common.writeU16(buf[pos..], 0x0000); // server_name
pos += 2;
common.writeU16(buf[pos..], @intCast(2 + 1 + 2 + sni.len));
pos += 2;
// ServerNameList length
common.writeU16(buf[pos..], @intCast(1 + 2 + sni.len));
pos += 2;
buf[pos] = 0; // name_type = host_name
pos += 1;
common.writeU16(buf[pos..], @intCast(sni.len));
pos += 2;
@memcpy(buf[pos .. pos + sni.len], sni);
pos += sni.len;
}

// supported_versions: list_len(1) + TLS 1.3
common.writeU16(buf[pos..], 0x002B);
pos += 2;
common.writeU16(buf[pos..], 1 + 2);
pos += 2;
buf[pos] = 2;
pos += 1;
common.writeU16(buf[pos..], 0x0304);
pos += 2;

// supported_groups: list_len(u16) + X25519
common.writeU16(buf[pos..], 0x000A);
pos += 2;
common.writeU16(buf[pos..], 2 + 2);
pos += 2;
common.writeU16(buf[pos..], 2);
pos += 2;
common.writeU16(buf[pos..], 0x001D);
pos += 2;

// key_share: list_len(u16) + (group + key_len + key)
common.writeU16(buf[pos..], 0x0033);
pos += 2;
common.writeU16(buf[pos..], 2 + 2 + 2 + 32);
pos += 2;
common.writeU16(buf[pos..], 2 + 2 + 32);
pos += 2;
common.writeU16(buf[pos..], 0x001D);
pos += 2;
common.writeU16(buf[pos..], 32);
pos += 2;
@memcpy(buf[pos .. pos + 32], &client_x25519_pub);
pos += 32;

// signature_algorithms: list_len(u16) + scheme
common.writeU16(buf[pos..], 0x000D);
pos += 2;
common.writeU16(buf[pos..], 2 + 2);
pos += 2;
common.writeU16(buf[pos..], 2);
pos += 2;
common.writeU16(buf[pos..], 0x0403); // ecdsa_secp256r1_sha256
pos += 2;

// ALPN: list_len(u16) + (proto_len(u8) + proto_bytes) for h2 and http/1.1
const alpn_inner_len: u16 = (1 + 2) + (1 + 8);
common.writeU16(buf[pos..], 0x0010);
pos += 2;
common.writeU16(buf[pos..], 2 + alpn_inner_len);
pos += 2;
common.writeU16(buf[pos..], alpn_inner_len);
pos += 2;
buf[pos] = 2;
pos += 1;
@memcpy(buf[pos .. pos + 2], "h2");
pos += 2;
buf[pos] = 8;
pos += 1;
@memcpy(buf[pos .. pos + 8], "http/1.1");
pos += 8;

return pos;
}

/// build a CertificateRequest advertising just ECDSA P-256 + SHA-256 in
/// the signature_algorithms extension. context is empty (TLS 1.3 default).
pub fn buildCertificateRequest(buf: []u8) common.HandshakeError!usize {
// body = context_len(1) + extensions_len(2) + signature_algorithms ext
// sig_algs ext = type(2) + len(2) + list_len(2) + scheme(2)
const sig_ext_len: usize = 2 + 2 + 2 + 2;
const body_len: usize = 1 + 2 + sig_ext_len;
const total = 4 + body_len;
if (buf.len < total) return common.HandshakeError.BufferTooSmall;

var pos: usize = 0;
buf[pos] = 0x0D;
pos += 1;
common.writeU24(buf[pos..], @intCast(body_len));
pos += 3;
buf[pos] = 0; // certificate_request_context = empty
pos += 1;
common.writeU16(buf[pos..], @intCast(sig_ext_len));
pos += 2;
common.writeU16(buf[pos..], 0x000D); // signature_algorithms
pos += 2;
common.writeU16(buf[pos..], 2 + 2); // ext_data_len
pos += 2;
common.writeU16(buf[pos..], 2); // SignatureSchemeList length
pos += 2;
common.writeU16(buf[pos..], 0x0403); // ecdsa_secp256r1_sha256
pos += 2;

return pos;
}

pub fn buildServerHello(
buf: []u8,
client_random: [32]u8,
Expand Down Expand Up @@ -117,16 +287,27 @@ pub fn buildCertificate(buf: []u8, cert_der: []const u8) common.HandshakeError!u
return pos;
}

/// which side of the TLS handshake is signing. picks the context string
/// the spec mandates for that side — the bytes differ and the bug is
/// silent (handshake completes but no other implementation accepts).
pub const CertVerifySide = enum { server, client };

/// build a CertificateVerify. the context string is selected from `side`;
/// the rest of the message is identical across sides.
pub fn buildCertificateVerify(
buf: []u8,
side: CertVerifySide,
transcript_hash: [common.hash_len]u8,
private_key: common.EcdsaP256.SecretKey,
) common.HandshakeError!usize {
var signed_content: [64 + 33 + 1 + common.hash_len]u8 = undefined;
defer std.crypto.secureZero(u8, &signed_content);

@memset(signed_content[0..64], 0x20);
const context = "TLS 1.3, server CertificateVerify";
const context = switch (side) {
.server => "TLS 1.3, server CertificateVerify",
.client => "TLS 1.3, client CertificateVerify",
};
@memcpy(signed_content[64 .. 64 + context.len], context);
signed_content[64 + context.len] = 0x00;
@memcpy(signed_content[64 + context.len + 1 ..], &transcript_hash);
Expand Down
54 changes: 54 additions & 0 deletions src/tls/handshake/message_parse.zig
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,60 @@ test "Truncated message returns error" {
try std.testing.expectError(ParseError.Truncated, nextMessage(&[_]u8{ 0x08, 0x00, 0x00, 0x05, 0x00 }, &pos2));
}

test "ClientHello from buildClientHello parses with the existing server parser" {
const client_hello = @import("client_hello.zig");
var buf: [512]u8 = undefined;
const random = [_]u8{0xAA} ** 32;
const pub_key = [_]u8{0x42} ** 32;
const len = try message_build.buildClientHello(&buf, random, pub_key, "billing.svc");

// parseClientHelloFields takes the body bytes (after the 4-byte header)
const info = try client_hello.parseClientHelloFields(buf[4..len]);
try std.testing.expectEqualSlices(u8, &random, &info.client_random);
try std.testing.expect(info.has_aes_256_gcm);
try std.testing.expect(info.supported_versions_has_tls13);
try std.testing.expect(info.x25519_key_share != null);
try std.testing.expectEqualSlices(u8, &pub_key, &info.x25519_key_share.?);
try std.testing.expect(info.offers_h2_alpn);
try std.testing.expect(info.offers_http11_alpn);
}

test "ClientHello without SNI still parses" {
const client_hello = @import("client_hello.zig");
var buf: [512]u8 = undefined;
const random = [_]u8{0x11} ** 32;
const pub_key = [_]u8{0x22} ** 32;
const len = try message_build.buildClientHello(&buf, random, pub_key, null);
const info = try client_hello.parseClientHelloFields(buf[4..len]);
try std.testing.expect(info.has_aes_256_gcm);
try std.testing.expect(info.supported_versions_has_tls13);
}

test "CertificateRequest from builder round-trips" {
var buf: [128]u8 = undefined;
const len = try message_build.buildCertificateRequest(&buf);
const got = try parseCertificateRequest(buf[4..len]);
try std.testing.expectEqual(@as(usize, 0), got.context.len);
try std.testing.expect(certRequestOffersEcdsaP256Sha256(got));
}

test "buildCertificateVerify picks the per-side context string" {
var server_buf: [256]u8 = undefined;
var client_buf: [256]u8 = undefined;
const transcript = [_]u8{0xCC} ** common.hash_len;
const kp = common.EcdsaP256.KeyPair.generate(std.testing.io);

const slen = try message_build.buildCertificateVerify(&server_buf, .server, transcript, kp.secret_key);
const clen = try message_build.buildCertificateVerify(&client_buf, .client, transcript, kp.secret_key);

// signatures differ because the signed prefix differs
try std.testing.expect(!std.mem.eql(u8, server_buf[0..slen], client_buf[0..clen]));

// structure is identical: same type, same algorithm code
try std.testing.expectEqual(@as(u8, 0x0F), server_buf[0]);
try std.testing.expectEqual(@as(u8, 0x0F), client_buf[0]);
}

test "CertificateRequest with ecdsa-sha256 offer parses and matches" {
// hand-build a CertificateRequest body: empty context, single
// signature_algorithms extension carrying just 0x0403.
Expand Down
2 changes: 1 addition & 1 deletion src/tls/proxy/session_runtime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ pub fn handleTlsSession(
const cv_transcript_hash = transcript.peek();

var cv_buf: [512]u8 = undefined;
const cv_len = handshake.buildCertificateVerify(&cv_buf, cv_transcript_hash, private_key) catch
const cv_len = handshake.buildCertificateVerify(&cv_buf, .server, cv_transcript_hash, private_key) catch
return error.HandshakeFailed;
transcript.update(cv_buf[0..cv_len]);
try sendEncryptedHandshake(client_fd, cv_buf[0..cv_len], server_hs_traffic, &server_seq);
Expand Down
Loading