1#!/usr/bin/env perl 2 3# SPDX-License-Identifier: MIT 4# 5# Copyright (c) 2025, Rob Norris <robn@despairlabs.com> 6# 7# Permission is hereby granted, free of charge, to any person obtaining a copy 8# of this software and associated documentation files (the "Software"), to 9# deal in the Software without restriction, including without limitation the 10# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11# sell copies of the Software, and to permit persons to whom the Software is 12# furnished to do so, subject to the following conditions: 13# 14# The above copyright notice and this permission notice shall be included in 15# all copies or substantial portions of the Software. 16# 17# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 23# IN THE SOFTWARE. 24 25# 26# This programs converts AEAD test vectors from Project Wycheproof into a 27# format that can be consumed more easily by tests/zfs-tests/cmd/crypto_test. 28# See tests/zfs-tests/tests/functional/crypto/README for more info. 29# 30 31use 5.010; 32use warnings; 33use strict; 34use JSON qw(decode_json); 35 36sub usage { 37 say "usage: $0 <infile> [<outfile>]"; 38 exit 1; 39} 40 41my ($infile, $outfile) = @ARGV; 42 43usage() if !defined $infile; 44 45open my $infh, '<', $infile or die "E: $infile: $!\n"; 46my $json = do { local $/; <$infh> }; 47close $infh; 48 49my $data = decode_json $json; 50 51select STDERR; 52 53# 0.8 had a slightly different format. 0.9* is current, stabilising for 1.0 54my $version = $data->{generatorVersion} // "[unknown]"; 55if ("$version" !~ m/^0\.9[^0-9]/) { 56 warn 57 "W: this converter was written for Wycheproof 0.9 test vectors\n". 58 " input file has version: $version\n". 59 " bravely continuing, but expect crashes or garbled output\n"; 60} 61 62# we only support AEAD tests 63my $schema = $data->{schema} // "[unknown]"; 64if ("$schema" ne 'aead_test_schema.json') { 65 warn 66 "W: this converter is expecting AEAD test vectors\n". 67 " input file has schema: $schema\n". 68 " bravely continuing, but expect crashes or garbled output\n"; 69} 70 71# sanity check; algorithm is provided 72my $algorithm = $data->{algorithm}; 73if (!defined $algorithm) { 74 die "E: $infile: required field 'algorithm' not found\n"; 75} 76 77# sanity check; test count is present and correct 78my $ntests = 0; 79$ntests += $_ for map { scalar @{$_->{tests}} } @{$data->{testGroups}}; 80if (!exists $data->{numberOfTests}) { 81 warn "W: input file has no test count, using mine: $ntests\n"; 82} elsif ($data->{numberOfTests} != $ntests) { 83 warn 84 "W: input file has incorrect test count: $data->{numberOfTests}\n". 85 " using my own count: $ntests\n"; 86} 87 88say " version: $version"; 89say " schema: $schema"; 90say "algorithm: $algorithm"; 91say " ntests: $ntests"; 92 93my $skipped = 0; 94 95my @tests; 96 97# tests are grouped into "test groups". groups have the same type and IV, key 98# and tag sizes. we can infer this info from the tests themselves, but it's 99# useful for sanity checks 100# 101# "testGroups" : [ 102# { 103# "ivSize" : 96, 104# "keySize" : 128, 105# "tagSize" : 128, 106# "type" : "AeadTest", 107# "tests" : [ ... ] 108# 109for my $group (@{$data->{testGroups}}) { 110 # skip non-AEAD test groups 111 my $type = $group->{type} // "[unknown]"; 112 if ($type ne 'AeadTest') { 113 warn "W: group has unexpected type '$type', skipping it\n"; 114 $skipped += @{$data->{tests}}; 115 next; 116 } 117 118 my ($iv_size, $key_size, $tag_size) = 119 @$group{qw(ivSize keySize tagSize)}; 120 121 # a typical test: 122 # 123 # { 124 # "tcId" : 48, 125 # "comment" : "Flipped bit 63 in tag", 126 # "flags" : [ 127 # "ModifiedTag" 128 # ], 129 # "key" : "000102030405060708090a0b0c0d0e0f", 130 # "iv" : "505152535455565758595a5b", 131 # "aad" : "", 132 # "msg" : "202122232425262728292a2b2c2d2e2f", 133 # "ct" : "eb156d081ed6b6b55f4612f021d87b39", 134 # "tag" : "d8847dbc326a066988c77ad3863e6083", 135 # "result" : "invalid" 136 # }, 137 # 138 # we include everything in the output. the id is useful output so the 139 # user can go back to the original test. comment and flags are useful 140 # for output in a failing test 141 # 142 for my $test (@{$group->{tests}}) { 143 my ($id, $comment, $iv, $key, $msg, $ct, $aad, $tag, $result) = 144 @$test{qw(tcId comment iv key msg ct aad tag result)}; 145 146 # sanity check; iv, key and tag must have the length declared 147 # by the group params 148 unless ( 149 length_check($id, 'iv', $iv, $iv_size) && 150 length_check($id, 'key', $key, $key_size) && 151 length_check($id, 'tag', $tag, $tag_size)) { 152 $skipped++; 153 next; 154 } 155 156 # flatten and sort the flags into a single string 157 my $flags; 158 if ($test->{flags}) { 159 $flags = join(' ', sort @{$test->{flags}}); 160 } 161 162 # the completed test record. we'll emit this later once we're 163 # finished with the input; the output file is not open yet. 164 push @tests, [ 165 [ id => $id ], 166 [ comment => $comment ], 167 (defined $flags ? [ flags => $flags ] : ()), 168 [ iv => $iv ], 169 [ key => $key ], 170 [ msg => $msg ], 171 [ ct => $ct ], 172 [ aad => $aad ], 173 [ tag => $tag ], 174 [ result => $result ], 175 ]; 176 } 177} 178 179if ($skipped) { 180 $ntests -= $skipped; 181 warn "W: skipped $skipped tests; new test count: $ntests\n"; 182} 183if ($ntests == 0) { 184 die "E: no tests extracted, sorry!\n"; 185 186 187my $outfh; 188if ($outfile) { 189 open $outfh, '>', $outfile or die "E: $outfile: $!\n"; 190} else { 191 $outfh = *STDOUT; 192} 193 194# the "header" record has the algorithm and count of tests 195say $outfh "algorithm: $algorithm"; 196say $outfh "tests: $ntests"; 197 198# 199for my $test (@tests) { 200 # blank line is a record separator 201 say $outfh ""; 202 203 # output the test data in a simple record of 'key: value' lines 204 # 205 # id: 48 206 # comment: Flipped bit 63 in tag 207 # flags: ModifiedTag 208 # iv: 505152535455565758595a5b 209 # key: 000102030405060708090a0b0c0d0e0f 210 # msg: 202122232425262728292a2b2c2d2e2f 211 # ct: eb156d081ed6b6b55f4612f021d87b39 212 # aad: 213 # tag: d8847dbc326a066988c77ad3863e6083 214 # result: invalid 215 for my $row (@$test) { 216 my ($k, $v) = @$row; 217 say $outfh "$k: $v"; 218 } 219} 220 221close $outfh; 222 223# check that the length of hex string matches the wanted number of bits 224sub length_check { 225 my ($id, $name, $hexstr, $wantbits) = @_; 226 my $got = length($hexstr)/2; 227 my $want = $wantbits/8; 228 return 1 if $got == $want; 229 my $gotbits = $got*8; 230 say 231 "W: $id: '$name' has incorrect len, skipping test:\n". 232 " got $got bytes ($gotbits bits)\n". 233 " want $want bytes ($wantbits bits)\n"; 234 return; 235} 236