openssl/test/recipes/15-test_ml_kem_codecs.t
Jon Spillett 58d548d84e Use text compare for PEM and text files
- Fix ml_dsa_codecs test
- Fix ml_kem_codecs test
- Fix pkey test
- Fix dsaparam test
- Fix dhparam test
- Fix pkcs8 test

Reviewed-by: Tim Hudson <tjh@openssl.org>
Reviewed-by: Shane Lontis <shane.lontis@oracle.com>
Reviewed-by: Dmitry Belyavskiy <beldmit@gmail.com>
Reviewed-by: Paul Dale <ppzgs1@gmail.com>
Reviewed-by: Tomas Mraz <tomas@openssl.org>
(Merged from https://github.com/openssl/openssl/pull/27082)
2025-03-19 11:48:49 +01:00

224 lines
9.8 KiB
Perl

#! /usr/bin/env perl
# Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
#
# Licensed under the Apache License 2.0 (the "License"). You may not use
# this file except in compliance with the License. You can obtain a copy
# in the file LICENSE in the source distribution or at
# https://www.openssl.org/source/license.html
use strict;
use warnings;
use File::Spec;
use File::Copy;
use File::Compare qw/compare_text compare/;
use OpenSSL::Glob;
use OpenSSL::Test qw/:DEFAULT data_file srctop_file bldtop_dir/;
use OpenSSL::Test::Utils;
setup("test_ml_kem_codecs");
my @algs = qw(512 768 1024);
my @formats = qw(seed-priv priv-only seed-only oqskeypair bare-seed bare-priv);
plan skip_all => "ML-KEM isn't supported in this build"
if disabled("ml-kem");
plan tests => @algs * (25 + 10 * @formats);
my $seed = join ("", map {sprintf "%02x", $_} (0..63));
my $weed = join ("", map {sprintf "%02x", $_} (1..64));
my $ikme = join ("", map {sprintf "%02x", $_} (0..31));
foreach my $alg (@algs) {
my $pub = sprintf("pub-%s.pem", $alg);
my %formats = map { ($_, sprintf("prv-%s-%s.pem", $alg, $_)) } @formats;
# (1 + 6 * @formats) tests
my $i = 0;
my $in0 = data_file($pub);
my $der0 = sprintf("pub-%s.%d.der", $alg, $i++);
ok(run(app(['openssl', 'pkey', '-pubin', '-in', $in0,
'-outform', 'DER', '-out', $der0])));
foreach my $f (keys %formats) {
my $k = $formats{$f};
my %pruned = %formats;
delete $pruned{$f};
my $rest = join(", ", keys %pruned);
my $in = data_file($k);
my $der = sprintf("pub-%s.%d.der", $alg, $i);
#
# Compare expected DER public key with DER public key of private
ok(run(app(['openssl', 'pkey', '-in', $in, '-pubout',
'-outform', 'DER', '-out', $der])));
ok(!compare($der0, $der),
sprintf("pubkey DER match: %s, %s", $alg, $f));
#
# Compare expected PEM private key with regenerated key
my $pem = sprintf("prv-%s-%s.%d.pem", $alg, $f, $i++);
ok(run(app(['openssl', 'genpkey', '-out', $pem,
'-pkeyopt', "hexseed:$seed", '-algorithm', "ml-kem-$alg",
'-provparam', "ml-kem.output_formats=$f"])));
ok(!compare_text($in, $pem),
sprintf("prvkey PEM match: %s, %s", $alg, $f));
ok(run(app(['openssl', 'pkey', '-in', $in, '-noout',
'-provparam', "ml-kem.input_formats=$f"])));
ok(!run(app(['openssl', 'pkey', '-in', $in, '-noout',
'-provparam', "ml-kem.input_formats=$rest"])));
}
# (3 + 2 * @formats) tests
# Check encap/decap ciphertext and shared secrets
$i = 0;
my $refct = sprintf("ct-%s.dat", $alg);
my $refss = sprintf("ss-%s.dat", $alg);
my $ct = sprintf("ct-%s.%d.dat", $alg, $i);
my $ss0 = sprintf("ss-%s.%d.dat", $alg, $i++);
ok(run(app(['openssl', 'pkeyutl', '-encap', '-inkey', $in0,
'-pkeyopt', "hexikme:$ikme", '-secret',
$ss0, '-out', $ct])));
ok(!compare($ct, data_file($refct)),
sprintf("reference ciphertext match: %s", $pub));
ok(!compare($ss0, data_file($refss)),
sprintf("reference secret match: %s", $pub));
while (my ($f, $k) = each %formats) {
my $in = data_file($k);
my $ss = sprintf("ss-%s.%d.dat", $alg, $i++);
ok(run(app(['openssl', 'pkeyutl', '-decap', '-inkey', $in,
'-in', $ct, '-secret', $ss])));
ok(!compare($ss0, $ss),
sprintf("shared secret match: %s with %s", $alg, $f));
}
# 6 tests
# Test keygen seed suppression via the command-line and config file.
my $seedless = sprintf("seedless-%s.gen.cli.pem", $alg);
ok(run(app(['openssl', 'genpkey', '-provparam', 'ml-kem.retain_seed=no',
'-algorithm', "ml-kem-$alg", '-pkeyopt', "hexseed:$seed",
'-out', $seedless])));
ok(!compare_text(data_file($formats{'priv-only'}), $seedless),
sprintf("seedless via cli key match: %s", $alg));
{
local $ENV{'OPENSSL_CONF'} = data_file("ml-kem.cnf");
local $ENV{'RETAIN_SEED'} = "no";
$seedless = sprintf("seedless-%s.gen.cnf.pem", $alg);
ok(run(app(['openssl', 'genpkey',
'-algorithm', "ml-kem-$alg", '-pkeyopt', "hexseed:$seed",
'-out', $seedless])));
ok(!compare_text(data_file($formats{'priv-only'}), $seedless),
sprintf("seedless via config match: %s", $alg));
my $seedfull = sprintf("seedfull-%s.gen.conf+cli.pem", $alg);
ok(run(app(['openssl', 'genpkey', '-provparam', 'ml-kem.retain_seed=yes',
'-algorithm', "ml-kem-$alg", '-pkeyopt', "hexseed:$seed",
'-out', $seedfull])));
ok(!compare_text(data_file($formats{'seed-priv'}), $seedfull),
sprintf("seedfull via cli vs. conf key match: %s", $alg));
}
# 6 tests
# Test decoder seed suppression via the config file and command-line.
$seedless = sprintf("seedless-%s.dec.cli.pem", $alg);
ok(run(app(['openssl', 'pkey', '-provparam', 'ml-kem.retain_seed=no',
'-in', data_file($formats{'seed-only'}), '-out', $seedless])));
ok(!compare_text(data_file($formats{'priv-only'}), $seedless),
sprintf("seedless via provparam key match: %s", $alg));
{
local $ENV{'OPENSSL_CONF'} = data_file("ml-kem.cnf");
local $ENV{'RETAIN_SEED'} = "no";
$seedless = sprintf("seedless-%s.dec.cnf.pem", $alg);
ok(run(app(['openssl', 'pkey',
'-in', data_file($formats{'seed-only'}), '-out', $seedless])));
ok(!compare_text(data_file($formats{'priv-only'}), $seedless),
sprintf("seedless via config match: %s", $alg));
my $seedfull = sprintf("seedfull-%s.dec.conf+cli.pem", $alg);
ok(run(app(['openssl', 'pkey', '-provparam', 'ml-kem.retain_seed=yes',
'-in', data_file($formats{'seed-only'}), '-out', $seedfull])));
ok(!compare_text(data_file($formats{'seed-priv'}), $seedfull),
sprintf("seedfull via cli vs. conf key match: %s", $alg));
}
# 2 tests
# Test decoder seed non-preference via the command-line.
my $privpref = sprintf("privpref-%s.dec.cli.pem", $alg);
ok(run(app(['openssl', 'pkey', '-provparam', 'ml-kem.prefer_seed=no',
'-in', data_file($formats{'seed-priv'}), '-out', $privpref])));
ok(!compare_text(data_file($formats{'priv-only'}), $privpref),
sprintf("seed non-preference via provparam key match: %s", $alg));
# (2 * @formats) tests
# Check text encoding
while (my ($f, $k) = each %formats) {
my $txt = sprintf("prv-%s-%s.txt", $alg,
($f =~ m{seed}) ? 'seed' : 'priv');
my $out = sprintf("prv-%s-%s.txt", $alg, $f);
ok(run(app(['openssl', 'pkey', '-in', data_file($k),
'-noout', '-text', '-out', $out])));
ok(!compare_text(data_file($txt), $out),
sprintf("text form private key: %s with %s", $alg, $f));
}
# (6 tests): Test import/load PCT failure
my $real = sprintf('real-%s.der', $alg);
my $fake = sprintf('fake-%s.der', $alg);
my $mixt = sprintf('mixt-%s.der', $alg);
my $mash = sprintf('mash-%s.der', $alg);
my $slen = $alg * 3 / 2; # Secret vector |s|
my $plen = $slen + 64; # Public |t|, |rho| and hash
my $zlen = 32; # FO implicit reject seed
ok(run(app([qw(openssl genpkey -algorithm), "ml-kem-$alg",
qw(-provparam ml-kem.output_formats=seed-priv -pkeyopt),
"hexseed:$seed", qw(-outform DER -out), $real])),
sprintf("create real private key: %s", $alg));
ok(run(app([qw(openssl genpkey -algorithm), "ml-kem-$alg",
qw(-provparam ml-kem.output_formats=seed-priv -pkeyopt),
"hexseed:$weed", qw(-outform DER -out), $fake])),
sprintf("create fake private key: %s", $alg));
my $realfh = IO::File->new($real, "<:raw");
my $fakefh = IO::File->new($fake, "<:raw");
local $/ = undef;
my $realder = <$realfh>;
my $fakeder = <$fakefh>;
$realfh->close();
$fakefh->close();
#
# - 20 bytes PKCS8 fixed overhead,
# - 4 byte private key octet string tag + length
# - 4 byte seed + key sequence tag + length
# - 2 byte seed tag + length
# - 64 byte seed
# - 4 byte key tag + length
# - |dk| 's' vector
# - |ek| public key ('t' vector || 'rho')
# - implicit rejection 'z' seed component
#
my $svec_off = 28 + (2 + 64) + 4;
my $p8_len = $svec_off + $slen + $plen + $zlen;
ok((length($realder) == $p8_len && length($fakeder) == $p8_len),
sprintf("Got expected DER lengths of %s seed-priv key", $alg));
my $mixtder = substr($realder, 0, $svec_off + $slen)
. substr($fakeder, $svec_off + $slen, $plen)
. substr($realder, $svec_off + $slen + $plen, $zlen);
my $mixtfh = IO::File->new($mixt, ">:raw");
print $mixtfh $mixtder;
$mixtfh->close();
ok(run(app([qw(openssl pkey -inform DER -noout -in), $real])),
sprintf("accept valid keypair: %s", $alg));
ok(!run(app([qw(openssl pkey -provparam ml-kem.prefer_seed=no),
qw(-inform DER -noout -in), $mixt])),
sprintf("reject real private and fake public: %s", $alg));
ok(run(app([qw(openssl pkey -provparam ml-kem.prefer_seed=no),
qw(-provparam ml-kem.import_pct_type=none),
qw(-inform DER -noout -in), $mixt])),
sprintf("Absent PCT accept fake public: %s", $alg));
# Mutate the first byte of the |s| vector
my $mashder = $realder;
substr($mashder, $svec_off, 1) =~ s{(.)}{chr(ord($1)^1)}es;
my $mashfh = IO::File->new($mash, ">:raw");
print $mashfh $mashder;
$mashfh->close();
ok(!run(app([qw(openssl pkey -inform DER -noout -in), $mash])),
sprintf("reject real private and mutated public: %s", $alg));
}