diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cdba7a8a..16faef3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: diff --git a/.gitignore b/.gitignore index 83f8f4b6..cc5d410c 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/build.zig b/build.zig index 6d368afa..5fbcae5e 100644 --- a/build.zig +++ b/build.zig @@ -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); + } } diff --git a/configure.ac b/configure.ac index b5e9a8ea..56de1501 100644 --- a/configure.ac +++ b/configure.ac @@ -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 diff --git a/test/Makefile.am b/test/Makefile.am index f9894bb8..92a27e63 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -1,5 +1,6 @@ SUBDIRS = \ - default + default \ + vectors EXTRA_DIST = \ quirks/quirks.h diff --git a/test/vectors/Makefile.am b/test/vectors/Makefile.am new file mode 100644 index 00000000..e9400048 --- /dev/null +++ b/test/vectors/Makefile.am @@ -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 diff --git a/test/vectors/fetch.zig b/test/vectors/fetch.zig new file mode 100644 index 00000000..ed77d8ac --- /dev/null +++ b/test/vectors/fetch.zig @@ -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; + }; +} diff --git a/test/vectors/main.zig b/test/vectors/main.zig new file mode 100644 index 00000000..9b14ae05 --- /dev/null +++ b/test/vectors/main.zig @@ -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; + } +} diff --git a/test/vectors/parse.zig b/test/vectors/parse.zig new file mode 100644 index 00000000..e67df3a7 --- /dev/null +++ b/test/vectors/parse.zig @@ -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; +} diff --git a/test/vectors/suites/aead.zig b/test/vectors/suites/aead.zig new file mode 100644 index 00000000..45192727 --- /dev/null +++ b/test/vectors/suites/aead.zig @@ -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, + }; +} diff --git a/test/vectors/suites/box.zig b/test/vectors/suites/box.zig new file mode 100644 index 00000000..5c65af4b --- /dev/null +++ b/test/vectors/suites/box.zig @@ -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, + }; +} diff --git a/test/vectors/suites/eddsa.zig b/test/vectors/suites/eddsa.zig new file mode 100644 index 00000000..302175b9 --- /dev/null +++ b/test/vectors/suites/eddsa.zig @@ -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; +} diff --git a/test/vectors/suites/hash.zig b/test/vectors/suites/hash.zig new file mode 100644 index 00000000..ab8017f7 --- /dev/null +++ b/test/vectors/suites/hash.zig @@ -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, + }; +} diff --git a/test/vectors/suites/kdf.zig b/test/vectors/suites/kdf.zig new file mode 100644 index 00000000..8f2ae227 --- /dev/null +++ b/test/vectors/suites/kdf.zig @@ -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, + }; +} diff --git a/test/vectors/suites/kem.zig b/test/vectors/suites/kem.zig new file mode 100644 index 00000000..13a9df35 --- /dev/null +++ b/test/vectors/suites/kem.zig @@ -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; +} diff --git a/test/vectors/suites/mac.zig b/test/vectors/suites/mac.zig new file mode 100644 index 00000000..eac61911 --- /dev/null +++ b/test/vectors/suites/mac.zig @@ -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, + }; +} diff --git a/test/vectors/suites/pwhash.zig b/test/vectors/suites/pwhash.zig new file mode 100644 index 00000000..8a37c5f6 --- /dev/null +++ b/test/vectors/suites/pwhash.zig @@ -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; +} diff --git a/test/vectors/suites/secretbox.zig b/test/vectors/suites/secretbox.zig new file mode 100644 index 00000000..6852cb81 --- /dev/null +++ b/test/vectors/suites/secretbox.zig @@ -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, + }; +} diff --git a/test/vectors/suites/stream.zig b/test/vectors/suites/stream.zig new file mode 100644 index 00000000..77ff57cd --- /dev/null +++ b/test/vectors/suites/stream.zig @@ -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, + }; +} diff --git a/test/vectors/suites/xdh.zig b/test/vectors/suites/xdh.zig new file mode 100644 index 00000000..c57c5d0f --- /dev/null +++ b/test/vectors/suites/xdh.zig @@ -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; +} diff --git a/test/vectors/suites/xof.zig b/test/vectors/suites/xof.zig new file mode 100644 index 00000000..a3b376eb --- /dev/null +++ b/test/vectors/suites/xof.zig @@ -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, + }; +}