Skip to content

Instantly share code, notes, and snippets.

@magurotuna
Created July 18, 2022 12:50
Show Gist options
  • Save magurotuna/c5bdd28dea59a4d0773eac425751a3d3 to your computer and use it in GitHub Desktop.
Save magurotuna/c5bdd28dea59a4d0773eac425751a3d3 to your computer and use it in GitHub Desktop.

zig-tls-client-hello

TLS 1.3 Client Hello in Zig

version

  • Zig: 0.10.0-dev.3007+6ba2fb3db
  • OpenSSL: 3.05

プロジェクト構造

.
├── README.md
├── build.zig
└── src
    ├── cipher_suite.zig
    ├── client_hello.zig
    ├── extension.zig
    ├── handshake.zig
    ├── key_exchange.zig
    ├── key_share.zig
    ├── legacy_compression_methods.zig
    ├── legacy_session_id.zig
    ├── main.zig
    ├── named_group.zig
    ├── server_hello.zig
    ├── signature_scheme.zig
    ├── supported_groups.zig
    ├── supported_versions.zig
    └── tls_plaintext.zig

動作確認

  1. テスト用サーバー立ち上げ
$ openssl s_server -accept 10443 -cert <path_to_cert> -key <path_to_key>
  1. ClientHello 送信
$ zig build run
  1. 以下の結果が得られる
info: Establish TCP connection to 127.0.0.1:10443

info: Send ClientHello: tls_plaintext.TLSPlaintext{ .handshake = handshake.Handshake{ .client_hello = client_hello.ClientHello{ .legacy_protocol_version = 771, .random = { ... }, .cipher_suites = cipher_suite.CipherSuites{ ... }, .extensions = extension.Extensions{ ... } } } }

info: Received ServerHello: tls_plaintext.TLSPlaintext{ .handshake = handshake.Handshake{ .server_hello = server_hello.ServerHello{ .legacy_protocol_version = 771, .random = { ... }, .legacy_session_id_echo = server_hello.LegacySessionIdEcho{ ... }, .cipher_suite = { ... }, .legacy_compression_method = 0, .extensions = extension.Extensions{ ... } } } }

ユニットテスト

$ zig build test

References

