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_dsa_codecs"); 22*e7be843bSPierre Pronchery 23*e7be843bSPierre Proncherymy @algs = qw(44 65 87); 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-DSA isn't supported in this build" 27*e7be843bSPierre Pronchery if disabled("ml-dsa"); 28*e7be843bSPierre Pronchery 29*e7be843bSPierre Proncheryplan tests => @algs * (23 + 10 * @formats); 30*e7be843bSPierre Proncherymy $seed = join ("", map {sprintf "%02x", $_} (0..31)); 31*e7be843bSPierre Proncherymy $weed = join ("", map {sprintf "%02x", $_} (1..32)); 32*e7be843bSPierre Proncherymy $ikme = join ("", map {sprintf "%02x", $_} (0..31)); 33*e7be843bSPierre Proncherymy %alg = ("44" => [4, 4, 2560], "65" => [6, 5, 4032], "87" => [8, 7, 4896]); 34*e7be843bSPierre Pronchery 35*e7be843bSPierre Proncheryforeach my $alg (@algs) { 36*e7be843bSPierre Pronchery my $pub = sprintf("pub-%s.pem", $alg); 37*e7be843bSPierre Pronchery my %formats = map { ($_, sprintf("prv-%s-%s.pem", $alg, $_)) } @formats; 38*e7be843bSPierre Pronchery my ($k, $l, $sk_len) = @{$alg{$alg}}; 39*e7be843bSPierre Pronchery # The number of low-bits |d| in t_0 is 13 across all the variants 40*e7be843bSPierre Pronchery my $t0_len = $k * 13 * 32; 41*e7be843bSPierre Pronchery 42*e7be843bSPierre Pronchery # (1 + 6 * @formats) tests 43*e7be843bSPierre Pronchery my $i = 0; 44*e7be843bSPierre Pronchery my $in0 = data_file($pub); 45*e7be843bSPierre Pronchery my $der0 = sprintf("pub-%s.%d.der", $alg, $i++); 46*e7be843bSPierre Pronchery ok(run(app(['openssl', 'pkey', '-pubin', '-in', $in0, 47*e7be843bSPierre Pronchery '-outform', 'DER', '-out', $der0]))); 48*e7be843bSPierre Pronchery foreach my $f (keys %formats) { 49*e7be843bSPierre Pronchery my $kf = $formats{$f}; 50*e7be843bSPierre Pronchery my %pruned = %formats; 51*e7be843bSPierre Pronchery delete $pruned{$f}; 52*e7be843bSPierre Pronchery my $rest = join(", ", keys %pruned); 53*e7be843bSPierre Pronchery my $in = data_file($kf); 54*e7be843bSPierre Pronchery my $der = sprintf("pub-%s.%d.der", $alg, $i); 55*e7be843bSPierre Pronchery # 56*e7be843bSPierre Pronchery # Compare expected DER public key with DER public key of private 57*e7be843bSPierre Pronchery ok(run(app(['openssl', 'pkey', '-in', $in, '-pubout', 58*e7be843bSPierre Pronchery '-outform', 'DER', '-out', $der]))); 59*e7be843bSPierre Pronchery ok(!compare($der0, $der), 60*e7be843bSPierre Pronchery sprintf("pubkey DER match: %s, %s", $alg, $f)); 61*e7be843bSPierre Pronchery # 62*e7be843bSPierre Pronchery # Compare expected PEM private key with regenerated key 63*e7be843bSPierre Pronchery my $pem = sprintf("prv-%s-%s.%d.pem", $alg, $f, $i++); 64*e7be843bSPierre Pronchery ok(run(app(['openssl', 'genpkey', '-out', $pem, 65*e7be843bSPierre Pronchery '-pkeyopt', "hexseed:$seed", '-algorithm', "ml-dsa-$alg", 66*e7be843bSPierre Pronchery '-provparam', "ml-dsa.output_formats=$f"]))); 67*e7be843bSPierre Pronchery ok(!compare_text($in, $pem), 68*e7be843bSPierre Pronchery sprintf("prvkey PEM match: %s, %s", $alg, $f)); 69*e7be843bSPierre Pronchery 70*e7be843bSPierre Pronchery ok(run(app(['openssl', 'pkey', '-in', $in, '-noout', 71*e7be843bSPierre Pronchery '-provparam', "ml-dsa.input_formats=$f"]))); 72*e7be843bSPierre Pronchery ok(!run(app(['openssl', 'pkey', '-in', $in, '-noout', 73*e7be843bSPierre Pronchery '-provparam', "ml-dsa.input_formats=$rest"]))); 74*e7be843bSPierre Pronchery } 75*e7be843bSPierre Pronchery 76*e7be843bSPierre Pronchery # (1 + 2 * @formats) tests 77*e7be843bSPierre Pronchery # Perform sign/verify PCT 78*e7be843bSPierre Pronchery $i = 0; 79*e7be843bSPierre Pronchery my $refsig = data_file(sprintf("sig-%s.dat", $alg)); 80*e7be843bSPierre Pronchery my $sig = sprintf("sig-%s.%d.dat", $alg, $i); 81*e7be843bSPierre Pronchery ok(run(app([qw(openssl pkeyutl -verify -rawin -pubin -inkey), 82*e7be843bSPierre Pronchery $in0, '-in', $der0, '-sigfile', $refsig], 83*e7be843bSPierre Pronchery sprintf("Signature verify with pubkey: %s", $alg)))); 84*e7be843bSPierre Pronchery while (my ($f, $kf) = each %formats) { 85*e7be843bSPierre Pronchery my $sk = data_file($kf); 86*e7be843bSPierre Pronchery my $s = sprintf("sig-%s.%d.dat", $alg, $i++); 87*e7be843bSPierre Pronchery ok(run(app([qw(openssl pkeyutl -sign -rawin -inkey), $sk, '-in', $der0, 88*e7be843bSPierre Pronchery qw(-pkeyopt deterministic:1 -out), $s]))); 89*e7be843bSPierre Pronchery ok(!compare($s, $refsig), 90*e7be843bSPierre Pronchery sprintf("Signature blob match %s with %s", $alg, $f)); 91*e7be843bSPierre Pronchery } 92*e7be843bSPierre Pronchery 93*e7be843bSPierre Pronchery # 6 tests 94*e7be843bSPierre Pronchery # Test keygen seed suppression via the command-line and config file. 95*e7be843bSPierre Pronchery my $seedless = sprintf("seedless-%s.gen.cli.pem", $alg); 96*e7be843bSPierre Pronchery ok(run(app([qw(openssl genpkey -provparam ml-dsa.retain_seed=no), 97*e7be843bSPierre Pronchery '-algorithm', "ml-dsa-$alg", '-pkeyopt', "hexseed:$seed", 98*e7be843bSPierre Pronchery '-out', $seedless]))); 99*e7be843bSPierre Pronchery ok(!compare_text(data_file($formats{'priv-only'}), $seedless), 100*e7be843bSPierre Pronchery sprintf("seedless via cli key match: %s", $alg)); 101*e7be843bSPierre Pronchery { 102*e7be843bSPierre Pronchery local $ENV{'OPENSSL_CONF'} = data_file("ml-dsa.cnf"); 103*e7be843bSPierre Pronchery local $ENV{'RETAIN_SEED'} = "no"; 104*e7be843bSPierre Pronchery $seedless = sprintf("seedless-%s.gen.cnf.pem", $alg); 105*e7be843bSPierre Pronchery ok(run(app(['openssl', 'genpkey', 106*e7be843bSPierre Pronchery '-algorithm', "ml-dsa-$alg", '-pkeyopt', "hexseed:$seed", 107*e7be843bSPierre Pronchery '-out', $seedless]))); 108*e7be843bSPierre Pronchery ok(!compare_text(data_file($formats{'priv-only'}), $seedless), 109*e7be843bSPierre Pronchery sprintf("seedless via config match: %s", $alg)); 110*e7be843bSPierre Pronchery 111*e7be843bSPierre Pronchery my $seedfull = sprintf("seedfull-%s.gen.conf+cli.pem", $alg); 112*e7be843bSPierre Pronchery ok(run(app(['openssl', 'genpkey', '-provparam', 'ml-dsa.retain_seed=yes', 113*e7be843bSPierre Pronchery '-algorithm', "ml-dsa-$alg", '-pkeyopt', "hexseed:$seed", 114*e7be843bSPierre Pronchery '-out', $seedfull]))); 115*e7be843bSPierre Pronchery ok(!compare_text(data_file($formats{'seed-priv'}), $seedfull), 116*e7be843bSPierre Pronchery sprintf("seedfull via cli vs. conf key match: %s", $alg)); 117*e7be843bSPierre Pronchery } 118*e7be843bSPierre Pronchery 119*e7be843bSPierre Pronchery # 6 tests 120*e7be843bSPierre Pronchery # Test decoder seed suppression via the config file and command-line. 121*e7be843bSPierre Pronchery $seedless = sprintf("seedless-%s.dec.cli.pem", $alg); 122*e7be843bSPierre Pronchery ok(run(app(['openssl', 'pkey', '-provparam', 'ml-dsa.retain_seed=no', 123*e7be843bSPierre Pronchery '-in', data_file($formats{'seed-only'}), '-out', $seedless]))); 124*e7be843bSPierre Pronchery ok(!compare_text(data_file($formats{'priv-only'}), $seedless), 125*e7be843bSPierre Pronchery sprintf("seedless via provparam key match: %s", $alg)); 126*e7be843bSPierre Pronchery { 127*e7be843bSPierre Pronchery local $ENV{'OPENSSL_CONF'} = data_file("ml-dsa.cnf"); 128*e7be843bSPierre Pronchery local $ENV{'RETAIN_SEED'} = "no"; 129*e7be843bSPierre Pronchery $seedless = sprintf("seedless-%s.dec.cnf.pem", $alg); 130*e7be843bSPierre Pronchery ok(run(app(['openssl', 'pkey', 131*e7be843bSPierre Pronchery '-in', data_file($formats{'seed-only'}), '-out', $seedless]))); 132*e7be843bSPierre Pronchery ok(!compare_text(data_file($formats{'priv-only'}), $seedless), 133*e7be843bSPierre Pronchery sprintf("seedless via config match: %s", $alg)); 134*e7be843bSPierre Pronchery 135*e7be843bSPierre Pronchery my $seedfull = sprintf("seedfull-%s.dec.conf+cli.pem", $alg); 136*e7be843bSPierre Pronchery ok(run(app(['openssl', 'pkey', '-provparam', 'ml-dsa.retain_seed=yes', 137*e7be843bSPierre Pronchery '-in', data_file($formats{'seed-only'}), '-out', $seedfull]))); 138*e7be843bSPierre Pronchery ok(!compare_text(data_file($formats{'seed-priv'}), $seedfull), 139*e7be843bSPierre Pronchery sprintf("seedfull via cli vs. conf key match: %s", $alg)); 140*e7be843bSPierre Pronchery } 141*e7be843bSPierre Pronchery 142*e7be843bSPierre Pronchery # 2 tests 143*e7be843bSPierre Pronchery # Test decoder seed non-preference via the command-line. 144*e7be843bSPierre Pronchery my $privpref = sprintf("privpref-%s.dec.cli.pem", $alg); 145*e7be843bSPierre Pronchery ok(run(app(['openssl', 'pkey', '-provparam', 'ml-dsa.prefer_seed=no', 146*e7be843bSPierre Pronchery '-in', data_file($formats{'seed-priv'}), '-out', $privpref]))); 147*e7be843bSPierre Pronchery ok(!compare_text(data_file($formats{'priv-only'}), $privpref), 148*e7be843bSPierre Pronchery sprintf("seed non-preference via provparam key match: %s", $alg)); 149*e7be843bSPierre Pronchery 150*e7be843bSPierre Pronchery # (2 * @formats) tests 151*e7be843bSPierre Pronchery # Check text encoding 152*e7be843bSPierre Pronchery while (my ($f, $kf) = each %formats) { 153*e7be843bSPierre Pronchery my $txt = sprintf("prv-%s-%s.txt", $alg, 154*e7be843bSPierre Pronchery ($f =~ m{seed}) ? 'seed' : 'priv'); 155*e7be843bSPierre Pronchery my $out = sprintf("prv-%s-%s.txt", $alg, $f); 156*e7be843bSPierre Pronchery ok(run(app(['openssl', 'pkey', '-in', data_file($kf), 157*e7be843bSPierre Pronchery '-noout', '-text', '-out', $out]))); 158*e7be843bSPierre Pronchery ok(!compare_text(data_file($txt), $out), 159*e7be843bSPierre Pronchery sprintf("text form private key: %s with %s", $alg, $f)); 160*e7be843bSPierre Pronchery } 161*e7be843bSPierre Pronchery 162*e7be843bSPierre Pronchery # (8 tests): Test import/load seed/priv consistency checks 163*e7be843bSPierre Pronchery my $real = sprintf('real-%s.der', $alg); 164*e7be843bSPierre Pronchery my $fake = sprintf('fake-%s.der', $alg); 165*e7be843bSPierre Pronchery my $mixt = sprintf('mixt-%s.der', $alg); 166*e7be843bSPierre Pronchery my $mash = sprintf('mash-%s.der', $alg); 167*e7be843bSPierre Pronchery ok(run(app([qw(openssl genpkey -algorithm), "ml-dsa-$alg", 168*e7be843bSPierre Pronchery qw(-provparam ml-dsa.output_formats=seed-priv -pkeyopt), 169*e7be843bSPierre Pronchery "hexseed:$seed", qw(-outform DER -out), $real])), 170*e7be843bSPierre Pronchery sprintf("create real private key: %s", $alg)); 171*e7be843bSPierre Pronchery ok(run(app([qw(openssl genpkey -algorithm), "ml-dsa-$alg", 172*e7be843bSPierre Pronchery qw(-provparam ml-dsa.output_formats=seed-priv -pkeyopt), 173*e7be843bSPierre Pronchery "hexseed:$weed", qw(-outform DER -out), $fake])), 174*e7be843bSPierre Pronchery sprintf("create fake private key: %s", $alg)); 175*e7be843bSPierre Pronchery my $realfh = IO::File->new($real, "<:raw"); 176*e7be843bSPierre Pronchery my $fakefh = IO::File->new($fake, "<:raw"); 177*e7be843bSPierre Pronchery local $/ = undef; 178*e7be843bSPierre Pronchery my $realder = <$realfh>; 179*e7be843bSPierre Pronchery $realfh->close(); 180*e7be843bSPierre Pronchery my $fakeder = <$fakefh>; 181*e7be843bSPierre Pronchery $fakefh->close(); 182*e7be843bSPierre Pronchery # 183*e7be843bSPierre Pronchery # - 20 bytes PKCS8 fixed overhead, 184*e7be843bSPierre Pronchery # - 4 byte private key octet string tag + length 185*e7be843bSPierre Pronchery # - 4 byte seed + key sequence tag + length 186*e7be843bSPierre Pronchery # - 2 byte seed tag + length 187*e7be843bSPierre Pronchery # - 32 byte seed 188*e7be843bSPierre Pronchery # - 4 byte key tag + length 189*e7be843bSPierre Pronchery # - $sk_len private key, ending in t0. 190*e7be843bSPierre Pronchery # 191*e7be843bSPierre Pronchery my $p8_len = 28 + (2 + 32) + (4 + $sk_len); 192*e7be843bSPierre Pronchery ok((length($realder) == $p8_len && length($fakeder) == $p8_len), 193*e7be843bSPierre Pronchery sprintf("Got expected DER lengths of %s seed-priv key", $alg)); 194*e7be843bSPierre Pronchery my $mixtder = substr($realder, 0, 28 + 34) 195*e7be843bSPierre Pronchery . substr($fakeder, 28 + 34); 196*e7be843bSPierre Pronchery my $mixtfh = IO::File->new($mixt, ">:raw"); 197*e7be843bSPierre Pronchery print $mixtfh $mixtder; 198*e7be843bSPierre Pronchery $mixtfh->close(); 199*e7be843bSPierre Pronchery ok(run(app([qw(openssl pkey -inform DER -noout -in), $real])), 200*e7be843bSPierre Pronchery sprintf("accept valid keypair: %s", $alg)); 201*e7be843bSPierre Pronchery ok(!run(app([qw(openssl pkey -inform DER -noout -in), $mixt])), 202*e7be843bSPierre Pronchery sprintf("Using seed reject mismatched private %s", $alg)); 203*e7be843bSPierre Pronchery ok(run(app([qw(openssl pkey -provparam ml-dsa.prefer_seed=no), 204*e7be843bSPierre Pronchery qw(-inform DER -noout -in), $mixt])), 205*e7be843bSPierre Pronchery sprintf("Ignoring seed accept mismatched private %s", $alg)); 206*e7be843bSPierre Pronchery # Mutate the t0 vector 207*e7be843bSPierre Pronchery my $mashder = $realder; 208*e7be843bSPierre Pronchery substr($mashder, -$t0_len, 1) =~ s{(.)}{chr(ord($1)^1)}es; 209*e7be843bSPierre Pronchery my $mashfh = IO::File->new($mash, ">:raw"); 210*e7be843bSPierre Pronchery print $mashfh $mashder; 211*e7be843bSPierre Pronchery $mashfh->close(); 212*e7be843bSPierre Pronchery ok(!run(app([qw(openssl pkey -provparam ml-dsa.prefer_seed=no), 213*e7be843bSPierre Pronchery qw(-inform DER -noout -in), $mash])), 214*e7be843bSPierre Pronchery sprintf("reject real private and mutated public: %s", $alg)); 215*e7be843bSPierre Pronchery} 216