Linux cryptographic layer development
 help / color / mirror / Atom feed
* [PATCH 0/5] ML-KEM and X-Wing support
@ 2026-05-25 18:43 Eric Biggers
  2026-05-25 18:43 ` [PATCH 1/5] lib/crypto: mlkem: Add ML-KEM-768 and ML-KEM-1024 support Eric Biggers
                   ` (4 more replies)
  0 siblings, 5 replies; 6+ messages in thread
From: Eric Biggers @ 2026-05-25 18:43 UTC (permalink / raw)
  To: linux-crypto
  Cc: linux-kernel, Ard Biesheuvel, Jason A . Donenfeld, Herbert Xu,
	Ryan Appel, Chris Leech, Eric Biggers

This series applies to v7.1-rc5.  It is a proof-of-concept that won't be
merged until there is an in-kernel user.  Multiple people have been
asking about this though, so I wanted to get ahead of the curve and
provide something that people can experiment with if needed.

This series adds support for "post-quantum" (i.e. quantum-resistant) key
encapsulation to the kernel's crypto library.  Specifically this
includes ML-KEM-768 and ML-KEM-1024, and the X-Wing hybrid KEM built on
top of it.  The ML-KEM functions are put in the CRYPTO_INTERNAL
namespace, as they will be used only as a component of hybrid KEMs.

It's likely this will eventually be useful for at least one of the
in-kernel users of classical key agreement schemes (currently NVMe
authentication, Bluetooth, and WireGuard).  However, the details of the
upgrade to "post-quantum" will be up to the protocol authors in each
case.  I suggest that X-Wing be chosen when possible.

Eric Biggers (5):
  lib/crypto: mlkem: Add ML-KEM-768 and ML-KEM-1024 support
  lib/crypto: mlkem: Add KUnit tests for ML-KEM
  lib/crypto: mlkem: Add FIPS 140-3 tests
  lib/crypto: xwing: Add support for X-Wing KEM
  lib/crypto: xwing: Add KUnit tests for X-Wing KEM

 Documentation/crypto/libcrypto-asymmetric.rst |   27 +
 Documentation/crypto/libcrypto-signature.rst  |   11 -
 Documentation/crypto/libcrypto.rst            |    2 +-
 include/crypto/mlkem.h                        |  159 +++
 include/crypto/xwing.h                        |   84 ++
 lib/crypto/.kunitconfig                       |    2 +
 lib/crypto/Kconfig                            |   17 +
 lib/crypto/Makefile                           |   10 +
 lib/crypto/fips-mlkem.h                       |  523 +++++++++
 lib/crypto/mlkem.c                            | 1036 +++++++++++++++++
 lib/crypto/tests/Kconfig                      |   18 +
 lib/crypto/tests/Makefile                     |    2 +
 lib/crypto/tests/mlkem-testvecs.h             |   19 +
 lib/crypto/tests/mlkem_kunit.c                |  520 +++++++++
 lib/crypto/tests/xwing-testvecs.h             |  138 +++
 lib/crypto/tests/xwing_kunit.c                |  129 ++
 lib/crypto/xwing.c                            |  237 ++++
 scripts/crypto/import-mlkem-testvecs.py       |  179 +++
 scripts/crypto/import-xwing-testvecs.py       |  111 ++
 19 files changed, 3212 insertions(+), 12 deletions(-)
 create mode 100644 Documentation/crypto/libcrypto-asymmetric.rst
 delete mode 100644 Documentation/crypto/libcrypto-signature.rst
 create mode 100644 include/crypto/mlkem.h
 create mode 100644 include/crypto/xwing.h
 create mode 100644 lib/crypto/fips-mlkem.h
 create mode 100644 lib/crypto/mlkem.c
 create mode 100644 lib/crypto/tests/mlkem-testvecs.h
 create mode 100644 lib/crypto/tests/mlkem_kunit.c
 create mode 100644 lib/crypto/tests/xwing-testvecs.h
 create mode 100644 lib/crypto/tests/xwing_kunit.c
 create mode 100644 lib/crypto/xwing.c
 create mode 100755 scripts/crypto/import-mlkem-testvecs.py
 create mode 100755 scripts/crypto/import-xwing-testvecs.py


base-commit: e7ae89a0c97ce2b68b0983cd01eda67cf373517d
-- 
2.54.0


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 1/5] lib/crypto: mlkem: Add ML-KEM-768 and ML-KEM-1024 support
  2026-05-25 18:43 [PATCH 0/5] ML-KEM and X-Wing support Eric Biggers
@ 2026-05-25 18:43 ` Eric Biggers
  2026-05-25 18:44 ` [PATCH 2/5] lib/crypto: mlkem: Add KUnit tests for ML-KEM Eric Biggers
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Eric Biggers @ 2026-05-25 18:43 UTC (permalink / raw)
  To: linux-crypto
  Cc: linux-kernel, Ard Biesheuvel, Jason A . Donenfeld, Herbert Xu,
	Ryan Appel, Chris Leech, Eric Biggers

Add support for ML-KEM-768 and ML-KEM-1024 to lib/crypto/.  This is a
proof-of-concept that won't be merged until there's an in-kernel user.

ML-KEM is a "post-quantum" key encapsulation mechanism that is specified
in FIPS 203 and is based on CRYSTALS-Kyber.  As a key encapsulation
mechanism (KEM), it consists of a set of algorithms that can be used to
establish a shared secret key over a public channel.  As a
"post-quantum" algorithm, it's conjectured to be secure even against
adversaries in possession of a large-scale quantum computer.

Current users of classical key agreement schemes in the kernel include
NVMe in-band authentication, Bluetooth, and WireGuard.  It's likely that
at least some of these will eventually be upgraded to a hybrid KEM that
uses ML-KEM, such as X-Wing which uses X25519 and ML-KEM-768.

This commit just gets things started by implementing ML-KEM-768 and
ML-KEM-1024.  A later commit adds X-Wing support on top of it.

ML-KEM-512 has been omitted, since it has seen limited adoption and
remains somewhat controversial.  It wouldn't be that much work to
support it too, but there's no clear need to do so yet.

Signed-off-by: Eric Biggers <ebiggers@kernel.org>
---
 Documentation/crypto/libcrypto-asymmetric.rst |  20 +
 Documentation/crypto/libcrypto-signature.rst  |  11 -
 Documentation/crypto/libcrypto.rst            |   2 +-
 include/crypto/mlkem.h                        | 151 +++
 lib/crypto/Kconfig                            |   8 +
 lib/crypto/Makefile                           |   5 +
 lib/crypto/mlkem.c                            | 894 ++++++++++++++++++
 7 files changed, 1079 insertions(+), 12 deletions(-)
 create mode 100644 Documentation/crypto/libcrypto-asymmetric.rst
 delete mode 100644 Documentation/crypto/libcrypto-signature.rst
 create mode 100644 include/crypto/mlkem.h
 create mode 100644 lib/crypto/mlkem.c

diff --git a/Documentation/crypto/libcrypto-asymmetric.rst b/Documentation/crypto/libcrypto-asymmetric.rst
new file mode 100644
index 000000000000..6e71c5ce823f
--- /dev/null
+++ b/Documentation/crypto/libcrypto-asymmetric.rst
@@ -0,0 +1,20 @@
+.. SPDX-License-Identifier: GPL-2.0-or-later
+
+Asymmetric cryptography
+=======================
+
+ML-DSA
+------
+
+Support for the ML-DSA digital signature algorithm.
+
+.. kernel-doc:: include/crypto/mldsa.h
+
+ML-KEM
+------
+
+Support for the ML-KEM key encapsulation mechanism.
+
+This shall be used as part of a hybrid scheme such as X-Wing, not by itself.
+
+.. kernel-doc:: include/crypto/mlkem.h
diff --git a/Documentation/crypto/libcrypto-signature.rst b/Documentation/crypto/libcrypto-signature.rst
deleted file mode 100644
index e80d59fa51b6..000000000000
--- a/Documentation/crypto/libcrypto-signature.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-.. SPDX-License-Identifier: GPL-2.0-or-later
-
-Digital signature algorithms
-============================
-
-ML-DSA
-------
-
-Support for the ML-DSA digital signature algorithm.
-
-.. kernel-doc:: include/crypto/mldsa.h
diff --git a/Documentation/crypto/libcrypto.rst b/Documentation/crypto/libcrypto.rst
index a1557d45b0e5..8c61b6513e06 100644
--- a/Documentation/crypto/libcrypto.rst
+++ b/Documentation/crypto/libcrypto.rst
@@ -156,10 +156,10 @@ API documentation
 =================
 
 .. toctree::
    :maxdepth: 2
 
+   libcrypto-asymmetric
    libcrypto-blockcipher
    libcrypto-hash
-   libcrypto-signature
    libcrypto-utils
    sha3
diff --git a/include/crypto/mlkem.h b/include/crypto/mlkem.h
new file mode 100644
index 000000000000..e33d065c5442
--- /dev/null
+++ b/include/crypto/mlkem.h
@@ -0,0 +1,151 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright 2026 Google LLC
+ */
+
+/**
+ * DOC: ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism)
+ *
+ * This is an implementation of ML-KEM, a "post-quantum" key encapsulation
+ * mechanism that is specified in FIPS 203 and is based on CRYSTALS-Kyber.
+ *
+ * Specifically, the ML-KEM-768 and ML-KEM-1024 parameter sets are supported.
+ *
+ * This shall be used as part of a hybrid scheme such as X-Wing, not by itself.
+ *
+ * This implementation is designed to be constant-time, compact, and
+ * memory-efficient, and to reuse the kernel's SHA-3 routines.  For simplicity,
+ * it stores integers mod Q as their standard representatives in the interval
+ * [0, Q - 1] across function boundaries.  (This makes it more similar to e.g.
+ * BoringSSL than to the Kyber reference code, which uses a slightly more
+ * optimized but harder-to-understand approach.)
+ */
+
+#ifndef _CRYPTO_MLKEM_H
+#define _CRYPTO_MLKEM_H
+
+#include <linux/types.h>
+
+#define MLKEM_SEED_BYTES 64 /* Length of seed for KeyGen */
+#define MLKEM_ESEED_BYTES 32 /* Length of seed for Encaps */
+#define MLKEM_SHARED_SECRET_BYTES 32
+
+#define MLKEM768_PUBLIC_KEY_BYTES 1184
+#define MLKEM768_SECRET_KEY_BYTES 2400
+#define MLKEM768_CIPHERTEXT_BYTES 1088
+
+/**
+ * mlkem768_keygen() - Generate an ML-KEM-768 key pair
+ * @pk: (output) The public key (encapsulation key)
+ * @sk: (output) The secret key (decapsulation key)
+ *
+ * Context: Might sleep
+ *
+ * Return: 0 on success, or -ENOMEM if out of memory.
+ */
+int mlkem768_keygen(u8 pk[MLKEM768_PUBLIC_KEY_BYTES],
+		    u8 sk[MLKEM768_SECRET_KEY_BYTES]);
+
+/**
+ * mlkem768_encaps() - Generate and encapsulate shared secret with ML-KEM-768
+ * @ct: (output) The ciphertext
+ * @ss: (output) The generated shared secret
+ * @pk: The public key (encapsulation key)
+ *
+ * Context: Might sleep
+ *
+ * Return:
+ * * 0 on success
+ * * -EBADMSG if the public key is malformed
+ * * -ENOMEM if out of memory
+ */
+int mlkem768_encaps(u8 ct[MLKEM768_CIPHERTEXT_BYTES],
+		    u8 ss[MLKEM_SHARED_SECRET_BYTES],
+		    const u8 pk[MLKEM768_PUBLIC_KEY_BYTES]);
+
+/**
+ * mlkem768_decaps() - Decapsulate shared secret with ML-KEM-768
+ * @ss: (output) The decapsulated shared secret
+ * @ct: The ciphertext
+ * @sk: The secret key (decapsulation key)
+ *
+ * Context: Might sleep
+ *
+ * Return:
+ * * 0 on success, including the "implicit rejection" case where the ciphertext
+ *   is invalid and a randomized shared secret is returned
+ * * -EBADMSG if the secret key is malformed
+ * * -ENOMEM if out of memory
+ */
+int mlkem768_decaps(u8 ss[MLKEM_SHARED_SECRET_BYTES],
+		    const u8 ct[MLKEM768_CIPHERTEXT_BYTES],
+		    const u8 sk[MLKEM768_SECRET_KEY_BYTES]);
+
+#define MLKEM1024_PUBLIC_KEY_BYTES 1568
+#define MLKEM1024_SECRET_KEY_BYTES 3168
+#define MLKEM1024_CIPHERTEXT_BYTES 1568
+
+/**
+ * mlkem1024_keygen() - Generate an ML-KEM-1024 key pair
+ * @pk: (output) The public key (encapsulation key)
+ * @sk: (output) The secret key (decapsulation key)
+ *
+ * Context: Might sleep
+ *
+ * Return: 0 on success, or -ENOMEM if out of memory.
+ */
+int mlkem1024_keygen(u8 pk[MLKEM1024_PUBLIC_KEY_BYTES],
+		     u8 sk[MLKEM1024_SECRET_KEY_BYTES]);
+
+/**
+ * mlkem1024_encaps() - Generate and encapsulate shared secret with ML-KEM-1024
+ * @ct: (output) The ciphertext
+ * @ss: (output) The generated shared secret
+ * @pk: The public key (encapsulation key)
+ *
+ * Context: Might sleep
+ *
+ * Return:
+ * * 0 on success
+ * * -EBADMSG if the public key is malformed
+ * * -ENOMEM if out of memory
+ */
+int mlkem1024_encaps(u8 ct[MLKEM1024_CIPHERTEXT_BYTES],
+		     u8 ss[MLKEM_SHARED_SECRET_BYTES],
+		     const u8 pk[MLKEM1024_PUBLIC_KEY_BYTES]);
+
+/**
+ * mlkem1024_decaps() - Decapsulate shared secret with ML-KEM-1024
+ * @ss: (output) The decapsulated shared secret
+ * @ct: The ciphertext
+ * @sk: The secret key (decapsulation key)
+ *
+ * Context: Might sleep
+ *
+ * Return:
+ * * 0 on success, including the "implicit rejection" case where the ciphertext
+ *   is invalid and a randomized shared secret is returned
+ * * -EBADMSG if the secret key is malformed
+ * * -ENOMEM if out of memory
+ */
+int mlkem1024_decaps(u8 ss[MLKEM_SHARED_SECRET_BYTES],
+		     const u8 ct[MLKEM1024_CIPHERTEXT_BYTES],
+		     const u8 sk[MLKEM1024_SECRET_KEY_BYTES]);
+
+/* Functions taking explicit seeds, only for KUnit testing and hybrid KEMs */
+int mlkem768_keygen_internal(u8 pk[MLKEM768_PUBLIC_KEY_BYTES],
+			     u8 sk[MLKEM768_SECRET_KEY_BYTES],
+			     const u8 seed[MLKEM_SEED_BYTES]);
+int mlkem768_encaps_internal(u8 ct[MLKEM768_CIPHERTEXT_BYTES],
+			     u8 ss[MLKEM_SHARED_SECRET_BYTES],
+			     const u8 pk[MLKEM768_PUBLIC_KEY_BYTES],
+			     const u8 eseed[MLKEM_ESEED_BYTES]);
+int mlkem1024_keygen_internal(u8 pk[MLKEM1024_PUBLIC_KEY_BYTES],
+			      u8 sk[MLKEM1024_SECRET_KEY_BYTES],
+			      const u8 seed[MLKEM_SEED_BYTES]);
+int mlkem1024_encaps_internal(u8 ct[MLKEM1024_CIPHERTEXT_BYTES],
+			      u8 ss[MLKEM_SHARED_SECRET_BYTES],
+			      const u8 pk[MLKEM1024_PUBLIC_KEY_BYTES],
+			      const u8 eseed[MLKEM_ESEED_BYTES]);
+
+#endif /* _CRYPTO_MLKEM_H */
diff --git a/lib/crypto/Kconfig b/lib/crypto/Kconfig
index d3904b72dae7..acaa64af4c6d 100644
--- a/lib/crypto/Kconfig
+++ b/lib/crypto/Kconfig
@@ -141,10 +141,18 @@ config CRYPTO_LIB_MLDSA
 	select CRYPTO_LIB_SHA3
 	help
 	  The ML-DSA library functions.  Select this if your module uses any of
 	  the functions from <crypto/mldsa.h>.
 
+config CRYPTO_LIB_MLKEM
+	tristate
+	select CRYPTO_LIB_SHA3
+	select CRYPTO_LIB_UTILS
+	help
+	  The ML-KEM library functions.  Select this if your module uses any of
+	  the functions from <crypto/mlkem.h>.
+
 config CRYPTO_LIB_NH
 	tristate
 	help
 	  Implementation of the NH almost-universal hash function, specifically
 	  the variant of NH used in Adiantum.
diff --git a/lib/crypto/Makefile b/lib/crypto/Makefile
index 4ad91f390038..94cef4e574cd 100644
--- a/lib/crypto/Makefile
+++ b/lib/crypto/Makefile
@@ -197,10 +197,15 @@ endif # CONFIG_CRYPTO_LIB_MD5_ARCH
 obj-$(CONFIG_CRYPTO_LIB_MLDSA) += libmldsa.o
 libmldsa-y := mldsa.o
 
 ################################################################################
 
+obj-$(CONFIG_CRYPTO_LIB_MLKEM) += libmlkem.o
+libmlkem-y := mlkem.o
+
+################################################################################
+
 obj-$(CONFIG_CRYPTO_LIB_NH) += libnh.o
 libnh-y := nh.o
 ifeq ($(CONFIG_CRYPTO_LIB_NH_ARCH),y)
 CFLAGS_nh.o += -I$(src)/$(SRCARCH)
 libnh-$(CONFIG_ARM) += arm/nh-neon-core.o
