#include <openssl/core_dispatch.h>
#include <openssl/core_names.h>
+#include <openssl/err.h>
#include <openssl/params.h>
#include <openssl/proverr.h>
#include <openssl/rand.h>
*/
void ossl_ml_dsa_key_reset(ML_DSA_KEY *key)
{
- vector_zero(&key->s2);
- vector_zero(&key->s1);
- vector_zero(&key->t0);
- vector_free(&key->s1);
+ /*
+ * The allocation for |s1.poly| subsumes those for |s2| and |t0|, which we
+ * must not access after |s1|'s poly is freed.
+ */
+ if (key->s1.poly != NULL) {
+ vector_zero(&key->s1);
+ vector_zero(&key->s2);
+ vector_zero(&key->t0);
+ vector_free(&key->s1);
+ key->s2.poly = NULL;
+ key->t0.poly = NULL;
+ }
+ /* The |t1| vector is public and allocated separately */
vector_free(&key->t1);
OPENSSL_cleanse(key->K, sizeof(key->K));
OPENSSL_free(key->pub_encoding);
int ossl_ml_dsa_generate_key(ML_DSA_KEY *out)
{
size_t seed_len = ML_DSA_SEED_BYTES;
+ uint8_t *sk;
+ int ret;
if (out->seed == NULL) {
if ((out->seed = OPENSSL_malloc(seed_len)) == NULL)
}
}
/* We're generating from a seed, drop private prekey encoding */
- OPENSSL_free(out->priv_encoding);
+ sk = out->priv_encoding;
out->priv_encoding = NULL;
- return keygen_internal(out);
+ if (sk == NULL) {
+ ret = keygen_internal(out);
+ } else {
+ if ((ret = keygen_internal(out)) != 0
+ && memcmp(out->priv_encoding, sk, out->params->sk_len) != 0) {
+ ret = 0;
+ ossl_ml_dsa_key_reset(out);
+ ERR_raise_data(ERR_LIB_PROV, PROV_R_INVALID_KEY,
+ "explicit %s private key does not match seed",
+ out->params->alg);
+ }
+ OPENSSL_free(sk);
+ }
+ return ret;
}
/**
plan skip_all => "ML-DSA isn't supported in this build"
if disabled("ml-dsa");
-plan tests => @algs * (16 + 10 * @formats);
+plan tests => @algs * (23 + 10 * @formats);
my $seed = join ("", map {sprintf "%02x", $_} (0..31));
+my $weed = join ("", map {sprintf "%02x", $_} (1..32));
my $ikme = join ("", map {sprintf "%02x", $_} (0..31));
+my %alg = ("44" => [4, 4, 2560], "65" => [6, 5, 4032], "87" => [8, 7, 4896]);
foreach my $alg (@algs) {
my $pub = sprintf("pub-%s.pem", $alg);
my %formats = map { ($_, sprintf("prv-%s-%s.pem", $alg, $_)) } @formats;
+ my ($k, $l, $sk_len) = @{$alg{$alg}};
+ # The number of low-bits |d| in t_0 is 13 across all the variants
+ my $t0_len = $k * 13 * 32;
# (1 + 6 * @formats) tests
my $i = 0;
ok(run(app(['openssl', 'pkey', '-pubin', '-in', $in0,
'-outform', 'DER', '-out', $der0])));
foreach my $f (keys %formats) {
- my $k = $formats{$f};
+ my $kf = $formats{$f};
my %pruned = %formats;
delete $pruned{$f};
my $rest = join(", ", keys %pruned);
- my $in = data_file($k);
+ my $in = data_file($kf);
my $der = sprintf("pub-%s.%d.der", $alg, $i);
#
# Compare expected DER public key with DER public key of private
ok(run(app([qw(openssl pkeyutl -verify -rawin -pubin -inkey),
$in0, '-in', $der0, '-sigfile', $refsig],
sprintf("Signature verify with pubkey: %s", $alg))));
- while (my ($f, $k) = each %formats) {
- my $sk = data_file($k);
+ while (my ($f, $kf) = each %formats) {
+ my $sk = data_file($kf);
my $s = sprintf("sig-%s.%d.dat", $alg, $i++);
ok(run(app([qw(openssl pkeyutl -sign -rawin -inkey), $sk, '-in', $der0,
qw(-pkeyopt deterministic:1 -out), $s])));
# (2 * @formats) tests
# Check text encoding
- while (my ($f, $k) = each %formats) {
+ while (my ($f, $kf) = each %formats) {
my $txt = sprintf("prv-%s-%s.txt", $alg,
($f =~ m{seed}) ? 'seed' : 'priv');
my $out = sprintf("prv-%s-%s.txt", $alg, $f);
- ok(run(app(['openssl', 'pkey', '-in', data_file($k),
+ ok(run(app(['openssl', 'pkey', '-in', data_file($kf),
'-noout', '-text', '-out', $out])));
ok(!compare(data_file($txt), $out),
sprintf("text form private key: %s with %s", $alg, $f));
}
+
+ # (8 tests): Test import/load seed/priv consistency checks
+ my $real = sprintf('real-%s.der', $alg);
+ my $fake = sprintf('fake-%s.der', $alg);
+ my $mixt = sprintf('mixt-%s.der', $alg);
+ my $mash = sprintf('mash-%s.der', $alg);
+ ok(run(app([qw(openssl genpkey -algorithm), "ml-dsa-$alg",
+ qw(-provparam ml-dsa.output_formats=seed-priv -pkeyopt),
+ "hexseed:$seed", qw(-outform DER -out), $real])),
+ sprintf("create real private key: %s", $alg));
+ ok(run(app([qw(openssl genpkey -algorithm), "ml-dsa-$alg",
+ qw(-provparam ml-dsa.output_formats=seed-priv -pkeyopt),
+ "hexseed:$weed", qw(-outform DER -out), $fake])),
+ sprintf("create fake private key: %s", $alg));
+ my $realfh = IO::File->new($real, "<:raw");
+ my $fakefh = IO::File->new($fake, "<:raw");
+ local $/ = undef;
+ my $realder = <$realfh>;
+ $realfh->close();
+ my $fakeder = <$fakefh>;
+ $fakefh->close();
+ #
+ # - 20 bytes PKCS8 fixed overhead,
+ # - 4 byte private key octet string tag + length
+ # - 4 byte seed + key sequence tag + length
+ # - 2 byte seed tag + length
+ # - 32 byte seed
+ # - 4 byte key tag + length
+ # - $sk_len private key, ending in t0.
+ #
+ my $p8_len = 28 + (2 + 32) + (4 + $sk_len);
+ ok((length($realder) == $p8_len && length($fakeder) == $p8_len),
+ sprintf("Got expected DER lengths of %s seed-priv key", $alg));
+ my $mixtder = substr($realder, 0, 28 + 34)
+ . substr($fakeder, 28 + 34);
+ my $mixtfh = IO::File->new($mixt, ">:raw");
+ print $mixtfh $mixtder;
+ $mixtfh->close();
+ ok(run(app([qw(openssl pkey -inform DER -noout -in), $real])),
+ sprintf("accept valid keypair: %s", $alg));
+ ok(!run(app([qw(openssl pkey -inform DER -noout -in), $mixt])),
+ sprintf("Using seed reject mismatched private %s", $alg));
+ ok(run(app([qw(openssl pkey -provparam ml-dsa.prefer_seed=no),
+ qw(-inform DER -noout -in), $mixt])),
+ sprintf("Ignoring seed accept mismatched private %s", $alg));
+ # Mutate the t0 vector
+ my $mashder = $realder;
+ substr($mashder, -$t0_len, 1) =~ s{(.)}{chr(ord($1)^1)}es;
+ my $mashfh = IO::File->new($mash, ">:raw");
+ print $mashfh $mashder;
+ $mashfh->close();
+ ok(!run(app([qw(openssl pkey -provparam ml-dsa.prefer_seed=no),
+ qw(-inform DER -noout -in), $mash])),
+ sprintf("reject real private and mutated public: %s", $alg));
}
qw(-provparam ml-kem.output_formats=seed-priv -pkeyopt),
"hexseed:$weed", qw(-outform DER -out), $fake])),
sprintf("create fake private key: %s", $alg));
- my $realfh = IO::File->new($real, "r");
- my $fakefh = IO::File->new($fake, "r");
+ my $realfh = IO::File->new($real, "<:raw");
+ my $fakefh = IO::File->new($fake, "<:raw");
local $/ = undef;
my $realder = <$realfh>;
my $fakeder = <$fakefh>;
+ $realfh->close();
+ $fakefh->close();
#
# - 20 bytes PKCS8 fixed overhead,
# - 4 byte private key octet string tag + length
# - |ek| public key ('t' vector || 'rho')
# - implicit rejection 'z' seed component
#
- ok(length($realder) == 28 + (2 + 64) + (4 + $slen + $plen + $zlen)
- && length($fakeder) == 28 + (2 + 64) + (4 + $slen + $plen + $zlen));
+ my $p8_len = 28 + (2 + 64) + (4 + $slen + $plen + $zlen);
+ ok((length($realder) == $p8_len && length($fakeder) == $p8_len),
+ sprintf("Got expected DER lengths of %s seed-priv key", $alg));
my $mixtder = substr($realder, 0, 28 + 66 + 4 + $slen)
. substr($fakeder, 28 + 66 + 4 + $slen, $plen)
. substr($realder, 28 + 66 + 4 + $slen + $plen, $zlen);