From 29c714f5c0e5f24dd6d288d5f8cb82863a48d3e6 Mon Sep 17 00:00:00 2001 From: kaleb-himes Date: Fri, 1 May 2026 10:50:29 -0600 Subject: [PATCH] Phase 3: Security and FIPS Compliance Audit --- fips-hash.sh | 6 +- tests/api/test_evp_pkey.c | 4 +- tests/api/test_mldsa.c | 21 +- tests/api/test_ossl_rsa.c | 10 +- wolfcrypt/benchmark/fips_cast_bench.c | 349 ++++++++++++++++++++++++++ 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_slhdsa.c | 39 +++ wolfssl/wolfcrypt/error-crypt.h | 14 +- wolfssl/wolfcrypt/fips_test.h | 24 +- wolfssl/wolfcrypt/random.h | 6 +- wolfssl/wolfcrypt/settings.h | 21 ++ 15 files changed, 567 insertions(+), 22 deletions(-) create mode 100644 wolfcrypt/benchmark/fips_cast_bench.c diff --git a/fips-hash.sh b/fips-hash.sh index 36f320c0bb..8f8a1a8631 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_evp_pkey.c b/tests/api/test_evp_pkey.c index d1b7b28cc9..5f43493cca 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 @@ -2028,7 +2028,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_mldsa.c b/tests/api/test_mldsa.c index 4e8cf78d24..36e415a0ce 100644 --- a/tests/api/test_mldsa.c +++ b/tests/api/test_mldsa.c @@ -752,9 +752,20 @@ int test_wc_dilithium_sign_pubonly_fails(void) /* Import only the public key into a fresh key object. */ ExpectIntEQ(wc_dilithium_import_public(pubBuf, pubLen, pubOnlyKey), 0); - /* Signing with a public-key-only object must fail. */ + /* Signing with a public-key-only object must fail. + * + * In FIPS v7.0.0 mode the ML-DSA sign wrappers enforce the + * privateKeyReadEnable contract (FIPS 140-3 sec 7.10.2 CSP access + * control); without unlocking, the wrapper short-circuits to + * FIPS_PRIVATE_KEY_LOCKED_E before reaching the no-private-key + * detection. Unlock briefly so this test exercises the underlying + * BAD_FUNC_ARG path it is designed to verify. The + * PRIVATE_KEY_UNLOCK / PRIVATE_KEY_LOCK macros expand to no-ops in + * non-FIPS builds. */ + PRIVATE_KEY_UNLOCK(); ExpectIntEQ(wc_dilithium_sign_ctx_msg(NULL, 0, msg, sizeof(msg), sig, &sigLen, pubOnlyKey, &rng), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + PRIVATE_KEY_LOCK(); DoExpectIntEQ(wc_FreeRng(&rng), 0); wc_dilithium_free(pubOnlyKey); @@ -1236,6 +1247,12 @@ int test_wc_dilithium_sign_vfy(void) ExpectIntEQ(wc_InitRng(&rng), 0); + /* FIPS v7.0.0 ML-DSA sign wrappers enforce the privateKeyReadEnable + * contract (FIPS 140-3 sec 7.10.2 CSP access control); unlock for the + * duration of this test's signing operations and re-lock at the end. + * Macros expand to no-ops in non-FIPS builds. */ + PRIVATE_KEY_UNLOCK(); + #ifndef WOLFSSL_NO_ML_DSA_44 ExpectIntEQ(wc_dilithium_init(key), 0); ExpectIntEQ(wc_dilithium_set_level(key, WC_ML_DSA_44), 0); @@ -1300,6 +1317,8 @@ int test_wc_dilithium_sign_vfy(void) wc_dilithium_free(key); #endif + PRIVATE_KEY_LOCK(); + wc_FreeRng(&rng); XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); XFREE(key, NULL, DYNAMIC_TYPE_TMP_BUFFER); diff --git a/tests/api/test_ossl_rsa.c b/tests/api/test_ossl_rsa.c index 26cf360105..62cee2702a 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/wolfcrypt/benchmark/fips_cast_bench.c b/wolfcrypt/benchmark/fips_cast_bench.c new file mode 100644 index 0000000000..66d181934d --- /dev/null +++ b/wolfcrypt/benchmark/fips_cast_bench.c @@ -0,0 +1,349 @@ +/* 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 */ + +#ifdef HAVE_FIPS + +#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 */ + +#include + +int main(void) +{ + fprintf(stderr, + "fips_cast_bench: built without HAVE_FIPS - nothing to measure\n"); + return 0; +} + +#endif /* HAVE_FIPS */ diff --git a/wolfcrypt/benchmark/include.am b/wolfcrypt/benchmark/include.am index 22cecbdaef..130343a14e 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 c2c982203f..eea0ebf33b 100644 --- a/wolfcrypt/src/aes.c +++ b/wolfcrypt/src/aes.c @@ -10968,6 +10968,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 @@ -12671,6 +12681,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 5e2863c343..422ef70c13 100644 --- a/wolfcrypt/src/dh.c +++ b/wolfcrypt/src/dh.c @@ -1400,8 +1400,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 */ RESTORE_VECTOR_REGISTERS(); @@ -1428,8 +1440,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 */ diff --git a/wolfcrypt/src/error.c b/wolfcrypt/src/error.c index 8826d9b198..f8bb774bed 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 b3fbb83fe0..891babe934 100644 --- a/wolfcrypt/src/rsa.c +++ b/wolfcrypt/src/rsa.c @@ -5018,9 +5018,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: @@ -5283,6 +5289,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_slhdsa.c b/wolfcrypt/src/wc_slhdsa.c index 3909bbb653..1ed80461ea 100644 --- a/wolfcrypt/src/wc_slhdsa.c +++ b/wolfcrypt/src/wc_slhdsa.c @@ -6702,6 +6702,45 @@ 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. */ + if (ret == 0) { + static const byte pct_msg[] = "wolfSSL SLH-DSA PCT"; + byte* pct_sig = (byte*)XMALLOC(WC_SLHDSA_MAX_SIG_LEN, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + word32 pct_sigSz = WC_SLHDSA_MAX_SIG_LEN; + + 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, WC_SLHDSA_MAX_SIG_LEN); + 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; } diff --git a/wolfssl/wolfcrypt/error-crypt.h b/wolfssl/wolfcrypt/error-crypt.h index 8d49ffc18e..7ffdc1db6a 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 de2b506df2..b258017f14 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 @@ -80,7 +95,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 102f05d6b5..f52b1b663b 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 e02620e7a4..c7d590ad37 100644 --- a/wolfssl/wolfcrypt/settings.h +++ b/wolfssl/wolfcrypt/settings.h @@ -508,6 +508,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. */ @@ -3903,8 +3914,18 @@ extern void uITRON4_free(void *p) ; #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