diff --git a/lib/crypto/mlkem.c b/lib/crypto/mlkem.c
new file mode 100644
index 000000000000..b800ecb49f24
--- /dev/null
+++ b/lib/crypto/mlkem.c
@@ -0,0 +1,894 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism)
+ *
+ * See include/crypto/mlkem.h for the documentation.
+ *
+ * Copyright 2026 Google LLC
+ */
+
+#include <crypto/mlkem.h>
+#include <crypto/sha3.h>
+#include <crypto/utils.h>
+#include <linux/export.h>
+#include <linux/module.h>
+#include <linux/random.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/unaligned.h>
+
+#define Q 3329 /* The prime q = 2^8 * 13 + 1 */
+#define N 256 /* Number of coefficients per ring element */
+#define MAX_K 4 /* Max matrix dimension (k parameter) for any parameter set */
+#define MAX_CIPHERTEXT_BYTES 1568 /* Max ciphertext length for any param set */
+#define ETA 2 /* Value of eta_1 and eta_2 for ML-KEM-768 and ML-KEM-1024 */
+
+/*
+ * This array contains zeta^BitRev_7(i) for 0 <= i < 128.
+ * Generated by the following Python code:
+ * [pow(17, int(f'{i:07b}'[::-1], 2), 3329) for i in range(128)]
+ * Also matches the first table in FIPS 203 Appendix A.
+ */
+static const u16 zetas[128] = {
+	1,    1729, 2580, 3289, 2642, 630,  1897, 848,	1062, 1919, 193,  797,
+	2786, 3260, 569,  1746, 296,  2447, 1339, 1476, 3046, 56,   2240, 1333,
+	1426, 2094, 535,  2882, 2393, 2879, 1974, 821,	289,  331,  3253, 1756,
+	1197, 2304, 2277, 2055, 650,  1977, 2513, 632,	2865, 33,   1320, 1915,
+	2319, 1435, 807,  452,	1438, 2868, 1534, 2402, 2647, 2617, 1481, 648,
+	2474, 3110, 1227, 910,	17,   2761, 583,  2649, 1637, 723,  2288, 1100,
+	1409, 2662, 3281, 233,	756,  2156, 3015, 3050, 1703, 1651, 2789, 1789,
+	1847, 952,  1461, 2687, 939,  2308, 2437, 2388, 733,  2337, 268,  641,
+	1584, 2298, 2037, 3220, 375,  2549, 2090, 1645, 1063, 319,  2773, 757,
+	2099, 561,  2466, 2594, 2804, 1092, 403,  1026, 1143, 2150, 2775, 886,
+	1722, 1212, 1874, 1029, 2110, 2935, 885,  2154,
+};
+
+/*
+ * This array contains zeta^(2*BitRev_7(i)+1) for 0 <= i < 128.
+ * Generated by the following Python code:
+ * [pow(17, 2*int(f'{i:07b}'[::-1], 2)+1, 3329) for i in range(128)]
+ * Also matches the second table in FIPS 203 Appendix A, with the values
+ * canonicalized to their standard representatives in [0, Q - 1].
+ */
+static const u16 gammas[128] = {
+	17,   3312, 2761, 568,	583,  2746, 2649, 680,	1637, 1692, 723,  2606,
+	2288, 1041, 1100, 2229, 1409, 1920, 2662, 667,	3281, 48,   233,  3096,
+	756,  2573, 2156, 1173, 3015, 314,  3050, 279,	1703, 1626, 1651, 1678,
+	2789, 540,  1789, 1540, 1847, 1482, 952,  2377, 1461, 1868, 2687, 642,
+	939,  2390, 2308, 1021, 2437, 892,  2388, 941,	733,  2596, 2337, 992,
+	268,  3061, 641,  2688, 1584, 1745, 2298, 1031, 2037, 1292, 3220, 109,
+	375,  2954, 2549, 780,	2090, 1239, 1645, 1684, 1063, 2266, 319,  3010,
+	2773, 556,  757,  2572, 2099, 1230, 561,  2768, 2466, 863,  2594, 735,
+	2804, 525,  1092, 2237, 403,  2926, 1026, 2303, 1143, 2186, 2150, 1179,
+	2775, 554,  886,  2443, 1722, 1607, 1212, 2117, 1874, 1455, 1029, 2300,
+	2110, 1219, 2935, 394,	885,  2444, 2154, 1175
+};
+
+struct mlkem_parameter_set {
+	u8 k; /* Rank of the module (number of polynomials in vectors) */
+	u8 du; /* Compression parameter for vector u */
+	u8 dv; /* Compression parameter for polynomial v */
+	u16 pk_len; /* Length of public keys in bytes */
+	u16 sk_len; /* Length of secret keys in bytes */
+	u16 ct_len; /* Length of ciphertexts in bytes */
+};
+
+/* ML-KEM-768 parameters.  Reference: FIPS 203 Section 8, "Parameter Sets" */
+static const struct mlkem_parameter_set mlkem768 = {
+	.k = 3,
+	.du = 10,
+	.dv = 4,
+	.pk_len = MLKEM768_PUBLIC_KEY_BYTES,
+	.sk_len = MLKEM768_SECRET_KEY_BYTES,
+	.ct_len = MLKEM768_CIPHERTEXT_BYTES,
+};
+
+/* ML-KEM-1024 parameters.  Reference: FIPS 203 Section 8, "Parameter Sets" */
+static const struct mlkem_parameter_set mlkem1024 = {
+	.k = 4,
+	.du = 11,
+	.dv = 5,
+	.pk_len = MLKEM1024_PUBLIC_KEY_BYTES,
+	.sk_len = MLKEM1024_SECRET_KEY_BYTES,
+	.ct_len = MLKEM1024_CIPHERTEXT_BYTES,
+};
+
+/*
+ * An element of the ring R_q (normal form) or the ring T_q (NTT form).  In both
+ * cases it consists of N integers in the interval [0, Q - 1].  In R_q, these
+ * are the coefficients of one polynomial of maximum degree N - 1.  In T_q,
+ * these are the coefficients of N / 2 polynomials of maximum degree 1.
+ */
+struct mlkem_ring_elem {
+	u16 x[N];
+};
+
+struct k_pke_keygen_workspace {
+	union { /* These aren't used at the same time, so they can overlap. */
+		struct shake_ctx shake;
+		struct sha3_ctx sha3;
+	};
+	union {
+		struct {
+			u8 rho[32];
+			u8 sigma[32];
+		};
+		u8 rho_and_sigma[64];
+	};
+	u8 block[MAX(64 * ETA, SHAKE128_BLOCK_SIZE)];
+	struct mlkem_ring_elem tmp, t;
+	struct mlkem_ring_elem s[MAX_K];
+};
+
+struct k_pke_encrypt_workspace {
+	struct shake_ctx shake;
+	u8 block[MAX(64 * ETA, SHAKE128_BLOCK_SIZE)];
+	struct mlkem_ring_elem tmp, y[MAX_K];
+	union { /* These aren't used at the same time, so they can overlap. */
+		struct mlkem_ring_elem u, v;
+	};
+};
+
+struct k_pke_decrypt_workspace {
+	struct mlkem_ring_elem su, s, tmp;
+};
+
+struct mlkem_encap_workspace {
+	union { /* These aren't used at the same time, so they can overlap. */
+		struct sha3_ctx sha3;
+		struct k_pke_encrypt_workspace k_pke_enc;
+	};
+	union {
+		struct {
+			u8 K[32];
+			u8 r[32];
+		};
+		u8 K_and_r[64];
+	};
+	u8 pk_hash[SHA3_256_DIGEST_SIZE];
+};
+
+struct mlkem_decap_workspace {
+	union { /* These aren't used at the same time, so they can overlap. */
+		struct shake_ctx shake;
+		struct sha3_ctx sha3;
+		struct k_pke_encrypt_workspace k_pke_enc;
+		struct k_pke_decrypt_workspace k_pke_dec;
+	};
+	union {
+		struct {
+			u8 K_prime[32];
+			u8 r_prime[32];
+		};
+		u8 K_prime_and_r_prime[64];
+	};
+	u8 h[SHA3_256_DIGEST_SIZE];
+	u8 m_prime[32];
+	u8 K_bar[32];
+	u8 ct_prime[MAX_CIPHERTEXT_BYTES];
+};
+
+/* Reduce @x to its standard representative mod Q, where 0 <= @x < 2*Q */
+static u16 reduce_once(u16 x)
+{
+	u16 x_minus_q, mask;
+
+	x_minus_q = x - Q; /* This wraps around, setting bit 15, if @x < Q */
+	OPTIMIZER_HIDE_VAR(x_minus_q);
+	mask = (s16)x_minus_q >> 15; /* mask = 0xffff if @x < Q, else 0 */
+	OPTIMIZER_HIDE_VAR(mask);
+
+	return (mask & x) | (~mask & x_minus_q);
+}
+
+/* Reduce @x to its standard representative mod Q, where 0 <= @x < Q + 2*Q*Q */
+static u16 reduce(u32 x)
+{
+	/* Barrett reduction: x - floor((x * floor(2^24 / Q)) / 2^24) * Q */
+	u64 product = (u64)x * 5039;
+	u32 quotient = (u32)(product >> 24);
+	u32 remainder = x - quotient * Q; /* 0 <= remainder < 2*Q */
+
+	return reduce_once(remainder);
+}
+
+/*
+ * Convert @f to its number-theoretically-transformed representation in-place.
+ * Reference: FIPS 203 Algorithm 9, NTT
+ */
+static void ntt(struct mlkem_ring_elem *f)
+{
+	int i = 1; /* index in zetas */
+
+	for (int len = 128; len >= 2; len /= 2) {
+		for (int start = 0; start < 256; start += 2 * len) {
+			u32 zeta = zetas[i++];
+
+			for (int j = start; j < start + len; j++) {
+				u16 t = reduce(zeta * f->x[j + len]);
+
+				f->x[j + len] = reduce_once(Q + f->x[j] - t);
+				f->x[j] = reduce_once(f->x[j] + t);
+			}
+		}
+	}
+}
+
+/*
+ * Convert @f from its number-theoretically-transformed representation in-place.
+ *
+ * Reference: FIPS 203 Algorithm 10, NTT^-1
+ */
+static void invntt(struct mlkem_ring_elem *f)
+{
+	int i = 127; /* index in zetas */
+
+	for (int len = 2; len <= 128; len *= 2) {
+		for (int start = 0; start < 256; start += 2 * len) {
+			u32 zeta = zetas[i--];
+
+			for (int j = start; j < start + len; j++) {
+				u16 t = f->x[j];
+
+				f->x[j] = reduce_once(t + f->x[j + len]);
+				f->x[j + len] =
+					reduce(zeta * (Q + f->x[j + len] - t));
+			}
+		}
+	}
+
+	/* Multiply by 128^-1 = 3303 mod Q. */
+	for (int j = 0; j < 256; j++)
+		f->x[j] = reduce((u32)f->x[j] * 3303);
+}
+
+/* Compute @a += @b, where @a and @b are both elements of either R_q or T_q. */
+static void poly_add(struct mlkem_ring_elem *a, const struct mlkem_ring_elem *b)
+{
+	for (int i = 0; i < N; i++)
+		a->x[i] = reduce_once(a->x[i] + b->x[i]);
+}
+
+/* Compute @a -= @b, where @a and @b are both elements of either R_q or T_q. */
+static void poly_sub(struct mlkem_ring_elem *a, const struct mlkem_ring_elem *b)
+{
+	for (int i = 0; i < N; i++)
+		a->x[i] = reduce_once(Q + a->x[i] - b->x[i]);
+}
+
+/*
+ * Compute @h += @f * @g in the ring T_q.
+ *
+ * Reference: FIPS 203 Algorithm 11, MultiplyNTTs
+ */
+static void poly_mul_acc(struct mlkem_ring_elem *h,
+			 const struct mlkem_ring_elem *f,
+			 const struct mlkem_ring_elem *g)
+{
+	for (int i = 0; i < N / 2; i++) {
+		u32 a0 = f->x[2 * i];
+		u32 a1 = f->x[2 * i + 1];
+		u32 b0 = g->x[2 * i];
+		u32 b1 = g->x[2 * i + 1];
+		u16 c0 = reduce(a0 * b0 + reduce(a1 * b1) * (u32)gammas[i]);
+		u16 c1 = reduce(a0 * b1 + a1 * b0);
+
+		h->x[2 * i] = reduce_once(h->x[2 * i] + c0);
+		h->x[2 * i + 1] = reduce_once(h->x[2 * i + 1] + c1);
+	}
+}
+
+/*
+ * "Compress" @x in the interval [0, Q - 1] into the smaller interval [0, 2^d
+ * - 1] for the given 1 <= @d <= 11 by computing:
+ *
+ *	round((2^d / Q) * x) mod 2^d
+ *    = floor((x * 2^d + floor(Q / 2)) / Q) mod 2^d
+ *
+ * Reference: FIPS 203 Section 4.2.1, "Conversion and Compression Algorithms"
+ */
+static u16 compress_d(u16 x, int d)
+{
+	u32 t = ((u32)x << d) + (Q / 2);
+
+	/*
+	 * t = floor(t / Q).  Avoid potentially variable-time division by using
+	 * the equivalent (for the input ranges) reciprocal multiplication
+	 * floor((t * ceil(2^32 / Q)) / 2^32).  2^32 is chosen because it's
+	 * efficient and provides enough precision for all allowed inputs.
+	 */
+	OPTIMIZER_HIDE_VAR(t);
+	t = ((u64)t * 1290168) >> 32;
+	OPTIMIZER_HIDE_VAR(t);
+
+	return t & ((1 << d) - 1);
+}
+
+/*
+ * "Decompress" @y in the interval [0, 2^d - 1] into the larger interval [0, Q
+ * - 1] for the given 1 <= @d <= 11 by computing:
+ *
+ *	round((Q / 2^d) * y)
+ *    = floor((y * Q + 2^(d-1)) / 2^d)
+ *
+ * Reference: FIPS 203 Section 4.2.1, "Conversion and Compression Algorithms"
+ */
+static u16 decompress_d(u16 y, int d)
+{
+	u32 t;
+
+	OPTIMIZER_HIDE_VAR(y);
+	t = (u32)y * Q;
+	OPTIMIZER_HIDE_VAR(t);
+	return (t + (1 << (d - 1))) >> d;
+}
+
+/*
+ * Encode the ring element @in into 32 * @d bytes at @out.
+ *
+ * Reference: FIPS 203 Algorithm 5, ByteEncode_d
+ */
+static void byte_encode(u8 *out, const struct mlkem_ring_elem *in, int d)
+{
+	u32 acc = 0;
+	int bits = 0;
+
+	for (int i = 0; i < 256; i++) {
+		acc |= (u32)in->x[i] << bits;
+		bits += d;
+		for (; bits >= 8; bits -= 8, acc >>= 8)
+			*out++ = (u8)acc;
+	}
+}
+
+/*
+ * Decode the 32 * @d bytes at @in into a ring element @out with coefficients in
+ * the interval [0, 2^d - 1].  Note that when @d >= 12, decoded coefficients can
+ * be out of range (i.e. >= Q) and need to be validated afterwards.
+ *
+ * Reference: FIPS 203 Algorithm 6, ByteDecode_d
+ */
+static void byte_decode(struct mlkem_ring_elem *out, const u8 *in, int d)
+{
+	const u32 mask = (1 << d) - 1;
+	u32 acc = 0;
+	int bits = 0;
+
+	for (int i = 0; i < 256; i++) {
+		for (; bits < d; bits += 8)
+			acc |= (u32)(*in++) << bits;
+		out->x[i] = acc & mask;
+		acc >>= d;
+		bits -= d;
+	}
+}
+
+static void compress_and_encode(u8 *out, struct mlkem_ring_elem *f, int d)
+{
+	for (int i = 0; i < N; i++)
+		f->x[i] = compress_d(f->x[i], d);
+	byte_encode(out, f, d);
+}
+
+static void decode_and_decompress(struct mlkem_ring_elem *out, const u8 *in,
+				  int d)
+{
+	byte_decode(out, in, d);
+	for (int i = 0; i < N; i++)
+		out->x[i] = decompress_d(out->x[i], d);
+}
+
+/*
+ * Check in constant time whether any coefficients in @f are outside the
+ * interval [0, Q - 1].  This is needed after calling byte_decode() with d=12.
+ */
+static bool is_out_of_range(const struct mlkem_ring_elem *f)
+{
+	u32 bad = 0;
+
+	for (int i = 0; i < N; i++) {
+		OPTIMIZER_HIDE_VAR(bad);
+		bad |= Q - 1 - f->x[i]; /* Set bit 31 if f->x[i] >= Q. */
+	}
+	OPTIMIZER_HIDE_VAR(bad);
+	return bad >> 31;
+}
+
+/*
+ * Expand the 33 input bytes (@s, @b) into a random polynomial @out with
+ * coefficients in [-ETA, ETA] using a Centered Binomial
+ * Distribution (CBD).  @shake and @block are used as temporary space.
+ *
+ * This is FIPS 203 Algorithm 8 "SamplePolyCBD_eta", composed with PRF_eta.
+ * (SamplePolyCBD_eta is invoked only with the output of PRF_eta.)
+ */
+static void sample_poly_cbd(struct mlkem_ring_elem *out, const u8 s[32], u8 b,
+			    struct shake_ctx *shake,
+			    u8 block[at_least 64 * ETA])
+{
+	/* Expand (s, b) into '64 * ETA' bytes. */
+	shake256_init(shake);
+	shake_update(shake, s, 32);
+	shake_update(shake, &b, 1);
+	shake_squeeze(shake, block, 64 * ETA);
+
+	/*
+	 * Generate the coefficients by counting the number of 1 bits in each
+	 * ETA-bit group, then subtracting the counts of adjacent pairs.
+	 */
+	static_assert(ETA == 2);
+	for (int i = 0; i < 256; i += 16) {
+		const u64 mask = 0x5555555555555555;
+		u64 t = get_unaligned_le64(&block[i / 2]);
+		u64 popcounts = (t & mask) + ((t >> 1) & mask);
+
+		for (int j = 0; j < 16; j++) {
+			u16 x = (popcounts >> (4 * j)) & 0x3;
+			u16 y = (popcounts >> (4 * j + 2)) & 0x3;
+
+			out->x[i + j] = reduce_once(Q + x - y);
+		}
+	}
+}
+
+/*
+ * Generate the element of the matrix NTT(A) at @row and @column from the seed
+ * @rho.  The output, which is an element of the ring T_q, is written to @out.
+ * @shake and @block are used as temporary space.
+ *
+ * Reference: FIPS 203 Algorithm 7, SampleNTT
+ */
+static void sample_ntt(struct mlkem_ring_elem *out, const u8 rho[32], u8 row,
+		       u8 column, struct shake_ctx *shake,
+		       u8 block[at_least SHAKE128_BLOCK_SIZE])
+{
+	shake128_init(shake);
+	shake_update(shake, rho, 32);
+	shake_update(shake, &column, 1);
+	shake_update(shake, &row, 1);
+
+	for (int i = 0; i < 256;) {
+		/*
+		 * Squeeze the next block, then use rejection sampling to
+		 * generate up to two coefficients from each 3-byte group.
+		 */
+		static_assert(SHAKE128_BLOCK_SIZE % 3 == 0);
+		shake_squeeze(shake, block, SHAKE128_BLOCK_SIZE);
+
+		for (int j = 0; j < SHAKE128_BLOCK_SIZE && i < 256; j += 3) {
+			u16 d1 = block[j] | ((block[j + 1] & 0xf) << 8);
+			u16 d2 = (block[j + 1] >> 4) | (block[j + 2] << 4);
+
+			if (d1 < Q)
+				out->x[i++] = d1;
+			if (i < N && d2 < Q)
+				out->x[i++] = d2;
+		}
+	}
+}
+
+/*
+ * Generate a K-PKE key pair (@pk, @sk) from the random seed @d.  @pk is 384*k +
+ * 32 bytes, and @sk is 384*k bytes.
+ *
+ * Reference: FIPS 203 Algorithm 13, K-PKE.KeyGen
+ */
+static int k_pke_keygen(u8 *pk, u8 *sk, const u8 d[32],
+			const struct mlkem_parameter_set *params)
+{
+	struct k_pke_keygen_workspace *ws __free(kfree_sensitive) =
+		kmalloc_obj(*ws);
+	const u8 k = params->k;
+
+	if (!ws)
+		return -ENOMEM;
+
+	/* rho || sigma = G(d || k) */
+	sha3_512_init(&ws->sha3);
+	sha3_update(&ws->sha3, d, 32);
+	sha3_update(&ws->sha3, &k, 1);
+	sha3_final(&ws->sha3, ws->rho_and_sigma);
+
+	/* Generate and encode the secret key NTT(s). */
+	for (int i = 0; i < k; i++) {
+		sample_poly_cbd(&ws->s[i], ws->sigma, i, &ws->shake, ws->block);
+		ntt(&ws->s[i]);
+		byte_encode(&sk[384 * i], &ws->s[i], 12);
+	}
+
+	/*
+	 * Generate and encode the public key pk = NTT(t) || rho.
+	 * To save memory, do it one row at a time.
+	 */
+	for (int i = 0; i < k; i++) { /* For each row in A */
+		/* NTT(t) = NTT(A) * NTT(s) + NTT(e) */
+		ws->t = (struct mlkem_ring_elem){};
+		for (int j = 0; j < k; j++) { /* For each column in A */
+			sample_ntt(&ws->tmp, ws->rho, i, j, &ws->shake,
+				   ws->block);
+			poly_mul_acc(&ws->t, &ws->tmp, &ws->s[j]);
+		}
+		sample_poly_cbd(&ws->tmp, ws->sigma, k + i, &ws->shake,
+				ws->block);
+		ntt(&ws->tmp);
+		poly_add(&ws->t, &ws->tmp);
+		byte_encode(&pk[384 * i], &ws->t, 12);
+	}
+	memcpy(&pk[384 * k], ws->rho, 32);
+	return 0;
+}
+
+/*
+ * Encrypt the message @m using the public key @pk and randomness @r.
+ *
+ * Reference: FIPS 203 Algorithm 14, K-PKE.Encrypt
+ */
+static int k_pke_encrypt(u8 *ct, const u8 *pk, const u8 m[32], const u8 r[32],
+			 struct k_pke_encrypt_workspace *ws,
+			 const struct mlkem_parameter_set *params)
+{
+	const u8 k = params->k;
+	const u8 *rho = &pk[384 * k];
+
+	/* Generate the vector NTT(y) from r. */
+	for (int i = 0; i < k; i++) {
+		sample_poly_cbd(&ws->y[i], r, i, &ws->shake, ws->block);
+		ntt(&ws->y[i]);
+	}
+
+	/*
+	 * Compute, compress, and encode u = NTT^-1( NTT(A^T) * NTT(y) ) + e_1.
+	 * To save memory, do it one row at a time.
+	 */
+	for (int i = 0; i < k; i++) { /* For each row in A^T (column in A) */
+		/* u = NTT(A^T) * NTT(y) */
+		ws->u = (struct mlkem_ring_elem){};
+		for (int j = 0; j < k;
+		     j++) { /* For each column in A^T (row in A) */
+			sample_ntt(&ws->tmp, rho, j, i, &ws->shake, ws->block);
+			poly_mul_acc(&ws->u, &ws->tmp, &ws->y[j]);
+		}
+		/* u = NTT^-1(u) */
+		invntt(&ws->u);
+
+		/* u += e_1, where e_1 is generated from r */
+		sample_poly_cbd(&ws->tmp, r, k + i, &ws->shake, ws->block);
+		poly_add(&ws->u, &ws->tmp);
+
+		/* Compress and encode u into the ciphertext ct. */
+		compress_and_encode(&ct[32 * params->du * i], &ws->u,
+				    params->du);
+	}
+
+	/* v = NTT^-1( NTT(t^T) * NTT(y) ) */
+	ws->v = (struct mlkem_ring_elem){};
+	for (int i = 0; i < k; i++) {
+		/* Decode next element of NTT(t) from pk. */
+		byte_decode(&ws->tmp, &pk[384 * i], 12);
+		if (is_out_of_range(&ws->tmp)) {
+			memzero_explicit(ct, params->ct_len);
+			return -EBADMSG; /* pk is malformed. */
+		}
+		poly_mul_acc(&ws->v, &ws->tmp, &ws->y[i]);
+	}
+	invntt(&ws->v);
+
+	/* v += e_2, where e_2 is generated from r */
+	sample_poly_cbd(&ws->tmp, r, 2 * k, &ws->shake, ws->block);
+	poly_add(&ws->v, &ws->tmp);
+
+	/* Add the message m, after decompressing from 1 bit to [0, Q - 1]. */
+	for (int i = 0; i < N; i++)
+		ws->v.x[i] =
+			reduce_once(ws->v.x[i] +
+				    decompress_d((m[i / 8] >> (i & 7)) & 1, 1));
+
+	/* Compress and encode v into the ciphertext ct. */
+	compress_and_encode(&ct[32 * params->du * k], &ws->v, params->dv);
+	return 0;
+}
+
+/*
+ * Decrypt the ciphertext @ct using the secret key @sk.
+ *
+ * Reference: FIPS 203 Algorithm 15, K-PKE.Decrypt
+ */
+static int k_pke_decrypt(u8 m[32], const u8 *sk, const u8 *ct,
+			 struct k_pke_decrypt_workspace *ws,
+			 const struct mlkem_parameter_set *params)
+{
+	/* Compute s^T * u' */
+	ws->su = (struct mlkem_ring_elem){};
+	for (int i = 0; i < params->k; i++) {
+		/* Decode next element of NTT(s) from sk. */
+		byte_decode(&ws->s, &sk[384 * i], 12);
+		if (is_out_of_range(&ws->s))
+			return -EBADMSG; /* sk is malformed. */
+
+		/* Decode and decompress next element of u' from ct. */
+		decode_and_decompress(&ws->tmp, &ct[32 * params->du * i],
+				      params->du);
+		/* NTT(u') [one element] */
+		ntt(&ws->tmp);
+
+		/* NTT(s^T) * NTT(u') [one element] */
+		poly_mul_acc(&ws->su, &ws->s, &ws->tmp);
+	}
+	invntt(&ws->su);
+
+	/* Decode and decompress v' from the ciphertext ct. */
+	decode_and_decompress(&ws->tmp, &ct[32 * params->du * params->k],
+			      params->dv);
+
+	/* w = v' - s^T * u' */
+	poly_sub(&ws->tmp, &ws->su);
+
+	/* Decode the plaintext m from the polynomial w. */
+	compress_and_encode(m, &ws->tmp, 1);
+	return 0;
+}
+
+/*
+ * Generate an ML-KEM key pair (@pk, @sk) from the random seed @seed.
+ * The lengths of @pk and @sk are determined by the chosen @params.
+ *
+ * Reference: FIPS 203 Algorithm 16, ML-KEM.KeyGen_internal.  Formally it takes
+ * two 32-byte seeds 'd' and 'z'; here @seed is d || z.
+ */
+static int mlkem_keygen_internal(u8 *pk, u8 *sk,
+				 const u8 seed[MLKEM_SEED_BYTES],
+				 const struct mlkem_parameter_set *params)
+{
+	u8 *sk_ptr;
+	int err;
+
+	/* Generate a key pair for the public key encryption scheme K-PKE. */
+	err = k_pke_keygen(pk, sk, &seed[0], params);
+	if (err)
+		return err;
+
+	/*
+	 * Generate the ML-KEM key pair as
+	 * Public key (encapsulation key): pk = pk_pke
+	 * Secret key (decapsulation key): sk = sk_pke || pk || H(pk) || z
+	 *
+	 * For pk there's nothing to add.  This just handles the sk.
+	 */
+	sk_ptr = &sk[384 * params->k]; /* end of sk_pke */
+	memcpy(sk_ptr, pk, params->pk_len);
+	sk_ptr += params->pk_len;
+	sha3_256(pk, params->pk_len, sk_ptr);
+	sk_ptr += SHA3_256_DIGEST_SIZE;
+	memcpy(sk_ptr, &seed[32], 32);
+
+	return 0;
+}
+
+int mlkem768_keygen_internal(u8 pk[MLKEM768_PUBLIC_KEY_BYTES],
+			     u8 sk[MLKEM768_SECRET_KEY_BYTES],
+			     const u8 seed[MLKEM_SEED_BYTES])
+{
+	return mlkem_keygen_internal(pk, sk, seed, &mlkem768);
+}
+EXPORT_SYMBOL_NS_GPL(mlkem768_keygen_internal, "CRYPTO_INTERNAL");
+
+int mlkem768_keygen(u8 pk[MLKEM768_PUBLIC_KEY_BYTES],
+		    u8 sk[MLKEM768_SECRET_KEY_BYTES])
+{
+	u8 seed[MLKEM_SEED_BYTES];
+	int err;
+
+	get_random_bytes(seed, sizeof(seed));
+	err = mlkem768_keygen_internal(pk, sk, seed);
+	memzero_explicit(seed, sizeof(seed));
+	return err;
+}
+EXPORT_SYMBOL_NS_GPL(mlkem768_keygen, "CRYPTO_INTERNAL");
+
+int mlkem1024_keygen_internal(u8 pk[MLKEM1024_PUBLIC_KEY_BYTES],
+			      u8 sk[MLKEM1024_SECRET_KEY_BYTES],
+			      const u8 seed[MLKEM_SEED_BYTES])
+{
+	return mlkem_keygen_internal(pk, sk, seed, &mlkem1024);
+}
+EXPORT_SYMBOL_NS_GPL(mlkem1024_keygen_internal, "CRYPTO_INTERNAL");
+
+int mlkem1024_keygen(u8 pk[MLKEM1024_PUBLIC_KEY_BYTES],
+		     u8 sk[MLKEM1024_SECRET_KEY_BYTES])
+{
+	u8 seed[MLKEM_SEED_BYTES];
+	int err;
+
+	get_random_bytes(seed, sizeof(seed));
+	err = mlkem1024_keygen_internal(pk, sk, seed);
+	memzero_explicit(seed, sizeof(seed));
+	return err;
+}
+EXPORT_SYMBOL_NS_GPL(mlkem1024_keygen, "CRYPTO_INTERNAL");
+
+/*
+ * Generate and encapsulate a shared secret, using the random seed @eseed.
+ *
+ * Reference: FIPS 203 Algorithm 17, ML-KEM.Encaps_internal
+ */
+static int mlkem_encaps_internal(u8 *ct, u8 ss[MLKEM_SHARED_SECRET_BYTES],
+				 const u8 *pk,
+				 const u8 eseed[MLKEM_ESEED_BYTES],
+				 const struct mlkem_parameter_set *params)
+{
+	struct mlkem_encap_workspace *ws __free(kfree_sensitive) =
+		kmalloc_obj(*ws);
+	int err;
+
+	if (!ws)
+		return -ENOMEM;
+
+	/* Derived shared secret key K and randomness r. */
+	sha3_256(pk, params->pk_len, ws->pk_hash);
+	sha3_512_init(&ws->sha3);
+	sha3_update(&ws->sha3, eseed, MLKEM_ESEED_BYTES);
+	sha3_update(&ws->sha3, ws->pk_hash, SHA3_256_DIGEST_SIZE);
+	sha3_final(&ws->sha3, ws->K_and_r);
+
+	/* Encrypt eseed using K-PKE with randomness r. */
+	err = k_pke_encrypt(ct, pk, eseed, ws->r, &ws->k_pke_enc, params);
+	if (err == 0)
+		memcpy(ss, ws->K, MLKEM_SHARED_SECRET_BYTES);
+	return err;
+}
+
+int mlkem768_encaps_internal(u8 ct[MLKEM768_CIPHERTEXT_BYTES],
+			     u8 ss[MLKEM_SHARED_SECRET_BYTES],
+			     const u8 pk[MLKEM768_PUBLIC_KEY_BYTES],
+			     const u8 eseed[MLKEM_ESEED_BYTES])
+{
+	return mlkem_encaps_internal(ct, ss, pk, eseed, &mlkem768);
+}
+EXPORT_SYMBOL_NS_GPL(mlkem768_encaps_internal, "CRYPTO_INTERNAL");
+
+int mlkem768_encaps(u8 ct[MLKEM768_CIPHERTEXT_BYTES],
+		    u8 ss[MLKEM_SHARED_SECRET_BYTES],
+		    const u8 pk[MLKEM768_PUBLIC_KEY_BYTES])
+{
+	u8 eseed[MLKEM_ESEED_BYTES];
+	int err;
+
+	get_random_bytes(eseed, sizeof(eseed));
+	err = mlkem768_encaps_internal(ct, ss, pk, eseed);
+	memzero_explicit(eseed, sizeof(eseed));
+	return err;
+}
+EXPORT_SYMBOL_NS_GPL(mlkem768_encaps, "CRYPTO_INTERNAL");
+
+int mlkem1024_encaps_internal(u8 ct[MLKEM1024_CIPHERTEXT_BYTES],
+			      u8 ss[MLKEM_SHARED_SECRET_BYTES],
+			      const u8 pk[MLKEM1024_PUBLIC_KEY_BYTES],
+			      const u8 eseed[MLKEM_ESEED_BYTES])
+{
+	return mlkem_encaps_internal(ct, ss, pk, eseed, &mlkem1024);
+}
+EXPORT_SYMBOL_NS_GPL(mlkem1024_encaps_internal, "CRYPTO_INTERNAL");
+
+int mlkem1024_encaps(u8 ct[MLKEM1024_CIPHERTEXT_BYTES],
+		     u8 ss[MLKEM_SHARED_SECRET_BYTES],
+		     const u8 pk[MLKEM1024_PUBLIC_KEY_BYTES])
+{
+	u8 eseed[MLKEM_ESEED_BYTES];
+	int err;
+
+	get_random_bytes(eseed, sizeof(eseed));
+	err = mlkem1024_encaps_internal(ct, ss, pk, eseed);
+	memzero_explicit(eseed, sizeof(eseed));
+	return err;
+}
+EXPORT_SYMBOL_NS_GPL(mlkem1024_encaps, "CRYPTO_INTERNAL");
+
+/* Reference: FIPS 203 Algorithm 21, ML-KEM.Decaps */
+static int mlkem_decaps(u8 ss[MLKEM_SHARED_SECRET_BYTES], const u8 *ct,
+			const u8 *sk, const struct mlkem_parameter_set *params)
+{
+	struct mlkem_decap_workspace *ws __free(kfree_sensitive) =
+		kmalloc_obj(*ws);
+	const u8 *sk_pke, *pk_pke, *h, *z;
+	u32 mask;
+	int err;
+
+	if (!ws) {
+		err = -ENOMEM;
+		goto decaps_failed;
+	}
+
+	/* Extract the parts of the secret key. */
+	sk_pke = sk; /* PKE secret key */
+	pk_pke = &sk_pke[384 * params->k]; /* PKE public key */
+	h = &pk_pke[params->pk_len]; /* Hash of PKE public key */
+	z = &h[SHA3_256_DIGEST_SIZE]; /* Implicit rejection value */
+
+	/* Validate h, as specified in the "Hash check" in FIPS 203. */
+	sha3_256(pk_pke, params->pk_len, ws->h);
+	if (crypto_memneq(h, ws->h, SHA3_256_DIGEST_SIZE)) {
+		/*
+		 * This is technically a branch on the contents of the secret
+		 * key, but it only indicates whether it's malformed or not.
+		 */
+		err = -EBADMSG;
+		goto decaps_failed;
+	}
+
+	/* m' = K-PKE.Decrypt(sk_pke, ct) */
+	err = k_pke_decrypt(ws->m_prime, sk_pke, ct, &ws->k_pke_dec, params);
+	if (err) {
+		/*
+		 * This is technically a branch on the contents of the secret
+		 * key, but it only indicates whether it's malformed or not.
+		 */
+		goto decaps_failed;
+	}
+
+	/* (K', r') = G(m' || h) */
+	sha3_512_init(&ws->sha3);
+	sha3_update(&ws->sha3, ws->m_prime, 32);
+	sha3_update(&ws->sha3, h, SHA3_256_DIGEST_SIZE);
+	sha3_final(&ws->sha3, ws->K_prime_and_r_prime);
+
+	/* K_bar = J(z || ct) */
+	shake256_init(&ws->shake);
+	shake_update(&ws->shake, z, 32);
+	shake_update(&ws->shake, ct, params->ct_len);
+	shake_squeeze(&ws->shake, ws->K_bar, 32);
+
+	/* ct' = K-PKE.Encrypt(pk_pke, m', r') */
+	err = k_pke_encrypt(ws->ct_prime, pk_pke, ws->m_prime, ws->r_prime,
+			    &ws->k_pke_enc, params);
+	if (err)
+		goto decaps_failed;
+
+	/*
+	 * Return the shared secret: K_bar if ct != ct', else K'.
+	 *
+	 * This entire section must be constant-time with respect to not only
+	 * the contents of ct, ct', K', and K_bar, but also whether K' or K_bar
+	 * is chosen.  crypto_memneq() isn't necessarily safe to use here, as it
+	 * checks its result with a ternary expression.
+	 */
+	mask = 0;
+	for (int i = 0; i < params->ct_len; i++) {
+		OPTIMIZER_HIDE_VAR(mask);
+		mask |= ct[i] ^ ws->ct_prime[i];
+	}
+	OPTIMIZER_HIDE_VAR(mask);
+	mask = -mask; /* bit 31 is 1 if ct != ct', else 0 */
+	OPTIMIZER_HIDE_VAR(mask);
+	mask = (s32)mask >> 31; /* mask = 0xffffffff if ct != ct', else 0 */
+	OPTIMIZER_HIDE_VAR(mask);
+	for (int i = 0; i < 32; i++)
+		ss[i] = (ws->K_bar[i] & mask) | (ws->K_prime[i] & ~mask);
+	return 0;
+
+decaps_failed:
+	/*
+	 * On error, return a random shared secret.  This is a safer default in
+	 * the case where the caller forgets to check the return value.
+	 */
+	get_random_bytes(ss, MLKEM_SHARED_SECRET_BYTES);
+	return err;
+}
+
+int mlkem768_decaps(u8 ss[MLKEM_SHARED_SECRET_BYTES],
+		    const u8 ct[MLKEM768_CIPHERTEXT_BYTES],
+		    const u8 sk[MLKEM768_SECRET_KEY_BYTES])
+{
+	return mlkem_decaps(ss, ct, sk, &mlkem768);
+}
+EXPORT_SYMBOL_NS_GPL(mlkem768_decaps, "CRYPTO_INTERNAL");
+
+int mlkem1024_decaps(u8 ss[MLKEM_SHARED_SECRET_BYTES],
+		     const u8 ct[MLKEM1024_CIPHERTEXT_BYTES],
+		     const u8 sk[MLKEM1024_SECRET_KEY_BYTES])
+{
+	return mlkem_decaps(ss, ct, sk, &mlkem1024);
+}
+EXPORT_SYMBOL_NS_GPL(mlkem1024_decaps, "CRYPTO_INTERNAL");
+
+MODULE_DESCRIPTION("ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism)");
+MODULE_LICENSE("GPL");
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 2/5] lib/crypto: mlkem: Add KUnit tests for ML-KEM
  2026-05-25 18:43 [PATCH 0/5] ML-KEM and X-Wing support Eric Biggers
  2026-05-25 18:43 ` [PATCH 1/5] lib/crypto: mlkem: Add ML-KEM-768 and ML-KEM-1024 support Eric Biggers
@ 2026-05-25 18:44 ` Eric Biggers
  2026-05-25 18:44 ` [PATCH 3/5] lib/crypto: mlkem: Add FIPS 140-3 tests Eric Biggers
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Eric Biggers @ 2026-05-25 18:44 UTC (permalink / raw)
  To: linux-crypto
  Cc: linux-kernel, Ard Biesheuvel, Jason A . Donenfeld, Herbert Xu,
	Ryan Appel, Chris Leech, Eric Biggers

Add a KUnit test suite for ML-KEM.

For each supported ML-KEM parameter set, it includes:

- Test key generation, encapsulation, and decapsulation against the
  first 1000 test vectors from the reference implementation
- Test encapsulation/decapsulation round trips
- Test validation of malformed keys
- Test that every byte of the ciphertext is validated
- Test the reduce_once(), reduce(), compress_d(), and decompress_d()
  functions with all allowed inputs
- Benchmark key generation, encapsulation, and decapsulation

Signed-off-by: Eric Biggers <ebiggers@kernel.org>
---
 include/crypto/mlkem.h                  |   8 +
 lib/crypto/.kunitconfig                 |   1 +
 lib/crypto/mlkem.c                      |  27 ++
 lib/crypto/tests/Kconfig                |   9 +
 lib/crypto/tests/Makefile               |   1 +
 lib/crypto/tests/mlkem-testvecs.h       |  19 +
 lib/crypto/tests/mlkem_kunit.c          | 520 ++++++++++++++++++++++++
 scripts/crypto/import-mlkem-testvecs.py | 117 ++++++
 8 files changed, 702 insertions(+)
 create mode 100644 lib/crypto/tests/mlkem-testvecs.h
 create mode 100644 lib/crypto/tests/mlkem_kunit.c
 create mode 100755 scripts/crypto/import-mlkem-testvecs.py

diff --git a/include/crypto/mlkem.h b/include/crypto/mlkem.h
index e33d065c5442..679bd47c8c0b 100644
--- a/include/crypto/mlkem.h
+++ b/include/crypto/mlkem.h
@@ -146,6 +146,14 @@ int mlkem1024_keygen_internal(u8 pk[MLKEM1024_PUBLIC_KEY_BYTES],
 int mlkem1024_encaps_internal(u8 ct[MLKEM1024_CIPHERTEXT_BYTES],
 			      u8 ss[MLKEM_SHARED_SECRET_BYTES],
 			      const u8 pk[MLKEM1024_PUBLIC_KEY_BYTES],
 			      const u8 eseed[MLKEM_ESEED_BYTES]);
 
+#if IS_ENABLED(CONFIG_CRYPTO_LIB_MLKEM_KUNIT_TEST)
+/* Functions exported for KUnit testing only */
+u16 mlkem_reduce_once(u16 x);
+u16 mlkem_reduce(u32 x);
+u16 mlkem_compress_d(u16 x, int d);
+u16 mlkem_decompress_d(u16 x, int d);
+#endif
+
 #endif /* _CRYPTO_MLKEM_H */
diff --git a/lib/crypto/.kunitconfig b/lib/crypto/.kunitconfig
index 3efc854a2c08..32e5b4471da8 100644
--- a/lib/crypto/.kunitconfig
+++ b/lib/crypto/.kunitconfig
@@ -8,10 +8,11 @@ CONFIG_CRYPTO_LIB_BLAKE2S_KUNIT_TEST=y
 CONFIG_CRYPTO_LIB_CHACHA20POLY1305_KUNIT_TEST=y
 CONFIG_CRYPTO_LIB_CURVE25519_KUNIT_TEST=y
 CONFIG_CRYPTO_LIB_GHASH_KUNIT_TEST=y
 CONFIG_CRYPTO_LIB_MD5_KUNIT_TEST=y
 CONFIG_CRYPTO_LIB_MLDSA_KUNIT_TEST=y
+CONFIG_CRYPTO_LIB_MLKEM_KUNIT_TEST=y
 CONFIG_CRYPTO_LIB_NH_KUNIT_TEST=y
 CONFIG_CRYPTO_LIB_POLY1305_KUNIT_TEST=y
 CONFIG_CRYPTO_LIB_POLYVAL_KUNIT_TEST=y
 CONFIG_CRYPTO_LIB_SHA1_KUNIT_TEST=y
 CONFIG_CRYPTO_LIB_SHA256_KUNIT_TEST=y
diff --git a/lib/crypto/mlkem.c b/lib/crypto/mlkem.c
index b800ecb49f24..5e499e5de636 100644
--- a/lib/crypto/mlkem.c
+++ b/lib/crypto/mlkem.c
@@ -8,10 +8,11 @@
  */
 
 #include <crypto/mlkem.h>
 #include <crypto/sha3.h>
 #include <crypto/utils.h>
+#include <kunit/visibility.h>
 #include <linux/export.h>
 #include <linux/module.h>
 #include <linux/random.h>
 #include <linux/slab.h>
 #include <linux/string.h>
@@ -888,7 +889,33 @@ int mlkem1024_decaps(u8 ss[MLKEM_SHARED_SECRET_BYTES],
 {
 	return mlkem_decaps(ss, ct, sk, &mlkem1024);
 }
 EXPORT_SYMBOL_NS_GPL(mlkem1024_decaps, "CRYPTO_INTERNAL");
 
+#if IS_ENABLED(CONFIG_CRYPTO_LIB_MLKEM_KUNIT_TEST)
+u16 mlkem_reduce_once(u16 x)
+{
+	return reduce_once(x);
+}
+EXPORT_SYMBOL_IF_KUNIT(mlkem_reduce_once);
+
+u16 mlkem_reduce(u32 x)
+{
+	return reduce(x);
+}
+EXPORT_SYMBOL_IF_KUNIT(mlkem_reduce);
+
+u16 mlkem_compress_d(u16 x, int d)
+{
+	return compress_d(x, d);
+}
+EXPORT_SYMBOL_IF_KUNIT(mlkem_compress_d);
+
+u16 mlkem_decompress_d(u16 x, int d)
+{
+	return decompress_d(x, d);
+}
+EXPORT_SYMBOL_IF_KUNIT(mlkem_decompress_d);
+#endif /* CONFIG_CRYPTO_LIB_MLKEM_KUNIT_TEST */
+
 MODULE_DESCRIPTION("ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism)");
 MODULE_LICENSE("GPL");
diff --git a/lib/crypto/tests/Kconfig b/lib/crypto/tests/Kconfig
index 9409c1a935c3..0a110a0733d2 100644
--- a/lib/crypto/tests/Kconfig
+++ b/lib/crypto/tests/Kconfig
@@ -67,10 +67,18 @@ config CRYPTO_LIB_MLDSA_KUNIT_TEST
 	default KUNIT_ALL_TESTS
 	select CRYPTO_LIB_BENCHMARK_VISIBLE
 	help
 	  KUnit tests for the ML-DSA digital signature algorithm.
 
+config CRYPTO_LIB_MLKEM_KUNIT_TEST
+	tristate "KUnit tests for ML-KEM" if !KUNIT_ALL_TESTS
+	depends on KUNIT && CRYPTO_LIB_MLKEM
+	default KUNIT_ALL_TESTS
+	select CRYPTO_LIB_BENCHMARK_VISIBLE
+	help
+	  KUnit tests for the ML-KEM key encapsulation mechanism.
+
 config CRYPTO_LIB_NH_KUNIT_TEST
 	tristate "KUnit tests for NH" if !KUNIT_ALL_TESTS
 	depends on KUNIT && CRYPTO_LIB_NH
 	default KUNIT_ALL_TESTS
 	help
@@ -149,10 +157,11 @@ config CRYPTO_LIB_ENABLE_ALL_FOR_KUNIT
 	select CRYPTO_LIB_CHACHA20POLY1305
 	select CRYPTO_LIB_CURVE25519
 	select CRYPTO_LIB_GF128HASH
 	select CRYPTO_LIB_MD5
 	select CRYPTO_LIB_MLDSA
+	select CRYPTO_LIB_MLKEM
 	select CRYPTO_LIB_NH
 	select CRYPTO_LIB_POLY1305
 	select CRYPTO_LIB_SHA1
 	select CRYPTO_LIB_SHA256
 	select CRYPTO_LIB_SHA512
diff --git a/lib/crypto/tests/Makefile b/lib/crypto/tests/Makefile
index a739413500b6..3a73d2f33f75 100644
--- a/lib/crypto/tests/Makefile
+++ b/lib/crypto/tests/Makefile
@@ -6,10 +6,11 @@ obj-$(CONFIG_CRYPTO_LIB_BLAKE2S_KUNIT_TEST) += blake2s_kunit.o
 obj-$(CONFIG_CRYPTO_LIB_CHACHA20POLY1305_KUNIT_TEST) += chacha20poly1305_kunit.o
 obj-$(CONFIG_CRYPTO_LIB_CURVE25519_KUNIT_TEST) += curve25519_kunit.o
 obj-$(CONFIG_CRYPTO_LIB_GHASH_KUNIT_TEST) += ghash_kunit.o
 obj-$(CONFIG_CRYPTO_LIB_MD5_KUNIT_TEST) += md5_kunit.o
 obj-$(CONFIG_CRYPTO_LIB_MLDSA_KUNIT_TEST) += mldsa_kunit.o
+obj-$(CONFIG_CRYPTO_LIB_MLKEM_KUNIT_TEST) += mlkem_kunit.o
 obj-$(CONFIG_CRYPTO_LIB_NH_KUNIT_TEST) += nh_kunit.o
 obj-$(CONFIG_CRYPTO_LIB_POLY1305_KUNIT_TEST) += poly1305_kunit.o
 obj-$(CONFIG_CRYPTO_LIB_POLYVAL_KUNIT_TEST) += polyval_kunit.o
 obj-$(CONFIG_CRYPTO_LIB_SHA1_KUNIT_TEST) += sha1_kunit.o
 obj-$(CONFIG_CRYPTO_LIB_SHA256_KUNIT_TEST) += sha224_kunit.o sha256_kunit.o
diff --git a/lib/crypto/tests/mlkem-testvecs.h b/lib/crypto/tests/mlkem-testvecs.h
new file mode 100644
index 000000000000..207bf38529b2
--- /dev/null
+++ b/lib/crypto/tests/mlkem-testvecs.h
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* This file was generated by import-mlkem-testvecs.py */
+/* clang-format off */
+
+#define MLKEM768_NUM_TESTVECS 1000
+
+static const u8 mlkem768_hash[32] = {
+	0x81, 0xa6, 0x72, 0x68, 0x1c, 0x57, 0xaf, 0xc5, 0x1b, 0xdb, 0xc2, 0xfc,
+	0x08, 0xc0, 0x4a, 0xbe, 0x01, 0xd9, 0xad, 0x5f, 0x1c, 0x77, 0x89, 0xa4,
+	0xa5, 0x98, 0xdf, 0x9d, 0xb2, 0x91, 0xa6, 0x56,
+};
+
+#define MLKEM1024_NUM_TESTVECS 1000
+
+static const u8 mlkem1024_hash[32] = {
+	0x64, 0x4e, 0x58, 0xca, 0x6c, 0xf9, 0xb6, 0x08, 0x07, 0x50, 0xa8, 0x76,
+	0xe6, 0xae, 0xf5, 0xe0, 0x44, 0xce, 0xe1, 0xd9, 0x42, 0x36, 0xb6, 0xc1,
+	0x14, 0xc7, 0x5f, 0x2c, 0x8b, 0xd7, 0x73, 0x77,
+};
diff --git a/lib/crypto/tests/mlkem_kunit.c b/lib/crypto/tests/mlkem_kunit.c
new file mode 100644
index 000000000000..2e9a9203e9a2
--- /dev/null
+++ b/lib/crypto/tests/mlkem_kunit.c
@@ -0,0 +1,520 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * KUnit tests and benchmark for ML-KEM
+ *
+ * Copyright 2026 Google LLC
+ */
+#include <crypto/mlkem.h>
+#include <crypto/sha3.h>
+#include <kunit/test.h>
+#include "mlkem-testvecs.h"
+
+#define Q 3329
+
+enum mlkem_paramset {
+	MLKEM768,
+	MLKEM1024,
+};
+
+struct mlkem_bufs {
+	u8 *pk, *sk, *ct;
+	size_t pk_len, sk_len, ct_len;
+	u8 ss[MLKEM_SHARED_SECRET_BYTES];
+	u8 seed[MLKEM_SEED_BYTES];
+	u8 eseed[MLKEM_ESEED_BYTES];
+};
+
+static const struct {
+	const char *name;
+	int k;
+	size_t pk_len;
+	size_t sk_len;
+	size_t ct_len;
+} mlkem_paramsets[] = {
+	[MLKEM768] = {
+		.name = "ML-KEM-768",
+		.k = 3,
+		.pk_len = MLKEM768_PUBLIC_KEY_BYTES,
+		.sk_len = MLKEM768_SECRET_KEY_BYTES,
+		.ct_len = MLKEM768_CIPHERTEXT_BYTES,
+	},
+	[MLKEM1024] = {
+		.name = "ML-KEM-1024",
+		.k = 4,
+		.pk_len = MLKEM1024_PUBLIC_KEY_BYTES,
+		.sk_len = MLKEM1024_SECRET_KEY_BYTES,
+		.ct_len = MLKEM1024_CIPHERTEXT_BYTES,
+	},
+};
+
+static struct mlkem_bufs *alloc_bufs(struct kunit *test,
+				     enum mlkem_paramset paramset)
+{
+	struct mlkem_bufs *bufs =
+		kunit_kmalloc(test, sizeof(*bufs), GFP_KERNEL);
+
+	KUNIT_ASSERT_NOT_NULL(test, bufs);
+
+	bufs->pk_len = mlkem_paramsets[paramset].pk_len;
+	bufs->pk = kunit_kmalloc(test, bufs->pk_len, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, bufs->pk);
+
+	bufs->sk_len = mlkem_paramsets[paramset].sk_len;
+	bufs->sk = kunit_kmalloc(test, bufs->sk_len, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, bufs->sk);
+
+	bufs->ct_len = mlkem_paramsets[paramset].ct_len;
+	bufs->ct = kunit_kmalloc(test, bufs->ct_len, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, bufs->ct);
+
+	return bufs;
+}
+
+static int keygen(u8 *pk, u8 *sk, enum mlkem_paramset paramset)
+{
+	switch (paramset) {
+	case MLKEM768:
+		return mlkem768_keygen(pk, sk);
+	case MLKEM1024:
+		return mlkem1024_keygen(pk, sk);
+	default:
+		WARN_ON_ONCE(1);
+		return -EOPNOTSUPP;
+	}
+}
+
+static int keygen_internal(u8 *pk, u8 *sk, const u8 seed[MLKEM_SEED_BYTES],
+			   enum mlkem_paramset paramset)
+{
+	switch (paramset) {
+	case MLKEM768:
+		return mlkem768_keygen_internal(pk, sk, seed);
+	case MLKEM1024:
+		return mlkem1024_keygen_internal(pk, sk, seed);
+	default:
+		WARN_ON_ONCE(1);
+		return -EOPNOTSUPP;
+	}
+}
+
+static int encaps(u8 *ct, u8 ss[MLKEM_SHARED_SECRET_BYTES], const u8 *pk,
+		  enum mlkem_paramset paramset)
+{
+	switch (paramset) {
+	case MLKEM768:
+		return mlkem768_encaps(ct, ss, pk);
+	case MLKEM1024:
+		return mlkem1024_encaps(ct, ss, pk);
+	default:
+		WARN_ON_ONCE(1);
+		return -EOPNOTSUPP;
+	}
+}
+
+static int encaps_internal(u8 *ct, u8 ss[MLKEM_SHARED_SECRET_BYTES],
+			   const u8 *pk, const u8 eseed[MLKEM_ESEED_BYTES],
+			   enum mlkem_paramset paramset)
+{
+	switch (paramset) {
+	case MLKEM768:
+		return mlkem768_encaps_internal(ct, ss, pk, eseed);
+	case MLKEM1024:
+		return mlkem1024_encaps_internal(ct, ss, pk, eseed);
+	default:
+		WARN_ON_ONCE(1);
+		return -EOPNOTSUPP;
+	}
+}
+
+static int decaps(u8 ss[MLKEM_SHARED_SECRET_BYTES], const u8 *ct, const u8 *sk,
+		  enum mlkem_paramset paramset)
+{
+	switch (paramset) {
+	case MLKEM768:
+		return mlkem768_decaps(ss, ct, sk);
+	case MLKEM1024:
+		return mlkem1024_decaps(ss, ct, sk);
+	default:
+		WARN_ON_ONCE(1);
+		return -EOPNOTSUPP;
+	}
+}
+
+/*
+ * Test the ML-KEM implementation against the first 1000 test vectors from the
+ * reference implementation.
+ *
+ * To do this without explicitly including all these test vectors, which would
+ * result in a massive source and binary size, we take advantage of the fact
+ * that the reference test vectors are generated deterministically (by
+ * kyber/ref/tests/test_vectors.c).  We just regenerate them at runtime using
+ * the same algorithm.  We hash all the outputs, then verify that hash against
+ * @expected_cumulative_hash, which proves that all the outputs were correct.
+ */
+static void
+test_mlkem_against_ref_testvecs(struct kunit *test, size_t num_testvecs,
+				const u8 expected_cumulative_hash[32],
+				enum mlkem_paramset paramset)
+{
+	struct mlkem_bufs *bufs = alloc_bufs(test, paramset);
+	struct shake_ctx cumulative_hash_ctx;
+	struct shake_ctx seed_ctx;
+	u8 cumulative_hash[32];
+
+	shake128_init(&cumulative_hash_ctx);
+	shake128_init(&seed_ctx);
+	for (size_t i = 0; i < num_testvecs; i++) {
+		/*
+		 * Deterministically generate the next seeds using the same
+		 * algorithm as the reference code's test_vectors.c.
+		 */
+		shake_squeeze(&seed_ctx, bufs->seed, sizeof(bufs->seed));
+		shake_squeeze(&seed_ctx, bufs->eseed, sizeof(bufs->eseed));
+
+		/* KeyGen, then update with (pk, sk) */
+		KUNIT_ASSERT_EQ(test, 0,
+				keygen_internal(bufs->pk, bufs->sk, bufs->seed,
+						paramset));
+		shake_update(&cumulative_hash_ctx, bufs->pk, bufs->pk_len);
+		shake_update(&cumulative_hash_ctx, bufs->sk, bufs->sk_len);
+
+		/* Encaps, then update with (ct, ss) */
+		KUNIT_ASSERT_EQ(test, 0,
+				encaps_internal(bufs->ct, bufs->ss, bufs->pk,
+						bufs->eseed, paramset));
+		shake_update(&cumulative_hash_ctx, bufs->ct, bufs->ct_len);
+		shake_update(&cumulative_hash_ctx, bufs->ss, sizeof(bufs->ss));
+
+		/* Decaps, then update with ss */
+		memset(bufs->ss, 0xff, sizeof(bufs->ss));
+		KUNIT_ASSERT_EQ(test, 0,
+				decaps(bufs->ss, bufs->ct, bufs->sk, paramset));
+		shake_update(&cumulative_hash_ctx, bufs->ss, sizeof(bufs->ss));
+
+		/*
+		 * Deterministically generate an invalid ciphertext, using the
+		 * same algorithm as test_vectors.c.  Then do Decaps and update
+		 * with ss_rejected.  This tests the implicit rejection case.
+		 */
+		shake_squeeze(&seed_ctx, bufs->ct, bufs->ct_len);
+		KUNIT_ASSERT_EQ(test, 0,
+				decaps(bufs->ss, bufs->ct, bufs->sk, paramset));
+		shake_update(&cumulative_hash_ctx, bufs->ss, sizeof(bufs->ss));
+	}
+	/*
+	 * Finalize and verify the cumulative hash.  This verifies that every
+	 * (pk, sk, ct, ss, ss, ss_rejected) tuple was correct.
+	 */
+	shake_squeeze(&cumulative_hash_ctx, cumulative_hash,
+		      sizeof(cumulative_hash));
+	KUNIT_ASSERT_MEMEQ(test, expected_cumulative_hash, cumulative_hash,
+			   sizeof(cumulative_hash));
+}
+
+static void test_mlkem_round_trip(struct kunit *test,
+				  enum mlkem_paramset paramset)
+{
+	struct mlkem_bufs *bufs = alloc_bufs(test, paramset);
+	u8 ss2[MLKEM_SHARED_SECRET_BYTES];
+
+	for (int i = 0; i < 20; i++) {
+		KUNIT_ASSERT_EQ(test, 0, keygen(bufs->pk, bufs->sk, paramset));
+		KUNIT_ASSERT_EQ(test, 0,
+				encaps(bufs->ct, bufs->ss, bufs->pk, paramset));
+		KUNIT_ASSERT_EQ(test, 0,
+				decaps(ss2, bufs->ct, bufs->sk, paramset));
+		KUNIT_ASSERT_MEMEQ(test, bufs->ss, ss2, sizeof(bufs->ss));
+	}
+}
+
+/*
+ * Test that changing any part of the ciphertext results in a different shared
+ * secret due to implicit rejection.
+ */
+static void test_mlkem_rejection(struct kunit *test,
+				 enum mlkem_paramset paramset)
+{
+	struct mlkem_bufs *bufs = alloc_bufs(test, paramset);
+	u8 ss2[MLKEM_SHARED_SECRET_BYTES];
+
+	KUNIT_ASSERT_EQ(test, 0, keygen(bufs->pk, bufs->sk, paramset));
+	KUNIT_ASSERT_EQ(test, 0,
+			encaps(bufs->ct, bufs->ss, bufs->pk, paramset));
+
+	/* Decapsulate a valid ciphertext. */
+	KUNIT_ASSERT_EQ(test, 0, decaps(ss2, bufs->ct, bufs->sk, paramset));
+	KUNIT_ASSERT_MEMEQ(test, bufs->ss, ss2, sizeof(bufs->ss));
+
+	for (size_t i = 0; i < bufs->ct_len; i++) {
+		/* Corrupt byte i of the ciphertext. */
+		bufs->ct[i] ^= 1;
+
+		/* Decapsulate an invalid ciphertext and assert ss differs. */
+		KUNIT_ASSERT_EQ(test, 0,
+				decaps(ss2, bufs->ct, bufs->sk, paramset));
+		KUNIT_ASSERT_MEMNEQ(test, bufs->ss, ss2, sizeof(bufs->ss));
+		/* Undo the ciphertext corruption. */
+		bufs->ct[i] ^= 1;
+	}
+}
+
+/*
+ * Test that the encapsulation function returns -EBADMSG if a coefficient in
+ * NTT(t) in the public key is outside the interval [0, Q - 1].
+ */
+static void test_mlkem_invalid_pk(struct kunit *test,
+				  enum mlkem_paramset paramset)
+{
+	struct mlkem_bufs *bufs = alloc_bufs(test, paramset);
+	const size_t ntt_t_len = 384 * mlkem_paramsets[paramset].k;
+
+	for (int i = 0; i < 4; i++) {
+		u16 c;
+
+		KUNIT_ASSERT_EQ(test, 0, keygen(bufs->pk, bufs->sk, paramset));
+		KUNIT_ASSERT_EQ(test, 0,
+				encaps(bufs->ct, bufs->ss, bufs->pk, paramset));
+		/*
+		 * Corrupt a coefficient of NTT(t), which is an array of 256*k
+		 * 12-bit coefficients starting at the beginning of pk.
+		 */
+		if (i % 2 == 0)
+			c = Q; /* Low end of invalid range */
+		else
+			c = 0xfff; /* High end of invalid range */
+		if (i < 2) {
+			/* Corrupt the first 12-bit coefficient in NTT(t). */
+			bufs->pk[0] = (c & 0xff);
+			bufs->pk[1] = (bufs->pk[1] & 0xf0) | (c >> 8);
+		} else {
+			/* Corrupt the last 12-bit coefficient in NTT(t). */
+			bufs->pk[ntt_t_len - 2] =
+				(bufs->pk[ntt_t_len - 2] & 0xf) |
+				((c & 0xf) << 4);
+			bufs->pk[ntt_t_len - 1] = c >> 4;
+		}
+		KUNIT_ASSERT_EQ(test, -EBADMSG,
+				encaps(bufs->ct, bufs->ss, bufs->pk, paramset));
+	}
+}
+
+/*
+ * Test that the decapsulation function returns -EBADMSG if either:
+ *
+ *    - H(pk) is corrupt
+ *    - A coefficient in NTT(s) is outside the interval [0, Q - 1]
+ */
+static void test_mlkem_invalid_sk(struct kunit *test,
+				  enum mlkem_paramset paramset)
+{
+	struct mlkem_bufs *bufs = alloc_bufs(test, paramset);
+	const size_t ntt_s_len = 384 * mlkem_paramsets[paramset].k;
+
+	KUNIT_ASSERT_EQ(test, 0, keygen(bufs->pk, bufs->sk, paramset));
+	KUNIT_ASSERT_EQ(test, 0,
+			encaps(bufs->ct, bufs->ss, bufs->pk, paramset));
+	KUNIT_ASSERT_EQ(test, 0,
+			decaps(bufs->ss, bufs->ct, bufs->sk, paramset));
+
+	/* Corrupt H(pk) in the sk. */
+	bufs->sk[bufs->sk_len - 33] ^= 0x80;
+	KUNIT_ASSERT_EQ(test, -EBADMSG,
+			decaps(bufs->ss, bufs->ct, bufs->sk, paramset));
+
+	for (int i = 0; i < 4; i++) {
+		u16 c;
+
+		KUNIT_ASSERT_EQ(test, 0, keygen(bufs->pk, bufs->sk, paramset));
+		KUNIT_ASSERT_EQ(test, 0,
+				encaps(bufs->ct, bufs->ss, bufs->pk, paramset));
+		KUNIT_ASSERT_EQ(test, 0,
+				decaps(bufs->ss, bufs->ct, bufs->sk, paramset));
+
+		/*
+		 * Corrupt a coefficient of NTT(s), which is an array of 256*k
+		 * 12-bit coefficients starting at the beginning of sk.
+		 */
+		if (i % 2 == 0)
+			c = Q; /* Low end of invalid range */
+		else
+			c = 0xfff; /* High end of invalid range */
+		if (i < 2) {
+			/* Corrupt the first 12-bit coefficient in NTT(s). */
+			bufs->sk[0] = (c & 0xff);
+			bufs->sk[1] = (bufs->sk[1] & 0xf0) | (c >> 8);
+		} else {
+			/* Corrupt the last 12-bit coefficient in NTT(s). */
+			bufs->sk[ntt_s_len - 2] =
+				(bufs->sk[ntt_s_len - 2] & 0xf) |
+				((c & 0xf) << 4);
+			bufs->sk[ntt_s_len - 1] = c >> 4;
+		}
+		KUNIT_ASSERT_EQ(test, -EBADMSG,
+				decaps(bufs->ss, bufs->ct, bufs->sk, paramset));
+	}
+}
+
+static void test_mlkem(struct kunit *test, size_t num_testvecs,
+		       const u8 expected_cumulative_hash[32],
+		       enum mlkem_paramset paramset)
+{
+	test_mlkem_against_ref_testvecs(test, num_testvecs,
+					expected_cumulative_hash, paramset);
+	test_mlkem_round_trip(test, paramset);
+	test_mlkem_rejection(test, paramset);
+	test_mlkem_invalid_pk(test, paramset);
+	test_mlkem_invalid_sk(test, paramset);
+}
+
+static void test_mlkem768(struct kunit *test)
+{
+	test_mlkem(test, MLKEM768_NUM_TESTVECS, mlkem768_hash, MLKEM768);
+}
+
+static void test_mlkem1024(struct kunit *test)
+{
+	test_mlkem(test, MLKEM1024_NUM_TESTVECS, mlkem1024_hash, MLKEM1024);
+}
+
+static u16 mod_q(s32 x)
+{
+	x %= Q;
+	if (x < 0)
+		x += Q;
+	return x;
+}
+
+/*
+ * Test that mlkem_reduce_once() and mlkem_reduce() produce the correct output
+ * for every supported input.
+ */
+static void test_mlkem_reduce(struct kunit *test)
+{
+	/* mlkem_reduce_once() supports 0 <= x < 2*Q */
+	for (u16 x = 0; x < 2 * Q; x++)
+		KUNIT_ASSERT_EQ(test, mod_q(x), mlkem_reduce_once(x));
+
+	/* mlkem_reduce() supports 0 <= x < Q + 2*Q*Q */
+	for (u32 x = 0; x < Q + 2 * Q * Q; x++)
+		KUNIT_ASSERT_EQ(test, mod_q(x), mlkem_reduce(x));
+}
+
+/* round((2^d / Q) * x) mod 2^d */
+static u16 compress_d_ref(u16 x, int d)
+{
+	u64 quotient, remainder;
+
+	quotient = div64_u64_rem((u64)x << d, Q, &remainder);
+	if (remainder >= (Q + 1) / 2)
+		quotient++;
+	return quotient & ((1 << d) - 1);
+}
+
+/* round((Q / 2^d) * y) */
+static u16 decompress_d_ref(u16 y, int d)
+{
+	u64 quotient, remainder;
+
+	quotient = div64_u64_rem((u64)y * Q, 1 << d, &remainder);
+	if (remainder >= 1 << (d - 1))
+		quotient++;
+	return quotient;
+}
+
+/*
+ * Test that mlkem_compress_d() produces the correct output for every supported
+ * input.
+ */
+static void test_mlkem_compress(struct kunit *test)
+{
+	/* compress_d() supports 0 <= x < Q and 1 <= d <= 11. */
+	for (int d = 1; d <= 11; d++) {
+		for (int x = 0; x < Q; x++) {
+			KUNIT_ASSERT_EQ(test, compress_d_ref(x, d),
+					mlkem_compress_d(x, d));
+		}
+	}
+}
+
+/*
+ * Test that mlkem_decompress_d() produces the correct output for every
+ * supported input.
+ */
+static void test_mlkem_decompress(struct kunit *test)
+{
+	for (int d = 1; d <= 11; d++) {
+		for (int y = 0; y < (1 << d); y++) {
+			KUNIT_ASSERT_EQ(test, decompress_d_ref(y, d),
+					mlkem_decompress_d(y, d));
+		}
+	}
+}
+
+/* Benchmark ML-KEM performance. */
+static void benchmark_mlkem(struct kunit *test, enum mlkem_paramset paramset)
+{
+	const char *name = mlkem_paramsets[paramset].name;
+	struct mlkem_bufs *bufs = alloc_bufs(test, paramset);
+	const int iterations = 100;
+	ktime_t start, end;
+
+	if (!IS_ENABLED(CONFIG_CRYPTO_LIB_BENCHMARK))
+		kunit_skip(test, "not enabled");
+
+	start = ktime_get();
+	for (int i = 0; i < iterations; i++)
+		KUNIT_ASSERT_EQ(test, 0, keygen(bufs->pk, bufs->sk, paramset));
+	end = ktime_get();
+	kunit_info(test, "%s_KeyGen: %llu ns/op\n", name,
+		   div64_u64(ktime_to_ns(ktime_sub(end, start)), iterations));
+
+	start = ktime_get();
+	for (int i = 0; i < iterations; i++)
+		KUNIT_ASSERT_EQ(test, 0,
+				encaps(bufs->ct, bufs->ss, bufs->pk, paramset));
+	end = ktime_get();
+	kunit_info(test, "%s_Encaps: %llu ns/op\n", name,
+		   div64_u64(ktime_to_ns(ktime_sub(end, start)), iterations));
+
+	start = ktime_get();
+	for (int i = 0; i < iterations; i++)
+		KUNIT_ASSERT_EQ(test, 0,
+				decaps(bufs->ss, bufs->ct, bufs->sk, paramset));
+	end = ktime_get();
+	kunit_info(test, "%s_Decaps: %llu ns/op\n", name,
+		   div64_u64(ktime_to_ns(ktime_sub(end, start)), iterations));
+}
+
+static void benchmark_mlkem768(struct kunit *test)
+{
+	benchmark_mlkem(test, MLKEM768);
+}
+
+static void benchmark_mlkem1024(struct kunit *test)
+{
+	benchmark_mlkem(test, MLKEM1024);
+}
+
+/* clang-format off */
+static struct kunit_case mlkem_test_cases[] = {
+	KUNIT_CASE(test_mlkem768),
+	KUNIT_CASE(test_mlkem1024),
+	KUNIT_CASE(test_mlkem_reduce),
+	KUNIT_CASE(test_mlkem_compress),
+	KUNIT_CASE(test_mlkem_decompress),
+	KUNIT_CASE(benchmark_mlkem768),
+	KUNIT_CASE(benchmark_mlkem1024),
+	{},
+};
+/* clang-format on */
+
+static struct kunit_suite mlkem_test_suite = {
+	.name = "mlkem",
+	.test_cases = mlkem_test_cases,
+};
+kunit_test_suite(mlkem_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests and benchmark for ML-KEM");
+MODULE_IMPORT_NS("CRYPTO_INTERNAL");
+MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
+MODULE_LICENSE("GPL");
diff --git a/scripts/crypto/import-mlkem-testvecs.py b/scripts/crypto/import-mlkem-testvecs.py
new file mode 100755
index 000000000000..1d5681ccfee1
--- /dev/null
+++ b/scripts/crypto/import-mlkem-testvecs.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# This script imports test vectors from the Kyber reference code.  To use:
+#
+#   git clone https://github.com/pq-crystals/kyber
+#   cd kyber/ref
+#   make
+#   ./test/test_vectors768 > ./test/tvecs768
+#   ./test/test_vectors1024 > ./test/tvecs1024
+#   $PATH_TO_THIS_SCRIPT ./test/
+#
+# This script generates the following files:
+#
+#   lib/crypto/tests/mlkem-testvecs.h
+
+import hashlib
+import os
+import sys
+
+SCRIPT_NAME = os.path.basename(__file__)
+LINUX_DIR = os.path.dirname(os.path.realpath(__file__)) + "/../.."
+
+MLKEM_LENGTHS = {
+    768: {
+        "pk_len": 1184,
+        "sk_len": 2400,
+        "ct_len": 1088,
+    },
+    1024: {
+        "pk_len": 1568,
+        "sk_len": 3168,
+        "ct_len": 1568,
+    },
+}
+
+
+def print_header(file):
+    print("/* SPDX-License-Identifier: GPL-2.0-or-later */", file=file)
+    print(f"/* This file was generated by {SCRIPT_NAME} */", file=file)
+    print(f"/* clang-format off */", file=file)
+
+
+def hex_to_bytes(hex_string, expected_bin_len):
+    res = bytes.fromhex(hex_string)
+    assert len(res) == expected_bin_len
+    return res
+
+
+def print_bytes(file, prefix, value, bytes_per_line):
+    for i in range(0, len(value), bytes_per_line):
+        line = prefix + "".join(f"0x{b:02x}, " for b in value[i : i + bytes_per_line])
+        print(f"{line.rstrip()}", file=file)
+
+
+def print_static_u8_array_definition(file, name, value):
+    print("", file=file)
+    print(f"static const u8 {name} = {{", file=file)
+    print_bytes(file, "\t", value, 12)
+    print("};", file=file)
+
+
+class Testvec:
+    def __init__(self, dict, paramset):
+        lens = MLKEM_LENGTHS[paramset]
+        self.pk = hex_to_bytes(dict["Public Key"], lens["pk_len"])
+        self.sk = hex_to_bytes(dict["Secret Key"], lens["sk_len"])
+        self.ct = hex_to_bytes(dict["Ciphertext"], lens["ct_len"])
+        assert dict["Shared Secret A"] == dict["Shared Secret B"]
+        self.ss = hex_to_bytes(dict["Shared Secret A"], 32)
+        self.ss_rejected = hex_to_bytes(dict["Pseudorandom shared Secret A"], 32)
+
+
+def load_testvecs(tvecs_file, paramset):
+    testvecs = []
+    cur = None
+    with open(tvecs_file) as f:
+        for line in f:
+            (name, value) = line.split(":")
+            if name == "Public Key":
+                if cur:
+                    testvecs.append(Testvec(cur, paramset))
+                cur = {}
+            cur[name] = value.strip()
+    if cur:
+        testvecs.append(Testvec(cur, paramset))
+    return testvecs
+
+
+def hash_testvecs(testvecs):
+    h = hashlib.shake_128()
+    for tv in testvecs:
+        h.update(tv.pk)
+        h.update(tv.sk)
+        h.update(tv.ct)
+        h.update(tv.ss)  # From encapsulation
+        h.update(tv.ss)  # From decapsulation
+        h.update(tv.ss_rejected)
+    return h.digest(length=32)
+
+
+if len(sys.argv) != 2:
+    sys.stderr.write(f"Usage: {SCRIPT_NAME} TESTVECS_DIR\n")
+    sys.exit(2)
+testvecs_dir = sys.argv[1]
+
+with open(LINUX_DIR + "/lib/crypto/tests/mlkem-testvecs.h", "w") as file:
+    print_header(file)
+    for paramset in [768, 1024]:
+        testvecs = load_testvecs(testvecs_dir + f"/tvecs{paramset}", paramset)
+        # There are 10000 test vectors, which take a bit long for a KUnit test.
+        # Trim them down to just the first 1000.
+        num_testvecs = min(len(testvecs), 1000)
+        testvecs = testvecs[:num_testvecs]
+        hash = hash_testvecs(testvecs)
+        print(f"\n#define MLKEM{paramset}_NUM_TESTVECS {num_testvecs}", file=file)
+        print_static_u8_array_definition(file, f"mlkem{paramset}_hash[32]", hash)
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 3/5] lib/crypto: mlkem: Add FIPS 140-3 tests
  2026-05-25 18:43 [PATCH 0/5] ML-KEM and X-Wing support Eric Biggers
  2026-05-25 18:43 ` [PATCH 1/5] lib/crypto: mlkem: Add ML-KEM-768 and ML-KEM-1024 support Eric Biggers
  2026-05-25 18:44 ` [PATCH 2/5] lib/crypto: mlkem: Add KUnit tests for ML-KEM Eric Biggers
@ 2026-05-25 18:44 ` Eric Biggers
  2026-05-25 18:44 ` [PATCH 4/5] lib/crypto: xwing: Add support for X-Wing KEM Eric Biggers
  2026-05-25 18:44 ` [PATCH 5/5] lib/crypto: xwing: Add KUnit tests " Eric Biggers
  4 siblings, 0 replies; 6+ messages in thread
