From d2a6b4d8d53d6b35e46e668c308ff30754ce6c1f Mon Sep 17 00:00:00 2001 From: Paul Duncan Date: Fri, 23 Feb 2024 14:03:05 -0500 Subject: add tests/gen-shake-tests.rb --- tests/gen-shake-tests.rb | 199 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100755 tests/gen-shake-tests.rb diff --git a/tests/gen-shake-tests.rb b/tests/gen-shake-tests.rb new file mode 100755 index 0000000..55aec7c --- /dev/null +++ b/tests/gen-shake-tests.rb @@ -0,0 +1,199 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# Generate SHAKE128 and SHAKE256 test functions from response files in +# SHAKE byte test vectors archive (`shakebytetestvectors.zip`) from the +# Cryptographic Algorithm Validation Program (CAVP). +# +# Usage: +# # download archive of byte test vectors +# wget https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Algorithm-Validation-Program/documents/sha3/shakebytetestvectors.zip +# +# # run script to generate test functions on standard output +# tests/gen-shake-tests.rb path/to/shakebytetestvectors.zip +# + +# load libraries +require 'zip' + +# number of elements per line of test data +PER_LINE = 128 + +# TODO +ALGOS = [{ + name: 'SHAKE128', # algo name + fn: 'shake128', # function prefix + + # response files + rsp_files: %w{ + SHAKE128ShortMsg.rsp + SHAKE128LongMsg.rsp + SHAKE128VariableOut.rsp + }, +}, { + name: 'SHAKE256', # algo name + fn: 'shake256', # function prefix + + # response files + rsp_files: %w{ + SHAKE256ShortMsg.rsp + SHAKE256LongMsg.rsp + SHAKE256VariableOut.rsp + }, +}] + +# test vector template +TEST_TMPL = <d, %d, %d, %d, %d, %d }, +END_TEST_TMPL + +# test function template +FN_TMPL = <s tests from shakebytetestvectors.zip +static void test_%s_xof_cmvp(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)) { + // get response file name and test vector line + const char * const file_name = RSP_FILES[TESTS[i].rsp_file_ofs]; + const size_t file_line = TESTS[i].rsp_file_line; + + fprintf(stderr, "test_%s_xof_cmvp[%%zu, \\"%%s\\", line %%zu] failed:\\ngot = ", i, file_name, file_line); + dump_hex(stderr, got, exp_len); + fprintf(stderr, "\\nexp = "); + dump_hex(stderr, exp, exp_len); + fprintf(stderr, "\\n"); + } + } +} +END_FN_TMPL + +# parse hash of key/value pairs into test vector row +def parse(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 + + # 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_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 + +# get zip path from first command-line argument +zip_path = ARGV.shift +raise "Usage #$0 " unless zip_path + +# open zip file +Zip::File.open(zip_path, 'rb') do |zip| + 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(rsp_file, curr, data.size) { |bytes| data += bytes } + end + + curr = {} # clear current hash + end + end + end + + # get maximum length of expected output + # used to for size of "got" buffer in emitted code + max_exp_len = rows.reduce(0) { |r, row| row[:exp_len] > r ? row[:exp_len] : r } + + # get maximum number of lines of emitted static DATA array + num_lines = data.size / PER_LINE + ((data.size % PER_LINE) > 0 ? 1 : 0) + + # 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 -- cgit v1.2.3