const std = @import("std");
pub fn build(b: *std.build.Builder) void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});
// Standard release options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
const mode = b.standardReleaseOptions();
const exe = b.addExecutable("zig-tls-client-hello", "src/main.zig");
exe.setTarget(target);
exe.setBuildMode(mode);
exe.install();
const run_cmd = exe.run();
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const exe_tests = b.addTest("src/main.zig");
exe_tests.setTarget(target);
exe_tests.setBuildMode(mode);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&exe_tests.step);
}
const std = @import("std");
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.3.1
pub const CipherSuites = struct {
const Self = @This();
cipher_suites: std.ArrayList(CipherSuite),
pub fn init(allocator: std.mem.Allocator) Self {
return Self{
.cipher_suites = std.ArrayList(CipherSuite).init(allocator),
};
}
pub fn deinit(self: Self) void {
self.cipher_suites.deinit();
}
pub fn add_cipher_suite(self: *Self, cipher_suite: CipherSuite) !void {
try self.cipher_suites.append(cipher_suite);
}
pub fn encode(self: Self, out_stream: anytype) !void {
const size = self.cipher_suites.items.len * @sizeOf(CipherSuite);
try out_stream.writeIntBig(u16, @intCast(u16, size));
for (self.cipher_suites.items) |suite| {
try out_stream.writeAll(&suite);
}
}
pub fn encodedSize(self: Self) usize {
const size_length = 2;
const data_length = self.cipher_suites.items.len * @sizeOf(CipherSuite);
return size_length + data_length;
}
};
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.3.1
pub const CipherSuite = [2]u8;
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.4
pub const TLS_AES_128_GCM_SHA256: CipherSuite = .{ 0x13, 0x01 };
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.4
pub const TLS_AES_256_GCM_SHA384: CipherSuite = .{ 0x13, 0x02 };
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.4
pub const TLS_CHACHA20_POLY1305_SHA256: CipherSuite = .{ 0x13, 0x03 };
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.4
pub const TLS_AES_128_CCM_SHA256: CipherSuite = .{ 0x13, 0x04 };
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.4
pub const TLS_AES_128_CCM_8_SHA256: CipherSuite = .{ 0x13, 0x05 };
test "CipherSuites properly encoded" {
var cipher_suites = CipherSuites.init(std.testing.allocator);
defer cipher_suites.deinit();
try cipher_suites.add_cipher_suite(TLS_AES_128_GCM_SHA256);
var out_buf: [1024]u8 = undefined;
var slice_stream = std.io.fixedBufferStream(&out_buf);
const out = slice_stream.writer();
try cipher_suites.encode(out);
const result = slice_stream.getWritten();
const expected = [_]u8{ 0x00, 0x02, 0x13, 0x01 };
try std.testing.expect(std.mem.eql(u8, &expected, result));
}
const std = @import("std");
const LegacySessionId = @import("./legacy_session_id.zig");
const LegacyCompressionMethods = @import("./legacy_compression_methods.zig");
const cipher_suite = @import("./cipher_suite.zig");
const CipherSuites = cipher_suite.CipherSuites;
const ext = @import("./extension.zig");
const Extensions = ext.Extensions;
const SupportedVersions = @import("./supported_versions.zig").SupportedVersions;
const sig = @import("./signature_scheme.zig");
const SignatureAlgorithm = sig.SignatureScheme;
const SignatureAlgorithms = sig.SignatureSchemeList;
const KeyShareClientHello = @import("./key_share.zig").KeyShareClientHello;
const KeyShareEntry = @import("./key_share.zig").KeyShareEntry;
const SupportedGroups = @import("./supported_groups.zig").SupportedGroups;
const NamedGroup = @import("./named_group.zig").NamedGroup;
const key_exchange = @import("./key_exchange.zig").key_exchange;
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.3.1
pub const ClientHello = struct {
const Self = @This();
legacy_protocol_version: u16 = 0x0303, // TLS v1.2
random: [32]u8,
cipher_suites: CipherSuites,
extensions: Extensions,
/// Create new ClientHello with default values being set.
pub fn init(allocator: std.mem.Allocator) !Self {
var cipher_suites = CipherSuites.init(allocator);
errdefer cipher_suites.deinit();
try cipher_suites.add_cipher_suite(cipher_suite.TLS_AES_128_GCM_SHA256);
var extensions = ext.Extensions.init(allocator);
errdefer extensions.deinit();
// Supported Versions extension
var versions = SupportedVersions.init(allocator);
errdefer versions.deinit();
try versions.add_version(SupportedVersions.TLS_1_3);
const version_ext = ext.Extension{ .supported_versions = versions };
try extensions.add_ext(version_ext);
// Signature Algorithms extension
var sig_algos = SignatureAlgorithms.init(allocator);
errdefer sig_algos.deinit();
try sig_algos.add_signature_scheme(SignatureAlgorithm.rsa_pss_rsae_sha256);
const sig_algo_ext = ext.Extension{ .signature_algorithms = sig_algos };
try extensions.add_ext(sig_algo_ext);
// Key Share extension
var key_shares = KeyShareClientHello.init(allocator);
errdefer key_shares.deinit();
const key_share = KeyShareEntry{ .x25519 = key_exchange };
try key_shares.add_key_share_entry(key_share);
const key_share_ext = ext.Extension{ .key_share = .{ .client = key_shares } };
try extensions.add_ext(key_share_ext);
// Supported Groups extension
var supported_groups = SupportedGroups.init(allocator);
errdefer supported_groups.deinit();
try supported_groups.add_group(NamedGroup.x25519);
const supported_groups_ext = ext.Extension{ .supported_groups = supported_groups };
try extensions.add_ext(supported_groups_ext);
return Self{
.random = [_]u8{0x00} ** 32,
.cipher_suites = cipher_suites,
.extensions = extensions,
};
}
pub fn deinit(self: Self) void {
self.cipher_suites.deinit();
self.extensions.deinit();
}
pub fn encode(self: Self, out_stream: anytype) !void {
try out_stream.writeIntBig(u16, self.legacy_protocol_version);
try out_stream.writeAll(&self.random);
try LegacySessionId.encode(out_stream);
try self.cipher_suites.encode(out_stream);
try LegacyCompressionMethods.encode(out_stream);
try self.extensions.encode(out_stream);
}
pub fn encodedSize(self: Self) usize {
const legacy_protocol_version_length = 2;
const random_length = 32;
const legacy_session_id_length = LegacySessionId.encodedSize();
const cipher_suites_length = self.cipher_suites.encodedSize();
const legacy_compression_methods_length = LegacyCompressionMethods.encodedSize();
const extensions_length = self.extensions.encodedSize();
return legacy_protocol_version_length + random_length + legacy_session_id_length + cipher_suites_length + legacy_compression_methods_length + extensions_length;
}
};
test "ClientHello properly encoded" {
const allocator = std.testing.allocator;
const hello = try ClientHello.init(allocator);
defer hello.deinit();
var out_buf: [1024]u8 = undefined;
var slice_stream = std.io.fixedBufferStream(&out_buf);
const out = slice_stream.writer();
try hello.encode(out);
const result = slice_stream.getWritten();
const expected = blk: {
const b_legacy_protocol_version = [_]u8{ 0x03, 0x03 };
const b_random = [_]u8{0x00} ** 32;
const b_legacy_session_id = [_]u8{ 0x01, 0x00 };
const b_cipher_suites = [_]u8{ 0x00, 0x02, 0x13, 0x01 };
const b_legacy_compression_methods = [_]u8{ 0x01, 0x00 };
const b_ext_length = [_]u8{ 0x00, 0x41 }; // 65 in decimal
const b_ext_supported_versions = [_]u8{ 0x00, 0x2b, 0x00, 0x03, 0x02, 0x03, 0x04 };
const b_ext_signature_algorithms = [_]u8{ 0x00, 0x0d, 0x00, 0x04, 0x00, 0x02, 0x08, 0x04 };
const b_ext_key_shares = key_blk: {
const extension_type = [_]u8{ 0x00, 0x33 };
const data_length = [_]u8{ 0x00, 0x26 }; // 38 in decimal
const entries_length = [_]u8{ 0x00, 0x24 }; // 36 in decimal
const group = [_]u8{ 0x00, 0x1d };
const length = [_]u8{ 0x00, 0x20 }; // 32 in decimal
const data = key_exchange;
break :key_blk extension_type ++
data_length ++
entries_length ++
group ++
length ++
data;
};
const b_ext_supported_groups = [_]u8{ 0x00, 0x0a, 0x00, 0x04, 0x00, 0x02, 0x00, 0x1d };
break :blk b_legacy_protocol_version ++
b_random ++
b_legacy_session_id ++
b_cipher_suites ++
b_legacy_compression_methods ++
b_ext_length ++
b_ext_supported_versions ++
b_ext_signature_algorithms ++
b_ext_key_shares ++
b_ext_supported_groups;
};
try std.testing.expect(std.mem.eql(u8, &expected, result));
}
const std = @import("std");
const SupportedVersions = @import("./supported_versions.zig").SupportedVersions;
const sig = @import("./signature_scheme.zig");
const SignatureAlgorithm = sig.SignatureScheme;
const SignatureAlgorithms = sig.SignatureSchemeList;
const KeyShareClientHello = @import("./key_share.zig").KeyShareClientHello;
const KeyShareEntry = @import("./key_share.zig").KeyShareEntry;
const KeyShare = @import("./key_share.zig").KeyShare;
const SupportedGroups = @import("./supported_groups.zig").SupportedGroups;
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.3.1
pub const ExtensionType = enum(u16) {
server_name = 0, // RFC 6066
max_fragment_length = 1, // RFC 6066
status_request = 5, // RFC 6066
supported_groups = 10, // RFC 8422, 7919
signature_algorithms = 13, // RFC 8446
use_srtp = 14, // RFC 5764
heartbeat = 15, // RFC 6520
application_layer_protocol_negotiation = 16, // RFC 7301
signed_certificate_timestamp = 18, // RFC 6962
client_certificate_type = 19, // RFC 7250
server_certificate_type = 20, // RFC 7250
padding = 21, // RFC 7685
RESERVED_1 = 40, // Used but never assigned
pre_shared_key = 41, // RFC 8446
early_data = 42, // RFC 8446
supported_versions = 43, // RFC 8446
cookie = 44, // RFC 8446
psk_key_exchange_modes = 45, // RFC 8446
RESERVED_2 = 46, // Used but never assigned
certificate_authorities = 47, // RFC 8446
oid_filters = 48, // RFC 8446
post_handshake_auth = 49, // RFC 8446
signature_algorithms_cert = 50, // RFC 8446
key_share = 51, // RFC 8446
};
// unimplemented yet
pub const ServerName = struct {};
pub const MaxFragmentLength = struct {};
pub const StatusRequest = struct {};
pub const UseStrp = struct {};
pub const Heartbeat = struct {};
pub const ApplicationLayerProtocolNegotiation = struct {};
pub const SignedCertificateTimestamp = struct {};
pub const ClientCertificateType = struct {};
pub const ServerCertificateType = struct {};
pub const Padding = struct {};
pub const Reserved = struct {};
pub const PreSharedKey = struct {};
pub const EarlyData = struct {};
pub const Cookie = struct {};
pub const PskKeyExchangeModes = struct {};
pub const CertificateAurhorities = struct {};
pub const OidFilters = struct {};
pub const PostHandshakeAuth = struct {};
pub const SignatureAlgorithmsCert = struct {};
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.3.1
pub const Extension = union(ExtensionType) {
server_name: ServerName,
max_fragment_length: MaxFragmentLength,
status_request: StatusRequest,
supported_groups: SupportedGroups,
signature_algorithms: SignatureAlgorithms,
use_srtp: UseStrp,
heartbeat: Heartbeat,
application_layer_protocol_negotiation: ApplicationLayerProtocolNegotiation,
signed_certificate_timestamp: SignedCertificateTimestamp,
client_certificate_type: ClientCertificateType,
server_certificate_type: ServerCertificateType,
padding: Padding,
RESERVED_1: Reserved,
pre_shared_key: PreSharedKey,
early_data: EarlyData,
supported_versions: SupportedVersions,
cookie: Cookie,
psk_key_exchange_modes: PskKeyExchangeModes,
RESERVED_2: Reserved,
certificate_authorities: CertificateAurhorities,
oid_filters: OidFilters,
post_handshake_auth: PostHandshakeAuth,
signature_algorithms_cert: SignatureAlgorithmsCert,
key_share: KeyShare,
const Self = @This();
pub fn deinit(self: Self) void {
switch (self) {
.supported_versions => |s| s.deinit(),
.signature_algorithms => |s| s.deinit(),
.key_share => |k| k.deinit(),
.supported_groups => |s| s.deinit(),
else => {
// TODO: unimplemented
},
}
}
pub fn encode(self: Self, out_stream: anytype) !void {
// extension_type
try out_stream.writeIntBig(u16, @enumToInt(self));
// extension_data
switch (self) {
.supported_versions => |supported_versions| {
try out_stream.writeIntBig(u16, @intCast(u16, supported_versions.encodedSize()));
try supported_versions.encode(out_stream);
},
.signature_algorithms => |signature_algorithms| {
try out_stream.writeIntBig(u16, @intCast(u16, signature_algorithms.encodedSize()));
try signature_algorithms.encode(out_stream);
},
.key_share => |key_share| {
try out_stream.writeIntBig(u16, @intCast(u16, key_share.encodedSize()));
try key_share.encode(out_stream);
},
.supported_groups => |supported_groups| {
try out_stream.writeIntBig(u16, @intCast(u16, supported_groups.encodedSize()));
try supported_groups.encode(out_stream);
},
else => {
// TODO: unimplemented
},
}
}
pub fn encodedSize(self: Self) usize {
const extension_type_size = @sizeOf(@typeInfo(Extension).Union.tag_type.?);
const extension_data_length_size = 2;
const data_length = switch (self) {
.supported_versions => |supported_versions| supported_versions.encodedSize(),
.signature_algorithms => |signature_algorithms| signature_algorithms.encodedSize(),
.key_share => |key_share| key_share.encodedSize(),
.supported_groups => |supported_groups| supported_groups.encodedSize(),
else => @as(usize, 0), // TODO(magurotuna) unimplemented
};
return extension_type_size + extension_data_length_size + data_length;
}
const DecodeReturnType = struct {
decoded: Self,
bytes_read: usize,
};
pub fn decode(allocator: std.mem.Allocator, in_stream: anytype) !DecodeReturnType {
const ext_type = @intToEnum(ExtensionType, try in_stream.readIntBig(u16));
var bytes_read: usize = 2;
const decoded = switch (ext_type) {
.supported_versions => blk: {
const res = try SupportedVersions.decode(allocator, in_stream);
bytes_read += res.bytes_read;
break :blk Extension{ .supported_versions = res.decoded };
},
.key_share => blk: {
// currently only support decoding for message coming from server
const res = try KeyShare.decode_server(in_stream);
bytes_read += res.bytes_read;
break :blk Extension{ .key_share = res.decoded };
},
else => unreachable, // TODO: unimplemented yet
};
return DecodeReturnType{
.decoded = decoded,
.bytes_read = bytes_read,
};
}
};
test "Supported Versions Extension properly encoded" {
const allocator = std.testing.allocator;
var versions = std.ArrayList(u16).init(allocator);
try versions.append(SupportedVersions.TLS_1_3);
const supported_versions = SupportedVersions{ .versions = versions };
const ext = Extension{ .supported_versions = supported_versions };
defer ext.deinit();
try std.testing.expectEqual(@as(usize, 7), ext.encodedSize());
var out_buf: [1024]u8 = undefined;
var slice_stream = std.io.fixedBufferStream(&out_buf);
const out = slice_stream.writer();
try ext.encode(out);
const result = slice_stream.getWritten();
// extension_type versions_length
// vvvvvvvvvv vvvv
const expected = [_]u8{ 0x00, 0x2b, 0x00, 0x03, 0x02, 0x03, 0x04 };
// ^^^^^^^^^^ ^^^^^^^^^^
// data_length version (TLS v1.3)
try std.testing.expect(std.mem.eql(u8, &expected, result));
}
test "Signature Algorithms Extension properly encoded" {
const allocator = std.testing.allocator;
var signature_algorithms = SignatureAlgorithms.init(allocator);
try signature_algorithms.add_signature_scheme(SignatureAlgorithm.rsa_pss_pss_sha256);
const ext = Extension{ .signature_algorithms = signature_algorithms };
defer ext.deinit();
try std.testing.expectEqual(@as(usize, 8), ext.encodedSize());
var out_buf: [1024]u8 = undefined;
var slice_stream = std.io.fixedBufferStream(&out_buf);
const out = slice_stream.writer();
try ext.encode(out);
const result = slice_stream.getWritten();
// extension_type algo_length
// vvvvvvvvvv vvvvvvvvvv
const expected = [_]u8{ 0x00, 0x0d, 0x00, 0x04, 0x00, 0x02, 0x08, 0x09 };
// ^^^^^^^^^^ ^^^^^^^^^^
// data_length algo
try std.testing.expect(std.mem.eql(u8, &expected, result));
}
test "Key Share Extension properly encoded" {
const allocator = std.testing.allocator;
const key_share = KeyShareEntry{ .x25519 = [_]u8{0x00} ** 32 };
var key_shares = KeyShareClientHello.init(allocator);
try key_shares.add_key_share_entry(key_share);
const ext = Extension{ .key_share = .{ .client = key_shares } };
defer ext.deinit();
try std.testing.expectEqual(@as(usize, 42), ext.encodedSize());
var out_buf: [1024]u8 = undefined;
var slice_stream = std.io.fixedBufferStream(&out_buf);
const out = slice_stream.writer();
try ext.encode(out);
const result = slice_stream.getWritten();
const expected = blk: {
const extension_type = [_]u8{ 0x00, 0x33 };
const data_length = [_]u8{ 0x00, 0x26 }; // 38 in decimal
const key_share_length = [_]u8{ 0x00, 0x24 }; // 36 in decimal
const group = [_]u8{ 0x00, 0x1d };
const length = [_]u8{ 0x00, 0x20 }; // 32 in decimal
const data = [_]u8{0x00} ** 32;
break :blk extension_type ++
data_length ++
key_share_length ++
group ++
length ++
data;
};
try std.testing.expect(std.mem.eql(u8, &expected, result));
}
test "Supported Groups Extension properly encoded" {
const NamedGroup = @import("./named_group.zig").NamedGroup;
const allocator = std.testing.allocator;
var groups = SupportedGroups.init(allocator);
try groups.add_group(NamedGroup.x25519);
const ext = Extension{ .supported_groups = groups };
defer ext.deinit();
try std.testing.expectEqual(@as(usize, 8), ext.encodedSize());
var out_buf: [1024]u8 = undefined;
var slice_stream = std.io.fixedBufferStream(&out_buf);
const out = slice_stream.writer();
try ext.encode(out);
const result = slice_stream.getWritten();
const expected = [_]u8{ 0x00, 0x0a, 0x00, 0x04, 0x00, 0x02, 0x00, 0x1d };
try std.testing.expect(std.mem.eql(u8, &expected, result));
}
pub const Extensions = struct {
const Self = @This();
extensions: std.ArrayList(Extension),
pub fn init(allocator: std.mem.Allocator) Self {
return Self{
.extensions = std.ArrayList(Extension).init(allocator),
};
}
/// This type has the responsibility to free all the extensions space that it holds inside.
pub fn deinit(self: Self) void {
for (self.extensions.items) |ext| {
ext.deinit();
}
self.extensions.deinit();
}
pub fn add_ext(self: *Self, ext: Extension) !void {
try self.extensions.append(ext);
}
pub fn encode(self: Extensions, out_stream: anytype) !void {
var length: usize = 0;
for (self.extensions.items) |ext| {
length += ext.encodedSize();
}
try out_stream.writeIntBig(u16, @intCast(u16, length));
for (self.extensions.items) |ext| {
try ext.encode(out_stream);
}
}
pub fn encodedSize(self: Extensions) usize {
const size_length = 2;
var data_length: usize = 0;
for (self.extensions.items) |ext| {
data_length += ext.encodedSize();
}
return size_length + data_length;
}
pub fn decode(allocator: std.mem.Allocator, in_stream: anytype) !Self {
const length = try in_stream.readIntBig(u16);
var exts = std.ArrayList(Extension).init(allocator);
errdefer exts.deinit();
var i: usize = 0;
while (i < @intCast(usize, length)) {
const res = try Extension.decode(allocator, in_stream);
i += res.bytes_read;
try exts.append(res.decoded);
}
return Self{ .extensions = exts };
}
};
test "Extensions properly encoded" {
var extensions_data = std.ArrayList(Extension).init(std.testing.allocator);
var versions = std.ArrayList(u16).init(std.testing.allocator);
try versions.append(SupportedVersions.TLS_1_3);
const supported_versions = SupportedVersions{ .versions = versions };
const ext = Extension{ .supported_versions = supported_versions };
try extensions_data.append(ext);
const extensions = Extensions{ .extensions = extensions_data };
defer extensions.deinit();
var out_buf: [1024]u8 = undefined;
var slice_stream = std.io.fixedBufferStream(&out_buf);
const out = slice_stream.writer();
try extensions.encode(out);
const result = slice_stream.getWritten();
const expected = [_]u8{ 0x00, 0x07, 0x00, 0x2b, 0x00, 0x03, 0x02, 0x03, 0x04 };
try std.testing.expect(std.mem.eql(u8, &expected, result));
}
const std = @import("std");
const ClientHello = @import("./client_hello.zig").ClientHello;
const ServerHello = @import("./server_hello.zig").ServerHello;
// unimplemented yet
const NewSessionTicket = struct {};
const EndOfEarlyData = struct {};
const EncryptedExtensions = struct {};
const Certificate = struct {};
const CertificateRequest = struct {};
const CertificateVerify = struct {};
const Finished = struct {};
const KeyUpdate = struct {};
const MessageHash = struct {};
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.3
pub const HandshakeType = enum(u8) {
client_hello = 1,
server_hello = 2,
new_session_ticket = 4,
end_of_early_data = 5,
encrypted_extensions = 8,
certificate = 11,
certificate_request = 13,
certificate_verify = 15,
finished = 20,
key_update = 24,
message_hash = 254,
};
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.3
pub const Handshake = union(HandshakeType) {
const Self = @This();
client_hello: ClientHello,
server_hello: ServerHello,
new_session_ticket: NewSessionTicket,
end_of_early_data: EndOfEarlyData,
encrypted_extensions: EncryptedExtensions,
certificate: Certificate,
certificate_request: CertificateRequest,
certificate_verify: CertificateVerify,
finished: Finished,
key_update: KeyUpdate,
message_hash: MessageHash,
pub fn encode(self: Self, out_stream: anytype) !void {
// msg_type
try out_stream.writeIntBig(u8, @enumToInt(self));
switch (self) {
.client_hello => |client_hello| {
// length
try out_stream.writeIntBig(u24, @intCast(u24, client_hello.encodedSize()));
// data
try client_hello.encode(out_stream);
},
else => {
// TODO: unimplemented yet
},
}
}
pub fn encodedSize(self: Self) usize {
const msg_type_length = 1;
const length_length = 3;
const data_length = switch (self) {
.client_hello => |client_hello| blk: {
break :blk client_hello.encodedSize();
},
else => 0, // TODO: unimplemented yet
};
return msg_type_length + length_length + data_length;
}
pub fn decode(allocator: std.mem.Allocator, in_stream: anytype) !Self {
const handshake_type = try in_stream.readByte();
return switch (handshake_type) {
@enumToInt(HandshakeType.server_hello) => blk: {
// skip handshake length
try in_stream.skipBytes(3, .{});
const hello = try ServerHello.decode(allocator, in_stream);
break :blk .{ .server_hello = hello };
},
else => unreachable, // TODO: unimplemented yet
};
}
pub fn deinit(self: Self) void {
switch (self) {
.client_hello => |client_hello| client_hello.deinit(),
.server_hello => |server_hello| server_hello.deinit(),
else => unreachable, // TODO: unimplemented yet
}
}
};
test "Handshake of ClientHello encoded properly" {
const allocator = std.testing.allocator;
const hello = try ClientHello.init(allocator);
defer hello.deinit();
const handshake = Handshake{ .client_hello = hello };
try std.testing.expectEqual(@as(usize, 113), handshake.encodedSize());
var out_buf: [1024]u8 = undefined;
var slice_stream = std.io.fixedBufferStream(&out_buf);
const out = slice_stream.writer();
try handshake.encode(out);
const result = slice_stream.getWritten();
const expected = blk: {
const b_msg_type = [_]u8{0x01};
const b_length = [_]u8{ 0x00, 0x00, 0x6d }; // 109 in decimal
const b_legacy_protocol_version = [_]u8{ 0x03, 0x03 };
const b_random = [_]u8{0x00} ** 32;
const b_legacy_session_id = [_]u8{ 0x01, 0x00 };
const b_cipher_suites = [_]u8{ 0x00, 0x02, 0x13, 0x01 };
const b_legacy_compression_methods = [_]u8{ 0x01, 0x00 };
const b_ext_length = [_]u8{ 0x00, 0x41 }; // 65 in decimal
const b_ext_supported_versions = [_]u8{ 0x00, 0x2b, 0x00, 0x03, 0x02, 0x03, 0x04 };
const b_ext_signature_algorithms = [_]u8{ 0x00, 0x0d, 0x00, 0x04, 0x00, 0x02, 0x08, 0x04 };
const b_ext_key_shares = key_blk: {
const extension_type = [_]u8{ 0x00, 0x33 };
const data_length = [_]u8{ 0x00, 0x26 }; // 38 in decimal
const entries_length = [_]u8{ 0x00, 0x24 }; // 36 in decimal
const group = [_]u8{ 0x00, 0x1d };
const length = [_]u8{ 0x00, 0x20 }; // 32 in decimal
const data = @import("./key_exchange.zig").key_exchange;
break :key_blk extension_type ++
data_length ++
entries_length ++
group ++
length ++
data;
};
const b_ext_supported_groups = [_]u8{ 0x00, 0x0a, 0x00, 0x04, 0x00, 0x02, 0x00, 0x1d };
break :blk b_msg_type ++
b_length ++
b_legacy_protocol_version ++
b_random ++
b_legacy_session_id ++
b_cipher_suites ++
b_legacy_compression_methods ++
b_ext_length ++
b_ext_supported_versions ++
b_ext_signature_algorithms ++
b_ext_key_shares ++
b_ext_supported_groups;
};
try std.testing.expect(std.mem.eql(u8, &expected, result));
}
// Brought from https://datatracker.ietf.org/doc/html/rfc8448#section-3
//
// {client} create an ephemeral x25519 key pair
// public key (32 octets):
// 99 38 1d e5 60 e4 bd 43 d2 3d 8e 43 5a 7d ba fe
// b3 c0 6e 51 c1 3c ae 4d 54 13 69 1e 52 9a af 2c
pub const key_exchange = [_]u8{
0x99,
0x38,
0x1d,
0xe5,
0x60,
0xe4,
0xbd,
0x43,
0xd2,
0x3d,
0x8e,
0x43,
0x5a,
0x7d,
0xba,
0xfe,
0xb3,
0xc0,
0x6e,
0x51,
0xc1,
0x3c,
0xae,
0x4d,
0x54,
0x13,
0x69,
0x1e,
0x52,
0x9a,
0xaf,
0x2c,
};
const std = @import("std");
const NamedGroup = @import("./named_group.zig").NamedGroup;
// unimplemented yet
pub const Reserved = struct {};
pub const Secp256r1 = struct {};
pub const Secp384r1 = struct {};
pub const Secp521r1 = struct {};
pub const X448 = struct {};
pub const Ffdhe2048 = struct {};
pub const Ffdhe3072 = struct {};
pub const Ffdhe4096 = struct {};
pub const Ffdhe6144 = struct {};
pub const Ffdhe8192 = struct {};
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.3.1
pub const KeyShareEntry = union(NamedGroup) {
const Self = @This();
unallocated_RESERVED: Reserved,
obsolete_RESERVED_01: Reserved,
obsolete_RESERVED_02: Reserved,
obsolete_RESERVED_03: Reserved,
obsolete_RESERVED_04: Reserved,
obsolete_RESERVED_05: Reserved,
obsolete_RESERVED_06: Reserved,
obsolete_RESERVED_07: Reserved,
obsolete_RESERVED_08: Reserved,
obsolete_RESERVED_09: Reserved,
obsolete_RESERVED_0a: Reserved,
obsolete_RESERVED_0b: Reserved,
obsolete_RESERVED_0c: Reserved,
obsolete_RESERVED_0d: Reserved,
obsolete_RESERVED_0e: Reserved,
obsolete_RESERVED_0f: Reserved,
obsolete_RESERVED_10: Reserved,
obsolete_RESERVED_11: Reserved,
obsolete_RESERVED_12: Reserved,
obsolete_RESERVED_13: Reserved,
obsolete_RESERVED_14: Reserved,
obsolete_RESERVED_15: Reserved,
obsolete_RESERVED_16: Reserved,
secp256r1: Secp256r1,
secp384r1: Secp384r1,
secp521r1: Secp521r1,
obsolete_RESERVED_1a: Reserved,
obsolete_RESERVED_1b: Reserved,
obsolete_RESERVED_1c: Reserved,
x25519: [32]u8,
x448: X448,
ffdhe2048: Ffdhe2048,
ffdhe3072: Ffdhe3072,
ffdhe4096: Ffdhe4096,
ffdhe6144: Ffdhe6144,
ffdhe8192: Ffdhe8192,
obsolete_RESERVED_ff01: Reserved,
obsolete_RESERVED_ff02: Reserved,
pub fn encode(self: Self, out_stream: anytype) !void {
// group
try out_stream.writeIntBig(u16, @enumToInt(self));
switch (self) {
.x25519 => |x25519| {
// length
try out_stream.writeIntBig(u16, 32);
// data
try out_stream.writeAll(&x25519);
},
else => {
// TODO: unimplemented yet
},
}
}
pub fn encodedSize(self: Self) usize {
const group_length = @sizeOf(@typeInfo(NamedGroup).Enum.tag_type);
const key_exchange_length = switch (self) {
.x25519 => blk: {
const length_length = 2;
const data_length = 32;
break :blk length_length + data_length;
},
else => @as(usize, 0), // TODO: unimplemented yet
};
return group_length + key_exchange_length;
}
const DecodeReturnType = struct {
decoded: Self,
bytes_read: usize,
};
pub fn decode(in_stream: anytype) !DecodeReturnType {
const length = try in_stream.readIntBig(u16);
const key_share_group = @intToEnum(NamedGroup, try in_stream.readIntBig(u16));
// unused for the moment, but should be used in the future
_ = try in_stream.readIntBig(u16);
const decoded = switch (key_share_group) {
.x25519 => blk: {
const key = try in_stream.readBytesNoEof(32);
break :blk Self{ .x25519 = key };
},
else => unreachable, // TODO: unimplemented yet
};
return DecodeReturnType{
.decoded = decoded,
.bytes_read = 2 + length,
};
}
};
test "KeyShareEntry of X25519 properly encoded" {
const key_share = KeyShareEntry{ .x25519 = [_]u8{0x00} ** 32 };
try std.testing.expectEqual(@as(usize, 36), key_share.encodedSize());
var out_buf: [1024]u8 = undefined;
var slice_stream = std.io.fixedBufferStream(&out_buf);
const out = slice_stream.writer();
try key_share.encode(out);
const result = slice_stream.getWritten();
const expected = blk: {
const group = [_]u8{ 0x00, 0x1d };
const length = [_]u8{ 0x00, 0x20 }; // 32 in decimal
const data = [_]u8{0x00} ** 32;
break :blk group ++ length ++ data;
};
try std.testing.expect(std.mem.eql(u8, &expected, result));
}
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.3.1
pub const KeyShare = union(enum) {
client: KeyShareClientHello,
server: KeyShareEntry,
const Self = @This();
pub fn deinit(self: Self) void {
switch (self) {
.client => |client| client.deinit(),
.server => {}, // KeyShareEntry doesn't need to deinit
}
}
pub fn encode(self: Self, out_stream: anytype) !void {
switch (self) {
.client => |client| try client.encode(out_stream),
.server => |server| try server.encode(out_stream),
}
}
pub fn encodedSize(self: Self) usize {
return switch (self) {
.client => |client| client.encodedSize(),
.server => |server| server.encodedSize(),
};
}
const DecodeReturnType = struct {
decoded: Self,
bytes_read: usize,
};
pub fn decode_server(in_stream: anytype) !DecodeReturnType {
const res = try KeyShareEntry.decode(in_stream);
return DecodeReturnType{
.decoded = .{ .server = res.decoded },
.bytes_read = res.bytes_read,
};
}
};
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.3.1
pub const KeyShareClientHello = struct {
const Self = @This();
client_shares: std.ArrayList(KeyShareEntry),
pub fn init(allocator: std.mem.Allocator) Self {
return Self{
.client_shares = std.ArrayList(KeyShareEntry).init(allocator),
};
}
pub fn deinit(self: Self) void {
self.client_shares.deinit();
}
pub fn add_key_share_entry(self: *Self, key_share_entry: KeyShareEntry) !void {
try self.client_shares.append(key_share_entry);
}
pub fn encode(self: Self, out_stream: anytype) !void {
var data_length: usize = 0;
for (self.client_shares.items) |s| {
data_length += s.encodedSize();
}
try out_stream.writeIntBig(u16, @intCast(u16, data_length));
for (self.client_shares.items) |s| {
try s.encode(out_stream);
}
}
pub fn encodedSize(self: Self) usize {
const length_length = 2;
var data_length: usize = 0;
for (self.client_shares.items) |s| {
data_length += s.encodedSize();
}
return length_length + data_length;
}
};
test "KeyShareClientHello properly encoded" {
const allocator = std.testing.allocator;
const key_share = KeyShareEntry{ .x25519 = [_]u8{0x00} ** 32 };
var key_share_clinet_hello = KeyShareClientHello.init(allocator);
defer key_share_clinet_hello.deinit();
try key_share_clinet_hello.add_key_share_entry(key_share);
var out_buf: [1024]u8 = undefined;
var slice_stream = std.io.fixedBufferStream(&out_buf);
const out = slice_stream.writer();
try key_share_clinet_hello.encode(out);
const result = slice_stream.getWritten();
const expected = blk: {
const entries_length = [_]u8{ 0x00, 0x24 }; // 36 in decimal
const group = [_]u8{ 0x00, 0x1d };
const length = [_]u8{ 0x00, 0x20 }; // 32 in decimal
const data = [_]u8{0x00} ** 32;
break :blk entries_length ++ group ++ length ++ data;
};
try std.testing.expect(std.mem.eql(u8, &expected, result));
}
//! ref: https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2
const legacy_compression_methods: u8 = 0; // always set to 0 in TLS v1.3
pub fn encode(out_stream: anytype) !void {
const length = 1;
try out_stream.writeIntBig(u8, length);
try out_stream.writeIntBig(u8, legacy_compression_methods);
}
pub fn encodedSize() usize {
const length = 1;
const data_length = @sizeOf(u8);
return length + data_length;
}
//! ref: https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2
const legacy_session_id: u8 = 0; // always set to 0 in TLS v1.3
pub fn encode(out_stream: anytype) !void {
const length = 1;
try out_stream.writeIntBig(u8, length);
try out_stream.writeIntBig(u8, legacy_session_id);
}
pub fn encodedSize() usize {
const length = 1;
const data_length = @sizeOf(u8);
return length + data_length;
}
const std = @import("std");
const net = std.net;
const log = std.log;
const ClientHello = @import("./client_hello.zig").ClientHello;
const TLSPlaintext = @import("./tls_plaintext.zig").TLSPlaintext;
const Handshake = @import("./handshake.zig").Handshake;
const Buffer = std.ArrayList(u8);
/// This application attempts
/// - to connect to `127.0.0.1:10443` with TCP
/// - to send TLC v1.3 ClientHello message
/// - to receive and decode ServerHello message
pub fn main() void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
const addr = net.Address.parseIp4("127.0.0.1", 10443) catch unreachable;
log.info("Establish TCP connection to {}\n", .{addr});
const stream = net.tcpConnectToAddress(addr) catch |err| {
log.err("Failed to connect to {} due to {}\n", .{ addr, err });
std.process.exit(1);
};
defer stream.close();
const client_hello = ClientHello.init(allocator) catch |err| {
log.err("Failed to create ClientHello data due to {}\n", .{err});
std.process.exit(1);
};
defer client_hello.deinit();
// encode ClientHello
const message = TLSPlaintext{ .handshake = .{ .client_hello = client_hello } };
log.info("Send ClientHello: {s}\n", .{message});
var write_buf = Buffer.init(allocator);
defer write_buf.deinit();
message.encode(write_buf.writer()) catch |err| {
log.err("Failed to encode ClientHello due to {}\n", .{err});
std.process.exit(1);
};
// send ClientHello
stream.writer().writeAll(write_buf.items) catch |err| {
log.err("Failed to send ClientHello due to {}\n", .{err});
std.process.exit(1);
};
write_buf.clearAndFree();
// receive and decode ServerHello
const reader = stream.reader();
const server_hello = TLSPlaintext.decode(allocator, reader) catch |err| {
log.err("Failed to receive or decode ServerHello due to {}\n", .{err});
std.process.exit(1);
};
log.info("Received ServerHello: {s}\n", .{server_hello});
}
test {
std.testing.refAllDecls(@This());
}
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.3.1.4
pub const NamedGroup = enum(u16) {
unallocated_RESERVED = 0x0000,
// Elliptic Curve Groups (ECDHE)
obsolete_RESERVED_01 = 0x0001,
obsolete_RESERVED_02 = 0x0002,
obsolete_RESERVED_03 = 0x0003,
obsolete_RESERVED_04 = 0x0004,
obsolete_RESERVED_05 = 0x0005,
obsolete_RESERVED_06 = 0x0006,
obsolete_RESERVED_07 = 0x0007,
obsolete_RESERVED_08 = 0x0008,
obsolete_RESERVED_09 = 0x0009,
obsolete_RESERVED_0a = 0x000a,
obsolete_RESERVED_0b = 0x000b,
obsolete_RESERVED_0c = 0x000c,
obsolete_RESERVED_0d = 0x000d,
obsolete_RESERVED_0e = 0x000e,
obsolete_RESERVED_0f = 0x000f,
obsolete_RESERVED_10 = 0x0010,
obsolete_RESERVED_11 = 0x0011,
obsolete_RESERVED_12 = 0x0012,
obsolete_RESERVED_13 = 0x0013,
obsolete_RESERVED_14 = 0x0014,
obsolete_RESERVED_15 = 0x0015,
obsolete_RESERVED_16 = 0x0016,
secp256r1 = 0x0017,
secp384r1 = 0x0018,
secp521r1 = 0x0019,
obsolete_RESERVED_1a = 0x001a,
obsolete_RESERVED_1b = 0x001b,
obsolete_RESERVED_1c = 0x001c,
x25519 = 0x001d,
x448 = 0x001e,
// Finite Field Groups (DHE)
ffdhe2048 = 0x0100,
ffdhe3072 = 0x0101,
ffdhe4096 = 0x0102,
ffdhe6144 = 0x0103,
ffdhe8192 = 0x0104,
// Reserved Code Points
// ffdhe_private_use(0x01FC..0x01FF),
// ecdhe_private_use(0xFE00..0xFEFF),
obsolete_RESERVED_ff01 = 0xff01,
obsolete_RESERVED_ff02 = 0xff02,
};
const std = @import("std");
const CipherSuite = @import("./cipher_suite.zig").CipherSuite;
const ext = @import("./extension.zig");
const Extensions = ext.Extensions;
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.3.1
pub const ServerHello = struct {
const Self = @This();
legacy_protocol_version: u16,
random: [32]u8,
legacy_session_id_echo: LegacySessionIdEcho,
cipher_suite: CipherSuite,
legacy_compression_method: u8,
extensions: Extensions,
pub fn decode(allocator: std.mem.Allocator, in_stream: anytype) !Self {
const proto_ver = try in_stream.readIntBig(u16);
const rand = try in_stream.readBytesNoEof(32);
const session_id_echo = try LegacySessionIdEcho.decode(allocator, in_stream);
const suite = try in_stream.readBytesNoEof(2);
const compression_method = try in_stream.readIntBig(u8);
const exts = try Extensions.decode(allocator, in_stream);
return Self{
.legacy_protocol_version = proto_ver,
.random = rand,
.legacy_session_id_echo = session_id_echo,
.cipher_suite = suite,
.legacy_compression_method = compression_method,
.extensions = exts,
};
}
pub fn deinit(self: Self) void {
self.legacy_session_id_echo.deinit();
self.extensions.deinit();
}
};
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.3
pub const LegacySessionIdEcho = struct {
const Self = @This();
session_id: std.ArrayList(u8),
pub fn init(allocator: std.mem.Allocator) Self {
return Self{
.session_id = std.ArrayList(u8).init(allocator),
};
}
pub fn deinit(self: Self) void {
self.session_id.deinit();
}
pub fn decode(allocator: std.mem.Allocator, in_stream: anytype) !Self {
const length = try in_stream.readIntBig(u8);
var data = try std.ArrayList(u8).initCapacity(allocator, @intCast(usize, length));
errdefer data.deinit();
var i: usize = 0;
while (i < length) : (i += 1) {
const b = try in_stream.readByte();
try data.append(b);
}
return Self{ .session_id = data };
}
};
const std = @import("std");
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.3.1.3
pub const SignatureScheme = enum(u16) {
// RSASSA-PKCS1-v1_5 algorithms
rsa_pkcs1_sha256 = 0x0401,
rsa_pkcs1_sha384 = 0x0501,
rsa_pkcs1_sha512 = 0x0601,
// ECDSA algorithms ecdsa_secp256r1_sha256(0x0403),
ecdsa_secp384r1_sha384 = 0x0503,
ecdsa_secp521r1_sha512 = 0x0603,
// RSASSA-PSS algorithms with public key OID rsaEncryption
rsa_pss_rsae_sha256 = 0x0804,
rsa_pss_rsae_sha384 = 0x0805,
rsa_pss_rsae_sha512 = 0x0806,
// EdDSA algorithms
ed25519 = 0x0807,
ed448 = 0x0808,
// RSASSA-PSS algorithms with public key OID RSASSA-PSS
rsa_pss_pss_sha256 = 0x0809,
rsa_pss_pss_sha384 = 0x080a,
rsa_pss_pss_sha512 = 0x080b,
// Legacy algorithms
rsa_pkcs1_sha1 = 0x0201,
ecdsa_sha1 = 0x0203,
};
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.3.1.3
pub const SignatureSchemeList = struct {
const Self = @This();
scheme_list: std.ArrayList(SignatureScheme),
pub fn init(allocator: std.mem.Allocator) Self {
return Self{
.scheme_list = std.ArrayList(SignatureScheme).init(allocator),
};
}
pub fn deinit(self: Self) void {
self.scheme_list.deinit();
}
pub fn add_signature_scheme(self: *Self, signature_scheme: SignatureScheme) !void {
try self.scheme_list.append(signature_scheme);
}
pub fn encode(self: Self, out_stream: anytype) !void {
const data_length = self.scheme_list.items.len * @sizeOf(SignatureScheme);
try out_stream.writeIntBig(u16, @intCast(u16, data_length));
for (self.scheme_list.items) |scheme| {
try out_stream.writeIntBig(u16, @enumToInt(scheme));
}
}
pub fn encodedSize(self: Self) usize {
const length_length = 2;
const data_length = self.scheme_list.items.len * @sizeOf(SignatureScheme);
return length_length + data_length;
}
};
test "SignatureSchemeList properly encoded" {
const allocator = std.testing.allocator;
var signature_scheme_list = SignatureSchemeList.init(allocator);
defer signature_scheme_list.deinit();
try signature_scheme_list.add_signature_scheme(SignatureScheme.rsa_pss_rsae_sha256);
try signature_scheme_list.add_signature_scheme(SignatureScheme.rsa_pss_rsae_sha512);
var out_buf: [1024]u8 = undefined;
var slice_stream = std.io.fixedBufferStream(&out_buf);
const out = slice_stream.writer();
try signature_scheme_list.encode(out);
const result = slice_stream.getWritten();
const expected = [_]u8{ 0x00, 0x04, 0x08, 0x04, 0x08, 0x06 };
try std.testing.expect(std.mem.eql(u8, &expected, result));
}
const std = @import("std");
const NamedGroup = @import("./named_group.zig").NamedGroup;
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.3.1.4
pub const SupportedGroups = struct {
const Self = @This();
groups: std.ArrayList(NamedGroup),
pub fn init(allocator: std.mem.Allocator) Self {
return Self{
.groups = std.ArrayList(NamedGroup).init(allocator),
};
}
pub fn deinit(self: Self) void {
self.groups.deinit();
}
pub fn add_group(self: *Self, group: NamedGroup) !void {
try self.groups.append(group);
}
pub fn encode(self: Self, out_stream: anytype) !void {
const data_length = self.groups.items.len * @sizeOf(@typeInfo(NamedGroup).Enum.tag_type);
try out_stream.writeIntBig(u16, @intCast(u16, data_length));
for (self.groups.items) |group| {
try out_stream.writeIntBig(u16, @enumToInt(group));
}
}
pub fn encodedSize(self: Self) usize {
const length_length = 2;
const data_length = self.groups.items.len * @sizeOf(@typeInfo(NamedGroup).Enum.tag_type);
return length_length + data_length;
}
};
test "SupportedGroups properly encoded" {
const allocator = std.testing.allocator;
var groups = SupportedGroups.init(allocator);
defer groups.deinit();
try groups.add_group(NamedGroup.x25519);
try groups.add_group(NamedGroup.ffdhe2048);
try std.testing.expectEqual(@as(usize, 6), groups.encodedSize());
var out_buf: [1024]u8 = undefined;
var slice_stream = std.io.fixedBufferStream(&out_buf);
const out = slice_stream.writer();
try groups.encode(out);
const result = slice_stream.getWritten();
const expected = [_]u8{ 0x00, 0x04, 0x00, 0x1d, 0x01, 0x00 };
try std.testing.expect(std.mem.eql(u8, &expected, result));
}
const std = @import("std");
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.3.1.1
pub const SupportedVersions = struct {
const Self = @This();
pub const TLS_1_3: u16 = 0x0304;
versions: std.ArrayList(u16),
pub fn init(allocator: std.mem.Allocator) Self {
return Self{
.versions = std.ArrayList(u16).init(allocator),
};
}
pub fn deinit(self: Self) void {
self.versions.deinit();
}
pub fn add_version(self: *Self, version: u16) !void {
try self.versions.append(version);
}
pub fn encode(self: Self, out_stream: anytype) !void {
const data_length = self.versions.items.len * @sizeOf(u16);
try out_stream.writeIntBig(u8, @intCast(u8, data_length));
for (self.versions.items) |ver| {
try out_stream.writeIntBig(u16, ver);
}
}
pub fn encodedSize(self: Self) usize {
const data_length_length = 1;
const data_length = self.versions.items.len * @sizeOf(u16);
return data_length_length + data_length;
}
const DecodeReturnType = struct {
decoded: Self,
bytes_read: usize,
};
pub fn decode(allocator: std.mem.Allocator, in_stream: anytype) !DecodeReturnType {
const length = try in_stream.readIntBig(u16);
var versions = try std.ArrayList(u16).initCapacity(allocator, @intCast(usize, length) / 2);
errdefer versions.deinit();
var i: usize = 0;
while (i < length) : (i += 2) {
const v = try in_stream.readIntBig(u16);
try versions.append(v);
}
return DecodeReturnType{
.decoded = .{ .versions = versions },
.bytes_read = length + 2,
};
}
};
test "SupportedVersions properly encoded" {
const allocator = std.testing.allocator;
var versions = std.ArrayList(u16).init(allocator);
try versions.append(SupportedVersions.TLS_1_3);
const supported_versions = SupportedVersions{ .versions = versions };
defer supported_versions.deinit();
try std.testing.expectEqual(@as(usize, 3), supported_versions.encodedSize());
var out_buf: [1024]u8 = undefined;
var slice_stream = std.io.fixedBufferStream(&out_buf);
const out = slice_stream.writer();
try supported_versions.encode(out);
const result = slice_stream.getWritten();
const expected = [_]u8{ 0x02, 0x03, 0x04 };
try std.testing.expect(std.mem.eql(u8, &expected, result));
}
const std = @import("std");
const Handshake = @import("./handshake.zig").Handshake;
// unimplemented yet
const Invalid = struct {};
const ChangeCipherSpec = struct {};
const Alert = struct {};
const ApplicationData = struct {};
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.1
pub const ContentType = enum(u8) {
invalid = 0,
change_cipher_spec = 20,
alert = 21,
handshake = 22,
application_data = 23,
};
/// ref: https://datatracker.ietf.org/doc/html/rfc8446#appendix-B.1
pub const TLSPlaintext = union(ContentType) {
invalid: Invalid,
change_cipher_spec: ChangeCipherSpec,
alert: Alert,
handshake: Handshake,
application_data: ApplicationData,
const Self = @This();
const legacy_record_version: u16 = 0x0301; // TLS v1.0 for compatibility reason
pub fn encode(self: Self, out_stream: anytype) !void {
// type
try out_stream.writeIntBig(u8, @enumToInt(self));
// legacy_record_version
try out_stream.writeIntBig(u16, Self.legacy_record_version);
// length + fragment
switch (self) {
.handshake => |handshake| {
// length
try out_stream.writeIntBig(u16, @intCast(u16, handshake.encodedSize()));
// fragment
try handshake.encode(out_stream);
},
else => {
// TODO: unimplemented yet
},
}
}
pub fn decode(allocator: std.mem.Allocator, in_stream: anytype) !Self {
const content_type = try in_stream.readByte();
return switch (content_type) {
@enumToInt(ContentType.handshake) => blk: {
// skip protocol version and record length
try in_stream.skipBytes(4, .{});
const handshake = try Handshake.decode(allocator, in_stream);
break :blk .{ .handshake = handshake };
},
else => unreachable, // TODO: unimplemented yet
};
}
pub fn deinit(self: Self) void {
switch (self) {
.handshake => |handshake| handshake.deinit(),
else => unreachable, // TODO: unimplemented yet
}
}
};
test "TLSPlaintext encoded properly for ClientHello" {
const ClientHello = @import("./client_hello.zig").ClientHello;
const allocator = std.testing.allocator;
const hello = try ClientHello.init(allocator);
defer hello.deinit();
const handshake = Handshake{ .client_hello = hello };
const plaintext = TLSPlaintext{ .handshake = handshake };
var out_buf: [1024]u8 = undefined;
var slice_stream = std.io.fixedBufferStream(&out_buf);
const out = slice_stream.writer();
try plaintext.encode(out);
const result = slice_stream.getWritten();
const expected = blk: {
const b_content_type = [_]u8{0x16};
const b_legacy_record_version = [_]u8{ 0x03, 0x01 };
const b_fragment_length = [_]u8{ 0x00, 0x71 }; // 113 in decimal
const b_msg_type = [_]u8{0x01};
const b_handshake_length = [_]u8{ 0x00, 0x00, 0x6d }; // 109 in decimal
const b_legacy_protocol_version = [_]u8{ 0x03, 0x03 };
const b_random = [_]u8{0x00} ** 32;
const b_legacy_session_id = [_]u8{ 0x01, 0x00 };
const b_cipher_suites = [_]u8{ 0x00, 0x02, 0x13, 0x01 };
const b_legacy_compression_methods = [_]u8{ 0x01, 0x00 };
const b_ext_length = [_]u8{ 0x00, 0x41 }; // 65 in decimal
const b_ext_supported_versions = [_]u8{ 0x00, 0x2b, 0x00, 0x03, 0x02, 0x03, 0x04 };
const b_ext_signature_algorithms = [_]u8{ 0x00, 0x0d, 0x00, 0x04, 0x00, 0x02, 0x08, 0x04 };
const b_ext_key_shares = key_blk: {
const extension_type = [_]u8{ 0x00, 0x33 };
const data_length = [_]u8{ 0x00, 0x26 }; // 38 in decimal
const entries_length = [_]u8{ 0x00, 0x24 }; // 36 in decimal
const group = [_]u8{ 0x00, 0x1d };
const length = [_]u8{ 0x00, 0x20 }; // 32 in decimal
const data = @import("./key_exchange.zig").key_exchange;
break :key_blk extension_type ++
data_length ++
entries_length ++
group ++
length ++
data;
};
const b_ext_supported_groups = [_]u8{ 0x00, 0x0a, 0x00, 0x04, 0x00, 0x02, 0x00, 0x1d };
break :blk b_content_type ++
b_legacy_record_version ++
b_fragment_length ++
b_msg_type ++
b_handshake_length ++
b_legacy_protocol_version ++
b_random ++
b_legacy_session_id ++
b_cipher_suites ++
b_legacy_compression_methods ++
b_ext_length ++
b_ext_supported_versions ++
b_ext_signature_algorithms ++
b_ext_key_shares ++
b_ext_supported_groups;
};
std.debug.print("\n", .{});
std.debug.print("expected: {}\n", .{std.fmt.fmtSliceHexLower(&expected)});
std.debug.print("actual : {}\n", .{std.fmt.fmtSliceHexLower(result)});
try std.testing.expect(std.mem.eql(u8, &expected, result));
}
test "TLSPlaintext decoded properly for ServerHello" {
const HandshakeType = @import("./handshake.zig").HandshakeType;
const ExtensionType = @import("./extension.zig").ExtensionType;
const NamedGroup = @import("./named_group.zig").NamedGroup;
// Brought from actual ServerHello coming from `openssl s_server`
const sample = [_]u8{
0x16, 0x03, 0x03, 0x00, 0x5b, 0x02, 0x00, 0x00, 0x57, 0x03, 0x03, 0xca, 0xdb, 0xdf, 0x8e, 0xa3,
0xbd, 0x18, 0x5a, 0xf3, 0x61, 0x83, 0xa8, 0x8c, 0x40, 0xcb, 0xa7, 0x03, 0x40, 0x60, 0x63, 0xf2,
0xc2, 0x6b, 0xa2, 0x34, 0x76, 0x53, 0x6b, 0xfb, 0x77, 0x14, 0x36, 0x01, 0x00, 0x13, 0x01, 0x00,
0x00, 0x2e, 0x00, 0x2b, 0x00, 0x02, 0x03, 0x04, 0x00, 0x33, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20,
0xd3, 0x2c, 0xea, 0x53, 0x4a, 0xbc, 0x1d, 0xbe, 0xbb, 0x9f, 0x91, 0xd7, 0xc7, 0xe9, 0x39, 0xe0,
0x5b, 0x51, 0x4e, 0xff, 0xf6, 0x5c, 0x1e, 0x98, 0x71, 0x38, 0xfd, 0x57, 0x33, 0x95, 0x84, 0x10,
};
var stream = std.io.fixedBufferStream(&sample);
const decoded = try TLSPlaintext.decode(std.testing.allocator, stream.reader());
defer decoded.deinit();
try std.testing.expectEqual(ContentType.handshake, @as(ContentType, decoded));
switch (decoded) {
.handshake => |handshake| {
try std.testing.expectEqual(HandshakeType.server_hello, @as(HandshakeType, handshake));
switch (handshake) {
.server_hello => |server_hello| {
try std.testing.expectEqual(@as(u16, 0x0303), server_hello.legacy_protocol_version);
const session_id = server_hello.legacy_session_id_echo.session_id;
try std.testing.expectEqual(@as(usize, 1), session_id.items.len);
try std.testing.expectEqual(@as(u8, 0x00), session_id.items[0]);
try std.testing.expectEqual([_]u8{ 0x13, 0x01 }, server_hello.cipher_suite);
try std.testing.expectEqual(@as(u8, 0x00), server_hello.legacy_compression_method);
const exts = server_hello.extensions.extensions;
try std.testing.expectEqual(@as(usize, 2), exts.items.len);
const ext1 = exts.items[0];
try std.testing.expectEqual(ExtensionType.supported_versions, @as(ExtensionType, ext1));
switch (ext1) {
.supported_versions => |supported_versions| {
const versions = supported_versions.versions;
try std.testing.expectEqual(@as(usize, 1), versions.items.len);
try std.testing.expectEqual(@as(u16, 0x0304), versions.items[0]);
},
else => unreachable,
}
const ext2 = exts.items[1];
try std.testing.expectEqual(ExtensionType.key_share, @as(ExtensionType, ext2));
switch (ext2) {
.key_share => |key_share| {
try std.testing.expect(std.mem.eql(u8, "server", @tagName(key_share)));
switch (key_share) {
.server => |ent| {
try std.testing.expectEqual(NamedGroup.x25519, @as(NamedGroup, ent));
},
.client => unreachable,
}
},
else => unreachable,
}
},
else => unreachable,
}
},
else => unreachable,
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment