platform layer: address types, stdlib tls client wrapper, mbedtls tls server
This commit is contained in:
@@ -0,0 +1,411 @@
|
|||||||
|
//! IP address and prefix values for nxdns. Pure: no `std.Io` operations, only
|
||||||
|
//! conversions to and from `std.Io.net.IpAddress`.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const net = std.Io.net;
|
||||||
|
const Writer = std.Io.Writer;
|
||||||
|
|
||||||
|
pub const NetAddress = union(enum) {
|
||||||
|
ip4: [4]u8,
|
||||||
|
ip6: [16]u8,
|
||||||
|
|
||||||
|
/// Tag byte (4 or 6) followed by the address bytes, zero-padded for ip4.
|
||||||
|
pub const Key = [17]u8;
|
||||||
|
|
||||||
|
pub const ParseError = error{InvalidAddress};
|
||||||
|
|
||||||
|
/// Accepts "1.2.3.4" and "fd00::1". No port, no brackets, no scope id.
|
||||||
|
/// An IPv4-mapped IPv6 literal normalizes to `.ip4`.
|
||||||
|
pub fn parse(text: []const u8) ParseError!NetAddress {
|
||||||
|
const addr = net.IpAddress.parse(text, 0) catch return error.InvalidAddress;
|
||||||
|
return fromIp(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// IPv4 in dotted-decimal, IPv6 per RFC 5952.
|
||||||
|
pub fn format(self: NetAddress, w: *Writer) Writer.Error!void {
|
||||||
|
switch (self) {
|
||||||
|
.ip4 => |b| try w.print("{d}.{d}.{d}.{d}", .{ b[0], b[1], b[2], b[3] }),
|
||||||
|
.ip6 => |b| try formatIp6(b, w),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(self: NetAddress) Key {
|
||||||
|
var out: Key = @splat(0);
|
||||||
|
switch (self) {
|
||||||
|
.ip4 => |b| {
|
||||||
|
out[0] = 4;
|
||||||
|
@memcpy(out[1..5], &b);
|
||||||
|
},
|
||||||
|
.ip6 => |b| {
|
||||||
|
out[0] = 6;
|
||||||
|
@memcpy(out[1..17], &b);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drops the port. An IPv4-mapped IPv6 address (::ffff:a.b.c.d) yields `.ip4`.
|
||||||
|
pub fn fromIp(addr: net.IpAddress) NetAddress {
|
||||||
|
return switch (addr) {
|
||||||
|
.ip4 => |a| .{ .ip4 = a.bytes },
|
||||||
|
.ip6 => |a| if (net.Ip4Address.fromIp6(a)) |mapped|
|
||||||
|
.{ .ip4 = mapped.bytes }
|
||||||
|
else
|
||||||
|
.{ .ip6 = a.bytes },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toIp(self: NetAddress, port: u16) net.IpAddress {
|
||||||
|
return switch (self) {
|
||||||
|
.ip4 => |b| .{ .ip4 = .{ .bytes = b, .port = port } },
|
||||||
|
.ip6 => |b| .{ .ip6 = .{ .bytes = b, .port = port } },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eql(a: NetAddress, b: NetAddress) bool {
|
||||||
|
return switch (a) {
|
||||||
|
.ip4 => |x| switch (b) {
|
||||||
|
.ip4 => |y| std.mem.eql(u8, &x, &y),
|
||||||
|
.ip6 => false,
|
||||||
|
},
|
||||||
|
.ip6 => |x| switch (b) {
|
||||||
|
.ip4 => false,
|
||||||
|
.ip6 => |y| std.mem.eql(u8, &x, &y),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes(self: *const NetAddress) []const u8 {
|
||||||
|
return switch (self.*) {
|
||||||
|
.ip4 => |*b| b,
|
||||||
|
.ip6 => |*b| b,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn familyBits(self: NetAddress) u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.ip4 => 32,
|
||||||
|
.ip6 => 128,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn formatIp6(addr: [16]u8, w: *Writer) Writer.Error!void {
|
||||||
|
var parts: [8]u16 = undefined;
|
||||||
|
for (&parts, 0..) |*part, i| {
|
||||||
|
part.* = std.mem.readInt(u16, addr[i * 2 ..][0..2], .big);
|
||||||
|
}
|
||||||
|
|
||||||
|
// RFC 5952 4.2: compress the longest run of zero groups, earliest run on a
|
||||||
|
// tie, and never a run of only one group.
|
||||||
|
var run_start: usize = 0;
|
||||||
|
var run_len: usize = 0;
|
||||||
|
var current_start: usize = 0;
|
||||||
|
var current_len: usize = 0;
|
||||||
|
for (parts, 0..) |part, i| {
|
||||||
|
if (part != 0) {
|
||||||
|
current_len = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (current_len == 0) current_start = i;
|
||||||
|
current_len += 1;
|
||||||
|
if (current_len > run_len) {
|
||||||
|
run_start = current_start;
|
||||||
|
run_len = current_len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (run_len < 2) run_len = 0;
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
var at_start = true;
|
||||||
|
while (i < parts.len) {
|
||||||
|
if (run_len != 0 and i == run_start) {
|
||||||
|
try w.writeAll("::");
|
||||||
|
i += run_len;
|
||||||
|
at_start = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!at_start) try w.writeByte(':');
|
||||||
|
try w.print("{x}", .{parts[i]});
|
||||||
|
at_start = false;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Prefix = struct {
|
||||||
|
/// Host bits are zeroed by `parse`.
|
||||||
|
addr: NetAddress,
|
||||||
|
bits: u8,
|
||||||
|
|
||||||
|
pub const ParseError = error{InvalidPrefix};
|
||||||
|
|
||||||
|
/// Accepts "192.168.1.0/24" and "fd00:abcd::/48".
|
||||||
|
pub fn parse(text: []const u8) ParseError!Prefix {
|
||||||
|
const slash = std.mem.findScalar(u8, text, '/') orelse return error.InvalidPrefix;
|
||||||
|
const addr = NetAddress.parse(text[0..slash]) catch return error.InvalidPrefix;
|
||||||
|
const bits = std.fmt.parseInt(u8, text[slash + 1 ..], 10) catch return error.InvalidPrefix;
|
||||||
|
if (bits > addr.familyBits()) return error.InvalidPrefix;
|
||||||
|
|
||||||
|
var masked = addr;
|
||||||
|
switch (masked) {
|
||||||
|
.ip4 => |*b| maskBytes(b, bits),
|
||||||
|
.ip6 => |*b| maskBytes(b, bits),
|
||||||
|
}
|
||||||
|
return .{ .addr = masked, .bits = bits };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A family mismatch is never a match.
|
||||||
|
pub fn contains(self: Prefix, addr: NetAddress) bool {
|
||||||
|
if (std.meta.activeTag(self.addr) != std.meta.activeTag(addr)) return false;
|
||||||
|
const net_bytes = self.addr.bytes();
|
||||||
|
const addr_bytes = addr.bytes();
|
||||||
|
var remaining = self.bits;
|
||||||
|
var i: usize = 0;
|
||||||
|
while (remaining >= 8) : (i += 1) {
|
||||||
|
if (net_bytes[i] != addr_bytes[i]) return false;
|
||||||
|
remaining -= 8;
|
||||||
|
}
|
||||||
|
if (remaining == 0) return true;
|
||||||
|
const mask = ~(@as(u8, 0xff) >> @intCast(remaining));
|
||||||
|
return (net_bytes[i] & mask) == (addr_bytes[i] & mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format(self: Prefix, w: *Writer) Writer.Error!void {
|
||||||
|
try self.addr.format(w);
|
||||||
|
try w.print("/{d}", .{self.bits});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn maskBytes(addr: []u8, bits: u8) void {
|
||||||
|
for (addr, 0..) |*byte, i| {
|
||||||
|
const offset = i * 8;
|
||||||
|
if (bits >= offset + 8) continue;
|
||||||
|
if (bits <= offset) {
|
||||||
|
byte.* = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
byte.* &= ~(@as(u8, 0xff) >> @intCast(bits - offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Longest-prefix winner; ties broken by lower `priority` value. Returns null
|
||||||
|
/// when nothing matches. `T` must have fields `prefix: Prefix` and
|
||||||
|
/// `priority: i64`.
|
||||||
|
pub fn matchLongest(comptime T: type, entries: []const T, addr: NetAddress) ?*const T {
|
||||||
|
var best: ?*const T = null;
|
||||||
|
for (entries) |*entry| {
|
||||||
|
if (!entry.prefix.contains(addr)) continue;
|
||||||
|
const current = best orelse {
|
||||||
|
best = entry;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if (entry.prefix.bits > current.prefix.bits) {
|
||||||
|
best = entry;
|
||||||
|
} else if (entry.prefix.bits == current.prefix.bits and entry.priority < current.priority) {
|
||||||
|
best = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
fn expectFormat(expected: []const u8, value: anytype) !void {
|
||||||
|
var buf: [64]u8 = undefined;
|
||||||
|
var w = Writer.fixed(&buf);
|
||||||
|
try value.format(&w);
|
||||||
|
try testing.expectEqualStrings(expected, w.buffered());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expectRoundTrip(text: []const u8) !void {
|
||||||
|
try expectFormat(text, try NetAddress.parse(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "round trip of canonical addresses" {
|
||||||
|
try expectRoundTrip("192.168.1.1");
|
||||||
|
try expectRoundTrip("0.0.0.0");
|
||||||
|
try expectRoundTrip("255.255.255.255");
|
||||||
|
try expectRoundTrip("fd00::1");
|
||||||
|
try expectRoundTrip("::");
|
||||||
|
try expectRoundTrip("::1");
|
||||||
|
try expectRoundTrip("2001:db8::8:800:200c:417a");
|
||||||
|
try expectRoundTrip("2001:db8::");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "rfc 5952 formatting rules" {
|
||||||
|
// Longest run wins; earliest run wins a tie; uppercase input lowercases.
|
||||||
|
try expectFormat("2001:db8::1:0:0:1", try NetAddress.parse("2001:0DB8:0:0:1::1"));
|
||||||
|
// A single zero group is never compressed.
|
||||||
|
try expectFormat("2001:db8:0:1:1:1:1:1", try NetAddress.parse("2001:db8:0:1:1:1:1:1"));
|
||||||
|
// Leading zeros in a group are suppressed.
|
||||||
|
try expectFormat("2001:db8:aaaa:bbbb:cccc:dddd:eeee:1", try NetAddress.parse("2001:0db8:aaaa:bbbb:cccc:dddd:eeee:0001"));
|
||||||
|
// The longest run is compressed even when a shorter run comes first.
|
||||||
|
try expectFormat("2001:0:0:1::1", try NetAddress.parse("2001:0:0:1:0:0:0:1"));
|
||||||
|
// A trailing run compresses to a trailing "::".
|
||||||
|
try expectFormat("2001:db8::", try NetAddress.parse("2001:0db8:0000:0000:0000:0000:0000:0000"));
|
||||||
|
// Hex digits are lowercase.
|
||||||
|
try expectFormat("fe80::abcd:ef01", try NetAddress.parse("FE80::ABCD:EF01"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse rejects malformed input" {
|
||||||
|
try testing.expectError(error.InvalidAddress, NetAddress.parse(""));
|
||||||
|
try testing.expectError(error.InvalidAddress, NetAddress.parse("1.2.3"));
|
||||||
|
try testing.expectError(error.InvalidAddress, NetAddress.parse("1.2.3.4.5"));
|
||||||
|
try testing.expectError(error.InvalidAddress, NetAddress.parse("256.1.1.1"));
|
||||||
|
try testing.expectError(error.InvalidAddress, NetAddress.parse("1.2.3.4:53"));
|
||||||
|
try testing.expectError(error.InvalidAddress, NetAddress.parse("[fd00::1]"));
|
||||||
|
try testing.expectError(error.InvalidAddress, NetAddress.parse("[fd00::1]:853"));
|
||||||
|
try testing.expectError(error.InvalidAddress, NetAddress.parse("fd00::1%eth0"));
|
||||||
|
try testing.expectError(error.InvalidAddress, NetAddress.parse("fd00:::1"));
|
||||||
|
try testing.expectError(error.InvalidAddress, NetAddress.parse("nonsense"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "key encodes family and bytes" {
|
||||||
|
const v4 = try NetAddress.parse("192.168.1.1");
|
||||||
|
var expected_v4: NetAddress.Key = @splat(0);
|
||||||
|
expected_v4[0] = 4;
|
||||||
|
expected_v4[1] = 192;
|
||||||
|
expected_v4[2] = 168;
|
||||||
|
expected_v4[3] = 1;
|
||||||
|
expected_v4[4] = 1;
|
||||||
|
try testing.expectEqualSlices(u8, &expected_v4, &v4.key());
|
||||||
|
|
||||||
|
const v6 = try NetAddress.parse("fd00::1");
|
||||||
|
const k6 = v6.key();
|
||||||
|
try testing.expectEqual(@as(u8, 6), k6[0]);
|
||||||
|
try testing.expectEqual(@as(u8, 0xfd), k6[1]);
|
||||||
|
try testing.expectEqual(@as(u8, 1), k6[16]);
|
||||||
|
|
||||||
|
// Distinct families never collide.
|
||||||
|
try testing.expect(!std.mem.eql(u8, &v4.key(), &v6.key()));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "fromIp normalizes ipv4-mapped ipv6" {
|
||||||
|
const mapped = try net.IpAddress.parse("::ffff:192.168.1.1", 853);
|
||||||
|
try testing.expectEqual(net.IpAddress.Family.ip6, std.meta.activeTag(mapped));
|
||||||
|
|
||||||
|
const addr = NetAddress.fromIp(mapped);
|
||||||
|
try testing.expectEqual(NetAddress.ip4, std.meta.activeTag(addr));
|
||||||
|
try testing.expect(addr.eql(.{ .ip4 = .{ 192, 168, 1, 1 } }));
|
||||||
|
try expectFormat("192.168.1.1", addr);
|
||||||
|
|
||||||
|
// "::" is not a mapped address.
|
||||||
|
const any6 = try net.IpAddress.parse("::", 0);
|
||||||
|
try testing.expectEqual(NetAddress.ip6, std.meta.activeTag(NetAddress.fromIp(any6)));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "toIp restores family and sets port" {
|
||||||
|
const v4 = (try NetAddress.parse("10.0.0.1")).toIp(53);
|
||||||
|
try testing.expectEqual(@as(u16, 53), v4.getPort());
|
||||||
|
try testing.expectEqualSlices(u8, &.{ 10, 0, 0, 1 }, &v4.ip4.bytes);
|
||||||
|
|
||||||
|
const v6 = (try NetAddress.parse("fd00::1")).toIp(853);
|
||||||
|
try testing.expectEqual(@as(u16, 853), v6.getPort());
|
||||||
|
try testing.expect(NetAddress.fromIp(v6).eql(try NetAddress.parse("fd00::1")));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "eql compares family and bytes" {
|
||||||
|
const a = try NetAddress.parse("192.168.1.1");
|
||||||
|
const b = try NetAddress.parse("192.168.1.2");
|
||||||
|
try testing.expect(a.eql(a));
|
||||||
|
try testing.expect(!a.eql(b));
|
||||||
|
try testing.expect(!a.eql(try NetAddress.parse("::")));
|
||||||
|
try testing.expect((try NetAddress.parse("fd00::1")).eql(try NetAddress.parse("fd00:0:0:0:0:0:0:1")));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prefix parse zeroes host bits" {
|
||||||
|
try expectFormat("192.168.1.0/24", try Prefix.parse("192.168.1.55/24"));
|
||||||
|
try expectFormat("0.0.0.0/0", try Prefix.parse("192.168.1.55/0"));
|
||||||
|
try expectFormat("10.128.0.0/9", try Prefix.parse("10.255.3.4/9"));
|
||||||
|
try expectFormat("192.168.1.55/32", try Prefix.parse("192.168.1.55/32"));
|
||||||
|
try expectFormat("fd00:abcd::/48", try Prefix.parse("fd00:abcd:0:1234::5/48"));
|
||||||
|
try expectFormat("::/0", try Prefix.parse("2001:db8::1/0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prefix parse rejects malformed input" {
|
||||||
|
try testing.expectError(error.InvalidPrefix, Prefix.parse("192.168.1.0"));
|
||||||
|
try testing.expectError(error.InvalidPrefix, Prefix.parse("192.168.1.0/"));
|
||||||
|
try testing.expectError(error.InvalidPrefix, Prefix.parse("192.168.1.0/33"));
|
||||||
|
try testing.expectError(error.InvalidPrefix, Prefix.parse("fd00::/129"));
|
||||||
|
try testing.expectError(error.InvalidPrefix, Prefix.parse("/24"));
|
||||||
|
try testing.expectError(error.InvalidPrefix, Prefix.parse("192.168.1.0/x"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "prefix contains" {
|
||||||
|
const p24 = try Prefix.parse("192.168.1.0/24");
|
||||||
|
try testing.expect(p24.contains(try NetAddress.parse("192.168.1.5")));
|
||||||
|
try testing.expect(p24.contains(try NetAddress.parse("192.168.1.0")));
|
||||||
|
try testing.expect(p24.contains(try NetAddress.parse("192.168.1.255")));
|
||||||
|
try testing.expect(!p24.contains(try NetAddress.parse("192.168.2.5")));
|
||||||
|
|
||||||
|
// Family mismatch is false in both directions.
|
||||||
|
try testing.expect(!p24.contains(try NetAddress.parse("fd00::1")));
|
||||||
|
const p6 = try Prefix.parse("fd00::/8");
|
||||||
|
try testing.expect(!p6.contains(try NetAddress.parse("192.168.1.5")));
|
||||||
|
try testing.expect(p6.contains(try NetAddress.parse("fdff::abcd")));
|
||||||
|
try testing.expect(!p6.contains(try NetAddress.parse("fe80::1")));
|
||||||
|
|
||||||
|
// /0 contains everything of its family, nothing of the other.
|
||||||
|
const any4 = try Prefix.parse("0.0.0.0/0");
|
||||||
|
try testing.expect(any4.contains(try NetAddress.parse("8.8.8.8")));
|
||||||
|
try testing.expect(any4.contains(try NetAddress.parse("0.0.0.0")));
|
||||||
|
try testing.expect(!any4.contains(try NetAddress.parse("::")));
|
||||||
|
const any6 = try Prefix.parse("::/0");
|
||||||
|
try testing.expect(any6.contains(try NetAddress.parse("2001:db8::1")));
|
||||||
|
try testing.expect(!any6.contains(try NetAddress.parse("8.8.8.8")));
|
||||||
|
|
||||||
|
// Single-host prefixes.
|
||||||
|
const host4 = try Prefix.parse("10.1.2.3/32");
|
||||||
|
try testing.expect(host4.contains(try NetAddress.parse("10.1.2.3")));
|
||||||
|
try testing.expect(!host4.contains(try NetAddress.parse("10.1.2.4")));
|
||||||
|
const host6 = try Prefix.parse("fd00::1/128");
|
||||||
|
try testing.expect(host6.contains(try NetAddress.parse("fd00::1")));
|
||||||
|
try testing.expect(!host6.contains(try NetAddress.parse("fd00::2")));
|
||||||
|
|
||||||
|
// Non-byte-aligned boundary.
|
||||||
|
const p9 = try Prefix.parse("10.128.0.0/9");
|
||||||
|
try testing.expect(p9.contains(try NetAddress.parse("10.255.255.255")));
|
||||||
|
try testing.expect(!p9.contains(try NetAddress.parse("10.127.255.255")));
|
||||||
|
}
|
||||||
|
|
||||||
|
const Rule = struct {
|
||||||
|
prefix: Prefix,
|
||||||
|
priority: i64,
|
||||||
|
name: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
test "matchLongest prefers longer prefixes" {
|
||||||
|
const rules = [_]Rule{
|
||||||
|
.{ .prefix = try Prefix.parse("192.168.0.0/16"), .priority = 0, .name = "wide" },
|
||||||
|
.{ .prefix = try Prefix.parse("192.168.1.0/24"), .priority = 0, .name = "narrow" },
|
||||||
|
.{ .prefix = try Prefix.parse("0.0.0.0/0"), .priority = 0, .name = "default" },
|
||||||
|
};
|
||||||
|
const hit = matchLongest(Rule, &rules, try NetAddress.parse("192.168.1.5")).?;
|
||||||
|
try testing.expectEqualStrings("narrow", hit.name);
|
||||||
|
|
||||||
|
const wide = matchLongest(Rule, &rules, try NetAddress.parse("192.168.2.5")).?;
|
||||||
|
try testing.expectEqualStrings("wide", wide.name);
|
||||||
|
|
||||||
|
const fallback = matchLongest(Rule, &rules, try NetAddress.parse("8.8.8.8")).?;
|
||||||
|
try testing.expectEqualStrings("default", fallback.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "matchLongest breaks ties by lower priority" {
|
||||||
|
const rules = [_]Rule{
|
||||||
|
.{ .prefix = try Prefix.parse("192.168.1.0/24"), .priority = 7, .name = "later" },
|
||||||
|
.{ .prefix = try Prefix.parse("192.168.1.0/24"), .priority = -3, .name = "winner" },
|
||||||
|
.{ .prefix = try Prefix.parse("192.168.1.0/24"), .priority = 2, .name = "middle" },
|
||||||
|
};
|
||||||
|
const hit = matchLongest(Rule, &rules, try NetAddress.parse("192.168.1.5")).?;
|
||||||
|
try testing.expectEqualStrings("winner", hit.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "matchLongest returns null when nothing matches" {
|
||||||
|
const rules = [_]Rule{
|
||||||
|
.{ .prefix = try Prefix.parse("192.168.1.0/24"), .priority = 0, .name = "a" },
|
||||||
|
.{ .prefix = try Prefix.parse("fd00::/8"), .priority = 0, .name = "b" },
|
||||||
|
};
|
||||||
|
try testing.expect(matchLongest(Rule, &rules, try NetAddress.parse("10.0.0.1")) == null);
|
||||||
|
try testing.expect(matchLongest(Rule, &rules, try NetAddress.parse("2001:db8::1")) == null);
|
||||||
|
try testing.expect(matchLongest(Rule, &[_]Rule{}, try NetAddress.parse("10.0.0.1")) == null);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/* Reports the C sizes and alignments of the Mbed TLS context structs so
|
||||||
|
* src/platform/tls_server.zig can treat them as opaque and heap-allocate them
|
||||||
|
* without mirroring their layout in Zig. */
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#include <mbedtls/ctr_drbg.h>
|
||||||
|
#include <mbedtls/entropy.h>
|
||||||
|
#include <mbedtls/pk.h>
|
||||||
|
#include <mbedtls/ssl.h>
|
||||||
|
#include <mbedtls/x509_crt.h>
|
||||||
|
|
||||||
|
size_t nx_sizeof_ssl_context(void) { return sizeof(mbedtls_ssl_context); }
|
||||||
|
size_t nx_sizeof_ssl_config(void) { return sizeof(mbedtls_ssl_config); }
|
||||||
|
size_t nx_sizeof_x509_crt(void) { return sizeof(mbedtls_x509_crt); }
|
||||||
|
size_t nx_sizeof_pk_context(void) { return sizeof(mbedtls_pk_context); }
|
||||||
|
size_t nx_sizeof_entropy_context(void) { return sizeof(mbedtls_entropy_context); }
|
||||||
|
size_t nx_sizeof_ctr_drbg_context(void) { return sizeof(mbedtls_ctr_drbg_context); }
|
||||||
|
|
||||||
|
/* The public key of the first certificate in a chain. tls_server.zig keeps
|
||||||
|
* mbedtls_x509_crt opaque, so it cannot reach this field on its own. */
|
||||||
|
mbedtls_pk_context *nx_x509_crt_pk(mbedtls_x509_crt *crt) { return &crt->pk; }
|
||||||
|
|
||||||
|
/* Largest alignment any of the above needs. tls_server.zig allocates every
|
||||||
|
* context at a fixed comptime alignment and asserts it covers this value. */
|
||||||
|
size_t nx_max_context_alignment(void)
|
||||||
|
{
|
||||||
|
size_t max = _Alignof(mbedtls_ssl_context);
|
||||||
|
if (_Alignof(mbedtls_ssl_config) > max) max = _Alignof(mbedtls_ssl_config);
|
||||||
|
if (_Alignof(mbedtls_x509_crt) > max) max = _Alignof(mbedtls_x509_crt);
|
||||||
|
if (_Alignof(mbedtls_pk_context) > max) max = _Alignof(mbedtls_pk_context);
|
||||||
|
if (_Alignof(mbedtls_entropy_context) > max) max = _Alignof(mbedtls_entropy_context);
|
||||||
|
if (_Alignof(mbedtls_ctr_drbg_context) > max) max = _Alignof(mbedtls_ctr_drbg_context);
|
||||||
|
return max;
|
||||||
|
}
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
//! Client-side TLS over a `std.Io.net.Stream`, used for upstream DoT.
|
||||||
|
//!
|
||||||
|
//! `std.crypto.tls.Client` holds its `Io.Reader`/`Io.Writer` by value and its
|
||||||
|
//! `input`/`output` point at the stream reader/writer stored beside it, so
|
||||||
|
//! `TlsStream` is pinned: it MUST NOT be moved or copied after `init`.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const tls = std.crypto.tls;
|
||||||
|
const net = std.Io.net;
|
||||||
|
const Certificate = std.crypto.Certificate;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.tls_client);
|
||||||
|
|
||||||
|
pub const ErrorClass = enum { handshake, certificate, io, protocol };
|
||||||
|
|
||||||
|
pub const InitError = tls.Client.InitError || error{CertificateBundleLoadFailure};
|
||||||
|
|
||||||
|
/// Exhaustive over `std.crypto.tls.Client.InitError`. A new error in that stdlib set
|
||||||
|
/// breaks this switch at compile time, which is the point.
|
||||||
|
fn classifyInit(err: tls.Client.InitError) ErrorClass {
|
||||||
|
return switch (err) {
|
||||||
|
error.InsufficientEntropy,
|
||||||
|
error.TlsAlert,
|
||||||
|
error.TlsUnexpectedMessage,
|
||||||
|
error.TlsIllegalParameter,
|
||||||
|
error.TlsDecryptFailure,
|
||||||
|
error.TlsDecryptError,
|
||||||
|
error.TlsRecordOverflow,
|
||||||
|
error.TlsBadRecordMac,
|
||||||
|
=> .handshake,
|
||||||
|
|
||||||
|
error.CertificateFieldHasInvalidLength,
|
||||||
|
error.CertificateHostMismatch,
|
||||||
|
error.CertificatePublicKeyInvalid,
|
||||||
|
error.CertificateExpired,
|
||||||
|
error.CertificateFieldHasWrongDataType,
|
||||||
|
error.CertificateIssuerMismatch,
|
||||||
|
error.CertificateNotYetValid,
|
||||||
|
error.CertificateSignatureAlgorithmMismatch,
|
||||||
|
error.CertificateSignatureAlgorithmUnsupported,
|
||||||
|
error.CertificateSignatureInvalid,
|
||||||
|
error.CertificateSignatureInvalidLength,
|
||||||
|
error.CertificateSignatureNamedCurveUnsupported,
|
||||||
|
error.CertificateSignatureUnsupportedBitCount,
|
||||||
|
error.CertificateTimeInvalid,
|
||||||
|
error.CertificateHasUnrecognizedObjectId,
|
||||||
|
error.CertificateHasInvalidBitString,
|
||||||
|
error.UnsupportedCertificateVersion,
|
||||||
|
error.TlsCertificateNotVerified,
|
||||||
|
error.TlsBadSignatureScheme,
|
||||||
|
error.TlsBadRsaSignatureBitCount,
|
||||||
|
error.SignatureVerificationFailed,
|
||||||
|
error.InvalidSignature,
|
||||||
|
=> .certificate,
|
||||||
|
|
||||||
|
error.ReadFailed,
|
||||||
|
error.WriteFailed,
|
||||||
|
error.Canceled,
|
||||||
|
error.DiskQuota,
|
||||||
|
error.LockViolation,
|
||||||
|
error.NotOpenForWriting,
|
||||||
|
=> .io,
|
||||||
|
|
||||||
|
error.TlsConnectionTruncated,
|
||||||
|
error.TlsDecodeError,
|
||||||
|
error.InvalidEncoding,
|
||||||
|
error.IdentityElement,
|
||||||
|
error.MessageTooLong,
|
||||||
|
error.NegativeIntoUnsigned,
|
||||||
|
error.TargetTooSmall,
|
||||||
|
error.BufferTooSmall,
|
||||||
|
error.NotSquare,
|
||||||
|
error.NonCanonical,
|
||||||
|
error.WeakPublicKey,
|
||||||
|
=> .protocol,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Errors reachable after the handshake, or added by this module.
|
||||||
|
fn classifyOther(err: anyerror) ErrorClass {
|
||||||
|
return switch (err) {
|
||||||
|
error.CertificateBundleLoadFailure,
|
||||||
|
error.CertificateIssuerNotFound,
|
||||||
|
=> .certificate,
|
||||||
|
|
||||||
|
error.TlsBadLength,
|
||||||
|
error.TlsSequenceOverflow,
|
||||||
|
=> .protocol,
|
||||||
|
|
||||||
|
error.EndOfStream,
|
||||||
|
error.ConnectionResetByPeer,
|
||||||
|
error.ConnectionRefused,
|
||||||
|
error.ConnectionTimedOut,
|
||||||
|
error.BrokenPipe,
|
||||||
|
error.NetworkDown,
|
||||||
|
error.NetworkUnreachable,
|
||||||
|
error.HostUnreachable,
|
||||||
|
error.Timeout,
|
||||||
|
error.SocketUnconnected,
|
||||||
|
error.SystemResources,
|
||||||
|
error.AccessDenied,
|
||||||
|
=> .io,
|
||||||
|
|
||||||
|
else => .protocol,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn classify(err: anyerror) ErrorClass {
|
||||||
|
inline for (@typeInfo(tls.Client.InitError).error_set.?) |member| {
|
||||||
|
if (err == @field(anyerror, member.name)) {
|
||||||
|
return classifyInit(@field(tls.Client.InitError, member.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return classifyOther(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const TlsStream = struct {
|
||||||
|
stream_reader: net.Stream.Reader,
|
||||||
|
stream_writer: net.Stream.Writer,
|
||||||
|
client: tls.Client,
|
||||||
|
|
||||||
|
pub const Options = struct {
|
||||||
|
/// SNI name, and the name matched against the leaf certificate. Sent and
|
||||||
|
/// matched under both `ca` settings.
|
||||||
|
host: []const u8,
|
||||||
|
/// `.insecure_skip_verify` drops only the chain of trust: the stdlib
|
||||||
|
/// verifies the leaf certificate host name before it consults `ca`
|
||||||
|
/// (crypto/tls/Client.zig:645), so `host` must still match the leaf
|
||||||
|
/// certificate or the handshake fails with `error.CertificateHostMismatch`.
|
||||||
|
/// Issuer authorization and certificate expiry are not checked.
|
||||||
|
ca: enum { system, insecure_skip_verify },
|
||||||
|
/// Plaintext read buffer.
|
||||||
|
read_buffer: []u8,
|
||||||
|
/// Plaintext write buffer.
|
||||||
|
write_buffer: []u8,
|
||||||
|
/// Ciphertext read buffer; must hold at least `tls.Client.min_buffer_len` bytes.
|
||||||
|
stream_read_buffer: []u8,
|
||||||
|
/// Ciphertext write buffer; must hold at least `tls.Client.min_buffer_len` bytes.
|
||||||
|
stream_write_buffer: []u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// In-place init. `self` must not move afterwards.
|
||||||
|
///
|
||||||
|
/// With `.ca = .system`, an empty `bundle` is filled by
|
||||||
|
/// `Certificate.Bundle.rescan` under `bundle_lock`. The caller owns the
|
||||||
|
/// lifetime of `bundle`, `bundle_lock` and `stream`.
|
||||||
|
pub fn init(
|
||||||
|
self: *TlsStream,
|
||||||
|
io: std.Io,
|
||||||
|
stream: *net.Stream,
|
||||||
|
bundle: *Certificate.Bundle,
|
||||||
|
bundle_lock: *std.Io.RwLock,
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
options: Options,
|
||||||
|
) InitError!void {
|
||||||
|
std.debug.assert(options.stream_read_buffer.len >= tls.Client.min_buffer_len);
|
||||||
|
std.debug.assert(options.stream_write_buffer.len >= tls.Client.min_buffer_len);
|
||||||
|
|
||||||
|
const now = std.Io.Clock.real.now(io);
|
||||||
|
|
||||||
|
switch (options.ca) {
|
||||||
|
.system => try ensureBundle(io, bundle, bundle_lock, gpa, now),
|
||||||
|
.insecure_skip_verify => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
var entropy: [tls.Client.Options.entropy_len]u8 = undefined;
|
||||||
|
io.random(&entropy);
|
||||||
|
|
||||||
|
self.* = .{
|
||||||
|
.stream_reader = stream.reader(io, options.stream_read_buffer),
|
||||||
|
.stream_writer = stream.writer(io, options.stream_write_buffer),
|
||||||
|
.client = undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.client = try tls.Client.init(
|
||||||
|
&self.stream_reader.interface,
|
||||||
|
&self.stream_writer.interface,
|
||||||
|
.{
|
||||||
|
.host = .{ .explicit = options.host },
|
||||||
|
.ca = switch (options.ca) {
|
||||||
|
.system => .{ .bundle = .{
|
||||||
|
.gpa = gpa,
|
||||||
|
.io = io,
|
||||||
|
.lock = bundle_lock,
|
||||||
|
.bundle = bundle,
|
||||||
|
} },
|
||||||
|
.insecure_skip_verify => .no_verification,
|
||||||
|
},
|
||||||
|
.read_buffer = options.read_buffer,
|
||||||
|
.write_buffer = options.write_buffer,
|
||||||
|
.entropy = &entropy,
|
||||||
|
.realtime_now = now,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensureBundle(
|
||||||
|
io: std.Io,
|
||||||
|
bundle: *Certificate.Bundle,
|
||||||
|
bundle_lock: *std.Io.RwLock,
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
now: std.Io.Timestamp,
|
||||||
|
) InitError!void {
|
||||||
|
{
|
||||||
|
try bundle_lock.lockShared(io);
|
||||||
|
defer bundle_lock.unlockShared(io);
|
||||||
|
if (bundle.map.count() != 0) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try bundle_lock.lock(io);
|
||||||
|
defer bundle_lock.unlock(io);
|
||||||
|
if (bundle.map.count() != 0) return;
|
||||||
|
|
||||||
|
// A partial rescan leaves entries in `map`, which the fast path above would
|
||||||
|
// read as "already loaded". Reset so the next init retries a full rescan.
|
||||||
|
bundle.rescan(gpa, io, now) catch |err| {
|
||||||
|
bundle.deinit(gpa);
|
||||||
|
bundle.* = .empty;
|
||||||
|
return switch (err) {
|
||||||
|
error.Canceled => error.Canceled,
|
||||||
|
else => error.CertificateBundleLoadFailure,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plaintext reader.
|
||||||
|
pub fn reader(self: *TlsStream) *std.Io.Reader {
|
||||||
|
return &self.client.reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plaintext writer.
|
||||||
|
pub fn writer(self: *TlsStream) *std.Io.Writer {
|
||||||
|
return &self.client.writer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends close_notify and flushes the socket. Does not close the underlying
|
||||||
|
/// stream; the caller owns it.
|
||||||
|
pub fn close(self: *TlsStream) void {
|
||||||
|
self.client.end() catch |err| {
|
||||||
|
log.debug("close_notify failed: {s}", .{@errorName(err)});
|
||||||
|
};
|
||||||
|
self.stream_writer.interface.flush() catch |err| {
|
||||||
|
log.debug("flush after close_notify failed: {s}", .{@errorName(err)});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "classify maps handshake errors" {
|
||||||
|
try std.testing.expectEqual(ErrorClass.handshake, classify(error.TlsAlert));
|
||||||
|
try std.testing.expectEqual(ErrorClass.handshake, classify(error.TlsIllegalParameter));
|
||||||
|
try std.testing.expectEqual(ErrorClass.handshake, classify(error.InsufficientEntropy));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "classify maps certificate errors" {
|
||||||
|
try std.testing.expectEqual(ErrorClass.certificate, classify(error.CertificateExpired));
|
||||||
|
try std.testing.expectEqual(ErrorClass.certificate, classify(error.CertificateHostMismatch));
|
||||||
|
try std.testing.expectEqual(ErrorClass.certificate, classify(error.TlsCertificateNotVerified));
|
||||||
|
try std.testing.expectEqual(ErrorClass.certificate, classify(error.CertificateBundleLoadFailure));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "classify maps io errors" {
|
||||||
|
try std.testing.expectEqual(ErrorClass.io, classify(error.ReadFailed));
|
||||||
|
try std.testing.expectEqual(ErrorClass.io, classify(error.WriteFailed));
|
||||||
|
try std.testing.expectEqual(ErrorClass.io, classify(error.Canceled));
|
||||||
|
try std.testing.expectEqual(ErrorClass.io, classify(error.ConnectionResetByPeer));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "classify maps protocol errors" {
|
||||||
|
try std.testing.expectEqual(ErrorClass.protocol, classify(error.TlsDecodeError));
|
||||||
|
try std.testing.expectEqual(ErrorClass.protocol, classify(error.InvalidEncoding));
|
||||||
|
try std.testing.expectEqual(ErrorClass.protocol, classify(error.TlsBadLength));
|
||||||
|
try std.testing.expectEqual(ErrorClass.protocol, classify(error.TlsConnectionTruncated));
|
||||||
|
try std.testing.expectEqual(ErrorClass.protocol, classify(error.SomethingNobodyMapped));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "classify covers every InitError member" {
|
||||||
|
inline for (@typeInfo(tls.Client.InitError).error_set.?) |member| {
|
||||||
|
_ = classifyInit(@field(tls.Client.InitError, member.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
//! Network-dependent tests for `tls_client.zig`.
|
||||||
|
//!
|
||||||
|
//! This lives in its own file because it needs `@import("build_options")`, which only
|
||||||
|
//! exists when the compilation is driven by build.zig. Keeping it out of
|
||||||
|
//! `tls_client.zig` lets that file stay compilable with a bare
|
||||||
|
//! `zig test src/platform/tls_client.zig`.
|
||||||
|
//!
|
||||||
|
//! The test is compiled by every `zig build test` run, so it cannot rot, and skips at
|
||||||
|
//! run time unless `-Dlive` is passed. (`-Dintegration` stays hermetic: loopback only.
|
||||||
|
//! `-Dlive` is the gate for tests that leave the machine.)
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const build_options = @import("build_options");
|
||||||
|
const tls = std.crypto.tls;
|
||||||
|
const net = std.Io.net;
|
||||||
|
const Certificate = std.crypto.Certificate;
|
||||||
|
|
||||||
|
const tls_client = @import("tls_client.zig");
|
||||||
|
|
||||||
|
const budget: std.Io.Clock.Duration = .{ .raw = .fromSeconds(10), .clock = .awake };
|
||||||
|
|
||||||
|
/// `net.IpAddress.ConnectOptions.timeout` panics with "TODO implement
|
||||||
|
/// netConnectIpPosix with timeout" on POSIX in 0.16.0, and TLS stream reads take no
|
||||||
|
/// timeout at all. So the whole exchange runs as one task raced against a sleep, and
|
||||||
|
/// the loser is canceled.
|
||||||
|
const Outcome = union(enum) {
|
||||||
|
exchange: anyerror!void,
|
||||||
|
expiry: std.Io.Cancelable!void,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Params = struct {
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
bundle: *Certificate.Bundle,
|
||||||
|
bundle_lock: *std.Io.RwLock,
|
||||||
|
stream_read_buffer: []u8,
|
||||||
|
stream_write_buffer: []u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Connects to the documented anycast address rather than resolving the name, so the
|
||||||
|
/// test exercises this module and not the stdlib resolver. `host` still drives SNI and
|
||||||
|
/// certificate hostname verification against "cloudflare-dns.com".
|
||||||
|
fn connectAndHandshake(io: std.Io, params: Params) anyerror!void {
|
||||||
|
const address: net.IpAddress = try .parse("1.1.1.1", 853);
|
||||||
|
var stream = try address.connect(io, .{ .mode = .stream });
|
||||||
|
defer stream.close(io);
|
||||||
|
|
||||||
|
var read_buffer: [4096]u8 = undefined;
|
||||||
|
var write_buffer: [4096]u8 = undefined;
|
||||||
|
|
||||||
|
var tls_stream: tls_client.TlsStream = undefined;
|
||||||
|
try tls_stream.init(io, &stream, params.bundle, params.bundle_lock, params.gpa, .{
|
||||||
|
.host = "cloudflare-dns.com",
|
||||||
|
.ca = .system,
|
||||||
|
.read_buffer = &read_buffer,
|
||||||
|
.write_buffer = &write_buffer,
|
||||||
|
.stream_read_buffer = params.stream_read_buffer,
|
||||||
|
.stream_write_buffer = params.stream_write_buffer,
|
||||||
|
});
|
||||||
|
tls_stream.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expire(io: std.Io, duration: std.Io.Clock.Duration) std.Io.Cancelable!void {
|
||||||
|
return duration.sleep(io);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "live DoT handshake against cloudflare-dns.com" {
|
||||||
|
if (!build_options.live) return error.SkipZigTest;
|
||||||
|
|
||||||
|
const gpa = std.testing.allocator;
|
||||||
|
|
||||||
|
var threaded: std.Io.Threaded = .init(gpa, .{});
|
||||||
|
defer threaded.deinit();
|
||||||
|
const io = threaded.io();
|
||||||
|
|
||||||
|
var bundle: Certificate.Bundle = .empty;
|
||||||
|
defer bundle.deinit(gpa);
|
||||||
|
var bundle_lock: std.Io.RwLock = .init;
|
||||||
|
|
||||||
|
const stream_read_buffer = try gpa.alloc(u8, tls.Client.min_buffer_len);
|
||||||
|
defer gpa.free(stream_read_buffer);
|
||||||
|
const stream_write_buffer = try gpa.alloc(u8, tls.Client.min_buffer_len);
|
||||||
|
defer gpa.free(stream_write_buffer);
|
||||||
|
|
||||||
|
var outcomes: [2]Outcome = undefined;
|
||||||
|
var race: std.Io.Select(Outcome) = .init(io, &outcomes);
|
||||||
|
defer race.cancelDiscard();
|
||||||
|
|
||||||
|
try race.concurrent(.exchange, connectAndHandshake, .{ io, Params{
|
||||||
|
.gpa = gpa,
|
||||||
|
.bundle = &bundle,
|
||||||
|
.bundle_lock = &bundle_lock,
|
||||||
|
.stream_read_buffer = stream_read_buffer,
|
||||||
|
.stream_write_buffer = stream_write_buffer,
|
||||||
|
} });
|
||||||
|
try race.concurrent(.expiry, expire, .{ io, budget });
|
||||||
|
|
||||||
|
switch (try race.await()) {
|
||||||
|
.exchange => |result| result catch |err| {
|
||||||
|
std.debug.print("DoT handshake failed: {s} (class: {t})\n", .{
|
||||||
|
@errorName(err),
|
||||||
|
tls_client.classify(err),
|
||||||
|
});
|
||||||
|
return err;
|
||||||
|
},
|
||||||
|
.expiry => |result| {
|
||||||
|
try result;
|
||||||
|
return error.DotExchangeTimedOut;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,823 @@
|
|||||||
|
//! Server-side TLS termination backed by Mbed TLS 3.6, exposing plaintext as
|
||||||
|
//! `std.Io.Reader` / `std.Io.Writer` so `std.http.Server` and the DoT server can
|
||||||
|
//! sit on top of any accepted `std.Io.net.Stream`.
|
||||||
|
//!
|
||||||
|
//! The Mbed TLS surface is declared by hand below. No `@cImport` — the context
|
||||||
|
//! structs stay opaque and are heap-allocated using sizes reported by
|
||||||
|
//! `src/platform/mbedtls_shim.c`.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Io = std.Io;
|
||||||
|
const Reader = std.Io.Reader;
|
||||||
|
const Writer = std.Io.Writer;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.tls_server);
|
||||||
|
|
||||||
|
/// Every Mbed TLS context is allocated at this alignment; `ServerContext.init`
|
||||||
|
/// asserts the shim agrees it is enough.
|
||||||
|
const context_alignment = 16;
|
||||||
|
const ContextAlignment: std.mem.Alignment = .fromByteUnits(context_alignment);
|
||||||
|
|
||||||
|
pub const InitError = error{
|
||||||
|
OutOfMemory,
|
||||||
|
/// The certificate chain PEM could not be parsed.
|
||||||
|
CertParse,
|
||||||
|
/// The private key PEM could not be parsed.
|
||||||
|
KeyParse,
|
||||||
|
/// The private key does not belong to the leaf certificate.
|
||||||
|
KeyMismatch,
|
||||||
|
/// Seeding the CTR-DRBG from the platform entropy source failed.
|
||||||
|
EntropyFailed,
|
||||||
|
/// Mbed TLS rejected the server configuration.
|
||||||
|
ConfigFailed,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const AcceptError = error{
|
||||||
|
OutOfMemory,
|
||||||
|
/// Binding the connection to the shared configuration failed.
|
||||||
|
SetupFailed,
|
||||||
|
/// The peer went away before the handshake completed.
|
||||||
|
PeerClosed,
|
||||||
|
HandshakeFailed,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Parsed certificate chain, private key, RNG and `mbedtls_ssl_config` for one
|
||||||
|
/// listener. Create it once and share it across every accepted connection.
|
||||||
|
///
|
||||||
|
/// Safe to move: every Mbed TLS context lives on the heap, so the pointers Mbed
|
||||||
|
/// TLS stores internally stay valid.
|
||||||
|
pub const ServerContext = struct {
|
||||||
|
entropy: Blocks.Entropy,
|
||||||
|
drbg: Blocks.Drbg,
|
||||||
|
cert: Blocks.Cert,
|
||||||
|
key: Blocks.Key,
|
||||||
|
config: Blocks.Config,
|
||||||
|
|
||||||
|
const Blocks = struct {
|
||||||
|
const Entropy = Block(EntropyContext);
|
||||||
|
const Drbg = Block(CtrDrbgContext);
|
||||||
|
const Cert = Block(X509Crt);
|
||||||
|
const Key = Block(PkContext);
|
||||||
|
const Config = Block(SslConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(gpa: std.mem.Allocator, cert_pem: [:0]const u8, key_pem: [:0]const u8) InitError!ServerContext {
|
||||||
|
assert(nx_max_context_alignment() <= context_alignment);
|
||||||
|
|
||||||
|
// TLS 1.3 is on in the stock config; its key schedule runs through PSA.
|
||||||
|
const psa_status = psa_crypto_init();
|
||||||
|
if (psa_status != 0) {
|
||||||
|
log.err("psa_crypto_init failed: {d}", .{psa_status});
|
||||||
|
return error.ConfigFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entropy = try Blocks.Entropy.alloc(gpa, nx_sizeof_entropy_context());
|
||||||
|
errdefer entropy.free(gpa);
|
||||||
|
mbedtls_entropy_init(entropy.ptr);
|
||||||
|
errdefer mbedtls_entropy_free(entropy.ptr);
|
||||||
|
|
||||||
|
var drbg = try Blocks.Drbg.alloc(gpa, nx_sizeof_ctr_drbg_context());
|
||||||
|
errdefer drbg.free(gpa);
|
||||||
|
mbedtls_ctr_drbg_init(drbg.ptr);
|
||||||
|
errdefer mbedtls_ctr_drbg_free(drbg.ptr);
|
||||||
|
|
||||||
|
const personalization = "nxdns-tls-server";
|
||||||
|
try check(mbedtls_ctr_drbg_seed(
|
||||||
|
drbg.ptr,
|
||||||
|
mbedtls_entropy_func,
|
||||||
|
entropy.ptr,
|
||||||
|
personalization,
|
||||||
|
personalization.len,
|
||||||
|
), "ctr_drbg_seed", error.EntropyFailed);
|
||||||
|
|
||||||
|
var cert = try Blocks.Cert.alloc(gpa, nx_sizeof_x509_crt());
|
||||||
|
errdefer cert.free(gpa);
|
||||||
|
mbedtls_x509_crt_init(cert.ptr);
|
||||||
|
errdefer mbedtls_x509_crt_free(cert.ptr);
|
||||||
|
|
||||||
|
// PEM input must include the terminating zero byte in the length.
|
||||||
|
try check(
|
||||||
|
mbedtls_x509_crt_parse(cert.ptr, cert_pem.ptr, cert_pem.len + 1),
|
||||||
|
"x509_crt_parse",
|
||||||
|
error.CertParse,
|
||||||
|
);
|
||||||
|
|
||||||
|
var key = try Blocks.Key.alloc(gpa, nx_sizeof_pk_context());
|
||||||
|
errdefer key.free(gpa);
|
||||||
|
mbedtls_pk_init(key.ptr);
|
||||||
|
errdefer mbedtls_pk_free(key.ptr);
|
||||||
|
|
||||||
|
try check(mbedtls_pk_parse_key(
|
||||||
|
key.ptr,
|
||||||
|
key_pem.ptr,
|
||||||
|
key_pem.len + 1,
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
mbedtls_ctr_drbg_random,
|
||||||
|
drbg.ptr,
|
||||||
|
), "pk_parse_key", error.KeyParse);
|
||||||
|
|
||||||
|
// mbedtls_ssl_conf_own_cert accepts a mismatched pair; only this call
|
||||||
|
// rejects one, and it needs an RNG for the blinded EC comparison.
|
||||||
|
try check(mbedtls_pk_check_pair(
|
||||||
|
nx_x509_crt_pk(cert.ptr),
|
||||||
|
key.ptr,
|
||||||
|
mbedtls_ctr_drbg_random,
|
||||||
|
drbg.ptr,
|
||||||
|
), "pk_check_pair", error.KeyMismatch);
|
||||||
|
|
||||||
|
var config = try Blocks.Config.alloc(gpa, nx_sizeof_ssl_config());
|
||||||
|
errdefer config.free(gpa);
|
||||||
|
mbedtls_ssl_config_init(config.ptr);
|
||||||
|
errdefer mbedtls_ssl_config_free(config.ptr);
|
||||||
|
|
||||||
|
try check(mbedtls_ssl_config_defaults(
|
||||||
|
config.ptr,
|
||||||
|
ssl_is_server,
|
||||||
|
ssl_transport_stream,
|
||||||
|
ssl_preset_default,
|
||||||
|
), "ssl_config_defaults", error.ConfigFailed);
|
||||||
|
|
||||||
|
mbedtls_ssl_conf_rng(config.ptr, mbedtls_ctr_drbg_random, drbg.ptr);
|
||||||
|
// nxdns never asks clients for a certificate.
|
||||||
|
mbedtls_ssl_conf_authmode(config.ptr, ssl_verify_none);
|
||||||
|
try check(
|
||||||
|
mbedtls_ssl_conf_own_cert(config.ptr, cert.ptr, key.ptr),
|
||||||
|
"ssl_conf_own_cert",
|
||||||
|
error.ConfigFailed,
|
||||||
|
);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.entropy = entropy,
|
||||||
|
.drbg = drbg,
|
||||||
|
.cert = cert,
|
||||||
|
.key = key,
|
||||||
|
.config = config,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *ServerContext, gpa: std.mem.Allocator) void {
|
||||||
|
mbedtls_ssl_config_free(self.config.ptr);
|
||||||
|
mbedtls_pk_free(self.key.ptr);
|
||||||
|
mbedtls_x509_crt_free(self.cert.ptr);
|
||||||
|
mbedtls_ctr_drbg_free(self.drbg.ptr);
|
||||||
|
mbedtls_entropy_free(self.entropy.ptr);
|
||||||
|
|
||||||
|
self.config.free(gpa);
|
||||||
|
self.key.free(gpa);
|
||||||
|
self.cert.free(gpa);
|
||||||
|
self.drbg.free(gpa);
|
||||||
|
self.entropy.free(gpa);
|
||||||
|
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// One TLS connection. Pinned once `accept` succeeds: the `std.Io` interfaces
|
||||||
|
/// are recovered from their addresses inside this struct, and Mbed TLS holds a
|
||||||
|
/// pointer to it as the BIO context.
|
||||||
|
pub const ServerStream = struct {
|
||||||
|
ssl: Block(SslContext),
|
||||||
|
io: Io,
|
||||||
|
net_reader: Io.net.Stream.Reader,
|
||||||
|
net_writer: Io.net.Stream.Writer,
|
||||||
|
reader_interface: Reader,
|
||||||
|
writer_interface: Writer,
|
||||||
|
/// Set once the peer's close_notify is seen. A transport EOF without one
|
||||||
|
/// is a truncated stream, not a close.
|
||||||
|
peer_closed: bool,
|
||||||
|
/// The concrete failure behind `error.ReadFailed`, in the arrangement
|
||||||
|
/// `std.crypto.tls.Client.read_err` uses.
|
||||||
|
read_err: ?ReadError,
|
||||||
|
/// Most recent Mbed TLS code behind `read_err`.
|
||||||
|
read_code: c_int,
|
||||||
|
/// Most recent negative Mbed TLS code behind `error.WriteFailed`.
|
||||||
|
write_code: c_int,
|
||||||
|
|
||||||
|
pub const ReadError = error{
|
||||||
|
/// The peer closed the transport without sending close_notify. Any
|
||||||
|
/// data it withheld is indistinguishable from data an attacker cut
|
||||||
|
/// off, so the stream never ends cleanly this way.
|
||||||
|
TlsConnectionTruncated,
|
||||||
|
/// Mbed TLS rejected the record; `read_code` holds its code.
|
||||||
|
TlsFailed,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Zero-length transport buffers: Mbed TLS keeps its own record buffers, so
|
||||||
|
/// a second layer of buffering under the BIO callbacks would only add
|
||||||
|
/// copies. Mutable because `Stream.reader`/`Stream.writer` take `[]u8`.
|
||||||
|
var no_transport_buffer: [0]u8 = .{};
|
||||||
|
|
||||||
|
const reader_vtable: Reader.VTable = .{
|
||||||
|
.stream = readerStream,
|
||||||
|
.readVec = readerReadVec,
|
||||||
|
};
|
||||||
|
|
||||||
|
const writer_vtable: Writer.VTable = .{
|
||||||
|
.drain = writerDrain,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Runs the TLS handshake over an already accepted TCP stream. `stream`
|
||||||
|
/// must outlive the `ServerStream`; closing it stays the caller's job.
|
||||||
|
///
|
||||||
|
/// `read_buffer` and `write_buffer` hold plaintext for the `reader` and
|
||||||
|
/// `writer` interfaces.
|
||||||
|
pub fn accept(
|
||||||
|
self: *ServerStream,
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
ctx: *ServerContext,
|
||||||
|
io: Io,
|
||||||
|
stream: *Io.net.Stream,
|
||||||
|
read_buffer: []u8,
|
||||||
|
write_buffer: []u8,
|
||||||
|
) AcceptError!void {
|
||||||
|
var ssl = try Block(SslContext).alloc(gpa, nx_sizeof_ssl_context());
|
||||||
|
errdefer ssl.free(gpa);
|
||||||
|
mbedtls_ssl_init(ssl.ptr);
|
||||||
|
errdefer mbedtls_ssl_free(ssl.ptr);
|
||||||
|
|
||||||
|
self.* = .{
|
||||||
|
.ssl = ssl,
|
||||||
|
.io = io,
|
||||||
|
.net_reader = stream.reader(io, &no_transport_buffer),
|
||||||
|
.net_writer = stream.writer(io, &no_transport_buffer),
|
||||||
|
.reader_interface = .{
|
||||||
|
.vtable = &reader_vtable,
|
||||||
|
.buffer = read_buffer,
|
||||||
|
.seek = 0,
|
||||||
|
.end = 0,
|
||||||
|
},
|
||||||
|
.writer_interface = .{
|
||||||
|
.vtable = &writer_vtable,
|
||||||
|
.buffer = write_buffer,
|
||||||
|
},
|
||||||
|
.peer_closed = false,
|
||||||
|
.read_err = null,
|
||||||
|
.read_code = 0,
|
||||||
|
.write_code = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
try check(mbedtls_ssl_setup(self.ssl.ptr, ctx.config.ptr), "ssl_setup", error.SetupFailed);
|
||||||
|
mbedtls_ssl_set_bio(self.ssl.ptr, self, bioSend, bioRecv, null);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const rc = mbedtls_ssl_handshake(self.ssl.ptr);
|
||||||
|
if (rc == 0) return;
|
||||||
|
if (isRetry(rc)) continue;
|
||||||
|
if (rc == err_conn_eof or rc == err_peer_close_notify) return error.PeerClosed;
|
||||||
|
report("ssl_handshake", rc);
|
||||||
|
return error.HandshakeFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plaintext read side. The `ServerStream` must not move after `accept`.
|
||||||
|
pub fn reader(self: *ServerStream) *Reader {
|
||||||
|
return &self.reader_interface;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plaintext write side. Buffered — call `flush` to force records out.
|
||||||
|
pub fn writer(self: *ServerStream) *Writer {
|
||||||
|
return &self.writer_interface;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flushes pending plaintext, sends close_notify, and frees the connection
|
||||||
|
/// context. Does not close the underlying TCP stream.
|
||||||
|
pub fn close(self: *ServerStream, gpa: std.mem.Allocator) void {
|
||||||
|
self.writer_interface.flush() catch {
|
||||||
|
log.debug("flush before close_notify failed: mbedtls {d}", .{self.write_code});
|
||||||
|
};
|
||||||
|
while (true) {
|
||||||
|
const rc = mbedtls_ssl_close_notify(self.ssl.ptr);
|
||||||
|
if (isRetry(rc)) continue;
|
||||||
|
if (rc != 0) report("ssl_close_notify", rc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
mbedtls_ssl_free(self.ssl.ptr);
|
||||||
|
self.ssl.free(gpa);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readerStream(r: *Reader, w: *Writer, limit: Io.Limit) Reader.StreamError!usize {
|
||||||
|
_ = w;
|
||||||
|
_ = limit;
|
||||||
|
const self: *ServerStream = @alignCast(@fieldParentPtr("reader_interface", r));
|
||||||
|
return self.readIntoBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readerReadVec(r: *Reader, data: [][]u8) Reader.Error!usize {
|
||||||
|
_ = data;
|
||||||
|
const self: *ServerStream = @alignCast(@fieldParentPtr("reader_interface", r));
|
||||||
|
return self.readIntoBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypts into `Reader.buffer` and reports zero bytes streamed, the
|
||||||
|
/// arrangement `Reader.VTable` documents for buffer-writing implementations.
|
||||||
|
fn readIntoBuffer(self: *ServerStream) Reader.Error!usize {
|
||||||
|
const r = &self.reader_interface;
|
||||||
|
if (self.read_err) |err| return self.failRead(err, self.read_code);
|
||||||
|
if (self.peer_closed) return error.EndOfStream;
|
||||||
|
|
||||||
|
if (r.end == r.buffer.len and r.seek != 0) {
|
||||||
|
const pending = r.buffer[r.seek..r.end];
|
||||||
|
@memmove(r.buffer[0..pending.len], pending);
|
||||||
|
r.seek = 0;
|
||||||
|
r.end = pending.len;
|
||||||
|
}
|
||||||
|
const dest = r.buffer[r.end..];
|
||||||
|
if (dest.len == 0) return 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const rc = mbedtls_ssl_read(self.ssl.ptr, dest.ptr, dest.len);
|
||||||
|
if (isRetry(rc)) continue;
|
||||||
|
if (rc == err_peer_close_notify) {
|
||||||
|
self.peer_closed = true;
|
||||||
|
return error.EndOfStream;
|
||||||
|
}
|
||||||
|
// mbedtls_ssl_read reports a transport EOF that arrived without a
|
||||||
|
// close_notify as 0 (ssl_msg.c maps MBEDTLS_ERR_SSL_CONN_EOF to 0
|
||||||
|
// for application data); the raw code reaches us from the
|
||||||
|
// handshake it may run internally. Both are a truncation attack
|
||||||
|
// until the peer proves otherwise, so neither ends the stream
|
||||||
|
// cleanly.
|
||||||
|
if (rc == 0 or rc == err_conn_eof) {
|
||||||
|
// Debug, not warn: any client that drops its TCP connection
|
||||||
|
// reaches this, so a louder level would be a log-spam vector.
|
||||||
|
log.debug("peer closed the transport without close_notify", .{});
|
||||||
|
return self.failRead(error.TlsConnectionTruncated, rc);
|
||||||
|
}
|
||||||
|
if (rc < 0) {
|
||||||
|
report("ssl_read", rc);
|
||||||
|
return self.failRead(error.TlsFailed, rc);
|
||||||
|
}
|
||||||
|
r.end += @intCast(rc);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn failRead(self: *ServerStream, err: ReadError, rc: c_int) error{ReadFailed} {
|
||||||
|
self.read_err = err;
|
||||||
|
self.read_code = rc;
|
||||||
|
return error.ReadFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writerDrain(w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize {
|
||||||
|
const self: *ServerStream = @alignCast(@fieldParentPtr("writer_interface", w));
|
||||||
|
|
||||||
|
const buffered = w.buffered();
|
||||||
|
if (buffered.len != 0) return w.consume(try self.sslWrite(buffered));
|
||||||
|
|
||||||
|
for (data[0 .. data.len - 1]) |chunk| {
|
||||||
|
if (chunk.len == 0) continue;
|
||||||
|
return w.consume(try self.sslWrite(chunk));
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = data[data.len - 1];
|
||||||
|
if (last.len == 0) return w.consume(0);
|
||||||
|
var written: usize = 0;
|
||||||
|
for (0..splat) |_| {
|
||||||
|
const n = try self.sslWrite(last);
|
||||||
|
written += n;
|
||||||
|
if (n < last.len) break;
|
||||||
|
}
|
||||||
|
return w.consume(written);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sslWrite(self: *ServerStream, bytes: []const u8) Writer.Error!usize {
|
||||||
|
while (true) {
|
||||||
|
// A retry after WANT_WRITE must repeat the same arguments.
|
||||||
|
const rc = mbedtls_ssl_write(self.ssl.ptr, bytes.ptr, bytes.len);
|
||||||
|
if (isRetry(rc)) continue;
|
||||||
|
if (rc < 0) {
|
||||||
|
self.write_code = rc;
|
||||||
|
report("ssl_write", rc);
|
||||||
|
return error.WriteFailed;
|
||||||
|
}
|
||||||
|
return @intCast(rc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bioSend(bio: ?*anyopaque, buf: [*]const u8, len: usize) callconv(.c) c_int {
|
||||||
|
const self: *ServerStream = @ptrCast(@alignCast(bio.?));
|
||||||
|
const w = &self.net_writer.interface;
|
||||||
|
w.writeAll(buf[0..len]) catch return err_net_send_failed;
|
||||||
|
w.flush() catch return err_net_send_failed;
|
||||||
|
return @intCast(len);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bioRecv(bio: ?*anyopaque, buf: [*]u8, len: usize) callconv(.c) c_int {
|
||||||
|
const self: *ServerStream = @ptrCast(@alignCast(bio.?));
|
||||||
|
if (len == 0) return 0;
|
||||||
|
var data: [1][]u8 = .{buf[0..len]};
|
||||||
|
const n = self.net_reader.interface.readVec(&data) catch |err| switch (err) {
|
||||||
|
error.EndOfStream => return 0,
|
||||||
|
error.ReadFailed => return err_net_recv_failed,
|
||||||
|
};
|
||||||
|
// A zero-length transport buffer makes a short read impossible, but the
|
||||||
|
// interface permits it; ask Mbed TLS to come back rather than reporting EOF.
|
||||||
|
if (n == 0) return err_want_read;
|
||||||
|
return @intCast(n);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Heap storage for one Mbed TLS context whose layout stays opaque to Zig.
|
||||||
|
fn Block(comptime T: type) type {
|
||||||
|
return struct {
|
||||||
|
ptr: *T,
|
||||||
|
bytes: []align(context_alignment) u8,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
fn alloc(gpa: std.mem.Allocator, size: usize) error{OutOfMemory}!Self {
|
||||||
|
const bytes = try gpa.alignedAlloc(u8, ContextAlignment, size);
|
||||||
|
return .{ .ptr = @ptrCast(bytes.ptr), .bytes = bytes };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn free(self: Self, gpa: std.mem.Allocator) void {
|
||||||
|
gpa.free(self.bytes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Codes that mean "call me again with the same arguments".
|
||||||
|
///
|
||||||
|
/// MBEDTLS_ERR_SSL_{CRYPTO,ASYNC}_IN_PROGRESS are deliberately absent: nxdns
|
||||||
|
/// configures no async or opaque crypto, so those codes can only mean the
|
||||||
|
/// configuration is wrong. Retrying them would spin.
|
||||||
|
fn isRetry(rc: c_int) bool {
|
||||||
|
return rc == err_want_read or rc == err_want_write;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(rc: c_int, comptime op: []const u8, comptime failure: anytype) @TypeOf(failure)!void {
|
||||||
|
if (rc == 0) return;
|
||||||
|
report(op, rc);
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn report(comptime op: []const u8, rc: c_int) void {
|
||||||
|
var text: [160]u8 = undefined;
|
||||||
|
mbedtls_strerror(rc, &text, text.len);
|
||||||
|
log.warn("mbedtls_{s} failed: {s} ({d})", .{ op, std.mem.sliceTo(&text, 0), rc });
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Mbed TLS 3.6.7 surface ------------------------------------------------
|
||||||
|
// Signatures transcribed from include/mbedtls/{ssl,x509_crt,pk,entropy,ctr_drbg,error}.h
|
||||||
|
// and include/psa/crypto.h of the pinned release.
|
||||||
|
|
||||||
|
const SslContext = opaque {};
|
||||||
|
const SslConfig = opaque {};
|
||||||
|
const X509Crt = opaque {};
|
||||||
|
const PkContext = opaque {};
|
||||||
|
const EntropyContext = opaque {};
|
||||||
|
const CtrDrbgContext = opaque {};
|
||||||
|
|
||||||
|
/// `mbedtls_f_rng_t` (platform_util.h)
|
||||||
|
const RngFn = fn (p_rng: ?*anyopaque, output: [*]u8, output_len: usize) callconv(.c) c_int;
|
||||||
|
/// `mbedtls_ssl_send_t` (ssl.h)
|
||||||
|
const SendFn = fn (ctx: ?*anyopaque, buf: [*]const u8, len: usize) callconv(.c) c_int;
|
||||||
|
/// `mbedtls_ssl_recv_t` (ssl.h)
|
||||||
|
const RecvFn = fn (ctx: ?*anyopaque, buf: [*]u8, len: usize) callconv(.c) c_int;
|
||||||
|
/// `mbedtls_ssl_recv_timeout_t` (ssl.h)
|
||||||
|
const RecvTimeoutFn = fn (ctx: ?*anyopaque, buf: [*]u8, len: usize, timeout: u32) callconv(.c) c_int;
|
||||||
|
|
||||||
|
const ssl_transport_stream: c_int = 0;
|
||||||
|
const ssl_is_server: c_int = 1;
|
||||||
|
const ssl_verify_none: c_int = 0;
|
||||||
|
const ssl_preset_default: c_int = 0;
|
||||||
|
|
||||||
|
const err_conn_eof: c_int = -0x7280;
|
||||||
|
const err_peer_close_notify: c_int = -0x7880;
|
||||||
|
const err_want_read: c_int = -0x6900;
|
||||||
|
const err_want_write: c_int = -0x6880;
|
||||||
|
const err_net_recv_failed: c_int = -0x004C;
|
||||||
|
const err_net_send_failed: c_int = -0x004E;
|
||||||
|
|
||||||
|
extern fn mbedtls_ssl_init(ssl: *SslContext) void;
|
||||||
|
extern fn mbedtls_ssl_free(ssl: *SslContext) void;
|
||||||
|
extern fn mbedtls_ssl_setup(ssl: *SslContext, conf: *const SslConfig) c_int;
|
||||||
|
extern fn mbedtls_ssl_handshake(ssl: *SslContext) c_int;
|
||||||
|
extern fn mbedtls_ssl_read(ssl: *SslContext, buf: [*]u8, len: usize) c_int;
|
||||||
|
extern fn mbedtls_ssl_write(ssl: *SslContext, buf: [*]const u8, len: usize) c_int;
|
||||||
|
extern fn mbedtls_ssl_close_notify(ssl: *SslContext) c_int;
|
||||||
|
extern fn mbedtls_ssl_set_bio(
|
||||||
|
ssl: *SslContext,
|
||||||
|
p_bio: ?*anyopaque,
|
||||||
|
f_send: ?*const SendFn,
|
||||||
|
f_recv: ?*const RecvFn,
|
||||||
|
f_recv_timeout: ?*const RecvTimeoutFn,
|
||||||
|
) void;
|
||||||
|
|
||||||
|
extern fn mbedtls_ssl_config_init(conf: *SslConfig) void;
|
||||||
|
extern fn mbedtls_ssl_config_free(conf: *SslConfig) void;
|
||||||
|
extern fn mbedtls_ssl_config_defaults(
|
||||||
|
conf: *SslConfig,
|
||||||
|
endpoint: c_int,
|
||||||
|
transport: c_int,
|
||||||
|
preset: c_int,
|
||||||
|
) c_int;
|
||||||
|
extern fn mbedtls_ssl_conf_rng(conf: *SslConfig, f_rng: *const RngFn, p_rng: ?*anyopaque) void;
|
||||||
|
extern fn mbedtls_ssl_conf_authmode(conf: *SslConfig, authmode: c_int) void;
|
||||||
|
extern fn mbedtls_ssl_conf_own_cert(conf: *SslConfig, own_cert: *X509Crt, pk_key: *PkContext) c_int;
|
||||||
|
|
||||||
|
extern fn mbedtls_x509_crt_init(crt: *X509Crt) void;
|
||||||
|
extern fn mbedtls_x509_crt_free(crt: *X509Crt) void;
|
||||||
|
/// For PEM input, `buflen` must count the terminating zero byte.
|
||||||
|
extern fn mbedtls_x509_crt_parse(chain: *X509Crt, buf: [*]const u8, buflen: usize) c_int;
|
||||||
|
|
||||||
|
extern fn mbedtls_pk_init(ctx: *PkContext) void;
|
||||||
|
extern fn mbedtls_pk_free(ctx: *PkContext) void;
|
||||||
|
/// For PEM input, `keylen` must count the terminating zero byte.
|
||||||
|
extern fn mbedtls_pk_parse_key(
|
||||||
|
ctx: *PkContext,
|
||||||
|
key: [*]const u8,
|
||||||
|
keylen: usize,
|
||||||
|
pwd: ?[*]const u8,
|
||||||
|
pwdlen: usize,
|
||||||
|
f_rng: ?*const RngFn,
|
||||||
|
p_rng: ?*anyopaque,
|
||||||
|
) c_int;
|
||||||
|
|
||||||
|
/// `f_rng` must not be NULL. Returns 0 when the pair matches.
|
||||||
|
extern fn mbedtls_pk_check_pair(
|
||||||
|
pub_key: *const PkContext,
|
||||||
|
prv: *const PkContext,
|
||||||
|
f_rng: *const RngFn,
|
||||||
|
p_rng: ?*anyopaque,
|
||||||
|
) c_int;
|
||||||
|
|
||||||
|
extern fn mbedtls_entropy_init(ctx: *EntropyContext) void;
|
||||||
|
extern fn mbedtls_entropy_free(ctx: *EntropyContext) void;
|
||||||
|
extern fn mbedtls_entropy_func(data: ?*anyopaque, output: [*]u8, len: usize) callconv(.c) c_int;
|
||||||
|
|
||||||
|
extern fn mbedtls_ctr_drbg_init(ctx: *CtrDrbgContext) void;
|
||||||
|
extern fn mbedtls_ctr_drbg_free(ctx: *CtrDrbgContext) void;
|
||||||
|
extern fn mbedtls_ctr_drbg_seed(
|
||||||
|
ctx: *CtrDrbgContext,
|
||||||
|
f_entropy: *const fn (?*anyopaque, [*]u8, usize) callconv(.c) c_int,
|
||||||
|
p_entropy: ?*anyopaque,
|
||||||
|
custom: ?[*]const u8,
|
||||||
|
len: usize,
|
||||||
|
) c_int;
|
||||||
|
extern fn mbedtls_ctr_drbg_random(p_rng: ?*anyopaque, output: [*]u8, output_len: usize) callconv(.c) c_int;
|
||||||
|
|
||||||
|
extern fn mbedtls_strerror(errnum: c_int, buffer: [*]u8, buflen: usize) void;
|
||||||
|
|
||||||
|
/// `psa_status_t psa_crypto_init(void)` — TLS 1.3 runs its key schedule through PSA.
|
||||||
|
extern fn psa_crypto_init() i32;
|
||||||
|
|
||||||
|
// -- src/platform/mbedtls_shim.c -------------------------------------------
|
||||||
|
|
||||||
|
extern fn nx_sizeof_ssl_context() usize;
|
||||||
|
extern fn nx_sizeof_ssl_config() usize;
|
||||||
|
extern fn nx_sizeof_x509_crt() usize;
|
||||||
|
extern fn nx_sizeof_pk_context() usize;
|
||||||
|
extern fn nx_sizeof_entropy_context() usize;
|
||||||
|
extern fn nx_sizeof_ctr_drbg_context() usize;
|
||||||
|
extern fn nx_max_context_alignment() usize;
|
||||||
|
/// The public key of the first certificate in `crt`.
|
||||||
|
extern fn nx_x509_crt_pk(crt: *X509Crt) *PkContext;
|
||||||
|
|
||||||
|
// -- tests -----------------------------------------------------------------
|
||||||
|
|
||||||
|
test "ServerContext.init accepts the fixture cert and key" {
|
||||||
|
const fixtures = @import("test_fixtures");
|
||||||
|
const gpa = std.testing.allocator;
|
||||||
|
|
||||||
|
var ctx = try ServerContext.init(gpa, fixtures.cert_pem, fixtures.key_pem);
|
||||||
|
defer ctx.deinit(gpa);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "ServerContext.init rejects a truncated certificate" {
|
||||||
|
const fixtures = @import("test_fixtures");
|
||||||
|
const gpa = std.testing.allocator;
|
||||||
|
|
||||||
|
const truncated = try truncate(gpa, fixtures.cert_pem);
|
||||||
|
defer gpa.free(truncated);
|
||||||
|
|
||||||
|
try std.testing.expectError(
|
||||||
|
error.CertParse,
|
||||||
|
ServerContext.init(gpa, truncated, fixtures.key_pem),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "ServerContext.init rejects a truncated private key" {
|
||||||
|
const fixtures = @import("test_fixtures");
|
||||||
|
const gpa = std.testing.allocator;
|
||||||
|
|
||||||
|
const truncated = try truncate(gpa, fixtures.key_pem);
|
||||||
|
defer gpa.free(truncated);
|
||||||
|
|
||||||
|
try std.testing.expectError(
|
||||||
|
error.KeyParse,
|
||||||
|
ServerContext.init(gpa, fixtures.cert_pem, truncated),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "ServerContext.init rejects a key that is not the certificate's" {
|
||||||
|
const fixtures = @import("test_fixtures");
|
||||||
|
const gpa = std.testing.allocator;
|
||||||
|
|
||||||
|
try std.testing.expectError(
|
||||||
|
error.KeyMismatch,
|
||||||
|
ServerContext.init(gpa, fixtures.cert_pem, fixtures.mismatched_key_pem),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "the shim agrees with the alignment contexts are allocated at" {
|
||||||
|
try std.testing.expect(nx_max_context_alignment() <= context_alignment);
|
||||||
|
try std.testing.expect(nx_sizeof_ssl_context() > 0);
|
||||||
|
try std.testing.expect(nx_sizeof_ssl_config() > 0);
|
||||||
|
try std.testing.expect(nx_sizeof_x509_crt() > 0);
|
||||||
|
try std.testing.expect(nx_sizeof_pk_context() > 0);
|
||||||
|
try std.testing.expect(nx_sizeof_entropy_context() > 0);
|
||||||
|
try std.testing.expect(nx_sizeof_ctr_drbg_context() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drops the second half of a PEM document, keeping the header intact so the
|
||||||
|
/// parser fails on the body rather than on a missing "-----BEGIN" line.
|
||||||
|
fn truncate(gpa: std.mem.Allocator, pem: [:0]const u8) ![:0]const u8 {
|
||||||
|
return gpa.dupeZ(u8, pem[0 .. pem.len / 2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loopback_message = "nxdns loopback probe";
|
||||||
|
|
||||||
|
test "loopback echo between the mbedtls server and std.crypto.tls.Client" {
|
||||||
|
const build_options = @import("build_options");
|
||||||
|
if (!build_options.integration) return error.SkipZigTest;
|
||||||
|
|
||||||
|
const fixtures = @import("test_fixtures");
|
||||||
|
const gpa = std.testing.allocator;
|
||||||
|
|
||||||
|
var threaded: Io.Threaded = .init(gpa, .{});
|
||||||
|
defer threaded.deinit();
|
||||||
|
const io = threaded.io();
|
||||||
|
|
||||||
|
var ctx = try ServerContext.init(gpa, fixtures.cert_pem, fixtures.key_pem);
|
||||||
|
defer ctx.deinit(gpa);
|
||||||
|
|
||||||
|
const listen_address: Io.net.IpAddress = .{ .ip4 = .loopback(0) };
|
||||||
|
var server = try listen_address.listen(io, .{ .reuse_address = true });
|
||||||
|
defer server.deinit(io);
|
||||||
|
|
||||||
|
var server_task = try io.concurrent(echoOnce, .{ gpa, &ctx, io, &server });
|
||||||
|
const client_result = runEchoClient(io, server.socket.address);
|
||||||
|
// A client that never connects would leave the server blocked in `accept`.
|
||||||
|
const server_result = if (client_result) |_|
|
||||||
|
server_task.await(io)
|
||||||
|
else |_|
|
||||||
|
server_task.cancel(io);
|
||||||
|
|
||||||
|
try client_result;
|
||||||
|
try server_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a transport EOF without close_notify reads as a truncated stream" {
|
||||||
|
const build_options = @import("build_options");
|
||||||
|
if (!build_options.integration) return error.SkipZigTest;
|
||||||
|
|
||||||
|
const fixtures = @import("test_fixtures");
|
||||||
|
const gpa = std.testing.allocator;
|
||||||
|
|
||||||
|
var threaded: Io.Threaded = .init(gpa, .{});
|
||||||
|
defer threaded.deinit();
|
||||||
|
const io = threaded.io();
|
||||||
|
|
||||||
|
var ctx = try ServerContext.init(gpa, fixtures.cert_pem, fixtures.key_pem);
|
||||||
|
defer ctx.deinit(gpa);
|
||||||
|
|
||||||
|
const listen_address: Io.net.IpAddress = .{ .ip4 = .loopback(0) };
|
||||||
|
var server = try listen_address.listen(io, .{ .reuse_address = true });
|
||||||
|
defer server.deinit(io);
|
||||||
|
|
||||||
|
var server_task = try io.concurrent(expectTruncation, .{ gpa, &ctx, io, &server });
|
||||||
|
const client_result = runTruncatingClient(io, server.socket.address);
|
||||||
|
const server_result = if (client_result) |_|
|
||||||
|
server_task.await(io)
|
||||||
|
else |_|
|
||||||
|
server_task.cancel(io);
|
||||||
|
|
||||||
|
try client_result;
|
||||||
|
try server_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expectTruncation(
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
ctx: *ServerContext,
|
||||||
|
io: Io,
|
||||||
|
server: *Io.net.Server,
|
||||||
|
) anyerror!void {
|
||||||
|
var stream = try server.accept(io);
|
||||||
|
defer stream.close(io);
|
||||||
|
|
||||||
|
var read_buffer: [4096]u8 = undefined;
|
||||||
|
var write_buffer: [4096]u8 = undefined;
|
||||||
|
var tls: ServerStream = undefined;
|
||||||
|
try tls.accept(gpa, ctx, io, &stream, &read_buffer, &write_buffer);
|
||||||
|
defer tls.close(gpa);
|
||||||
|
|
||||||
|
var received: [loopback_message.len]u8 = undefined;
|
||||||
|
try tls.reader().readSliceAll(&received);
|
||||||
|
try std.testing.expectEqualStrings(loopback_message, &received);
|
||||||
|
|
||||||
|
var tail: [1]u8 = undefined;
|
||||||
|
try std.testing.expectError(error.ReadFailed, tls.reader().readSliceAll(&tail));
|
||||||
|
try std.testing.expectEqual(ServerStream.ReadError.TlsConnectionTruncated, tls.read_err.?);
|
||||||
|
try std.testing.expect(!tls.peer_closed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes one message, then drops the TCP connection without close_notify.
|
||||||
|
fn runTruncatingClient(io: Io, address: Io.net.IpAddress) anyerror!void {
|
||||||
|
const tls = std.crypto.tls;
|
||||||
|
|
||||||
|
var stream = try address.connect(io, .{ .mode = .stream });
|
||||||
|
defer stream.close(io);
|
||||||
|
|
||||||
|
var transport_read_buffer: [tls.Client.min_buffer_len]u8 = undefined;
|
||||||
|
var transport_write_buffer: [tls.Client.min_buffer_len]u8 = undefined;
|
||||||
|
var net_reader = stream.reader(io, &transport_read_buffer);
|
||||||
|
var net_writer = stream.writer(io, &transport_write_buffer);
|
||||||
|
|
||||||
|
var entropy: [tls.Client.Options.entropy_len]u8 = undefined;
|
||||||
|
io.random(&entropy);
|
||||||
|
|
||||||
|
var plaintext_read_buffer: [4096]u8 = undefined;
|
||||||
|
var plaintext_write_buffer: [4096]u8 = undefined;
|
||||||
|
|
||||||
|
var client = try tls.Client.init(&net_reader.interface, &net_writer.interface, .{
|
||||||
|
.host = .no_verification,
|
||||||
|
.ca = .no_verification,
|
||||||
|
.read_buffer = &plaintext_read_buffer,
|
||||||
|
.write_buffer = &plaintext_write_buffer,
|
||||||
|
.entropy = &entropy,
|
||||||
|
.realtime_now = Io.Timestamp.now(io, .real),
|
||||||
|
});
|
||||||
|
try net_writer.interface.flush();
|
||||||
|
|
||||||
|
try client.writer.writeAll(loopback_message);
|
||||||
|
try client.writer.flush();
|
||||||
|
try net_writer.interface.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn echoOnce(
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
ctx: *ServerContext,
|
||||||
|
io: Io,
|
||||||
|
server: *Io.net.Server,
|
||||||
|
) anyerror!void {
|
||||||
|
var stream = try server.accept(io);
|
||||||
|
defer stream.close(io);
|
||||||
|
|
||||||
|
var read_buffer: [4096]u8 = undefined;
|
||||||
|
var write_buffer: [4096]u8 = undefined;
|
||||||
|
var tls: ServerStream = undefined;
|
||||||
|
try tls.accept(gpa, ctx, io, &stream, &read_buffer, &write_buffer);
|
||||||
|
defer tls.close(gpa);
|
||||||
|
|
||||||
|
var received: [loopback_message.len]u8 = undefined;
|
||||||
|
try tls.reader().readSliceAll(&received);
|
||||||
|
try tls.writer().writeAll(&received);
|
||||||
|
try tls.writer().flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runEchoClient(io: Io, address: Io.net.IpAddress) anyerror!void {
|
||||||
|
const tls = std.crypto.tls;
|
||||||
|
|
||||||
|
var stream = try address.connect(io, .{ .mode = .stream });
|
||||||
|
defer stream.close(io);
|
||||||
|
|
||||||
|
var transport_read_buffer: [tls.Client.min_buffer_len]u8 = undefined;
|
||||||
|
var transport_write_buffer: [tls.Client.min_buffer_len]u8 = undefined;
|
||||||
|
var net_reader = stream.reader(io, &transport_read_buffer);
|
||||||
|
var net_writer = stream.writer(io, &transport_write_buffer);
|
||||||
|
|
||||||
|
var entropy: [tls.Client.Options.entropy_len]u8 = undefined;
|
||||||
|
io.random(&entropy);
|
||||||
|
|
||||||
|
var plaintext_read_buffer: [4096]u8 = undefined;
|
||||||
|
var plaintext_write_buffer: [4096]u8 = undefined;
|
||||||
|
|
||||||
|
var client = try tls.Client.init(&net_reader.interface, &net_writer.interface, .{
|
||||||
|
.host = .no_verification,
|
||||||
|
.ca = .no_verification,
|
||||||
|
.read_buffer = &plaintext_read_buffer,
|
||||||
|
.write_buffer = &plaintext_write_buffer,
|
||||||
|
.entropy = &entropy,
|
||||||
|
.realtime_now = Io.Timestamp.now(io, .real),
|
||||||
|
});
|
||||||
|
try net_writer.interface.flush();
|
||||||
|
|
||||||
|
try client.writer.writeAll(loopback_message);
|
||||||
|
try client.writer.flush();
|
||||||
|
try net_writer.interface.flush();
|
||||||
|
|
||||||
|
var echoed: [loopback_message.len]u8 = undefined;
|
||||||
|
try client.reader.readSliceAll(&echoed);
|
||||||
|
try std.testing.expectEqualStrings(loopback_message, &echoed);
|
||||||
|
|
||||||
|
try client.end();
|
||||||
|
try net_writer.interface.flush();
|
||||||
|
|
||||||
|
// The server's close_notify must arrive as a clean end of stream.
|
||||||
|
var tail: [1]u8 = undefined;
|
||||||
|
try std.testing.expectError(error.EndOfStream, client.reader.readSliceAll(&tail));
|
||||||
|
}
|
||||||
Vendored
+34
@@ -0,0 +1,34 @@
|
|||||||
|
# Test fixtures
|
||||||
|
|
||||||
|
`self_signed_cert.pem` and `self_signed_key.pem` are a **test fixture**. The
|
||||||
|
private key is **intentionally committed** to this public repository. It is not
|
||||||
|
a secret and it must never protect anything real.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
|
||||||
|
- EC P-256 (`prime256v1`), SHA-256 signature
|
||||||
|
- Subject `CN=localhost`
|
||||||
|
- SAN `DNS:localhost`, `IP:127.0.0.1`
|
||||||
|
- Validity 36500 days from generation
|
||||||
|
|
||||||
|
The loopback TLS test in `src/platform/tls_server.zig` uses this pair. Nothing
|
||||||
|
in the shipped binary reads it.
|
||||||
|
|
||||||
|
Regenerate with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
openssl ecparam -name prime256v1 -genkey -noout -out self_signed_key.pem
|
||||||
|
openssl req -new -x509 -key self_signed_key.pem -out self_signed_cert.pem \
|
||||||
|
-days 36500 -sha256 -subj "/CN=localhost" \
|
||||||
|
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
`mismatched_key.pem` is a second EC P-256 key with no certificate. It parses,
|
||||||
|
but it does not belong to `self_signed_cert.pem`, so `ServerContext.init` must
|
||||||
|
reject the pair with `error.KeyMismatch`. It is not a secret either.
|
||||||
|
|
||||||
|
Regenerate with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
openssl ecparam -name prime256v1 -genkey -noout -out mismatched_key.pem
|
||||||
|
```
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
//! Test fixtures embedded as a separate module so `@embedFile` paths stay
|
||||||
|
//! inside this directory (a module root may not embed files above itself).
|
||||||
|
//! See tests/fixtures/README.md — the private key is not a secret.
|
||||||
|
|
||||||
|
pub const cert_pem: [:0]const u8 = @embedFile("self_signed_cert.pem");
|
||||||
|
pub const key_pem: [:0]const u8 = @embedFile("self_signed_key.pem");
|
||||||
|
|
||||||
|
/// A well-formed private key that does not belong to `cert_pem`.
|
||||||
|
pub const mismatched_key_pem: [:0]const u8 = @embedFile("mismatched_key.pem");
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MHcCAQEEIHD4PI2utb6T0UJSzHJsS0OBYpxv9Yl1UQ9zkr++7oVWoAoGCCqGSM49
|
||||||
|
AwEHoUQDQgAE2tUQSqRzgr7lCvTiin1mhRe4lzQPv9/U62lKm4MAtTQA6TvlqS8M
|
||||||
|
n7c5kNuoR57s9hV+D64+pJqYMzNocaN5Yw==
|
||||||
|
-----END EC PRIVATE KEY-----
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBmzCCAUGgAwIBAgIUST2RAi7t5xJCdgasH59MhX+teKgwCgYIKoZIzj0EAwIw
|
||||||
|
FDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI2MDczMTIxMzA0MVoYDzIxMjYwNzA3
|
||||||
|
MjEzMDQxWjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjO
|
||||||
|
PQMBBwNCAATZ5CFaK693mW+3/GdkLUo0fSjtAy2wUr0WttWLbzYVv3R3k0RmKMOt
|
||||||
|
GTkYeN+H8gp281HW3sRcgGCJNNayzmU0o28wbTAdBgNVHQ4EFgQUb2BIm4JeUXZN
|
||||||
|
WJu1ioHlwZijo9QwHwYDVR0jBBgwFoAUb2BIm4JeUXZNWJu1ioHlwZijo9QwDwYD
|
||||||
|
VR0TAQH/BAUwAwEB/zAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwCgYIKoZI
|
||||||
|
zj0EAwIDSAAwRQIhAOOhKN8H3xayyKA/2e42d6bdMI7PYLyT4ihymzA9zrFfAiAO
|
||||||
|
0oHljOrRBIVz2PPxZ4YCOwM140tFesV50EtK3VnGZA==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MHcCAQEEILVQieUq8yy8RIuZoDGKWTNiPdq8cOBi9k33Zo1WunVYoAoGCCqGSM49
|
||||||
|
AwEHoUQDQgAE2eQhWiuvd5lvt/xnZC1KNH0o7QMtsFK9FrbVi282Fb90d5NEZijD
|
||||||
|
rRk5GHjfh/IKdvNR1t7EXIBgiTTWss5lNA==
|
||||||
|
-----END EC PRIVATE KEY-----
|
||||||
Reference in New Issue
Block a user