From: Eric Biggers @ 2026-05-25 18:44 UTC (permalink / raw)
  To: linux-crypto
  Cc: linux-kernel, Ard Biesheuvel, Jason A . Donenfeld, Herbert Xu,
	Ryan Appel, Chris Leech, Eric Biggers

Add the ML-KEM self-tests required by FIPS 140-3:

  - Pairwise Consistency Test (PCT) at KeyGen time
  - Cryptographic Algorithm Self-Test (CAST) at initialization time

As with the other crypto algorithms, these are implemented separately
from the KUnit test suite, due to the very different requirements.

Signed-off-by: Eric Biggers <ebiggers@kernel.org>
---
 lib/crypto/fips-mlkem.h                 | 523 ++++++++++++++++++++++++
 lib/crypto/mlkem.c                      | 115 ++++++
 scripts/crypto/import-mlkem-testvecs.py |  62 +++
 3 files changed, 700 insertions(+)
 create mode 100644 lib/crypto/fips-mlkem.h

diff --git a/lib/crypto/fips-mlkem.h b/lib/crypto/fips-mlkem.h
new file mode 100644
index 000000000000..bb91c97d4b28
--- /dev/null
+++ b/lib/crypto/fips-mlkem.h
@@ -0,0 +1,523 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* This file was generated by import-mlkem-testvecs.py */
+/* clang-format off */
+
+static const u8 fips_test_mlkem768_seed[MLKEM_SEED_BYTES] __initconst __maybe_unused = {
+	0x7f, 0x9c, 0x2b, 0xa4, 0xe8, 0x8f, 0x82, 0x7d, 0x61, 0x60, 0x45, 0x50,
+	0x76, 0x05, 0x85, 0x3e, 0xd7, 0x3b, 0x80, 0x93, 0xf6, 0xef, 0xbc, 0x88,
+	0xeb, 0x1a, 0x6e, 0xac, 0xfa, 0x66, 0xef, 0x26, 0x3c, 0xb1, 0xee, 0xa9,
+	0x88, 0x00, 0x4b, 0x93, 0x10, 0x3c, 0xfb, 0x0a, 0xee, 0xfd, 0x2a, 0x68,
+	0x6e, 0x01, 0xfa, 0x4a, 0x58, 0xe8, 0xa3, 0x63, 0x9c, 0xa8, 0xa1, 0xe3,
+	0xf9, 0xae, 0x57, 0xe2,
+};
+
+static const u8 fips_test_mlkem768_eseed[MLKEM_ESEED_BYTES] __initconst __maybe_unused = {
+	0x35, 0xb8, 0xcc, 0x87, 0x3c, 0x23, 0xdc, 0x62, 0xb8, 0xd2, 0x60, 0x16,
+	0x9a, 0xfa, 0x2f, 0x75, 0xab, 0x91, 0x6a, 0x58, 0xd9, 0x74, 0x91, 0x88,
+	0x35, 0xd2, 0x5e, 0x6a, 0x43, 0x50, 0x85, 0xb2,
+};
+
+static const u8 fips_test_mlkem768_pk[MLKEM768_PUBLIC_KEY_BYTES] __initconst __maybe_unused = {
+	0x78, 0x20, 0x32, 0x02, 0x30, 0x23, 0x8e, 0x44, 0x7a, 0xcf, 0xa9, 0x9b,
+	0x63, 0x32, 0xb7, 0x53, 0x1c, 0x7c, 0xe5, 0x42, 0x03, 0x1b, 0x93, 0xca,
+	0x14, 0x25, 0x8f, 0x5f, 0x98, 0xb3, 0x0c, 0x87, 0x3f, 0x6d, 0x01, 0xb2,
+	0xc5, 0x85, 0x3f, 0x34, 0x02, 0x69, 0x18, 0x04, 0x80, 0x47, 0x08, 0xa7,
+	0xec, 0xfa, 0x5b, 0xa5, 0x98, 0x0f, 0xef, 0x88, 0x36, 0xce, 0xf5, 0xa8,
+	0x2d, 0xac, 0x83, 0x75, 0xe0, 0x62, 0x34, 0xcb, 0xbe, 0xd6, 0x4c, 0xc5,
+	0xa4, 0xb2, 0xac, 0x06, 0x84, 0xa0, 0xb4, 0xd9, 0x86, 0x3e, 0x66, 0x44,
+	0xa8, 0x33, 0x48, 0x49, 0xc0, 0xcf, 0xdb, 0xfb, 0x85, 0x1b, 0x84, 0xb0,
+	0xe4, 0x8c, 0x6f, 0xb0, 0x48, 0x01, 0xd0, 0x52, 0x2f, 0xd5, 0x76, 0x68,
+	0x9b, 0x16, 0x44, 0x67, 0x60, 0x4a, 0xb4, 0xda, 0x2e, 0x34, 0xab, 0xcf,
+	0x76, 0xdc, 0x92, 0x44, 0x67, 0x4c, 0x4e, 0x57, 0x30, 0xf5, 0x78, 0x7c,
+	0x67, 0x90, 0x21, 0x42, 0x4c, 0x13, 0x40, 0x15, 0x4d, 0x24, 0x49, 0x33,
+	0x0d, 0xbc, 0x94, 0x0e, 0xba, 0x7b, 0xe1, 0x03, 0x99, 0xec, 0x94, 0x0a,
+	0x6b, 0x35, 0x40, 0x60, 0x14, 0x99, 0x66, 0x50, 0xb1, 0x48, 0xa1, 0x89,
+	0x43, 0x47, 0x7f, 0x5e, 0x95, 0x0f, 0x41, 0xa0, 0x1a, 0xb6, 0x34, 0x88,
+	0xfa, 0xf2, 0x06, 0xf0, 0xf2, 0x25, 0xa7, 0x31, 0x09, 0x67, 0x77, 0x84,
+	0x79, 0x1b, 0x6d, 0xec, 0x98, 0x0e, 0x60, 0xeb, 0x8f, 0x96, 0xbb, 0xbf,
+	0x47, 0x22, 0x22, 0xb0, 0x24, 0x8a, 0x12, 0xf9, 0x61, 0x88, 0xa1, 0x7d,
+	0x68, 0xb8, 0x3e, 0x52, 0xc9, 0x82, 0x7b, 0xa2, 0xc6, 0xbc, 0x82, 0x75,
+	0x0b, 0x31, 0xae, 0x46, 0xb9, 0x82, 0x7a, 0x28, 0x77, 0xfc, 0xb3, 0x38,
+	0x00, 0x3a, 0x68, 0xb8, 0xc1, 0xaa, 0x9c, 0x96, 0x6b, 0x55, 0xd2, 0x03,
+	0x78, 0x74, 0x11, 0x57, 0x77, 0x05, 0xf6, 0xdb, 0x15, 0x59, 0xf9, 0xb4,
+	0x25, 0x8a, 0x57, 0x18, 0x20, 0x50, 0x5d, 0x83, 0xc1, 0x00, 0x9a, 0xa2,
+	0xb2, 0x84, 0xb3, 0x75, 0xa7, 0xac, 0x61, 0x6a, 0x1b, 0x29, 0x42, 0xa2,
+	0xb4, 0x94, 0xc4, 0x3e, 0xb3, 0x70, 0xc1, 0xb9, 0x4a, 0xcd, 0x52, 0x2b,
+	0x20, 0x8c, 0x17, 0xc7, 0x35, 0x07, 0x7e, 0x60, 0x2f, 0x41, 0x41, 0x0c,
+	0xfb, 0x1b, 0x3e, 0xa1, 0x09, 0x21, 0x58, 0xc3, 0xb6, 0xea, 0x43, 0xcc,
+	0x7a, 0xa7, 0x03, 0x09, 0xa6, 0x0f, 0xa9, 0x64, 0x47, 0x21, 0x33, 0x2a,
+	0x0d, 0xc4, 0x55, 0xbd, 0xb5, 0x63, 0xd5, 0xe6, 0x6e, 0xe2, 0x12, 0x10,
+	0x87, 0x2a, 0x59, 0x8d, 0x13, 0xa9, 0x51, 0x83, 0x11, 0x9a, 0x50, 0x7a,
+	0x76, 0x34, 0x3b, 0x45, 0x4b, 0xb3, 0x20, 0x4b, 0xad, 0x83, 0x46, 0x9b,
+	0x56, 0x27, 0x52, 0xa7, 0x3a, 0x00, 0xe5, 0xe8, 0xc5, 0xa6, 0xd9, 0x87,
+	0x0a, 0xca, 0x93, 0x78, 0x69, 0x05, 0xc2, 0x0b, 0x85, 0x02, 0xf4, 0x5d,
+	0x91, 0x14, 0xa8, 0xd2, 0xc7, 0xbd, 0x2e, 0xe0, 0x08, 0x7f, 0xb7, 0x80,
+	0xfa, 0x50, 0x78, 0x58, 0x8b, 0x02, 0x6f, 0x14, 0x19, 0x6c, 0xc0, 0x68,
+	0xf6, 0xa7, 0x5e, 0x7b, 0x4a, 0x44, 0x3a, 0xea, 0x41, 0x42, 0x5c, 0xaf,
+	0x2d, 0xa2, 0x12, 0xa3, 0xe6, 0xad, 0xf6, 0x44, 0x51, 0x64, 0xc2, 0x90,
+	0x8f, 0x40, 0x9d, 0x43, 0x96, 0x5e, 0xe4, 0x0b, 0x89, 0xd6, 0x54, 0xc8,
+	0x41, 0x30, 0x97, 0x48, 0x6a, 0x9b, 0x90, 0xe2, 0xb0, 0x44, 0x74, 0x38,
+	0x91, 0x18, 0x36, 0xb7, 0xca, 0x84, 0xe7, 0x6b, 0x7a, 0x8e, 0xe6, 0x00,
+	0xa1, 0x3b, 0x55, 0xa8, 0xb8, 0x3d, 0xa0, 0xb2, 0x08, 0x25, 0x38, 0x2b,
+	0xda, 0xc3, 0xbe, 0x25, 0x04, 0x60, 0x24, 0xc7, 0x88, 0x76, 0x40, 0x53,
+	0xf1, 0x92, 0x2c, 0xfa, 0xa2, 0xa1, 0xb8, 0x16, 0x22, 0x30, 0xa3, 0x6d,
+	0xbb, 0x36, 0xcf, 0x8e, 0x62, 0xc8, 0xa1, 0xb1, 0x5f, 0x9d, 0x51, 0x90,
+	0x07, 0x0a, 0xb4, 0xf5, 0x70, 0xa3, 0xba, 0xf8, 0x63, 0x7c, 0x04, 0xad,
+	0x19, 0x51, 0xa4, 0xb4, 0x61, 0x0b, 0x5b, 0x7a, 0xb2, 0x33, 0x56, 0xc3,
+	0x9e, 0x36, 0xa8, 0x12, 0x85, 0x58, 0xa2, 0x59, 0x14, 0xe1, 0xa8, 0xaf,
+	0x76, 0xe4, 0x77, 0x12, 0xf8, 0x54, 0x38, 0xb1, 0x9e, 0x8d, 0x04, 0xce,
+	0x92, 0x35, 0x20, 0xd3, 0xab, 0x0a, 0xc4, 0xe4, 0x08, 0x58, 0xf2, 0x36,
+	0x2d, 0xd4, 0x9d, 0xb4, 0x46, 0xb7, 0xed, 0xf7, 0xa0, 0xdd, 0x4c, 0x83,
+	0xaa, 0x30, 0x7e, 0xfa, 0x87, 0x76, 0xb3, 0xb8, 0xc7, 0xa1, 0x0a, 0x1f,
+	0xdf, 0xe6, 0x8e, 0xda, 0x1b, 0x9c, 0xd4, 0x2a, 0xc0, 0xce, 0xc4, 0x76,
+	0x0c, 0x25, 0x93, 0x18, 0x08, 0x2c, 0x18, 0x41, 0x5d, 0x2c, 0xc2, 0x93,
+	0x67, 0xf6, 0x06, 0xee, 0xea, 0x1c, 0x01, 0x45, 0x38, 0x9e, 0xa2, 0x88,
+	0xf3, 0x83, 0xa6, 0x8d, 0xd6, 0x20, 0x12, 0x7a, 0xac, 0xa8, 0x8b, 0xcb,
+	0x6b, 0x42, 0x45, 0xa6, 0x64, 0x4f, 0x05, 0xa6, 0xb9, 0xf1, 0x16, 0x3d,
+	0x38, 0xe1, 0x7e, 0x57, 0xcb, 0x27, 0xcc, 0x5c, 0x46, 0x7b, 0x2c, 0xca,
+	0x94, 0xf9, 0x74, 0xcf, 0xd8, 0x15, 0x0d, 0x52, 0x94, 0xe1, 0x52, 0x20,
+	0x0b, 0x39, 0xba, 0xfa, 0x46, 0x9b, 0x9c, 0xfa, 0xc3, 0xac, 0xeb, 0x7b,
+	0x56, 0xb4, 0x7b, 0x13, 0xf1, 0x71, 0x81, 0xea, 0x95, 0x87, 0x78, 0x10,
+	0xa7, 0x77, 0xae, 0x9d, 0x15, 0x60, 0xed, 0x51, 0x19, 0xab, 0x09, 0xb7,
+	0x6a, 0xb1, 0x20, 0xe3, 0xc6, 0x19, 0x61, 0xd5, 0x8c, 0x3a, 0x93, 0x16,
+	0x69, 0xa0, 0x5f, 0xd2, 0x88, 0x2f, 0x51, 0x97, 0xb7, 0xc3, 0xb0, 0x47,
+	0xa1, 0x97, 0xaa, 0x82, 0x0c, 0x20, 0x8a, 0x5c, 0xb5, 0xbc, 0x40, 0xbe,
+	0xb8, 0x25, 0x88, 0xf5, 0x61, 0x0b, 0x0b, 0x35, 0x4e, 0xb8, 0x50, 0x39,
+	0xb0, 0xa4, 0x1d, 0xfd, 0x39, 0x24, 0x8f, 0x16, 0x5a, 0xbb, 0x02, 0x7a,
+	0xfd, 0x68, 0x17, 0xe0, 0x0c, 0x8d, 0xad, 0xaa, 0xcb, 0x29, 0xcb, 0x25,
+	0xe4, 0x24, 0x75, 0x09, 0x32, 0xae, 0x27, 0x75, 0x6a, 0x98, 0xb7, 0xb7,
+	0x49, 0x28, 0x53, 0x90, 0x9c, 0xa9, 0x5a, 0x56, 0x92, 0x2a, 0x13, 0x36,
+	0x90, 0x6c, 0x02, 0xb8, 0x12, 0x05, 0xbb, 0x58, 0x69, 0x4d, 0xe8, 0xc0,
+	0x2a, 0x00, 0x5c, 0x23, 0x26, 0x09, 0xc8, 0x79, 0x97, 0xb1, 0x6a, 0xb6,
+	0xbc, 0x79, 0x0d, 0xd3, 0x29, 0x30, 0x03, 0xb9, 0xb4, 0x86, 0x7b, 0x2b,
+	0x6e, 0x04, 0xc4, 0x56, 0x18, 0x50, 0x35, 0x94, 0xaa, 0x26, 0x9c, 0x9d,
+	0x4f, 0xc3, 0x9a, 0xa1, 0xec, 0x36, 0x3f, 0xbc, 0x57, 0xc7, 0x02, 0x41,
+	0x31, 0x0b, 0x1d, 0x6b, 0x44, 0x98, 0xe8, 0xa5, 0x56, 0xc4, 0x87, 0xb1,
+	0x6e, 0x45, 0xaa, 0x9b, 0xb9, 0xb1, 0xa6, 0xa9, 0xbb, 0x2a, 0x44, 0x6f,
+	0x6e, 0xe3, 0x09, 0xed, 0xec, 0x1d, 0x55, 0x32, 0x99, 0x7a, 0x86, 0x94,
+	0xdd, 0xb9, 0x91, 0xe3, 0xa6, 0x77, 0xf8, 0x95, 0x88, 0x5f, 0xa9, 0x34,
+	0x81, 0xea, 0x21, 0x7e, 0xba, 0x1c, 0x47, 0x4b, 0xc4, 0x81, 0x88, 0x14,
+	0x7f, 0xdc, 0x03, 0x22, 0x81, 0xcc, 0xa5, 0xc3, 0x0d, 0x5b, 0x1c, 0x3e,
+	0xd9, 0x09, 0x32, 0xbd, 0xe1, 0x48, 0x55, 0x92, 0x9f, 0x5e, 0x22, 0x4f,
+	0x27, 0x38, 0x75, 0x36, 0x4b, 0x60, 0x0c, 0x7c, 0x91, 0x6b, 0xd3, 0x1f,
+	0x66, 0xd7, 0xbb, 0xc4, 0x70, 0xc5, 0x36, 0xe6, 0x00, 0xe8, 0xda, 0x7f,
+	0x31, 0xfb, 0x3b, 0x99, 0x45, 0x73, 0xe7, 0x77, 0x96, 0xc5, 0x39, 0x69,
+	0x37, 0x1a, 0x5e, 0x30, 0xc3, 0x71, 0xa0, 0x32, 0x50, 0xe4, 0x26, 0xc2,
+	0xdb, 0xda, 0x9e, 0xee, 0xd3, 0x9b, 0x6b, 0x65, 0x03, 0x38, 0x80, 0x66,
+	0x05, 0xd3, 0x5e, 0x64, 0xd5, 0x59, 0xd0, 0x91, 0x0d, 0x2e, 0xf7, 0x51,
+	0xaf, 0xa8, 0x33, 0x00, 0x2c, 0x90, 0x5d, 0xb5, 0x5e, 0xe0, 0x09, 0x81,
+	0x75, 0xf2, 0x11, 0x11, 0x82, 0x2f, 0x6a, 0x9c, 0x1a, 0xab, 0xcb, 0x97,
+	0x71, 0xcc, 0x15, 0x3b, 0xb4, 0xb6, 0x19, 0xd7, 0x35, 0xfa, 0x82, 0x23,
+	0xd5, 0xf0, 0x6a, 0x8b, 0x2b, 0x79, 0x08, 0xe3, 0x2d, 0x8f, 0x7b, 0x82,
+	0x9d, 0xac, 0x88, 0xc9, 0xca, 0xa4, 0x74, 0x2a, 0x12, 0xa2, 0xa4, 0x4b,
+	0xc6, 0x14, 0x20, 0xf7, 0xd2, 0x6b, 0x87, 0xc3, 0xc8, 0x33, 0xc5, 0x6c,
+	0xb5, 0x1a, 0xcc, 0x1b, 0xd4, 0x8c, 0x9a, 0x64, 0x8b, 0x4f, 0x60, 0x8e,
+	0x4f, 0xeb, 0x37, 0x8e, 0x65, 0x54, 0xb7, 0x3c, 0xa8, 0xe3, 0x7b, 0x9d,
+	0x5f, 0xa7, 0x6b, 0x44, 0x1c, 0x03, 0x97, 0xf8, 0xc0, 0xaa, 0x39, 0xb2,
+	0x34, 0x8a, 0xbc, 0x3c, 0x6d, 0x0d, 0x13, 0xa6, 0xbd, 0x91, 0xd6, 0x2a,
+	0x43, 0xc6, 0x51, 0x60, 0x63, 0xdf, 0xd5, 0xbf, 0x77, 0xf0, 0xf1, 0x08,
+	0x42, 0x24, 0x43, 0x7b, 0x47, 0xa8, 0x2b, 0xa2,
+};
+
+static const u8 fips_test_mlkem768_sk[MLKEM768_SECRET_KEY_BYTES] __initconst __maybe_unused = {
+	0xe7, 0xfa, 0x46, 0xcd, 0x07, 0xc1, 0xb4, 0xda, 0x48, 0xcb, 0x48, 0xa3,
+	0xaf, 0xe1, 0x47, 0x9f, 0x96, 0x71, 0x1b, 0xac, 0x5c, 0xd2, 0x62, 0xc4,
+	0x3d, 0xaa, 0xc5, 0xbc, 0x68, 0x66, 0x2d, 0x5c, 0x5d, 0x0b, 0x83, 0x55,
+	0x1b, 0xda, 0xb9, 0xd1, 0x48, 0xa4, 0xd7, 0xe4, 0x3d, 0xc6, 0x16, 0x56,
+	0x1f, 0xb2, 0x7f, 0x58, 0x42, 0x83, 0x8c, 0x71, 0x47, 0x22, 0x45, 0x6c,
+	0x4c, 0x7c, 0x44, 0xb2, 0x48, 0xa8, 0xe0, 0x66, 0x37, 0xad, 0x40, 0x30,
+	0xe5, 0x39, 0x5b, 0xd2, 0xa7, 0x81, 0x36, 0x1a, 0x9e, 0x23, 0x61, 0xc1,
+	0x19, 0x38, 0x39, 0x04, 0x87, 0x81, 0x2e, 0xdb, 0x21, 0xbd, 0x04, 0x8c,
+	0x45, 0xe9, 0x83, 0xb2, 0x33, 0x94, 0x98, 0x5c, 0x5a, 0x2b, 0x60, 0x7e,
+	0xfc, 0x97, 0x50, 0x57, 0x3c, 0x7e, 0x9f, 0x74, 0x90, 0xce, 0x42, 0x66,
+	0x79, 0x59, 0x92, 0x97, 0xc5, 0x8c, 0xe6, 0x42, 0x0f, 0x2a, 0x67, 0x33,
+	0xb9, 0x01, 0x81, 0x9a, 0xe2, 0x59, 0x9e, 0x50, 0x70, 0xf9, 0xf0, 0x77,
+	0xb9, 0xe0, 0xb9, 0x94, 0x73, 0x80, 0x55, 0x2c, 0xc4, 0xb6, 0x24, 0x2b,
+	0x30, 0xf2, 0x88, 0x46, 0xd5, 0x0e, 0x62, 0x38, 0xbd, 0xcb, 0x0b, 0x74,
+	0x2f, 0x17, 0x5c, 0xfe, 0xbb, 0xab, 0xc6, 0x47, 0xa2, 0xf0, 0x32, 0x93,
+	0x99, 0x8c, 0x7d, 0x96, 0x41, 0x13, 0xdd, 0x5a, 0x79, 0xe4, 0x7b, 0x40,
+	0x7c, 0x6b, 0x9c, 0x23, 0x25, 0x2d, 0xa4, 0xf3, 0x40, 0x1e, 0xb5, 0x02,
+	0x1f, 0x3c, 0xc1, 0xfd, 0x43, 0xc7, 0x87, 0x01, 0xb9, 0x72, 0x87, 0x71,
+	0x00, 0x12, 0xb9, 0x85, 0x44, 0xac, 0x0f, 0x35, 0xc0, 0x78, 0x8b, 0x7a,
+	0xa5, 0x14, 0x94, 0x5d, 0x41, 0x80, 0xce, 0x44, 0x9d, 0x12, 0x28, 0x01,
+	0x77, 0x79, 0xb7, 0xf3, 0xb5, 0x16, 0xf1, 0x97, 0x58, 0x5a, 0x75, 0x29,
+	0x90, 0x2a, 0x57, 0xd3, 0x08, 0xc0, 0x00, 0xc1, 0x42, 0xe3, 0x96, 0x08,
+	0xd3, 0xba, 0x70, 0x81, 0x21, 0x8b, 0x3a, 0x1a, 0x29, 0xe4, 0xe3, 0x9e,
+	0x4f, 0x21, 0x0c, 0x27, 0xa9, 0x2c, 0x30, 0xd0, 0xbf, 0x61, 0x1a, 0x28,
+	0x03, 0x3a, 0x44, 0xc7, 0x13, 0xa1, 0x6a, 0xb2, 0x0a, 0x59, 0x71, 0x7d,
+	0xa1, 0xe7, 0x1a, 0xae, 0x78, 0x67, 0x5b, 0xfc, 0x66, 0xa3, 0x97, 0x6d,
+	0xfb, 0xac, 0x7e, 0xab, 0xfc, 0x89, 0x61, 0x4a, 0x24, 0xe7, 0xe0, 0x9e,
+	0xf7, 0xe4, 0x48, 0xef, 0xc5, 0x28, 0xa8, 0x23, 0x30, 0xc2, 0xcb, 0xa2,
+	0x12, 0x8b, 0xc8, 0x17, 0x2a, 0xb6, 0x97, 0x88, 0xba, 0xee, 0x2c, 0x8f,
+	0x72, 0x1a, 0x30, 0x81, 0xb3, 0xb7, 0x83, 0x5c, 0x9d, 0xfd, 0xf9, 0xc8,
+	0x37, 0x25, 0xa9, 0x42, 0xfc, 0xb0, 0xe4, 0x06, 0x45, 0xeb, 0x4b, 0x25,
+	0x8d, 0x62, 0x2c, 0x7c, 0xd5, 0x78, 0x0b, 0x27, 0x09, 0x14, 0x42, 0x69,
+	0xeb, 0x82, 0x3f, 0xa8, 0xa8, 0x55, 0x40, 0xa2, 0x74, 0x9a, 0xa1, 0x71,
+	0x31, 0x85, 0x11, 0x94, 0xbc, 0x1d, 0xd9, 0x07, 0x08, 0xf8, 0x09, 0xc3,
+	0x0d, 0xd8, 0x99, 0x66, 0x82, 0x23, 0xdd, 0x20, 0x06, 0xaf, 0x02, 0x42,
+	0x39, 0x50, 0x0c, 0x7d, 0x85, 0x6f, 0xef, 0xb7, 0x16, 0xcd, 0x98, 0x8a,
+	0x2d, 0xea, 0x8a, 0x2f, 0xc1, 0x03, 0x6a, 0x22, 0x3e, 0x24, 0x01, 0x80,
+	0x99, 0x91, 0x22, 0x65, 0x5a, 0xcc, 0xee, 0xa5, 0x12, 0xc1, 0x42, 0xb2,
+	0xf9, 0xd7, 0xc1, 0xd3, 0x92, 0x1a, 0x34, 0xe9, 0x49, 0x04, 0xf0, 0x82,
+	0x97, 0xd8, 0x9d, 0xd9, 0xf6, 0x3f, 0xd3, 0x6b, 0xa8, 0x03, 0xf9, 0x9b,
+	0x1a, 0xf5, 0x19, 0x71, 0xf5, 0xaf, 0xde, 0xf3, 0x09, 0x61, 0x8a, 0x8b,
+	0x9c, 0xc2, 0x5f, 0xf4, 0x10, 0x90, 0x0a, 0x43, 0x71, 0x8a, 0xb1, 0xbc,
+	0x4a, 0xc2, 0xac, 0x9a, 0x57, 0x61, 0x7f, 0x15, 0x0e, 0x2a, 0xc8, 0x3a,
+	0x06, 0x03, 0x2e, 0x37, 0x3a, 0x92, 0xf1, 0x5a, 0xc4, 0xaa, 0xa8, 0x70,
+	0x76, 0xe7, 0x2c, 0xa5, 0x21, 0x12, 0xda, 0x95, 0xc0, 0x0c, 0x93, 0x28,
+	0xea, 0xfa, 0x70, 0x89, 0x01, 0x32, 0x4a, 0xa3, 0x4d, 0xe9, 0xc3, 0x92,
+	0x78, 0x83, 0x49, 0x2d, 0x79, 0x1a, 0x97, 0x45, 0x2d, 0x9d, 0x18, 0x3c,
+	0x37, 0xb4, 0x45, 0x6c, 0x84, 0x21, 0x13, 0xac, 0x6c, 0xfe, 0x59, 0x7e,
+	0x51, 0xe4, 0x82, 0x34, 0xe0, 0x99, 0x7e, 0xe9, 0xcf, 0x45, 0x6a, 0x5f,
+	0xa3, 0x26, 0x40, 0xe1, 0xfb, 0x1e, 0xa6, 0x49, 0x14, 0xff, 0x62, 0x68,
+	0xe1, 0x2b, 0x65, 0x75, 0xd5, 0xb8, 0x2e, 0x2b, 0x81, 0x7a, 0x50, 0x1d,
+	0x2b, 0x07, 0x4f, 0x91, 0xfb, 0x3c, 0xa2, 0xac, 0x6e, 0x2a, 0x39, 0x3e,
+	0xa6, 0xc1, 0x52, 0x7f, 0x95, 0x0c, 0xdd, 0xa9, 0x2d, 0xe8, 0xa1, 0xa7,
+	0x60, 0x10, 0x36, 0xa7, 0x33, 0xb5, 0x6d, 0x4b, 0x51, 0xfc, 0xac, 0xc7,
+	0x03, 0xd1, 0x25, 0xdc, 0x87, 0x12, 0x68, 0xc5, 0x93, 0xb1, 0xb7, 0x97,
+	0x47, 0x00, 0x15, 0xe7, 0xba, 0x5c, 0x6d, 0x07, 0x5a, 0x0d, 0x93, 0x56,
+	0xac, 0xb0, 0x5a, 0x1a, 0x80, 0x62, 0x5e, 0xb7, 0xbb, 0x0e, 0x6b, 0xc4,
+	0xdb, 0xe0, 0xb4, 0x6e, 0x2c, 0x81, 0x53, 0x5c, 0xcd, 0x9b, 0xb2, 0xa5,
+	0x4f, 0x02, 0x9d, 0xb1, 0x8b, 0x3c, 0xf1, 0x6c, 0xc3, 0x03, 0x00, 0xbe,
+	0x16, 0x73, 0x0e, 0x39, 0x49, 0x57, 0xcf, 0xe7, 0xa3, 0x88, 0xfc, 0x07,
+	0xf8, 0x58, 0x3e, 0xac, 0xb0, 0x1f, 0x22, 0x91, 0xc0, 0xf3, 0xf9, 0x13,
+	0xb3, 0x6a, 0x0e, 0x3f, 0x85, 0x43, 0x6d, 0x78, 0x43, 0xcc, 0x49, 0x23,
+	0x1e, 0x09, 0x16, 0x70, 0x3b, 0x79, 0x1c, 0x72, 0x0e, 0xed, 0xa5, 0x5b,
+	0x47, 0xaa, 0xbb, 0xcc, 0x4c, 0x76, 0x72, 0x82, 0x84, 0xee, 0x39, 0x3b,
+	0xb7, 0x81, 0xab, 0x3e, 0xbc, 0x59, 0x60, 0x94, 0x96, 0xac, 0xbc, 0x44,
+	0x22, 0x9a, 0x61, 0xf5, 0xa1, 0x6c, 0x9d, 0xec, 0x6d, 0x0f, 0x5c, 0x81,
+	0xb9, 0x9c, 0x0f, 0xbe, 0xab, 0x47, 0xe6, 0xdc, 0x92, 0xb7, 0xa7, 0x92,
+	0xa7, 0xa1, 0x15, 0xc5, 0xf3, 0x8b, 0x22, 0x3b, 0x3e, 0x3e, 0xa1, 0x43,
+	0xd2, 0x09, 0x83, 0xff, 0x1b, 0x8b, 0xd3, 0x75, 0x1e, 0x27, 0x6a, 0x35,
+	0x95, 0xc3, 0x36, 0xfc, 0x38, 0x9d, 0x46, 0x87, 0xb1, 0xf8, 0x03, 0x24,
+	0x37, 0x98, 0xca, 0x37, 0x35, 0x80, 0x9f, 0x06, 0x14, 0xe1, 0xd8, 0x6d,
+	0x1d, 0xab, 0x06, 0xce, 0xe8, 0x95, 0xaa, 0xa7, 0x46, 0xeb, 0xb5, 0x96,
+	0xce, 0x12, 0x68, 0xa5, 0xc3, 0xcd, 0xbf, 0xb2, 0x57, 0xe9, 0xfc, 0xa7,
+	0x52, 0x13, 0x38, 0x70, 0x33, 0x0c, 0x8e, 0x7c, 0x22, 0x29, 0xe4, 0x21,
+	0x29, 0xe1, 0x58, 0x60, 0xb5, 0xc7, 0xe9, 0x17, 0xaf, 0x4b, 0x30, 0x42,
+	0x9c, 0xdc, 0xb6, 0x86, 0x57, 0x54, 0x6c, 0x06, 0x73, 0xe5, 0xa8, 0x46,
+	0xec, 0xa7, 0x64, 0xcc, 0x1c, 0xb1, 0x77, 0xb1, 0x3b, 0x9d, 0x33, 0x21,
+	0x3e, 0x20, 0x39, 0xe8, 0xf1, 0x8b, 0x80, 0xd2, 0x8e, 0xc0, 0x73, 0x7c,
+	0xa0, 0x5c, 0x10, 0x14, 0x5b, 0x00, 0xd0, 0x44, 0x6e, 0x49, 0xf3, 0x0e,
+	0xfa, 0x23, 0x9e, 0xef, 0x00, 0x7f, 0x37, 0x82, 0x85, 0x92, 0x75, 0x04,
+	0xa3, 0x25, 0x89, 0xf4, 0x3a, 0x77, 0x72, 0xe3, 0x6c, 0x52, 0x05, 0x9c,
+	0xad, 0xe9, 0x54, 0x08, 0xf8, 0x17, 0x9f, 0x89, 0x4e, 0xc2, 0xf5, 0xc7,
+	0xef, 0xda, 0x05, 0x14, 0x37, 0x58, 0x09, 0xf6, 0x2d, 0x93, 0x11, 0xa7,
+	0x79, 0xbb, 0xb1, 0xec, 0x50, 0x35, 0x5a, 0x87, 0x75, 0xa6, 0xc7, 0x8b,
+	0xe5, 0x63, 0xc5, 0x3f, 0xfb, 0x51, 0x9b, 0xa4, 0x9f, 0x60, 0x21, 0xc5,
+	0xd7, 0xca, 0x5f, 0x94, 0x13, 0x70, 0x00, 0x48, 0x40, 0x58, 0x4a, 0x7b,
+	0x58, 0x78, 0xa3, 0xfc, 0x38, 0x49, 0x3a, 0xd0, 0x72, 0x99, 0xb5, 0x58,
+	0x81, 0x62, 0x83, 0xd3, 0x7a, 0x03, 0x08, 0x76, 0xb3, 0xcf, 0x27, 0xa9,
+	0x88, 0xc8, 0xae, 0x35, 0xdb, 0x0d, 0x50, 0x6a, 0xa8, 0xa2, 0x31, 0x4f,
+	0x3a, 0x6b, 0x5f, 0x04, 0x37, 0x54, 0xbe, 0x51, 0xb8, 0x92, 0xb7, 0x02,
+	0x5a, 0x06, 0x50, 0xee, 0x40, 0xa5, 0xff, 0x76, 0x6c, 0x63, 0x09, 0xc9,
+	0x26, 0x5c, 0x16, 0x61, 0xca, 0x5a, 0xe5, 0xb1, 0xaa, 0xe1, 0xf3, 0x84,
+	0xd3, 0x29, 0x25, 0x9a, 0xf7, 0x4c, 0xed, 0xc6, 0x0f, 0x29, 0xfc, 0x04,
+	0xb2, 0x28, 0x24, 0xe3, 0x78, 0x86, 0xed, 0x08, 0x49, 0x5c, 0xd6, 0xb5,
+	0x50, 0x4c, 0xbe, 0x0e, 0x13, 0xcc, 0x94, 0x71, 0xcf, 0x99, 0x04, 0x3e,
+	0x6e, 0x75, 0x07, 0x83, 0x2c, 0x1b, 0x47, 0xcb, 0xac, 0x22, 0x68, 0xa8,
+	0x78, 0x20, 0x32, 0x02, 0x30, 0x23, 0x8e, 0x44, 0x7a, 0xcf, 0xa9, 0x9b,
+	0x63, 0x32, 0xb7, 0x53, 0x1c, 0x7c, 0xe5, 0x42, 0x03, 0x1b, 0x93, 0xca,
+	0x14, 0x25, 0x8f, 0x5f, 0x98, 0xb3, 0x0c, 0x87, 0x3f, 0x6d, 0x01, 0xb2,
+	0xc5, 0x85, 0x3f, 0x34, 0x02, 0x69, 0x18, 0x04, 0x80, 0x47, 0x08, 0xa7,
+	0xec, 0xfa, 0x5b, 0xa5, 0x98, 0x0f, 0xef, 0x88, 0x36, 0xce, 0xf5, 0xa8,
+	0x2d, 0xac, 0x83, 0x75, 0xe0, 0x62, 0x34, 0xcb, 0xbe, 0xd6, 0x4c, 0xc5,
+	0xa4, 0xb2, 0xac, 0x06, 0x84, 0xa0, 0xb4, 0xd9, 0x86, 0x3e, 0x66, 0x44,
+	0xa8, 0x33, 0x48, 0x49, 0xc0, 0xcf, 0xdb, 0xfb, 0x85, 0x1b, 0x84, 0xb0,
+	0xe4, 0x8c, 0x6f, 0xb0, 0x48, 0x01, 0xd0, 0x52, 0x2f, 0xd5, 0x76, 0x68,
+	0x9b, 0x16, 0x44, 0x67, 0x60, 0x4a, 0xb4, 0xda, 0x2e, 0x34, 0xab, 0xcf,
+	0x76, 0xdc, 0x92, 0x44, 0x67, 0x4c, 0x4e, 0x57, 0x30, 0xf5, 0x78, 0x7c,
+	0x67, 0x90, 0x21, 0x42, 0x4c, 0x13, 0x40, 0x15, 0x4d, 0x24, 0x49, 0x33,
+	0x0d, 0xbc, 0x94, 0x0e, 0xba, 0x7b, 0xe1, 0x03, 0x99, 0xec, 0x94, 0x0a,
+	0x6b, 0x35, 0x40, 0x60, 0x14, 0x99, 0x66, 0x50, 0xb1, 0x48, 0xa1, 0x89,
+	0x43, 0x47, 0x7f, 0x5e, 0x95, 0x0f, 0x41, 0xa0, 0x1a, 0xb6, 0x34, 0x88,
+	0xfa, 0xf2, 0x06, 0xf0, 0xf2, 0x25, 0xa7, 0x31, 0x09, 0x67, 0x77, 0x84,
+	0x79, 0x1b, 0x6d, 0xec, 0x98, 0x0e, 0x60, 0xeb, 0x8f, 0x96, 0xbb, 0xbf,
+	0x47, 0x22, 0x22, 0xb0, 0x24, 0x8a, 0x12, 0xf9, 0x61, 0x88, 0xa1, 0x7d,
+	0x68, 0xb8, 0x3e, 0x52, 0xc9, 0x82, 0x7b, 0xa2, 0xc6, 0xbc, 0x82, 0x75,
+	0x0b, 0x31, 0xae, 0x46, 0xb9, 0x82, 0x7a, 0x28, 0x77, 0xfc, 0xb3, 0x38,
+	0x00, 0x3a, 0x68, 0xb8, 0xc1, 0xaa, 0x9c, 0x96, 0x6b, 0x55, 0xd2, 0x03,
+	0x78, 0x74, 0x11, 0x57, 0x77, 0x05, 0xf6, 0xdb, 0x15, 0x59, 0xf9, 0xb4,
+	0x25, 0x8a, 0x57, 0x18, 0x20, 0x50, 0x5d, 0x83, 0xc1, 0x00, 0x9a, 0xa2,
+	0xb2, 0x84, 0xb3, 0x75, 0xa7, 0xac, 0x61, 0x6a, 0x1b, 0x29, 0x42, 0xa2,
+	0xb4, 0x94, 0xc4, 0x3e, 0xb3, 0x70, 0xc1, 0xb9, 0x4a, 0xcd, 0x52, 0x2b,
+	0x20, 0x8c, 0x17, 0xc7, 0x35, 0x07, 0x7e, 0x60, 0x2f, 0x41, 0x41, 0x0c,
+	0xfb, 0x1b, 0x3e, 0xa1, 0x09, 0x21, 0x58, 0xc3, 0xb6, 0xea, 0x43, 0xcc,
+	0x7a, 0xa7, 0x03, 0x09, 0xa6, 0x0f, 0xa9, 0x64, 0x47, 0x21, 0x33, 0x2a,
+	0x0d, 0xc4, 0x55, 0xbd, 0xb5, 0x63, 0xd5, 0xe6, 0x6e, 0xe2, 0x12, 0x10,
+	0x87, 0x2a, 0x59, 0x8d, 0x13, 0xa9, 0x51, 0x83, 0x11, 0x9a, 0x50, 0x7a,
+	0x76, 0x34, 0x3b, 0x45, 0x4b, 0xb3, 0x20, 0x4b, 0xad, 0x83, 0x46, 0x9b,
+	0x56, 0x27, 0x52, 0xa7, 0x3a, 0x00, 0xe5, 0xe8, 0xc5, 0xa6, 0xd9, 0x87,
+	0x0a, 0xca, 0x93, 0x78, 0x69, 0x05, 0xc2, 0x0b, 0x85, 0x02, 0xf4, 0x5d,
+	0x91, 0x14, 0xa8, 0xd2, 0xc7, 0xbd, 0x2e, 0xe0, 0x08, 0x7f, 0xb7, 0x80,
+	0xfa, 0x50, 0x78, 0x58, 0x8b, 0x02, 0x6f, 0x14, 0x19, 0x6c, 0xc0, 0x68,
+	0xf6, 0xa7, 0x5e, 0x7b, 0x4a, 0x44, 0x3a, 0xea, 0x41, 0x42, 0x5c, 0xaf,
+	0x2d, 0xa2, 0x12, 0xa3, 0xe6, 0xad, 0xf6, 0x44, 0x51, 0x64, 0xc2, 0x90,
+	0x8f, 0x40, 0x9d, 0x43, 0x96, 0x5e, 0xe4, 0x0b, 0x89, 0xd6, 0x54, 0xc8,
+	0x41, 0x30, 0x97, 0x48, 0x6a, 0x9b, 0x90, 0xe2, 0xb0, 0x44, 0x74, 0x38,
+	0x91, 0x18, 0x36, 0xb7, 0xca, 0x84, 0xe7, 0x6b, 0x7a, 0x8e, 0xe6, 0x00,
+	0xa1, 0x3b, 0x55, 0xa8, 0xb8, 0x3d, 0xa0, 0xb2, 0x08, 0x25, 0x38, 0x2b,
+	0xda, 0xc3, 0xbe, 0x25, 0x04, 0x60, 0x24, 0xc7, 0x88, 0x76, 0x40, 0x53,
+	0xf1, 0x92, 0x2c, 0xfa, 0xa2, 0xa1, 0xb8, 0x16, 0x22, 0x30, 0xa3, 0x6d,
+	0xbb, 0x36, 0xcf, 0x8e, 0x62, 0xc8, 0xa1, 0xb1, 0x5f, 0x9d, 0x51, 0x90,
+	0x07, 0x0a, 0xb4, 0xf5, 0x70, 0xa3, 0xba, 0xf8, 0x63, 0x7c, 0x04, 0xad,
+	0x19, 0x51, 0xa4, 0xb4, 0x61, 0x0b, 0x5b, 0x7a, 0xb2, 0x33, 0x56, 0xc3,
+	0x9e, 0x36, 0xa8, 0x12, 0x85, 0x58, 0xa2, 0x59, 0x14, 0xe1, 0xa8, 0xaf,
+	0x76, 0xe4, 0x77, 0x12, 0xf8, 0x54, 0x38, 0xb1, 0x9e, 0x8d, 0x04, 0xce,
+	0x92, 0x35, 0x20, 0xd3, 0xab, 0x0a, 0xc4, 0xe4, 0x08, 0x58, 0xf2, 0x36,
+	0x2d, 0xd4, 0x9d, 0xb4, 0x46, 0xb7, 0xed, 0xf7, 0xa0, 0xdd, 0x4c, 0x83,
+	0xaa, 0x30, 0x7e, 0xfa, 0x87, 0x76, 0xb3, 0xb8, 0xc7, 0xa1, 0x0a, 0x1f,
+	0xdf, 0xe6, 0x8e, 0xda, 0x1b, 0x9c, 0xd4, 0x2a, 0xc0, 0xce, 0xc4, 0x76,
+	0x0c, 0x25, 0x93, 0x18, 0x08, 0x2c, 0x18, 0x41, 0x5d, 0x2c, 0xc2, 0x93,
+	0x67, 0xf6, 0x06, 0xee, 0xea, 0x1c, 0x01, 0x45, 0x38, 0x9e, 0xa2, 0x88,
+	0xf3, 0x83, 0xa6, 0x8d, 0xd6, 0x20, 0x12, 0x7a, 0xac, 0xa8, 0x8b, 0xcb,
+	0x6b, 0x42, 0x45, 0xa6, 0x64, 0x4f, 0x05, 0xa6, 0xb9, 0xf1, 0x16, 0x3d,
+	0x38, 0xe1, 0x7e, 0x57, 0xcb, 0x27, 0xcc, 0x5c, 0x46, 0x7b, 0x2c, 0xca,
+	0x94, 0xf9, 0x74, 0xcf, 0xd8, 0x15, 0x0d, 0x52, 0x94, 0xe1, 0x52, 0x20,
+	0x0b, 0x39, 0xba, 0xfa, 0x46, 0x9b, 0x9c, 0xfa, 0xc3, 0xac, 0xeb, 0x7b,
+	0x56, 0xb4, 0x7b, 0x13, 0xf1, 0x71, 0x81, 0xea, 0x95, 0x87, 0x78, 0x10,
+	0xa7, 0x77, 0xae, 0x9d, 0x15, 0x60, 0xed, 0x51, 0x19, 0xab, 0x09, 0xb7,
+	0x6a, 0xb1, 0x20, 0xe3, 0xc6, 0x19, 0x61, 0xd5, 0x8c, 0x3a, 0x93, 0x16,
+	0x69, 0xa0, 0x5f, 0xd2, 0x88, 0x2f, 0x51, 0x97, 0xb7, 0xc3, 0xb0, 0x47,
+	0xa1, 0x97, 0xaa, 0x82, 0x0c, 0x20, 0x8a, 0x5c, 0xb5, 0xbc, 0x40, 0xbe,
+	0xb8, 0x25, 0x88, 0xf5, 0x61, 0x0b, 0x0b, 0x35, 0x4e, 0xb8, 0x50, 0x39,
+	0xb0, 0xa4, 0x1d, 0xfd, 0x39, 0x24, 0x8f, 0x16, 0x5a, 0xbb, 0x02, 0x7a,
+	0xfd, 0x68, 0x17, 0xe0, 0x0c, 0x8d, 0xad, 0xaa, 0xcb, 0x29, 0xcb, 0x25,
+	0xe4, 0x24, 0x75, 0x09, 0x32, 0xae, 0x27, 0x75, 0x6a, 0x98, 0xb7, 0xb7,
+	0x49, 0x28, 0x53, 0x90, 0x9c, 0xa9, 0x5a, 0x56, 0x92, 0x2a, 0x13, 0x36,
+	0x90, 0x6c, 0x02, 0xb8, 0x12, 0x05, 0xbb, 0x58, 0x69, 0x4d, 0xe8, 0xc0,
+	0x2a, 0x00, 0x5c, 0x23, 0x26, 0x09, 0xc8, 0x79, 0x97, 0xb1, 0x6a, 0xb6,
+	0xbc, 0x79, 0x0d, 0xd3, 0x29, 0x30, 0x03, 0xb9, 0xb4, 0x86, 0x7b, 0x2b,
+	0x6e, 0x04, 0xc4, 0x56, 0x18, 0x50, 0x35, 0x94, 0xaa, 0x26, 0x9c, 0x9d,
+	0x4f, 0xc3, 0x9a, 0xa1, 0xec, 0x36, 0x3f, 0xbc, 0x57, 0xc7, 0x02, 0x41,
+	0x31, 0x0b, 0x1d, 0x6b, 0x44, 0x98, 0xe8, 0xa5, 0x56, 0xc4, 0x87, 0xb1,
+	0x6e, 0x45, 0xaa, 0x9b, 0xb9, 0xb1, 0xa6, 0xa9, 0xbb, 0x2a, 0x44, 0x6f,
+	0x6e, 0xe3, 0x09, 0xed, 0xec, 0x1d, 0x55, 0x32, 0x99, 0x7a, 0x86, 0x94,
+	0xdd, 0xb9, 0x91, 0xe3, 0xa6, 0x77, 0xf8, 0x95, 0x88, 0x5f, 0xa9, 0x34,
+	0x81, 0xea, 0x21, 0x7e, 0xba, 0x1c, 0x47, 0x4b, 0xc4, 0x81, 0x88, 0x14,
+	0x7f, 0xdc, 0x03, 0x22, 0x81, 0xcc, 0xa5, 0xc3, 0x0d, 0x5b, 0x1c, 0x3e,
+	0xd9, 0x09, 0x32, 0xbd, 0xe1, 0x48, 0x55, 0x92, 0x9f, 0x5e, 0x22, 0x4f,
+	0x27, 0x38, 0x75, 0x36, 0x4b, 0x60, 0x0c, 0x7c, 0x91, 0x6b, 0xd3, 0x1f,
+	0x66, 0xd7, 0xbb, 0xc4, 0x70, 0xc5, 0x36, 0xe6, 0x00, 0xe8, 0xda, 0x7f,
+	0x31, 0xfb, 0x3b, 0x99, 0x45, 0x73, 0xe7, 0x77, 0x96, 0xc5, 0x39, 0x69,
+	0x37, 0x1a, 0x5e, 0x30, 0xc3, 0x71, 0xa0, 0x32, 0x50, 0xe4, 0x26, 0xc2,
+	0xdb, 0xda, 0x9e, 0xee, 0xd3, 0x9b, 0x6b, 0x65, 0x03, 0x38, 0x80, 0x66,
+	0x05, 0xd3, 0x5e, 0x64, 0xd5, 0x59, 0xd0, 0x91, 0x0d, 0x2e, 0xf7, 0x51,
+	0xaf, 0xa8, 0x33, 0x00, 0x2c, 0x90, 0x5d, 0xb5, 0x5e, 0xe0, 0x09, 0x81,
+	0x75, 0xf2, 0x11, 0x11, 0x82, 0x2f, 0x6a, 0x9c, 0x1a, 0xab, 0xcb, 0x97,
+	0x71, 0xcc, 0x15, 0x3b, 0xb4, 0xb6, 0x19, 0xd7, 0x35, 0xfa, 0x82, 0x23,
+	0xd5, 0xf0, 0x6a, 0x8b, 0x2b, 0x79, 0x08, 0xe3, 0x2d, 0x8f, 0x7b, 0x82,
+	0x9d, 0xac, 0x88, 0xc9, 0xca, 0xa4, 0x74, 0x2a, 0x12, 0xa2, 0xa4, 0x4b,
+	0xc6, 0x14, 0x20, 0xf7, 0xd2, 0x6b, 0x87, 0xc3, 0xc8, 0x33, 0xc5, 0x6c,
+	0xb5, 0x1a, 0xcc, 0x1b, 0xd4, 0x8c, 0x9a, 0x64, 0x8b, 0x4f, 0x60, 0x8e,
+	0x4f, 0xeb, 0x37, 0x8e, 0x65, 0x54, 0xb7, 0x3c, 0xa8, 0xe3, 0x7b, 0x9d,
+	0x5f, 0xa7, 0x6b, 0x44, 0x1c, 0x03, 0x97, 0xf8, 0xc0, 0xaa, 0x39, 0xb2,
+	0x34, 0x8a, 0xbc, 0x3c, 0x6d, 0x0d, 0x13, 0xa6, 0xbd, 0x91, 0xd6, 0x2a,
+	0x43, 0xc6, 0x51, 0x60, 0x63, 0xdf, 0xd5, 0xbf, 0x77, 0xf0, 0xf1, 0x08,
+	0x42, 0x24, 0x43, 0x7b, 0x47, 0xa8, 0x2b, 0xa2, 0x28, 0xb8, 0x74, 0x69,
+	0xd4, 0xee, 0x89, 0x06, 0xec, 0x34, 0xdb, 0xa7, 0x6c, 0x68, 0xd8, 0xa8,
+	0x22, 0x8d, 0xf3, 0x3c, 0xcf, 0x3a, 0x80, 0xbf, 0x15, 0x6b, 0x29, 0x53,
+	0xa5, 0x31, 0x26, 0x9f, 0x3c, 0xb1, 0xee, 0xa9, 0x88, 0x00, 0x4b, 0x93,
+	0x10, 0x3c, 0xfb, 0x0a, 0xee, 0xfd, 0x2a, 0x68, 0x6e, 0x01, 0xfa, 0x4a,
+	0x58, 0xe8, 0xa3, 0x63, 0x9c, 0xa8, 0xa1, 0xe3, 0xf9, 0xae, 0x57, 0xe2,
+};
+
+static const u8 fips_test_mlkem768_ct[MLKEM768_CIPHERTEXT_BYTES] __initconst __maybe_unused = {
+	0x1d, 0x3b, 0xe0, 0x4a, 0x6a, 0x14, 0xb4, 0x98, 0xe3, 0x65, 0xde, 0x61,
+	0xa7, 0x00, 0xb2, 0x3a, 0x20, 0x01, 0xdb, 0xc3, 0xdc, 0xd3, 0x87, 0xcf,
+	0xa1, 0xf1, 0x69, 0x5f, 0x11, 0x1a, 0xad, 0x47, 0x4b, 0x34, 0x22, 0x3e,
+	0x10, 0x0a, 0x29, 0xe5, 0x3c, 0x3d, 0x16, 0xa5, 0xd7, 0x69, 0xe1, 0xa6,
+	0x4f, 0x50, 0xbe, 0x02, 0x9a, 0x09, 0x84, 0x54, 0x23, 0x67, 0x49, 0xa2,
+	0xc7, 0x03, 0xc0, 0x32, 0xfb, 0x19, 0x9d, 0x2d, 0xee, 0x02, 0x50, 0x90,
+	0xbb, 0xbc, 0x60, 0x9a, 0x14, 0x28, 0x4c, 0x13, 0x5f, 0xf2, 0xf8, 0xfe,
+	0xa9, 0x99, 0x3a, 0x36, 0x68, 0x82, 0x37, 0xb0, 0x38, 0x54, 0x7e, 0x77,
+	0xa7, 0xd5, 0xca, 0xc0, 0x43, 0x11, 0x44, 0xde, 0x34, 0xf7, 0x79, 0x01,
+	0xfe, 0x25, 0x27, 0xf0, 0x98, 0xd4, 0xca, 0xc1, 0x59, 0x67, 0x4b, 0x99,
+	0x04, 0xe6, 0xa7, 0x21, 0x90, 0xc8, 0x86, 0xd1, 0x41, 0x0c, 0x5a, 0x53,
+	0x07, 0x8b, 0xb7, 0xc1, 0x8d, 0x74, 0x9c, 0x21, 0x3d, 0xe4, 0x2d, 0xab,
+	0xe6, 0xc8, 0x3b, 0x8c, 0x1f, 0xfe, 0xc8, 0xac, 0xb9, 0x32, 0xd0, 0x47,
+	0x52, 0xa5, 0xe1, 0x6e, 0x0e, 0x58, 0x71, 0x1b, 0xf3, 0xc7, 0xe7, 0x01,
+	0x19, 0x9f, 0x49, 0x57, 0x45, 0xe0, 0x74, 0x40, 0x93, 0xa8, 0xdd, 0x88,
+	0x6c, 0xa0, 0x6b, 0xa5, 0xbb, 0x4e, 0x09, 0x36, 0x5f, 0x58, 0xe9, 0x9e,
+	0x08, 0xe6, 0xb6, 0x90, 0x2a, 0x0c, 0x1b, 0x06, 0x42, 0xe8, 0xfd, 0x42,
+	0x71, 0x41, 0x6a, 0x25, 0x12, 0x2e, 0x21, 0x9a, 0x19, 0xa8, 0xcd, 0xce,
+	0x6e, 0x66, 0xaa, 0x62, 0xff, 0x30, 0x2d, 0xf8, 0x57, 0xb4, 0x65, 0x54,
+	0x79, 0xcf, 0x29, 0x2b, 0xe9, 0xa7, 0xea, 0x31, 0x2b, 0xa4, 0x7c, 0xb7,
+	0x7e, 0x73, 0xad, 0x9b, 0x2d, 0x4b, 0x56, 0xea, 0xca, 0x24, 0x0c, 0x35,
+	0x27, 0x16, 0x96, 0xcd, 0xad, 0x56, 0x0f, 0x75, 0xb1, 0xb0, 0x64, 0x16,
+	0x0c, 0x3c, 0xf5, 0xe6, 0x2f, 0x93, 0x03, 0xab, 0x95, 0x92, 0x44, 0x76,
+	0x80, 0xd6, 0x7c, 0xc7, 0x57, 0x15, 0x92, 0xef, 0xdf, 0xb4, 0xe3, 0x05,
+	0xc4, 0x4d, 0x5e, 0x4f, 0xe4, 0x65, 0x5e, 0x7c, 0xf9, 0xed, 0x78, 0xcf,
+	0x70, 0xb6, 0xea, 0x82, 0x3d, 0x69, 0x24, 0x91, 0x66, 0x62, 0x0d, 0x19,
+	0xf5, 0xe0, 0x33, 0x46, 0xf0, 0x6b, 0x9b, 0x9a, 0xef, 0x26, 0x59, 0x84,
+	0xdd, 0x61, 0xf0, 0x2b, 0x94, 0xc6, 0xb0, 0x69, 0xdd, 0x4a, 0x44, 0x66,
+	0x2f, 0xfd, 0x55, 0x02, 0x12, 0x68, 0x5a, 0x48, 0x3e, 0x9e, 0xd7, 0x01,
+	0x3e, 0xc3, 0x67, 0x73, 0xaa, 0xc6, 0xdc, 0x45, 0xce, 0x1d, 0xa8, 0x5b,
+	0x14, 0x04, 0x5c, 0x13, 0xd2, 0x59, 0x32, 0x94, 0x0e, 0xc1, 0xfd, 0xbf,
+	0xac, 0xaa, 0x7f, 0x66, 0x03, 0xdc, 0x1a, 0xef, 0xf9, 0xf5, 0x2f, 0xae,
+	0xdb, 0x87, 0x2f, 0xac, 0x14, 0x73, 0xf5, 0xcb, 0x8f, 0xb2, 0x0c, 0xff,
+	0x71, 0x24, 0x85, 0x59, 0x79, 0x38, 0x90, 0x3a, 0x7d, 0x1e, 0x48, 0x95,
+	0x22, 0x62, 0x62, 0x94, 0xd4, 0x77, 0x3b, 0x71, 0xcb, 0xdc, 0x01, 0x79,
+	0xc8, 0x13, 0x08, 0x75, 0x5f, 0x3f, 0x3f, 0x11, 0x5b, 0xc1, 0x78, 0xc2,
+	0x95, 0xdc, 0xfb, 0x1c, 0xcf, 0xbf, 0xb5, 0x7d, 0x3d, 0x33, 0xb6, 0x97,
+	0x41, 0xa8, 0x2d, 0xa6, 0x13, 0x8f, 0x73, 0x15, 0x92, 0x19, 0xbd, 0x61,
+	0x50, 0x20, 0x22, 0x6a, 0x24, 0xb5, 0x8f, 0xac, 0x81, 0x26, 0x6e, 0x46,
+	0x8e, 0x6a, 0x6c, 0xca, 0x6b, 0xe2, 0xa5, 0x8d, 0xec, 0x46, 0xa3, 0xf7,
+	0x42, 0xc7, 0xc4, 0x0c, 0x8d, 0x2b, 0xe6, 0x6f, 0xb0, 0x2c, 0x32, 0x4c,
+	0x3b, 0x58, 0x5d, 0x9a, 0xd0, 0xa4, 0x60, 0x4d, 0x6c, 0xb6, 0xc9, 0xe9,
+	0xf6, 0x79, 0x06, 0xcb, 0x29, 0x60, 0x6d, 0x1a, 0x77, 0x41, 0xbe, 0x34,
+	0xeb, 0x24, 0xff, 0xdc, 0x1f, 0x63, 0x4b, 0xc7, 0x4f, 0x76, 0xcf, 0xca,
+	0x0d, 0xd5, 0x7e, 0xdf, 0x62, 0x62, 0xc8, 0xe7, 0xf6, 0x1c, 0xd0, 0xcc,
+	0xcf, 0x73, 0x91, 0xbd, 0x2b, 0xd6, 0x9b, 0xef, 0xdf, 0x35, 0xfa, 0xf3,
+	0x69, 0x33, 0xa3, 0x9b, 0x1d, 0xf4, 0xf6, 0x94, 0x98, 0x4d, 0xbb, 0xd2,
+	0xd4, 0x77, 0xc3, 0x8e, 0xd3, 0x29, 0xc0, 0x55, 0x8d, 0x0b, 0xc8, 0xa5,
+	0x71, 0x6f, 0x75, 0xb4, 0x3b, 0xff, 0x17, 0xdc, 0x80, 0xa5, 0x99, 0x9f,
+	0x39, 0x7a, 0x2f, 0x52, 0xd3, 0xdd, 0x4f, 0x7a, 0x9c, 0x6b, 0x39, 0x43,
+	0x5b, 0x0c, 0x35, 0x73, 0xeb, 0x65, 0xf5, 0x51, 0x4a, 0xdd, 0xbf, 0x49,
+	0x89, 0x65, 0xf2, 0x70, 0xa6, 0xc1, 0x9b, 0xaf, 0xce, 0x46, 0x6e, 0x48,
+	0x36, 0x35, 0xf1, 0x0a, 0x90, 0x68, 0x22, 0x06, 0xa0, 0x5c, 0x0d, 0x24,
+	0x99, 0x4b, 0x39, 0x79, 0x34, 0xfe, 0xcc, 0x8f, 0x08, 0x44, 0x63, 0x26,
+	0xa4, 0x9d, 0x3e, 0xba, 0xec, 0x72, 0x22, 0xcd, 0xcf, 0x76, 0xb2, 0x99,
+	0x80, 0x07, 0x52, 0x82, 0x97, 0xb4, 0xfe, 0x04, 0xa2, 0x1d, 0x8b, 0xfe,
+	0x76, 0x27, 0xec, 0xe4, 0xbe, 0xd4, 0x89, 0xde, 0x02, 0x70, 0x57, 0x89,
+	0xda, 0xc5, 0xe6, 0x00, 0x26, 0x48, 0x6f, 0x79, 0xde, 0x2f, 0x93, 0x14,
+	0x76, 0xd8, 0x09, 0x9e, 0xe2, 0x7d, 0x2d, 0xd8, 0x75, 0x86, 0xb9, 0xe2,
+	0xeb, 0x0d, 0xa2, 0x24, 0x19, 0xe9, 0x48, 0xc1, 0x87, 0x99, 0x8e, 0xde,
+	0x7f, 0xac, 0xb0, 0x6a, 0xc9, 0x2b, 0xdc, 0xfc, 0xe4, 0x13, 0x82, 0xb2,
+	0x9a, 0xf1, 0xcb, 0x62, 0xb4, 0x1a, 0x16, 0xd4, 0xd8, 0xd9, 0xe5, 0x95,
+	0x80, 0xfe, 0x03, 0x3b, 0x0b, 0xee, 0xd4, 0x5e, 0x23, 0xb0, 0xee, 0x74,
+	0xc4, 0x0c, 0xa9, 0x00, 0x4d, 0x79, 0x3a, 0x09, 0x68, 0xc9, 0xda, 0x52,
+	0xbe, 0xad, 0x79, 0x30, 0x9c, 0x4f, 0x84, 0xb4, 0xe8, 0x52, 0x15, 0x14,
+	0xca, 0xa0, 0x16, 0x30, 0x8d, 0x74, 0x72, 0x6f, 0x89, 0x9f, 0xbc, 0x91,
+	0x00, 0xe3, 0x52, 0x7d, 0xd9, 0x2e, 0xca, 0xd0, 0xce, 0x35, 0xbe, 0xf8,
+	0x69, 0x9a, 0x91, 0x1c, 0x36, 0xec, 0xf1, 0x61, 0xe7, 0xff, 0xa7, 0xa5,
+	0x3e, 0x03, 0xa1, 0xf9, 0xe0, 0x28, 0xe3, 0xa3, 0x15, 0x5b, 0x98, 0xe8,
+	0x19, 0x86, 0xee, 0x84, 0x90, 0x2f, 0x94, 0x3a, 0x80, 0xa4, 0x7b, 0x20,
+	0xaa, 0xff, 0x91, 0xb2, 0x19, 0x3d, 0xd1, 0xea, 0x97, 0xf2, 0xc3, 0x8e,
+	0x89, 0x56, 0xf0, 0x3b, 0xe2, 0x06, 0xd4, 0xb9, 0x06, 0xc7, 0x67, 0xbf,
+	0x11, 0xfd, 0xc2, 0x96, 0xfa, 0xc2, 0x03, 0x60, 0x5c, 0x1b, 0x18, 0xe1,
+	0x8c, 0x01, 0xa2, 0x51, 0xc7, 0x42, 0x78, 0x10, 0x12, 0x45, 0xd5, 0x8b,
+	0xfb, 0x31, 0x98, 0x04, 0x06, 0x7d, 0xe2, 0x97, 0x65, 0xd1, 0xd4, 0x37,
+	0x74, 0x6b, 0xa3, 0x41, 0x2c, 0x6d, 0xda, 0xd7, 0xcb, 0x01, 0xc2, 0xf3,
+	0xad, 0x5e, 0x26, 0x51, 0x3b, 0x25, 0xb2, 0x69, 0xfc, 0xa9, 0xeb, 0x3f,
+	0x84, 0xce, 0x48, 0xa9, 0xe9, 0x01, 0x86, 0xe3, 0xa8, 0x51, 0x50, 0x8b,
+	0xee, 0xa2, 0x29, 0x2d, 0x8d, 0x3b, 0xb9, 0x6b, 0x5b, 0xa1, 0xf7, 0x35,
+	0xf0, 0xd4, 0x15, 0x9a, 0x27, 0x95, 0x2c, 0x20, 0x66, 0x45, 0x67, 0x1d,
+	0x45, 0xf2, 0xd3, 0xa7, 0x95, 0x5e, 0xe7, 0x72, 0x9b, 0x7f, 0xd4, 0xb2,
+	0x0d, 0xba, 0x88, 0x1f, 0xac, 0x39, 0xbf, 0xa7, 0x5b, 0xfa, 0xf7, 0x60,
+	0x39, 0xc8, 0xe3, 0x2d, 0x36, 0x38, 0x4e, 0x67, 0x18, 0xbe, 0xab, 0x6f,
+	0xce, 0xf8, 0x38, 0xf3, 0x91, 0x3a, 0x34, 0xe0, 0x05, 0xfb, 0x4c, 0x19,
+	0xa8, 0x6a, 0x9c, 0xff, 0x2f, 0x52, 0xff, 0xc6, 0x69, 0x4b, 0xd7, 0x72,
+	0x14, 0x40, 0x51, 0x86, 0x7c, 0x2c, 0x21, 0x6e, 0xe7, 0x12, 0xe5, 0x9d,
+	0x8a, 0x02, 0xaa, 0x37, 0x3e, 0xab, 0x4a, 0x13, 0x17, 0x8e, 0xa3, 0xe6,
+	0x8b, 0xff, 0x5f, 0x96, 0xbf, 0x77, 0x68, 0x6d, 0xa2, 0xf9, 0x27, 0x14,
+	0x34, 0xf9, 0xcc, 0x7e, 0x38, 0xfd, 0xcd, 0x06, 0x47, 0x58, 0xd6, 0x39,
+	0xe7, 0xa1, 0xbd, 0x8a, 0x89, 0x7d, 0xf5, 0xfa, 0xb7, 0x57, 0x18, 0xe4,
+	0x2f, 0x85, 0x8f, 0xe9, 0xba, 0x52, 0x56, 0x79,
+};
+
+static const u8 fips_test_mlkem768_ss[MLKEM_SHARED_SECRET_BYTES] __initconst __maybe_unused = {
+	0xfe, 0x62, 0x76, 0x21, 0xfe, 0x29, 0x61, 0x86, 0xfc, 0xe3, 0x22, 0x43,
+	0xdd, 0x55, 0x4b, 0xdd, 0xa3, 0x89, 0x71, 0xb4, 0x7f, 0x18, 0x46, 0x1f,
+	0x21, 0x32, 0x37, 0x82, 0xdf, 0xe5, 0xff, 0x89,
+};
+
+static const u8 fips_test_mlkem768_ct_invalid[MLKEM768_CIPHERTEXT_BYTES] __initconst __maybe_unused = {
+	0xba, 0xdf, 0xd6, 0xdf, 0xaa, 0xc3, 0x59, 0xa5, 0xef, 0xbb, 0x7b, 0xcc,
+	0x4b, 0x59, 0xd5, 0x38, 0xdf, 0x9a, 0x04, 0x30, 0x2e, 0x10, 0xc8, 0xbc,
+	0x1c, 0xbf, 0x1a, 0x0b, 0x3a, 0x51, 0x20, 0xea, 0x17, 0xcd, 0xa7, 0xcf,
+	0xad, 0x76, 0x5f, 0x56, 0x23, 0x47, 0x4d, 0x36, 0x8c, 0xcc, 0xa8, 0xaf,
+	0x00, 0x07, 0xcd, 0x9f, 0x5e, 0x4c, 0x84, 0x9f, 0x16, 0x7a, 0x58, 0x0b,
+	0x14, 0xaa, 0xbd, 0xef, 0xae, 0xe7, 0xee, 0xf4, 0x7c, 0xb0, 0xfc, 0xa9,
+	0x76, 0x7b, 0xe1, 0xfd, 0xa6, 0x94, 0x19, 0xdf, 0xb9, 0x27, 0xe9, 0xdf,
+	0x07, 0x34, 0x8b, 0x19, 0x66, 0x91, 0xab, 0xae, 0xb5, 0x80, 0xb3, 0x2d,
+	0xef, 0x58, 0x53, 0x8b, 0x8d, 0x23, 0xf8, 0x77, 0x32, 0xea, 0x63, 0xb0,
+	0x2b, 0x4f, 0xa0, 0xf4, 0x87, 0x33, 0x60, 0xe2, 0x84, 0x19, 0x28, 0xcd,
+	0x60, 0xdd, 0x4c, 0xee, 0x8c, 0xc0, 0xd4, 0xc9, 0x22, 0xa9, 0x61, 0x88,
+	0xd0, 0x32, 0x67, 0x5c, 0x8a, 0xc8, 0x50, 0x93, 0x3c, 0x7a, 0xff, 0x15,
+	0x33, 0xb9, 0x4c, 0x83, 0x4a, 0xdb, 0xb6, 0x9c, 0x61, 0x15, 0xba, 0xd4,
+	0x69, 0x2d, 0x86, 0x19, 0xf9, 0x0b, 0x0c, 0xdf, 0x8a, 0x7b, 0x9c, 0x26,
+	0x40, 0x29, 0xac, 0x18, 0x5b, 0x70, 0xb8, 0x3f, 0x28, 0x01, 0xf2, 0xf4,
+	0xb3, 0xf7, 0x0c, 0x59, 0x3e, 0xa3, 0xae, 0xeb, 0x61, 0x3a, 0x7f, 0x1b,
+	0x1d, 0xe3, 0x3f, 0xd7, 0x50, 0x81, 0xf5, 0x92, 0x30, 0x5f, 0x2e, 0x45,
+	0x26, 0xed, 0xc0, 0x96, 0x31, 0xb1, 0x09, 0x58, 0xf4, 0x64, 0xd8, 0x89,
+	0xf3, 0x1b, 0xa0, 0x10, 0x25, 0x0f, 0xda, 0x7f, 0x13, 0x68, 0xec, 0x29,
+	0x67, 0xfc, 0x84, 0xef, 0x2a, 0xe9, 0xaf, 0xf2, 0x68, 0xe0, 0xb1, 0x70,
+	0x0a, 0xff, 0xc6, 0x82, 0x0b, 0x52, 0x3a, 0x3d, 0x91, 0x71, 0x35, 0xf2,
+	0xdf, 0xf2, 0xee, 0x06, 0xbf, 0xe7, 0x2b, 0x31, 0x24, 0x72, 0x1d, 0x4a,
+	0x26, 0xc0, 0x4e, 0x53, 0xa7, 0x5e, 0x30, 0xe7, 0x3a, 0x7a, 0x9c, 0x4a,
+	0x95, 0xd9, 0x1c, 0x55, 0xd4, 0x95, 0xe9, 0xf5, 0x1d, 0xd0, 0xb5, 0xe9,
+	0xd8, 0x3c, 0x6d, 0x5e, 0x8c, 0xe8, 0x03, 0xaa, 0x62, 0xb8, 0xd6, 0x54,
+	0xdb, 0x53, 0xd0, 0x9b, 0x8d, 0xcf, 0xf2, 0x73, 0xcd, 0xfe, 0xb5, 0x73,
+	0xfa, 0xd8, 0xbc, 0xd4, 0x55, 0x78, 0xbe, 0xc2, 0xe7, 0x70, 0xd0, 0x1e,
+	0xfd, 0xe8, 0x6e, 0x72, 0x1a, 0x3f, 0x7c, 0x6c, 0xce, 0x27, 0x5d, 0xab,
+	0xe6, 0xe2, 0x14, 0x3f, 0x1a, 0xf1, 0x8d, 0xa7, 0xef, 0xdd, 0xc4, 0xc7,
+	0xb7, 0x0b, 0x5e, 0x34, 0x5d, 0xb9, 0x3c, 0xc9, 0x36, 0xbe, 0xa3, 0x23,
+	0x49, 0x1c, 0xcb, 0x38, 0xa3, 0x88, 0xf5, 0x46, 0xa9, 0xff, 0x00, 0xdd,
+	0x4e, 0x13, 0x00, 0xb9, 0xb2, 0x15, 0x3d, 0x20, 0x41, 0xd2, 0x05, 0xb4,
+	0x43, 0xe4, 0x1b, 0x45, 0xa6, 0x53, 0xf2, 0xa5, 0xc4, 0x49, 0x2c, 0x1a,
+	0xdd, 0x54, 0x45, 0x12, 0xdd, 0xa2, 0x52, 0x98, 0x33, 0x46, 0x2b, 0x71,
+	0xa4, 0x1a, 0x45, 0xbe, 0x97, 0x29, 0x0b, 0x6f, 0x4c, 0xff, 0xda, 0x2c,
+	0xf9, 0x90, 0x05, 0x16, 0x34, 0xa4, 0xb1, 0xed, 0xf6, 0x11, 0x4f, 0xb4,
+	0x90, 0x83, 0xc1, 0xfa, 0x3b, 0x30, 0x2e, 0xe0, 0x97, 0xf0, 0x51, 0x26,
+	0x6b, 0xe6, 0x9d, 0xc7, 0x16, 0xfd, 0xee, 0xf9, 0x1b, 0x0d, 0x4a, 0xb2,
+	0xde, 0x52, 0x55, 0x50, 0xbf, 0x80, 0xdc, 0x8a, 0x68, 0x4b, 0xc3, 0xb5,
+	0xa4, 0xd4, 0x6b, 0x7e, 0xfa, 0xe7, 0xaf, 0xdc, 0x62, 0x92, 0x98, 0x8d,
+	0xc9, 0xac, 0xae, 0x03, 0xf8, 0x63, 0x44, 0x86, 0xc1, 0xab, 0xe2, 0x78,
+	0x1a, 0xae, 0x4c, 0x02, 0xf3, 0x46, 0x0d, 0x2c, 0xd4, 0xe6, 0xa4, 0x63,
+	0xa2, 0xba, 0x95, 0x62, 0xee, 0x62, 0x3c, 0xf0, 0xe9, 0xf8, 0x2a, 0xb4,
+	0xd0, 0xb5, 0xc9, 0xd0, 0x40, 0xa2, 0x69, 0x36, 0x64, 0x79, 0xdf, 0xf0,
+	0x03, 0x8a, 0xbf, 0xaf, 0x2e, 0x0f, 0xf2, 0x1f, 0x36, 0x96, 0x89, 0x72,
+	0xe3, 0xf1, 0x04, 0xdd, 0xcb, 0xe1, 0xeb, 0x83, 0x1a, 0x87, 0xc2, 0x13,
+	0x16, 0x2e, 0x29, 0xb3, 0x4a, 0xdf, 0xa5, 0x64, 0xd1, 0x21, 0xe9, 0xf6,
+	0xe7, 0x72, 0x9f, 0x42, 0x03, 0xfc, 0x5c, 0x6c, 0x22, 0xfa, 0x7a, 0x73,
+	0x50, 0xaf, 0xdd, 0xb6, 0x20, 0x92, 0x3a, 0x4a, 0x12, 0x9b, 0x8a, 0xcb,
+	0x19, 0xea, 0x10, 0xf8, 0x18, 0xc3, 0x0e, 0x3b, 0x5b, 0x1c, 0x57, 0x1f,
+	0xa7, 0x9e, 0x57, 0xee, 0x30, 0x43, 0x88, 0x31, 0x6a, 0x02, 0xfc, 0xd9,
+	0x3a, 0x0d, 0x8e, 0xe0, 0x2b, 0xb8, 0x57, 0x01, 0xee, 0x4f, 0xf0, 0x97,
+	0x53, 0x4b, 0x50, 0x2c, 0x1b, 0x12, 0xfb, 0xb9, 0x5c, 0x8c, 0xcb, 0x2f,
+	0x54, 0x89, 0x21, 0xd9, 0x9c, 0xc7, 0xc9, 0xfe, 0x17, 0xac, 0x99, 0x1b,
+	0x67, 0x5e, 0x63, 0x11, 0x44, 0x42, 0x3e, 0xef, 0x7a, 0x58, 0x69, 0x16,
+	0x8d, 0xa6, 0x3d, 0x1f, 0x4c, 0x21, 0xf6, 0x50, 0xc0, 0x29, 0x23, 0xbf,
+	0xd3, 0x96, 0xca, 0x6a, 0x5d, 0xb5, 0x41, 0x06, 0x86, 0x24, 0xcb, 0xc5,
+	0xff, 0xe2, 0x08, 0xc0, 0xd1, 0xa7, 0x4e, 0x1a, 0x29, 0x61, 0x8d, 0x0b,
+	0xb6, 0x00, 0x36, 0xf5, 0x24, 0x9a, 0xbf, 0xa8, 0x88, 0x98, 0xe3, 0x93,
+	0x71, 0x8d, 0x6e, 0xfa, 0xb0, 0x5b, 0xb4, 0x12, 0x79, 0xef, 0xcd, 0x4c,
+	0x5a, 0x0c, 0xc8, 0x37, 0xcc, 0xfc, 0x22, 0xbe, 0x4f, 0x72, 0x5c, 0x08,
+	0x1f, 0x6a, 0xa0, 0x90, 0x74, 0x9d, 0xba, 0x70, 0x77, 0xba, 0xe8, 0xd4,
+	0x1a, 0xf3, 0xfe, 0xc5, 0xa6, 0xee, 0x1b, 0x8a, 0xdc, 0xd2, 0x5e, 0x72,
+	0xde, 0x36, 0x43, 0x45, 0x84, 0xef, 0x56, 0x7c, 0x64, 0x3d, 0x34, 0x42,
+	0x94, 0xe8, 0xb2, 0x08, 0x6b, 0x87, 0xf6, 0x9c, 0x3b, 0xdc, 0x0d, 0x59,
+	0x69, 0x85, 0x70, 0x82, 0x98, 0x7c, 0xa1, 0xc6, 0x3b, 0x71, 0x82, 0xe8,
+	0x68, 0x98, 0xfb, 0x9b, 0x80, 0x39, 0xe7, 0x5e, 0xda, 0x21, 0x9e, 0x28,
+	0x93, 0x31, 0x61, 0x03, 0x69, 0x27, 0x18, 0x67, 0xb1, 0x45, 0xb2, 0x90,
+	0x82, 0x93, 0x96, 0x3c, 0xd6, 0x77, 0xc9, 0xa1, 0xae, 0x6c, 0xeb, 0x28,
+	0x28, 0x9b, 0x25, 0x4c, 0xde, 0xb7, 0x6b, 0x12, 0xf3, 0x3c, 0xe5, 0xcf,
+	0x37, 0x43, 0x13, 0x1b, 0xfb, 0x55, 0x0f, 0x01, 0x97, 0xbf, 0xe1, 0x6a,
+	0xff, 0x92, 0x36, 0x72, 0x27, 0xad, 0xc5, 0x07, 0x4f, 0xe3, 0xdc, 0x0d,
+	0x8d, 0x11, 0x62, 0x53, 0x98, 0x0a, 0x38, 0x63, 0x6b, 0xc9, 0xd2, 0x9f,
+	0x79, 0x9b, 0xbb, 0x2d, 0x76, 0xa0, 0xa5, 0xf1, 0x38, 0xb8, 0xc7, 0x3b,
+	0xa4, 0x84, 0xd6, 0x58, 0x87, 0x64, 0xe3, 0x31, 0xd7, 0x0c, 0x37, 0x8c,
+	0x06, 0x41, 0xf2, 0xd9, 0xb6, 0xfd, 0x7c, 0x09, 0x0d, 0xf5, 0xa7, 0x46,
+	0x04, 0xa1, 0x32, 0x4b, 0xa0, 0xcc, 0x5c, 0x44, 0x7b, 0x2d, 0xca, 0x64,
+	0x4a, 0x50, 0xf1, 0xad, 0x04, 0x77, 0xa7, 0x01, 0xb9, 0x05, 0x2e, 0xe9,
+	0xbe, 0xf2, 0x88, 0x33, 0x47, 0x63, 0x43, 0xc8, 0x2a, 0xf2, 0x9f, 0xf3,
+	0xa9, 0xb1, 0xc4, 0xcf, 0x12, 0xde, 0x55, 0x9c, 0xb9, 0xd9, 0x41, 0x1f,
+	0x62, 0xbe, 0xc8, 0x38, 0x12, 0x1f, 0xd7, 0x4b, 0xc1, 0xfa, 0x71, 0x2d,
+	0x8a, 0xdd, 0x51, 0x50, 0x5c, 0x55, 0xe8, 0x9a, 0x35, 0xde, 0xaf, 0x7a,
+	0x69, 0xdc, 0x0a, 0x18, 0xad, 0x27, 0x39, 0x60, 0x29, 0xcb, 0xf8, 0x9f,
+	0x51, 0x3e, 0x1b, 0x8f, 0x48, 0xbc, 0x01, 0x78, 0x3d, 0x68, 0x49, 0xfb,
+	0x32, 0xf2, 0x11, 0xa4, 0xc8, 0x7e, 0x16, 0xbc, 0xce, 0x0c, 0x41, 0x24,
+	0x0a, 0x22, 0x3b, 0xa6, 0xd6, 0x9e, 0x0c, 0x51, 0x56, 0x9f, 0x73, 0xcb,
+	0x10, 0x7e, 0xad, 0x84, 0xd1, 0x4d, 0xee, 0x92, 0x70, 0x2e, 0x3a, 0x95,
+	0xeb, 0x84, 0x4c, 0x71, 0x6a, 0xec, 0x98, 0x29, 0xd0, 0x65, 0x91, 0xeb,
+	0xd2, 0x50, 0x1a, 0x32, 0x83, 0xcc, 0x0f, 0xfc, 0x0f, 0xdc, 0xc0, 0x31,
+	0xfe, 0x8d, 0x86, 0x5e, 0x77, 0xfa, 0xe5, 0xd6, 0xbb, 0x73, 0x81, 0x5d,
+	0x9a, 0xe3, 0x76, 0x00, 0x6d, 0x0a, 0xe3, 0x20,
+};
+
+static const u8 fips_test_mlkem768_ss_rejected[MLKEM_SHARED_SECRET_BYTES] __initconst __maybe_unused = {
+	0xb8, 0x77, 0xda, 0x79, 0x2d, 0x89, 0xf2, 0x80, 0x49, 0xb5, 0x90, 0x12,
+	0x16, 0x01, 0x20, 0x2d, 0x2b, 0xc8, 0xf5, 0xf1, 0xaf, 0x83, 0x82, 0xbf,
+	0x4f, 0x39, 0x41, 0x05, 0x0d, 0xd5, 0x17, 0x2b,
+};
diff --git a/lib/crypto/mlkem.c b/lib/crypto/mlkem.c
index 5e499e5de636..d2183a526956 100644
--- a/lib/crypto/mlkem.c
+++ b/lib/crypto/mlkem.c
@@ -10,15 +10,17 @@
 #include <crypto/mlkem.h>
 #include <crypto/sha3.h>
 #include <crypto/utils.h>
 #include <kunit/visibility.h>
 #include <linux/export.h>
