diff --git a/.github/workflows/fips-ready.yml b/.github/workflows/fips-ready.yml index 8e8560c1..228baa92 100644 --- a/.github/workflows/fips-ready.yml +++ b/.github/workflows/fips-ready.yml @@ -68,7 +68,10 @@ jobs: - name: Build wolfProvider with FIPS Ready Bundle run: | - ./scripts/build-wolfprovider.sh --fips-bundle="$FIPS_BUNDLE_PATH" \ + # WOLFPROV_FORCE_FIPS_FAILURE is an internal, test-only knob (no CLI + # flag) that enables HAVE_FORCE_FIPS_FAILURE for the fips_status test. + WOLFPROV_FORCE_FIPS_FAILURE=1 ./scripts/build-wolfprovider.sh \ + --fips-bundle="$FIPS_BUNDLE_PATH" \ --fips-check=ready --wolfssl-ver=v${{matrix.wolfssl_bundle_ref}}-stable - name: Run FIPS Command Tests @@ -82,3 +85,14 @@ jobs: # --- force-fail mode --- WOLFPROV_FORCE_FAIL=1 ./scripts/cmd_test/do-cmd-tests.sh + + - name: Run FIPS Status Test + run: | + # Exercises the FIPS-status gate in wolfssl_prov_is_running(): + export WOLFSSL_ISFIPS=1 + source scripts/env-setup + set -o pipefail + ./test/standalone/tests/fips_status/run.sh 2>&1 | tee fips-status.log + # The forced-failure branch must have actually run in this FIPS build; + # fail if the test silently degraded to the healthy path only. + grep -q "correctly rejected after FIPS failure" fips-status.log diff --git a/scripts/utils-wolfssl.sh b/scripts/utils-wolfssl.sh index 823227a7..ea174336 100644 --- a/scripts/utils-wolfssl.sh +++ b/scripts/utils-wolfssl.sh @@ -215,6 +215,14 @@ install_wolfssl() { printf "with FIPS ${fips_tag} ... " fi CONF_ARGS+=" --enable-fips=$fips_configure_arg" + + # Internal test-only knob: expose wolfCrypt_SetStatus_fips for the + # fips_status test. The define reaches the provider and test builds + # automatically via the generated wolfssl options.h. + if [ "${WOLFPROV_FORCE_FIPS_FAILURE:-}" = "1" ]; then + WOLFSSL_FIPS_CONFIG_CFLAGS="${WOLFSSL_FIPS_CONFIG_CFLAGS} -DHAVE_FORCE_FIPS_FAILURE" + fi + WOLFSSL_CONFIG_OPTS=$WOLFSSL_FIPS_CONFIG_OPTS WOLFSSL_CONFIG_CFLAGS=$WOLFSSL_FIPS_CONFIG_CFLAGS # Only run fips-check if not using a bundle diff --git a/src/wp_wolfprov.c b/src/wp_wolfprov.c index d92618ef..5246e449 100644 --- a/src/wp_wolfprov.c +++ b/src/wp_wolfprov.c @@ -36,6 +36,10 @@ #include "wolfssl/wolfcrypt/logging.h" +#ifdef HAVE_FIPS +#include +#endif + const char* wolfprovider_id = "libwolfprov"; /* Core function that gets the table of parameters. */ @@ -72,10 +76,11 @@ static const OSSL_PARAM* wolfprov_gettable_params(void* provCtx) /* * Returns whether the provider is running/usable. * - * In FIPS, if there is an issue with the integrity check, then this can return - * 0 to indicate provider is unusable. + * In FIPS builds the live wolfCrypt FIPS status is queried; a non-zero status + * (integrity/POST or continuous-test failure) returns 0 so a status-polling + * caller can observe the failure. * - * @return 1 indicating provider is running. + * @return 1 indicating provider is running, 0 otherwise. */ int wolfssl_prov_is_running(void) { @@ -87,7 +92,12 @@ int wolfssl_prov_is_running(void) return 0; } #endif - /* Always running. */ +#ifdef HAVE_FIPS + if (wolfCrypt_GetStatus_fips() != 0) { + WOLFPROV_LEAVE(WP_LOG_COMP_PROVIDER, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } +#endif WOLFPROV_LEAVE(WP_LOG_COMP_PROVIDER, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; } @@ -1348,8 +1358,6 @@ static const OSSL_DISPATCH wolfprov_dispatch_table[] = { }; #ifdef HAVE_FIPS - #include - static void wp_fipsCb(int ok, int err, const char* hash) { (void)ok; diff --git a/test/standalone/include.am b/test/standalone/include.am index 6408e26d..a8b9349e 100644 --- a/test/standalone/include.am +++ b/test/standalone/include.am @@ -17,8 +17,8 @@ noinst_HEADERS += test/standalone/test_common.h \ # Standalone test programs # Each test compiles to its own binary for isolated execution # Note: These are NOT in check_PROGRAMS because they must be run through scripts, not directly -noinst_PROGRAMS += test/sha256_simple.test test/hardload.test test/fips_baseline.test test/pqc_interop.test -DISTCLEANFILES += test/.libs/sha256_simple.test test/.libs/hardload.test test/.libs/fips_baseline.test test/.libs/pqc_interop.test +noinst_PROGRAMS += test/sha256_simple.test test/hardload.test test/fips_baseline.test test/pqc_interop.test test/fips_status.test +DISTCLEANFILES += test/.libs/sha256_simple.test test/.libs/hardload.test test/.libs/fips_baseline.test test/.libs/pqc_interop.test test/.libs/fips_status.test # Common flags for all standalone tests STANDALONE_COMMON_CPPFLAGS = -DCERTS_DIR='"$(abs_top_srcdir)/certs"' \ @@ -60,6 +60,13 @@ test_pqc_interop_test_CPPFLAGS = $(STANDALONE_COMMON_CPPFLAGS) test_pqc_interop_test_SOURCES = test/standalone/tests/pqc_interop/test_pqc_interop.c test_pqc_interop_test_LDADD = $(STANDALONE_COMMON_LDADD) +test_fips_status_test_CPPFLAGS = $(STANDALONE_COMMON_CPPFLAGS) +test_fips_status_test_SOURCES = test/standalone/tests/fips_status/test_fips_status.c +test_fips_status_test_LDADD = $(STANDALONE_COMMON_LDADD) +# Export the executable's dynamic symbols so libwolfprov resolves +# wolfCrypt_GetStatus_fips to this binary on the interposition path. +test_fips_status_test_LDFLAGS = -export-dynamic + # Common test utilities are built automatically by automake # Standalone tests are available for manual execution but not part of make check diff --git a/test/standalone/runners/run_standalone_tests.sh b/test/standalone/runners/run_standalone_tests.sh index 9fcf6c01..a7a5b4e8 100755 --- a/test/standalone/runners/run_standalone_tests.sh +++ b/test/standalone/runners/run_standalone_tests.sh @@ -38,6 +38,19 @@ else fi set -e +# Run FIPS status test +echo "" +echo "Running FIPS status test..." +set +e +"$ROOT_DIR/test/standalone/tests/fips_status/run.sh" +if [ $? -eq 0 ]; then + echo "FIPS status test: PASSED" +else + echo "FIPS status test: FAILED" + TOTAL_FAILURES=$((TOTAL_FAILURES + 1)) +fi +set -e + echo "" echo "================================" if [ $TOTAL_FAILURES -eq 0 ]; then diff --git a/test/standalone/tests/fips_status/run.sh b/test/standalone/tests/fips_status/run.sh new file mode 100755 index 00000000..0b46f43b --- /dev/null +++ b/test/standalone/tests/fips_status/run.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# FIPS provider status test runner + +set -e + +# Get the directory of this script and find the root +TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$TEST_DIR/../../../.." && pwd)" + +# Binary should be in the test/.libs/ directory +BINARY="fips_status.test" +BINARY_PATH="$ROOT_DIR/test/.libs/$BINARY" + +# Make sure we can find the binary +if [ ! -f "$BINARY_PATH" ]; then + echo "Error: Cannot find binary $BINARY_PATH" + echo "Make sure you've built the test with: make test/fips_status.test" + exit 1 +fi + +# Source env-setup +if ! source "$ROOT_DIR/scripts/env-setup" >/dev/null; then + echo "Error: env-setup failed" + exit 1 +fi + +# Source common test utilities +source "$ROOT_DIR/test/standalone/test_common.sh" + +# Check if this is a replace-default build +WP_USING_REPLACE_DEFAULT="0" +if detect_replace_default_build; then + WP_USING_REPLACE_DEFAULT="1" + unset OPENSSL_CONF +fi + +echo "Using environment:" +echo "LD_LIBRARY_PATH: $LD_LIBRARY_PATH" +echo "OPENSSL_CONF: $OPENSSL_CONF" +echo "OPENSSL_BIN: $OPENSSL_BIN" + +echo "Running FIPS status test: $BINARY_PATH" +echo "" + +# The healthy-path assertion runs on every build. The forced-failure path runs +# in FIPS builds via wolfCrypt_SetStatus_fips (HAVE_FORCE_FIPS_FAILURE) or, on +# Linux, symbol interposition; the binary self-reports which path it took. +if "$BINARY_PATH"; then + echo "FIPS status test: PASSED" + exit 0 +else + echo "FIPS status test: FAILED" + exit 1 +fi diff --git a/test/standalone/tests/fips_status/test_fips_status.c b/test/standalone/tests/fips_status/test_fips_status.c new file mode 100644 index 00000000..64360a86 --- /dev/null +++ b/test/standalone/tests/fips_status/test_fips_status.c @@ -0,0 +1,245 @@ +/* test_fips_status.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfProvider. + * + * wolfProvider 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. + * + * wolfProvider 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 wolfProvider. If not, see . + */ + +/* Exercises the FIPS-status gate in wolfssl_prov_is_running(): a healthy + * module must report OSSL_PROV_PARAM_STATUS == 1 and perform a crypto op; + * after a forced FIPS failure it must report 0 and reject the same op. + * + * Failure-injection mechanism is chosen at compile time from what the build + * provides: wolfCrypt_SetStatus_fips() when the FIPS module was built with + * HAVE_FORCE_FIPS_FAILURE (the define propagates via wolfssl options.h), + * else ELF symbol interposition of wolfCrypt_GetStatus_fips() on Linux, + * else the healthy path only. */ + +#include +#include +#include + +#ifdef WOLFPROV_USER_SETTINGS +#include +#endif +#include +#include + +#if defined(HAVE_FIPS) && defined(HAVE_FORCE_FIPS_FAILURE) + #include + #include + #define WP_CAN_FORCE_FIPS_FAILURE +#elif defined(HAVE_FIPS) && defined(__linux__) + #include + #include + #include + #define WP_CAN_FORCE_FIPS_FAILURE + #define WP_USE_INTERPOSITION +#endif + +#include +#include +#include +#include +#include + +#include +#include + +#include "../../test_common.h" + +#ifdef WP_USE_INTERPOSITION +static int forcedStatus = 0; + +/* Interposes libwolfssl's wolfCrypt_GetStatus_fips: ELF executable-first + * symbol resolution makes libwolfprov's call land here, letting the test + * drive the status the provider sees without a special wolfSSL build. */ +int wolfCrypt_GetStatus_fips(void) +{ + return forcedStatus; +} +#endif + +/* Read OSSL_PROV_PARAM_STATUS from the provider. Returns the status integer, + * or -1 if the parameter could not be read. */ +static int wp_get_prov_status(OSSL_PROVIDER *prov) +{ + int status = -1; + OSSL_PARAM params[] = { + { OSSL_PROV_PARAM_STATUS, OSSL_PARAM_INTEGER, &status, sizeof(status), 0 }, + { NULL, 0, NULL, 0, 0 } + }; + + if (OSSL_PROVIDER_get_params(prov, params) != 1) { + TEST_ERROR("Failed to get provider status parameter"); + ERR_print_errors_fp(stderr); + return -1; + } + return status; +} + +/* Run the gated AES-256-CBC encrypt init used to probe provider operability. + * Returns 1 if the init succeeded, 0 if the provider rejected it, or -1 on a + * setup error (fetch/alloc failure) unrelated to the provider running state. */ +static int wp_aes_encrypt_init(OSSL_LIB_CTX *libctx) +{ + EVP_CIPHER *cipher = NULL; + EVP_CIPHER_CTX *cctx = NULL; + unsigned char key[32]; + unsigned char iv[16]; + int result = -1; + + memset(key, 0, sizeof(key)); + memset(iv, 0, sizeof(iv)); + + cipher = EVP_CIPHER_fetch(libctx, "AES-256-CBC", NULL); + cctx = EVP_CIPHER_CTX_new(); + if (cipher != NULL && cctx != NULL) { + result = (EVP_EncryptInit_ex(cctx, cipher, NULL, key, iv) == 1) ? 1 : 0; + } + + if (cctx != NULL) { + EVP_CIPHER_CTX_free(cctx); + } + if (cipher != NULL) { + EVP_CIPHER_free(cipher); + } + return result; +} + +#ifdef WP_CAN_FORCE_FIPS_FAILURE +/* Force the FIPS status the provider observes to a failure code. + * Returns 0 on success, non-zero if the mechanism is unavailable. */ +static int wp_force_fips_failure(void) +{ +#ifdef WP_USE_INTERPOSITION + int (*resolved)(void); + + /* Canary: default lookup must resolve to our definition, else the + * provider is not seeing the interposed status - fail loudly rather + * than misreport an ineffective injection as a provider regression. */ + resolved = (int (*)(void))dlsym(RTLD_DEFAULT, "wolfCrypt_GetStatus_fips"); + if (resolved != &wolfCrypt_GetStatus_fips) { + TEST_ERROR("Symbol interposition not in effect"); + return 1; + } + TEST_INFO("Forcing FIPS failure via interposed wolfCrypt_GetStatus_fips()"); + forcedStatus = IN_CORE_FIPS_E; + return 0; +#else + TEST_INFO("Forcing FIPS failure via wolfCrypt_SetStatus_fips()"); + if (wolfCrypt_SetStatus_fips(IN_CORE_FIPS_E) != 0) { + TEST_ERROR("wolfCrypt_SetStatus_fips failed"); + return 1; + } + return 0; +#endif +} +#endif /* WP_CAN_FORCE_FIPS_FAILURE */ + +int main(int argc, char *argv[]) +{ + OSSL_LIB_CTX *libctx = NULL; + OSSL_PROVIDER *prov = NULL; + int status; + int op; + int ret = TEST_FAILURE; + + (void)argc; + (void)argv; + + TEST_INFO("Starting FIPS provider status test"); + + OPENSSL_init_ssl(OPENSSL_INIT_LOAD_CONFIG, NULL); + + libctx = OSSL_LIB_CTX_new(); + if (libctx == NULL) { + TEST_ERROR("Failed to create OpenSSL library context"); + goto cleanup; + } + + prov = OSSL_PROVIDER_load(libctx, "libwolfprov"); + if (prov == NULL) { + TEST_ERROR("Failed to load libwolfprov provider"); + ERR_print_errors_fp(stderr); + goto cleanup; + } + + /* Healthy module must report status 1 and perform a real crypto op. */ + status = wp_get_prov_status(prov); + TEST_INFO("Provider status (healthy): %d", status); + if (status != 1) { + TEST_ERROR("Expected status 1 for healthy provider, got %d", status); + goto cleanup; + } + + op = wp_aes_encrypt_init(libctx); + if (op < 0) { + TEST_ERROR("Setup error running healthy crypto operation"); + goto cleanup; + } + if (op != 1) { + TEST_ERROR("Healthy crypto operation was unexpectedly rejected"); + goto cleanup; + } + TEST_INFO("Crypto operation succeeded while healthy"); + +#ifdef WP_CAN_FORCE_FIPS_FAILURE + /* Force a FIPS failure and confirm the provider now reports status 0 + * and rejects the same crypto operation that just succeeded. */ + if (wp_force_fips_failure() != 0) { + goto cleanup; + } + + status = wp_get_prov_status(prov); + TEST_INFO("Provider status (after forced failure): %d", status); + if (status != 0) { + TEST_ERROR("Expected status 0 after FIPS failure, got %d", status); + goto cleanup; + } + + op = wp_aes_encrypt_init(libctx); + if (op < 0) { + TEST_ERROR("Setup error running post-failure crypto operation"); + goto cleanup; + } + if (op != 0) { + TEST_ERROR("Crypto operation not rejected after FIPS failure"); + goto cleanup; + } + TEST_INFO("Crypto operation correctly rejected after FIPS failure"); +#else + TEST_INFO("FIPS failure injection not available - only healthy path checked"); +#endif + + ret = TEST_SUCCESS; + +cleanup: + if (prov != NULL) { + OSSL_PROVIDER_unload(prov); + } + if (libctx != NULL) { + OSSL_LIB_CTX_free(libctx); + } + + if (ret == TEST_SUCCESS) { + TEST_INFO("Test PASSED"); + } + else { + TEST_ERROR("Test FAILED"); + } + return ret; +}