blob: 67f8f93e3dc6b6dcb4acfce452b52a84c3f9dc6c [file] [edit]
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* KUnit tests and benchmark for ML-DSA
*
* Copyright 2025 Google LLC
*/
#include <crypto/mldsa.h>
#include <kunit/test.h>
#include <linux/random.h>
#include <linux/unaligned.h>
#define Q 8380417 /* The prime q = 2^23 - 2^13 + 1 */
/* ML-DSA parameters that the tests use */
static const struct {
int sig_len;
int pk_len;
int k;
int lambda;
int gamma1;
int beta;
int omega;
} params[] = {
[MLDSA44] = {
.sig_len = MLDSA44_SIGNATURE_SIZE,
.pk_len = MLDSA44_PUBLIC_KEY_SIZE,
.k = 4,
.lambda = 128,
.gamma1 = 1 << 17,
.beta = 78,
.omega = 80,
},
[MLDSA65] = {
.sig_len = MLDSA65_SIGNATURE_SIZE,
.pk_len = MLDSA65_PUBLIC_KEY_SIZE,
.k = 6,
.lambda = 192,
.gamma1 = 1 << 19,
.beta = 196,
.omega = 55,
},
[MLDSA87] = {
.sig_len = MLDSA87_SIGNATURE_SIZE,
.pk_len = MLDSA87_PUBLIC_KEY_SIZE,
.k = 8,
.lambda = 256,
.gamma1 = 1 << 19,
.beta = 120,
.omega = 75,
},
};
#include "mldsa-testvecs.h"
static void do_mldsa_and_assert_success(struct kunit *test,
const struct mldsa_testvector *tv)
{
int err = mldsa_verify(tv->alg, tv->sig, tv->sig_len, tv->msg,
tv->msg_len, tv->pk, tv->pk_len);
KUNIT_ASSERT_EQ(test, err, 0);
}
static u8 *kunit_kmemdup_or_fail(struct kunit *test, const u8 *src, size_t len)
{
u8 *dst = kunit_kmalloc(test, len, GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, dst);
return memcpy(dst, src, len);
}
/*
* Test that changing coefficients in a valid signature's z vector results in
* the following behavior from mldsa_verify():
*
* * -EBADMSG if a coefficient is changed to have an out-of-range value, i.e.
* absolute value >= gamma1 - beta, corresponding to the verifier detecting
* the out-of-range coefficient and rejecting the signature as malformed
*
* * -EKEYREJECTED if a coefficient is changed to a different in-range value,
* i.e. absolute value < gamma1 - beta, corresponding to the verifier
* continuing to the "real" signature check and that check failing
*/
static void test_mldsa_z_range(struct kunit *test,
const struct mldsa_testvector *tv)
{
u8 *sig = kunit_kmemdup_or_fail(test, tv->sig, tv->sig_len);
const int lambda = params[tv->alg].lambda;
const s32 gamma1 = params[tv->alg].gamma1;
const int beta = params[tv->alg].beta;
/*
* We just modify the first coefficient. The coefficient is gamma1
* minus either the first 18 or 20 bits of the u32, depending on gamma1.
*
* The layout of ML-DSA signatures is ctilde || z || h. ctilde is
* lambda / 4 bytes, so z starts at &sig[lambda / 4].
*/
u8 *z_ptr = &sig[lambda / 4];
const u32 z_data = get_unaligned_le32(z_ptr);
const u32 mask = (gamma1 << 1) - 1;
/* These are the four boundaries of the out-of-range values. */
const s32 out_of_range_coeffs[] = {
-gamma1 + 1,
-(gamma1 - beta),
gamma1,
gamma1 - beta,
};
/*
* These are the two boundaries of the valid range, along with 0. We
* assume that none of these matches the original coefficient.
*/
const s32 in_range_coeffs[] = {
-(gamma1 - beta - 1),
0,
gamma1 - beta - 1,
};
/* Initially the signature is valid. */
do_mldsa_and_assert_success(test, tv);
/* Test some out-of-range coefficients. */
for (int i = 0; i < ARRAY_SIZE(out_of_range_coeffs); i++) {
const s32 c = out_of_range_coeffs[i];
put_unaligned_le32((z_data & ~mask) | (mask & (gamma1 - c)),
z_ptr);
KUNIT_ASSERT_EQ(test, -EBADMSG,
mldsa_verify(tv->alg, sig, tv->sig_len, tv->msg,
tv->msg_len, tv->pk, tv->pk_len));
}
/* Test some in-range coefficients. */
for (int i = 0; i < ARRAY_SIZE(in_range_coeffs); i++) {
const s32 c = in_range_coeffs[i];
put_unaligned_le32((z_data & ~mask) | (mask & (gamma1 - c)),
z_ptr);
KUNIT_ASSERT_EQ(test, -EKEYREJECTED,
mldsa_verify(tv->alg, sig, tv->sig_len, tv->msg,
tv->msg_len, tv->pk, tv->pk_len));
}
}
/* Test that mldsa_verify() rejects malformed hint vectors with -EBADMSG. */
static void test_mldsa_bad_hints(struct kunit *test,
const struct mldsa_testvector *tv)
{
const int omega = params[tv->alg].omega;
const int k = params[tv->alg].k;
u8 *sig = kunit_kmemdup_or_fail(test, tv->sig, tv->sig_len);
/* Pointer to the encoded hint vector in the signature */
u8 *hintvec = &sig[tv->sig_len - omega - k];
u8 h;
/* Initially the signature is valid. */
do_mldsa_and_assert_success(test, tv);
/* Cumulative hint count exceeds omega */
memcpy(sig, tv->sig, tv->sig_len);
hintvec[omega + k - 1] = omega + 1;
KUNIT_ASSERT_EQ(test, -EBADMSG,
mldsa_verify(tv->alg, sig, tv->sig_len, tv->msg,
tv->msg_len, tv->pk, tv->pk_len));
/* Cumulative hint count decreases */
memcpy(sig, tv->sig, tv->sig_len);
KUNIT_ASSERT_GE(test, hintvec[omega + k - 2], 1);
hintvec[omega + k - 1] = hintvec[omega + k - 2] - 1;
KUNIT_ASSERT_EQ(test, -EBADMSG,
mldsa_verify(tv->alg, sig, tv->sig_len, tv->msg,
tv->msg_len, tv->pk, tv->pk_len));
/*
* Hint indices out of order. To test this, swap hintvec[0] and
* hintvec[1]. This assumes that the original valid signature had at
* least two nonzero hints in the first element (asserted below).
*/
memcpy(sig, tv->sig, tv->sig_len);
KUNIT_ASSERT_GE(test, hintvec[omega], 2);
h = hintvec[0];
hintvec[0] = hintvec[1];
hintvec[1] = h;
KUNIT_ASSERT_EQ(test, -EBADMSG,
mldsa_verify(tv->alg, sig, tv->sig_len, tv->msg,
tv->msg_len, tv->pk, tv->pk_len));
/*
* Extra hint indices given. For this test to work, the original valid
* signature must have fewer than omega nonzero hints (asserted below).
*/
memcpy(sig, tv->sig, tv->sig_len);
KUNIT_ASSERT_LT(test, hintvec[omega + k - 1], omega);
hintvec[omega - 1] = 0xff;
KUNIT_ASSERT_EQ(test, -EBADMSG,
mldsa_verify(tv->alg, sig, tv->sig_len, tv->msg,
tv->msg_len, tv->pk, tv->pk_len));
}
static void test_mldsa_mutation(struct kunit *test,
const struct mldsa_testvector *tv)
{
const int sig_len = tv->sig_len;
const int msg_len = tv->msg_len;
const int pk_len = tv->pk_len;
const int num_iter = 200;
u8 *sig = kunit_kmemdup_or_fail(test, tv->sig, sig_len);
u8 *msg = kunit_kmemdup_or_fail(test, tv->msg, msg_len);
u8 *pk = kunit_kmemdup_or_fail(test, tv->pk, pk_len);
/* Initially the signature is valid. */
do_mldsa_and_assert_success(test, tv);
/* Changing any bit in the signature should invalidate the signature */
for (int i = 0; i < num_iter; i++) {
size_t pos = get_random_u32_below(sig_len);
u8 b = 1 << get_random_u32_below(8);
sig[pos] ^= b;
KUNIT_ASSERT_NE(test, 0,
mldsa_verify(tv->alg, sig, sig_len, msg,
msg_len, pk, pk_len));
sig[pos] ^= b;
}
/* Changing any bit in the message should invalidate the signature */
for (int i = 0; i < num_iter; i++) {
size_t pos = get_random_u32_below(msg_len);
u8 b = 1 << get_random_u32_below(8);
msg[pos] ^= b;
KUNIT_ASSERT_NE(test, 0,
mldsa_verify(tv->alg, sig, sig_len, msg,
msg_len, pk, pk_len));
msg[pos] ^= b;
}
/* Changing any bit in the public key should invalidate the signature */
for (int i = 0; i < num_iter; i++) {
size_t pos = get_random_u32_below(pk_len);
u8 b = 1 << get_random_u32_below(8);
pk[pos] ^= b;
KUNIT_ASSERT_NE(test, 0,
mldsa_verify(tv->alg, sig, sig_len, msg,
msg_len, pk, pk_len));
pk[pos] ^= b;
}
/* All changes should have been undone. */
KUNIT_ASSERT_EQ(test, 0,
mldsa_verify(tv->alg, sig, sig_len, msg, msg_len, pk,
pk_len));
}
static void test_mldsa(struct kunit *test, const struct mldsa_testvector *tv)
{
/* Valid signature */
KUNIT_ASSERT_EQ(test, tv->sig_len, params[tv->alg].sig_len);
KUNIT_ASSERT_EQ(test, tv->pk_len, params[tv->alg].pk_len);
do_mldsa_and_assert_success(test, tv);
/* Signature too short */
KUNIT_ASSERT_EQ(test, -EBADMSG,
mldsa_verify(tv->alg, tv->sig, tv->sig_len - 1, tv->msg,
tv->msg_len, tv->pk, tv->pk_len));
/* Signature too long */
KUNIT_ASSERT_EQ(test, -EBADMSG,
mldsa_verify(tv->alg, tv->sig, tv->sig_len + 1, tv->msg,
tv->msg_len, tv->pk, tv->pk_len));
/* Public key too short */
KUNIT_ASSERT_EQ(test, -EBADMSG,
mldsa_verify(tv->alg, tv->sig, tv->sig_len, tv->msg,
tv->msg_len, tv->pk, tv->pk_len - 1));
/* Public key too long */
KUNIT_ASSERT_EQ(test, -EBADMSG,
mldsa_verify(tv->alg, tv->sig, tv->sig_len, tv->msg,
tv->msg_len, tv->pk, tv->pk_len + 1));
/*
* Message too short. Error is EKEYREJECTED because it gets rejected by
* the "real" signature check rather than the well-formedness checks.
*/
KUNIT_ASSERT_EQ(test, -EKEYREJECTED,
mldsa_verify(tv->alg, tv->sig, tv->sig_len, tv->msg,
tv->msg_len - 1, tv->pk, tv->pk_len));
/*
* Can't simply try (tv->msg, tv->msg_len + 1) too, as tv->msg would be
* accessed out of bounds. However, ML-DSA just hashes the message and
* doesn't handle different message lengths differently anyway.
*/
/* Test the validity checks on the z vector. */
test_mldsa_z_range(test, tv);
/* Test the validity checks on the hint vector. */
test_mldsa_bad_hints(test, tv);
/* Test randomly mutating the inputs. */
test_mldsa_mutation(test, tv);
}
static void test_mldsa44(struct kunit *test)
{
test_mldsa(test, &mldsa44_testvector);
}
static void test_mldsa65(struct kunit *test)
{
test_mldsa(test, &mldsa65_testvector);
}
static void test_mldsa87(struct kunit *test)
{
test_mldsa(test, &mldsa87_testvector);
}
static s32 mod(s32 a, s32 m)
{
a %= m;
if (a < 0)
a += m;
return a;
}
static s32 symmetric_mod(s32 a, s32 m)
{
a = mod(a, m);
if (a > m / 2)
a -= m;
return a;
}
/* Mechanical, inefficient translation of FIPS 204 Algorithm 36, Decompose */
static void decompose_ref(s32 r, s32 gamma2, s32 *r0, s32 *r1)
{
s32 rplus = mod(r, Q);
*r0 = symmetric_mod(rplus, 2 * gamma2);
if (rplus - *r0 == Q - 1) {
*r1 = 0;
*r0 = *r0 - 1;
} else {
*r1 = (rplus - *r0) / (2 * gamma2);
}
}
/* Mechanical, inefficient translation of FIPS 204 Algorithm 40, UseHint */
static s32 use_hint_ref(u8 h, s32 r, s32 gamma2)
{
s32 m = (Q - 1) / (2 * gamma2);
s32 r0, r1;
decompose_ref(r, gamma2, &r0, &r1);
if (h == 1 && r0 > 0)
return mod(r1 + 1, m);
if (h == 1 && r0 <= 0)
return mod(r1 - 1, m);
return r1;
}
/*
* Test that for all possible inputs, mldsa_use_hint() gives the same output as
* a mechanical translation of the pseudocode from FIPS 204.
*/
static void test_mldsa_use_hint(struct kunit *test)
{
for (int i = 0; i < 2; i++) {
const s32 gamma2 = (Q - 1) / (i == 0 ? 88 : 32);
for (u8 h = 0; h < 2; h++) {
for (s32 r = 0; r < Q; r++) {
KUNIT_ASSERT_EQ(test,
mldsa_use_hint(h, r, gamma2),
use_hint_ref(h, r, gamma2));
}
}
}
}
static void benchmark_mldsa(struct kunit *test,
const struct mldsa_testvector *tv)
{
const int warmup_niter = 200;
const int benchmark_niter = 200;
u64 t0, t1;
if (!IS_ENABLED(CONFIG_CRYPTO_LIB_BENCHMARK))
kunit_skip(test, "not enabled");
for (int i = 0; i < warmup_niter; i++)
do_mldsa_and_assert_success(test, tv);
t0 = ktime_get_ns();
for (int i = 0; i < benchmark_niter; i++)
do_mldsa_and_assert_success(test, tv);
t1 = ktime_get_ns();
kunit_info(test, "%llu ops/s",
div64_u64((u64)benchmark_niter * NSEC_PER_SEC,
t1 - t0 ?: 1));
}
static void benchmark_mldsa44(struct kunit *test)
{
benchmark_mldsa(test, &mldsa44_testvector);
}
static void benchmark_mldsa65(struct kunit *test)
{
benchmark_mldsa(test, &mldsa65_testvector);
}
static void benchmark_mldsa87(struct kunit *test)
{
benchmark_mldsa(test, &mldsa87_testvector);
}
static struct kunit_case mldsa_kunit_cases[] = {
KUNIT_CASE(test_mldsa44),
KUNIT_CASE(test_mldsa65),
KUNIT_CASE(test_mldsa87),
KUNIT_CASE(test_mldsa_use_hint),
KUNIT_CASE(benchmark_mldsa44),
KUNIT_CASE(benchmark_mldsa65),
KUNIT_CASE(benchmark_mldsa87),
{},
};
static struct kunit_suite mldsa_kunit_suite = {
.name = "mldsa",
.test_cases = mldsa_kunit_cases,
};
kunit_test_suite(mldsa_kunit_suite);
MODULE_DESCRIPTION("KUnit tests and benchmark for ML-DSA");
MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
MODULE_LICENSE("GPL");