xref: /freebsd/crypto/openssl/test/recipes/15-test_ml_dsa_codecs.t (revision e7be843b4a162e68651d3911f0357ed464915629)
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