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