Add test vectors from Rooterberg and Wycheproof test suites

Closes #1517
This commit is contained in:
Frank Denis
2026-04-08 12:00:28 +02:00
parent 3e039f7c03
commit 4a6a7a010d
21 changed files with 1728 additions and 1 deletions
+9
View File
@@ -71,6 +71,15 @@ jobs:
zig build -Doptimize=ReleaseFast
rm -fr zig-cache zig-out
- name: Tests with zig
run: |
zig build test
zig build test-vectors
zig build test -Doptimize=ReleaseSafe
zig build test-vectors -Doptimize=ReleaseSafe
zig build test -Doptimize=ReleaseFast
zig build test-vectors -Doptimize=ReleaseFast
regular:
runs-on: ubuntu-latest
steps:
+1
View File
@@ -193,6 +193,7 @@ test/js.done
test/symbols/all-host-symbols.txt
test/symbols/internal-host-symbols.txt
test/symbols/internal-symbols.txt
test/vectors/cache/
testing
Vagrantfile
zig-cache
+26
View File
@@ -355,4 +355,30 @@ pub fn build(b: *std.Build) !void {
test_step.dependOn(&run_test.step);
}
}
if (build_tests) {
const offline = b.option(bool, "offline", "Skip downloading test vectors; use cached files only") orelse false;
const tv_options = b.addOptions();
tv_options.addOption(bool, "offline", offline);
tv_options.addOption([]const u8, "cache_dir", "test/vectors/cache");
const tv_mod = b.createModule(.{
.root_source_file = b.path("test/vectors/main.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
tv_mod.linkLibrary(static_lib);
tv_mod.addIncludePath(b.path("src/libsodium/include"));
tv_mod.addOptions("build_options", tv_options);
const tv_exe = b.addExecutable(.{
.name = "test-vectors",
.root_module = tv_mod,
});
b.installArtifact(tv_exe);
const run_tv = b.addRunArtifact(tv_exe);
const tv_step = b.step("test-vectors", "Run external test vectors (Rooterberg)");
tv_step.dependOn(&run_tv.step);
}
}
+1
View File
@@ -1028,6 +1028,7 @@ AC_CONFIG_FILES([Makefile
src/libsodium/include/Makefile
src/libsodium/include/sodium/version.h
test/default/Makefile
test/vectors/Makefile
test/Makefile
])
AC_OUTPUT
+2 -1
View File
@@ -1,5 +1,6 @@
SUBDIRS = \
default
default \
vectors
EXTRA_DIST = \
quirks/quirks.h
+16
View File
@@ -0,0 +1,16 @@
EXTRA_DIST = \
fetch.zig \
main.zig \
parse.zig \
suites/aead.zig \
suites/box.zig \
suites/eddsa.zig \
suites/hash.zig \
suites/kdf.zig \
suites/kem.zig \
suites/mac.zig \
suites/pwhash.zig \
suites/secretbox.zig \
suites/stream.zig \
suites/xdh.zig \
suites/xof.zig
+153
View File
@@ -0,0 +1,153 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const Io = std.Io;
const Dir = Io.Dir;
pub const Source = struct {
base_url: []const u8,
category: []const u8,
filename: []const u8,
suite_tag: SuiteTag,
};
pub const SuiteTag = enum {
aead_chacha20poly1305_ietf,
aead_xchacha20poly1305,
aead_aes256gcm,
aead_aegis128l,
aead_aegis256,
hash_sha256,
hash_sha512,
hash_blake2b,
hash_sha3_256,
hash_sha3_512,
mac_hmacsha256,
mac_hmacsha512,
mac_siphash24,
mac_siphashx24,
onetimeauth_poly1305,
xdh_x25519,
eddsa_ed25519_verify,
eddsa_ed25519_cofactored_verify,
eddsa_ed25519_sign,
secretbox_xsalsa20,
secretbox_xchacha20,
box_xsalsa20,
box_xchacha20,
stream_salsa20,
stream_xsalsa20,
kdf_hkdf_sha256,
kdf_hkdf_sha512,
pwhash_scrypt,
xof_shake128,
xof_shake256,
kem_mlkem768,
};
const rooterberg_base = "https://raw.githubusercontent.com/bleichenbacher-daniel/Rooterberg/master/test_vectors";
const wycheproof_base = "https://raw.githubusercontent.com/C2SP/wycheproof/master/testvectors_v1";
pub const rooterberg_sources = [_]Source{
.{ .base_url = rooterberg_base, .category = "aead", .filename = "chacha20_poly1305.json", .suite_tag = .aead_chacha20poly1305_ietf },
.{ .base_url = rooterberg_base, .category = "aead", .filename = "xchacha20_poly1305.json", .suite_tag = .aead_xchacha20poly1305 },
.{ .base_url = rooterberg_base, .category = "aead", .filename = "aes_gcm_256_96_128.json", .suite_tag = .aead_aes256gcm },
.{ .base_url = rooterberg_base, .category = "aead", .filename = "aegis128_l_256.json", .suite_tag = .aead_aegis128l },
.{ .base_url = rooterberg_base, .category = "aead", .filename = "aegis256_256.json", .suite_tag = .aead_aegis256 },
.{ .base_url = rooterberg_base, .category = "message_digest", .filename = "sha_256.json", .suite_tag = .hash_sha256 },
.{ .base_url = rooterberg_base, .category = "message_digest", .filename = "sha_512.json", .suite_tag = .hash_sha512 },
.{ .base_url = rooterberg_base, .category = "message_digest", .filename = "blake2b.json", .suite_tag = .hash_blake2b },
.{ .base_url = rooterberg_base, .category = "message_digest", .filename = "sha3_256.json", .suite_tag = .hash_sha3_256 },
.{ .base_url = rooterberg_base, .category = "message_digest", .filename = "sha3_512.json", .suite_tag = .hash_sha3_512 },
.{ .base_url = rooterberg_base, .category = "mac", .filename = "hmac_sha256_256.json", .suite_tag = .mac_hmacsha256 },
.{ .base_url = rooterberg_base, .category = "mac", .filename = "hmac_sha512_512.json", .suite_tag = .mac_hmacsha512 },
.{ .base_url = rooterberg_base, .category = "mac", .filename = "sip_hash_2_4.json", .suite_tag = .mac_siphash24 },
.{ .base_url = rooterberg_base, .category = "mac", .filename = "sip_hash128_2_4.json", .suite_tag = .mac_siphashx24 },
.{ .base_url = rooterberg_base, .category = "one_time_mac", .filename = "poly1305.json", .suite_tag = .onetimeauth_poly1305 },
.{ .base_url = rooterberg_base, .category = "xdh", .filename = "x25519.json", .suite_tag = .xdh_x25519 },
.{ .base_url = rooterberg_base, .category = "eddsa", .filename = "ed25519.json", .suite_tag = .eddsa_ed25519_verify },
.{ .base_url = rooterberg_base, .category = "eddsa", .filename = "ed25519_cofactored.json", .suite_tag = .eddsa_ed25519_cofactored_verify },
.{ .base_url = rooterberg_base, .category = "eddsa", .filename = "ed25519_sign.json", .suite_tag = .eddsa_ed25519_sign },
.{ .base_url = rooterberg_base, .category = "auth_enc", .filename = "nacl_xsalsa20_poly1305.json", .suite_tag = .secretbox_xsalsa20 },
.{ .base_url = rooterberg_base, .category = "auth_enc", .filename = "nacl_xchacha20_poly1305.json", .suite_tag = .secretbox_xchacha20 },
.{ .base_url = rooterberg_base, .category = "nacl_crypto_box", .filename = "nacl_crypto_box_curve25519_xsalsa20_poly1305.json", .suite_tag = .box_xsalsa20 },
.{ .base_url = rooterberg_base, .category = "nacl_crypto_box", .filename = "nacl_crypto_box_curve25519_xchacha20_poly1305.json", .suite_tag = .box_xchacha20 },
.{ .base_url = rooterberg_base, .category = "ind_cpa", .filename = "salsa20_256_64.json", .suite_tag = .stream_salsa20 },
.{ .base_url = rooterberg_base, .category = "ind_cpa", .filename = "xsalsa20_256_192.json", .suite_tag = .stream_xsalsa20 },
.{ .base_url = rooterberg_base, .category = "kdf", .filename = "hkdf_sha256.json", .suite_tag = .kdf_hkdf_sha256 },
.{ .base_url = rooterberg_base, .category = "kdf", .filename = "hkdf_sha512.json", .suite_tag = .kdf_hkdf_sha512 },
.{ .base_url = rooterberg_base, .category = "kdf", .filename = "scrypt.json", .suite_tag = .pwhash_scrypt },
.{ .base_url = rooterberg_base, .category = "xof", .filename = "shake128.json", .suite_tag = .xof_shake128 },
.{ .base_url = rooterberg_base, .category = "xof", .filename = "shake256.json", .suite_tag = .xof_shake256 },
.{ .base_url = wycheproof_base, .category = "", .filename = "mlkem_768_test.json", .suite_tag = .kem_mlkem768 },
};
pub fn cacheName(buf: []u8, category: []const u8, filename: []const u8) []const u8 {
if (category.len == 0)
return std.fmt.bufPrint(buf, "{s}", .{filename}) catch buf[0..0];
return std.fmt.bufPrint(buf, "{s}_{s}", .{ category, filename }) catch buf[0..0];
}
pub fn ensureCached(
allocator: Allocator,
io: Io,
cache_dir: Dir,
sources: []const Source,
offline: bool,
) !void {
if (offline) return;
var http_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer http_arena.deinit();
var client: std.http.Client = .{ .allocator = http_arena.allocator(), .io = io };
defer client.deinit();
for (sources) |src| {
var name_buf: [256]u8 = undefined;
const name = cacheName(&name_buf, src.category, src.filename);
if (cache_dir.openFile(io, name, .{})) |file| {
file.close(io);
} else |err| {
if (err != error.FileNotFound) return err;
try download(allocator, &client, io, cache_dir, src, name);
}
}
}
fn download(
allocator: Allocator,
client: *std.http.Client,
io: Io,
cache_dir: Dir,
src: Source,
dest_name: []const u8,
) !void {
var url_buf: [512]u8 = undefined;
const url = (if (src.category.len == 0)
std.fmt.bufPrint(&url_buf, "{s}/{s}", .{ src.base_url, src.filename })
else
std.fmt.bufPrint(&url_buf, "{s}/{s}/{s}", .{ src.base_url, src.category, src.filename })) catch return error.UrlTooLong;
var body = std.Io.Writer.Allocating.init(allocator);
defer body.deinit();
const result = try client.fetch(.{
.location = .{ .url = url },
.response_writer = &body.writer,
});
if (result.status != .ok) {
std.debug.print("HTTP {d} fetching {s}\n", .{ @intFromEnum(result.status), url });
return error.HttpFetchFailed;
}
try cache_dir.writeFile(io, .{ .sub_path = dest_name, .data = body.written() });
}
pub fn loadCached(allocator: Allocator, io: Io, cache_dir: Dir, src: Source) !?[]const u8 {
var name_buf: [256]u8 = undefined;
const name = cacheName(&name_buf, src.category, src.filename);
return cache_dir.readFileAlloc(io, name, allocator, .unlimited) catch |err| {
if (err == error.FileNotFound) return null;
return err;
};
}
+151
View File
@@ -0,0 +1,151 @@
const std = @import("std");
const Io = std.Io;
const Dir = Io.Dir;
const Allocator = std.mem.Allocator;
const options = @import("build_options");
const fetch = @import("fetch.zig");
const parse = @import("parse.zig");
const aead = @import("suites/aead.zig");
const hash = @import("suites/hash.zig");
const mac = @import("suites/mac.zig");
const xdh = @import("suites/xdh.zig");
const eddsa = @import("suites/eddsa.zig");
const secretbox = @import("suites/secretbox.zig");
const box = @import("suites/box.zig");
const stream = @import("suites/stream.zig");
const kdf = @import("suites/kdf.zig");
const pwhash = @import("suites/pwhash.zig");
const xof = @import("suites/xof.zig");
const kem = @import("suites/kem.zig");
const c = @cImport(@cInclude("sodium.h"));
const Result = parse.Result;
fn runSuite(allocator: Allocator, data: []const u8, tag: fetch.SuiteTag) !Result {
return switch (tag) {
.aead_chacha20poly1305_ietf,
.aead_xchacha20poly1305,
.aead_aes256gcm,
.aead_aegis128l,
.aead_aegis256,
=> aead.run(allocator, data, tag),
.hash_sha256,
.hash_sha512,
.hash_blake2b,
.hash_sha3_256,
.hash_sha3_512,
=> hash.run(allocator, data, tag),
.mac_hmacsha256,
.mac_hmacsha512,
.mac_siphash24,
.mac_siphashx24,
.onetimeauth_poly1305,
=> mac.run(allocator, data, tag),
.xdh_x25519 => xdh.run(allocator, data),
.eddsa_ed25519_verify => eddsa.runVerify(allocator, data, false),
.eddsa_ed25519_cofactored_verify => eddsa.runVerify(allocator, data, true),
.eddsa_ed25519_sign => eddsa.runSign(allocator, data),
.secretbox_xsalsa20,
.secretbox_xchacha20,
=> secretbox.run(allocator, data, tag),
.box_xsalsa20,
.box_xchacha20,
=> box.run(allocator, data, tag),
.stream_salsa20,
.stream_xsalsa20,
=> stream.run(allocator, data, tag),
.kdf_hkdf_sha256,
.kdf_hkdf_sha512,
=> kdf.run(allocator, data, tag),
.pwhash_scrypt => pwhash.run(allocator, data),
.xof_shake128,
.xof_shake256,
=> xof.run(allocator, data, tag),
.kem_mlkem768 => kem.run(allocator, data),
};
}
pub fn main(init: std.process.Init) !void {
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
defer if (debug_allocator.deinit() == .leak) @panic("memory leak detected");
const allocator = debug_allocator.allocator();
const io = init.io;
if (c.sodium_init() < 0) return error.SodiumInitFailed;
const cache_dir = Dir.cwd().createDirPathOpen(io, options.cache_dir, .{}) catch |err| {
std.debug.print("Cannot open cache directory '{s}': {}\n", .{ options.cache_dir, err });
return err;
};
fetch.ensureCached(allocator, io, cache_dir, &fetch.rooterberg_sources, options.offline) catch |err| {
std.debug.print("Fetch failed: {}\n", .{err});
if (!options.offline) return err;
};
var total = Result{};
var suite_count: u32 = 0;
var suite_fail: u32 = 0;
for (&fetch.rooterberg_sources) |*src| {
const data = fetch.loadCached(allocator, io, cache_dir, src.*) catch |err| {
std.debug.print("Cannot read {s}/{s}: {}\n", .{ src.category, src.filename, err });
total.skipped += 1;
continue;
} orelse {
if (!options.offline) {
std.debug.print("SKIP {s}/{s}: not cached\n", .{ src.category, src.filename });
}
total.skipped += 1;
continue;
};
defer allocator.free(data);
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const result = runSuite(arena.allocator(), data, src.suite_tag) catch |err| {
std.debug.print("ERROR {s}/{s}: {}\n", .{ src.category, src.filename, err });
suite_fail += 1;
continue;
};
suite_count += 1;
total.passed += result.passed;
total.failed += result.failed;
total.skipped += result.skipped;
const status: []const u8 = if (result.failed > 0) "FAIL" else "ok";
std.debug.print("{s}: {s}/{s} -- {d} passed, {d} failed, {d} skipped\n", .{
status,
src.category,
src.filename,
result.passed,
result.failed,
result.skipped,
});
}
std.debug.print("\n{d} suites, {d} passed, {d} failed, {d} skipped\n", .{
suite_count,
total.passed,
total.failed,
total.skipped,
});
if (total.failed > 0 or suite_fail > 0) {
return error.TestsFailed;
}
}
+490
View File
@@ -0,0 +1,490 @@
const std = @import("std");
const json = std.json;
const Allocator = std.mem.Allocator;
pub const Validity = enum { valid, invalid, acceptable };
pub const Result = struct {
passed: u32 = 0,
failed: u32 = 0,
skipped: u32 = 0,
};
pub const AeadVector = struct {
tc_id: u32,
key: []const u8,
nonce: []const u8,
aad: []const u8,
msg: []const u8,
ct: []const u8,
tag: []const u8,
validity: Validity,
};
pub const HashVector = struct {
tc_id: u32,
msg: []const u8,
digest: []const u8,
out_len: usize,
key: []const u8,
validity: Validity,
};
pub const MacVector = struct {
tc_id: u32,
key: []const u8,
msg: []const u8,
mac: []const u8,
validity: Validity,
};
pub const XdhVector = struct {
tc_id: u32,
public_key: []const u8,
private_key: []const u8,
shared: []const u8,
validity: Validity,
};
pub const EddsaVerifyVector = struct {
tc_id: u32,
public_key: []const u8,
msg: []const u8,
sig: []const u8,
validity: Validity,
};
pub const EddsaSignVector = struct {
tc_id: u32,
private_key: []const u8,
public_key: []const u8,
msg: []const u8,
sig: []const u8,
validity: Validity,
};
pub const AuthEncVector = struct {
tc_id: u32,
key: []const u8,
nonce: []const u8,
msg: []const u8,
ct: []const u8,
tag: []const u8,
validity: Validity,
};
pub const BoxVector = struct {
tc_id: u32,
public_key: []const u8,
private_key: []const u8,
nonce: []const u8,
msg: []const u8,
ct: []const u8,
validity: Validity,
shared_zero: bool,
};
pub const StreamVector = struct {
tc_id: u32,
key: []const u8,
nonce: []const u8,
msg: []const u8,
ct: []const u8,
validity: Validity,
};
pub const HkdfVector = struct {
tc_id: u32,
ikm: []const u8,
salt: []const u8,
info: []const u8,
out_len: usize,
okm: []const u8,
validity: Validity,
};
pub const ScryptVector = struct {
tc_id: u32,
password: []const u8,
salt: []const u8,
n: u64,
r: u32,
p: u32,
dk_len: usize,
dk: []const u8,
validity: Validity,
};
pub const XofVector = struct {
tc_id: u32,
msg: []const u8,
out_len: usize,
output: []const u8,
validity: Validity,
};
pub const MlkemVector = struct {
tc_id: u32,
seed: []const u8,
ek: []const u8,
ct: []const u8,
ss: []const u8,
validity: Validity,
};
const TestFile = struct {
parsed: json.Parsed(json.Value),
root: json.ObjectMap,
tests: []const json.Value,
};
fn openTestFile(allocator: Allocator, data: []const u8) !TestFile {
const parsed = try json.parseFromSlice(json.Value, allocator, data, .{
.ignore_unknown_fields = true,
});
const root = getObj(parsed.value) orelse return error.InvalidVector;
const tests = getArr(root.get("tests") orelse return error.InvalidVector) orelse return error.InvalidVector;
return .{ .parsed = parsed, .root = root, .tests = tests };
}
fn getObj(val: json.Value) ?json.ObjectMap {
return switch (val) {
.object => |o| o,
else => null,
};
}
fn getArr(val: json.Value) ?[]const json.Value {
return switch (val) {
.array => |a| a.items,
else => null,
};
}
fn getStr(obj: json.ObjectMap, key: []const u8) ?[]const u8 {
const v = obj.get(key) orelse return null;
return switch (v) {
.string => |s| s,
else => null,
};
}
fn getInt(obj: json.ObjectMap, key: []const u8) ?i64 {
const v = obj.get(key) orelse return null;
return switch (v) {
.integer => |i| i,
else => null,
};
}
fn getBool(obj: json.ObjectMap, key: []const u8) ?bool {
const v = obj.get(key) orelse return null;
return switch (v) {
.bool => |b| b,
else => null,
};
}
fn hasFlag(obj: json.ObjectMap, flag: []const u8) bool {
const flags = switch (obj.get("flags") orelse return false) {
.array => |a| a.items,
else => return false,
};
for (flags) |f| {
const s = switch (f) {
.string => |s| s,
else => continue,
};
if (std.mem.eql(u8, s, flag)) return true;
}
return false;
}
fn hexField(allocator: Allocator, obj: json.ObjectMap, key: []const u8) ![]const u8 {
const s = getStr(obj, key) orelse return &.{};
const out = try allocator.alloc(u8, s.len / 2);
return std.fmt.hexToBytes(out, s) catch |err| {
allocator.free(out);
return err;
};
}
fn rooterbergValidity(obj: json.ObjectMap) Validity {
const v = getBool(obj, "valid") orelse return .valid;
return if (v) .valid else .invalid;
}
pub fn loadAeadVectors(allocator: Allocator, data: []const u8) ![]AeadVector {
var tf = try openTestFile(allocator, data);
defer tf.parsed.deinit();
var list = try std.ArrayList(AeadVector).initCapacity(allocator, tf.tests.len);
for (tf.tests) |t| {
const obj = getObj(t) orelse continue;
try list.append(allocator, .{
.tc_id = @intCast(getInt(obj, "tcId") orelse continue),
.key = try hexField(allocator, obj, "key"),
.nonce = try hexField(allocator, obj, "iv"),
.aad = try hexField(allocator, obj, "aad"),
.msg = try hexField(allocator, obj, "msg"),
.ct = try hexField(allocator, obj, "ct"),
.tag = try hexField(allocator, obj, "tag"),
.validity = rooterbergValidity(obj),
});
}
return list.items;
}
pub fn loadHashVectors(allocator: Allocator, data: []const u8) ![]HashVector {
var tf = try openTestFile(allocator, data);
defer tf.parsed.deinit();
var out_len: usize = 0;
if (getObj(tf.root.get("algorithm") orelse return error.InvalidVector)) |algo| {
if (getInt(algo, "digestSize")) |ds| out_len = @intCast(@divExact(ds, 8));
}
var list = try std.ArrayList(HashVector).initCapacity(allocator, tf.tests.len);
for (tf.tests) |t| {
const obj = getObj(t) orelse continue;
try list.append(allocator, .{
.tc_id = @intCast(getInt(obj, "tcId") orelse continue),
.msg = try hexField(allocator, obj, "msg"),
.digest = try hexField(allocator, obj, "digest"),
.out_len = out_len,
.key = try hexField(allocator, obj, "key"),
.validity = rooterbergValidity(obj),
});
}
return list.items;
}
pub fn loadMacVectors(allocator: Allocator, data: []const u8) ![]MacVector {
var tf = try openTestFile(allocator, data);
defer tf.parsed.deinit();
var list = try std.ArrayList(MacVector).initCapacity(allocator, tf.tests.len);
for (tf.tests) |t| {
const obj = getObj(t) orelse continue;
try list.append(allocator, .{
.tc_id = @intCast(getInt(obj, "tcId") orelse continue),
.key = try hexField(allocator, obj, "key"),
.msg = try hexField(allocator, obj, "msg"),
.mac = try hexField(allocator, obj, "mac"),
.validity = rooterbergValidity(obj),
});
}
return list.items;
}
pub fn loadXdhVectors(allocator: Allocator, data: []const u8) ![]XdhVector {
var tf = try openTestFile(allocator, data);
defer tf.parsed.deinit();
var list = try std.ArrayList(XdhVector).initCapacity(allocator, tf.tests.len);
for (tf.tests) |t| {
const obj = getObj(t) orelse continue;
try list.append(allocator, .{
.tc_id = @intCast(getInt(obj, "tcId") orelse continue),
.public_key = try hexField(allocator, obj, "publicKey"),
.private_key = try hexField(allocator, obj, "privateKey"),
.shared = try hexField(allocator, obj, "shared"),
.validity = rooterbergValidity(obj),
});
}
return list.items;
}
pub fn loadEddsaVerifyVectors(allocator: Allocator, data: []const u8) ![]EddsaVerifyVector {
var tf = try openTestFile(allocator, data);
defer tf.parsed.deinit();
var list = try std.ArrayList(EddsaVerifyVector).initCapacity(allocator, tf.tests.len);
for (tf.tests) |t| {
const obj = getObj(t) orelse continue;
try list.append(allocator, .{
.tc_id = @intCast(getInt(obj, "tcId") orelse continue),
.public_key = try hexField(allocator, obj, "publicKey"),
.msg = try hexField(allocator, obj, "msg"),
.sig = try hexField(allocator, obj, "sig"),
.validity = rooterbergValidity(obj),
});
}
return list.items;
}
pub fn loadEddsaSignVectors(allocator: Allocator, data: []const u8) ![]EddsaSignVector {
var tf = try openTestFile(allocator, data);
defer tf.parsed.deinit();
var list = try std.ArrayList(EddsaSignVector).initCapacity(allocator, tf.tests.len);
for (tf.tests) |t| {
const obj = getObj(t) orelse continue;
try list.append(allocator, .{
.tc_id = @intCast(getInt(obj, "tcId") orelse continue),
.private_key = try hexField(allocator, obj, "privateKey"),
.public_key = try hexField(allocator, obj, "publicKey"),
.msg = try hexField(allocator, obj, "msg"),
.sig = try hexField(allocator, obj, "sig"),
.validity = rooterbergValidity(obj),
});
}
return list.items;
}
pub fn loadAuthEncVectors(allocator: Allocator, data: []const u8) ![]AuthEncVector {
var tf = try openTestFile(allocator, data);
defer tf.parsed.deinit();
var list = try std.ArrayList(AuthEncVector).initCapacity(allocator, tf.tests.len);
for (tf.tests) |t| {
const obj = getObj(t) orelse continue;
try list.append(allocator, .{
.tc_id = @intCast(getInt(obj, "tcId") orelse continue),
.key = try hexField(allocator, obj, "key"),
.nonce = try hexField(allocator, obj, "iv"),
.msg = try hexField(allocator, obj, "msg"),
.ct = try hexField(allocator, obj, "ct"),
.tag = try hexField(allocator, obj, "tag"),
.validity = rooterbergValidity(obj),
});
}
return list.items;
}
pub fn loadBoxVectors(allocator: Allocator, data: []const u8) ![]BoxVector {
var tf = try openTestFile(allocator, data);
defer tf.parsed.deinit();
var list = try std.ArrayList(BoxVector).initCapacity(allocator, tf.tests.len);
for (tf.tests) |t| {
const obj = getObj(t) orelse continue;
try list.append(allocator, .{
.tc_id = @intCast(getInt(obj, "tcId") orelse continue),
.public_key = try hexField(allocator, obj, "publicKey"),
.private_key = try hexField(allocator, obj, "privateKey"),
.nonce = try hexField(allocator, obj, "iv"),
.msg = try hexField(allocator, obj, "msg"),
.ct = try hexField(allocator, obj, "ct"),
.validity = rooterbergValidity(obj),
.shared_zero = hasFlag(obj, "SharedZero"),
});
}
return list.items;
}
pub fn loadStreamVectors(allocator: Allocator, data: []const u8) ![]StreamVector {
var tf = try openTestFile(allocator, data);
defer tf.parsed.deinit();
var list = try std.ArrayList(StreamVector).initCapacity(allocator, tf.tests.len);
for (tf.tests) |t| {
const obj = getObj(t) orelse continue;
try list.append(allocator, .{
.tc_id = @intCast(getInt(obj, "tcId") orelse continue),
.key = try hexField(allocator, obj, "key"),
.nonce = try hexField(allocator, obj, "iv"),
.msg = try hexField(allocator, obj, "msg"),
.ct = try hexField(allocator, obj, "ct"),
.validity = rooterbergValidity(obj),
});
}
return list.items;
}
pub fn loadHkdfVectors(allocator: Allocator, data: []const u8) ![]HkdfVector {
var tf = try openTestFile(allocator, data);
defer tf.parsed.deinit();
var list = try std.ArrayList(HkdfVector).initCapacity(allocator, tf.tests.len);
for (tf.tests) |t| {
const obj = getObj(t) orelse continue;
const out_len: usize = @intCast(getInt(obj, "outLen") orelse continue);
try list.append(allocator, .{
.tc_id = @intCast(getInt(obj, "tcId") orelse continue),
.ikm = try hexField(allocator, obj, "ikm"),
.salt = try hexField(allocator, obj, "salt"),
.info = try hexField(allocator, obj, "info"),
.out_len = out_len,
.okm = try hexField(allocator, obj, "okm"),
.validity = rooterbergValidity(obj),
});
}
return list.items;
}
pub fn loadScryptVectors(allocator: Allocator, data: []const u8) ![]ScryptVector {
var tf = try openTestFile(allocator, data);
defer tf.parsed.deinit();
var list = try std.ArrayList(ScryptVector).initCapacity(allocator, tf.tests.len);
for (tf.tests) |t| {
const obj = getObj(t) orelse continue;
try list.append(allocator, .{
.tc_id = @intCast(getInt(obj, "tcId") orelse continue),
.password = try hexField(allocator, obj, "password"),
.salt = try hexField(allocator, obj, "salt"),
.n = @intCast(getInt(obj, "n") orelse continue),
.r = @intCast(getInt(obj, "r") orelse continue),
.p = @intCast(getInt(obj, "p") orelse continue),
.dk_len = @intCast(getInt(obj, "dkLen") orelse continue),
.dk = try hexField(allocator, obj, "dk"),
.validity = rooterbergValidity(obj),
});
}
return list.items;
}
pub fn loadXofVectors(allocator: Allocator, data: []const u8) ![]XofVector {
var tf = try openTestFile(allocator, data);
defer tf.parsed.deinit();
var list = try std.ArrayList(XofVector).initCapacity(allocator, tf.tests.len);
for (tf.tests) |t| {
const obj = getObj(t) orelse continue;
const out_len: usize = @intCast(getInt(obj, "size") orelse
(getInt(obj, "outLen") orelse continue));
try list.append(allocator, .{
.tc_id = @intCast(getInt(obj, "tcId") orelse continue),
.msg = try hexField(allocator, obj, "ikm"),
.out_len = out_len,
.output = try hexField(allocator, obj, "okm"),
.validity = rooterbergValidity(obj),
});
}
return list.items;
}
fn wycheproofValidity(obj: json.ObjectMap) Validity {
const s = getStr(obj, "result") orelse return .valid;
if (std.mem.eql(u8, s, "valid")) return .valid;
if (std.mem.eql(u8, s, "acceptable")) return .acceptable;
return .invalid;
}
pub fn loadMlkemVectors(allocator: Allocator, data: []const u8) ![]MlkemVector {
const parsed = try json.parseFromSlice(json.Value, allocator, data, .{
.ignore_unknown_fields = true,
});
defer parsed.deinit();
const root = getObj(parsed.value) orelse return error.InvalidVector;
const groups = getArr(root.get("testGroups") orelse return error.InvalidVector) orelse return error.InvalidVector;
var list: std.ArrayList(MlkemVector) = .empty;
for (groups) |g| {
const group = getObj(g) orelse continue;
const tests = getArr(group.get("tests") orelse continue) orelse continue;
for (tests) |t| {
const obj = getObj(t) orelse continue;
try list.append(allocator, .{
.tc_id = @intCast(getInt(obj, "tcId") orelse continue),
.seed = try hexField(allocator, obj, "seed"),
.ek = try hexField(allocator, obj, "ek"),
.ct = try hexField(allocator, obj, "c"),
.ss = try hexField(allocator, obj, "K"),
.validity = wycheproofValidity(obj),
});
}
}
return list.items;
}
+154
View File
@@ -0,0 +1,154 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const parse = @import("../parse.zig");
const fetch = @import("../fetch.zig");
const c = @cImport(@cInclude("sodium.h"));
const Result = parse.Result;
const EncryptDetachedFn = *const fn (
[*c]u8,
[*c]u8,
[*c]c_ulonglong,
[*c]const u8,
c_ulonglong,
[*c]const u8,
c_ulonglong,
[*c]const u8,
[*c]const u8,
[*c]const u8,
) callconv(.c) c_int;
const DecryptDetachedFn = *const fn (
[*c]u8,
[*c]u8,
[*c]const u8,
c_ulonglong,
[*c]const u8,
[*c]const u8,
c_ulonglong,
[*c]const u8,
[*c]const u8,
) callconv(.c) c_int;
fn runAead(
allocator: Allocator,
vectors: []const parse.AeadVector,
encrypt_fn: EncryptDetachedFn,
decrypt_fn: DecryptDetachedFn,
comptime tag_len: usize,
) !Result {
var result = Result{};
for (vectors) |v| {
if (v.tag.len != tag_len) {
result.skipped += 1;
continue;
}
if (v.validity == .valid) {
const ct_buf = try allocator.alloc(u8, v.msg.len);
var mac: [tag_len]u8 = undefined;
_ = encrypt_fn(
ct_buf.ptr,
&mac,
null,
v.msg.ptr,
@intCast(v.msg.len),
v.aad.ptr,
@intCast(v.aad.len),
null,
v.nonce.ptr,
v.key.ptr,
);
if (!std.mem.eql(u8, ct_buf, v.ct) or !std.mem.eql(u8, &mac, v.tag)) {
std.debug.print("FAIL tc={d}: encrypt mismatch\n", .{v.tc_id});
result.failed += 1;
continue;
}
const pt_buf = try allocator.alloc(u8, v.ct.len);
const ret = decrypt_fn(
pt_buf.ptr,
null,
v.ct.ptr,
@intCast(v.ct.len),
v.tag.ptr,
v.aad.ptr,
@intCast(v.aad.len),
v.nonce.ptr,
v.key.ptr,
);
if (ret != 0 or !std.mem.eql(u8, pt_buf, v.msg)) {
std.debug.print("FAIL tc={d}: decrypt mismatch\n", .{v.tc_id});
result.failed += 1;
} else {
result.passed += 1;
}
} else {
const pt_buf = try allocator.alloc(u8, if (v.ct.len > 0) v.ct.len else 1);
const ret = decrypt_fn(
pt_buf.ptr,
null,
v.ct.ptr,
@intCast(v.ct.len),
v.tag.ptr,
v.aad.ptr,
@intCast(v.aad.len),
v.nonce.ptr,
v.key.ptr,
);
if (ret == 0) {
std.debug.print("FAIL tc={d}: decrypt should have failed\n", .{v.tc_id});
result.failed += 1;
} else {
result.passed += 1;
}
}
}
return result;
}
pub fn run(allocator: Allocator, data: []const u8, tag: fetch.SuiteTag) !Result {
const vectors = try parse.loadAeadVectors(allocator, data);
return switch (tag) {
.aead_chacha20poly1305_ietf => try runAead(
allocator,
vectors,
&c.crypto_aead_chacha20poly1305_ietf_encrypt_detached,
&c.crypto_aead_chacha20poly1305_ietf_decrypt_detached,
c.crypto_aead_chacha20poly1305_ietf_ABYTES,
),
.aead_xchacha20poly1305 => try runAead(
allocator,
vectors,
&c.crypto_aead_xchacha20poly1305_ietf_encrypt_detached,
&c.crypto_aead_xchacha20poly1305_ietf_decrypt_detached,
c.crypto_aead_xchacha20poly1305_ietf_ABYTES,
),
.aead_aes256gcm => {
if (c.crypto_aead_aes256gcm_is_available() == 0) {
return Result{ .skipped = @intCast(vectors.len) };
}
return try runAead(
allocator,
vectors,
&c.crypto_aead_aes256gcm_encrypt_detached,
&c.crypto_aead_aes256gcm_decrypt_detached,
c.crypto_aead_aes256gcm_ABYTES,
);
},
.aead_aegis128l => try runAead(
allocator,
vectors,
&c.crypto_aead_aegis128l_encrypt_detached,
&c.crypto_aead_aegis128l_decrypt_detached,
c.crypto_aead_aegis128l_ABYTES,
),
.aead_aegis256 => try runAead(
allocator,
vectors,
&c.crypto_aead_aegis256_encrypt_detached,
&c.crypto_aead_aegis256_decrypt_detached,
c.crypto_aead_aegis256_ABYTES,
),
else => unreachable,
};
}
+99
View File
@@ -0,0 +1,99 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const parse = @import("../parse.zig");
const fetch = @import("../fetch.zig");
const c = @cImport(@cInclude("sodium.h"));
const Result = parse.Result;
const OpenEasyFn = *const fn ([*c]u8, [*c]const u8, c_ulonglong, [*c]const u8, [*c]const u8, [*c]const u8) callconv(.c) c_int;
fn runBox(
allocator: Allocator,
vectors: []const parse.BoxVector,
dec_fn: OpenEasyFn,
comptime mac_bytes: usize,
comptime nonce_bytes: usize,
) !Result {
var result = Result{};
for (vectors) |v| {
if (v.nonce.len != nonce_bytes) {
result.skipped += 1;
continue;
}
if (v.ct.len < nonce_bytes + mac_bytes) {
if (v.validity != .valid) {
result.passed += 1;
} else {
result.failed += 1;
}
continue;
}
const box_ct = v.ct[nonce_bytes..];
const ct_len = box_ct.len;
if (v.validity == .valid) {
const pt_buf = try allocator.alloc(u8, if (ct_len > mac_bytes) ct_len - mac_bytes else 0);
const ret = dec_fn(
pt_buf.ptr,
box_ct.ptr,
@intCast(ct_len),
v.nonce.ptr,
v.public_key.ptr,
v.private_key.ptr,
);
if (ret != 0) {
if (v.shared_zero) {
std.debug.print("INFO tc={d}: low-order public key rejected\n", .{v.tc_id});
result.skipped += 1;
continue;
}
std.debug.print("FAIL tc={d}: box_open failed\n", .{v.tc_id});
result.failed += 1;
} else if (!std.mem.eql(u8, pt_buf, v.msg)) {
std.debug.print("FAIL tc={d}: plaintext mismatch\n", .{v.tc_id});
result.failed += 1;
} else {
result.passed += 1;
}
} else {
const pt_buf = try allocator.alloc(u8, if (ct_len > mac_bytes) ct_len - mac_bytes else 1);
const ret = dec_fn(
pt_buf.ptr,
box_ct.ptr,
@intCast(ct_len),
v.nonce.ptr,
v.public_key.ptr,
v.private_key.ptr,
);
if (ret == 0) {
std.debug.print("FAIL tc={d}: box_open should have failed\n", .{v.tc_id});
result.failed += 1;
} else {
result.passed += 1;
}
}
}
return result;
}
pub fn run(allocator: Allocator, data: []const u8, tag: fetch.SuiteTag) !Result {
const vectors = try parse.loadBoxVectors(allocator, data);
return switch (tag) {
.box_xsalsa20 => runBox(
allocator,
vectors,
&c.crypto_box_open_easy,
c.crypto_box_MACBYTES,
c.crypto_box_NONCEBYTES,
),
.box_xchacha20 => runBox(
allocator,
vectors,
&c.crypto_box_curve25519xchacha20poly1305_open_easy,
c.crypto_box_curve25519xchacha20poly1305_MACBYTES,
c.crypto_box_curve25519xchacha20poly1305_NONCEBYTES,
),
else => unreachable,
};
}
+90
View File
@@ -0,0 +1,90 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const parse = @import("../parse.zig");
const c = @cImport(@cInclude("sodium.h"));
const Result = parse.Result;
pub fn runVerify(allocator: Allocator, data: []const u8, is_cofactored: bool) !Result {
const vectors = try parse.loadEddsaVerifyVectors(allocator, data);
var result = Result{};
for (vectors) |v| {
if (v.sig.len != c.crypto_sign_ed25519_BYTES or
v.public_key.len != c.crypto_sign_ed25519_PUBLICKEYBYTES)
{
result.skipped += 1;
continue;
}
const ret = c.crypto_sign_ed25519_verify_detached(
v.sig.ptr,
v.msg.ptr,
@intCast(v.msg.len),
v.public_key.ptr,
);
const accepted = ret == 0;
if (v.validity == .valid) {
if (!accepted) {
if (is_cofactored) {
std.debug.print("INFO tc={d}: cofactored-valid sig rejected (small-order)\n", .{v.tc_id});
result.skipped += 1;
} else {
std.debug.print("FAIL tc={d}: verify rejected valid sig\n", .{v.tc_id});
result.failed += 1;
}
} else {
result.passed += 1;
}
} else {
if (accepted) {
if (!is_cofactored) {
std.debug.print("INFO tc={d}: non-cofactored-invalid sig accepted (cofactored)\n", .{v.tc_id});
result.skipped += 1;
} else {
std.debug.print("FAIL tc={d}: verify accepted invalid sig\n", .{v.tc_id});
result.failed += 1;
}
} else {
result.passed += 1;
}
}
}
return result;
}
pub fn runSign(allocator: Allocator, data: []const u8) !Result {
const vectors = try parse.loadEddsaSignVectors(allocator, data);
var result = Result{};
for (vectors) |v| {
if (v.private_key.len != c.crypto_sign_ed25519_SEEDBYTES) {
result.skipped += 1;
continue;
}
if (v.validity != .valid) {
result.skipped += 1;
continue;
}
var pk: [c.crypto_sign_ed25519_PUBLICKEYBYTES]u8 = undefined;
var sk: [c.crypto_sign_ed25519_SECRETKEYBYTES]u8 = undefined;
_ = c.crypto_sign_ed25519_seed_keypair(&pk, &sk, v.private_key.ptr);
if (v.public_key.len == c.crypto_sign_ed25519_PUBLICKEYBYTES and
!std.mem.eql(u8, &pk, v.public_key))
{
std.debug.print("FAIL tc={d}: derived pk mismatch\n", .{v.tc_id});
result.failed += 1;
continue;
}
var sig: [c.crypto_sign_ed25519_BYTES]u8 = undefined;
_ = c.crypto_sign_ed25519_detached(&sig, null, v.msg.ptr, @intCast(v.msg.len), &sk);
if (std.mem.eql(u8, &sig, v.sig)) {
result.passed += 1;
} else {
std.debug.print("FAIL tc={d}: signature mismatch\n", .{v.tc_id});
result.failed += 1;
}
}
return result;
}
+75
View File
@@ -0,0 +1,75 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const parse = @import("../parse.zig");
const fetch = @import("../fetch.zig");
const c = @cImport(@cInclude("sodium.h"));
const Result = parse.Result;
const HashFn = *const fn ([*c]u8, [*c]const u8, c_ulonglong) callconv(.c) c_int;
const GenericHashFn = *const fn ([*c]u8, usize, [*c]const u8, c_ulonglong, [*c]const u8, usize) callconv(.c) c_int;
fn runFixedHash(vectors: []const parse.HashVector, hash_fn: HashFn, out_len: usize) Result {
var result = Result{};
for (vectors) |v| {
if (v.validity == .invalid) {
result.skipped += 1;
continue;
}
var out_buf: [64]u8 = undefined;
const out = out_buf[0..out_len];
const ret = hash_fn(out.ptr, v.msg.ptr, @intCast(v.msg.len));
if (ret != 0) {
std.debug.print("FAIL tc={d}: hash returned {d}\n", .{ v.tc_id, ret });
result.failed += 1;
continue;
}
if (!std.mem.eql(u8, out, v.digest)) {
std.debug.print("FAIL tc={d}: digest mismatch\n", .{v.tc_id});
result.failed += 1;
} else {
result.passed += 1;
}
}
return result;
}
fn runBlake2b(vectors: []const parse.HashVector, hash_fn: GenericHashFn) Result {
var result = Result{};
for (vectors) |v| {
if (v.validity == .invalid) {
result.skipped += 1;
continue;
}
const out_len = if (v.out_len > 0) v.out_len else v.digest.len;
var out_buf: [64]u8 = undefined;
const out = out_buf[0..out_len];
const key_ptr: [*c]const u8 = if (v.key.len > 0) v.key.ptr else null;
const ret = hash_fn(out.ptr, out_len, v.msg.ptr, @intCast(v.msg.len), key_ptr, v.key.len);
if (ret != 0) {
std.debug.print("FAIL tc={d}: blake2b returned {d}\n", .{ v.tc_id, ret });
result.failed += 1;
continue;
}
if (!std.mem.eql(u8, out, v.digest)) {
std.debug.print("FAIL tc={d}: digest mismatch\n", .{v.tc_id});
result.failed += 1;
} else {
result.passed += 1;
}
}
return result;
}
pub fn run(allocator: Allocator, data: []const u8, tag: fetch.SuiteTag) !Result {
const vectors = try parse.loadHashVectors(allocator, data);
return switch (tag) {
.hash_sha256 => runFixedHash(vectors, &c.crypto_hash_sha256, c.crypto_hash_sha256_BYTES),
.hash_sha512 => runFixedHash(vectors, &c.crypto_hash_sha512, c.crypto_hash_sha512_BYTES),
.hash_sha3_256 => runFixedHash(vectors, &c.crypto_hash_sha3256, c.crypto_hash_sha3256_BYTES),
.hash_sha3_512 => runFixedHash(vectors, &c.crypto_hash_sha3512, c.crypto_hash_sha3512_BYTES),
.hash_blake2b => runBlake2b(vectors, &c.crypto_generichash_blake2b),
else => unreachable,
};
}
+63
View File
@@ -0,0 +1,63 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const parse = @import("../parse.zig");
const fetch = @import("../fetch.zig");
const c = @cImport(@cInclude("sodium.h"));
const Result = parse.Result;
fn runHkdf(
allocator: Allocator,
vectors: []const parse.HkdfVector,
comptime prk_len: usize,
extract_fn: *const fn ([*c]u8, [*c]const u8, usize, [*c]const u8, usize) callconv(.c) c_int,
expand_fn: *const fn ([*c]u8, usize, [*c]const u8, usize, [*c]const u8) callconv(.c) c_int,
) !Result {
var result = Result{};
for (vectors) |v| {
if (v.validity != .valid) {
result.skipped += 1;
continue;
}
var prk: [prk_len]u8 = undefined;
const salt_ptr: [*c]const u8 = if (v.salt.len > 0) v.salt.ptr else null;
_ = extract_fn(&prk, salt_ptr, v.salt.len, v.ikm.ptr, v.ikm.len);
const out = try allocator.alloc(u8, v.out_len);
const info_ptr: [*c]const u8 = if (v.info.len > 0) v.info.ptr else null;
const ret = expand_fn(out.ptr, v.out_len, info_ptr, v.info.len, &prk);
if (ret != 0) {
std.debug.print("FAIL tc={d}: expand returned {d}\n", .{ v.tc_id, ret });
result.failed += 1;
continue;
}
if (std.mem.eql(u8, out, v.okm)) {
result.passed += 1;
} else {
std.debug.print("FAIL tc={d}: okm mismatch\n", .{v.tc_id});
result.failed += 1;
}
}
return result;
}
pub fn run(allocator: Allocator, data: []const u8, tag: fetch.SuiteTag) !Result {
const vectors = try parse.loadHkdfVectors(allocator, data);
return switch (tag) {
.kdf_hkdf_sha256 => runHkdf(
allocator,
vectors,
c.crypto_kdf_hkdf_sha256_KEYBYTES,
&c.crypto_kdf_hkdf_sha256_extract,
@ptrCast(&c.crypto_kdf_hkdf_sha256_expand),
),
.kdf_hkdf_sha512 => runHkdf(
allocator,
vectors,
c.crypto_kdf_hkdf_sha512_KEYBYTES,
&c.crypto_kdf_hkdf_sha512_extract,
@ptrCast(&c.crypto_kdf_hkdf_sha512_expand),
),
else => unreachable,
};
}
+62
View File
@@ -0,0 +1,62 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const parse = @import("../parse.zig");
const c = @cImport(@cInclude("sodium.h"));
const Result = parse.Result;
pub fn run(allocator: Allocator, data: []const u8) !Result {
const vectors = try parse.loadMlkemVectors(allocator, data);
var result = Result{};
for (vectors) |v| {
if (v.seed.len != c.crypto_kem_mlkem768_SEEDBYTES or
v.ct.len != c.crypto_kem_mlkem768_CIPHERTEXTBYTES)
{
if (v.validity == .invalid) {
result.passed += 1;
} else {
result.skipped += 1;
}
continue;
}
var pk: [c.crypto_kem_mlkem768_PUBLICKEYBYTES]u8 = undefined;
var sk: [c.crypto_kem_mlkem768_SECRETKEYBYTES]u8 = undefined;
_ = c.crypto_kem_mlkem768_seed_keypair(&pk, &sk, v.seed.ptr);
if (v.ek.len == c.crypto_kem_mlkem768_PUBLICKEYBYTES and
!std.mem.eql(u8, &pk, v.ek))
{
std.debug.print("FAIL tc={d}: derived pk mismatch\n", .{v.tc_id});
result.failed += 1;
continue;
}
var ss: [c.crypto_kem_mlkem768_SHAREDSECRETBYTES]u8 = undefined;
const ret = c.crypto_kem_mlkem768_dec(&ss, v.ct.ptr, &sk);
if (v.validity == .valid) {
if (ret != 0) {
std.debug.print("FAIL tc={d}: dec failed\n", .{v.tc_id});
result.failed += 1;
} else if (!std.mem.eql(u8, &ss, v.ss)) {
std.debug.print("FAIL tc={d}: shared secret mismatch\n", .{v.tc_id});
result.failed += 1;
} else {
result.passed += 1;
}
} else if (v.validity == .acceptable) {
result.passed += 1;
} else {
// ML-KEM implicit rejection: dec always returns 0 but produces
// a pseudorandom shared secret that won't match the expected one.
if (ret == 0 and std.mem.eql(u8, &ss, v.ss)) {
std.debug.print("FAIL tc={d}: invalid vector produced matching ss\n", .{v.tc_id});
result.failed += 1;
} else {
result.passed += 1;
}
}
}
return result;
}
+81
View File
@@ -0,0 +1,81 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const parse = @import("../parse.zig");
const fetch = @import("../fetch.zig");
const c = @cImport(@cInclude("sodium.h"));
const Result = parse.Result;
const MacFn = *const fn ([*c]u8, [*c]const u8, c_ulonglong, [*c]const u8) callconv(.c) c_int;
fn checkMac(result: *Result, v: parse.MacVector, computed: []const u8) void {
const matches = std.mem.eql(u8, computed, v.mac);
if (v.validity == .valid) {
if (matches) {
result.passed += 1;
} else {
std.debug.print("FAIL tc={d}: mac mismatch\n", .{v.tc_id});
result.failed += 1;
}
} else {
if (!matches) {
result.passed += 1;
} else {
std.debug.print("FAIL tc={d}: invalid mac should not match\n", .{v.tc_id});
result.failed += 1;
}
}
}
fn runHmac256(vectors: []const parse.MacVector) Result {
var result = Result{};
for (vectors) |v| {
var state: c.crypto_auth_hmacsha256_state = undefined;
_ = c.crypto_auth_hmacsha256_init(&state, v.key.ptr, v.key.len);
_ = c.crypto_auth_hmacsha256_update(&state, v.msg.ptr, @intCast(v.msg.len));
var out: [c.crypto_auth_hmacsha256_BYTES]u8 = undefined;
_ = c.crypto_auth_hmacsha256_final(&state, &out);
checkMac(&result, v, &out);
}
return result;
}
fn runHmac512(vectors: []const parse.MacVector) Result {
var result = Result{};
for (vectors) |v| {
var state: c.crypto_auth_hmacsha512_state = undefined;
_ = c.crypto_auth_hmacsha512_init(&state, v.key.ptr, v.key.len);
_ = c.crypto_auth_hmacsha512_update(&state, v.msg.ptr, @intCast(v.msg.len));
var out: [c.crypto_auth_hmacsha512_BYTES]u8 = undefined;
_ = c.crypto_auth_hmacsha512_final(&state, &out);
checkMac(&result, v, &out);
}
return result;
}
fn runOneShot(vectors: []const parse.MacVector, mac_fn: MacFn, comptime mac_len: usize) Result {
var result = Result{};
for (vectors) |v| {
var out: [mac_len]u8 = undefined;
const ret = mac_fn(&out, v.msg.ptr, @intCast(v.msg.len), v.key.ptr);
if (ret != 0) {
std.debug.print("FAIL tc={d}: mac returned {d}\n", .{ v.tc_id, ret });
result.failed += 1;
continue;
}
checkMac(&result, v, &out);
}
return result;
}
pub fn run(allocator: Allocator, data: []const u8, tag: fetch.SuiteTag) !Result {
const vectors = try parse.loadMacVectors(allocator, data);
return switch (tag) {
.mac_hmacsha256 => runHmac256(vectors),
.mac_hmacsha512 => runHmac512(vectors),
.mac_siphash24 => runOneShot(vectors, &c.crypto_shorthash_siphash24, c.crypto_shorthash_siphash24_BYTES),
.mac_siphashx24 => runOneShot(vectors, &c.crypto_shorthash_siphashx24, c.crypto_shorthash_siphashx24_BYTES),
.onetimeauth_poly1305 => runOneShot(vectors, &c.crypto_onetimeauth_poly1305, c.crypto_onetimeauth_poly1305_BYTES),
else => unreachable,
};
}
+45
View File
@@ -0,0 +1,45 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const parse = @import("../parse.zig");
const c = @cImport(@cInclude("sodium.h"));
const Result = parse.Result;
pub fn run(allocator: Allocator, data: []const u8) !Result {
const vectors = try parse.loadScryptVectors(allocator, data);
var result = Result{};
for (vectors) |v| {
if (v.validity != .valid) {
result.skipped += 1;
continue;
}
if (v.n > 1 << 20 or v.r > 8 or v.p > 1) {
result.skipped += 1;
continue;
}
const out = try allocator.alloc(u8, v.dk_len);
const ret = c.crypto_pwhash_scryptsalsa208sha256_ll(
v.password.ptr,
v.password.len,
v.salt.ptr,
v.salt.len,
v.n,
v.r,
v.p,
out.ptr,
v.dk_len,
);
if (ret != 0) {
std.debug.print("FAIL tc={d}: scrypt returned {d}\n", .{ v.tc_id, ret });
result.failed += 1;
continue;
}
if (std.mem.eql(u8, out, v.dk)) {
result.passed += 1;
} else {
std.debug.print("FAIL tc={d}: derived key mismatch\n", .{v.tc_id});
result.failed += 1;
}
}
return result;
}
+83
View File
@@ -0,0 +1,83 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const parse = @import("../parse.zig");
const fetch = @import("../fetch.zig");
const c = @cImport(@cInclude("sodium.h"));
const Result = parse.Result;
const DetachedEncFn = *const fn ([*c]u8, [*c]u8, [*c]const u8, c_ulonglong, [*c]const u8, [*c]const u8) callconv(.c) c_int;
const DetachedDecFn = *const fn ([*c]u8, [*c]const u8, [*c]const u8, c_ulonglong, [*c]const u8, [*c]const u8) callconv(.c) c_int;
fn runSecretbox(
allocator: Allocator,
vectors: []const parse.AuthEncVector,
enc_fn: DetachedEncFn,
dec_fn: DetachedDecFn,
) !Result {
var result = Result{};
for (vectors) |v| {
if (v.validity == .valid) {
const ct_buf = try allocator.alloc(u8, v.msg.len);
var mac: [c.crypto_secretbox_MACBYTES]u8 = undefined;
_ = enc_fn(ct_buf.ptr, &mac, v.msg.ptr, @intCast(v.msg.len), v.nonce.ptr, v.key.ptr);
if (!std.mem.eql(u8, ct_buf, v.ct) or !std.mem.eql(u8, &mac, v.tag)) {
std.debug.print("FAIL tc={d}: encrypt mismatch\n", .{v.tc_id});
result.failed += 1;
continue;
}
const pt_buf = try allocator.alloc(u8, v.ct.len);
const ret = dec_fn(
pt_buf.ptr,
v.ct.ptr,
v.tag.ptr,
@intCast(v.ct.len),
v.nonce.ptr,
v.key.ptr,
);
if (ret != 0 or !std.mem.eql(u8, pt_buf, v.msg)) {
std.debug.print("FAIL tc={d}: decrypt mismatch\n", .{v.tc_id});
result.failed += 1;
} else {
result.passed += 1;
}
} else {
const pt_buf = try allocator.alloc(u8, if (v.ct.len > 0) v.ct.len else 1);
const ret = dec_fn(
pt_buf.ptr,
v.ct.ptr,
v.tag.ptr,
@intCast(v.ct.len),
v.nonce.ptr,
v.key.ptr,
);
if (ret == 0) {
std.debug.print("FAIL tc={d}: decrypt should have failed\n", .{v.tc_id});
result.failed += 1;
} else {
result.passed += 1;
}
}
}
return result;
}
pub fn run(allocator: Allocator, data: []const u8, tag: fetch.SuiteTag) !Result {
const vectors = try parse.loadAuthEncVectors(allocator, data);
return switch (tag) {
.secretbox_xsalsa20 => runSecretbox(
allocator,
vectors,
&c.crypto_secretbox_detached,
&c.crypto_secretbox_open_detached,
),
.secretbox_xchacha20 => runSecretbox(
allocator,
vectors,
&c.crypto_secretbox_xchacha20poly1305_detached,
&c.crypto_secretbox_xchacha20poly1305_open_detached,
),
else => unreachable,
};
}
+42
View File
@@ -0,0 +1,42 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const parse = @import("../parse.zig");
const fetch = @import("../fetch.zig");
const c = @cImport(@cInclude("sodium.h"));
const Result = parse.Result;
const XorFn = *const fn ([*c]u8, [*c]const u8, c_ulonglong, [*c]const u8, [*c]const u8) callconv(.c) c_int;
fn runStream(allocator: Allocator, vectors: []const parse.StreamVector, xor_fn: XorFn) !Result {
var result = Result{};
for (vectors) |v| {
if (v.validity != .valid) {
result.skipped += 1;
continue;
}
const out = try allocator.alloc(u8, v.msg.len);
const ret = xor_fn(out.ptr, v.msg.ptr, @intCast(v.msg.len), v.nonce.ptr, v.key.ptr);
if (ret != 0) {
std.debug.print("FAIL tc={d}: stream_xor returned {d}\n", .{ v.tc_id, ret });
result.failed += 1;
continue;
}
if (std.mem.eql(u8, out, v.ct)) {
result.passed += 1;
} else {
std.debug.print("FAIL tc={d}: ciphertext mismatch\n", .{v.tc_id});
result.failed += 1;
}
}
return result;
}
pub fn run(allocator: Allocator, data: []const u8, tag: fetch.SuiteTag) !Result {
const vectors = try parse.loadStreamVectors(allocator, data);
return switch (tag) {
.stream_salsa20 => runStream(allocator, vectors, &c.crypto_stream_salsa20_xor),
.stream_xsalsa20 => runStream(allocator, vectors, &c.crypto_stream_xsalsa20_xor),
else => unreachable,
};
}
+43
View File
@@ -0,0 +1,43 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const parse = @import("../parse.zig");
const c = @cImport(@cInclude("sodium.h"));
const Result = parse.Result;
pub fn run(allocator: Allocator, data: []const u8) !Result {
const vectors = try parse.loadXdhVectors(allocator, data);
var result = Result{};
for (vectors) |v| {
if (v.private_key.len != c.crypto_scalarmult_curve25519_SCALARBYTES or
v.public_key.len != c.crypto_scalarmult_curve25519_BYTES)
{
result.skipped += 1;
continue;
}
var shared: [c.crypto_scalarmult_curve25519_BYTES]u8 = undefined;
const ret = c.crypto_scalarmult_curve25519(&shared, v.private_key.ptr, v.public_key.ptr);
if (v.validity == .valid) {
if (ret != 0) {
std.debug.print("FAIL tc={d}: scalarmult returned {d}\n", .{ v.tc_id, ret });
result.failed += 1;
} else if (!std.mem.eql(u8, &shared, v.shared)) {
std.debug.print("FAIL tc={d}: shared secret mismatch\n", .{v.tc_id});
result.failed += 1;
} else {
result.passed += 1;
}
} else {
if (ret != 0) {
result.passed += 1;
} else if (std.mem.eql(u8, &shared, v.shared)) {
std.debug.print("FAIL tc={d}: invalid vector produced matching output\n", .{v.tc_id});
result.failed += 1;
} else {
result.passed += 1;
}
}
}
return result;
}
+42
View File
@@ -0,0 +1,42 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const parse = @import("../parse.zig");
const fetch = @import("../fetch.zig");
const c = @cImport(@cInclude("sodium.h"));
const Result = parse.Result;
const XofFn = *const fn ([*c]u8, usize, [*c]const u8, c_ulonglong) callconv(.c) c_int;
fn runXof(allocator: Allocator, vectors: []const parse.XofVector, xof_fn: XofFn) !Result {
var result = Result{};
for (vectors) |v| {
if (v.validity == .invalid) {
result.skipped += 1;
continue;
}
const out = try allocator.alloc(u8, v.out_len);
const ret = xof_fn(out.ptr, v.out_len, v.msg.ptr, @intCast(v.msg.len));
if (ret != 0) {
std.debug.print("FAIL tc={d}: xof returned {d}\n", .{ v.tc_id, ret });
result.failed += 1;
continue;
}
if (std.mem.eql(u8, out, v.output)) {
result.passed += 1;
} else {
std.debug.print("FAIL tc={d}: output mismatch\n", .{v.tc_id});
result.failed += 1;
}
}
return result;
}
pub fn run(allocator: Allocator, data: []const u8, tag: fetch.SuiteTag) !Result {
const vectors = try parse.loadXofVectors(allocator, data);
return switch (tag) {
.xof_shake128 => runXof(allocator, vectors, &c.crypto_xof_shake128),
.xof_shake256 => runXof(allocator, vectors, &c.crypto_xof_shake256),
else => unreachable,
};
}