ZTS: ICP encryption tests

This commit adds tests that ensure that the ICP crypto_encrypt() and
crypto_decrypt() produce the correct results for all implementations
available on this platform.

The actual ZTS scripts are simple drivers for the crypto_test program in
it's "correctness" mode. This mode takes a file full of test vectors
(inputs and expected outputs), runs them, and checks that the results
are expected. It will run the tests for each implementation of the
algorithm provided by the ICP.

The test vectors are taken from Project Wycheproof, which provides a
huge number of tests, including exercising many edge cases and common
implementation mistakes. These tests are provided are JSON files, so a
program is included here to convert them into a simpler line-based
format for crypto_test to consume.

crypto_test also has a "performance" mode, which will run simple
benchmarks against all implementations provded by the ICP and output
them for comparison. This is not used by ZTS, but is available to assist
with development of new implementations of the underlying primitives.

Thanks-to: Joel Low <joel@joelsplace.sg>
Sponsored-by: https://despairlabs.com/sponsor/
Signed-off-by: Rob Norris <robn@despairlabs.com>
Reviewed-by: Tony Hutter <hutter2@llnl.gov>
Reviewed-by: Tino Reichardt <milky-zfs@mcmilk.de>
Reviewed-by: Attila Fülöp <attila@fueloep.org>
This commit is contained in:
Rob Norris 2025-02-19 21:16:46 +11:00 committed by Tony Hutter
parent ece35e0e66
commit 88b0594f93
14 changed files with 24554 additions and 3 deletions

235
scripts/convert_wycheproof.pl Executable file
View File

