From 648aa662d9ce34847c3201e6a65801b0c8e2bbb4 Mon Sep 17 00:00:00 2001 From: Paul Adelsbach Date: Wed, 19 Aug 2026 16:47:57 -0700 Subject: [PATCH] Add CB_ONLY mode for ML-KEM --- .github/workflows/cryptocb-only.yml | 10 +- tests/api/test_mlkem.c | 16 +- wolfcrypt/src/cryptocb.c | 1 + wolfcrypt/src/port/arm/armv8-32-mlkem-asm.S | 4 +- wolfcrypt/src/port/arm/armv8-32-mlkem-asm_c.c | 4 +- wolfcrypt/src/port/arm/armv8-mlkem-asm.S | 4 +- wolfcrypt/src/port/arm/armv8-mlkem-asm_c.c | 4 +- wolfcrypt/src/port/arm/thumb2-mlkem-asm.S | 4 +- wolfcrypt/src/port/arm/thumb2-mlkem-asm_c.c | 4 +- wolfcrypt/src/wc_mlkem.c | 54 +++- wolfcrypt/src/wc_mlkem_asm.S | 12 +- wolfcrypt/src/wc_mlkem_poly.c | 16 ++ wolfcrypt/test/test.c | 246 +++++++++++++++++- wolfssl/wolfcrypt/settings.h | 11 + wolfssl/wolfcrypt/wc_mlkem.h | 13 + 15 files changed, 375 insertions(+), 28 deletions(-) diff --git a/.github/workflows/cryptocb-only.yml b/.github/workflows/cryptocb-only.yml index d8883f81acd..3a50e94a4af 100644 --- a/.github/workflows/cryptocb-only.yml +++ b/.github/workflows/cryptocb-only.yml @@ -117,9 +117,15 @@ jobs: {"name": "curve25519", "comment": "WOLF_CRYPTO_CB_ONLY_CURVE25519: strips software X25519 (keygen/shared-secret); swdev provides the software path via cryptocb. Nonblock and async X25519 have no callback path and are left disabled.", "configure": ["CPPFLAGS=-DWOLF_CRYPTO_CB_ONLY_CURVE25519"]}, + {"name": "mlkem", + "comment": "WOLF_CRYPTO_CB_ONLY_MLKEM: strips the ML-KEM lattice math (key generation, encapsulation, decapsulation, the NTT, matrix generation, noise sampling and compression, plus the x86 and ARM assembly). The encode/decode helpers and the hash/PRF object lifecycle stay, because a callback that returns key material needs them. Built with --enable-cryptonly: TLS 1.3 offers ML-KEM key shares by default, and with the software path gone every such handshake needs a registered device, which no test device provides yet.", + "configure": ["--enable-cryptonly", "CPPFLAGS=-DWOLF_CRYPTO_CB_ONLY_MLKEM"]}, {"name": "all", - "comment": "All seven ONLY_* macros at once: every supported software primitive is stripped and dispatched through cryptocb. Catches any cross-algorithm call that a single-strip entry would still resolve via the remaining software paths.", - "configure": ["CPPFLAGS=-DWOLF_CRYPTO_CB_ONLY_ECC -DWOLF_CRYPTO_CB_ONLY_RSA -DWOLF_CRYPTO_CB_ONLY_SHA256 -DWOLF_CRYPTO_CB_ONLY_SHA512 -DWOLF_CRYPTO_CB_ONLY_AES -DWOLF_CRYPTO_CB_ONLY_ED25519 -DWOLF_CRYPTO_CB_ONLY_CURVE25519"]} + "comment": "All seven TLS-compatible ONLY_* macros at once: every supported software primitive is stripped and dispatched through cryptocb. Catches any cross-algorithm call that a single-strip entry would still resolve via the remaining software paths. MLKEM is not here because it needs --enable-cryptonly; the all-cryptonly entry below covers it alongside the rest.", + "configure": ["CPPFLAGS=-DWOLF_CRYPTO_CB_ONLY_ECC -DWOLF_CRYPTO_CB_ONLY_RSA -DWOLF_CRYPTO_CB_ONLY_SHA256 -DWOLF_CRYPTO_CB_ONLY_SHA512 -DWOLF_CRYPTO_CB_ONLY_AES -DWOLF_CRYPTO_CB_ONLY_ED25519 -DWOLF_CRYPTO_CB_ONLY_CURVE25519"]}, + {"name": "all-cryptonly", + "comment": "The 'all' set plus MLKEM, built --enable-cryptonly so ML-KEM can join. This is the only entry where a stripped ML-KEM meets the other stripped primitives; TLS is off because TLS 1.3 offers ML-KEM key shares by default and no test device services them.", + "configure": ["--enable-cryptonly", "CPPFLAGS=-DWOLF_CRYPTO_CB_ONLY_ECC -DWOLF_CRYPTO_CB_ONLY_RSA -DWOLF_CRYPTO_CB_ONLY_SHA256 -DWOLF_CRYPTO_CB_ONLY_SHA512 -DWOLF_CRYPTO_CB_ONLY_AES -DWOLF_CRYPTO_CB_ONLY_ED25519 -DWOLF_CRYPTO_CB_ONLY_CURVE25519 -DWOLF_CRYPTO_CB_ONLY_MLKEM"]} ]} EOF .github/scripts/parallel-make-check.py \ diff --git a/tests/api/test_mlkem.c b/tests/api/test_mlkem.c index 759f25f6449..f1e6e6642ee 100644 --- a/tests/api/test_mlkem.c +++ b/tests/api/test_mlkem.c @@ -39,6 +39,7 @@ int test_wc_mlkem_make_key_kats(void) { EXPECT_DECLS; #if defined(WOLFSSL_HAVE_MLKEM) && \ + defined(WC_MLKEM_HAVE_NATIVE) && \ !defined(WOLFSSL_NO_ML_KEM) && !defined(WOLFSSL_MLKEM_NO_MAKE_KEY) MlKemKey* key; #ifndef WOLFSSL_NO_ML_KEM_512 @@ -1498,6 +1499,7 @@ int test_wc_mlkem_encapsulate_kats(void) { EXPECT_DECLS; #if defined(WOLFSSL_HAVE_MLKEM) && \ + defined(WC_MLKEM_HAVE_NATIVE) && \ !defined(WOLFSSL_NO_ML_KEM) && !defined(WOLFSSL_MLKEM_NO_ENCAPSULATE) MlKemKey* key; #ifndef WOLFSSL_NO_ML_KEM_512 @@ -2472,6 +2474,7 @@ int test_wc_mlkem_decapsulate_kats(void) { EXPECT_DECLS; #if defined(WOLFSSL_HAVE_MLKEM) && \ + defined(WC_MLKEM_HAVE_NATIVE) && \ !defined(WOLFSSL_NO_ML_KEM) && !defined(WOLFSSL_MLKEM_NO_DECAPSULATE) MlKemKey* key; #ifndef WOLFSSL_NO_ML_KEM_512 @@ -3885,7 +3888,8 @@ int test_wc_mlkem_decapsulate_kats(void) int test_wc_mlkem_decapsulate_pubonly_fails(void) { EXPECT_DECLS; -#if !defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0) +#if (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) && \ + defined(WC_MLKEM_HAVE_NATIVE) #if defined(WOLFSSL_HAVE_MLKEM) && \ !defined(WOLFSSL_NO_ML_KEM) && !defined(WOLFSSL_MLKEM_NO_DECAPSULATE) && \ !defined(WOLFSSL_MLKEM_NO_ENCAPSULATE) && \ @@ -3962,7 +3966,8 @@ int test_wc_mlkem_decapsulate_pubonly_fails(void) int test_wc_mlkem_decap_fo_reject(void) { EXPECT_DECLS; -#if !defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0) +#if (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) && \ + defined(WC_MLKEM_HAVE_NATIVE) #if defined(WOLFSSL_HAVE_MLKEM) && \ !defined(WOLFSSL_NO_ML_KEM) && !defined(WOLFSSL_MLKEM_NO_DECAPSULATE) && \ !defined(WOLFSSL_MLKEM_NO_ENCAPSULATE) && \ @@ -4037,7 +4042,8 @@ int test_wc_mlkem_decap_fo_reject(void) int test_wc_mlkem_decode_privkey_bad_pubhash(void) { EXPECT_DECLS; -#if !defined(HAVE_FIPS) && !defined(HAVE_SELFTEST) +#if !defined(HAVE_FIPS) && !defined(HAVE_SELFTEST) && \ + defined(WC_MLKEM_HAVE_NATIVE) #if defined(WOLFSSL_HAVE_MLKEM) && \ !defined(WOLFSSL_NO_ML_KEM) && !defined(WOLFSSL_MLKEM_NO_MAKE_KEY) MlKemKey* key = NULL; @@ -4227,7 +4233,8 @@ int test_wc_MlkemFeatureCoverage(void) int test_wc_MlkemDecisionCoverage(void) { EXPECT_DECLS; -#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLFSSL_NO_ML_KEM) +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLFSSL_NO_ML_KEM) && \ + defined(WC_MLKEM_HAVE_NATIVE) MlKemKey* key = NULL; #ifndef WC_NO_CONSTRUCTORS MlKemKey* newKey = NULL; @@ -4603,6 +4610,7 @@ int test_wc_mlkem_encode_key_len_decision(void) { EXPECT_DECLS; #if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLFSSL_NO_ML_KEM) && \ + defined(WC_MLKEM_HAVE_NATIVE) && \ !defined(WOLFSSL_MLKEM_NO_MAKE_KEY) MlKemKey* key; WC_RNG rng; diff --git a/wolfcrypt/src/cryptocb.c b/wolfcrypt/src/cryptocb.c index 569a68e01a6..3d24b0deb44 100644 --- a/wolfcrypt/src/cryptocb.c +++ b/wolfcrypt/src/cryptocb.c @@ -66,6 +66,7 @@ Crypto Callback Build Options: * WOLF_CRYPTO_CB_ONLY_AES: Use only callbacks for AES default: off * WOLF_CRYPTO_CB_ONLY_ED25519: Use only callbacks for Ed25519 default: off * WOLF_CRYPTO_CB_ONLY_CURVE25519: Use only callbacks for X25519 default: off + * WOLF_CRYPTO_CB_ONLY_MLKEM: Use only callbacks for ML-KEM default: off */ #include diff --git a/wolfcrypt/src/port/arm/armv8-32-mlkem-asm.S b/wolfcrypt/src/port/arm/armv8-32-mlkem-asm.S index 1cfe370d0e1..0be10942133 100644 --- a/wolfcrypt/src/port/arm/armv8-32-mlkem-asm.S +++ b/wolfcrypt/src/port/arm/armv8-32-mlkem-asm.S @@ -33,7 +33,7 @@ #ifdef WOLFSSL_ARMASM #if !defined(__aarch64__) && !defined(WOLFSSL_ARMASM_THUMB2) #ifndef WOLFSSL_ARMASM_INLINE -#ifdef WOLFSSL_HAVE_MLKEM +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLF_CRYPTO_CB_ONLY_MLKEM) #ifndef __APPLE__ .text .type L_mlkem_arm32_ntt_zetas, %object @@ -8516,7 +8516,7 @@ L_mlkem_arm32_rej_uniform_done: lsr r0, r12, #1 pop {r4, r5, r6, r7, r8, pc} .size mlkem_arm32_rej_uniform,.-mlkem_arm32_rej_uniform -#endif /* WOLFSSL_HAVE_MLKEM */ +#endif /* WOLFSSL_HAVE_MLKEM && !WOLF_CRYPTO_CB_ONLY_MLKEM */ #if defined(__linux__) && defined(__ELF__) .section .note.GNU-stack,"",%progbits diff --git a/wolfcrypt/src/port/arm/armv8-32-mlkem-asm_c.c b/wolfcrypt/src/port/arm/armv8-32-mlkem-asm_c.c index df668bfb45f..51a23ae4d47 100644 --- a/wolfcrypt/src/port/arm/armv8-32-mlkem-asm_c.c +++ b/wolfcrypt/src/port/arm/armv8-32-mlkem-asm_c.c @@ -52,7 +52,7 @@ #include -#ifdef WOLFSSL_HAVE_MLKEM +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLF_CRYPTO_CB_ONLY_MLKEM) XALIGNED(4) static const word16 L_mlkem_arm32_ntt_zetas[] = { 0x08ed, 0x0a0b, 0x0b9a, 0x0714, 0x05d5, 0x058e, 0x011f, 0x00ca, 0x0c56, 0x026e, 0x0629, 0x00b6, 0x03c2, 0x084f, 0x073f, 0x05bc, @@ -8666,7 +8666,7 @@ WC_OMIT_FRAME_POINTER unsigned int mlkem_arm32_rej_uniform(sword16* p, return (word32)(size_t)p; } -#endif /* WOLFSSL_HAVE_MLKEM */ +#endif /* WOLFSSL_HAVE_MLKEM && !WOLF_CRYPTO_CB_ONLY_MLKEM */ #endif /* WOLFSSL_ARMASM_INLINE */ #endif /* !__aarch64__ && !WOLFSSL_ARMASM_THUMB2 */ diff --git a/wolfcrypt/src/port/arm/armv8-mlkem-asm.S b/wolfcrypt/src/port/arm/armv8-mlkem-asm.S index 14010de9035..ed76d0e8091 100644 --- a/wolfcrypt/src/port/arm/armv8-mlkem-asm.S +++ b/wolfcrypt/src/port/arm/armv8-mlkem-asm.S @@ -49,7 +49,7 @@ #endif /* __APPLE__ */ L_mlkem_aarch64_consts: .short 0x0d01,0xf301,0x4ebf,0x0549,0x5049,0x0000,0x0000,0x0000 -#ifdef WOLFSSL_HAVE_MLKEM +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLF_CRYPTO_CB_ONLY_MLKEM) #ifndef __APPLE__ .text .section .rodata @@ -12071,7 +12071,7 @@ L_SHA3_shake256_blocksx3_seed_neon_begin: #ifndef __APPLE__ .size mlkem_shake256_blocksx3_seed_neon,.-mlkem_shake256_blocksx3_seed_neon #endif /* __APPLE__ */ -#endif /* WOLFSSL_HAVE_MLKEM */ +#endif /* WOLFSSL_HAVE_MLKEM && !WOLF_CRYPTO_CB_ONLY_MLKEM */ #endif /* __aarch64__ */ #endif /* WOLFSSL_ARMASM */ diff --git a/wolfcrypt/src/port/arm/armv8-mlkem-asm_c.c b/wolfcrypt/src/port/arm/armv8-mlkem-asm_c.c index 2b1f5b40b0f..f9db20e91d4 100644 --- a/wolfcrypt/src/port/arm/armv8-mlkem-asm_c.c +++ b/wolfcrypt/src/port/arm/armv8-mlkem-asm_c.c @@ -40,7 +40,7 @@ XALIGNED(4) static const word16 L_mlkem_aarch64_consts[] = { #include -#ifdef WOLFSSL_HAVE_MLKEM +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLF_CRYPTO_CB_ONLY_MLKEM) XALIGNED(4) static const word16 L_mlkem_aarch64_zetas[] = { 0x08ed, 0x0a0b, 0x0b9a, 0x0714, 0x05d5, 0x058e, 0x011f, 0x00ca, 0x0c56, 0x026e, 0x0629, 0x00b6, 0x03c2, 0x084f, 0x073f, 0x05bc, @@ -11258,7 +11258,7 @@ void mlkem_shake256_blocksx3_seed_neon(word64* state, byte* seed) ); } -#endif /* WOLFSSL_HAVE_MLKEM */ +#endif /* WOLFSSL_HAVE_MLKEM && !WOLF_CRYPTO_CB_ONLY_MLKEM */ #endif /* __aarch64__ */ #endif /* WOLFSSL_ARMASM */ #endif /* WOLFSSL_ARMASM_INLINE */ diff --git a/wolfcrypt/src/port/arm/thumb2-mlkem-asm.S b/wolfcrypt/src/port/arm/thumb2-mlkem-asm.S index e71b9311ed5..ea12e5f8d5a 100644 --- a/wolfcrypt/src/port/arm/thumb2-mlkem-asm.S +++ b/wolfcrypt/src/port/arm/thumb2-mlkem-asm.S @@ -35,7 +35,7 @@ #ifndef WOLFSSL_ARMASM_INLINE .thumb .syntax unified -#ifdef WOLFSSL_HAVE_MLKEM +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLF_CRYPTO_CB_ONLY_MLKEM) #ifndef __APPLE__ .text .type L_mlkem_thumb2_ntt_zetas, %object @@ -3591,7 +3591,7 @@ L_mlkem_thumb2_rej_uniform_done: POP {r4, r5, r6, r7, r8, r9, r10, pc} /* Cycle Count = 225 */ .size mlkem_thumb2_rej_uniform,.-mlkem_thumb2_rej_uniform -#endif /* WOLFSSL_HAVE_MLKEM */ +#endif /* WOLFSSL_HAVE_MLKEM && !WOLF_CRYPTO_CB_ONLY_MLKEM */ #if defined(__linux__) && defined(__ELF__) .section .note.GNU-stack,"",%progbits diff --git a/wolfcrypt/src/port/arm/thumb2-mlkem-asm_c.c b/wolfcrypt/src/port/arm/thumb2-mlkem-asm_c.c index a0ee89a5fa1..710d33dba60 100644 --- a/wolfcrypt/src/port/arm/thumb2-mlkem-asm_c.c +++ b/wolfcrypt/src/port/arm/thumb2-mlkem-asm_c.c @@ -52,7 +52,7 @@ #include -#ifdef WOLFSSL_HAVE_MLKEM +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLF_CRYPTO_CB_ONLY_MLKEM) XALIGNED(4) static const word16 L_mlkem_thumb2_ntt_zetas[] = { 0x08ed, 0x0a0b, 0x0b9a, 0x0714, 0x05d5, 0x058e, 0x011f, 0x00ca, 0x0c56, 0x026e, 0x0629, 0x00b6, 0x03c2, 0x084f, 0x073f, 0x05bc, @@ -3889,7 +3889,7 @@ WC_OMIT_FRAME_POINTER unsigned int mlkem_thumb2_rej_uniform(sword16* p, return (word32)(size_t)p; } -#endif /* WOLFSSL_HAVE_MLKEM */ +#endif /* WOLFSSL_HAVE_MLKEM && !WOLF_CRYPTO_CB_ONLY_MLKEM */ #endif /* WOLFSSL_ARMASM_INLINE */ #endif /* WOLFSSL_ARMASM_THUMB2 */ diff --git a/wolfcrypt/src/wc_mlkem.c b/wolfcrypt/src/wc_mlkem.c index fb4f400a71f..8f8a3b7639a 100644 --- a/wolfcrypt/src/wc_mlkem.c +++ b/wolfcrypt/src/wc_mlkem.c @@ -189,6 +189,7 @@ sword16 wc_mlkem_opt_blocker(void) { * @param [in] key ML-KEM key object. * @return k value for the key type, or 0 if not recognized. */ +#ifndef WOLF_CRYPTO_CB_ONLY_MLKEM static int mlkemkey_get_k(const MlKemKey* key) { switch (key->type) { @@ -224,6 +225,7 @@ static int mlkemkey_get_k(const MlKemKey* key) return 0; } } +#endif /* !WOLF_CRYPTO_CB_ONLY_MLKEM */ #endif #ifdef WOLFSSL_MLKEM_DYNAMIC_KEYS @@ -802,6 +804,19 @@ int wc_MlKemKey_MakeKey(MlKemKey* key, WC_RNG* rng) int wc_MlKemKey_MakeKeyWithRandom(MlKemKey* key, const unsigned char* rand, int len) { +#ifdef WOLF_CRYPTO_CB_ONLY_MLKEM + /* Validate as the software path does, so the reported error stays the + * same for a bad call. */ + if ((key == NULL) || (rand == NULL)) { + return BAD_FUNC_ARG; + } + if (len != WC_ML_KEM_MAKEKEY_RAND_SZ) { + return BUFFER_E; + } + /* No software fallback: only a crypto callback can service the request, + * and no callback takes caller-chosen key generation randomness. */ + return NO_VALID_DEVID; +#else byte buf[2 * WC_ML_KEM_SYM_SZ + 1]; byte* rho = buf; #ifndef WC_MLKEM_FAULT_HARDEN @@ -1036,6 +1051,7 @@ int wc_MlKemKey_MakeKeyWithRandom(MlKemKey* key, const unsigned char* rand, * function and has the RNG parameter needed for encapsulation. */ return ret; +#endif /* WOLF_CRYPTO_CB_ONLY_MLKEM */ } #endif /* !WOLFSSL_MLKEM_NO_MAKE_KEY */ @@ -1131,6 +1147,7 @@ int wc_MlKemKey_SharedSecretSize(MlKemKey* key, word32* len) #if !defined(WOLFSSL_MLKEM_NO_ENCAPSULATE) || \ !defined(WOLFSSL_MLKEM_NO_DECAPSULATE) +#ifndef WOLF_CRYPTO_CB_ONLY_MLKEM /* Encrypt a message to cipher text with the encryption key. * * FIPS 203, Algorithm 14: K-PKE.Encrypt(ek_PKE, m, r) @@ -1379,10 +1396,12 @@ static int mlkemkey_encapsulate(MlKemKey* key, const byte* m, byte* r, byte* c) return ret; } +#endif /* !WOLF_CRYPTO_CB_ONLY_MLKEM */ #endif #if !defined(WOLFSSL_MLKEM_NO_ENCAPSULATE) || \ !defined(WOLFSSL_MLKEM_NO_DECAPSULATE) +#ifndef WOLF_CRYPTO_CB_ONLY_MLKEM static int wc_mlkemkey_check_h(MlKemKey* key) { int ret = 0; @@ -1424,6 +1443,7 @@ static int wc_mlkemkey_check_h(MlKemKey* key) return ret; } +#endif /* !WOLF_CRYPTO_CB_ONLY_MLKEM */ #endif #ifndef WOLFSSL_MLKEM_NO_ENCAPSULATE @@ -1552,6 +1572,22 @@ int wc_MlKemKey_Encapsulate(MlKemKey* key, unsigned char* ct, unsigned char* ss, int wc_MlKemKey_EncapsulateWithRandom(MlKemKey* key, unsigned char* ct, unsigned char* ss, const unsigned char* rand, int len) { +#ifdef WOLF_CRYPTO_CB_ONLY_MLKEM + /* Validate as the software path does, so the reported error stays the + * same for a bad call. */ + if ((key == NULL) || (ct == NULL) || (ss == NULL) || (rand == NULL)) { + return BAD_FUNC_ARG; + } + if (len != WC_ML_KEM_ENC_RAND_SZ) { + return BUFFER_E; + } + if ((key->flags & MLKEM_FLAG_PUB_SET) == 0) { + return BAD_STATE_E; + } + /* No software fallback: only a crypto callback can service the request, + * and no callback takes caller-chosen encapsulation randomness. */ + return NO_VALID_DEVID; +#else #ifdef WOLFSSL_MLKEM_KYBER byte msg[WC_ML_KEM_SYM_SZ]; #endif @@ -1723,12 +1759,14 @@ int wc_MlKemKey_EncapsulateWithRandom(MlKemKey* key, unsigned char* ct, #endif return ret; +#endif /* WOLF_CRYPTO_CB_ONLY_MLKEM */ } #endif /* !WOLFSSL_MLKEM_NO_ENCAPSULATE */ /******************************************************************************/ #ifndef WOLFSSL_MLKEM_NO_DECAPSULATE +#ifndef WOLF_CRYPTO_CB_ONLY_MLKEM /* Decapsulate cipher text to the message using key. * * FIPS 203, Algorithm 15: K-PKE.Decrypt(dk_PKE,c) @@ -1893,6 +1931,7 @@ static MLKEM_NOINLINE int mlkemkey_decapsulate(MlKemKey* key, byte* m, return ret; } +#endif /* !WOLF_CRYPTO_CB_ONLY_MLKEM */ /** * Decapsulate the cipher text to calculate the shared secret. @@ -1941,16 +1980,18 @@ static MLKEM_NOINLINE int mlkemkey_decapsulate(MlKemKey* key, byte* m, int wc_MlKemKey_Decapsulate(MlKemKey* key, unsigned char* ss, const unsigned char* ct, word32 len) { - byte msg[WC_ML_KEM_SYM_SZ]; - byte kr[2 * WC_ML_KEM_SYM_SZ + 1]; int ret = 0; unsigned int ctSz = 0; +#ifndef WOLF_CRYPTO_CB_ONLY_MLKEM + byte msg[WC_ML_KEM_SYM_SZ]; + byte kr[2 * WC_ML_KEM_SYM_SZ + 1]; unsigned int i = 0; int fail = 0; #if !defined(USE_INTEL_SPEEDUP) && !defined(WOLFSSL_NO_MALLOC) byte* cmp = NULL; #else byte cmp[WC_ML_KEM_MAX_CIPHER_TEXT_SIZE]; +#endif #endif /* Validate parameters. */ @@ -2025,6 +2066,14 @@ int wc_MlKemKey_Decapsulate(MlKemKey* key, unsigned char* ss, } #endif +#ifdef WOLF_CRYPTO_CB_ONLY_MLKEM + if (ret == 0) { + /* No software fallback: only a crypto callback can service + * the request. */ + ret = NO_VALID_DEVID; + } +#else + #if !defined(USE_INTEL_SPEEDUP) && !defined(WOLFSSL_NO_MALLOC) if (ret == 0) { /* Allocate memory for cipher text that is generated. */ @@ -2119,6 +2168,7 @@ int wc_MlKemKey_Decapsulate(MlKemKey* key, unsigned char* ss, wc_MemZero_Check(msg, sizeof(msg)); wc_MemZero_Check(kr, sizeof(kr)); #endif +#endif /* WOLF_CRYPTO_CB_ONLY_MLKEM */ return ret; } diff --git a/wolfcrypt/src/wc_mlkem_asm.S b/wolfcrypt/src/wc_mlkem_asm.S index 5c1c9a307f2..6524f8d3405 100644 --- a/wolfcrypt/src/wc_mlkem_asm.S +++ b/wolfcrypt/src/wc_mlkem_asm.S @@ -43,7 +43,7 @@ #endif /* HAVE_INTEL_AVX512 */ #endif /* NO_AVX512_SUPPORT */ -#ifdef WOLFSSL_HAVE_MLKEM +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLF_CRYPTO_CB_ONLY_MLKEM) #ifdef HAVE_INTEL_AVX2 #ifndef __APPLE__ .data @@ -15918,8 +15918,8 @@ _mlkem_redistribute_8_rand_avx2: .size mlkem_redistribute_8_rand_avx2,.-mlkem_redistribute_8_rand_avx2 #endif /* __APPLE__ */ #endif /* HAVE_INTEL_AVX2 */ -#endif /* WOLFSSL_HAVE_MLKEM */ -#ifdef WOLFSSL_HAVE_MLKEM +#endif /* WOLFSSL_HAVE_MLKEM && !WOLF_CRYPTO_CB_ONLY_MLKEM */ +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLF_CRYPTO_CB_ONLY_MLKEM) #ifndef NO_AVX512_SUPPORT #ifndef HAVE_INTEL_AVX512 #define HAVE_INTEL_AVX512 @@ -35274,8 +35274,8 @@ L_mlkem_rej_uniform_n_avx512_vbmi2_tail_end: .size mlkem_rej_uniform_n_avx512_vbmi2,.-mlkem_rej_uniform_n_avx512_vbmi2 #endif /* __APPLE__ */ #endif /* HAVE_INTEL_AVX512_VBMI2 */ -#endif /* WOLFSSL_HAVE_MLKEM */ -#ifdef WOLFSSL_HAVE_MLKEM +#endif /* WOLFSSL_HAVE_MLKEM && !WOLF_CRYPTO_CB_ONLY_MLKEM */ +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLF_CRYPTO_CB_ONLY_MLKEM) #ifndef NO_AVX512_SUPPORT #ifndef NO_AVX512_VBMI_SUPPORT #ifndef HAVE_INTEL_AVX512_VBMI @@ -36969,7 +36969,7 @@ L_mlkem_rej_uniform_n_avx512_vbmi_vbmi2_tail_end: #endif /* __APPLE__ */ #endif /* HAVE_INTEL_AVX512_VBMI2 */ #endif /* HAVE_INTEL_AVX512_VBMI */ -#endif /* WOLFSSL_HAVE_MLKEM */ +#endif /* WOLFSSL_HAVE_MLKEM && !WOLF_CRYPTO_CB_ONLY_MLKEM */ #if defined(__linux__) && defined(__ELF__) .section .note.GNU-stack,"",%progbits diff --git a/wolfcrypt/src/wc_mlkem_poly.c b/wolfcrypt/src/wc_mlkem_poly.c index c587c9f660c..8a73198c897 100644 --- a/wolfcrypt/src/wc_mlkem_poly.c +++ b/wolfcrypt/src/wc_mlkem_poly.c @@ -70,6 +70,22 @@ #define WC_FIPS_LL_CRYPTO #define _WC_BUILDING_WC_MLKEM_POLY_C +#ifdef WOLF_CRYPTO_CB_ONLY_MLKEM +/* Only the encode/decode helpers and the hash/PRF object lifecycle are needed: + * every operation that uses the lattice math is serviced by a crypto callback. + * These are set for this file alone, so the public API in wc_mlkem.c keeps + * every entry point. */ +#ifndef WOLFSSL_MLKEM_NO_MAKE_KEY + #define WOLFSSL_MLKEM_NO_MAKE_KEY +#endif +#ifndef WOLFSSL_MLKEM_NO_ENCAPSULATE + #define WOLFSSL_MLKEM_NO_ENCAPSULATE +#endif +#ifndef WOLFSSL_MLKEM_NO_DECAPSULATE + #define WOLFSSL_MLKEM_NO_DECAPSULATE +#endif +#endif /* WOLF_CRYPTO_CB_ONLY_MLKEM */ + #include #ifdef WC_MLKEM_NO_ASM diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index 0556fd53513..8cb2ea58fe9 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -52093,6 +52093,7 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t ed448_test(void) #endif /* HAVE_ED448 */ #ifdef WOLFSSL_HAVE_MLKEM +#ifdef WC_MLKEM_HAVE_NATIVE #if !defined(WOLFSSL_NO_KYBER512) && !defined(WOLFSSL_NO_ML_KEM_512) static wc_test_ret_t mlkem512_kat(void) { @@ -56473,10 +56474,35 @@ static wc_test_ret_t mlkem1024_kat(void) return ret; } #endif /* !WOLFSSL_NO_KYBER1024 && !WOLFSSL_NO_ML_KEM_1024 */ +#endif /* WC_MLKEM_HAVE_NATIVE */ + +#ifndef WC_MLKEM_HAVE_NATIVE +/* Any compiled-in parameter set proves the dispatch behaviour; which one is + * irrelevant, so pick the first that is actually built. */ +#ifndef WOLFSSL_NO_ML_KEM + #if defined(WOLFSSL_WC_ML_KEM_512) + #define MLKEM_CB_ONLY_TYPE WC_ML_KEM_512 + #elif defined(WOLFSSL_WC_ML_KEM_768) + #define MLKEM_CB_ONLY_TYPE WC_ML_KEM_768 + #elif defined(WOLFSSL_WC_ML_KEM_1024) + #define MLKEM_CB_ONLY_TYPE WC_ML_KEM_1024 + #endif +#endif +#if !defined(MLKEM_CB_ONLY_TYPE) && defined(WOLFSSL_MLKEM_KYBER) + #if defined(WOLFSSL_KYBER512) + #define MLKEM_CB_ONLY_TYPE KYBER512 + #elif defined(WOLFSSL_KYBER768) + #define MLKEM_CB_ONLY_TYPE KYBER768 + #elif defined(WOLFSSL_KYBER1024) + #define MLKEM_CB_ONLY_TYPE KYBER1024 + #endif +#endif +#endif /* !WC_MLKEM_HAVE_NATIVE */ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t mlkem_test(void) { wc_test_ret_t ret; +#ifdef WC_MLKEM_HAVE_NATIVE int i; #ifndef WC_NO_RNG WC_RNG rng; @@ -56771,6 +56797,58 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t mlkem_test(void) #endif return ret; +#else /* !WC_MLKEM_HAVE_NATIVE */ + /* Software ML-KEM is compiled out. Confirm the public API refuses an + * operation when no crypto-callback device is available (INVALID_DEVID), + * rather than silently doing nothing. */ + ret = 0; +#ifdef MLKEM_CB_ONLY_TYPE + { + /* MlKemKey carries maximum-sized polynomial buffers, so keep it off + * the stack as the parameter-set tests do. */ + WC_DECLARE_VAR(key, MlKemKey, 1, HEAP_HINT); + int key_inited = 0; + int r; + + WC_ALLOC_VAR(key, MlKemKey, 1, HEAP_HINT); + if (!WC_VAR_OK(key)) + ret = WC_TEST_RET_ENC_EC(MEMORY_E); + if (ret == 0) { + r = wc_MlKemKey_Init(key, MLKEM_CB_ONLY_TYPE, HEAP_HINT, + INVALID_DEVID); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + else + key_inited = 1; + } + if (ret == 0) { + byte rand[WC_ML_KEM_MAKEKEY_RAND_SZ]; + + XMEMSET(rand, 0, sizeof(rand)); + /* Argument checks still run ahead of the dispatch report. */ + r = wc_MlKemKey_MakeKeyWithRandom(key, NULL, (int)sizeof(rand)); + if (r != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) + ret = WC_TEST_RET_ENC_NC; + if (ret == 0) { + r = wc_MlKemKey_MakeKeyWithRandom(key, rand, + (int)sizeof(rand) - 1); + if (r != WC_NO_ERR_TRACE(BUFFER_E)) + ret = WC_TEST_RET_ENC_NC; + } + if (ret == 0) { + r = wc_MlKemKey_MakeKeyWithRandom(key, rand, + (int)sizeof(rand)); + if (r != WC_NO_ERR_TRACE(NO_VALID_DEVID)) + ret = WC_TEST_RET_ENC_NC; + } + } + if (key_inited) + wc_MlKemKey_Free(key); + WC_FREE_VAR(key, HEAP_HINT); + } +#endif /* MLKEM_CB_ONLY_TYPE */ + return ret; +#endif /* WC_MLKEM_HAVE_NATIVE */ } #endif /* WOLFSSL_HAVE_MLKEM */ @@ -79235,6 +79313,10 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t blob_test(void) /* Example custom context for crypto callback */ typedef struct { int exampleVar; /* flag for testing if only crypt is enabled. */ +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WC_MLKEM_HAVE_NATIVE) + int mlkemCount; /* ML-KEM callback invocations */ + int mlkemFail; /* when set, the ML-KEM handler returns this error */ +#endif #ifdef HAVE_ECC int eccMakePubCount; /* EC make-pub callback invocations */ int eccCheckPubCount; /* EC check-pubkey callback invocations */ @@ -81028,7 +81110,7 @@ static int myCryptoDevCb(int devIdArg, wc_CryptoInfo* info, void* ctx) myCtx->exampleVar++; } #endif /* HAVE_FALCON && !WOLF_CRYPTO_CB_ONLY_FALCON */ - #ifdef WOLFSSL_HAVE_MLKEM + #if defined(WOLFSSL_HAVE_MLKEM) && defined(WC_MLKEM_HAVE_NATIVE) if (info->pk.type == WC_PK_TYPE_PQC_KEM_KEYGEN) { if ((info->pk.pqc_kem_kg.type == WC_PQC_KEM_TYPE_MLKEM) && (info->pk.pqc_kem_kg.key != NULL)) { @@ -81095,7 +81177,78 @@ static int myCryptoDevCb(int devIdArg, wc_CryptoInfo* info, void* ctx) key->prf.devId = prfDevId; } } - #endif /* WOLFSSL_HAVE_MLKEM */ + #endif /* WOLFSSL_HAVE_MLKEM && WC_MLKEM_HAVE_NATIVE */ + #if defined(WOLFSSL_HAVE_MLKEM) && !defined(WC_MLKEM_HAVE_NATIVE) + /* The software core is stripped, so this device cannot delegate to the + * public API the way the other handlers do; it would dispatch straight + * back here. It instead answers with its own deterministic material, + * which is enough to prove the dispatch reaches a device, that the + * ciphertext and shared secret travel back to the caller, and that a + * device error is reported as-is. The shared secret is carried in the + * first bytes of the ciphertext, so decapsulate only reproduces it + * when encapsulate's output made the round trip. */ + if ((info->pk.type == WC_PK_TYPE_PQC_KEM_KEYGEN) && + (info->pk.pqc_kem_kg.type == WC_PQC_KEM_TYPE_MLKEM) && + (info->pk.pqc_kem_kg.key != NULL)) { + MlKemKey* key = (MlKemKey*)info->pk.pqc_kem_kg.key; + + myCtx->mlkemCount++; + if (myCtx->mlkemFail != 0) { + ret = myCtx->mlkemFail; + } + else { + XMEMSET(key->z, 0x5a, sizeof(key->z)); + key->flags |= MLKEM_FLAG_BOTH_SET; + ret = 0; + } + } + else if ((info->pk.type == WC_PK_TYPE_PQC_KEM_ENCAPS) && + (info->pk.pqc_encaps.type == WC_PQC_KEM_TYPE_MLKEM) && + (info->pk.pqc_encaps.key != NULL)) { + MlKemKey* key = (MlKemKey*)info->pk.pqc_encaps.key; + word32 ctSz = 0; + + myCtx->mlkemCount++; + if (myCtx->mlkemFail != 0) { + ret = myCtx->mlkemFail; + } + else { + ret = wc_MlKemKey_CipherTextSize(key, &ctSz); + } + if (ret == 0) { + byte* ct = info->pk.pqc_encaps.ciphertext; + byte* ss = info->pk.pqc_encaps.sharedSecret; + word32 i; + + for (i = 0; i < WC_ML_KEM_SS_SZ; i++) { + ss[i] = (byte)(key->z[i] ^ 0xa5); + } + XMEMSET(ct, 0xc7, ctSz); + XMEMCPY(ct, ss, WC_ML_KEM_SS_SZ); + } + } + else if ((info->pk.type == WC_PK_TYPE_PQC_KEM_DECAPS) && + (info->pk.pqc_decaps.type == WC_PQC_KEM_TYPE_MLKEM) && + (info->pk.pqc_decaps.key != NULL)) { + MlKemKey* key = (MlKemKey*)info->pk.pqc_decaps.key; + word32 ctSz = 0; + + myCtx->mlkemCount++; + if (myCtx->mlkemFail != 0) { + ret = myCtx->mlkemFail; + } + else { + ret = wc_MlKemKey_CipherTextSize(key, &ctSz); + } + if ((ret == 0) && (info->pk.pqc_decaps.ciphertextLen != ctSz)) { + ret = BUFFER_E; + } + if (ret == 0) { + XMEMCPY(info->pk.pqc_decaps.sharedSecret, + info->pk.pqc_decaps.ciphertext, WC_ML_KEM_SS_SZ); + } + } + #endif /* WOLFSSL_HAVE_MLKEM && !WC_MLKEM_HAVE_NATIVE */ #ifdef WOLFSSL_HAVE_FRODOKEM if (info->pk.type == WC_PK_TYPE_PQC_KEM_KEYGEN) { if ((info->pk.pqc_kem_kg.type == WC_PQC_KEM_TYPE_FRODOKEM) && @@ -82747,6 +82900,10 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t cryptocb_test(void) /* example data for callback */ myCtx.exampleVar = 1; +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WC_MLKEM_HAVE_NATIVE) + myCtx.mlkemCount = 0; + myCtx.mlkemFail = 0; +#endif #ifdef HAVE_ECC myCtx.eccMakePubCount = 0; myCtx.eccCheckPubCount = 0; @@ -83034,6 +83191,91 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t cryptocb_test(void) #ifdef WOLFSSL_HAVE_MLKEM if (ret == 0) ret = mlkem_test(); +#if !defined(WC_MLKEM_HAVE_NATIVE) && defined(MLKEM_CB_ONLY_TYPE) && \ + !defined(WC_NO_RNG) + /* With the software core stripped, the only way an ML-KEM operation can + * succeed is through a registered device. Drive all three operations that + * way and confirm the results came back, so a dispatch regression cannot + * hide behind the NO_VALID_DEVID checks in mlkem_test(). */ + if (ret == 0) { + WC_DECLARE_VAR(key, MlKemKey, 1, HEAP_HINT); + WC_DECLARE_VAR(ct, byte, WC_ML_KEM_MAX_CIPHER_TEXT_SIZE, HEAP_HINT); + WC_DECLARE_VAR(mlkemRng, WC_RNG, 1, HEAP_HINT); + byte ss[WC_ML_KEM_SS_SZ]; + byte ssDec[WC_ML_KEM_SS_SZ]; + int key_inited = 0; + int rng_inited = 0; + int baseline = myCtx.mlkemCount; + word32 ctSz = 0; + int r; + + WC_ALLOC_VAR(key, MlKemKey, 1, HEAP_HINT); + WC_ALLOC_VAR(ct, byte, WC_ML_KEM_MAX_CIPHER_TEXT_SIZE, HEAP_HINT); + WC_ALLOC_VAR(mlkemRng, WC_RNG, 1, HEAP_HINT); + if ((!WC_VAR_OK(key)) || (!WC_VAR_OK(ct)) || (!WC_VAR_OK(mlkemRng))) + ret = WC_TEST_RET_ENC_EC(MEMORY_E); + if (ret == 0) { + /* The device ignores the RNG; keep it off the callback path. */ + r = wc_InitRng_ex(mlkemRng, HEAP_HINT, INVALID_DEVID); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + else + rng_inited = 1; + } + if (ret == 0) { + r = wc_MlKemKey_Init(key, MLKEM_CB_ONLY_TYPE, HEAP_HINT, devId); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + else + key_inited = 1; + } + if (ret == 0) { + r = wc_MlKemKey_MakeKey(key, mlkemRng); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + } + if (ret == 0) { + r = wc_MlKemKey_CipherTextSize(key, &ctSz); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + } + if (ret == 0) { + XMEMSET(ct, 0, WC_ML_KEM_MAX_CIPHER_TEXT_SIZE); + XMEMSET(ss, 0, sizeof(ss)); + r = wc_MlKemKey_Encapsulate(key, ct, ss, mlkemRng); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + } + if (ret == 0) { + XMEMSET(ssDec, 0, sizeof(ssDec)); + r = wc_MlKemKey_Decapsulate(key, ssDec, ct, ctSz); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + } + /* Shared secrets agree only if the device's ciphertext reached the + * caller and was handed back to decapsulate unchanged. */ + if ((ret == 0) && (XMEMCMP(ss, ssDec, sizeof(ss)) != 0)) + ret = WC_TEST_RET_ENC_NC; + /* Three operations, three callback invocations: no silent bypass. */ + if ((ret == 0) && (myCtx.mlkemCount != baseline + 3)) + ret = WC_TEST_RET_ENC_NC; + /* A device error must reach the caller unchanged. */ + if (ret == 0) { + myCtx.mlkemFail = WC_NO_ERR_TRACE(WC_HW_E); + r = wc_MlKemKey_Encapsulate(key, ct, ss, mlkemRng); + myCtx.mlkemFail = 0; + if (r != WC_NO_ERR_TRACE(WC_HW_E)) + ret = WC_TEST_RET_ENC_NC; + } + if (key_inited) + wc_MlKemKey_Free(key); + if (rng_inited) + wc_FreeRng(mlkemRng); + WC_FREE_VAR(mlkemRng, HEAP_HINT); + WC_FREE_VAR(ct, HEAP_HINT); + WC_FREE_VAR(key, HEAP_HINT); + } +#endif /* !WC_MLKEM_HAVE_NATIVE && MLKEM_CB_ONLY_TYPE && !WC_NO_RNG */ #endif #ifdef WOLFSSL_HAVE_FRODOKEM if (ret == 0) diff --git a/wolfssl/wolfcrypt/settings.h b/wolfssl/wolfcrypt/settings.h index 2bfbb4ddef4..7eae33b4a22 100644 --- a/wolfssl/wolfcrypt/settings.h +++ b/wolfssl/wolfcrypt/settings.h @@ -5880,6 +5880,17 @@ blinding by defining WC_BLINDING_NO_RNG_ACKNOWLEDGE_WEAKNESS." #error "WOLF_CRYPTO_CB_ONLY_ED25519 with " \ "WOLFSSL_ED25519_STREAMING_VERIFY is not supported" #endif +#if defined(WOLF_CRYPTO_CB_ONLY_MLKEM) && !defined(WOLF_CRYPTO_CB) + #error "WOLF_CRYPTO_CB_ONLY_MLKEM requires WOLF_CRYPTO_CB" +#endif +#if defined(WOLF_CRYPTO_CB_ONLY_MLKEM) && !defined(WOLFSSL_HAVE_MLKEM) + #error "WOLF_CRYPTO_CB_ONLY_MLKEM requires WOLFSSL_HAVE_MLKEM" +#endif +#if defined(WOLF_CRYPTO_CB_ONLY_MLKEM) && defined(HAVE_FIPS) + /* Key generation runs a pairwise consistency test that the callback would + * have to service; not validated. */ + #error "WOLF_CRYPTO_CB_ONLY_MLKEM is incompatible with FIPS builds" +#endif #if defined(WOLF_CRYPTO_CB_ONLY_CURVE25519) && !defined(WOLF_CRYPTO_CB) #error "WOLF_CRYPTO_CB_ONLY_CURVE25519 requires WOLF_CRYPTO_CB" #endif diff --git a/wolfssl/wolfcrypt/wc_mlkem.h b/wolfssl/wolfcrypt/wc_mlkem.h index 739bd4a7937..9240a4b626d 100644 --- a/wolfssl/wolfcrypt/wc_mlkem.h +++ b/wolfssl/wolfcrypt/wc_mlkem.h @@ -865,6 +865,19 @@ WOLFSSL_LOCAL unsigned int mlkem_arm32_rej_uniform(sword16* p, unsigned int len, } /* extern "C" */ #endif +/* Native implementation core (internal). The public wc_MlKemKey_* functions + * in wc_mlkem.c wrap it with cryptocb dispatch and argument checking. With + * WOLF_CRYPTO_CB_ONLY_MLKEM the lattice math is not compiled: key generation, + * encapsulation and decapsulation all go through the crypto callback. The + * encode and decode helpers stay, since a callback that returns key material + * needs them. */ +#ifndef WOLF_CRYPTO_CB_ONLY_MLKEM +/* Signals that native key generation, encapsulation and decapsulation are + * available. Tests gate on this rather than on the build switch, so a test + * says what it needs rather than which configuration removed it. */ +#define WC_MLKEM_HAVE_NATIVE +#endif + #endif /* WOLFSSL_HAVE_MLKEM */ #endif /* WOLF_CRYPT_WC_MLKEM_H */