From 588e7a075eebd891dd77466dd383caf64ed146b5 Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Wed, 27 May 2026 14:10:40 -0600 Subject: [PATCH] Phase 3: Security and FIPS Compliance Audit --- configure.ac | 35 ++- fips-hash.sh | 6 +- tests/api/test_aes.c | 13 +- tests/api/test_evp_pkey.c | 4 +- tests/api/test_ossl_rsa.c | 10 +- tests/api/test_slhdsa.c | 8 +- wolfcrypt/benchmark/fips_cast_bench.c | 363 ++++++++++++++++++++++++++ wolfcrypt/benchmark/include.am | 10 + wolfcrypt/src/aes.c | 14 + wolfcrypt/src/dh.c | 32 ++- wolfcrypt/src/error.c | 15 ++ wolfcrypt/src/rsa.c | 24 +- wolfcrypt/src/wc_mldsa.c | 114 +++++++- wolfcrypt/src/wc_mlkem.c | 115 ++++---- wolfcrypt/src/wc_slhdsa.c | 165 +++++++++++- wolfcrypt/test/test.c | 310 ++++++++++++++++++++-- wolfssl/wolfcrypt/error-crypt.h | 14 +- wolfssl/wolfcrypt/fips_test.h | 30 ++- wolfssl/wolfcrypt/random.h | 6 +- wolfssl/wolfcrypt/settings.h | 21 ++ 20 files changed, 1201 insertions(+), 108 deletions(-) create mode 100644 wolfcrypt/benchmark/fips_cast_bench.c diff --git a/configure.ac b/configure.ac index 606ed241182..bd19c80c500 100644 --- a/configure.ac +++ b/configure.ac @@ -6313,13 +6313,7 @@ AS_CASE([$FIPS_VERSION], -DWC_RSA_NO_PADDING \ -DECC_USER_CURVES \ -DHAVE_ECC384 \ - -DHAVE_ECC521 \ - -DWOLFSSL_VALIDATE_FFC_IMPORT \ - -DHAVE_FFDHE_Q \ - -DHAVE_FFDHE_3072 \ - -DHAVE_FFDHE_4096 \ - -DHAVE_FFDHE_6144 \ - -DHAVE_FFDHE_8192" + -DHAVE_ECC521" # KCAPI API does not support custom k for sign, don't force enable ECC key sizes and don't use seed callback AS_IF([test "x$ENABLED_KCAPI_ECC" = "xno"], @@ -6333,6 +6327,20 @@ AS_CASE([$FIPS_VERSION], -DHAVE_ECC256"]) DEFAULT_MAX_CLASSIC_ASYM_KEY_BITS=8192 + +# Classic DH and DSA are OUT OF SCOPE for the FIPS 140-3 v7 PQ module. +# (FIPS 186-5 retires DSA; v7 boundary keeps only ECDH/ECDSA + PQ KEM/DSA.) +# Hard-error if explicitly enabled; otherwise force off and add NO_DH/NO_DSA. + AS_IF([test "$enable_dh" = "yes"], + [AC_MSG_ERROR([--enable-dh is not supported with --enable-fips=$FIPS_VERSION. Classic finite-field DH is out of scope for the FIPS 140-3 v7 PQ module. Use --enable-fips=v6 if you need DH support.])], + [test "$ENABLED_DH" != "no"], + [ENABLED_DH="no"; enable_dh="no"; AM_CFLAGS="$AM_CFLAGS -DNO_DH"]) + + AS_IF([test "$enable_dsa" = "yes"], + [AC_MSG_ERROR([--enable-dsa is not supported with --enable-fips=$FIPS_VERSION. DSA is retired by FIPS 186-5 and is out of scope for the FIPS 140-3 v7 PQ module. Use --enable-fips=v6 if you need DSA support.])], + [test "$ENABLED_DSA" != "no"], + [ENABLED_DSA="no"; enable_dsa="no"; AM_CFLAGS="$AM_CFLAGS -DNO_DSA"]) + # optimizations section # protocol section @@ -8887,8 +8895,17 @@ then fi if test "x$ENABLED_DH" = "xno" then - ENABLED_DH="yes" - AM_CFLAGS="$AM_CFLAGS -DHAVE_DH" + # Classic DH is out of scope for the FIPS 140-3 v7 PQ module. + # JNI normally auto-enables DH for legacy TLS suites; with FIPS v7+ + # we report and skip the auto-enable rather than silently turning DH + # back on (which would conflict with the boundary). + if test "$FIPS_VERSION" = "v7" || test "$FIPS_VERSION" = "ready" || test "$FIPS_VERSION" = "dev" + then + AC_MSG_NOTICE([JNI enabled but FIPS is $FIPS_VERSION, NOT turning on DH with this module]) + else + ENABLED_DH="yes" + AM_CFLAGS="$AM_CFLAGS -DHAVE_DH" + fi fi if test "x$ENABLED_PSK" = "xno" then diff --git a/fips-hash.sh b/fips-hash.sh index 36f320c0bbd..8f8a1a86317 100755 --- a/fips-hash.sh +++ b/fips-hash.sh @@ -13,7 +13,11 @@ then fi OUT=$(./wolfcrypt/test/testwolfcrypt | sed -n 's/hash = \(.*\)/\1/p') -NEWHASH=$(echo "$OUT" | cut -c1-64) +# FIPS v7.0.0+ uses HMAC-SHA-512 (128 hex chars); older FIPS versions +# use HMAC-SHA-256 (64 hex chars). Take the whole captured hash; the +# static_assert on sizeof(verifyCore) guards against wrong length at +# compile time after this script runs. +NEWHASH=$(echo "$OUT" | head -n1 | tr -d '[:space:]') if test -n "$NEWHASH" then cp wolfcrypt/src/fips_test.c wolfcrypt/src/fips_test.c.bak diff --git a/tests/api/test_aes.c b/tests/api/test_aes.c index b5f90a89ccc..017a986cd0f 100644 --- a/tests/api/test_aes.c +++ b/tests/api/test_aes.c @@ -693,7 +693,14 @@ static int test_wc_AesCbcEncryptDecrypt_WithKey(Aes* aes, byte* key, ExpectIntEQ(wc_AesCbcEncrypt(aes, cipher, vector, vector_len), 0); ExpectBufEQ(cipher, vector_enc, vector_len); -#ifdef WOLFSSL_AES_CBC_LENGTH_CHECKS + /* The BAD_LENGTH_E enforcement is in the non-FIPS aes.c implementation + * (see WOLFSSL_AES_CBC_LENGTH_CHECKS guard there). FIPSv2 (cert3389) + * routes through its own historical wc_AesCbcEncrypt_fips wrapper that + * predates this check and silently returns 0 on unaligned input. Only + * v5.x and newer FIPS modules carry the wrapper-level check. Skip the + * assertion for FIPSv2 builds. */ +#if defined(WOLFSSL_AES_CBC_LENGTH_CHECKS) && \ + (!defined(HAVE_FIPS) || FIPS_VERSION_GE(5,0)) ExpectIntEQ(wc_AesCbcEncrypt(aes, cipher, vector, vector_len - 1), WC_NO_ERR_TRACE(BAD_LENGTH_E)); #endif @@ -703,7 +710,9 @@ static int test_wc_AesCbcEncryptDecrypt_WithKey(Aes* aes, byte* key, ExpectIntEQ(wc_AesCbcDecrypt(aes, decrypted, cipher, WC_AES_BLOCK_SIZE * 2), 0); ExpectBufEQ(decrypted, vector, vector_len); -#ifdef WOLFSSL_AES_CBC_LENGTH_CHECKS +#if defined(WOLFSSL_AES_CBC_LENGTH_CHECKS) && \ + (!defined(HAVE_FIPS) || FIPS_VERSION_GE(5,0)) + /* Same FIPSv2 vs v5+ rationale as the encrypt assertion above. */ ExpectIntEQ(wc_AesCbcDecrypt(aes, decrypted, cipher, WC_AES_BLOCK_SIZE * 2 - 1), WC_NO_ERR_TRACE(BAD_LENGTH_E)); #else diff --git a/tests/api/test_evp_pkey.c b/tests/api/test_evp_pkey.c index 9bdd5b9339d..2e106d16d6a 100644 --- a/tests/api/test_evp_pkey.c +++ b/tests/api/test_evp_pkey.c @@ -1526,7 +1526,7 @@ static int test_wolfSSL_EVP_PKEY_sign_verify(int keyType) !defined(HAVE_SELFTEST) #if !defined(HAVE_FIPS) || (defined(HAVE_FIPS_VERSION) && (HAVE_FIPS_VERSION>2)) { - ExpectNotNull(rsa = RSA_generate_key(2048, 3, NULL, NULL)); + ExpectNotNull(rsa = RSA_generate_key(2048, 65537, NULL, NULL)); ExpectIntEQ(EVP_PKEY_assign_RSA(pkey, rsa), WOLFSSL_SUCCESS); } #endif @@ -2159,7 +2159,7 @@ int test_wolfSSL_EVP_PKEY_encrypt(void) XMEMSET(outDec, 0, rsaKeySz); } - ExpectNotNull(rsa = RSA_generate_key(2048, 3, NULL, NULL)); + ExpectNotNull(rsa = RSA_generate_key(2048, 65537, NULL, NULL)); ExpectNotNull(pkey = wolfSSL_EVP_PKEY_new()); ExpectIntEQ(EVP_PKEY_assign_RSA(pkey, rsa), WOLFSSL_SUCCESS); if (EXPECT_FAIL()) { diff --git a/tests/api/test_ossl_rsa.c b/tests/api/test_ossl_rsa.c index 26cf360105f..62cee2702a2 100644 --- a/tests/api/test_ossl_rsa.c +++ b/tests/api/test_ossl_rsa.c @@ -65,7 +65,7 @@ int test_wolfSSL_RSA(void) RSA_free(rsa); rsa = NULL; - ExpectNotNull(rsa = RSA_generate_key(2048, 3, NULL, NULL)); + ExpectNotNull(rsa = RSA_generate_key(2048, 65537, NULL, NULL)); ExpectIntEQ(RSA_size(rsa), 256); #if (!defined(HAVE_FIPS) || FIPS_VERSION3_GT(6,0,0)) && !defined(HAVE_SELFTEST) @@ -306,7 +306,7 @@ int test_wolfSSL_RSA(void) rsa = NULL; #if !defined(USE_FAST_MATH) || (FP_MAX_BITS >= (3072*2)) - ExpectNotNull(rsa = RSA_generate_key(3072, 17, NULL, NULL)); + ExpectNotNull(rsa = RSA_generate_key(3072, 65537, NULL, NULL)); ExpectIntEQ(RSA_size(rsa), 384); ExpectIntEQ(RSA_bits(rsa), 3072); RSA_free(rsa); @@ -461,7 +461,7 @@ int test_wolfSSL_RSA_print(void) RSA_free(rsa); rsa = NULL; - ExpectNotNull(rsa = RSA_generate_key(2048, 3, NULL, NULL)); + ExpectNotNull(rsa = RSA_generate_key(2048, 65537, NULL, NULL)); ExpectIntEQ(RSA_print(bio, rsa, 0), 1); ExpectIntEQ(RSA_print(bio, rsa, 4), 1); @@ -626,11 +626,11 @@ int test_wolfSSL_RSA_meth(void) RSA_METHOD *rsa_meth = NULL; #ifdef WOLFSSL_KEY_GEN - ExpectNotNull(rsa = RSA_generate_key(2048, 3, NULL, NULL)); + ExpectNotNull(rsa = RSA_generate_key(2048, 65537, NULL, NULL)); RSA_free(rsa); rsa = NULL; #else - ExpectNull(rsa = RSA_generate_key(2048, 3, NULL, NULL)); + ExpectNull(rsa = RSA_generate_key(2048, 65537, NULL, NULL)); #endif ExpectNotNull(RSA_get_default_method()); diff --git a/tests/api/test_slhdsa.c b/tests/api/test_slhdsa.c index 988bbc579e0..4510319925e 100644 --- a/tests/api/test_slhdsa.c +++ b/tests/api/test_slhdsa.c @@ -1081,12 +1081,14 @@ int test_wc_slhdsa_sign_hash(void) WC_HASH_TYPE_SHA256, sig, sigLen), WC_NO_ERR_TRACE(BAD_LENGTH_E)); - /* Unsupported hashType (FIPS 205 doesn't list WC_HASH_TYPE_NONE) hits - * the default branch of slhdsakey_validate_prehash. */ + /* WC_HASH_TYPE_NONE is the "pure SLH-DSA" sentinel and is never a valid + * pre-hash algorithm (FIPS 205 Section 10.2.2 / Table 9). HashSLH-DSA + * signing rejects it with an explicit early check (BAD_FUNC_ARG), not via + * the slhdsa_check_hash_for_n() switch default. */ sigLen = WC_SLHDSA_MAX_SIG_LEN; ExpectIntEQ(wc_SlhDsaKey_SignHash(&key, ctx, sizeof(ctx), hash, 32, WC_HASH_TYPE_NONE, sig, &sigLen, &rng), - WC_NO_ERR_TRACE(NOT_COMPILED_IN)); + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* Test SignHash with SHA-256. */ sigLen = WC_SLHDSA_MAX_SIG_LEN; diff --git a/wolfcrypt/benchmark/fips_cast_bench.c b/wolfcrypt/benchmark/fips_cast_bench.c new file mode 100644 index 00000000000..bd7c0e9dbc6 --- /dev/null +++ b/wolfcrypt/benchmark/fips_cast_bench.c @@ -0,0 +1,363 @@ +/* fips_cast_bench.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* FIPS CAST benchmark. + * + * Measures the wall-clock cost of each Conditional Algorithm Self-Test (CAST) + * defined by the wolfCrypt v7.0.0 FIPS module so operators can budget module + * power-on latency on resource-constrained operational environments (DSP, + * MCU) where every additional CAST is directly observable as boot-time delay. + * + * Compiled only when HAVE_FIPS is defined (see wolfcrypt/benchmark/include.am + * BUILD_FIPS gate). Calls wc_RunCast_fips(id) repeatedly per CAST and reports + * mean / stddev / min / max for each, plus total time for one pass over all + * enabled CASTs (the cost paid by callers that invoke wc_RunAllCast_fips() at + * application start). + * + * Citations: + * FIPS 140-3 sec 7.10 (Self-Tests) - CAST framework + * FIPS 140-3 IG 10.3.A - Algorithm-by-algorithm CAST coverage + * ISO/IEC 19790:2012 sec 7.10.2 - Conditional self-test execution + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#if !defined(WOLFSSL_USER_SETTINGS) && !defined(WOLFSSL_NO_OPTIONS_H) + #include +#endif +#include /* also picks up user_settings.h */ + +/* fips_cast_bench drives wc_RunCast_fips() / wc_RunAllCast_fips() which were + * introduced in the v7.0.0 module's CAST framework. Older 140-3 modules + * (v5.x, v6.0.0) and the FIPSv2 module do not export these symbols, so when + * fips-check.sh swaps in an older-flavor fips/ tree this file would otherwise + * fail to link. Gate the entire benchmark on FIPS_VERSION3_GE(7,0,0); for + * older flavors we fall through to the empty-main stub at the bottom of the + * file so the build still produces an executable. */ +#if defined(HAVE_FIPS) && FIPS_VERSION3_GE(7,0,0) + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#ifdef _WIN32 + #define WIN32_LEAN_AND_MEAN + #include +#else + #include +#endif + + +#define BENCH_DEFAULT_ITERS 10 + +/* Map FIPS_CAST_* enum value to a printable name. Kept in sync with + * wolfssl/wolfcrypt/fips_test.h FipsCastId enum. */ +static const char* cast_name(int id) +{ + switch (id) { + case FIPS_CAST_AES_CBC: return "AES-CBC"; + case FIPS_CAST_AES_GCM: return "AES-GCM"; + case FIPS_CAST_HMAC_SHA1: return "HMAC-SHA-1"; + case FIPS_CAST_HMAC_SHA2_256: return "HMAC-SHA2-256"; + case FIPS_CAST_HMAC_SHA2_512: return "HMAC-SHA2-512"; + case FIPS_CAST_HMAC_SHA3_256: return "HMAC-SHA3-256"; + case FIPS_CAST_DRBG: return "DRBG (SHA-256)"; + case FIPS_CAST_RSA_SIGN_PKCS1v15: return "RSA-SIGN-PKCS1v15"; + case FIPS_CAST_ECC_CDH: return "ECC-CDH"; + case FIPS_CAST_ECC_PRIMITIVE_Z: return "ECC-Primitive-Z"; + case FIPS_CAST_DH_PRIMITIVE_Z: return "DH-Primitive-Z"; + case FIPS_CAST_ECDSA: return "ECDSA"; + case FIPS_CAST_KDF_TLS12: return "KDF-TLS12"; + case FIPS_CAST_KDF_TLS13: return "KDF-TLS13"; + case FIPS_CAST_KDF_SSH: return "KDF-SSH"; +#if defined(FIPS_VERSION_GE) && FIPS_VERSION_GE(6,0) + case FIPS_CAST_KDF_SRTP: return "KDF-SRTP"; + case FIPS_CAST_ED25519: return "Ed25519"; + case FIPS_CAST_ED448: return "Ed448"; + case FIPS_CAST_PBKDF2: return "PBKDF2"; +#endif +#if defined(FIPS_VERSION_GE) && FIPS_VERSION_GE(7,0) + case FIPS_CAST_AES_ECB: return "AES-ECB"; + case FIPS_CAST_ML_KEM: return "ML-KEM"; + case FIPS_CAST_ML_DSA: return "ML-DSA"; + case FIPS_CAST_LMS: return "LMS"; + case FIPS_CAST_XMSS: return "XMSS"; + case FIPS_CAST_DRBG_SHA512: return "DRBG (SHA-512)"; + case FIPS_CAST_SLH_DSA: return "SLH-DSA"; + case FIPS_CAST_AES_CMAC: return "AES-CMAC"; + case FIPS_CAST_SHAKE: return "SHAKE"; + case FIPS_CAST_AES_KW: return "AES-KW"; +#endif + default: return "(unknown)"; + } +} + + +/* Monotonic clock in nanoseconds. POSIX clock_gettime(CLOCK_MONOTONIC) on + * Unix-like systems; QueryPerformanceCounter on Windows. */ +static long long now_ns(void) +{ +#ifdef _WIN32 + static LARGE_INTEGER freq = { 0 }; + LARGE_INTEGER count; + if (freq.QuadPart == 0) + QueryPerformanceFrequency(&freq); + QueryPerformanceCounter(&count); + /* Multiply before divide to keep precision; freq is typically 10MHz. */ + return (long long)((count.QuadPart * 1000000000LL) / freq.QuadPart); +#else + struct timespec ts; + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) + return 0; + return (long long)ts.tv_sec * 1000000000LL + (long long)ts.tv_nsec; +#endif +} + + +/* Run a single CAST iters times, populate stats (in milliseconds). + * Returns 0 on success, non-zero on first CAST failure. */ +static int run_one_cast(int id, int iters, + double* out_mean_ms, double* out_stddev_ms, + double* out_min_ms, double* out_max_ms) +{ + int i; + long long total = 0; + long long mn = LLONG_MAX; + long long mx = 0; + long long* samples; + double mean_ns; + double variance_acc = 0.0; + + if (iters <= 0) + return BAD_FUNC_ARG; + + samples = (long long*)XMALLOC((size_t)iters * sizeof(long long), NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (samples == NULL) + return MEMORY_E; + + for (i = 0; i < iters; i++) { + long long t0, t1, dt; + int rc; + + t0 = now_ns(); + rc = wc_RunCast_fips(id); + t1 = now_ns(); + if (rc != 0) { + XFREE(samples, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return rc; + } + dt = t1 - t0; + if (dt < 0) + dt = 0; + samples[i] = dt; + total += dt; + if (dt < mn) + mn = dt; + if (dt > mx) + mx = dt; + } + + mean_ns = (double)total / (double)iters; + for (i = 0; i < iters; i++) { + double d = (double)samples[i] - mean_ns; + variance_acc += d * d; + } + XFREE(samples, NULL, DYNAMIC_TYPE_TMP_BUFFER); + + *out_mean_ms = mean_ns / 1.0e6; + *out_stddev_ms = sqrt(variance_acc / (double)iters) / 1.0e6; + *out_min_ms = (double)mn / 1.0e6; + *out_max_ms = (double)mx / 1.0e6; + return 0; +} + + +static void usage(const char* prog) +{ + printf("usage: %s [-i ITERS] [-c CAST_ID] [-l]\n", prog); + printf(" -i ITERS iterations per CAST (default %d)\n", + BENCH_DEFAULT_ITERS); + printf(" -c CAST_ID benchmark only the named CAST id\n"); + printf(" -l list CAST ids and names; do not run\n"); + printf(" -h show this help\n"); +} + + +int main(int argc, char** argv) +{ + int iters = BENCH_DEFAULT_ITERS; + int single = -1; + int list_only = 0; + int i; + int first, last; + int failures = 0; + int run_count = 0; + double total_mean_ms = 0.0; + + for (i = 1; i < argc; i++) { + if (XSTRCMP(argv[i], "-i") == 0 && i + 1 < argc) { + iters = atoi(argv[++i]); + if (iters <= 0) { + fprintf(stderr, "-i requires a positive iteration count\n"); + return 2; + } + } else if (XSTRCMP(argv[i], "-c") == 0 && i + 1 < argc) { + single = atoi(argv[++i]); + } else if (XSTRCMP(argv[i], "-l") == 0) { + list_only = 1; + } else if (XSTRCMP(argv[i], "-h") == 0 + || XSTRCMP(argv[i], "--help") == 0) { + usage(argv[0]); + return 0; + } else { + fprintf(stderr, "unknown argument: %s\n", argv[i]); + usage(argv[0]); + return 2; + } + } + + if (list_only) { + printf("FIPS CAST IDs (FIPS_CAST_COUNT = %d):\n", FIPS_CAST_COUNT); + for (i = 0; i < FIPS_CAST_COUNT; i++) + printf(" %2d %s\n", i, cast_name(i)); + return 0; + } + + if (single >= 0 && single >= FIPS_CAST_COUNT) { + fprintf(stderr, "CAST id %d out of range (0..%d)\n", + single, FIPS_CAST_COUNT - 1); + return 2; + } + + printf("wolfCrypt FIPS CAST benchmark\n"); + printf("Library version: %s\n", LIBWOLFSSL_VERSION_STRING); + printf("FIPS_CAST_COUNT: %d\n", FIPS_CAST_COUNT); + printf("Iterations per CAST: %d\n", iters); + printf("Clock: %s\n", +#ifdef _WIN32 + "QueryPerformanceCounter" +#else + "clock_gettime(CLOCK_MONOTONIC)" +#endif + ); + printf("\n"); + + /* Register the default DRBG seed callback (mirrors benchmark.c and + * wolfcrypt/test/test.c). Builds with WC_RNG_SEED_CB - which include + * the FIPS optest CFLAGS - require every application that initializes + * the RNG to register a seed generator before _InitRng can produce a + * working DRBG; without it, wc_InitRng inside the ECC_PRIMITIVE_Z and + * ECDSA CASTs returns -199 (RNG_FAILURE_E) and the dependent CASTs + * cascade-fail. */ +#ifdef WC_RNG_SEED_CB + { + int seed_cb_rc = wc_SetSeed_Cb(WC_GENERATE_SEED_DEFAULT); + if (seed_cb_rc != 0) { + fprintf(stderr, + "wc_SetSeed_Cb returned %d - DRBG-using CASTs will fail.\n", + seed_cb_rc); + } + } +#endif + + /* Prime: run every CAST once via wc_RunAllCast_fips() so each CAST + * reaches FIPS_CAST_STATE_SUCCESS before we begin measuring. This + * isolates the per-CAST KAT runtime cost from the cascading + * recursive-CAST init chain that fires on the first invocation of a + * cold CAST whose KAT internally calls FIPS-wrapped primitives whose + * own CASTs are still in INIT state. Customers calling + * wc_RunAllCast_fips() at boot pay this one-time cost up front, so + * priming here matches that real-world workflow. */ + { + int prime_rc = wc_RunAllCast_fips(); + if (prime_rc != 0) { + fprintf(stderr, + "wc_RunAllCast_fips() prime returned %d - some CASTs may have failed.\n" + "Per-CAST measurements continue but failed CASTs will report errors.\n\n", + prime_rc); + } + } + + printf("ID | Name | Mean(ms) | StdDev(ms) | Min(ms) " + "| Max(ms)\n"); + printf("---+---------------------+----------+------------+---------" + "+---------\n"); + + first = (single >= 0) ? single : 0; + last = (single >= 0) ? single + 1 : FIPS_CAST_COUNT; + + for (i = first; i < last; i++) { + double mean_ms = 0, sd_ms = 0, mn_ms = 0, mx_ms = 0; + int rc = run_one_cast(i, iters, &mean_ms, &sd_ms, &mn_ms, &mx_ms); + if (rc != 0) { + printf("%2d | %-19s | FAILED rc=%d (%s)\n", + i, cast_name(i), rc, wc_GetErrorString(rc)); + failures++; + continue; + } + printf("%2d | %-19s | %8.3f | %10.3f | %7.3f | %7.3f\n", + i, cast_name(i), mean_ms, sd_ms, mn_ms, mx_ms); + total_mean_ms += mean_ms; + run_count++; + } + + printf("\n"); + if (run_count > 0) { + printf("Sum of mean CAST times (one wc_RunAllCast_fips() pass): " + "%.3f ms\n", total_mean_ms); + } + if (failures > 0) { + printf("WARN: %d CAST(s) failed.\n", failures); + return 1; + } + return 0; +} + +#else /* !(HAVE_FIPS && FIPS_VERSION3_GE(7,0,0)) */ + +#include + +int main(void) +{ +#ifndef HAVE_FIPS + fprintf(stderr, + "fips_cast_bench: built without HAVE_FIPS - nothing to measure\n"); +#else + fprintf(stderr, + "fips_cast_bench: requires v7.0.0+ FIPS module " + "(wc_RunCast_fips / wc_RunAllCast_fips were added in v7) - " + "nothing to measure on this older module flavor\n"); +#endif + return 0; +} + +#endif /* HAVE_FIPS && FIPS_VERSION3_GE(7,0,0) */ diff --git a/wolfcrypt/benchmark/include.am b/wolfcrypt/benchmark/include.am index 22cecbdaefe..130343a14e1 100644 --- a/wolfcrypt/benchmark/include.am +++ b/wolfcrypt/benchmark/include.am @@ -10,6 +10,16 @@ wolfcrypt_benchmark_benchmark_LDADD = src/libwolfssl@LIBSUFFIX@.la $(LIB_ wolfcrypt_benchmark_benchmark_DEPENDENCIES = src/libwolfssl@LIBSUFFIX@.la noinst_HEADERS += wolfcrypt/benchmark/benchmark.h +# FIPS CAST benchmark - measures wc_RunCast_fips() execution time per CAST. +# Helps operators of resource-constrained operational environments budget +# module power-on latency. Compiled only when FIPS is enabled. +if BUILD_FIPS +noinst_PROGRAMS += wolfcrypt/benchmark/fips_cast_bench +wolfcrypt_benchmark_fips_cast_bench_SOURCES = wolfcrypt/benchmark/fips_cast_bench.c +wolfcrypt_benchmark_fips_cast_bench_LDADD = src/libwolfssl@LIBSUFFIX@.la $(LIB_STATIC_ADD) -lm +wolfcrypt_benchmark_fips_cast_bench_DEPENDENCIES = src/libwolfssl@LIBSUFFIX@.la +endif + endif endif diff --git a/wolfcrypt/src/aes.c b/wolfcrypt/src/aes.c index dccab8ff785..341d3af2502 100644 --- a/wolfcrypt/src/aes.c +++ b/wolfcrypt/src/aes.c @@ -11334,6 +11334,16 @@ int wc_AesGcmDecrypt(Aes* aes, byte* out, const byte* in, word32 sz, VECTOR_REGISTERS_POP; + /* FIPS 140-3 / SP 800-38D: on authentication failure, the decrypted-but- + * unauthenticated plaintext in `out` must not be released to the caller. + * Wipe it here so a caller that ignores the return value cannot observe + * plaintext derived from forged ciphertext. All software paths (AES-NI, + * AVX1/2, ARM HW/NEON, C fallback) funnel through `ret` here, so this + * single guard covers every sub-implementation. */ + if (ret == WC_NO_ERR_TRACE(AES_GCM_AUTH_E) && out != NULL && sz > 0) { + ForceZero(out, sz); + } + return ret; } #endif @@ -13037,6 +13047,10 @@ int wc_AesGcmDecryptFinal(Aes* aes, const byte* authTag, word32 authTagSz) } } + /* Streaming decrypt cannot zeroize prior Update output buffers from here + * (Final does not see them). On AES_GCM_AUTH_E, the caller is responsible + * for treating all Update-produced plaintext as invalid and wiping it. + * See PL-R34 Security Policy section 8 (Operational Rules). */ return ret; } #endif /* HAVE_AES_DECRYPT || HAVE_AESGCM_DECRYPT */ diff --git a/wolfcrypt/src/dh.c b/wolfcrypt/src/dh.c index c7560d185c0..56fa2dce9da 100644 --- a/wolfcrypt/src/dh.c +++ b/wolfcrypt/src/dh.c @@ -1422,8 +1422,20 @@ int wc_DhGeneratePublic(DhKey* key, byte* priv, word32 privSz, #if FIPS_VERSION_GE(5,0) || defined(WOLFSSL_VALIDATE_DH_KEYGEN) if (ret == 0) ret = _ffc_validate_public_key(key, pub, *pubSz, NULL, 0, 0); - if (ret == 0) - ret = _ffc_pairwise_consistency_test(key, pub, *pubSz, priv, privSz); + if (ret == 0) { + /* Pairwise Consistency Test per SP 800-56A r3 sec 5.6.2.1.4 + * (FFC key pair). FIPS 140-3 IG 10.3.B requires a PCT after + * KeyGen for key-establishment algorithms; on failure under a + * FIPS build the error is remapped to DH_PCT_E so the FIPS + * module's DEGRADE_STATE handler transitions FIPS_CAST_DH_ + * PRIMITIVE_Z to the error state. */ + ret = _ffc_pairwise_consistency_test(key, pub, *pubSz, priv, + privSz); + #ifdef HAVE_FIPS + if (ret != 0) + ret = DH_PCT_E; + #endif + } #endif /* FIPS V5 or later || WOLFSSL_VALIDATE_DH_KEYGEN */ return ret; @@ -1446,8 +1458,20 @@ static int wc_DhGenerateKeyPair_Sync(DhKey* key, WC_RNG* rng, #if FIPS_VERSION_GE(5,0) || defined(WOLFSSL_VALIDATE_DH_KEYGEN) if (ret == 0) ret = _ffc_validate_public_key(key, pub, *pubSz, NULL, 0, 0); - if (ret == 0) - ret = _ffc_pairwise_consistency_test(key, pub, *pubSz, priv, *privSz); + if (ret == 0) { + /* Pairwise Consistency Test per SP 800-56A r3 sec 5.6.2.1.4 + * (FFC key pair). FIPS 140-3 IG 10.3.B requires a PCT after + * KeyGen for key-establishment algorithms; on failure under a + * FIPS build the error is remapped to DH_PCT_E so the FIPS + * module's DEGRADE_STATE handler transitions FIPS_CAST_DH_ + * PRIMITIVE_Z to the error state. */ + ret = _ffc_pairwise_consistency_test(key, pub, *pubSz, priv, + *privSz); + #ifdef HAVE_FIPS + if (ret != 0) + ret = DH_PCT_E; + #endif + } #endif /* FIPS V5 or later || WOLFSSL_VALIDATE_DH_KEYGEN */ return ret; diff --git a/wolfcrypt/src/error.c b/wolfcrypt/src/error.c index 8826d9b1988..f8bb774bed5 100644 --- a/wolfcrypt/src/error.c +++ b/wolfcrypt/src/error.c @@ -692,6 +692,21 @@ const char* wc_GetErrorString(int error) case SLH_DSA_KAT_FIPS_E: return "SLH-DSA Known Answer Test check FIPS error"; + case SLH_DSA_PCT_E: + return "wolfcrypt SLH-DSA Pairwise Consistency Test Failure"; + + case CMAC_KAT_FIPS_E: + return "AES-CMAC Known Answer Test FIPS error"; + + case SHAKE_KAT_FIPS_E: + return "SHAKE Known Answer Test FIPS error"; + + case DH_PCT_E: + return "wolfcrypt DH (FFC) Pairwise Consistency Test Failure"; + + case AES_KW_KAT_FIPS_E: + return "AES-KW Known Answer Test FIPS error"; + case SEQ_OVERFLOW_E: return "Sequence counter would overflow"; diff --git a/wolfcrypt/src/rsa.c b/wolfcrypt/src/rsa.c index 0a6b6143a7a..d313f81ac95 100644 --- a/wolfcrypt/src/rsa.c +++ b/wolfcrypt/src/rsa.c @@ -5153,9 +5153,15 @@ static WC_INLINE int RsaSizeCheck(int size) } #ifdef HAVE_FIPS - /* Key size requirements for CAVP */ + /* Approved RSA key sizes per FIPS 186-5 sec 5.1 and NIST SP 800-131Ar2 + * sec 4 Table 2 (Asymmetric Key Establishment) - 2048, 3072, 4096 only. + * 1024-bit RSA was deprecated for FIPS-Approved key generation by + * SP 800-131Ar2 effective 2014-01-01 and is disallowed thereafter. The + * outer wc_MakeRsaKey_fips wrapper already gates on WC_RSA_FIPS_GEN_MIN, + * but RsaSizeCheck itself is reached by library-internal paths that do + * not pass through that wrapper - defense-in-depth removal here closes + * the gap. */ switch (size) { - case 1024: case 2048: case 3072: case 4096: @@ -5415,6 +5421,20 @@ int wc_MakeRsaKey(RsaKey* key, int size, long e, WC_RNG* rng) goto out; } +#ifdef HAVE_FIPS + /* FIPS 186-5 sec 5.2 (Public Verification Exponent e): 2^16 + 1 <= e < + * 2^256 and e odd. The general non-FIPS check above accepts e >= 3 odd; + * the FIPS Approved range is narrower. e is a long here so the upper + * bound 2^256 is structurally satisfied on any LP64 / LLP64 platform + * (long is at most 64 bits), but the lower bound 65537 must be enforced + * explicitly. Defense-in-depth even though FIPS application code + * conventionally passes e = 65537 (RSA_F4). */ + if (e < 65537L) { + err = BAD_FUNC_ARG; + goto out; + } +#endif + #if defined(WOLFSSL_CRYPTOCELL) err = cc310_RSA_GenerateKeyPair(key, size, e); goto out; diff --git a/wolfcrypt/src/wc_mldsa.c b/wolfcrypt/src/wc_mldsa.c index 3e08dad88a6..d3ab2003840 100644 --- a/wolfcrypt/src/wc_mldsa.c +++ b/wolfcrypt/src/wc_mldsa.c @@ -760,8 +760,103 @@ static int mldsa_hash256_ctx_msg(wc_Shake* shake256, const byte* tr, * @return 0 on success. * @return BAD_FUNC_ARG if hash algorithm not known. */ -static int mldsa_get_hash_oid(int hash, byte* oidBuffer, word32* oidLen) -{ +/* HashML-DSA PH-vs-paramSet enforcement. + * + * FIPS 204 sec. 5.4 (Table 4) restricts the pre-hash PH for HashML-DSA to + * algorithms whose collision-resistance strength meets or exceeds the + * parameter set's claimed security level. Any other PH must be rejected + * for BOTH sigGen and sigVer: + * ML-DSA-44 (128-bit): SHA2-256, SHA2-384, SHA2-512, SHA2-512/256, + * SHA3-256, SHA3-384, SHA3-512, + * SHAKE-128, SHAKE-256 + * ML-DSA-65 (192-bit): SHA2-384, SHA2-512, SHA3-384, SHA3-512, SHAKE-256 + * ML-DSA-87 (256-bit): SHA2-512, SHA3-512, SHAKE-256 + * + * Returns 0 if (hashAlg, level) is an approved combination. Returns + * BAD_FUNC_ARG otherwise -- including for any hash not on the approved + * list (SHA-224, SHA-512/224, SHA3-224, etc.). + */ +static int mldsa_check_hash_for_level(int hashAlg, byte level) +{ + int strengthBits; /* collision-resistance strength of the chosen hash */ + int requiredBits; /* security level required by the paramSet */ + + switch (hashAlg) { + #ifndef NO_SHA256 + case WC_HASH_TYPE_SHA256: + strengthBits = 128; + break; + #endif + #ifdef WOLFSSL_SHA384 + case WC_HASH_TYPE_SHA384: + strengthBits = 192; + break; + #endif + #ifdef WOLFSSL_SHA512 + case WC_HASH_TYPE_SHA512: + strengthBits = 256; + break; + #ifndef WOLFSSL_NOSHA512_256 + case WC_HASH_TYPE_SHA512_256: + /* SHA-512/256 has 128-bit collision resistance (truncated). */ + strengthBits = 128; + break; + #endif + #endif + #ifdef WOLFSSL_SHA3 + #ifndef WOLFSSL_NOSHA3_256 + case WC_HASH_TYPE_SHA3_256: + strengthBits = 128; + break; + #endif + #ifndef WOLFSSL_NOSHA3_384 + case WC_HASH_TYPE_SHA3_384: + strengthBits = 192; + break; + #endif + #ifndef WOLFSSL_NOSHA3_512 + case WC_HASH_TYPE_SHA3_512: + strengthBits = 256; + break; + #endif + #endif + #ifdef WOLFSSL_SHAKE128 + case WC_HASH_TYPE_SHAKE128: + strengthBits = 128; + break; + #endif + #ifdef WOLFSSL_SHAKE256 + case WC_HASH_TYPE_SHAKE256: + strengthBits = 256; + break; + #endif + default: + /* Hash not on the FIPS 204 Table 4 approved list (e.g. SHA-224, + * SHA-512/224, SHA3-224, MD5). Reject regardless of level. */ + return BAD_FUNC_ARG; + } + + switch (level) { + case WC_ML_DSA_44: + requiredBits = 128; + break; + case WC_ML_DSA_65: + requiredBits = 192; + break; + case WC_ML_DSA_87: + requiredBits = 256; + break; + default: + return BAD_FUNC_ARG; + } + + if (strengthBits < requiredBits) { + return BAD_FUNC_ARG; + } + return 0; +} + +static int mldsa_get_hash_oid(int hash, byte* oidBuffer, word32* oidLen){ int ret = 0; const byte* oid; @@ -9428,11 +9523,17 @@ static int mldsa_sign_ctx_hash_with_seed(wc_MlDsaKey* key, byte oidMsgHash[MLDSA_HASH_OID_LEN + WC_MAX_DIGEST_SIZE]; word32 oidMsgHashLen = 0; - /* Check that the input hash length is valid. */ + /* Check that the input hash length is valid (guards against caller-side + * buffer overruns before we touch hash). */ if ((int)hashLen != wc_HashGetDigestSize((enum wc_HashType)hashAlg)) { ret = BAD_LENGTH_E; } + /* FIPS 204 sec. 5.4 Table 4: enforce hash <-> paramSet matching. */ + if (ret == 0) { + ret = mldsa_check_hash_for_level(hashAlg, key->level); + } + if (ret == 0) { XMEMCPY(seedMu, seed, MLDSA_RND_SZ); @@ -10101,12 +10202,17 @@ static int mldsa_verify_ctx_hash(wc_MlDsaKey* key, const byte* ctx, if (key == NULL) { ret = BAD_FUNC_ARG; } - /* Check that the input hash length is valid. */ + /* Check that the input hash length is valid (guards against caller-side + * buffer overruns before we touch hash). */ if ((ret == 0) && ((int)hashLen != wc_HashGetDigestSize((enum wc_HashType)hashAlg))) { ret = BAD_LENGTH_E; } + /* FIPS 204 sec. 5.4 Table 4: enforce hash <-> paramSet matching. */ + if (ret == 0) { + ret = mldsa_check_hash_for_level(hashAlg, key->level); + } if (ret == 0) { /* Step 6: Hash public key. */ diff --git a/wolfcrypt/src/wc_mlkem.c b/wolfcrypt/src/wc_mlkem.c index 89f647ebefb..cc7234ff8cf 100644 --- a/wolfcrypt/src/wc_mlkem.c +++ b/wolfcrypt/src/wc_mlkem.c @@ -694,49 +694,12 @@ int wc_MlKemKey_MakeKey(MlKemKey* key, WC_RNG* rng) ret = wc_MlKemKey_MakeKeyWithRandom(key, rand, sizeof(rand)); } -#ifdef HAVE_FIPS - /* Pairwise Consistency Test (PCT) per FIPS 140-3 / ISO 19790:2012 - * Section 7.10.3.3: encapsulate with ek, decapsulate with dk, - * verify shared secrets match. */ - if (ret == 0) { - WC_DECLARE_VAR(pct_ct, byte, WC_ML_KEM_MAX_CIPHER_TEXT_SIZE, - key->heap); - byte pct_ss1[WC_ML_KEM_SS_SZ]; - byte pct_ss2[WC_ML_KEM_SS_SZ]; - word32 ctSz = 0; - - WC_ALLOC_VAR_EX(pct_ct, byte, WC_ML_KEM_MAX_CIPHER_TEXT_SIZE, - key->heap, DYNAMIC_TYPE_TMP_BUFFER, ret = MEMORY_E); - - if (ret == 0) - ret = wc_MlKemKey_CipherTextSize(key, &ctSz); - - if (ret == 0) - ret = wc_MlKemKey_Encapsulate(key, pct_ct, pct_ss1, rng); - - if (ret == 0) - ret = wc_MlKemKey_Decapsulate(key, pct_ss2, pct_ct, ctSz); - - if (ret == 0) { - if (XMEMCMP(pct_ss1, pct_ss2, WC_ML_KEM_SS_SZ) != 0) - ret = ML_KEM_PCT_E; - } - - ForceZero(pct_ss1, sizeof(pct_ss1)); - ForceZero(pct_ss2, sizeof(pct_ss2)); - if (WC_VAR_OK(pct_ct)) - ForceZero(pct_ct, WC_ML_KEM_MAX_CIPHER_TEXT_SIZE); - - WC_FREE_VAR_EX(pct_ct, key->heap, DYNAMIC_TYPE_TMP_BUFFER); - - /* FIPS 140-3 IG 10.3.A (TE10.35.02): a key pair that fails the PCT - * must be rendered unusable. Zeroize the generated key material so - * a caller that ignores the return value cannot use it. */ - if (ret != 0) { - wc_MlKemKey_Free(key); - } - } -#endif /* HAVE_FIPS */ + /* PCT now lives in wc_MlKemKey_MakeKeyWithRandom() (called above) so + * that BOTH the random-seeded path (this function) and the + * caller-supplied-seed path (direct invocation of MakeKeyWithRandom) + * exercise the FIPS 140-3 IG 10.3.A 1.B Pairwise Consistency Test. + * Audit A16-1: previously the PCT lived only here, leaving the + * deterministic-seed entry uncovered. */ /* Ensure seeds are zeroized. */ ForceZero((void*)rand, (word32)sizeof(rand)); @@ -972,8 +935,70 @@ int wc_MlKemKey_MakeKeyWithRandom(MlKemKey* key, const unsigned char* rand, } #endif - /* Note: PCT is performed in wc_MlKemKey_MakeKey() which calls this - * function and has the RNG parameter needed for encapsulation. */ +#ifdef HAVE_FIPS + /* Pairwise Consistency Test (PCT) per FIPS 140-3 IG 10.3.A 1.B and + * ISO/IEC 19790:2012 Section 7.10.3.3: encapsulate with the freshly + * generated encapsulation key (ek), decapsulate the ciphertext with + * the matching decapsulation key (dk), and verify the recovered + * shared secret matches. This entry point (MakeKeyWithRandom) is + * a deterministic key-gen path with no caller-supplied RNG; the PCT + * uses wc_MlKemKey_EncapsulateWithRandom() with a fixed 32-byte test + * value for `m` (FIPS 203 Algorithm 17 input). The encapsulation + * `m` does not need to be unpredictable for the PCT - it only needs + * the encap/decap roundtrip to recover the same shared secret. + * + * Audit A16-1: previously the PCT lived only in wc_MlKemKey_MakeKey + * which generates `rand` from the DRBG, leaving callers of this + * deterministic-seed entry without PCT coverage. */ + if (ret == 0) { + WC_DECLARE_VAR(pct_ct, byte, WC_ML_KEM_MAX_CIPHER_TEXT_SIZE, + key->heap); + byte pct_ss1[WC_ML_KEM_SS_SZ]; + byte pct_ss2[WC_ML_KEM_SS_SZ]; + word32 pct_ctSz = 0; + /* Fixed 32-byte test pattern for FIPS 203 Alg 17 `m` parameter. + * Value is arbitrary - PCT only requires encap/decap roundtrip, + * not encap unpredictability. */ + static const byte pct_m[WC_ML_KEM_ENC_RAND_SZ] = { + 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, + 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, + 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, + 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB + }; + + WC_ALLOC_VAR_EX(pct_ct, byte, WC_ML_KEM_MAX_CIPHER_TEXT_SIZE, + key->heap, DYNAMIC_TYPE_TMP_BUFFER, ret = MEMORY_E); + + if (ret == 0) + ret = wc_MlKemKey_CipherTextSize(key, &pct_ctSz); + + if (ret == 0) + ret = wc_MlKemKey_EncapsulateWithRandom(key, pct_ct, pct_ss1, + pct_m, (int)sizeof(pct_m)); + + if (ret == 0) + ret = wc_MlKemKey_Decapsulate(key, pct_ss2, pct_ct, pct_ctSz); + + if (ret == 0) { + if (XMEMCMP(pct_ss1, pct_ss2, WC_ML_KEM_SS_SZ) != 0) + ret = ML_KEM_PCT_E; + } + + ForceZero(pct_ss1, sizeof(pct_ss1)); + ForceZero(pct_ss2, sizeof(pct_ss2)); + if (WC_VAR_OK(pct_ct)) + ForceZero(pct_ct, WC_ML_KEM_MAX_CIPHER_TEXT_SIZE); + + WC_FREE_VAR_EX(pct_ct, key->heap, DYNAMIC_TYPE_TMP_BUFFER); + + /* FIPS 140-3 IG 10.3.A (TE10.35.02): a key pair that fails the PCT + * must be rendered unusable. Zeroize the generated key material so + * a caller that ignores the return value cannot use it. */ + if (ret != 0) { + wc_MlKemKey_Free(key); + } + } +#endif /* HAVE_FIPS */ return ret; } diff --git a/wolfcrypt/src/wc_slhdsa.c b/wolfcrypt/src/wc_slhdsa.c index e1e7b115e0e..581e845358a 100644 --- a/wolfcrypt/src/wc_slhdsa.c +++ b/wolfcrypt/src/wc_slhdsa.c @@ -6890,6 +6890,49 @@ int wc_SlhDsaKey_MakeKey(SlhDsaKey* key, WC_RNG* rng) key->sk + 2 * n, n); } +#ifdef HAVE_FIPS + /* Pairwise Consistency Test (PCT) per FIPS 140-3 IG 10.3.A (TE10.35.02): + * sign with the new sk, verify with the matching pk. SLH-DSA is a + * stateless hash-based signature scheme (FIPS 205), so the relaxed PCT + * rule for stateful HBS (LMS/XMSS) does not apply -- PCT runs on every + * KeyGen. SignDeterministic avoids consuming RNG state; heap allocation + * is used because SLH-DSA signatures can reach ~50 KB. The paramSet is + * known by this point, so allocate the exact signature length for this + * variant rather than the family-wide worst case -- the difference + * across SLH-DSA variants is roughly 8 KB to 50 KB. */ + if (ret == 0) { + static const byte pct_msg[] = "wolfSSL SLH-DSA PCT"; + word32 pct_sigLen = key->params->sigLen; + byte* pct_sig = (byte*)XMALLOC(pct_sigLen, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + word32 pct_sigSz = pct_sigLen; + + if (pct_sig == NULL) { + ret = MEMORY_E; + } + if (ret == 0) { + ret = wc_SlhDsaKey_SignDeterministic(key, NULL, 0, + pct_msg, sizeof(pct_msg), pct_sig, &pct_sigSz); + } + if (ret == 0) { + ret = wc_SlhDsaKey_Verify(key, NULL, 0, + pct_msg, sizeof(pct_msg), pct_sig, pct_sigSz); + if (ret != 0) { + ret = SLH_DSA_PCT_E; + } + } + if (pct_sig != NULL) { + ForceZero(pct_sig, pct_sigLen); + XFREE(pct_sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + /* IG 10.3.A (TE10.35.02): a key pair that fails the PCT must be + * rendered unusable. */ + if (ret != 0) { + wc_SlhDsaKey_Free(key); + } + } +#endif /* HAVE_FIPS */ + return ret; } @@ -7836,6 +7879,99 @@ static const byte slhdsakey_oid_sha3_512[] = { #endif #endif +/* HashSLH-DSA PH-vs-paramSet enforcement. + * + * FIPS 205 sec. 10.2.2 (Table 9) restricts the pre-hash PH for HashSLH-DSA + * to algorithms whose collision-resistance strength meets or exceeds the + * parameter set's security level (encoded as key->params->n in bytes): + * n = 16 (128-bit): SHA2-256, SHA2-384, SHA2-512, SHA2-512/256, + * SHA3-256, SHA3-384, SHA3-512, + * SHAKE-128, SHAKE-256 + * n = 24 (192-bit): SHA2-384, SHA2-512, SHA3-384, SHA3-512, SHAKE-256 + * n = 32 (256-bit): SHA2-512, SHA3-512, SHAKE-256 + * + * Returns 0 if (hashType, n) is an approved combination. Returns + * BAD_FUNC_ARG otherwise -- including for any hash not on the approved + * list (SHA-224, SHA-512/224, SHA3-224, etc.). + */ +static int slhdsa_check_hash_for_n(enum wc_HashType hashType, byte n) +{ + int strengthBits; + int requiredBits; + + switch ((int)hashType) { + #ifndef NO_SHA256 + case WC_HASH_TYPE_SHA256: + strengthBits = 128; + break; + #endif + #ifdef WOLFSSL_SHA384 + case WC_HASH_TYPE_SHA384: + strengthBits = 192; + break; + #endif + #ifdef WOLFSSL_SHA512 + case WC_HASH_TYPE_SHA512: + strengthBits = 256; + break; + #ifndef WOLFSSL_NOSHA512_256 + case WC_HASH_TYPE_SHA512_256: + /* SHA-512/256 has 128-bit collision resistance (truncated). */ + strengthBits = 128; + break; + #endif + #endif + #ifdef WOLFSSL_SHA3 + #ifndef WOLFSSL_NOSHA3_256 + case WC_HASH_TYPE_SHA3_256: + strengthBits = 128; + break; + #endif + #ifndef WOLFSSL_NOSHA3_384 + case WC_HASH_TYPE_SHA3_384: + strengthBits = 192; + break; + #endif + #ifndef WOLFSSL_NOSHA3_512 + case WC_HASH_TYPE_SHA3_512: + strengthBits = 256; + break; + #endif + #endif + #ifdef WOLFSSL_SHAKE128 + case WC_HASH_TYPE_SHAKE128: + strengthBits = 128; + break; + #endif + #ifdef WOLFSSL_SHAKE256 + case WC_HASH_TYPE_SHAKE256: + strengthBits = 256; + break; + #endif + default: + /* Hash not on the FIPS 205 Table 9 approved list. */ + return BAD_FUNC_ARG; + } + + if (n == WC_SLHDSA_N_128) { + requiredBits = 128; + } + else if (n == WC_SLHDSA_N_192) { + requiredBits = 192; + } + else if (n == WC_SLHDSA_N_256) { + requiredBits = 256; + } + else { + return BAD_FUNC_ARG; + } + + if (strengthBits < requiredBits) { + return BAD_FUNC_ARG; + } + return 0; +} + /* Validate the caller-supplied pre-hashed digest length and look up the * corresponding OID for the chosen hash algorithm. * @@ -8053,6 +8189,16 @@ static int slhdsakey_signhash_external(SlhDsaKey* key, const byte* ctx, (sigSz == NULL)) { ret = BAD_FUNC_ARG; } + /* HashSLH-DSA requires an explicit, approved pre-hash algorithm. + * WC_HASH_TYPE_NONE is the "pure SLH-DSA" sentinel used by the non + * pre-hash Sign/Verify paths and is never valid here. Reject it + * explicitly (FIPS 205 Section 10.2.2 / Table 9) rather than relying on + * the slhdsa_check_hash_for_n() switch default below, so the rejection + * survives any future reordering of the validators or the addition of a + * WC_HASH_TYPE_NONE case to that switch. */ + else if (hashType == WC_HASH_TYPE_NONE) { + ret = BAD_FUNC_ARG; + } /* Check sig buffer is large enough to hold generated signature. */ else if (*sigSz < key->params->sigLen) { ret = BAD_LENGTH_E; @@ -8062,6 +8208,12 @@ static int slhdsakey_signhash_external(SlhDsaKey* key, const byte* ctx, /* Alg 23, Step 6: Return error. */ ret = BAD_FUNC_ARG; } + /* FIPS 205 sec. 10.2.2 Table 9: enforce PH <-> paramSet matching before + * pre-hashing the message. Rejects PHs whose collision-resistance + * strength is below the paramSet's security level (n). */ + if (ret == 0) { + ret = slhdsa_check_hash_for_n(hashType, key->params->n); + } if (ret == 0) { /* Alg 23, Steps 8-23: Validate caller-supplied pre-hashed digest length * and select OID for the chosen hash algorithm. */ @@ -8296,8 +8448,11 @@ int wc_SlhDsaKey_SignHash(SlhDsaKey* key, const byte* ctx, byte ctxSz, ret = MISSING_KEY; } /* First sanity check on hashType; the downstream prehash validator does - * the detailed check for the actual type. */ - else if ((word32)hashType > (word32)WC_HASH_TYPE_MAX) { + * the detailed check for the actual type. Reject WC_HASH_TYPE_NONE here + * too -- it is never a valid pre-hash (FIPS 205 Section 10.2.2 / Table 9), + * so a known-invalid call fails before consuming DRBG output below. */ + else if ((hashType == WC_HASH_TYPE_NONE) || + ((word32)hashType > (word32)WC_HASH_TYPE_MAX)) { ret = BAD_FUNC_ARG; } @@ -8426,6 +8581,12 @@ int wc_SlhDsaKey_VerifyHash(SlhDsaKey* key, const byte* ctx, byte ctxSz, } #ifdef WOLF_CRYPTO_CB + /* FIPS 205 sec. 10.2.2 Table 9: enforce PH <-> paramSet matching. + * Rejects PHs whose collision-resistance strength is below the + * paramSet's security level (n). */ + if (ret == 0) { + ret = slhdsa_check_hash_for_n(hashType, key->params->n); + } if (ret == 0) { #ifndef WOLF_CRYPTO_CB_FIND if (key->devId != INVALID_DEVID) diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index f279018ed04..4e1ad06bf6f 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -55610,6 +55610,142 @@ static wc_test_ret_t mldsa_param_test(int param, WC_RNG* rng) #endif return ret; } + +#if !defined(WOLFSSL_DILITHIUM_NO_SIGN) && \ + !defined(WOLFSSL_DILITHIUM_NO_VERIFY) +/* Negative test: HashML-DSA must reject pre-hash algorithms whose collision + * resistance is below the parameter set's claimed security strength. + * + * Per FIPS 204 sec. 5.4, Table 4 (Approved PH algorithms for HashML-DSA): + * ML-DSA-44 (128-bit security): SHA2-256, SHA2-384, SHA2-512, SHA2-512/256, + * SHA3-256, SHA3-384, SHA3-512, + * SHAKE-128, SHAKE-256 + * ML-DSA-65 (192-bit security): SHA2-384, SHA2-512, SHA3-384, SHA3-512, + * SHAKE-256 + * ML-DSA-87 (256-bit security): SHA2-512, SHA3-512, SHAKE-256 + * + * This test attempts sigGen and sigVer with disallowed (paramSet, hash) pairs + * and asserts both reject the call (non-zero return). Before the in-module + * hash-vs-paramSet check exists, wc_dilithium_sign_ctx_hash and + * wc_dilithium_verify_ctx_hash happily proceed with any compiled-in hash, + * so this test is expected to FAIL until the check is added. */ +static wc_test_ret_t mldsa_hash_paramset_rejection_test(WC_RNG* rng) +{ + wc_test_ret_t ret = 0; + int i; +#if defined(WOLFSSL_SMALL_STACK) && !defined(WOLFSSL_NO_MALLOC) + dilithium_key* key = NULL; + byte* sig = NULL; +#else + dilithium_key key[1]; + byte sig[DILITHIUM_MAX_SIG_SIZE]; +#endif + word32 sigLen; + int verified; + + /* Fixed-content digests; for a rejection test the bytes don't matter, + * only the (paramSet, hashAlg, hashLen) tuple. Sizes match each hash's + * digest length so the existing length sanity check inside + * wc_dilithium_*_ctx_hash() does NOT short-circuit before reaching the + * hash-vs-paramSet gate we are validating here. */ + static const byte hash32[32] = { /* SHA-256 digest size */ + 0xBA,0x78,0x16,0xBF,0x8F,0x01,0xCF,0xEA, + 0x41,0x41,0x40,0xDE,0x5D,0xAE,0x22,0x23, + 0xB0,0x03,0x61,0xA3,0x96,0x17,0x7A,0x9C, + 0xB4,0x10,0xFF,0x61,0xF2,0x00,0x15,0xAD + }; + static const byte hash48[48] = { /* SHA-384 digest size */ + 0xCB,0x00,0x75,0x3F,0x45,0xA3,0x5E,0x8B, + 0xB5,0xA0,0x3D,0x69,0x9A,0xC6,0x50,0x07, + 0x27,0x2C,0x32,0xAB,0x0E,0xDE,0xD1,0x63, + 0x1A,0x8B,0x60,0x5A,0x43,0xFF,0x5B,0xED, + 0x80,0x86,0x07,0x2B,0xA1,0xE7,0xCC,0x23, + 0x58,0xBA,0xEC,0xA1,0x34,0xC8,0x25,0xA7 + }; + + struct { + int level; + int hashAlg; + const byte* hash; + word32 hashLen; + } forbidden[] = { + /* ML-DSA-65 needs >=192-bit collision strength; SHA-256 = 128-bit. */ + { WC_ML_DSA_65, WC_HASH_TYPE_SHA256, hash32, 32 }, + /* ML-DSA-87 needs >=256-bit collision strength; SHA-384 = 192-bit. */ + { WC_ML_DSA_87, WC_HASH_TYPE_SHA384, hash48, 48 } + }; + +#if defined(WOLFSSL_SMALL_STACK) && !defined(WOLFSSL_NO_MALLOC) + key = (dilithium_key*)XMALLOC(sizeof(*key), HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER); + sig = (byte*)XMALLOC(DILITHIUM_MAX_SIG_SIZE, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER); + if ((key == NULL) || (sig == NULL)) { + ERROR_OUT(WC_TEST_RET_ENC_ERRNO, neg_out); + } +#endif + XMEMSET(sig, 0, DILITHIUM_MAX_SIG_SIZE); + + for (i = 0; i < (int)(sizeof(forbidden) / sizeof(forbidden[0])); i++) { + #ifdef WOLFSSL_NO_ML_DSA_65 + if (forbidden[i].level == WC_ML_DSA_65) continue; + #endif + #ifdef WOLFSSL_NO_ML_DSA_87 + if (forbidden[i].level == WC_ML_DSA_87) continue; + #endif + + ret = wc_dilithium_init_ex(key, NULL, devId); + if (ret != 0) { + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), neg_out); + } + ret = wc_dilithium_set_level(key, (byte)forbidden[i].level); + if (ret != 0) { + wc_dilithium_free(key); + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), neg_out); + } + ret = wc_dilithium_make_key(key, rng); + if (ret != 0) { + wc_dilithium_free(key); + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), neg_out); + } + + sigLen = (word32)wc_dilithium_sig_size(key); + + /* sigGen with disallowed PH must be REJECTED. */ + PRIVATE_KEY_UNLOCK(); + ret = wc_dilithium_sign_ctx_hash(NULL, 0, forbidden[i].hashAlg, + forbidden[i].hash, forbidden[i].hashLen, sig, &sigLen, key, rng); + PRIVATE_KEY_LOCK(); + if (ret == 0) { + /* Module did NOT reject -- this is the missing-enforcement bug. */ + wc_dilithium_free(key); + ERROR_OUT(WC_TEST_RET_ENC_NC, neg_out); + } + + /* sigVer with disallowed PH must ALSO be REJECTED. */ + verified = -1; + sigLen = (word32)wc_dilithium_sig_size(key); + ret = wc_dilithium_verify_ctx_hash(sig, sigLen, NULL, 0, + forbidden[i].hashAlg, forbidden[i].hash, forbidden[i].hashLen, + &verified, key); + if (ret == 0) { + wc_dilithium_free(key); + ERROR_OUT(WC_TEST_RET_ENC_NC, neg_out); + } + + wc_dilithium_free(key); + ret = 0; + } + +neg_out: +#if defined(WOLFSSL_SMALL_STACK) && !defined(WOLFSSL_NO_MALLOC) + if (sig != NULL) XFREE(sig, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (key != NULL) XFREE(key, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); +#endif + return ret; +} +#endif /* !WOLFSSL_DILITHIUM_NO_SIGN && !WOLFSSL_DILITHIUM_NO_VERIFY */ + #endif #if defined(WC_MLDSA_CACHE_MATRIX_A) && \ @@ -56033,6 +56169,18 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t mldsa_test(void) #endif /* (WOLFSSL_MLDSA_PUBLIC_KEY && !WOLFSSL_MLDSA_NO_VERIFY) || * (WOLFSSL_MLDSA_PRIVATE_KEY && !WOLFSSL_MLDSA_NO_SIGN) */ +#if !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ + !defined(WOLFSSL_MLDSA_NO_SIGN) && \ + !defined(WOLFSSL_MLDSA_NO_VERIFY) && \ + (!defined(WOLFSSL_NO_ML_DSA_65) || !defined(WOLFSSL_NO_ML_DSA_87)) + /* FIPS 204 sec. 5.4 -- HashML-DSA must reject pre-hashes weaker than + * the parameter set's security level. */ + ret = mldsa_hash_paramset_rejection_test(&rng); + if (ret != 0) { + ERROR_OUT(ret, out); + } +#endif + #if !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) || \ !defined(WOLFSSL_MLDSA_NO_VERIFY) || \ defined(WOLFSSL_MLDSA_PRIVATE_KEY) || \ @@ -57478,29 +57626,18 @@ static wc_test_ret_t slhdsa_test_param(enum SlhDsaParam param) ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); } - /* HashSLH-DSA takes the caller's pre-hashed digest as input. */ + /* HashSLH-DSA takes the caller's pre-hashed digest as input. SHAKE-256 + * is universally approved by FIPS 205 sec. 10.2.2 Table 9 across all + * SLH-DSA-{128,192,256} variants, so use it unconditionally for the + * positive round-trip path -- avoids tripping the in-module + * hash-vs-paramSet validation gate for higher-security paramSets. */ { -#ifdef WOLFSSL_SLHDSA_SHA2 - enum wc_HashType phType = SLHDSA_IS_SHA2(param) ? - WC_HASH_TYPE_SHA256 : WC_HASH_TYPE_SHAKE256; -#else enum wc_HashType phType = WC_HASH_TYPE_SHAKE256; -#endif byte digest[WC_SHA3_512_DIGEST_SIZE]; - word32 digestLen; + word32 digestLen = WC_SHA3_512_DIGEST_SIZE; -#ifdef WOLFSSL_SLHDSA_SHA2 - if (phType == WC_HASH_TYPE_SHA256) { - ret = wc_Sha256Hash(msg, (word32)sizeof(msg), digest); - digestLen = WC_SHA256_DIGEST_SIZE; - } - else -#endif - { - ret = wc_Shake256Hash(msg, (word32)sizeof(msg), digest, - WC_SHA3_512_DIGEST_SIZE); - digestLen = WC_SHA3_512_DIGEST_SIZE; - } + ret = wc_Shake256Hash(msg, (word32)sizeof(msg), digest, + WC_SHA3_512_DIGEST_SIZE); if (ret != 0) { ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); } @@ -57519,9 +57656,13 @@ static wc_test_ret_t slhdsa_test_param(enum SlhDsaParam param) ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); } - /* Additional pre-hash test: SHA-384 exercises a different OID path */ + /* Additional pre-hash test: SHA-384 exercises a different OID path. + * Skip for SLH-DSA-256 because SHA-384 (192-bit collision) is below the + * 256-bit security level required by FIPS 205 sec. 10.2.2 Table 9. */ #ifdef WOLFSSL_SHA384 - { + /* Skip SHA-384 for SLH-DSA-256: 192-bit collision strength below the + * 256-bit security level (FIPS 205 sec. 10.2.2 Table 9). */ + if (key->params->n != WC_SLHDSA_N_256) { byte digest384[WC_SHA384_DIGEST_SIZE]; ret = wc_Sha384Hash(msg, (word32)sizeof(msg), digest384); @@ -57581,6 +57722,98 @@ static wc_test_ret_t slhdsa_test_param(enum SlhDsaParam param) return ret; } + +/* Negative test: HashSLH-DSA must reject pre-hash algorithms whose collision + * resistance is below the parameter set's claimed security strength. + * + * Per FIPS 205 sec. 10.2.2, Table 9 (Approved PH for HashSLH-DSA): + * SLH-DSA-*-128* (128-bit): SHA2-256, SHA2-384, SHA2-512, SHA2-512/256, + * SHA3-256, SHA3-384, SHA3-512, + * SHAKE-128, SHAKE-256 + * SLH-DSA-*-192* (192-bit): SHA2-384, SHA2-512, SHA3-384, SHA3-512, + * SHAKE-256 + * SLH-DSA-*-256* (256-bit): SHA2-512, SHA3-512, SHAKE-256 + * + * This test attempts sigGen / sigVer with a disallowed (paramSet, hash) pair + * and asserts both reject the call. Before the in-module hash-vs-paramSet + * check exists, wc_SlhDsaKey_SignHash / wc_SlhDsaKey_VerifyHash happily + * proceed with any compiled-in hash, so this test is expected to FAIL until + * the check is added. */ +static wc_test_ret_t slhdsa_hash_paramset_rejection_test(enum SlhDsaParam param) +{ + int ret = 0; + WC_RNG rng; + SlhDsaKey key[1]; + byte sig[WC_SLHDSA_MAX_SIG_LEN]; + word32 sigLen; + static const byte msg[] = { + 0x48,0x65,0x6c,0x6c,0x6f,0x20,0x57,0x6f, + 0x72,0x6c,0x64,0x21 + }; + byte ctx[1]; + /* Hash that is BELOW the security level of every 192/256-bit paramSet + * tested below. SHA-256 (128-bit collision) is approved only for the + * 128-bit SLH-DSA paramSets, so any 192/256-bit paramSet must reject it. */ + enum wc_HashType badHash = WC_HASH_TYPE_SHA256; + + XMEMSET(&key, 0, sizeof(key)); + +#ifndef HAVE_FIPS + ret = wc_InitRng_ex(&rng, HEAP_HINT, devId); +#else + ret = wc_InitRng(&rng); +#endif + if (ret != 0) return WC_TEST_RET_ENC_EC(ret); + + ret = wc_SlhDsaKey_Init(key, param, NULL, INVALID_DEVID); + if (ret != 0) { + wc_FreeRng(&rng); + return WC_TEST_RET_ENC_EC(ret); + } + + ret = wc_SlhDsaKey_MakeKey(key, &rng); + if (ret != 0) { + wc_SlhDsaKey_Free(key); + wc_FreeRng(&rng); + return WC_TEST_RET_ENC_EC(ret); + } + + /* Only enforce on paramSets above 128-bit security; SHA-256 is approved + * for 128-bit so wouldn't be a rejection target there. */ + if (key->params->n == WC_SLHDSA_N_128) { + wc_SlhDsaKey_Free(key); + wc_FreeRng(&rng); + return 0; + } + + /* sigGen with too-weak PH must be REJECTED. */ + sigLen = WC_SLHDSA_MAX_SIG_LEN; + PRIVATE_KEY_UNLOCK(); + ret = wc_SlhDsaKey_SignHash(key, ctx, 0, msg, (word32)sizeof(msg), + badHash, sig, &sigLen, &rng); + PRIVATE_KEY_LOCK(); + if (ret == 0) { + /* Module did NOT reject -- this is the missing-enforcement bug. */ + wc_SlhDsaKey_Free(key); + wc_FreeRng(&rng); + return WC_TEST_RET_ENC_NC; + } + + /* sigVer with too-weak PH must ALSO be REJECTED. */ + sigLen = WC_SLHDSA_MAX_SIG_LEN; + XMEMSET(sig, 0, sigLen); + ret = wc_SlhDsaKey_VerifyHash(key, ctx, 0, msg, (word32)sizeof(msg), + badHash, sig, sigLen); + if (ret == 0) { + wc_SlhDsaKey_Free(key); + wc_FreeRng(&rng); + return WC_TEST_RET_ENC_NC; + } + + wc_SlhDsaKey_Free(key); + wc_FreeRng(&rng); + return 0; +} #endif /* True iff slhdsa_test() actually emits at least one `goto out;` / @@ -59574,6 +59807,41 @@ wc_test_ret_t slhdsa_test(void) } #endif + /* FIPS 205 sec. 10.2.2 -- HashSLH-DSA must reject pre-hashes whose + * collision strength is below the paramSet's security level. Use any + * available 192- or 256-bit paramSet to exercise the rejection. The + * 128-bit paramSets allow SHA-256, so they are not useful as targets + * here. */ +#ifdef WOLFSSL_SLHDSA_PARAM_192S + ret = slhdsa_hash_paramset_rejection_test(SLHDSA_SHAKE192S); + if (ret != 0) { + wc_test_render_error_message("SLHDSA_SHAKE192S (hash-paramset reject)", + 0); + goto out; + } +#elif defined(WOLFSSL_SLHDSA_PARAM_256S) + ret = slhdsa_hash_paramset_rejection_test(SLHDSA_SHAKE256S); + if (ret != 0) { + wc_test_render_error_message("SLHDSA_SHAKE256S (hash-paramset reject)", + 0); + goto out; + } +#elif defined(WOLFSSL_SLHDSA_PARAM_SHA2_192S) + ret = slhdsa_hash_paramset_rejection_test(SLHDSA_SHA2_192S); + if (ret != 0) { + wc_test_render_error_message("SLHDSA_SHA2_192S (hash-paramset reject)", + 0); + goto out; + } +#elif defined(WOLFSSL_SLHDSA_PARAM_SHA2_256S) + ret = slhdsa_hash_paramset_rejection_test(SLHDSA_SHA2_256S); + if (ret != 0) { + wc_test_render_error_message("SLHDSA_SHA2_256S (hash-paramset reject)", + 0); + goto out; + } +#endif + #endif /* !WOLFSSL_SLHDSA_VERIFY_ONLY */ #if defined(WOLF_PRIVATE_KEY_ID) && \ diff --git a/wolfssl/wolfcrypt/error-crypt.h b/wolfssl/wolfcrypt/error-crypt.h index 736954a0a76..0894e5bb074 100644 --- a/wolfssl/wolfcrypt/error-crypt.h +++ b/wolfssl/wolfcrypt/error-crypt.h @@ -327,9 +327,17 @@ enum wolfCrypt_ErrorCodes { ML_DSA_PCT_E = -1016, /* ML-DSA Pairwise Consistency Test failure */ DRBG_SHA512_KAT_FIPS_E = -1017, /* SHA-512 DRBG KAT failure */ SLH_DSA_KAT_FIPS_E = -1018, /* SLH-DSA CAST KAT failure */ - - WC_SPAN2_LAST_E = -1018, /* Update to indicate last used error code */ - WC_LAST_E = -1018, /* the last code used either here or in + SLH_DSA_PCT_E = -1019, /* SLH-DSA Pairwise Consistency Test failure */ + CMAC_KAT_FIPS_E = -1020, /* AES-CMAC KAT failure (vendor-elected) */ + SHAKE_KAT_FIPS_E = -1021, /* SHAKE KAT failure (vendor-elected) */ + DH_PCT_E = -1022, /* DH (FFC) Pairwise Consistency Test + * failure (SP 800-56A r3 sec 5.6.2.1.4, + * FIPS 140-3 IG 10.3.B) */ + AES_KW_KAT_FIPS_E = -1023, /* AES-KW KAT failure (vendor-elected, + * SP 800-38F sec 6.2 / RFC 3394) */ + + WC_SPAN2_LAST_E = -1023, /* Update to indicate last used error code */ + WC_LAST_E = -1023, /* the last code used either here or in * error-ssl.h */ WC_SPAN2_MIN_CODE_E = -1999, /* Last usable code in span 2 */ diff --git a/wolfssl/wolfcrypt/fips_test.h b/wolfssl/wolfcrypt/fips_test.h index de2b506df2c..41467b0ee2a 100644 --- a/wolfssl/wolfcrypt/fips_test.h +++ b/wolfssl/wolfcrypt/fips_test.h @@ -31,8 +31,23 @@ extern "C" { #endif -/* Added for FIPS v5.3 or later */ -#if defined(FIPS_VERSION_GE) && FIPS_VERSION_GE(5,3) +/* Added for FIPS v5.3 or later. + * + * v7.0.0 and later upgrade the in-core integrity HMAC to SHA-512 (with a + * 512-bit key) for NSA 2.0 compliance. Customers that must avoid SHA-256 + * anywhere in the validated module can therefore use the v7 module without + * residual SHA-256 integrity material. v5.3 and v6.x retain HMAC-SHA-256. + */ +#if defined(FIPS_VERSION_GE) && FIPS_VERSION_GE(7,0) + #ifdef WOLFSSL_SHA512 + #define FIPS_IN_CORE_DIGEST_SIZE 64 + #define FIPS_IN_CORE_HASH_TYPE WC_SHA512 + #define FIPS_IN_CORE_KEY_SZ 64 + #define FIPS_IN_CORE_VERIFY_SZ FIPS_IN_CORE_KEY_SZ + #else + #error FIPS v7+ integrity test requires WOLFSSL_SHA512 + #endif +#elif defined(FIPS_VERSION_GE) && FIPS_VERSION_GE(5,3) /* Determine FIPS in core hash type and size */ #ifndef NO_SHA256 #define FIPS_IN_CORE_DIGEST_SIZE 32 @@ -62,7 +77,11 @@ enum FipsCastId { FIPS_CAST_RSA_SIGN_PKCS1v15 = 7, FIPS_CAST_ECC_CDH = 8, FIPS_CAST_ECC_PRIMITIVE_Z = 9, - FIPS_CAST_DH_PRIMITIVE_Z = 10, + FIPS_CAST_DH_PRIMITIVE_Z = 10, /* RETIRED (v7+): classic DH dropped + * from the FIPS 140-3 v7 PQ module + * boundary. Preserved for ABI -- + * do not reuse this id, no longer + * triggered. */ FIPS_CAST_ECDSA = 11, FIPS_CAST_KDF_TLS12 = 12, FIPS_CAST_KDF_TLS13 = 13, @@ -80,7 +99,10 @@ enum FipsCastId { FIPS_CAST_XMSS = 23, FIPS_CAST_DRBG_SHA512 = 24, FIPS_CAST_SLH_DSA = 25, - FIPS_CAST_COUNT = 26 + FIPS_CAST_AES_CMAC = 26, + FIPS_CAST_SHAKE = 27, + FIPS_CAST_AES_KW = 28, + FIPS_CAST_COUNT = 29 }; enum FipsCastStateId { diff --git a/wolfssl/wolfcrypt/random.h b/wolfssl/wolfcrypt/random.h index 102f05d6b55..f52b1b663b8 100644 --- a/wolfssl/wolfcrypt/random.h +++ b/wolfssl/wolfcrypt/random.h @@ -57,8 +57,12 @@ #define DRBG_SEED_LEN (440/8) #endif +/* Size of the DRBG seed (SHA-512) */ #ifdef WOLFSSL_DRBG_SHA512 - #define DRBG_SHA512_SEED_LEN (888/8) /* 111 bytes per SP 800-90A Table 2 */ + #ifndef DRBG_SHA512_SEED_LEN + #define DRBG_SHA512_SEED_LEN (888/8) /* 111 bytes per SP 800-90A + * Table 2 */ + #endif #endif diff --git a/wolfssl/wolfcrypt/settings.h b/wolfssl/wolfcrypt/settings.h index 4c9579db34d..4fa135a1b11 100644 --- a/wolfssl/wolfcrypt/settings.h +++ b/wolfssl/wolfcrypt/settings.h @@ -557,6 +557,17 @@ #endif /* blinding adds API not available yet in FIPS mode */ #undef WC_RSA_BLINDING + + /* NIST SP 800-38A sec 6.2 specifies CBC operates on plaintext that is + * a multiple of the block size; the cipher does not implement padding + * (project_aes_no_padding_policy). Force the wc_AesCbcEncrypt / + * wc_AesCbcDecrypt block-alignment check on for FIPS builds so a + * length not a multiple of WC_AES_BLOCK_SIZE returns BAD_LENGTH_E + * rather than silently truncating to the largest aligned prefix in + * the underlying implementation. */ + #ifndef WOLFSSL_AES_CBC_LENGTH_CHECKS + #define WOLFSSL_AES_CBC_LENGTH_CHECKS + #endif #endif /* old FIPS has only AES_BLOCK_SIZE. */ @@ -3983,8 +3994,18 @@ #undef HAVE_PUBLIC_FFDHE #endif + /* LinuxKM lkcapi previously needed a 4-byte minimum AES-GCM + * authentication tag for certain kernel-side test vectors. Per + * NIST SP 800-38D sec 5.2.1.2 / sec 8.2 a minimum tag length of 96 bits + * (12 bytes) provides robust integrity for general-purpose use; FIPS + * 140-3 IG C.H reaffirms this 96-bit minimum for Approved-mode AES-GCM. + * Gate the 32-bit-tag relaxation on non-FIPS builds only so the + * v7.0.0 module's Approved configuration retains the full 96-bit + * minimum in all linuxkm and non-linuxkm scenarios. */ +#ifndef HAVE_FIPS #undef WOLFSSL_MIN_AUTH_TAG_SZ #define WOLFSSL_MIN_AUTH_TAG_SZ 4 +#endif #if defined(LINUXKM_LKCAPI_REGISTER) && !defined(WOLFSSL_ASN_INT_LEAD_0_ANY) /* kernel 5.10 crypto manager tests key(s) that fail unless leading