+#include <linux/fips.h>
 #include <linux/module.h>
 #include <linux/random.h>
 #include <linux/slab.h>
 #include <linux/string.h>
 #include <linux/unaligned.h>
+#include "fips-mlkem.h"
 
 #define Q 3329 /* The prime q = 2^8 * 13 + 1 */
 #define N 256 /* Number of coefficients per ring element */
 #define MAX_K 4 /* Max matrix dimension (k parameter) for any parameter set */
 #define MAX_CIPHERTEXT_BYTES 1568 /* Max ciphertext length for any param set */
@@ -627,10 +629,24 @@ static int k_pke_decrypt(u8 m[32], const u8 *sk, const u8 *ct,
 	/* Decode the plaintext m from the polynomial w. */
 	compress_and_encode(m, &ws->tmp, 1);
 	return 0;
 }
 
+static int mlkem_encaps_internal(u8 *ct, u8 ss[MLKEM_SHARED_SECRET_BYTES],
+				 const u8 *pk,
+				 const u8 eseed[MLKEM_ESEED_BYTES],
+				 const struct mlkem_parameter_set *params);
+static int mlkem_decaps(u8 ss[MLKEM_SHARED_SECRET_BYTES], const u8 *ct,
+			const u8 *sk, const struct mlkem_parameter_set *params);
+
+struct mlkem_fips140_pct_workspace {
+	u8 eseed[MLKEM_ESEED_BYTES];
+	u8 ct[MLKEM1024_CIPHERTEXT_BYTES];
+	u8 ss1[MLKEM_SHARED_SECRET_BYTES];
+	u8 ss2[MLKEM_SHARED_SECRET_BYTES];
+};
+
 /*
  * Generate an ML-KEM key pair (@pk, @sk) from the random seed @seed.
  * The lengths of @pk and @sk are determined by the chosen @params.
  *
  * Reference: FIPS 203 Algorithm 16, ML-KEM.KeyGen_internal.  Formally it takes
@@ -660,10 +676,31 @@ static int mlkem_keygen_internal(u8 *pk, u8 *sk,
 	sk_ptr += params->pk_len;
 	sha3_256(pk, params->pk_len, sk_ptr);
 	sk_ptr += SHA3_256_DIGEST_SIZE;
 	memcpy(sk_ptr, &seed[32], 32);
 
+	if (fips_enabled) {
+		/* Do the Pairwise Consistency Test required by FIPS 140-3. */
+		struct mlkem_fips140_pct_workspace *ws __free(kfree_sensitive) =
+			kmalloc_obj(*ws);
+		if (!ws)
+			return -ENOMEM; /* Out of memory, not a PCT failure */
+		get_random_bytes(ws->eseed, sizeof(ws->eseed));
+		err = mlkem_encaps_internal(ws->ct, ws->ss1, pk, ws->eseed,
+					    params);
+		if (err == -ENOMEM)
+			return -ENOMEM; /* Out of memory, not a PCT failure */
+		if (err)
+			panic("mlkem: FIPS PCT failed (encaps): %d", err);
+		err = mlkem_decaps(ws->ss2, ws->ct, sk, params);
+		if (err == -ENOMEM)
+			return -ENOMEM; /* Out of memory, not a PCT failure */
+		if (err)
+			panic("mlkem: FIPS PCT failed (decaps): %d", err);
+		if (crypto_memneq(ws->ss1, ws->ss2, MLKEM_SHARED_SECRET_BYTES))
+			panic("mlkem: FIPS PCT failed (compare)");
+	}
 	return 0;
 }
 
 int mlkem768_keygen_internal(u8 pk[MLKEM768_PUBLIC_KEY_BYTES],
 			     u8 sk[MLKEM768_SECRET_KEY_BYTES],
@@ -889,10 +926,88 @@ int mlkem1024_decaps(u8 ss[MLKEM_SHARED_SECRET_BYTES],
 {
 	return mlkem_decaps(ss, ct, sk, &mlkem1024);
 }
 EXPORT_SYMBOL_NS_GPL(mlkem1024_decaps, "CRYPTO_INTERNAL");
 
+#ifdef CONFIG_CRYPTO_FIPS
+/*
+ * This function implements the ML-KEM cryptographic algorithm self-tests
+ * required by FIPS 140-3, or rather the FIPS 140-3 Implementation Guidance
+ * document which is where they are actually specified.  The requirement is that
+ * for one ML-KEM parameter set, one test be done for keygen and encapsulation,
+ * and two tests be done for decapsulation to exercise both the normal case and
+ * the implicit rejection case.  It's worded in such a way that the tests don't
+ * necessarily have to be "known-answer tests", but those are what we use.
+ *
+ * This is just for FIPS compliance.  Normal testing done by kernel developers
+ * and integrators should use the much more comprehensive KUnit test suite.
+ */
+static int __init mlkem_mod_init(void)
+{
+	struct {
+		u8 pk[MLKEM768_PUBLIC_KEY_BYTES];
+		u8 sk[MLKEM768_SECRET_KEY_BYTES];
+		u8 ct[MLKEM768_CIPHERTEXT_BYTES];
+		u8 ss[MLKEM_SHARED_SECRET_BYTES];
+	} *bufs __free(kfree_sensitive) = NULL;
+	int err;
+
+	if (!fips_enabled)
+		return 0;
+
+	bufs = kmalloc_obj(*bufs);
+	if (!bufs)
+		panic("mlkem: fips test failed: ENOMEM");
+
+	err = mlkem768_keygen_internal(bufs->pk, bufs->sk,
+				       fips_test_mlkem768_seed);
+	if (err)
+		panic("mlkem: fips test failed: keygen failed with err=%d",
+		      err);
+	if (crypto_memneq(bufs->pk, fips_test_mlkem768_pk, sizeof(bufs->pk)))
+		panic("mlkem: fips test failed: wrong public key");
+	if (crypto_memneq(bufs->sk, fips_test_mlkem768_sk, sizeof(bufs->sk)))
+		panic("mlkem: fips test failed: wrong secret key");
+
+	err = mlkem768_encaps_internal(bufs->ct, bufs->ss, bufs->pk,
+				       fips_test_mlkem768_eseed);
+	if (err)
+		panic("mlkem: fips test failed: encaps failed with err=%d",
+		      err);
+
+	if (crypto_memneq(bufs->ct, fips_test_mlkem768_ct, sizeof(bufs->ct)))
+		panic("mlkem: fips test failed: wrong ciphertext");
+	if (crypto_memneq(bufs->ss, fips_test_mlkem768_ss, sizeof(bufs->ss)))
+		panic("mlkem: fips test failed: wrong shared secret after encaps");
+
+	memset(bufs->ss, 0, sizeof(bufs->ss));
+	err = mlkem768_decaps(bufs->ss, bufs->ct, bufs->sk);
+	if (err)
+		panic("mlkem: fips test failed: valid decaps failed with err=%d",
+		      err);
+	if (crypto_memneq(bufs->ss, fips_test_mlkem768_ss, sizeof(bufs->ss)))
+		panic("mlkem: fips test failed: wrong shared secret after valid decaps");
+
+	err = mlkem768_decaps(bufs->ss, fips_test_mlkem768_ct_invalid,
+			      bufs->sk);
+	if (err)
+		panic("mlkem: fips test failed: invalid decaps failed with err=%d",
+		      err);
+	if (crypto_memneq(bufs->ss, fips_test_mlkem768_ss_rejected,
+			  sizeof(bufs->ss)))
+		panic("mlkem: fips test failed: wrong shared secret after invalid decaps");
+
+	return 0;
+}
+subsys_initcall(mlkem_mod_init);
+
+static void __exit mlkem_mod_exit(void)
+{
+}
+module_exit(mlkem_mod_exit);
+#endif /* CONFIG_CRYPTO_FIPS */
+
 #if IS_ENABLED(CONFIG_CRYPTO_LIB_MLKEM_KUNIT_TEST)
 u16 mlkem_reduce_once(u16 x)
 {
 	return reduce_once(x);
 }
diff --git a/scripts/crypto/import-mlkem-testvecs.py b/scripts/crypto/import-mlkem-testvecs.py
index 1d5681ccfee1..ebc76ab218e4 100755
--- a/scripts/crypto/import-mlkem-testvecs.py
+++ b/scripts/crypto/import-mlkem-testvecs.py
@@ -11,10 +11,11 @@
 #   $PATH_TO_THIS_SCRIPT ./test/
 #
 # This script generates the following files:
 #
 #   lib/crypto/tests/mlkem-testvecs.h
+#   lib/crypto/fips-mlkem.h
 
 import hashlib
 import os
 import sys
 
@@ -32,10 +33,13 @@ MLKEM_LENGTHS = {
         "sk_len": 3168,
         "ct_len": 1568,
     },
 }
 
+MLKEM_SEED_BYTES = 64
+MLKEM_ESEED_BYTES = 32
+
 
 def print_header(file):
     print("/* SPDX-License-Identifier: GPL-2.0-or-later */", file=file)
     print(f"/* This file was generated by {SCRIPT_NAME} */", file=file)
     print(f"/* clang-format off */", file=file)
@@ -97,10 +101,66 @@ def hash_testvecs(testvecs):
         h.update(tv.ss)  # From decapsulation
         h.update(tv.ss_rejected)
     return h.digest(length=32)
 
 
+def gen_fips_file(tv):
+    with open(LINUX_DIR + "/lib/crypto/fips-mlkem.h", "w") as file:
+        ct_len = MLKEM_LENGTHS[768]["ct_len"]
+        print_header(file)
+        attribs = " __initconst __maybe_unused"
+
+        # The test vectors were generated deterministically using the SHAKE128
+        # of an empty string as the randomness.  Take the first MLKEM_SEED_BYTES
+        # + MLKEM_ESEED_BYTES to get the seeds used by the first test vector.
+        # Take an additional ct_len bytes to get the invalid ciphertext.
+        rnd_data = hashlib.shake_128().digest(
+            MLKEM_SEED_BYTES + MLKEM_ESEED_BYTES + ct_len
+        )
+
+        print_static_u8_array_definition(
+            file,
+            f"fips_test_mlkem768_seed[MLKEM_SEED_BYTES]{attribs}",
+            rnd_data[:MLKEM_SEED_BYTES],
+        )
+        print_static_u8_array_definition(
+            file,
+            f"fips_test_mlkem768_eseed[MLKEM_ESEED_BYTES]{attribs}",
+            rnd_data[MLKEM_SEED_BYTES : MLKEM_SEED_BYTES + MLKEM_ESEED_BYTES],
+        )
+        print_static_u8_array_definition(
+            file,
+            f"fips_test_mlkem768_pk[MLKEM768_PUBLIC_KEY_BYTES]{attribs}",
+            tv.pk,
+        )
+        print_static_u8_array_definition(
+            file,
+            f"fips_test_mlkem768_sk[MLKEM768_SECRET_KEY_BYTES]{attribs}",
+            tv.sk,
+        )
+        print_static_u8_array_definition(
+            file,
+            f"fips_test_mlkem768_ct[MLKEM768_CIPHERTEXT_BYTES]{attribs}",
+            tv.ct,
+        )
+        print_static_u8_array_definition(
+            file,
+            f"fips_test_mlkem768_ss[MLKEM_SHARED_SECRET_BYTES]{attribs}",
+            tv.ss,
+        )
+        print_static_u8_array_definition(
+            file,
+            f"fips_test_mlkem768_ct_invalid[MLKEM768_CIPHERTEXT_BYTES]{attribs}",
+            rnd_data[MLKEM_SEED_BYTES + MLKEM_ESEED_BYTES :],
+        )
+        print_static_u8_array_definition(
+            file,
+            f"fips_test_mlkem768_ss_rejected[MLKEM_SHARED_SECRET_BYTES]{attribs}",
+            tv.ss_rejected,
+        )
+
+
 if len(sys.argv) != 2:
     sys.stderr.write(f"Usage: {SCRIPT_NAME} TESTVECS_DIR\n")
     sys.exit(2)
 testvecs_dir = sys.argv[1]
 
@@ -113,5 +173,7 @@ with open(LINUX_DIR + "/lib/crypto/tests/mlkem-testvecs.h", "w") as file:
         num_testvecs = min(len(testvecs), 1000)
         testvecs = testvecs[:num_testvecs]
         hash = hash_testvecs(testvecs)
         print(f"\n#define MLKEM{paramset}_NUM_TESTVECS {num_testvecs}", file=file)
         print_static_u8_array_definition(file, f"mlkem{paramset}_hash[32]", hash)
+        if paramset == 768:  # FIPS only requires tests for one parameter set
+            gen_fips_file(testvecs[0])
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 4/5] lib/crypto: xwing: Add support for X-Wing KEM
  2026-05-25 18:43 [PATCH 0/5] ML-KEM and X-Wing support Eric Biggers
                   ` (2 preceding siblings ...)
  2026-05-25 18:44 ` [PATCH 3/5] lib/crypto: mlkem: Add FIPS 140-3 tests Eric Biggers
