1*e7be843bSPierre Pronchery#! /usr/bin/env perl 2*e7be843bSPierre Pronchery# Copyright 2025 The OpenSSL Project Authors. All Rights Reserved. 3*e7be843bSPierre Pronchery# 4*e7be843bSPierre Pronchery# Licensed under the Apache License 2.0 (the "License"). You may not use 5*e7be843bSPierre Pronchery# this file except in compliance with the License. You can obtain a copy 6*e7be843bSPierre Pronchery# in the file LICENSE in the source distribution or at 7*e7be843bSPierre Pronchery# https://www.openssl.org/source/license.html 8*e7be843bSPierre Pronchery 9*e7be843bSPierre Pronchery 10*e7be843bSPierre Proncheryuse strict; 11*e7be843bSPierre Proncheryuse warnings; 12*e7be843bSPierre Pronchery 13*e7be843bSPierre Proncheryuse File::Spec; 14*e7be843bSPierre Proncheryuse File::Copy; 15*e7be843bSPierre Proncheryuse File::Compare qw/compare_text compare/; 16*e7be843bSPierre Proncheryuse IO::File; 17*e7be843bSPierre Proncheryuse OpenSSL::Glob; 18*e7be843bSPierre Proncheryuse OpenSSL::Test qw/:DEFAULT data_file srctop_file bldtop_dir/; 19*e7be843bSPierre Proncheryuse OpenSSL::Test::Utils; 20*e7be843bSPierre Pronchery 21*e7be843bSPierre Proncherysetup("test_ml_kem_codecs"); 22*e7be843bSPierre Pronchery 23*e7be843bSPierre Proncherymy @algs = qw(512 768 1024); 24*e7be843bSPierre Proncherymy @formats = qw(seed-priv priv-only seed-only oqskeypair bare-seed bare-priv); 25*e7be843bSPierre Pronchery 26*e7be843bSPierre Proncheryplan skip_all => "ML-KEM isn't supported in this build" 27*e7be843bSPierre Pronchery if disabled("ml-kem"); 28*e7be843bSPierre Pronchery 29*e7be843bSPierre Proncheryplan tests => @algs * (25 + 10 * @formats); 30*e7be843bSPierre Proncherymy $seed = join ("", map {sprintf "%02x", $_} (0..63)); 31*e7be843bSPierre Proncherymy $weed = join ("", map {sprintf "%02x", $_} (1..64)); 32*e7be843bSPierre Proncherymy $ikme = join ("", map {sprintf "%02x", $_} (0..31)); 33*e7be843bSPierre Pronchery 34*e7be843bSPierre Proncheryforeach my $alg (@algs) { 35*e7be843bSPierre Pronchery my $pub = sprintf("pub-%s.pem", $alg); 36*e7be843bSPierre Pronchery my %formats = map { ($_, sprintf("prv-%s-%s.pem", $alg, $_)) } @formats; 37*e7be843bSPierre Pronchery 38*e7be843bSPierre Pronchery # (1 + 6 * @formats) tests 39*e7be843bSPierre Pronchery my $i = 0; 40*e7be843bSPierre Pronchery my $in0 = data_file($pub); 41*e7be843bSPierre Pronchery my $der0 = sprintf("pub-%s.%d.der", $alg, $i++); 42*e7be843bSPierre Pronchery ok(run(app(['openssl', 'pkey', '-pubin', '-in', $in0, 43*e7be843bSPierre Pronchery '-outform', 'DER', '-out', $der0]))); 44*e7be843bSPierre Pronchery foreach my $f (keys %formats) { 45*e7be843bSPierre Pronchery my $k = $formats{$f}; 46*e7be843bSPierre Pronchery my %pruned = %formats; 47*e7be843bSPierre Pronchery delete $pruned{$f}; 48*e7be843bSPierre Pronchery my $rest = join(", ", keys %pruned); 49*e7be843bSPierre Pronchery my $in = data_file($k); 50*e7be843bSPierre Pronchery my $der = sprintf("pub-%s.%d.der", $alg, $i); 51*e7be843bSPierre Pronchery # 52*e7be843bSPierre Pronchery # Compare expected DER public key with DER public key of private 53*e7be843bSPierre Pronchery ok(run(app(['openssl', 'pkey', '-in', $in, '-pubout', 54*e7be843bSPierre Pronchery '-outform', 'DER', '-out', $der]))); 55*e7be843bSPierre Pronchery ok(!compare($der0, $der), 56*e7be843bSPierre Pronchery sprintf("pubkey DER match: %s, %s", $alg, $f)); 57*e7be843bSPierre Pronchery # 58*e7be843bSPierre Pronchery # Compare expected PEM private key with regenerated key 59*e7be843bSPierre Pronchery my $pem = sprintf("prv-%s-%s.%d.pem", $alg, $f, $i++); 60*e7be843bSPierre Pronchery ok(run(app(['openssl', 'genpkey', '-out', $pem, 61*e7be843bSPierre Pronchery '-pkeyopt', "hexseed:$seed", '-algorithm', "ml-kem-$alg", 62*e7be843bSPierre Pronchery '-provparam', "ml-kem.output_formats=$f"]))); 63*e7be843bSPierre Pronchery ok(!compare_text($in, $pem), 64*e7be843bSPierre Pronchery sprintf("prvkey PEM match: %s, %s", $alg, $f)); 65*e7be843bSPierre Pronchery 66*e7be843bSPierre Pronchery ok(run(app(['openssl', 'pkey', '-in', $in, '-noout', 67*e7be843bSPierre Pronchery '-provparam', "ml-kem.input_formats=$f"]))); 68*e7be843bSPierre Pronchery ok(!run(app(['openssl', 'pkey', '-in', $in, '-noout', 69*e7be843bSPierre Pronchery '-provparam', "ml-kem.input_formats=$rest"]))); 70*e7be843bSPierre Pronchery } 71*e7be843bSPierre Pronchery 72*e7be843bSPierre Pronchery # (3 + 2 * @formats) tests 73*e7be843bSPierre Pronchery # Check encap/decap ciphertext and shared secrets 74*e7be843bSPierre Pronchery $i = 0; 75*e7be843bSPierre Pronchery my $refct = sprintf("ct-%s.dat", $alg); 76*e7be843bSPierre Pronchery my $refss = sprintf("ss-%s.dat", $alg); 77*e7be843bSPierre Pronchery my $ct = sprintf("ct-%s.%d.dat", $alg, $i); 78*e7be843bSPierre Pronchery my $ss0 = sprintf("ss-%s.%d.dat", $alg, $i++); 79*e7be843bSPierre Pronchery ok(run(app(['openssl', 'pkeyutl', '-encap', '-inkey', $in0, 80*e7be843bSPierre Pronchery '-pkeyopt', "hexikme:$ikme", '-secret', 81*e7be843bSPierre Pronchery $ss0, '-out', $ct]))); 82*e7be843bSPierre Pronchery ok(!compare($ct, data_file($refct)), 83*e7be843bSPierre Pronchery sprintf("reference ciphertext match: %s", $pub)); 84*e7be843bSPierre Pronchery ok(!compare($ss0, data_file($refss)), 85*e7be843bSPierre Pronchery sprintf("reference secret match: %s", $pub)); 86*e7be843bSPierre Pronchery while (my ($f, $k) = each %formats) { 87*e7be843bSPierre Pronchery my $in = data_file($k); 88*e7be843bSPierre Pronchery my $ss = sprintf("ss-%s.%d.dat", $alg, $i++); 89*e7be843bSPierre Pronchery ok(run(app(['openssl', 'pkeyutl', '-decap', '-inkey', $in, 90*e7be843bSPierre Pronchery '-in', $ct, '-secret', $ss]))); 91*e7be843bSPierre Pronchery ok(!compare($ss0, $ss), 92*e7be843bSPierre Pronchery sprintf("shared secret match: %s with %s", $alg, $f)); 93*e7be843bSPierre Pronchery } 94*e7be843bSPierre Pronchery 95*e7be843bSPierre Pronchery # 6 tests 96*e7be843bSPierre Pronchery # Test keygen seed suppression via the command-line and config file. 97*e7be843bSPierre Pronchery my $seedless = sprintf("seedless-%s.gen.cli.pem", $alg); 98*e7be843bSPierre Pronchery ok(run(app(['openssl', 'genpkey', '-provparam', 'ml-kem.retain_seed=no', 99*e7be843bSPierre Pronchery '-algorithm', "ml-kem-$alg", '-pkeyopt', "hexseed:$seed", 100*e7be843bSPierre Pronchery '-out', $seedless]))); 101*e7be843bSPierre Pronchery ok(!compare_text(data_file($formats{'priv-only'}), $seedless), 102*e7be843bSPierre Pronchery sprintf("seedless via cli key match: %s", $alg)); 103*e7be843bSPierre Pronchery { 104*e7be843bSPierre Pronchery local $ENV{'OPENSSL_CONF'} = data_file("ml-kem.cnf"); 105*e7be843bSPierre Pronchery local $ENV{'RETAIN_SEED'} = "no"; 106*e7be843bSPierre Pronchery $seedless = sprintf("seedless-%s.gen.cnf.pem", $alg); 107*e7be843bSPierre Pronchery ok(run(app(['openssl', 'genpkey', 108*e7be843bSPierre Pronchery '-algorithm', "ml-kem-$alg", '-pkeyopt', "hexseed:$seed", 109*e7be843bSPierre Pronchery '-out', $seedless]))); 110*e7be843bSPierre Pronchery ok(!compare_text(data_file($formats{'priv-only'}), $seedless), 111*e7be843bSPierre Pronchery sprintf("seedless via config match: %s", $alg)); 112*e7be843bSPierre Pronchery 113*e7be843bSPierre Pronchery my $seedfull = sprintf("seedfull-%s.gen.conf+cli.pem", $alg); 114*e7be843bSPierre Pronchery ok(run(app(['openssl', 'genpkey', '-provparam', 'ml-kem.retain_seed=yes', 115*e7be843bSPierre Pronchery '-algorithm', "ml-kem-$alg", '-pkeyopt', "hexseed:$seed", 116*e7be843bSPierre Pronchery '-out', $seedfull]))); 117*e7be843bSPierre Pronchery ok(!compare_text(data_file($formats{'seed-priv'}), $seedfull), 118*e7be843bSPierre Pronchery sprintf("seedfull via cli vs. conf key match: %s", $alg)); 119*e7be843bSPierre Pronchery } 120*e7be843bSPierre Pronchery 121*e7be843bSPierre Pronchery # 6 tests 122*e7be843bSPierre Pronchery # Test decoder seed suppression via the config file and command-line. 123*e7be843bSPierre Pronchery $seedless = sprintf("seedless-%s.dec.cli.pem", $alg); 124*e7be843bSPierre Pronchery ok(run(app(['openssl', 'pkey', '-provparam', 'ml-kem.retain_seed=no', 125*e7be843bSPierre Pronchery '-in', data_file($formats{'seed-only'}), '-out', $seedless]))); 126*e7be843bSPierre Pronchery ok(!compare_text(data_file($formats{'priv-only'}), $seedless), 127*e7be843bSPierre Pronchery sprintf("seedless via provparam key match: %s", $alg)); 128*e7be843bSPierre Pronchery { 129*e7be843bSPierre Pronchery local $ENV{'OPENSSL_CONF'} = data_file("ml-kem.cnf"); 130*e7be843bSPierre Pronchery local $ENV{'RETAIN_SEED'} = "no"; 131*e7be843bSPierre Pronchery $seedless = sprintf("seedless-%s.dec.cnf.pem", $alg); 132*e7be843bSPierre Pronchery ok(run(app(['openssl', 'pkey', 133*e7be843bSPierre Pronchery '-in', data_file($formats{'seed-only'}), '-out', $seedless]))); 134*e7be843bSPierre Pronchery ok(!compare_text(data_file($formats{'priv-only'}), $seedless), 135*e7be843bSPierre Pronchery sprintf("seedless via config match: %s", $alg)); 136*e7be843bSPierre Pronchery 137*e7be843bSPierre Pronchery my $seedfull = sprintf("seedfull-%s.dec.conf+cli.pem", $alg); 138*e7be843bSPierre Pronchery ok(run(app(['openssl', 'pkey', '-provparam', 'ml-kem.retain_seed=yes', 139*e7be843bSPierre Pronchery '-in', data_file($formats{'seed-only'}), '-out', $seedfull]))); 140*e7be843bSPierre Pronchery ok(!compare_text(data_file($formats{'seed-priv'}), $seedfull), 141*e7be843bSPierre Pronchery sprintf("seedfull via cli vs. conf key match: %s", $alg)); 142*e7be843bSPierre Pronchery } 143*e7be843bSPierre Pronchery 144*e7be843bSPierre Pronchery # 2 tests 145*e7be843bSPierre Pronchery # Test decoder seed non-preference via the command-line. 146*e7be843bSPierre Pronchery my $privpref = sprintf("privpref-%s.dec.cli.pem", $alg); 147*e7be843bSPierre Pronchery ok(run(app(['openssl', 'pkey', '-provparam', 'ml-kem.prefer_seed=no', 148*e7be843bSPierre Pronchery '-in', data_file($formats{'seed-priv'}), '-out', $privpref]))); 149*e7be843bSPierre Pronchery ok(!compare_text(data_file($formats{'priv-only'}), $privpref), 150*e7be843bSPierre Pronchery sprintf("seed non-preference via provparam key match: %s", $alg)); 151*e7be843bSPierre Pronchery 152*e7be843bSPierre Pronchery # (2 * @formats) tests 153*e7be843bSPierre Pronchery # Check text encoding 154*e7be843bSPierre Pronchery while (my ($f, $k) = each %formats) { 155*e7be843bSPierre Pronchery my $txt = sprintf("prv-%s-%s.txt", $alg, 156*e7be843bSPierre Pronchery ($f =~ m{seed}) ? 'seed' : 'priv'); 157*e7be843bSPierre Pronchery my $out = sprintf("prv-%s-%s.txt", $alg, $f); 158*e7be843bSPierre Pronchery ok(run(app(['openssl', 'pkey', '-in', data_file($k), 159*e7be843bSPierre Pronchery '-noout', '-text', '-out', $out]))); 160*e7be843bSPierre Pronchery ok(!compare_text(data_file($txt), $out), 161*e7be843bSPierre Pronchery sprintf("text form private key: %s with %s", $alg, $f)); 162*e7be843bSPierre Pronchery } 163*e7be843bSPierre Pronchery 164*e7be843bSPierre Pronchery # (6 tests): Test import/load PCT failure 165*e7be843bSPierre Pronchery my $real = sprintf('real-%s.der', $alg); 166*e7be843bSPierre Pronchery my $fake = sprintf('fake-%s.der', $alg); 167*e7be843bSPierre Pronchery my $mixt = sprintf('mixt-%s.der', $alg); 168*e7be843bSPierre Pronchery my $mash = sprintf('mash-%s.der', $alg); 169*e7be843bSPierre Pronchery my $slen = $alg * 3 / 2; # Secret vector |s| 170*e7be843bSPierre Pronchery my $plen = $slen + 64; # Public |t|, |rho| and hash 171*e7be843bSPierre Pronchery my $zlen = 32; # FO implicit reject seed 172*e7be843bSPierre Pronchery ok(run(app([qw(openssl genpkey -algorithm), "ml-kem-$alg", 173*e7be843bSPierre Pronchery qw(-provparam ml-kem.output_formats=seed-priv -pkeyopt), 174*e7be843bSPierre Pronchery "hexseed:$seed", qw(-outform DER -out), $real])), 175*e7be843bSPierre Pronchery sprintf("create real private key: %s", $alg)); 176*e7be843bSPierre Pronchery ok(run(app([qw(openssl genpkey -algorithm), "ml-kem-$alg", 177*e7be843bSPierre Pronchery qw(-provparam ml-kem.output_formats=seed-priv -pkeyopt), 178*e7be843bSPierre Pronchery "hexseed:$weed", qw(-outform DER -out), $fake])), 179*e7be843bSPierre Pronchery sprintf("create fake private key: %s", $alg)); 180*e7be843bSPierre Pronchery my $realfh = IO::File->new($real, "<:raw"); 181*e7be843bSPierre Pronchery my $fakefh = IO::File->new($fake, "<:raw"); 182*e7be843bSPierre Pronchery local $/ = undef; 183*e7be843bSPierre Pronchery my $realder = <$realfh>; 184*e7be843bSPierre Pronchery my $fakeder = <$fakefh>; 185*e7be843bSPierre Pronchery $realfh->close(); 186*e7be843bSPierre Pronchery $fakefh->close(); 187*e7be843bSPierre Pronchery # 188*e7be843bSPierre Pronchery # - 20 bytes PKCS8 fixed overhead, 189*e7be843bSPierre Pronchery # - 4 byte private key octet string tag + length 190*e7be843bSPierre Pronchery # - 4 byte seed + key sequence tag + length 191*e7be843bSPierre Pronchery # - 2 byte seed tag + length 192*e7be843bSPierre Pronchery # - 64 byte seed 193*e7be843bSPierre Pronchery # - 4 byte key tag + length 194*e7be843bSPierre Pronchery # - |dk| 's' vector 195*e7be843bSPierre Pronchery # - |ek| public key ('t' vector || 'rho') 196*e7be843bSPierre Pronchery # - implicit rejection 'z' seed component 197*e7be843bSPierre Pronchery # 198*e7be843bSPierre Pronchery my $svec_off = 28 + (2 + 64) + 4; 199*e7be843bSPierre Pronchery my $p8_len = $svec_off + $slen + $plen + $zlen; 200*e7be843bSPierre Pronchery ok((length($realder) == $p8_len && length($fakeder) == $p8_len), 201*e7be843bSPierre Pronchery sprintf("Got expected DER lengths of %s seed-priv key", $alg)); 202*e7be843bSPierre Pronchery my $mixtder = substr($realder, 0, $svec_off + $slen) 203*e7be843bSPierre Pronchery . substr($fakeder, $svec_off + $slen, $plen) 204*e7be843bSPierre Pronchery . substr($realder, $svec_off + $slen + $plen, $zlen); 205*e7be843bSPierre Pronchery my $mixtfh = IO::File->new($mixt, ">:raw"); 206*e7be843bSPierre Pronchery print $mixtfh $mixtder; 207*e7be843bSPierre Pronchery $mixtfh->close(); 208*e7be843bSPierre Pronchery ok(run(app([qw(openssl pkey -inform DER -noout -in), $real])), 209*e7be843bSPierre Pronchery sprintf("accept valid keypair: %s", $alg)); 210*e7be843bSPierre Pronchery ok(!run(app([qw(openssl pkey -provparam ml-kem.prefer_seed=no), 211*e7be843bSPierre Pronchery qw(-inform DER -noout -in), $mixt])), 212*e7be843bSPierre Pronchery sprintf("reject real private and fake public: %s", $alg)); 213*e7be843bSPierre Pronchery ok(run(app([qw(openssl pkey -provparam ml-kem.prefer_seed=no), 214*e7be843bSPierre Pronchery qw(-provparam ml-kem.import_pct_type=none), 215*e7be843bSPierre Pronchery qw(-inform DER -noout -in), $mixt])), 216*e7be843bSPierre Pronchery sprintf("Absent PCT accept fake public: %s", $alg)); 217*e7be843bSPierre Pronchery # Mutate the first byte of the |s| vector 218*e7be843bSPierre Pronchery my $mashder = $realder; 219*e7be843bSPierre Pronchery substr($mashder, $svec_off, 1) =~ s{(.)}{chr(ord($1)^1)}es; 220*e7be843bSPierre Pronchery my $mashfh = IO::File->new($mash, ">:raw"); 221*e7be843bSPierre Pronchery print $mashfh $mashder; 222*e7be843bSPierre Pronchery $mashfh->close(); 223*e7be843bSPierre Pronchery ok(!run(app([qw(openssl pkey -inform DER -noout -in), $mash])), 224*e7be843bSPierre Pronchery sprintf("reject real private and mutated public: %s", $alg)); 225*e7be843bSPierre Pronchery} 226