From 9c41faf9bd6fba0cc577d3786883926bf66b2ab0 Mon Sep 17 00:00:00 2001 From: Paul Duncan Date: Sat, 24 Feb 2024 00:47:18 -0500 Subject: add tests/cavp-tests --- tests/cavp-tests/gen-main.rb | 446 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 446 insertions(+) create mode 100755 tests/cavp-tests/gen-main.rb (limited to 'tests/cavp-tests/gen-main.rb') diff --git a/tests/cavp-tests/gen-main.rb b/tests/cavp-tests/gen-main.rb new file mode 100755 index 0000000..5d73554 --- /dev/null +++ b/tests/cavp-tests/gen-main.rb @@ -0,0 +1,446 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# gen-main.rb: Generate `main.c` for cavp-tests. This script does the +# following: +# +# 1. Fetches the SHA-3 and SHAKE byte test vectors zip archives and +# caches them in `./zip-cache`. +# 3. Verifies the SHA-256 digest of the downloaded archives. +# 3. Parses the response files (`.rsp`) in the downloaded zip archives +# and extracts the SHA-3 and SHAKE test vectors. +# 4. Uses the test vectors to generate `main.c`. +# 5. Prints `main.c` to standard output. +# +# Usage: +# cd tests/cavp-tests/ +# +# # download/cache archives, then generate main.c from them +# ./gen-main.rb > main.c +# +# # build and run tests +# make && ./cavp-tests +# + +# load libraries +require 'open-uri' +require 'openssl' +require 'uri' +require 'zip' + +# number of elements per line of test data +PER_LINE = 128 + +# test vector sets +SETS = [{ + # URL to zip file containing test vectors for this set of algorithms + url: 'https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Algorithm-Validation-Program/documents/sha3/sha-3bytetestvectors.zip', + + # expected sha256 digest of zip file + hash: 'cd07701af2e47f5cc889d642528b4bf11f8b6eb55797c7307a96828ed8d8fc8c', + + # algorithms + algos: [{ + name: 'SHA3-224', # algo name + fn: 'sha3_224', # function prefix + + type: :hash, # function type + size: 28, # output size, in bytes + + # response files + rsp_files: %w{ + SHA3_224ShortMsg.rsp + SHA3_224LongMsg.rsp + }, + }, { + name: 'SHA3-256', # algo name + fn: 'sha3_256', # function prefix + + type: :hash, # function type + size: 32, # output size, in bytes + + # response files + rsp_files: %w{ + SHA3_256ShortMsg.rsp + SHA3_256LongMsg.rsp + }, + }, { + name: 'SHA3-384', # algo name + fn: 'sha3_384', # function prefix + + type: :hash, # function type + size: 48, # output size, in bytes + + # response files + rsp_files: %w{ + SHA3_384ShortMsg.rsp + SHA3_384LongMsg.rsp + }, + }, { + name: 'SHA3-512', # algo name + fn: 'sha3_512', # function prefix + + type: :hash, # function type + size: 64, # output size, in bytes + + # response files + rsp_files: %w{ + SHA3_512ShortMsg.rsp + SHA3_512LongMsg.rsp + }, + }], +}, { + # URL to zip file containing test vectors for this set of algorithms + url: 'https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Algorithm-Validation-Program/documents/sha3/shakebytetestvectors.zip', + + # expected sha256 digest of zip file + hash: 'debfebc3157b3ceea002b84ca38476420389a3bf7e97dc5f53ea4689a16de4c7', + + # algorithms + algos: [{ + name: 'SHAKE128', # algo name + fn: 'shake128', # function prefix + type: :xof, # function type + + # response files + rsp_files: %w{ + SHAKE128ShortMsg.rsp + SHAKE128LongMsg.rsp + SHAKE128VariableOut.rsp + }, + }, { + name: 'SHAKE256', # algo name + fn: 'shake256', # function prefix + type: :xof, # function type + + # response files + rsp_files: %w{ + SHAKE256ShortMsg.rsp + SHAKE256LongMsg.rsp + SHAKE256VariableOut.rsp + }, + }], +}] + +# header for main.c +MAIN_HEAD = < // uint8_t +#include // printf() +#include // memcmp() +#include "hex.h" // hex_write() +#include "sha3.h" + +typedef struct { + const char *test_fn; // test function name + size_t test_i; // test case index + const char *rsp_file; // response file name + size_t rsp_line; // start line of test in response file + const uint8_t *got; // result + size_t got_len; // result length + const uint8_t *exp; // expected result + size_t exp_len; // expected result length +} failure_t; + +// print failure +static void fail(FILE *fh, const failure_t f) { + fprintf(fh, "test_fn = %s\\n", f.test_fn); + fprintf(fh, "rsp_file = %s\\n", f.rsp_file); + fprintf(fh, "rsp_line = %zu\\n", f.rsp_line); + fprintf(fh, "got (%zu) = ", f.got_len); + hex_write(fh, f.got, f.got_len); + fprintf(fh, "\\nexp (%zu) = ", f.exp_len); + hex_write(fh, f.exp, f.exp_len); + fprintf(fh, "\\n"); +} +END_MAIN_HEAD + +# template for footer of main.c +MAIN_TAIL_TMPL = <s + return 0; +} +END_MAIN_TAIL_TMPL + +# xof test vector template +XOF_TEST_TMPL = <d, %d, %d, %d, %d, %d }, +END_XOF_TEST_TMPL + +# xof test function template +XOF_FN_TMPL = <s tests from shakebytetestvectors.zip +static void test_%s(void) { + // data for tests + static const uint8_t DATA[] = { +%s + }; // total size = %d + + // response file names + static const char *RSP_FILES[] = { %s }; + + // test vectors + static const struct { + const size_t rsp_file_ofs, // offset of source file name in RSP_FILES + rsp_file_line, // line within rsp file + src_ofs, // message offset into DATA + src_len, // message length of data + exp_ofs, // expected output offset into DATA + exp_len; // expected output length + } TESTS[] = { +%s + }; + + // run tests + for (size_t i = 0; i < sizeof(TESTS) / sizeof(TESTS[0]); i++) { + // get expected result + const uint8_t * const exp = DATA + TESTS[i].exp_ofs; + const size_t exp_len = TESTS[i].exp_len; + + // hash data into "got" + uint8_t got[%d] = { 0 }; + %s_xof_once(DATA + TESTS[i].src_ofs, TESTS[i].src_len, got, exp_len); + + // check for expected result + if (memcmp(got, exp, exp_len)) { + fail(stderr, (failure_t) { + .test_fn = __func__, // test function + .test_i = i, // test case index in TESTS + .rsp_file = RSP_FILES[TESTS[i].rsp_file_ofs], // response file + .rsp_line = TESTS[i].rsp_file_line, // start line of test case + .got = got, // result + .got_len = exp_len, // result length, in bytes + .exp = exp, // expected result + .exp_len = exp_len, // expected result length, in bytes + }); + } + } +} +END_XOF_FN_TMPL + +# hash test vector template +HASH_TEST_TMPL = <d, %d, %d, %d, %d }, +END_XOF_TEST_TMPL + +# hash test function template +HASH_FN_TMPL = <s tests +static void test_%s(void) { + // data for tests + static const uint8_t DATA[] = { +%s + }; // total size = %d + + // response file names + static const char *RSP_FILES[] = { %s }; + + // test vectors + static const struct { + const size_t rsp_file_ofs, // offset of source file name in RSP_FILES + rsp_file_line, // line within rsp file + src_ofs, // message offset into DATA + src_len, // message length of data + exp_ofs; // expected output offset into DATA + } TESTS[] = { +%s + }; + + // run tests + for (size_t i = 0; i < sizeof(TESTS) / sizeof(TESTS[0]); i++) { + // get expected result + const uint8_t * const exp = DATA + TESTS[i].exp_ofs; + + // hash data into "got" + uint8_t got[%d] = { 0 }; + %s(DATA + TESTS[i].src_ofs, TESTS[i].src_len, got); + + // check for expected result + if (memcmp(got, exp, sizeof(got))) { + fail(stderr, (failure_t) { + .test_fn = __func__, // test function + .test_i = i, // test case index in TESTS + .rsp_file = RSP_FILES[TESTS[i].rsp_file_ofs], // response file + .rsp_line = TESTS[i].rsp_file_line, // start line of test case + .got = got, // result + .got_len = sizeof(got), // result length, in bytes + .exp = exp, // expected result + .exp_len = sizeof(got), // expected result length, in bytes + }); + } + } +} +END_XOF_FN_TMPL + +# parse hash of key/value pairs into test vector row +def parse(algo_type, file, h, data_size, &block) + # parse hex-encoded message into array of bytes, then + # truncate message to "Len" bytes, if "Len" is specified + src = h[:Msg].scan(/../).map { |s| s.to_i(16) } + src = src[0, h[:Len].to_i] if h.key?(:Len) + src_ofs = data_size + + exp = case algo_type + when :xof + # parse hex-encoded expected output into array of bytes, then + # truncate expected output to "OutputLen" bytes, if "OutputLen" is + # specified + exp = h[:Output].scan(/../).map { |s| s.to_i(16) } + exp = exp[0, h[:OutputLen].to_i] if h.key?(:OutputLen) + exp + when :hash + # get expected output + h[:MD].scan(/../).map { |s| s.to_i(16) } + else + raise "unknown algorithm type: #{algo_type}" + end + exp_ofs = data_size + src.size + + # append message bytes and expected output bytes to data + block.call(src + exp) + + # return parsed entry + { + file: file, + line: h[:line], + + # message + src_ofs: src_ofs, + src_len: src.size, + + # expected output + exp_ofs: exp_ofs, + exp_len: exp.size, + } +end + +# build path to test vector archive cache +CACHE_DIR = File.join(__dir__, 'zip-cache') +Dir.mkdir(CACHE_DIR) unless Dir.exist?(CACHE_DIR) + +# download test vector archives +SETS.map do |set| + Thread.new(set) do |set| + # parse url + url = URI.parse(set[:url]) + + # destination archive name + zip_name = File.basename(url.path) + + # absolute path to destination archive + zip_path = File.join(CACHE_DIR, zip_name) + + # download to destination path + url.open { |src_io| IO.copy_stream(src_io, zip_path) } unless File.exist?(zip_path) + + # get sha256 hash of downloaded archive + got_hash = ::OpenSSL::Digest::SHA256.new.file(zip_path).hexdigest + + # check against expected digest + if got_hash != set[:hash] + raise '%s: hash mismatch: got %s, exp %s' % [zip_name, got_hash, set[:hash]] + end + end +end.each { |t| t.join } + +# print header +puts(MAIN_HEAD) + +# generate test code +SETS.each do |set| + # get absolute path to zip file + zip_path = File.join(CACHE_DIR, File.basename(URI.parse(set[:url]).path)) + + # open zip file + Zip::File.open(zip_path, 'rb') do |zip| + set[:algos].each do |algo| + data = [] # test data (shared across all rsp files) + + # parse test vectors from rsp files + rows = algo[:rsp_files].each_with_object([]) do |rsp_file, rows| + curr = {} # current row + + # read lines from rsp file + lines = zip.glob(rsp_file).first.get_input_stream.readlines.to_a.map { |line| line.strip } + + # parse lines into rows + lines.size.times.each do |line_i| + case lines[line_i] + when /^(\w+) = (\w+)$/ + k, v = $1, $2 # extract key and value + curr[k.intern] = v + + # cache line number + curr[:line] = line_i unless curr[:line] + when '' + if curr.size > 0 + # parse current hash into test vector and add it to results + rows << parse(algo[:type], rsp_file, curr, data.size) { |bytes| data += bytes } + end + + curr = {} # clear current hash + end + end + end + + # get maximum number of lines of emitted static DATA array + num_lines = data.size / PER_LINE + ((data.size % PER_LINE) > 0 ? 1 : 0) + + # get maximum length of expected output + # used to for size of "got" buffer in emitted code + max_exp_len = case algo[:type] + when :hash + algo[:size] + when :xof + rows.reduce(0) { |r, row| row[:exp_len] > r ? row[:exp_len] : r } + else + raise "unknown algorithm type: #{algo[:type]}" + end + + # get templates + fn_tmpl, test_tmpl = case algo[:type] + when :xof + [XOF_FN_TMPL, XOF_TEST_TMPL] + when :hash + [HASH_FN_TMPL, HASH_TEST_TMPL] + else + raise "unknown algorithm type: #{algo[:type]}" + end + + # generate test function + puts(fn_tmpl % { + name: algo[:name], # algorithm name + fn: algo[:fn], # algorithm function prefix + + # test vector data + data: num_lines.times.map { |ofs| + " %s,\n" % [data[PER_LINE * ofs, PER_LINE].join(', ')] + }.join, + + # response file names + rsp_files: algo[:rsp_files].map { |file| '"%s"' % [file] }.join(', '), + + data_size: data.size, # total size of emitted DATA array, in bytes + max_exp_len: max_exp_len, # maximum expected output length, in bytes + + # test vectors + tests: rows.map { |row| + test_tmpl % row.merge({ file_pos: algo[:rsp_files].index(row[:file]) }) + }.join, + }) + end + end +end + +# print main function body +puts(MAIN_TAIL_TMPL % { + fns: SETS.each_with_object([]) do |set, r| + set[:algos].each_with_object(r) do |algo, r| + r << ' test_%s();' % algo + end + end.join("\n"), +}) -- cgit v1.2.3