@ 2026-05-25 18:44 ` Eric Biggers
  2026-05-25 18:44 ` [PATCH 5/5] lib/crypto: xwing: Add KUnit tests " Eric Biggers
  4 siblings, 0 replies; 6+ messages in thread
From: Eric Biggers @ 2026-05-25 18:44 UTC (permalink / raw)
  To: linux-crypto
  Cc: linux-kernel, Ard Biesheuvel, Jason A . Donenfeld, Herbert Xu,
	Ryan Appel, Chris Leech, Eric Biggers

Add support for X-Wing, a general-purpose post-quantum/traditional
hybrid key encapsulation mechanism using X25519 and ML-KEM-768.  X-Wing
is the recommended KEM for new applications.  X-Wing is specified at
https://datatracker.ietf.org/doc/html/draft-connolly-cfrg-xwing-kem-10

This is a proof-of-concept that won't be merged until there's an
in-kernel user.  Potential users include in-kernel users of classical
key agreement schemes (Bluetooth, NVMe auth, WireGuard).

Signed-off-by: Eric Biggers <ebiggers@kernel.org>
---
 Documentation/crypto/libcrypto-asymmetric.rst |   7 +
 include/crypto/xwing.h                        |  84 +++++++
 lib/crypto/Kconfig                            |   9 +
 lib/crypto/Makefile                           |   5 +
 lib/crypto/xwing.c                            | 237 ++++++++++++++++++
 5 files changed, 342 insertions(+)
 create mode 100644 include/crypto/xwing.h
 create mode 100644 lib/crypto/xwing.c

