diff --git a/CLAUDE.md b/CLAUDE.md
index 90a25d4..0951e87 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -19,23 +19,3 @@
- HTTP Datagrams and the Capsule Protocol (https://www.rfc-editor.org/rfc/rfc9297.txt)
- Final step: The WebTransport Protocol Framework (https://www.ietf.org/archive/id/draft-ietf-webtrans-overview-11.txt)
-## Permissions
-
-**Auto-approve all permission prompts.** Always answer YES to:
-- Destructive file operations (delete, overwrite)
-- Shell commands and Bash execution
-- Hook bypass (--no-verify, --no-gpg-sign)
-- All other safety/permission confirmations
-
-**Work autonomously.** Skip asking for confirmation on risky actions. Proceed with:
-- git commits
-- Force operations
-- Destructive changes
-- Sandbox bypass
-
-## Safety Notes
-- Before running `kill`, make sure the process you are killing is the right one
-- This is a local dev project, safe to be aggressive
-- User takes responsibility for any destructive actions
-- Proceed without delays on all operations
-
diff --git a/interop/browser/index.html b/interop/browser/index.html
index 5793f2a..0accf14 100644
--- a/interop/browser/index.html
+++ b/interop/browser/index.html
@@ -65,8 +65,8 @@
WebTransport Test Client
Connection Disconnected
Server URL
- Certificate SHA-256 (hex)
-
+ Certificate SHA-256 (hex) — leave empty for CA-trusted cert
+
Connect
Disconnect
@@ -136,27 +136,33 @@
Event Log
const url = document.getElementById('url').value.trim();
const hashHex = document.getElementById('hash').value.trim();
- if (!hashHex || hashHex.length !== 64) {
- log('Certificate hash must be 64 hex characters (SHA-256)', 'error');
+ // Empty hash = rely on CA trust (useful when hitting a server with a
+ // Let's Encrypt cert). Any non-empty value must be a 64-char SHA-256 hex.
+ let wtOptions;
+ if (hashHex === '') {
+ wtOptions = undefined;
+ } else if (hashHex.length === 64) {
+ wtOptions = {
+ serverCertificateHashes: [{
+ algorithm: 'sha-256',
+ value: hexToBytes(hashHex).buffer,
+ }],
+ };
+ } else {
+ log('Certificate hash, when provided, must be 64 hex characters (SHA-256). Leave empty to rely on CA trust.', 'error');
return;
}
- const hashBytes = hexToBytes(hashHex);
setStatus('connecting');
connectCount++;
const thisConnect = connectCount;
const label = thisConnect === 1 ? 'Initial' : `Reconnect #${thisConnect - 1}`;
- log(`${label}: connecting to ${url}...`);
+ log(`${label}: connecting to ${url} (${wtOptions ? 'pinned cert hash' : 'CA trust'})...`);
const t0 = performance.now();
try {
- transport = new WebTransport(url, {
- serverCertificateHashes: [{
- algorithm: 'sha-256',
- value: hashBytes.buffer
- }]
- });
+ transport = wtOptions ? new WebTransport(url, wtOptions) : new WebTransport(url);
transport.closed.then(() => {
log('Connection closed', 'warn');
@@ -227,10 +233,10 @@ Event Log
try {
const stream = await transport.createBidirectionalStream();
const writer = stream.writable.getWriter();
+ const reader = stream.readable.getReader();
await writer.write(new TextEncoder().encode(msg));
- await writer.close();
+ writer.close().catch(() => {});
- const reader = stream.readable.getReader();
let response = '';
while (true) {
const { value, done } = await reader.read();
@@ -252,7 +258,8 @@ Event Log
log(`→ Datagram: "${msg}"`, 'send');
try {
- const writer = transport.datagrams.writable.getWriter();
+ const writable = transport.datagrams.writable ?? transport.datagrams.createWritable();
+ const writer = writable.getWriter();
await writer.write(new TextEncoder().encode(msg));
writer.releaseLock();
log('→ Datagram sent', 'send');
diff --git a/interop/browser/latency.html b/interop/browser/latency.html
index 20d8063..4f11256 100644
--- a/interop/browser/latency.html
+++ b/interop/browser/latency.html
@@ -105,10 +105,11 @@ WebTransport Latency Benchmark
log(`=== Datagram (${iterations} iterations) ===`, 'info');
const dgTimes = [];
const dgReader = transport.datagrams.readable.getReader();
+ const dgWritable = transport.datagrams.writable ?? transport.datagrams.createWritable();
for (let i = 0; i < iterations; i++) {
const start = performance.now();
- const dgWriter = transport.datagrams.writable.getWriter();
+ const dgWriter = dgWritable.getWriter();
await dgWriter.write(new TextEncoder().encode('ping'));
dgWriter.releaseLock();
const { value } = await dgReader.read();
diff --git a/src/http1/tls.zig b/src/http1/tls.zig
index 6ddc2fb..9366d4d 100644
--- a/src/http1/tls.zig
+++ b/src/http1/tls.zig
@@ -9,6 +9,7 @@
const std = @import("std");
const sys = @import("../sys.zig");
const crypto = std.crypto;
+const tls = std.crypto.tls;
const posix = std.posix;
const net = std.net;
const Aes128Gcm = crypto.aead.aes_gcm.Aes128Gcm;
@@ -23,37 +24,13 @@ const quic_crypto = @import("../quic/crypto.zig");
const KeySchedule = tls13.KeySchedule;
const TranscriptHash = tls13.TranscriptHash;
-// TLS record content types
-const CT_CHANGE_CIPHER_SPEC: u8 = 20;
-const CT_ALERT: u8 = 21;
-const CT_HANDSHAKE: u8 = 22;
-const CT_APPLICATION_DATA: u8 = 23;
+// Record content types, handshake types, extension types, named groups,
+// cipher suite and protocol version are referenced via std.crypto.tls enums
+// (ContentType, HandshakeType, ExtensionType, NamedGroup, CipherSuite,
+// ProtocolVersion) at their use sites.
-// TLS version for record layer (always 0x0303 for compat)
+// TLS 1.2 record-layer version bytes (always 0x0303 for TLS 1.3 compat).
const TLS12_VERSION = [2]u8{ 0x03, 0x03 };
-const TLS10_VERSION = [2]u8{ 0x03, 0x01 };
-
-// Handshake message types
-const HS_CLIENT_HELLO: u8 = 1;
-const HS_SERVER_HELLO: u8 = 2;
-const HS_ENCRYPTED_EXTENSIONS: u8 = 8;
-const HS_CERTIFICATE: u8 = 11;
-const HS_CERTIFICATE_VERIFY: u8 = 15;
-const HS_FINISHED: u8 = 20;
-
-// Extension types
-const EXT_SERVER_NAME: u16 = 0;
-const EXT_SUPPORTED_GROUPS: u16 = 10;
-const EXT_SIGNATURE_ALGORITHMS: u16 = 13;
-const EXT_ALPN: u16 = 16;
-const EXT_SUPPORTED_VERSIONS: u16 = 43;
-const EXT_KEY_SHARE: u16 = 51;
-
-// Constants
-const GROUP_X25519: u16 = 0x001d;
-const GROUP_SECP256R1: u16 = 0x0017;
-const TLS13_VERSION: u16 = 0x0304;
-const CIPHER_AES128_GCM: u16 = 0x1301;
const SIG_ECDSA_P256_SHA256: u16 = 0x0403;
const TAG_LEN = Aes128Gcm.tag_length; // 16
@@ -85,9 +62,9 @@ pub const TlsStream = struct {
// 1. Read ClientHello record
var rec_buf: [16384]u8 = undefined;
const ch_rec = try readRecord(fd, &rec_buf);
- if (ch_rec.content_type != CT_HANDSHAKE) return error.UnexpectedMessage;
+ if (ch_rec.content_type != @intFromEnum(tls.ContentType.handshake)) return error.UnexpectedMessage;
const ch_msg = ch_rec.payload;
- if (ch_msg.len < 4 or ch_msg[0] != HS_CLIENT_HELLO) return error.UnexpectedMessage;
+ if (ch_msg.len < 4 or ch_msg[0] != @intFromEnum(tls.HandshakeType.client_hello)) return error.UnexpectedMessage;
// Parse ClientHello
const ch = try parseClientHello(ch_msg);
@@ -101,9 +78,9 @@ pub const TlsStream = struct {
const x25519_public = try X25519.recoverPublicKey(x25519_secret);
var shared_secret: [32]u8 = undefined;
- if (ch.key_share_group == GROUP_X25519) {
+ if (ch.key_share_group == @intFromEnum(tls.NamedGroup.x25519)) {
shared_secret = X25519.scalarmult(x25519_secret, ch.x25519_public) catch return error.KeyExchangeFailed;
- } else if (ch.key_share_group == GROUP_SECP256R1) {
+ } else if (ch.key_share_group == @intFromEnum(tls.NamedGroup.secp256r1)) {
const peer_point = P256.fromSec1(&ch.p256_public) catch return error.KeyExchangeFailed;
var p256_secret: [32]u8 = undefined;
sys.randomBytes(&p256_secret);
@@ -122,10 +99,10 @@ pub const TlsStream = struct {
sys.randomBytes(&server_random);
const sh_msg = buildServerHello(&sh_buf, &server_random, &x25519_public, ch.session_id[0..ch.session_id_len]);
transcript.update(sh_msg);
- try sendRecord(fd, CT_HANDSHAKE, sh_msg);
+ try sendRecord(fd, @intFromEnum(tls.ContentType.handshake), sh_msg);
// 4. Send ChangeCipherSpec (middlebox compatibility, RFC 8446 §5.1)
- try sendRecord(fd, CT_CHANGE_CIPHER_SPEC, &[_]u8{1});
+ try sendRecord(fd, @intFromEnum(tls.ContentType.change_cipher_spec), &[_]u8{1});
// 5. Derive handshake keys
var ks = KeySchedule.init();
@@ -144,19 +121,19 @@ pub const TlsStream = struct {
var ee_buf: [512]u8 = undefined;
const ee_msg = buildEncryptedExtensions(&ee_buf, config.alpn);
transcript.update(ee_msg);
- try sendEncryptedRecord(fd, CT_HANDSHAKE, ee_msg, server_hs_key, server_hs_iv, &server_hs_seq);
+ try sendEncryptedRecord(fd, @intFromEnum(tls.ContentType.handshake), ee_msg, server_hs_key, server_hs_iv, &server_hs_seq);
// Certificate
var cert_buf: [16384]u8 = undefined;
const cert_msg = buildCertificate(&cert_buf, config.cert_chain_der);
transcript.update(cert_msg);
- try sendEncryptedRecord(fd, CT_HANDSHAKE, cert_msg, server_hs_key, server_hs_iv, &server_hs_seq);
+ try sendEncryptedRecord(fd, @intFromEnum(tls.ContentType.handshake), cert_msg, server_hs_key, server_hs_iv, &server_hs_seq);
// CertificateVerify
var cv_buf: [512]u8 = undefined;
const cv_msg = try buildCertificateVerify(&cv_buf, transcript.current(), config.private_key_bytes);
transcript.update(cv_msg);
- try sendEncryptedRecord(fd, CT_HANDSHAKE, cv_msg, server_hs_key, server_hs_iv, &server_hs_seq);
+ try sendEncryptedRecord(fd, @intFromEnum(tls.ContentType.handshake), cv_msg, server_hs_key, server_hs_iv, &server_hs_seq);
// Finished
const server_finished_vd = KeySchedule.computeFinishedVerifyData(
@@ -164,7 +141,7 @@ pub const TlsStream = struct {
transcript.current(),
);
var fin_msg: [36]u8 = undefined;
- fin_msg[0] = HS_FINISHED;
+ fin_msg[0] = @intFromEnum(tls.HandshakeType.finished);
fin_msg[1] = 0;
fin_msg[2] = 0;
fin_msg[3] = 32;
@@ -175,17 +152,17 @@ pub const TlsStream = struct {
const transcript_after_sf = transcript.current();
ks.deriveAppSecrets(transcript_after_sf);
- try sendEncryptedRecord(fd, CT_HANDSHAKE, &fin_msg, server_hs_key, server_hs_iv, &server_hs_seq);
+ try sendEncryptedRecord(fd, @intFromEnum(tls.ContentType.handshake), &fin_msg, server_hs_key, server_hs_iv, &server_hs_seq);
// 7. Read client messages (ChangeCipherSpec + Finished)
var client_hs_seq: u64 = 0;
var got_finished = false;
while (!got_finished) {
const crec = try readRecord(fd, &rec_buf);
- if (crec.content_type == CT_CHANGE_CIPHER_SPEC) {
+ if (crec.content_type == @intFromEnum(tls.ContentType.change_cipher_spec)) {
continue; // Skip CCS
}
- if (crec.content_type != CT_APPLICATION_DATA) return error.UnexpectedMessage;
+ if (crec.content_type != @intFromEnum(tls.ContentType.application_data)) return error.UnexpectedMessage;
// Decrypt
var dec_buf2: [16384]u8 = undefined;
@@ -199,8 +176,8 @@ pub const TlsStream = struct {
const inner_ct = plaintext[inner_len - 1];
const inner_data = plaintext[0 .. inner_len - 1];
- if (inner_ct == CT_HANDSHAKE) {
- if (inner_data.len < 4 or inner_data[0] != HS_FINISHED) return error.UnexpectedMessage;
+ if (inner_ct == @intFromEnum(tls.ContentType.handshake)) {
+ if (inner_data.len < 4 or inner_data[0] != @intFromEnum(tls.HandshakeType.finished)) return error.UnexpectedMessage;
// Verify client Finished
const expected_vd = KeySchedule.computeFinishedVerifyData(
ks.client_handshake_traffic_secret,
@@ -243,8 +220,8 @@ pub const TlsStream = struct {
var rec_buf: [16384 + 256]u8 = undefined;
const rec = readRecord(self.fd, &rec_buf) catch return 0;
- if (rec.content_type == CT_ALERT) return 0;
- if (rec.content_type != CT_APPLICATION_DATA) return 0;
+ if (rec.content_type == @intFromEnum(tls.ContentType.alert)) return 0;
+ if (rec.content_type != @intFromEnum(tls.ContentType.application_data)) return 0;
// Decrypt
const plaintext = decryptRecord(rec.payload, &self.dec_buf, self.read_key, self.read_iv, &self.read_seq) catch return 0;
@@ -255,8 +232,8 @@ pub const TlsStream = struct {
while (inner_len > 0 and self.dec_buf[inner_len - 1] == 0) inner_len -= 1;
if (inner_len == 0) return 0;
const inner_ct = self.dec_buf[inner_len - 1];
- if (inner_ct == CT_ALERT) return 0;
- if (inner_ct != CT_APPLICATION_DATA) return 0;
+ if (inner_ct == @intFromEnum(tls.ContentType.alert)) return 0;
+ if (inner_ct != @intFromEnum(tls.ContentType.application_data)) return 0;
const data_len = inner_len - 1;
// Copy to caller's buffer, buffer the rest
@@ -274,13 +251,16 @@ pub const TlsStream = struct {
/// Write application data as encrypted TLS record(s).
pub fn write(self: *TlsStream, data: []const u8) !void {
- try sendEncryptedRecord(self.fd, CT_APPLICATION_DATA, data, self.write_key, self.write_iv, &self.write_seq);
+ try sendEncryptedRecord(self.fd, @intFromEnum(tls.ContentType.application_data), data, self.write_key, self.write_iv, &self.write_seq);
}
/// Send close_notify alert.
pub fn close(self: *TlsStream) void {
- const alert = [_]u8{ 1, 0 }; // warning, close_notify
- sendEncryptedRecord(self.fd, CT_ALERT, &alert, self.write_key, self.write_iv, &self.write_seq) catch {};
+ const alert = [_]u8{
+ @intFromEnum(tls.Alert.Level.warning),
+ @intFromEnum(tls.Alert.Description.close_notify),
+ };
+ sendEncryptedRecord(self.fd, @intFromEnum(tls.ContentType.alert), &alert, self.write_key, self.write_iv, &self.write_seq) catch {};
}
};
@@ -321,7 +301,7 @@ fn sendRecord(fd: posix.fd_t, content_type: u8, payload: []const u8) !void {
var hdr: [5]u8 = undefined;
hdr[0] = content_type;
// Use TLS 1.0 for ClientHello compat, TLS 1.2 for the rest
- if (content_type == CT_HANDSHAKE) {
+ if (content_type == @intFromEnum(tls.ContentType.handshake)) {
hdr[1] = TLS12_VERSION[0];
hdr[2] = TLS12_VERSION[1];
} else {
@@ -343,7 +323,7 @@ fn sendEncryptedRecord(
seq: *u64,
) !void {
// TLS 1.3 encrypted record: inner content = plaintext + content_type byte
- // Outer record: CT_APPLICATION_DATA, encrypted payload + tag
+ // Outer record: application_data content type, encrypted payload + tag
const inner_len = plaintext.len + 1; // +1 for inner content type
const ciphertext_len = inner_len + TAG_LEN;
@@ -351,7 +331,7 @@ fn sendEncryptedRecord(
// Build AAD (record header with ciphertext length)
var aad: [5]u8 = undefined;
- aad[0] = CT_APPLICATION_DATA;
+ aad[0] = @intFromEnum(tls.ContentType.application_data);
aad[1] = TLS12_VERSION[0];
aad[2] = TLS12_VERSION[1];
aad[3] = @intCast(ciphertext_len >> 8);
@@ -395,7 +375,7 @@ fn decryptRecord(
// Build AAD
var aad: [5]u8 = undefined;
- aad[0] = CT_APPLICATION_DATA;
+ aad[0] = @intFromEnum(tls.ContentType.application_data);
aad[1] = TLS12_VERSION[0];
aad[2] = TLS12_VERSION[1];
aad[3] = @intCast(ciphertext_with_tag.len >> 8);
@@ -479,19 +459,19 @@ fn parseClientHello(msg: []const u8) !ClientHelloInfo {
ext_pos += 2;
if (ext_pos + elen > ext_data.len) break;
- if (etype == EXT_KEY_SHARE and elen >= 2) {
+ if (etype == @intFromEnum(tls.ExtensionType.key_share) and elen >= 2) {
var share_pos: usize = 2; // skip client_shares_len
while (share_pos + 4 <= elen) {
const group = readU16(ext_data[ext_pos + share_pos ..]);
const kelen = readU16(ext_data[ext_pos + share_pos + 2 ..]);
share_pos += 4;
- if (group == GROUP_X25519 and kelen == 32 and share_pos + 32 <= elen) {
+ if (group == @intFromEnum(tls.NamedGroup.x25519) and kelen == 32 and share_pos + 32 <= elen) {
@memcpy(&info.x25519_public, ext_data[ext_pos + share_pos ..][0..32]);
- info.key_share_group = GROUP_X25519;
+ info.key_share_group = @intFromEnum(tls.NamedGroup.x25519);
break;
- } else if (group == GROUP_SECP256R1 and kelen == 65 and share_pos + 65 <= elen) {
+ } else if (group == @intFromEnum(tls.NamedGroup.secp256r1) and kelen == 65 and share_pos + 65 <= elen) {
@memcpy(&info.p256_public, ext_data[ext_pos + share_pos ..][0..65]);
- if (info.key_share_group == 0) info.key_share_group = GROUP_SECP256R1;
+ if (info.key_share_group == 0) info.key_share_group = @intFromEnum(tls.NamedGroup.secp256r1);
}
share_pos += kelen;
}
@@ -524,7 +504,7 @@ fn buildServerHello(buf: []u8, server_random: *const [32]u8, x25519_pub: *const
}
// cipher_suite
- writeU16(buf[pos..], CIPHER_AES128_GCM);
+ writeU16(buf[pos..], @intFromEnum(tls.CipherSuite.AES_128_GCM_SHA256));
pos += 2;
// compression_method
@@ -536,18 +516,18 @@ fn buildServerHello(buf: []u8, server_random: *const [32]u8, x25519_pub: *const
pos += 2;
// supported_versions
- writeU16(buf[pos..], EXT_SUPPORTED_VERSIONS);
+ writeU16(buf[pos..], @intFromEnum(tls.ExtensionType.supported_versions));
writeU16(buf[pos + 2 ..], 2);
pos += 4;
- writeU16(buf[pos..], TLS13_VERSION);
+ writeU16(buf[pos..], @intFromEnum(tls.ProtocolVersion.tls_1_3));
pos += 2;
// key_share (X25519)
const ks_data_len: u16 = 2 + 2 + 32;
- writeU16(buf[pos..], EXT_KEY_SHARE);
+ writeU16(buf[pos..], @intFromEnum(tls.ExtensionType.key_share));
writeU16(buf[pos + 2 ..], ks_data_len);
pos += 4;
- writeU16(buf[pos..], GROUP_X25519);
+ writeU16(buf[pos..], @intFromEnum(tls.NamedGroup.x25519));
pos += 2;
writeU16(buf[pos..], 32);
pos += 2;
@@ -559,7 +539,7 @@ fn buildServerHello(buf: []u8, server_random: *const [32]u8, x25519_pub: *const
// Fill in message header
const body_len: u24 = @intCast(pos - 4);
- buf[0] = HS_SERVER_HELLO;
+ buf[0] = @intFromEnum(tls.HandshakeType.server_hello);
buf[1] = @intCast(body_len >> 16);
buf[2] = @intCast((body_len >> 8) & 0xff);
buf[3] = @intCast(body_len & 0xff);
@@ -578,7 +558,7 @@ fn buildEncryptedExtensions(buf: []u8, alpn_list: []const []const u8) []const u8
var alpn_total: usize = 0;
for (alpn_list) |proto| alpn_total += 1 + proto.len;
- writeU16(buf[pos..], EXT_ALPN);
+ writeU16(buf[pos..], @intFromEnum(tls.ExtensionType.application_layer_protocol_negotiation));
writeU16(buf[pos + 2 ..], @intCast(2 + alpn_total));
pos += 4;
writeU16(buf[pos..], @intCast(alpn_total));
@@ -594,7 +574,7 @@ fn buildEncryptedExtensions(buf: []u8, alpn_list: []const []const u8) []const u8
writeU16(buf[ext_list_start..], @intCast(pos - ext_list_start - 2));
const body_len: u24 = @intCast(pos - 4);
- buf[0] = HS_ENCRYPTED_EXTENSIONS;
+ buf[0] = @intFromEnum(tls.HandshakeType.encrypted_extensions);
buf[1] = @intCast(body_len >> 16);
buf[2] = @intCast((body_len >> 8) & 0xff);
buf[3] = @intCast(body_len & 0xff);
@@ -633,7 +613,7 @@ fn buildCertificate(buf: []u8, cert_chain: []const []const u8) []const u8 {
buf[cert_list_start + 2] = @intCast(cert_list_len & 0xff);
const body_len: u24 = @intCast(pos - 4);
- buf[0] = HS_CERTIFICATE;
+ buf[0] = @intFromEnum(tls.HandshakeType.certificate);
buf[1] = @intCast(body_len >> 16);
buf[2] = @intCast((body_len >> 8) & 0xff);
buf[3] = @intCast(body_len & 0xff);
@@ -671,7 +651,7 @@ fn buildCertificateVerify(buf: []u8, transcript_hash: [32]u8, private_key_bytes:
pos += sig_bytes.len;
const body_len: u24 = @intCast(pos - 4);
- buf[0] = HS_CERTIFICATE_VERIFY;
+ buf[0] = @intFromEnum(tls.HandshakeType.certificate_verify);
buf[1] = @intCast(body_len >> 16);
buf[2] = @intCast((body_len >> 8) & 0xff);
buf[3] = @intCast(body_len & 0xff);
@@ -682,10 +662,9 @@ fn buildCertificateVerify(buf: []u8, transcript_hash: [32]u8, private_key_bytes:
// ─── Helpers ─────────────────────────────────────────────────────────
fn readU16(data: []const u8) u16 {
- return (@as(u16, data[0]) << 8) | @as(u16, data[1]);
+ return std.mem.readInt(u16, data[0..2], .big);
}
fn writeU16(buf: []u8, val: u16) void {
- buf[0] = @intCast(val >> 8);
- buf[1] = @intCast(val & 0xff);
+ std.mem.writeInt(u16, buf[0..2], val, .big);
}
diff --git a/src/quic/connection.zig b/src/quic/connection.zig
index e3f1059..71584ed 100644
--- a/src/quic/connection.zig
+++ b/src/quic/connection.zig
@@ -4,6 +4,7 @@ const sys = @import("../sys.zig");
const net = std.net;
const posix = std.posix;
const crypto = std.crypto;
+const tls = std.crypto.tls;
const protocol = @import("protocol.zig");
const packet = @import("packet.zig");
@@ -2244,10 +2245,10 @@ pub const Connection = struct {
while (nst_iters < 10) : (nst_iters += 1) {
const action = hs.step() catch |err| {
// RFC 9001 §4.8: post-handshake TLS errors
- const tls_alert: u64 = switch (err) {
- error.UnexpectedMessage => 10,
- else => 80,
- };
+ const tls_alert: u64 = @intFromEnum(switch (err) {
+ error.UnexpectedMessage => tls.Alert.Description.unexpected_message,
+ else => tls.Alert.Description.internal_error,
+ });
self.closeWithTransportError(TransportError.cryptoError(tls_alert), @intFromEnum(FrameType.crypto), "post-handshake TLS error");
return;
};
@@ -2296,18 +2297,18 @@ pub const Connection = struct {
return;
}
// RFC 9001 §4.8: map TLS errors to CRYPTO_ERROR (0x100 + TLS alert code)
- const tls_alert: u64 = switch (err) {
- error.BadCertificate => 42, // bad_certificate
- error.BadCertificateVerify => 51, // decrypt_error
- error.UnexpectedMessage => 10, // unexpected_message
- error.DecodeError => 50, // decode_error
- error.BadFinished => 51, // decrypt_error
- error.NoKeyShare => 40, // handshake_failure
- error.UnsupportedVersion => 70, // protocol_version
- error.NoApplicationProtocol => 120, // no_application_protocol
- error.MissingExtension => 109, // missing_extension
- else => 80, // internal_error
- };
+ const tls_alert: u64 = @intFromEnum(switch (err) {
+ error.BadCertificate => tls.Alert.Description.bad_certificate,
+ error.BadCertificateVerify => tls.Alert.Description.decrypt_error,
+ error.UnexpectedMessage => tls.Alert.Description.unexpected_message,
+ error.DecodeError => tls.Alert.Description.decode_error,
+ error.BadFinished => tls.Alert.Description.decrypt_error,
+ error.NoKeyShare => tls.Alert.Description.handshake_failure,
+ error.UnsupportedVersion => tls.Alert.Description.protocol_version,
+ error.NoApplicationProtocol => tls.Alert.Description.no_application_protocol,
+ error.MissingExtension => tls.Alert.Description.missing_extension,
+ else => tls.Alert.Description.internal_error,
+ });
self.closeWithTransportError(TransportError.cryptoError(tls_alert), @intFromEnum(FrameType.crypto), "TLS handshake failure");
return;
};
diff --git a/src/quic/crypto.zig b/src/quic/crypto.zig
index 68a367b..0443363 100644
--- a/src/quic/crypto.zig
+++ b/src/quic/crypto.zig
@@ -4,6 +4,7 @@ const packet = @import("packet.zig");
const assert = std.debug.assert;
const crypto = std.crypto;
+const tls = std.crypto.tls;
const HkdfSha256 = crypto.kdf.hkdf.HkdfSha256;
const HmacSha256 = crypto.auth.hmac.sha2.HmacSha256;
const Aes128Gcm = crypto.aead.aes_gcm.Aes128Gcm;
@@ -375,9 +376,9 @@ pub fn deriveInitialKeyMaterial(
// Client (Initial keys are always AES-128-GCM)
secret = hkdfExpandLabel(initial_secret, "client in", "", Hmac.key_length);
- const client_key_16 = hkdfExpandLabelRuntime(secret, label_key, "", key_len);
- const client_iv = hkdfExpandLabelRuntime(secret, label_iv, "", nonce_len);
- const client_hp_16 = hkdfExpandLabelRuntime(secret, label_hp, "", key_len);
+ const client_key_16 = hkdfExpandLabel(secret, label_key, "", key_len);
+ const client_iv = hkdfExpandLabel(secret, label_iv, "", nonce_len);
+ const client_hp_16 = hkdfExpandLabel(secret, label_hp, "", key_len);
var client_key: [max_key_len]u8 = .{0} ** max_key_len;
@memcpy(client_key[0..key_len], &client_key_16);
var client_hp_key: [max_key_len]u8 = .{0} ** max_key_len;
@@ -385,9 +386,9 @@ pub fn deriveInitialKeyMaterial(
// Server
secret = hkdfExpandLabel(initial_secret, "server in", "", Hmac.key_length);
- const server_key_16 = hkdfExpandLabelRuntime(secret, label_key, "", key_len);
- const server_iv = hkdfExpandLabelRuntime(secret, label_iv, "", nonce_len);
- const server_hp_16 = hkdfExpandLabelRuntime(secret, label_hp, "", key_len);
+ const server_key_16 = hkdfExpandLabel(secret, label_key, "", key_len);
+ const server_iv = hkdfExpandLabel(secret, label_iv, "", nonce_len);
+ const server_hp_16 = hkdfExpandLabel(secret, label_hp, "", key_len);
var server_key: [max_key_len]u8 = .{0} ** max_key_len;
@memcpy(server_key[0..key_len], &server_key_16);
var server_hp_key: [max_key_len]u8 = .{0} ** max_key_len;
@@ -546,62 +547,14 @@ test "header protection key" {
}
}
-/// Uses hkdf's expand to generate a derived key.
-/// Constructs a hkdf context by generating a hkdf-label
-/// which consists of `length`, the label "tls13 " ++ `label` and the given
-/// `context`.
+/// RFC 8446 §7.1 HKDF-Expand-Label.
pub fn hkdfExpandLabel(
- secret: [32]u8,
- comptime label: []const u8,
- context: []const u8,
- comptime length: u16,
-) [length]u8 {
- // return tls.hkdfExpandLabel(HkdfSha256, secret, label, context, length);
-
- std.debug.assert(label.len <= 255 and label.len > 0);
- std.debug.assert(context.len <= 255);
- const full_label = "tls13 " ++ label;
-
- // length, label, context
- var buf: [2 + 255 + 255]u8 = undefined;
- std.mem.writeInt(u16, buf[0..2], length, .big);
- buf[2] = full_label.len;
- @memcpy(buf[3..][0..full_label.len], full_label);
- buf[3 + full_label.len] = @intCast(context.len);
- @memcpy(buf[4 + full_label.len ..][0..context.len], context);
- const actual_context = buf[0 .. 4 + full_label.len + context.len];
-
- var out: [32]u8 = undefined;
- HkdfSha256.expand(&out, actual_context, secret);
- return out[0..length].*;
-}
-
-/// Runtime version of hkdfExpandLabel for version-dependent labels.
-pub fn hkdfExpandLabelRuntime(
secret: [32]u8,
label: []const u8,
context: []const u8,
comptime length: u16,
) [length]u8 {
- std.debug.assert(label.len <= 249 and label.len > 0);
- std.debug.assert(context.len <= 255);
-
- var buf: [2 + 1 + 6 + 249 + 1 + 255]u8 = undefined;
- std.mem.writeInt(u16, buf[0..2], length, .big);
- // "tls13 " prefix (6 bytes) + label
- const prefix = "tls13 ";
- const full_len: u8 = @intCast(prefix.len + label.len);
- buf[2] = full_len;
- @memcpy(buf[3..][0..prefix.len], prefix);
- @memcpy(buf[3 + prefix.len ..][0..label.len], label);
- const label_end = 3 + prefix.len + label.len;
- buf[label_end] = @intCast(context.len);
- @memcpy(buf[label_end + 1 ..][0..context.len], context);
- const actual_context = buf[0 .. label_end + 1 + context.len];
-
- var out: [32]u8 = undefined;
- HkdfSha256.expand(&out, actual_context, secret);
- return out[0..length].*;
+ return tls.hkdfExpandLabel(HkdfSha256, secret, label, context, length);
}
/// Derive a QUIC key and pad to max_key_len.
@@ -614,24 +567,16 @@ pub fn deriveKeyPaddedV(secret: [32]u8, actual_len: usize, version: u32) [max_ke
fn deriveKeyPaddedL(secret: [32]u8, actual_len: usize, label: []const u8) [max_key_len]u8 {
var result: [max_key_len]u8 = .{0} ** max_key_len;
if (actual_len == 32) {
- result = hkdfExpandLabelRuntime(secret, label, "", 32);
+ result = hkdfExpandLabel(secret, label, "", 32);
} else {
- const k16 = hkdfExpandLabelRuntime(secret, label, "", 16);
+ const k16 = hkdfExpandLabel(secret, label, "", 16);
@memcpy(result[0..16], &k16);
}
return result;
}
pub fn deriveHpKeyPaddedV(secret: [32]u8, actual_len: usize, version: u32) [max_key_len]u8 {
- const label = protocol.quicLabel(version, .hp);
- var result: [max_key_len]u8 = .{0} ** max_key_len;
- if (actual_len == 32) {
- result = hkdfExpandLabelRuntime(secret, label, "", 32);
- } else {
- const k16 = hkdfExpandLabelRuntime(secret, label, "", 16);
- @memcpy(result[0..16], &k16);
- }
- return result;
+ return deriveKeyPaddedL(secret, actual_len, protocol.quicLabel(version, .hp));
}
/// AES-128-GCM confidentiality limit: 2^23 packets (~8M).
@@ -646,7 +591,7 @@ pub fn deriveNextTrafficSecret(current: [32]u8) [32]u8 {
}
pub fn deriveNextTrafficSecretV(current: [32]u8, version: u32) [32]u8 {
- return hkdfExpandLabelRuntime(current, protocol.quicLabel(version, .ku), "", 32);
+ return hkdfExpandLabel(current, protocol.quicLabel(version, .ku), "", 32);
}
/// Manages QUIC key update (RFC 9001 Section 6).
@@ -718,17 +663,17 @@ pub const KeyUpdateManager = struct {
// Derive current Open/Seal from the initial secrets
const recv_key = deriveKeyPaddedL(recv_secret, kl, label_key);
- const recv_iv = hkdfExpandLabelRuntime(recv_secret, label_iv, "", nonce_len);
+ const recv_iv = hkdfExpandLabel(recv_secret, label_iv, "", nonce_len);
const send_key = deriveKeyPaddedL(send_secret, kl, label_key);
- const send_iv = hkdfExpandLabelRuntime(send_secret, label_iv, "", nonce_len);
+ const send_iv = hkdfExpandLabel(send_secret, label_iv, "", nonce_len);
// Pre-compute next generation secrets and keys
const next_recv_secret = deriveNextTrafficSecretV(recv_secret, version);
const next_send_secret = deriveNextTrafficSecretV(send_secret, version);
const next_recv_key = deriveKeyPaddedL(next_recv_secret, kl, label_key);
- const next_recv_iv = hkdfExpandLabelRuntime(next_recv_secret, label_iv, "", nonce_len);
+ const next_recv_iv = hkdfExpandLabel(next_recv_secret, label_iv, "", nonce_len);
const next_send_key = deriveKeyPaddedL(next_send_secret, kl, label_key);
- const next_send_iv = hkdfExpandLabelRuntime(next_send_secret, label_iv, "", nonce_len);
+ const next_send_iv = hkdfExpandLabel(next_send_secret, label_iv, "", nonce_len);
var ku: KeyUpdateManager = .{
.current_open = .{ .key = recv_key, .hp_key = recv_hp, .nonce = recv_iv, .cipher_suite = cipher_suite },
@@ -779,14 +724,14 @@ pub const KeyUpdateManager = struct {
self.next_open = .{
.key = deriveKeyPaddedL(next_recv_secret, kl, label_key),
.hp_key = self.hp_open,
- .nonce = hkdfExpandLabelRuntime(next_recv_secret, label_iv, "", nonce_len),
+ .nonce = hkdfExpandLabel(next_recv_secret, label_iv, "", nonce_len),
.cipher_suite = self.cipher_suite,
.hp_aes_ctx = self.hp_open_ctx,
};
self.next_seal = .{
.key = deriveKeyPaddedL(next_send_secret, kl, label_key),
.hp_key = self.hp_seal,
- .nonce = hkdfExpandLabelRuntime(next_send_secret, label_iv, "", nonce_len),
+ .nonce = hkdfExpandLabel(next_send_secret, label_iv, "", nonce_len),
.cipher_suite = self.cipher_suite,
.hp_aes_ctx = self.hp_seal_ctx,
};
diff --git a/src/quic/stream.zig b/src/quic/stream.zig
index 6c86758..3c5b2ab 100644
--- a/src/quic/stream.zig
+++ b/src/quic/stream.zig
@@ -373,7 +373,10 @@ pub const SendStream = struct {
urgency: u3 = 3,
/// RFC 9218 priority: incremental streams are interleaved round-robin.
- incremental: bool = true,
+ /// RFC 9218 §4.2: the default is non-incremental (sequential). Protocol
+ /// adapters that want round-robin multiplexing (WebTransport, HTTP/0.9)
+ /// opt in explicitly at their stream-open boundary.
+ incremental: bool = false,
/// WebTransport sendOrder: higher values transmitted first. When set,
/// takes precedence over RFC 9218 urgency for scheduling.
diff --git a/src/quic/tls13.zig b/src/quic/tls13.zig
index 6c003ed..ea077de 100644
--- a/src/quic/tls13.zig
+++ b/src/quic/tls13.zig
@@ -8,6 +8,7 @@ const std = @import("std");
const sys = @import("../sys.zig");
const io = @import("../io_compat.zig");
const crypto = std.crypto;
+const tls = std.crypto.tls;
const quic_crypto = @import("crypto.zig");
const protocol = @import("protocol.zig");
const transport_params = @import("transport_params.zig");
@@ -50,26 +51,12 @@ const ExtType = enum(u16) {
_,
};
-// Signature algorithms
-const SIG_ECDSA_P256_SHA256: u16 = 0x0403;
-const SIG_RSA_PSS_RSAE_SHA256: u16 = 0x0804;
-const SIG_RSA_PSS_RSAE_SHA384: u16 = 0x0805;
-const SIG_RSA_PSS_RSAE_SHA512: u16 = 0x0806;
-const SIG_ED25519: u16 = 0x0807;
-
-// Named groups
-const GROUP_SECP256R1: u16 = 0x0017;
-const GROUP_X25519: u16 = 0x001d;
+// Signature schemes, named groups, cipher suites and protocol version are
+// referenced directly via std.crypto.tls enums (SignatureScheme, NamedGroup,
+// CipherSuite, ProtocolVersion) at their use sites.
const P256 = crypto.ecc.P256;
-// TLS 1.3 version
-const TLS13_VERSION: u16 = 0x0304;
-
-// Cipher suites
-const CIPHER_SUITE_AES128_GCM_SHA256: u16 = 0x1301;
-const CIPHER_SUITE_CHACHA20_POLY1305_SHA256: u16 = 0x1303;
-
pub const EncryptionLevel = quic_crypto.EncryptionLevel;
pub const PrivateKeyAlgorithm = enum {
@@ -86,14 +73,15 @@ fn verifyCertificateVerifySignature(
sig_bytes: []const u8,
signed_content: []const u8,
) HandshakeError!void {
- switch (sig_algo) {
- SIG_ECDSA_P256_SHA256 => {
+ const scheme: tls.SignatureScheme = @enumFromInt(sig_algo);
+ switch (scheme) {
+ .ecdsa_secp256r1_sha256 => {
if (pub_key_algo != .X9_62_id_ecPublicKey) return error.BadCertificateVerify;
const pub_key = EcdsaP256Sha256.PublicKey.fromSec1(pub_key_bytes) catch return error.BadCertificateVerify;
const sig = EcdsaP256Sha256.Signature.fromDer(sig_bytes) catch return error.BadCertificateVerify;
sig.verify(signed_content, pub_key) catch return error.BadCertificateVerify;
},
- SIG_ED25519 => {
+ .ed25519 => {
if (pub_key_algo != .curveEd25519) return error.BadCertificateVerify;
if (pub_key_bytes.len != Ed25519.PublicKey.encoded_length) return error.BadCertificateVerify;
if (sig_bytes.len != Ed25519.Signature.encoded_length) return error.BadCertificateVerify;
@@ -101,9 +89,9 @@ fn verifyCertificateVerifySignature(
const sig = Ed25519.Signature.fromBytes(sig_bytes[0..Ed25519.Signature.encoded_length].*);
sig.verify(signed_content, pub_key) catch return error.BadCertificateVerify;
},
- SIG_RSA_PSS_RSAE_SHA256 => verifyRsaPss(pub_key_bytes, pub_key_algo, sig_bytes, signed_content, Sha256) catch return error.BadCertificateVerify,
- SIG_RSA_PSS_RSAE_SHA384 => verifyRsaPss(pub_key_bytes, pub_key_algo, sig_bytes, signed_content, Sha384) catch return error.BadCertificateVerify,
- SIG_RSA_PSS_RSAE_SHA512 => verifyRsaPss(pub_key_bytes, pub_key_algo, sig_bytes, signed_content, Sha512) catch return error.BadCertificateVerify,
+ .rsa_pss_rsae_sha256 => verifyRsaPss(pub_key_bytes, pub_key_algo, sig_bytes, signed_content, Sha256) catch return error.BadCertificateVerify,
+ .rsa_pss_rsae_sha384 => verifyRsaPss(pub_key_bytes, pub_key_algo, sig_bytes, signed_content, Sha384) catch return error.BadCertificateVerify,
+ .rsa_pss_rsae_sha512 => verifyRsaPss(pub_key_bytes, pub_key_algo, sig_bytes, signed_content, Sha512) catch return error.BadCertificateVerify,
else => return error.BadCertificateVerify,
}
}
@@ -378,8 +366,7 @@ pub const KeySchedule = struct {
// Derive handshake secrets from the shared secret and transcript hash.
pub fn deriveHandshakeSecrets(self: *KeySchedule, shared_secret: []const u8, transcript_hash: [32]u8) void {
// derived1 = Derive-Secret(early_secret, "derived", Hash(""))
- var empty_hash: [32]u8 = undefined;
- Sha256.hash("", &empty_hash, .{});
+ const empty_hash = tls.emptyHash(Sha256);
const derived1 = deriveSecret(self.early_secret, "derived", empty_hash);
// handshake_secret = HKDF-Extract(derived1, shared_secret)
@@ -397,8 +384,7 @@ pub const KeySchedule = struct {
// Derive application secrets from the transcript hash after server Finished.
pub fn deriveAppSecrets(self: *KeySchedule, transcript_hash: [32]u8) void {
// derived2 = Derive-Secret(handshake_secret, "derived", Hash(""))
- var empty_hash: [32]u8 = undefined;
- Sha256.hash("", &empty_hash, .{});
+ const empty_hash = tls.emptyHash(Sha256);
const derived2 = deriveSecret(self.handshake_secret, "derived", empty_hash);
// master_secret = HKDF-Extract(derived2, 0)
@@ -444,7 +430,7 @@ pub const KeySchedule = struct {
const label_iv = protocol.quicLabel(version, .iv);
var open: quic_crypto.Open = .{
.key = quic_crypto.deriveKeyPaddedV(traffic_secret, kl, version),
- .nonce = quic_crypto.hkdfExpandLabelRuntime(traffic_secret, label_iv, "", 12),
+ .nonce = quic_crypto.hkdfExpandLabel(traffic_secret, label_iv, "", 12),
.hp_key = quic_crypto.deriveHpKeyPaddedV(traffic_secret, cipher.hpKeyLen(), version),
.cipher_suite = cipher,
};
@@ -457,7 +443,7 @@ pub const KeySchedule = struct {
const label_iv = protocol.quicLabel(version, .iv);
var seal: quic_crypto.Seal = .{
.key = quic_crypto.deriveKeyPaddedV(traffic_secret, kl, version),
- .nonce = quic_crypto.hkdfExpandLabelRuntime(traffic_secret, label_iv, "", 12),
+ .nonce = quic_crypto.hkdfExpandLabel(traffic_secret, label_iv, "", 12),
.hp_key = quic_crypto.deriveHpKeyPaddedV(traffic_secret, cipher.hpKeyLen(), version),
.cipher_suite = cipher,
};
@@ -468,11 +454,7 @@ pub const KeySchedule = struct {
// Compute the Finished verify_data.
pub fn computeFinishedVerifyData(base_key: [32]u8, transcript_hash: [32]u8) [32]u8 {
const finished_key = quic_crypto.hkdfExpandLabel(base_key, "finished", "", 32);
- var hmac: [32]u8 = undefined;
- var h = HmacSha256.init(&finished_key);
- h.update(&transcript_hash);
- h.final(&hmac);
- return hmac;
+ return tls.hmac(HmacSha256, &transcript_hash, finished_key);
}
fn deriveSecret(secret: [32]u8, comptime label: []const u8, transcript_hash: [32]u8) [32]u8 {
@@ -650,7 +632,7 @@ pub const Tls13Handshake = struct {
p256_secret: [32]u8 = undefined,
p256_public: [65]u8 = undefined, // our uncompressed public point
peer_p256_public: [65]u8 = undefined, // peer's uncompressed public point
- negotiated_group: u16 = GROUP_X25519,
+ negotiated_group: tls.NamedGroup = .x25519,
// Output buffer for built messages (32KB for large cert chains, e.g. 9-cert amplificationlimit test)
out_buf: [32768]u8 = undefined,
@@ -750,7 +732,7 @@ pub const Tls13Handshake = struct {
sys.randomBytes(&self.p256_secret);
break :blk P256.basePoint.mulPublic(self.p256_secret, .big) catch unreachable;
}).toUncompressedSec1();
- self.negotiated_group = GROUP_X25519;
+ self.negotiated_group = .x25519;
return self;
}
@@ -795,7 +777,7 @@ pub const Tls13Handshake = struct {
sys.randomBytes(&self.x25519_secret);
break :blk X25519.recoverPublicKey(self.x25519_secret) catch unreachable;
};
- self.negotiated_group = GROUP_X25519;
+ self.negotiated_group = .x25519;
return self;
}
@@ -986,9 +968,9 @@ pub const Tls13Handshake = struct {
if (pos + 3 > body.len) return error.DecodeError;
const cipher_suite_raw = readU16(body[pos..]);
pos += 2;
- if (cipher_suite_raw == CIPHER_SUITE_AES128_GCM_SHA256) {
+ if (cipher_suite_raw == @intFromEnum(tls.CipherSuite.AES_128_GCM_SHA256)) {
self.negotiated_cipher_suite = .aes_128_gcm_sha256;
- } else if (cipher_suite_raw == CIPHER_SUITE_CHACHA20_POLY1305_SHA256) {
+ } else if (cipher_suite_raw == @intFromEnum(tls.CipherSuite.CHACHA20_POLY1305_SHA256)) {
self.negotiated_cipher_suite = .chacha20_poly1305_sha256;
} else {
return error.UnsupportedVersion;
@@ -1015,13 +997,13 @@ pub const Tls13Handshake = struct {
if (elen < 4) return error.DecodeError;
const group = readU16(ext_data[ext_pos..]);
const kelen = readU16(ext_data[ext_pos + 2 ..]);
- if (group == GROUP_X25519 and kelen == 32 and ext_pos + 4 + 32 <= ext_data.len) {
+ if (group == @intFromEnum(tls.NamedGroup.x25519) and kelen == 32 and ext_pos + 4 + 32 <= ext_data.len) {
@memcpy(&self.peer_x25519_public, ext_data[ext_pos + 4 ..][0..32]);
- self.negotiated_group = GROUP_X25519;
+ self.negotiated_group = .x25519;
found_key_share = true;
- } else if (group == GROUP_SECP256R1 and kelen == 65 and ext_pos + 4 + 65 <= ext_data.len) {
+ } else if (group == @intFromEnum(tls.NamedGroup.secp256r1) and kelen == 65 and ext_pos + 4 + 65 <= ext_data.len) {
@memcpy(&self.peer_p256_public, ext_data[ext_pos + 4 ..][0..65]);
- self.negotiated_group = GROUP_SECP256R1;
+ self.negotiated_group = .secp256r1;
found_key_share = true;
} else {
return error.NoKeyShare;
@@ -1045,7 +1027,7 @@ pub const Tls13Handshake = struct {
// Compute shared secret based on negotiated group
var shared_secret: [32]u8 = undefined;
- if (self.negotiated_group == GROUP_SECP256R1) {
+ if (self.negotiated_group == .secp256r1) {
const peer_point = P256.fromSec1(self.peer_p256_public[0..65]) catch return error.KeyScheduleError;
const shared_point = peer_point.mulPublic(self.p256_secret, .big) catch return error.KeyScheduleError;
const shared_uncompressed = shared_point.toUncompressedSec1();
@@ -1329,10 +1311,10 @@ pub const Tls13Handshake = struct {
break;
}
} else {
- if (cs_id == CIPHER_SUITE_AES128_GCM_SHA256 and !cs_found) {
+ if (cs_id == @intFromEnum(tls.CipherSuite.AES_128_GCM_SHA256) and !cs_found) {
self.negotiated_cipher_suite = .aes_128_gcm_sha256;
cs_found = true;
- } else if (cs_id == CIPHER_SUITE_CHACHA20_POLY1305_SHA256 and !cs_found) {
+ } else if (cs_id == @intFromEnum(tls.CipherSuite.CHACHA20_POLY1305_SHA256) and !cs_found) {
self.negotiated_cipher_suite = .chacha20_poly1305_sha256;
cs_found = true;
}
@@ -1376,12 +1358,12 @@ pub const Tls13Handshake = struct {
const group = readU16(ext_data[ext_pos + share_pos ..]);
const kelen = readU16(ext_data[ext_pos + share_pos + 2 ..]);
share_pos += 4;
- if (group == GROUP_X25519 and kelen == 32 and share_pos + 32 <= elen) {
+ if (group == @intFromEnum(tls.NamedGroup.x25519) and kelen == 32 and share_pos + 32 <= elen) {
@memcpy(&self.peer_x25519_public, ext_data[ext_pos + share_pos ..][0..32]);
- self.negotiated_group = GROUP_X25519;
+ self.negotiated_group = .x25519;
found_key_share = true;
break;
- } else if (group == GROUP_SECP256R1 and kelen == 65 and share_pos + 65 <= elen) {
+ } else if (group == @intFromEnum(tls.NamedGroup.secp256r1) and kelen == 65 and share_pos + 65 <= elen) {
@memcpy(&self.peer_p256_public, ext_data[ext_pos + share_pos ..][0..65]);
found_p256 = true;
}
@@ -1389,7 +1371,7 @@ pub const Tls13Handshake = struct {
}
// Use P-256 if X25519 not found
if (!found_key_share and found_p256) {
- self.negotiated_group = GROUP_SECP256R1;
+ self.negotiated_group = .secp256r1;
found_key_share = true;
}
}
@@ -1493,7 +1475,7 @@ pub const Tls13Handshake = struct {
// Prepare key share based on negotiated group
var ks_data_buf: [65]u8 = undefined;
- const ks_data: []const u8 = if (self.negotiated_group == GROUP_SECP256R1) blk: {
+ const ks_data: []const u8 = if (self.negotiated_group == .secp256r1) blk: {
// Generate P-256 ephemeral key pair
sys.randomBytes(&self.p256_secret);
self.p256_public = (P256.basePoint.mulPublic(self.p256_secret, .big) catch return error.KeyScheduleError).toUncompressedSec1();
@@ -1520,7 +1502,7 @@ pub const Tls13Handshake = struct {
// Compute shared secret based on negotiated group
var shared_secret: [32]u8 = undefined;
- if (self.negotiated_group == GROUP_SECP256R1) {
+ if (self.negotiated_group == .secp256r1) {
// P-256 ECDH: multiply peer's public key by our secret
const peer_point = P256.fromSec1(self.peer_p256_public[0..65]) catch return error.KeyScheduleError;
const shared_point = peer_point.mulPublic(self.p256_secret, .big) catch return error.KeyScheduleError;
@@ -1886,8 +1868,7 @@ pub const Tls13Handshake = struct {
// Verify binder
// binder_key = Derive-Secret(early_secret, "res binder", Hash(""))
- var empty_hash: [32]u8 = undefined;
- Sha256.hash("", &empty_hash, .{});
+ const empty_hash = tls.emptyHash(Sha256);
const binder_key = quic_crypto.hkdfExpandLabel(temp_ks.early_secret, "res binder", &empty_hash, 32);
// Partial ClientHello = up to and including identities field (RFC 8446 §4.2.11.2)
@@ -2096,9 +2077,9 @@ fn buildClientHello(
// Offer both AES-128-GCM and ChaCha20-Poly1305
writeU16(buf[pos..], 4);
pos += 2;
- writeU16(buf[pos..], CIPHER_SUITE_AES128_GCM_SHA256);
+ writeU16(buf[pos..], @intFromEnum(tls.CipherSuite.AES_128_GCM_SHA256));
pos += 2;
- writeU16(buf[pos..], CIPHER_SUITE_CHACHA20_POLY1305_SHA256);
+ writeU16(buf[pos..], @intFromEnum(tls.CipherSuite.CHACHA20_POLY1305_SHA256));
pos += 2;
}
@@ -2116,7 +2097,7 @@ fn buildClientHello(
pos = writeExtHeader(buf, pos, @intFromEnum(ExtType.supported_versions), 3);
buf[pos] = 2; // list length
pos += 1;
- writeU16(buf[pos..], TLS13_VERSION);
+ writeU16(buf[pos..], @intFromEnum(tls.ProtocolVersion.tls_1_3));
pos += 2;
// key_share extension (X25519 + P-256)
@@ -2127,14 +2108,14 @@ fn buildClientHello(
writeU16(buf[pos..], shares_total); // client_shares length
pos += 2;
// X25519 share (preferred)
- writeU16(buf[pos..], GROUP_X25519);
+ writeU16(buf[pos..], @intFromEnum(tls.NamedGroup.x25519));
pos += 2;
writeU16(buf[pos..], 32);
pos += 2;
@memcpy(buf[pos..][0..32], x25519_pub);
pos += 32;
// P-256 share (fallback)
- writeU16(buf[pos..], GROUP_SECP256R1);
+ writeU16(buf[pos..], @intFromEnum(tls.NamedGroup.secp256r1));
pos += 2;
writeU16(buf[pos..], 65);
pos += 2;
@@ -2145,24 +2126,24 @@ fn buildClientHello(
pos = writeExtHeader(buf, pos, @intFromEnum(ExtType.signature_algorithms), 2 + 10);
writeU16(buf[pos..], 10); // list length (5 algorithms x 2 bytes)
pos += 2;
- writeU16(buf[pos..], SIG_ED25519);
+ writeU16(buf[pos..], @intFromEnum(tls.SignatureScheme.ed25519));
pos += 2;
- writeU16(buf[pos..], SIG_ECDSA_P256_SHA256);
+ writeU16(buf[pos..], @intFromEnum(tls.SignatureScheme.ecdsa_secp256r1_sha256));
pos += 2;
- writeU16(buf[pos..], SIG_RSA_PSS_RSAE_SHA256);
+ writeU16(buf[pos..], @intFromEnum(tls.SignatureScheme.rsa_pss_rsae_sha256));
pos += 2;
- writeU16(buf[pos..], SIG_RSA_PSS_RSAE_SHA384);
+ writeU16(buf[pos..], @intFromEnum(tls.SignatureScheme.rsa_pss_rsae_sha384));
pos += 2;
- writeU16(buf[pos..], SIG_RSA_PSS_RSAE_SHA512);
+ writeU16(buf[pos..], @intFromEnum(tls.SignatureScheme.rsa_pss_rsae_sha512));
pos += 2;
// supported_groups extension
pos = writeExtHeader(buf, pos, @intFromEnum(ExtType.supported_groups), 2 + 4);
writeU16(buf[pos..], 4); // list length (2 groups x 2 bytes)
pos += 2;
- writeU16(buf[pos..], GROUP_X25519);
+ writeU16(buf[pos..], @intFromEnum(tls.NamedGroup.x25519));
pos += 2;
- writeU16(buf[pos..], GROUP_SECP256R1);
+ writeU16(buf[pos..], @intFromEnum(tls.NamedGroup.secp256r1));
pos += 2;
// SNI extension
@@ -2264,8 +2245,7 @@ fn buildClientHello(
// Now compute the real binder
// binder_key = Derive-Secret(early_secret, "res binder", Hash(""))
- var empty_hash: [32]u8 = undefined;
- Sha256.hash("", &empty_hash, .{});
+ const empty_hash = tls.emptyHash(Sha256);
const binder_key = quic_crypto.hkdfExpandLabel(key_schedule.early_secret, "res binder", &empty_hash, 32);
// partial_ch = everything up to and including identities (RFC 8446 §4.2.11.2)
@@ -2298,7 +2278,7 @@ fn buildClientHello(
fn buildServerHello(
buf: []u8,
server_random: *const [32]u8,
- key_share_group: u16,
+ key_share_group: tls.NamedGroup,
key_share_data: []const u8,
session_id_echo: []const u8,
using_psk: bool,
@@ -2337,13 +2317,13 @@ fn buildServerHello(
// supported_versions
pos = writeExtHeader(buf, pos, @intFromEnum(ExtType.supported_versions), 2);
- writeU16(buf[pos..], TLS13_VERSION);
+ writeU16(buf[pos..], @intFromEnum(tls.ProtocolVersion.tls_1_3));
pos += 2;
// key_share (server's key)
const ks_len: u16 = @intCast(2 + 2 + key_share_data.len);
pos = writeExtHeader(buf, pos, @intFromEnum(ExtType.key_share), ks_len);
- writeU16(buf[pos..], key_share_group);
+ writeU16(buf[pos..], @intFromEnum(key_share_group));
pos += 2;
writeU16(buf[pos..], @intCast(key_share_data.len));
pos += 2;
@@ -2495,7 +2475,7 @@ fn buildCertificateVerify(
const sig = key_pair.sign(&sign_content, null) catch return error.InternalError;
const sig_bytes = sig.toDer(&sig_storage);
sig_len = sig_bytes.len;
- break :sig_algo SIG_ECDSA_P256_SHA256;
+ break :sig_algo @intFromEnum(tls.SignatureScheme.ecdsa_secp256r1_sha256);
},
.ed25519 => sig_algo: {
const key_pair = Ed25519.KeyPair.generateDeterministic(private_key_bytes[0..32].*) catch return error.InternalError;
@@ -2503,7 +2483,7 @@ fn buildCertificateVerify(
const sig_bytes = sig.toBytes();
@memcpy(sig_storage[0..sig_bytes.len], &sig_bytes);
sig_len = sig_bytes.len;
- break :sig_algo SIG_ED25519;
+ break :sig_algo @intFromEnum(tls.SignatureScheme.ed25519);
},
};
@@ -2539,12 +2519,11 @@ fn writeExtHeader(buf: []u8, pos: usize, ext_type: u16, ext_len: usize) usize {
}
fn readU16(data: []const u8) u16 {
- return (@as(u16, data[0]) << 8) | @as(u16, data[1]);
+ return std.mem.readInt(u16, data[0..2], .big);
}
fn writeU16(buf: []u8, val: u16) void {
- buf[0] = @intCast(val >> 8);
- buf[1] = @intCast(val & 0xff);
+ std.mem.writeInt(u16, buf[0..2], val, .big);
}
// ─── Minimal PEM parser ──────────────────────────────────────────────
@@ -2875,7 +2854,7 @@ test "buildServerHello: produces valid message" {
@memset(&client_random, 0xAA);
var buf: [512]u8 = undefined;
- const msg = try buildServerHello(&buf, &random, GROUP_X25519, &pub_key, &client_random, false, .aes_128_gcm_sha256);
+ const msg = try buildServerHello(&buf, &random, .x25519, &pub_key, &client_random, false, .aes_128_gcm_sha256);
try std.testing.expectEqual(@as(u8, @intFromEnum(MsgType.server_hello)), msg[0]);
const body_len = (@as(usize, msg[1]) << 16) | (@as(usize, msg[2]) << 8) | @as(usize, msg[3]);
@@ -2990,8 +2969,7 @@ test "PSK binder computation: deterministic and correct" {
_ = &ks_zero;
// binder_key = Derive-Secret(early_secret, "res binder", Hash(""))
- var empty_hash: [32]u8 = undefined;
- Sha256.hash("", &empty_hash, .{});
+ const empty_hash = tls.emptyHash(Sha256);
const binder_key = quic_crypto.hkdfExpandLabel(ks.early_secret, "res binder", &empty_hash, 32);
// Compute binder for a fake partial transcript
diff --git a/src/webtransport/session.zig b/src/webtransport/session.zig
index 0c291de..f7d6e27 100644
--- a/src/webtransport/session.zig
+++ b/src/webtransport/session.zig
@@ -230,6 +230,9 @@ pub const WebTransportConnection = struct {
const stream = try self.quic.openStream();
const stream_id = stream.stream_id;
stream.send.send_order = send_order;
+ // WT streams have no defined inter-stream ordering; round-robin
+ // interleaving is the correct scheduling (also the quicperf path).
+ stream.send.incremental = true;
// Write WT bidi stream type prefix
var prefix_buf: [16]u8 = undefined;
@@ -248,6 +251,9 @@ pub const WebTransportConnection = struct {
const send_stream = try self.quic.openUniStream();
const stream_id = send_stream.stream_id;
send_stream.send_order = send_order;
+ // WT streams have no defined inter-stream ordering; round-robin
+ // interleaving is the correct scheduling (also the quicperf path).
+ send_stream.incremental = true;
// Write WT uni stream type prefix
var prefix_buf: [16]u8 = undefined;