xref: /freebsd/crypto/openssl/test/recipes/15-test_ml_kem_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_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