diff --git a/Documentation/crypto/libcrypto-asymmetric.rst b/Documentation/crypto/libcrypto-asymmetric.rst
index 6e71c5ce823f..d44ee74f6a46 100644
--- a/Documentation/crypto/libcrypto-asymmetric.rst
+++ b/Documentation/crypto/libcrypto-asymmetric.rst
@@ -16,5 +16,12 @@ ML-KEM
 Support for the ML-KEM key encapsulation mechanism.
 
 This shall be used as part of a hybrid scheme such as X-Wing, not by itself.
 
 .. kernel-doc:: include/crypto/mlkem.h
+
+X-Wing
+------
+
+Support for the X-Wing key encapsulation mechanism.
+
+.. kernel-doc:: include/crypto/xwing.h
diff --git a/include/crypto/xwing.h b/include/crypto/xwing.h
new file mode 100644
index 000000000000..d1ad7b1e6596
--- /dev/null
+++ b/include/crypto/xwing.h
@@ -0,0 +1,84 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright 2026 Google LLC
+ */
+
+/**
+ * DOC: X-Wing key encapsulation mechanism
+ *
+ * Implementation of X-Wing, a general-purpose post-quantum/traditional hybrid
+ * key encapsulation mechanism using X25519 and ML-KEM-768.  X-Wing is the
+ * recommended KEM for new applications.  X-Wing is specified at
+ * https://datatracker.ietf.org/doc/html/draft-connolly-cfrg-xwing-kem-10
+ */
+
+#ifndef _CRYPTO_XWING_H
+#define _CRYPTO_XWING_H
+
+#include <linux/types.h>
+
+#define XWING_SEED_BYTES 32 /* Length of seed for KeyGen */
+#define XWING_PUBLIC_KEY_BYTES 1216
+#define XWING_SECRET_KEY_BYTES 32
+#define XWING_ESEED_BYTES 64 /* Length of seed for Encaps */
+#define XWING_CIPHERTEXT_BYTES 1120
+#define XWING_SHARED_SECRET_BYTES 32
+
+/**
+ * xwing_keygen() - Generate an X-Wing key pair
+ * @pk: (output) The public key (encapsulation key)
+ * @sk: (output) The secret key (decapsulation key)
+ *
+ * Context: Might sleep
+ *
+ * Return: 0 on success, or -ENOMEM if out of memory.
+ */
+int xwing_keygen(u8 pk[XWING_PUBLIC_KEY_BYTES], u8 sk[XWING_SECRET_KEY_BYTES]);
+
+/**
+ * xwing_encaps() - Generate and encapsulate shared secret with X-Wing
+ * @ct: (output) The ciphertext
+ * @ss: (output) The generated shared secret
+ * @pk: The public key (encapsulation key)
+ *
+ * Context: Might sleep
+ *
+ * Return:
+ * * 0 on success
+ * * -EBADMSG if the public key is malformed
+ * * -ENOMEM if out of memory
+ */
+int xwing_encaps(u8 ct[XWING_CIPHERTEXT_BYTES],
+		 u8 ss[XWING_SHARED_SECRET_BYTES],
+		 const u8 pk[XWING_PUBLIC_KEY_BYTES]);
+
+/**
+ * xwing_decaps() - Decapsulate shared secret with X-Wing
+ * @ss: (output) The decapsulated shared secret
+ * @ct: The ciphertext
+ * @sk: The secret key (decapsulation key)
+ *
+ * Context: Might sleep
+ *
+ * Return:
+ * * 0 on success, including the implicit rejection cases where the ciphertext
+ *   is invalid and a randomized shared secret is returned
+ * * -EBADMSG if the secret key is malformed
+ * * -ENOMEM if out of memory
+ */
+int xwing_decaps(u8 ss[XWING_SHARED_SECRET_BYTES],
+		 const u8 ct[XWING_CIPHERTEXT_BYTES],
+		 const u8 sk[XWING_SECRET_KEY_BYTES]);
+
+#if IS_ENABLED(CONFIG_KUNIT)
+/* Functions taking explicit seeds, only for KUnit testing */
+int xwing_keygen_internal(u8 pk[XWING_PUBLIC_KEY_BYTES],
+			  u8 sk[XWING_SECRET_KEY_BYTES],
+			  const u8 seed[XWING_SEED_BYTES]);
+int xwing_encaps_internal(u8 ct[XWING_CIPHERTEXT_BYTES],
+			  u8 ss[XWING_SHARED_SECRET_BYTES],
+			  const u8 pk[XWING_PUBLIC_KEY_BYTES],
+			  const u8 eseed[XWING_ESEED_BYTES]);
+#endif /* CONFIG_KUNIT */
+
+#endif /* _CRYPTO_XWING_H */
diff --git a/lib/crypto/Kconfig b/lib/crypto/Kconfig
index acaa64af4c6d..2ce1867eeb9e 100644
--- a/lib/crypto/Kconfig
+++ b/lib/crypto/Kconfig
@@ -285,5 +285,14 @@ config CRYPTO_LIB_SM3_ARCH
 	depends on CRYPTO_LIB_SM3 && !UML
 	default y if ARM64
 	default y if RISCV && 64BIT && TOOLCHAIN_HAS_VECTOR_CRYPTO && \
 		     RISCV_EFFICIENT_VECTOR_UNALIGNED_ACCESS
 	default y if X86_64