@ -0,0 +1,235 @@
#!/usr/bin/env perl
# SPDX-License-Identifier: MIT
#
# Copyright (c) 2025, Rob Norris <robn@despairlabs.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#
# This programs converts AEAD test vectors from Project Wycheproof into a
# format that can be consumed more easily by tests/zfs-tests/cmd/crypto_test.
# See tests/zfs-tests/tests/functional/crypto/README for more info.
#
use 5.010;
use warnings;
use strict;
use JSON qw(decode_json);
sub usage {
say "usage: $0 <infile> [<outfile>]";
exit 1;
}
my ($infile, $outfile) = @ARGV;
usage() if !defined $infile;
open my $infh, '<', $infile or die "E: $infile: $!\n";
my $json = do { local $/; <$infh> };
close $infh;
my $data = decode_json $json;
select STDERR;
# 0.8 had a slightly different format. 0.9* is current, stabilising for 1.0
my $version = $data->{generatorVersion} // "[unknown]";
if ("$version" !~ m/^0\.9[^0-9]/) {
warn
"W: this converter was written for Wycheproof 0.9 test vectors\n".
" input file has version: $version\n".
" bravely continuing, but expect crashes or garbled output\n";
}
# we only support AEAD tests
my $schema = $data->{schema} // "[unknown]";
if ("$schema" ne 'aead_test_schema.json') {
warn
"W: this converter is expecting AEAD test vectors\n".
" input file has schema: $schema\n".
" bravely continuing, but expect crashes or garbled output\n";
}
# sanity check; algorithm is provided
my $algorithm = $data->{algorithm};
if (!defined $algorithm) {
die "E: $infile: required field 'algorithm' not found\n";
}
# sanity check; test count is present and correct
my $ntests = 0;
$ntests += $_ for map { scalar @{$_->{tests}} } @{$data->{testGroups}};
if (!exists $data->{numberOfTests}) {
warn "W: input file has no test count, using mine: $ntests\n";
} elsif ($data->{numberOfTests} != $ntests) {
warn
"W: input file has incorrect test count: $data->{numberOfTests}\n".
" using my own count: $ntests\n";
}
say " version: $version";
say " schema: $schema";
say "algorithm: $algorithm";
say " ntests: $ntests";
my $skipped = 0;
my @tests;
# tests are grouped into "test groups". groups have the same type and IV, key
# and tag sizes. we can infer this info from the tests themselves, but it's
# useful for sanity checks
#
# "testGroups" : [
# {
# "ivSize" : 96,
# "keySize" : 128,
# "tagSize" : 128,
# "type" : "AeadTest",
# "tests" : [ ... ]
#
for my $group (@{$data->{testGroups}}) {
# skip non-AEAD test groups
my $type = $group->{type} // "[unknown]";
if ($type ne 'AeadTest') {
warn "W: group has unexpected type '$type', skipping it\n";
$skipped += @{$data->{tests}};
next;
}
my ($iv_size, $key_size, $tag_size) =
@$group{qw(ivSize keySize tagSize)};
# a typical test:
#
# {
# "tcId" : 48,
# "comment" : "Flipped bit 63 in tag",
# "flags" : [
# "ModifiedTag"
# ],
# "key" : "000102030405060708090a0b0c0d0e0f",
# "iv" : "505152535455565758595a5b",
# "aad" : "",
# "msg" : "202122232425262728292a2b2c2d2e2f",
# "ct" : "eb156d081ed6b6b55f4612f021d87b39",
# "tag" : "d8847dbc326a066988c77ad3863e6083",
# "result" : "invalid"
# },
#
# we include everything in the output. the id is useful output so the
# user can go back to the original test. comment and flags are useful
# for output in a failing test
#
for my $test (@{$group->{tests}}) {
my ($id, $comment, $iv, $key, $msg, $ct, $aad, $tag, $result) =
@$test{qw(tcId comment iv key msg ct aad tag result)};
# sanity check; iv, key and tag must have the length declared
# by the group params
unless (
length_check($id, 'iv', $iv, $iv_size) &&
length_check($id, 'key', $key, $key_size) &&
length_check($id, 'tag', $tag, $tag_size)) {
$skipped++;
next;
}
# flatten and sort the flags into a single string
my $flags;
if ($test->{flags}) {
$flags = join(' ', sort @{$test->{flags}});
}
# the completed test record. we'll emit this later once we're
# finished with the input; the output file is not open yet.
push @tests, [
[ id => $id ],
[ comment => $comment ],
(defined $flags ? [ flags => $flags ] : ()),
[ iv => $iv ],
[ key => $key ],
[ msg => $msg ],
[ ct => $ct ],
[ aad => $aad ],
[ tag => $tag ],
[ result => $result ],
];
}
}
if ($skipped) {
$ntests -= $skipped;
warn "W: skipped $skipped tests; new test count: $ntests\n";
}
if ($ntests == 0) {
die "E: no tests extracted, sorry!\n";
my $outfh;
if ($outfile) {
open $outfh, '>', $outfile or die "E: $outfile: $!\n";
} else {
$outfh = *STDOUT;
}
# the "header" record has the algorithm and count of tests
say $outfh "algorithm: $algorithm";
say $outfh "tests: $ntests";
#
for my $test (@tests) {
# blank line is a record separator
say $outfh "";
# output the test data in a simple record of 'key: value' lines
#
# id: 48
# comment: Flipped bit 63 in tag
# flags: ModifiedTag
# iv: 505152535455565758595a5b
# key: 000102030405060708090a0b0c0d0e0f
# msg: 202122232425262728292a2b2c2d2e2f
# ct: eb156d081ed6b6b55f4612f021d87b39
# aad:
# tag: d8847dbc326a066988c77ad3863e6083
# result: invalid
for my $row (@$test) {
my ($k, $v) = @$row;
say $outfh "$k: $v";
}
}
close $outfh;
# check that the length of hex string matches the wanted number of bits
sub length_check {
my ($id, $name, $hexstr, $wantbits) = @_;
my $got = length($hexstr)/2;
my $want = $wantbits/8;
return 1 if $got == $want;
my $gotbits = $got*8;
say
"W: $id: '$name' has incorrect len, skipping test:\n".
" got $got bytes ($gotbits bits)\n".
" want $want bytes ($wantbits bits)\n";
return;
}

View File

@ -665,6 +665,12 @@ tags = ['functional', 'zap_shrink']
tests = ['crtime_001_pos' ]
tags = ['functional', 'crtime']
[tests/functional/crypto]
tests = ['icp_aes_ccm', 'icp_aes_gcm']
pre =
post =
tags = ['functional', 'crypto']
[tests/functional/ctime]
tests = ['ctime_001_pos' ]
tags = ['functional', 'ctime']

View File

@ -4,6 +4,7 @@
/clonefile
/clone_mmap_cached
/clone_mmap_write
/crypto_test
/devname2devid
/dir_rd_update
/draid

View File

@ -30,6 +30,11 @@ scripts_zfs_tests_bin_PROGRAMS += %D%/btree_test
libzfs_core.la
scripts_zfs_tests_bin_PROGRAMS += %D%/crypto_test
%C%_crypto_test_SOURCES = %D%/crypto_test.c
%C%_crypto_test_LDADD = libzpool.la
if WANT_DEVNAME2DEVID
scripts_zfs_tests_bin_PROGRAMS += %D%/devname2devid
%C%_devname2devid_CFLAGS = $(AM_CFLAGS) $(LIBUDEV_CFLAGS)

File diff suppressed because it is too large Load Diff

View File

@ -186,6 +186,7 @@ export ZFSTEST_FILES='badsend
clonefile
clone_mmap_cached
clone_mmap_write
crypto_test
devname2devid
dir_rd_update
draid

View File

@ -139,6 +139,10 @@ nobase_dist_datadir_zfs_tests_tests_DATA += \
functional/checksum/default.cfg \
functional/clean_mirror/clean_mirror_common.kshlib \
functional/clean_mirror/default.cfg \
functional/crypto/aes_ccm_test.json \
functional/crypto/aes_ccm_test.txt \
functional/crypto/aes_gcm_test.json \
functional/crypto/aes_gcm_test.txt \
functional/cli_root/cli_common.kshlib \
functional/cli_root/zfs_copies/zfs_copies.cfg \
functional/cli_root/zfs_copies/zfs_copies.kshlib \
@ -1427,9 +1431,8 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \
functional/crtime/cleanup.ksh \
functional/crtime/crtime_001_pos.ksh \
functional/crtime/setup.ksh \
functional/ctime/cleanup.ksh \
functional/ctime/ctime_001_pos.ksh \
functional/ctime/setup.ksh \
functional/crypto/icp_aes_ccm.ksh \
functional/crypto/icp_aes_gcm.ksh \
functional/deadman/deadman_ratelimit.ksh \
functional/deadman/deadman_sync.ksh \
functional/deadman/deadman_zio.ksh \

View File

@ -0,0 +1,7 @@
.json test vectors taken from Project Wycheproof:
https://github.com/c2sp/wycheproof
Licensed under the Apache License, Version 2.0
.txt files generated with scripts/convert_wycheproof.pl

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
#!/bin/ksh -p
#
# CDDL HEADER START
#
# This file and its contents are supplied under the terms of the
# Common Development and Distribution License ("CDDL"), version 1.0.
# You may only use this file in accordance with the terms of version
# 1.0 of the CDDL.
#
# A full copy of the text of the CDDL should have accompanied this
# source. A copy of the CDDL is also available via the Internet at
# http://www.illumos.org/license/CDDL.
#
# CDDL HEADER END
#
#
# Copyright (c) 2025, Rob Norris <robn@despairlabs.com>
#
. $STF_SUITE/include/libtest.shlib
log_assert "ICP passes test vectors for AES-CCM"
log_must crypto_test -c $STF_SUITE/tests/functional/crypto/aes_ccm_test.txt
log_pass "ICP passes test vectors for AES-CCM"

View File

@ -0,0 +1,27 @@
#!/bin/ksh -p
#
# CDDL HEADER START
#
# This file and its contents are supplied under the terms of the
# Common Development and Distribution License ("CDDL"), version 1.0.
# You may only use this file in accordance with the terms of version
# 1.0 of the CDDL.
#
# A full copy of the text of the CDDL should have accompanied this
# source. A copy of the CDDL is also available via the Internet at
# http://www.illumos.org/license/CDDL.
#
# CDDL HEADER END
#
#
# Copyright (c) 2025, Rob Norris <robn@despairlabs.com>
#
. $STF_SUITE/include/libtest.shlib
log_assert "ICP passes test vectors for AES-GCM"
log_must crypto_test -c $STF_SUITE/tests/functional/crypto/aes_gcm_test.txt
log_pass "ICP passes test vectors for AES-GCM"