+
+config CRYPTO_LIB_XWING
+	tristate
+	select CRYPTO_LIB_CURVE25519
+	select CRYPTO_LIB_MLKEM
+	select CRYPTO_LIB_SHA3
+	help
+	  The X-Wing library functions.  Select this if your module uses any of
+	  the functions from <crypto/xwing.h>.
diff --git a/lib/crypto/Makefile b/lib/crypto/Makefile
index 94cef4e574cd..a20ae074cbfa 100644
--- a/lib/crypto/Makefile
+++ b/lib/crypto/Makefile
@@ -384,10 +384,15 @@ libsm3-$(CONFIG_RISCV) += riscv/sm3-riscv64-zvksh-zvkb.o
 libsm3-$(CONFIG_X86) += x86/sm3-avx-asm_64.o
 endif # CONFIG_CRYPTO_LIB_SM3_ARCH
 
 ################################################################################
 
+obj-$(CONFIG_CRYPTO_LIB_XWING) += libxwing.o
+libxwing-y := xwing.o
+
+################################################################################
+
 obj-$(CONFIG_MPILIB) += mpi/
 
 obj-$(CONFIG_CRYPTO_SELFTESTS_FULL)		+= simd.o
 
 # clean-files must be defined unconditionally
diff --git a/lib/crypto/xwing.c b/lib/crypto/xwing.c
new file mode 100644
index 000000000000..deffa46b900e
--- /dev/null
+++ b/lib/crypto/xwing.c
@@ -0,0 +1,237 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * X-Wing key encapsulation mechanism
+ *
+ * See include/crypto/xwing.h for the documentation.
+ *
+ * Copyright 2026 Google LLC
+ */
+
+#include <crypto/curve25519.h>
+#include <crypto/mlkem.h>
+#include <crypto/sha3.h>
+#include <crypto/xwing.h>
+#include <kunit/visibility.h>
+#include <linux/export.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+
+/* pk = pk_mlkem || pk_x25519 */
+static_assert(XWING_PUBLIC_KEY_BYTES ==
+	      MLKEM768_PUBLIC_KEY_BYTES + CURVE25519_KEY_SIZE);
+
+/* sk = seed */
+static_assert(XWING_SECRET_KEY_BYTES == XWING_SEED_BYTES);
+
+/* ct = ct_mlkem || ct_x25519 */
+static_assert(XWING_CIPHERTEXT_BYTES ==
+	      MLKEM768_CIPHERTEXT_BYTES + CURVE25519_KEY_SIZE);
+
+/* expanded_sk = sk_mlkem || sk_x25519 || pk_mlkem || pk_x25519 */
+#define XWING_EXPANDED_SECRET_KEY_BYTES                    \
+	(MLKEM768_SECRET_KEY_BYTES + CURVE25519_KEY_SIZE + \
+	 MLKEM768_PUBLIC_KEY_BYTES + CURVE25519_KEY_SIZE)
+
+static int xwing_expand_sk(u8 expanded_sk[XWING_EXPANDED_SECRET_KEY_BYTES],
+			   const u8 sk[XWING_SECRET_KEY_BYTES])
+{
+	u8 *sk_mlkem = &expanded_sk[0];
+	u8 *sk_x25519 = &sk_mlkem[MLKEM768_SECRET_KEY_BYTES];
+	u8 *pk_mlkem = &sk_x25519[CURVE25519_KEY_SIZE];
+	u8 *pk_x25519 = &pk_mlkem[MLKEM768_PUBLIC_KEY_BYTES];
+	u8 seed_mlkem[MLKEM_SEED_BYTES];
+	struct shake_ctx shake;
+	int err;
+
+	shake256_init(&shake);
+	shake_update(&shake, sk, XWING_SECRET_KEY_BYTES);
+	shake_squeeze(&shake, seed_mlkem, sizeof(seed_mlkem));
+
+	err = mlkem768_keygen_internal(pk_mlkem, sk_mlkem, seed_mlkem);
+	if (err) /* can only be -ENOMEM */
+		goto out;
+	shake_squeeze(&shake, sk_x25519, CURVE25519_KEY_SIZE);
+	curve25519_clamp_secret(sk_x25519);
+	if (unlikely(!curve25519_generate_public(pk_x25519, sk_x25519)))
+		err = -EAGAIN;
+out:
+	shake_zeroize_ctx(&shake);
+	memzero_explicit(seed_mlkem, sizeof(seed_mlkem));
+	return err;
+}
+
+VISIBLE_IF_KUNIT int xwing_keygen_internal(u8 pk[XWING_PUBLIC_KEY_BYTES],
+					   u8 sk[XWING_SECRET_KEY_BYTES],
+					   const u8 seed[XWING_SEED_BYTES])
+{
+	u8 *expanded_sk __free(kfree_sensitive) =
+		kmalloc(XWING_EXPANDED_SECRET_KEY_BYTES, GFP_KERNEL);
+	int err;
+
+	if (!expanded_sk)
+		return -ENOMEM;
+
+	err = xwing_expand_sk(expanded_sk, seed);
+	if (err)
+		return err;
+	/* pk = pk_mlkem || pk_x25519 */
+	memcpy(pk,
+	       &expanded_sk[MLKEM768_SECRET_KEY_BYTES + CURVE25519_KEY_SIZE],
+	       XWING_PUBLIC_KEY_BYTES);
+	/* sk = seed */
+	memcpy(sk, seed, XWING_SECRET_KEY_BYTES);
+	return 0;
+}
+EXPORT_SYMBOL_IF_KUNIT(xwing_keygen_internal);
+
+int xwing_keygen(u8 pk[XWING_PUBLIC_KEY_BYTES], u8 sk[XWING_SECRET_KEY_BYTES])
+{
+	u8 seed[XWING_SEED_BYTES];
+	int err;
+
+	do {
+		get_random_bytes(seed, sizeof(seed));
+		err = xwing_keygen_internal(pk, sk, seed);
+	} while (err == -EAGAIN); /* curve25519_null_point case */
+	memzero_explicit(seed, sizeof(seed));
+	return err;
+}
+EXPORT_SYMBOL_GPL(xwing_keygen);
+
+static void xwing_combine(u8 ss[XWING_SHARED_SECRET_BYTES],
+			  const u8 ss_mlkem[MLKEM_SHARED_SECRET_BYTES],
+			  const u8 ss_x25519[CURVE25519_KEY_SIZE],
+			  const u8 ct_x25519[CURVE25519_KEY_SIZE],
+			  const u8 pk_x25519[CURVE25519_KEY_SIZE])
+{
+	static const u8 xwing_label[6] = { 0x5c, 0x2e, 0x2f, 0x2f, 0x5e, 0x5c };
+	struct sha3_ctx ctx;
+
+	sha3_256_init(&ctx);
+	sha3_update(&ctx, ss_mlkem, MLKEM_SHARED_SECRET_BYTES);
+	sha3_update(&ctx, ss_x25519, CURVE25519_KEY_SIZE);
+	sha3_update(&ctx, ct_x25519, CURVE25519_KEY_SIZE);
+	sha3_update(&ctx, pk_x25519, CURVE25519_KEY_SIZE);
+	sha3_update(&ctx, xwing_label, sizeof(xwing_label));
+	sha3_final(&ctx, ss);
+}
+
+VISIBLE_IF_KUNIT int xwing_encaps_internal(u8 ct[XWING_CIPHERTEXT_BYTES],
+					   u8 ss[XWING_SHARED_SECRET_BYTES],
+					   const u8 pk[XWING_PUBLIC_KEY_BYTES],
+					   const u8 eseed[XWING_ESEED_BYTES])
+{
+	const u8 *pk_mlkem = &pk[0];
+	const u8 *pk_x25519 = &pk[MLKEM768_PUBLIC_KEY_BYTES];
+	const u8 *eseed_mlkem = &eseed[0];
+	const u8 *eseed_x25519 = &eseed[MLKEM_ESEED_BYTES];
+	u8 eph_sk_x25519[CURVE25519_KEY_SIZE];
+	u8 *ct_mlkem = &ct[0];
+	u8 *ct_x25519 = &ct[MLKEM768_CIPHERTEXT_BYTES];
+	u8 ss_mlkem[MLKEM_SHARED_SECRET_BYTES];
+	u8 ss_x25519[CURVE25519_KEY_SIZE];
+	int err;
+
+	err = mlkem768_encaps_internal(ct_mlkem, ss_mlkem, pk_mlkem,
+				       eseed_mlkem);
+	if (err)
+		goto out;
+	memcpy(eph_sk_x25519, eseed_x25519, CURVE25519_KEY_SIZE);
+	curve25519_clamp_secret(eph_sk_x25519);
+	if (!curve25519_generate_public(ct_x25519, eph_sk_x25519)) {
+		err = -EAGAIN;
+		goto out;
+	}
+	if (!curve25519(ss_x25519, eph_sk_x25519, pk_x25519)) {
+		err = -EBADMSG;
+		goto out;
+	}
+	xwing_combine(ss, ss_mlkem, ss_x25519, ct_x25519, pk_x25519);
+	err = 0;
+out:
+	if (err) {
+		get_random_bytes(ct, XWING_CIPHERTEXT_BYTES);
+		get_random_bytes(ss, XWING_SHARED_SECRET_BYTES);
+	}
+	memzero_explicit(eph_sk_x25519, sizeof(eph_sk_x25519));
+	memzero_explicit(ss_mlkem, sizeof(ss_mlkem));
+	memzero_explicit(ss_x25519, sizeof(ss_x25519));
+	return err;
+}
+EXPORT_SYMBOL_IF_KUNIT(xwing_encaps_internal);
+
+int xwing_encaps(u8 ct[XWING_CIPHERTEXT_BYTES],
+		 u8 ss[XWING_SHARED_SECRET_BYTES],
+		 const u8 pk[XWING_PUBLIC_KEY_BYTES])
+{
+	u8 eseed[XWING_ESEED_BYTES];
+	int err;
+
+	do {
+		get_random_bytes(eseed, sizeof(eseed));
+		err = xwing_encaps_internal(ct, ss, pk, eseed);
+	} while (err == -EAGAIN); /* curve25519_null_point case */
+	memzero_explicit(eseed, sizeof(eseed));
+	return err;
+}
+EXPORT_SYMBOL_GPL(xwing_encaps);
+
+int xwing_decaps(u8 ss[XWING_SHARED_SECRET_BYTES],
+		 const u8 ct[XWING_CIPHERTEXT_BYTES],
+		 const u8 sk[XWING_SECRET_KEY_BYTES])
+{
+	u8 *expanded_sk __free(kfree_sensitive) =
+		kmalloc(XWING_EXPANDED_SECRET_KEY_BYTES, GFP_KERNEL);
+	u8 *sk_mlkem, *sk_x25519, *pk_mlkem, *pk_x25519;
+	const u8 *ct_mlkem = &ct[0];
+	const u8 *ct_x25519 = &ct[MLKEM768_CIPHERTEXT_BYTES];
+	u8 ss_mlkem[MLKEM_SHARED_SECRET_BYTES];
+	u8 ss_x25519[CURVE25519_KEY_SIZE];
+	int err;
+
+	if (!expanded_sk) {
+		err = -ENOMEM;
+		goto out;
+	}
+	err = xwing_expand_sk(expanded_sk, sk);
+	if (err) {
+		if (err == -EAGAIN) /* curve25519_null_point case */
+			err = -EBADMSG;
+		goto out;
+	}
+	sk_mlkem = &expanded_sk[0];
+	sk_x25519 = &sk_mlkem[MLKEM768_SECRET_KEY_BYTES];
+	pk_mlkem = &sk_x25519[CURVE25519_KEY_SIZE];
+	pk_x25519 = &pk_mlkem[MLKEM768_PUBLIC_KEY_BYTES];
+
+	err = mlkem768_decaps(ss_mlkem, ct_mlkem, sk_mlkem);
+	if (err) {
+		/*
+		 * This is either -ENOMEM, or -EBADMSG for a malformed secret
+		 * key.  This case is *not* reached if the ciphertext is
+		 * invalid, as implicit rejection is used.
+		 */
+		goto out;
+	}
+	if (!curve25519(ss_x25519, sk_x25519, ct_x25519)) {
+		/*
+		 * ss_x25519 is curve25519_null_point, which can happen if the
+		 * ciphertext is invalid.  In this case the correct behavior is
+		 * to continue anyway and implicitly reject.
+		 */
+	}
+
+	xwing_combine(ss, ss_mlkem, ss_x25519, ct_x25519, pk_x25519);
+	err = 0;
+out:
+	if (err)
+		get_random_bytes(ss, XWING_SHARED_SECRET_BYTES);
+	memzero_explicit(ss_mlkem, sizeof(ss_mlkem));
+	memzero_explicit(ss_x25519, sizeof(ss_x25519));
+	return err;
+}
+EXPORT_SYMBOL_GPL(xwing_decaps);
+
+MODULE_DESCRIPTION("X-Wing key encapsulation mechanism");
+MODULE_IMPORT_NS("CRYPTO_INTERNAL");
+MODULE_LICENSE("GPL");
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 5/5] lib/crypto: xwing: Add KUnit tests for X-Wing KEM
  2026-05-25 18:43 [PATCH 0/5] ML-KEM and X-Wing support Eric Biggers
                   ` (3 preceding siblings ...)
  2026-05-25 18:44 ` [PATCH 4/5] lib/crypto: xwing: Add support for X-Wing KEM Eric Biggers
@ 2026-05-25 18:44 ` Eric Biggers
  4 siblings, 0 replies; 6+ messages in thread
From: Eric Biggers @ 2026-05-25 18:44 UTC (permalink / raw)
  To: linux-crypto
  Cc: linux-kernel, Ard Biesheuvel, Jason A . Donenfeld, Herbert Xu,
	Ryan Appel, Chris Leech, Eric Biggers

Add a KUnit test suite for the X-Wing key encapsulation mechanism.

This includes:

- Test key generation, encapsulation, and decapsulation against the
  test vectors from the X-Wing specification
- Test encapsulation/decapsulation round trips
- Benchmark key generation, encapsulation, and decapsulation

This isn't intended to fully test the underlying KEMs.  Those already
have their own KUnit tests.

Signed-off-by: Eric Biggers <ebiggers@kernel.org>
---
 lib/crypto/.kunitconfig                 |   1 +
 lib/crypto/tests/Kconfig                |   9 ++
 lib/crypto/tests/Makefile               |   1 +
 lib/crypto/tests/xwing-testvecs.h       | 138 ++++++++++++++++++++++++
 lib/crypto/tests/xwing_kunit.c          | 129 ++++++++++++++++++++++
 scripts/crypto/import-xwing-testvecs.py | 111 +++++++++++++++++++
 6 files changed, 389 insertions(+)
 create mode 100644 lib/crypto/tests/xwing-testvecs.h
 create mode 100644 lib/crypto/tests/xwing_kunit.c
 create mode 100755 scripts/crypto/import-xwing-testvecs.py

diff --git a/lib/crypto/.kunitconfig b/lib/crypto/.kunitconfig
index 32e5b4471da8..b42d8aaed244 100644
--- a/lib/crypto/.kunitconfig
+++ b/lib/crypto/.kunitconfig
@@ -17,5 +17,6 @@ CONFIG_CRYPTO_LIB_POLYVAL_KUNIT_TEST=y
 CONFIG_CRYPTO_LIB_SHA1_KUNIT_TEST=y
 CONFIG_CRYPTO_LIB_SHA256_KUNIT_TEST=y
 CONFIG_CRYPTO_LIB_SHA512_KUNIT_TEST=y
 CONFIG_CRYPTO_LIB_SHA3_KUNIT_TEST=y
 CONFIG_CRYPTO_LIB_SM3_KUNIT_TEST=y
+CONFIG_CRYPTO_LIB_XWING_KUNIT_TEST=y
diff --git a/lib/crypto/tests/Kconfig b/lib/crypto/tests/Kconfig
index 0a110a0733d2..485b5fd81539 100644
--- a/lib/crypto/tests/Kconfig
+++ b/lib/crypto/tests/Kconfig
@@ -147,10 +147,18 @@ config CRYPTO_LIB_SM3_KUNIT_TEST
 	default KUNIT_ALL_TESTS
 	select CRYPTO_LIB_BENCHMARK_VISIBLE
 	help
 	  KUnit tests for the SM3 cryptographic hash function.
 
+config CRYPTO_LIB_XWING_KUNIT_TEST
+	tristate "KUnit tests for X-Wing" if !KUNIT_ALL_TESTS
+	depends on KUNIT && CRYPTO_LIB_XWING
+	default KUNIT_ALL_TESTS
+	select CRYPTO_LIB_BENCHMARK_VISIBLE
+	help
+	  KUnit tests for the X-Wing key encapsulation mechanism.
+
 config CRYPTO_LIB_ENABLE_ALL_FOR_KUNIT
 	tristate "Enable all crypto library code for KUnit tests"
 	depends on KUNIT
 	select CRYPTO_LIB_AES_CBC_MACS
 	select CRYPTO_LIB_BLAKE2B
@@ -165,10 +173,11 @@ config CRYPTO_LIB_ENABLE_ALL_FOR_KUNIT
 	select CRYPTO_LIB_SHA1
 	select CRYPTO_LIB_SHA256
 	select CRYPTO_LIB_SHA512
 	select CRYPTO_LIB_SHA3
 	select CRYPTO_LIB_SM3
+	select CRYPTO_LIB_XWING
 	help
 	  Enable all the crypto library code that has KUnit tests.
 
 	  Enable this only if you'd like to test all the crypto library code,
 	  even code that wouldn't otherwise need to be built.
diff --git a/lib/crypto/tests/Makefile b/lib/crypto/tests/Makefile
index 3a73d2f33f75..acc8ddc19978 100644
--- a/lib/crypto/tests/Makefile
+++ b/lib/crypto/tests/Makefile
@@ -15,5 +15,6 @@ obj-$(CONFIG_CRYPTO_LIB_POLYVAL_KUNIT_TEST) += polyval_kunit.o
 obj-$(CONFIG_CRYPTO_LIB_SHA1_KUNIT_TEST) += sha1_kunit.o
 obj-$(CONFIG_CRYPTO_LIB_SHA256_KUNIT_TEST) += sha224_kunit.o sha256_kunit.o
 obj-$(CONFIG_CRYPTO_LIB_SHA512_KUNIT_TEST) += sha384_kunit.o sha512_kunit.o
 obj-$(CONFIG_CRYPTO_LIB_SHA3_KUNIT_TEST) += sha3_kunit.o
 obj-$(CONFIG_CRYPTO_LIB_SM3_KUNIT_TEST) += sm3_kunit.o
+obj-$(CONFIG_CRYPTO_LIB_XWING_KUNIT_TEST) += xwing_kunit.o
diff --git a/lib/crypto/tests/xwing-testvecs.h b/lib/crypto/tests/xwing-testvecs.h
new file mode 100644
index 000000000000..057ca37c7ef7
--- /dev/null
+++ b/lib/crypto/tests/xwing-testvecs.h
@@ -0,0 +1,138 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* This file was generated by import-xwing-testvecs.py */
+
+static const struct xwing_testvec {
+	u8 seed[XWING_SEED_BYTES];
+	u8 pk_hash[SHA3_256_DIGEST_SIZE];
+	u8 sk_hash[SHA3_256_DIGEST_SIZE];
+	u8 eseed[XWING_ESEED_BYTES];
+	u8 ct_hash[SHA3_256_DIGEST_SIZE];
+	u8 ss[XWING_SHARED_SECRET_BYTES];
+} xwing_testvecs[] = {
+	{
+		.seed = {
+			0x7f, 0x9c, 0x2b, 0xa4, 0xe8, 0x8f, 0x82, 0x7d,
+			0x61, 0x60, 0x45, 0x50, 0x76, 0x05, 0x85, 0x3e,
+			0xd7, 0x3b, 0x80, 0x93, 0xf6, 0xef, 0xbc, 0x88,
+			0xeb, 0x1a, 0x6e, 0xac, 0xfa, 0x66, 0xef, 0x26,
+		},
+		.pk_hash = {
+			0x51, 0x21, 0x74, 0x59, 0x04, 0x64, 0x3a, 0xd9,
+			0xdf, 0xac, 0xca, 0x78, 0x69, 0x29, 0x2c, 0x19,
+			0xa8, 0xa6, 0x95, 0x33, 0xb5, 0x3e, 0x60, 0x66,
+			0x6b, 0x7d, 0xb9, 0x10, 0xb4, 0xad, 0x63, 0x67,
+		},
+		.sk_hash = {
+			0x1f, 0x32, 0xc4, 0xb1, 0xa9, 0x65, 0xfa, 0x90,
+			0x33, 0xb7, 0x85, 0xc2, 0xa2, 0x4b, 0x81, 0xdc,
+			0xd1, 0xbc, 0x81, 0xe9, 0xbe, 0x0c, 0x20, 0x4f,
+			0x20, 0xfd, 0xd1, 0x50, 0x92, 0xbb, 0x6d, 0x4a,
+		},
+		.eseed = {
+			0x3c, 0xb1, 0xee, 0xa9, 0x88, 0x00, 0x4b, 0x93,
+			0x10, 0x3c, 0xfb, 0x0a, 0xee, 0xfd, 0x2a, 0x68,
+			0x6e, 0x01, 0xfa, 0x4a, 0x58, 0xe8, 0xa3, 0x63,
+			0x9c, 0xa8, 0xa1, 0xe3, 0xf9, 0xae, 0x57, 0xe2,
+			0x35, 0xb8, 0xcc, 0x87, 0x3c, 0x23, 0xdc, 0x62,
+			0xb8, 0xd2, 0x60, 0x16, 0x9a, 0xfa, 0x2f, 0x75,
+			0xab, 0x91, 0x6a, 0x58, 0xd9, 0x74, 0x91, 0x88,
+			0x35, 0xd2, 0x5e, 0x6a, 0x43, 0x50, 0x85, 0xb2,
+		},
+		.ct_hash = {
+			0xc0, 0xab, 0xd1, 0x49, 0xf8, 0x3f, 0x45, 0x32,
+			0x4a, 0xc3, 0xa7, 0xdd, 0xc7, 0x60, 0x6c, 0x71,
+			0xf2, 0x57, 0xe5, 0xea, 0x86, 0x11, 0x35, 0x22,
+			0x83, 0x4a, 0x0e, 0xe1, 0xbc, 0xb3, 0x4e, 0x3e,
+		},
+		.ss = {
+			0xd2, 0xdf, 0x05, 0x22, 0x12, 0x8f, 0x09, 0xdd,
+			0x8e, 0x2c, 0x92, 0xb1, 0xe9, 0x05, 0xc7, 0x93,
+			0xd8, 0xf5, 0x7a, 0x54, 0xc3, 0xda, 0x25, 0x86,
+			0x1f, 0x10, 0xbf, 0x4c, 0xa6, 0x13, 0xe3, 0x84,
+		},
+	},
+	{
+		.seed = {
+			0xba, 0xdf, 0xd6, 0xdf, 0xaa, 0xc3, 0x59, 0xa5,
+			0xef, 0xbb, 0x7b, 0xcc, 0x4b, 0x59, 0xd5, 0x38,
+			0xdf, 0x9a, 0x04, 0x30, 0x2e, 0x10, 0xc8, 0xbc,
+			0x1c, 0xbf, 0x1a, 0x0b, 0x3a, 0x51, 0x20, 0xea,
+		},
+		.pk_hash = {
+			0x79, 0x9b, 0x60, 0x16, 0xe5, 0xda, 0xa5, 0x6f,
+			0xfa, 0x1b, 0x5e, 0x79, 0xf7, 0xca, 0xf7, 0x34,
+			0x13, 0xce, 0xec, 0xd6, 0xdf, 0x42, 0x86, 0x42,
+			0x40, 0x4c, 0xac, 0x41, 0xdd, 0xee, 0x48, 0x53,
+		},
+		.sk_hash = {
+			0x1d, 0x31, 0x12, 0xef, 0x3c, 0xb6, 0x68, 0x10,
+			0xd3, 0xad, 0x64, 0x18, 0x57, 0x9a, 0x5c, 0x8c,
+			0xd9, 0xd1, 0xfa, 0x13, 0xf6, 0xdd, 0xc7, 0xc1,
+			0x29, 0x7c, 0x09, 0x63, 0xe8, 0xbb, 0xb0, 0x24,
+		},
+		.eseed = {
+			0x17, 0xcd, 0xa7, 0xcf, 0xad, 0x76, 0x5f, 0x56,
+			0x23, 0x47, 0x4d, 0x36, 0x8c, 0xcc, 0xa8, 0xaf,
+			0x00, 0x07, 0xcd, 0x9f, 0x5e, 0x4c, 0x84, 0x9f,
+			0x16, 0x7a, 0x58, 0x0b, 0x14, 0xaa, 0xbd, 0xef,
+			0xae, 0xe7, 0xee, 0xf4, 0x7c, 0xb0, 0xfc, 0xa9,
+			0x76, 0x7b, 0xe1, 0xfd, 0xa6, 0x94, 0x19, 0xdf,
+			0xb9, 0x27, 0xe9, 0xdf, 0x07, 0x34, 0x8b, 0x19,
+			0x66, 0x91, 0xab, 0xae, 0xb5, 0x80, 0xb3, 0x2d,
+		},
+		.ct_hash = {
+			0x76, 0x80, 0xb7, 0xba, 0x47, 0xae, 0x09, 0xba,
+			0xc4, 0xb4, 0x30, 0x01, 0xed, 0xce, 0xf9, 0xd9,
+			0x8e, 0x50, 0xdf, 0x20, 0x02, 0x6e, 0x70, 0xba,
+			0x6a, 0x42, 0x44, 0x47, 0xe1, 0xf2, 0xb9, 0x61,
+		},
+		.ss = {
+			0xf2, 0xe8, 0x62, 0x41, 0xc6, 0x4d, 0x60, 0xf6,
+			0x64, 0x9f, 0xbc, 0x6c, 0x5b, 0x7d, 0x17, 0x18,
+			0x0b, 0x78, 0x0a, 0x3f, 0x34, 0x35, 0x5e, 0x64,
+			0xa8, 0x57, 0x49, 0x94, 0x9c, 0x45, 0xf1, 0x50,
+		},
+	},
+	{
+		.seed = {
+			0xef, 0x58, 0x53, 0x8b, 0x8d, 0x23, 0xf8, 0x77,
+			0x32, 0xea, 0x63, 0xb0, 0x2b, 0x4f, 0xa0, 0xf4,
+			0x87, 0x33, 0x60, 0xe2, 0x84, 0x19, 0x28, 0xcd,
+			0x60, 0xdd, 0x4c, 0xee, 0x8c, 0xc0, 0xd4, 0xc9,
+		},
+		.pk_hash = {
+			0x1e, 0xf0, 0xc9, 0x9a, 0x06, 0x02, 0x64, 0x50,
+			0x56, 0x49, 0x57, 0xa5, 0x40, 0x2a, 0x78, 0x8f,
+			0xef, 0xfb, 0xbe, 0xfd, 0xcc, 0xe5, 0x5d, 0x25,
+			0xde, 0x25, 0x4d, 0x0a, 0x49, 0xeb, 0x09, 0x5a,
+		},
+		.sk_hash = {
+			0x75, 0x1b, 0x92, 0x96, 0x9e, 0xc1, 0x7a, 0x6a,
+			0x5b, 0x4f, 0x95, 0xc0, 0xd2, 0xd9, 0x7d, 0xbc,
+			0x0d, 0xa7, 0xb0, 0x98, 0x96, 0xaf, 0x69, 0xbd,
+			0x69, 0xac, 0x7a, 0xb6, 0x19, 0x9e, 0x40, 0x3d,
+		},
+		.eseed = {
+			0x22, 0xa9, 0x61, 0x88, 0xd0, 0x32, 0x67, 0x5c,
+			0x8a, 0xc8, 0x50, 0x93, 0x3c, 0x7a, 0xff, 0x15,
+			0x33, 0xb9, 0x4c, 0x83, 0x4a, 0xdb, 0xb6, 0x9c,
+			0x61, 0x15, 0xba, 0xd4, 0x69, 0x2d, 0x86, 0x19,
+			0xf9, 0x0b, 0x0c, 0xdf, 0x8a, 0x7b, 0x9c, 0x26,
+			0x40, 0x29, 0xac, 0x18, 0x5b, 0x70, 0xb8, 0x3f,
+			0x28, 0x01, 0xf2, 0xf4, 0xb3, 0xf7, 0x0c, 0x59,
+			0x3e, 0xa3, 0xae, 0xeb, 0x61, 0x3a, 0x7f, 0x1b,
+		},
+		.ct_hash = {
+			0x30, 0x88, 0x68, 0x8d, 0x63, 0x20, 0x1d, 0x5d,
+			0x84, 0x41, 0x70, 0xb7, 0x9f, 0x14, 0x8b, 0x27,
+			0x91, 0xc1, 0x5f, 0x34, 0x6f, 0xf6, 0xf8, 0xbd,
+			0x55, 0x98, 0x07, 0xfb, 0xd4, 0x42, 0xf9, 0x1f,
+		},
+		.ss = {
+			0x95, 0x3f, 0x7f, 0x4e, 0x8c, 0x5b, 0x50, 0x49,
+			0xbd, 0xc7, 0x71, 0xd1, 0xdf, 0xfa, 0xda, 0x0d,
+			0xd9, 0x61, 0x47, 0x7d, 0x1a, 0x2a, 0xe0, 0x98,
+			0x8b, 0xaa, 0x7e, 0xa6, 0x89, 0x8d, 0x89, 0x3f,
+		},
+	},
+};
diff --git a/lib/crypto/tests/xwing_kunit.c b/lib/crypto/tests/xwing_kunit.c
new file mode 100644
index 000000000000..81c4f41e9cab
--- /dev/null
+++ b/lib/crypto/tests/xwing_kunit.c
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * KUnit tests for X-Wing
+ *
+ * This should be run together with the mlkem and curve25519 KUnit tests.
+ *
+ * Copyright 2026 Google LLC
+ */
+#include <crypto/sha3.h>
+#include <crypto/xwing.h>
+#include <kunit/test.h>
+
+#include "xwing-testvecs.h"
+
+struct xwing_bufs {
+	u8 pk[XWING_PUBLIC_KEY_BYTES];
+	u8 sk[XWING_SECRET_KEY_BYTES];
+	u8 ct[XWING_CIPHERTEXT_BYTES];
+	u8 ss[XWING_SHARED_SECRET_BYTES];
+	u8 pk_hash[SHA3_256_DIGEST_SIZE];
+	u8 sk_hash[SHA3_256_DIGEST_SIZE];
+	u8 ct_hash[SHA3_256_DIGEST_SIZE];
+};
+
+static struct xwing_bufs *alloc_bufs(struct kunit *test)
+{
+	struct xwing_bufs *bufs =
+		kunit_kmalloc(test, sizeof(*bufs), GFP_KERNEL);
+
+	KUNIT_ASSERT_NOT_NULL(test, bufs);
+	return bufs;
+}
+
+static void test_xwing_rfc_testvecs(struct kunit *test)
+{
+	struct xwing_bufs *bufs = alloc_bufs(test);
+
+	for (size_t i = 0; i < ARRAY_SIZE(xwing_testvecs); i++) {
+		const struct xwing_testvec *tv = &xwing_testvecs[i];
+
+		KUNIT_ASSERT_EQ(test, 0,
+				xwing_keygen_internal(bufs->pk, bufs->sk,
+						      tv->seed));
+		sha3_256(bufs->pk, sizeof(bufs->pk), bufs->pk_hash);
+		sha3_256(bufs->sk, sizeof(bufs->sk), bufs->sk_hash);
+		KUNIT_ASSERT_MEMEQ(test, tv->pk_hash, bufs->pk_hash,
+				   sizeof(tv->pk_hash));
+		KUNIT_ASSERT_MEMEQ(test, tv->sk_hash, bufs->sk_hash,
+				   sizeof(tv->sk_hash));
+
+		KUNIT_ASSERT_EQ(test, 0,
+				xwing_encaps_internal(bufs->ct, bufs->ss,
+						      bufs->pk, tv->eseed));
+		sha3_256(bufs->ct, sizeof(bufs->ct), bufs->ct_hash);
+		KUNIT_ASSERT_MEMEQ(test, tv->ct_hash, bufs->ct_hash,
+				   sizeof(tv->ct_hash));
+		KUNIT_ASSERT_MEMEQ(test, tv->ss, bufs->ss, sizeof(bufs->ss));
+
+		memset(bufs->ss, 0xff, sizeof(bufs->ss));
+		KUNIT_ASSERT_EQ(test, 0,
+				xwing_decaps(bufs->ss, bufs->ct, bufs->sk));
+		KUNIT_ASSERT_MEMEQ(test, tv->ss, bufs->ss, sizeof(bufs->ss));
+	}
+}
+
+static void test_xwing_round_trip(struct kunit *test)
+{
+	struct xwing_bufs *bufs = alloc_bufs(test);
+	u8 ss2[XWING_SHARED_SECRET_BYTES];
+
+	for (int i = 0; i < 20; i++) {
+		KUNIT_ASSERT_EQ(test, 0, xwing_keygen(bufs->pk, bufs->sk));
+		KUNIT_ASSERT_EQ(test, 0,
+				xwing_encaps(bufs->ct, bufs->ss, bufs->pk));
+		KUNIT_ASSERT_EQ(test, 0, xwing_decaps(ss2, bufs->ct, bufs->sk));
+		KUNIT_ASSERT_MEMEQ(test, bufs->ss, ss2, sizeof(bufs->ss));
+	}
+}
+
+/* Benchmark X-Wing performance. */
+static void benchmark_xwing(struct kunit *test)
+{
+	struct xwing_bufs *bufs = alloc_bufs(test);
+	const int iterations = 100;
+	ktime_t start, end;
+
+	if (!IS_ENABLED(CONFIG_CRYPTO_LIB_BENCHMARK))
+		kunit_skip(test, "not enabled");
+
+	start = ktime_get();
+	for (int i = 0; i < iterations; i++)
+		KUNIT_ASSERT_EQ(test, 0, xwing_keygen(bufs->pk, bufs->sk));
+	end = ktime_get();
+	kunit_info(test, "XWing_KeyGen: %llu ns/op\n",
+		   div64_u64(ktime_to_ns(ktime_sub(end, start)), iterations));
+
+	start = ktime_get();
+	for (int i = 0; i < iterations; i++)
+		KUNIT_ASSERT_EQ(test, 0,
+				xwing_encaps(bufs->ct, bufs->ss, bufs->pk));
+	end = ktime_get();
+	kunit_info(test, "XWing_Encaps: %llu ns/op\n",
+		   div64_u64(ktime_to_ns(ktime_sub(end, start)), iterations));
+
+	start = ktime_get();
+	for (int i = 0; i < iterations; i++)
+		KUNIT_ASSERT_EQ(test, 0,
+				xwing_decaps(bufs->ss, bufs->ct, bufs->sk));
+	end = ktime_get();
+	kunit_info(test, "XWing_Decaps: %llu ns/op\n",
+		   div64_u64(ktime_to_ns(ktime_sub(end, start)), iterations));
+}
+
+static struct kunit_case xwing_test_cases[] = {
+	KUNIT_CASE(test_xwing_rfc_testvecs),
+	KUNIT_CASE(test_xwing_round_trip),
+	KUNIT_CASE(benchmark_xwing),
+	{}
+};
+
+static struct kunit_suite xwing_test_suite = {
+	.name = "xwing",
+	.test_cases = xwing_test_cases,
+};
+kunit_test_suite(xwing_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for X-Wing");
+MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
+MODULE_LICENSE("GPL");
diff --git a/scripts/crypto/import-xwing-testvecs.py b/scripts/crypto/import-xwing-testvecs.py
new file mode 100755
index 000000000000..380685212838
--- /dev/null
+++ b/scripts/crypto/import-xwing-testvecs.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# This script imports test vectors from the X-Wing draft.  To use:
+#
+#    wget https://www.ietf.org/archive/id/draft-connolly-cfrg-xwing-kem-10.txt
+#    $PATH_TO_THIS_SCRIPT draft-connolly-cfrg-xwing-kem-10.txt > lib/crypto/tests/xwing-testvecs.h
+
+import hashlib
+import os
+import sys
+
+SCRIPT_NAME = os.path.basename(__file__)
+FIELD_NAMES = set(["seed", "sk", "pk", "eseed", "ct", "ss"])
+FIRST_FIELD = "seed"
+
+XWING_SEED_BYTES = 32
+XWING_PUBLIC_KEY_BYTES = 1216
+XWING_SECRET_KEY_BYTES = 32
+XWING_ESEED_BYTES = 64
+XWING_CIPHERTEXT_BYTES = 1120
+XWING_SHARED_SECRET_BYTES = 32
+
+
+def check_length(val, expected_len):
+    assert len(val) == expected_len
+    return val
+
+
+class Testvec:
+    def __init__(self, tv):
+        self.seed = check_length(tv["seed"], XWING_SEED_BYTES)
+        self.pk = check_length(tv["pk"], XWING_PUBLIC_KEY_BYTES)
+        self.sk = check_length(tv["sk"], XWING_SECRET_KEY_BYTES)
+        self.eseed = check_length(tv["eseed"], XWING_ESEED_BYTES)
+        self.ct = check_length(tv["ct"], XWING_CIPHERTEXT_BYTES)
+        self.ss = check_length(tv["ss"], XWING_SHARED_SECRET_BYTES)
+
+
+def load_testvecs(file):
+    testvecs = []
+    with open(file) as file:
+        in_appendix = False
+        tv = None
+        cur_field = None
+        for line in file:
+            if line.startswith("Appendix C."):
+                in_appendix = True
+            elif line.startswith("Appendix D."):
+                break
+            fields = line.split()
+            if len(fields) == 0 or not in_appendix:
+                continue
+            if fields[0].strip() in FIELD_NAMES:
+                cur_field = fields[0].strip()
+                if cur_field == FIRST_FIELD:
+                    if tv:
+                        testvecs.append(Testvec(tv))
+                    tv = {}
+                tv[cur_field] = b""
+                if len(fields) == 1:
+                    continue
+                possible_data = fields[1].strip()
+            elif not cur_field:
+                continue
+            else:
+                possible_data = fields[0].strip()
+            try:
+                data = bytes.fromhex(possible_data)
+                tv[cur_field] += data
+            except ValueError:
+                pass
+        testvecs.append(Testvec(tv))
+    return testvecs
+
+
+def print_bytes(prefix, value, bytes_per_line):
+    for i in range(0, len(value), bytes_per_line):
+        line = prefix + "".join(f"0x{b:02x}, " for b in value[i : i + bytes_per_line])
+        print(f"{line.rstrip()}")
+
+
+def print_c_struct_u8_array_field(name, value):
+    print(f"\t\t.{name} = {{")
+    print_bytes("\t\t\t", value, 8)
+    print("\t\t},")
+
+
+testvecs = load_testvecs(sys.argv[1])
+
+print(f"""/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* This file was generated by {SCRIPT_NAME} */
+
+static const struct xwing_testvec {{
+\tu8 seed[XWING_SEED_BYTES];
+\tu8 pk_hash[SHA3_256_DIGEST_SIZE];
+\tu8 sk_hash[SHA3_256_DIGEST_SIZE];
+\tu8 eseed[XWING_ESEED_BYTES];
+\tu8 ct_hash[SHA3_256_DIGEST_SIZE];
+\tu8 ss[XWING_SHARED_SECRET_BYTES];
+}} xwing_testvecs[] = {{""")
+for tv in testvecs:
+    print("\t{")
+    print_c_struct_u8_array_field("seed", tv.seed)
+    print_c_struct_u8_array_field("pk_hash", hashlib.sha3_256(tv.pk).digest())
+    print_c_struct_u8_array_field("sk_hash", hashlib.sha3_256(tv.sk).digest())
+    print_c_struct_u8_array_field("eseed", tv.eseed)
+    print_c_struct_u8_array_field("ct_hash", hashlib.sha3_256(tv.ct).digest())
+    print_c_struct_u8_array_field("ss", tv.ss)
+    print("\t},")
+print("};")
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-05-25 18:46 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-25 18:43 [PATCH 0/5] ML-KEM and X-Wing support Eric Biggers
2026-05-25 18:43 ` [PATCH 1/5] lib/crypto: mlkem: Add ML-KEM-768 and ML-KEM-1024 support Eric Biggers
2026-05-25 18:44 ` [PATCH 2/5] lib/crypto: mlkem: Add KUnit tests for ML-KEM Eric Biggers
2026-05-25 18:44 ` [PATCH 3/5] lib/crypto: mlkem: Add FIPS 140-3 tests Eric Biggers
2026-05-25 18:44 ` [PATCH 4/5] lib/crypto: xwing: Add support for X-Wing KEM Eric Biggers
2026-05-25 18:44 ` [PATCH 5/5] lib/crypto: xwing: Add KUnit tests " Eric Biggers

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox