diff --git a/.github/workflows/fips-ready.yml b/.github/workflows/fips-ready.yml index 47688e21..699fd4fa 100644 --- a/.github/workflows/fips-ready.yml +++ b/.github/workflows/fips-ready.yml @@ -57,6 +57,10 @@ jobs: variant: fips-ready openssl_ref: ${{ matrix.openssl_ref }} wolfssl_fixed_key: wolfssl-fips-${{ matrix.wolfssl_bundle_ref }}-${{ matrix.openssl_ref }} + # The build below sets WOLFPROV_FIPS_FORCE_FAIL, which adds + # -DHAVE_FORCE_FIPS_FAILURE to the wolfSSL build; key the cached + # install on it so a wolfSSL built without the define is never reused. + extra_key: forcefail github_token: ${{ secrets.GITHUB_TOKEN }} - name: Download FIPS Ready Bundle @@ -80,7 +84,10 @@ jobs: - name: Build wolfProvider with FIPS Ready Bundle run: | + # WOLFPROV_FIPS_FORCE_FAIL is an internal, test-only knob (no CLI + # flag) that enables HAVE_FORCE_FIPS_FAILURE for the fips_status test. OPENSSL_TAG=${{ matrix.openssl_ref }} \ + WOLFPROV_FIPS_FORCE_FAIL=1 \ ./scripts/build-wolfprovider.sh --fips-bundle="$FIPS_BUNDLE_PATH" \ --fips-check=ready --wolfssl-ver=v${{matrix.wolfssl_bundle_ref}}-stable @@ -104,3 +111,12 @@ 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(). In a + # FIPS build the test fails unless the forced-failure branch actually + # ran, so the exit status is the whole check. + export WOLFSSL_ISFIPS=1 + source scripts/env-setup + ./test/standalone/tests/fips_status/run.sh diff --git a/scripts/utils-wolfssl.sh b/scripts/utils-wolfssl.sh index b6996cb9..48b5c204 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_FIPS_FORCE_FAIL:-}" = "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..3c244648 --- /dev/null +++ b/test/standalone/tests/fips_status/run.sh @@ -0,0 +1,47 @@ +#!/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 binary self-reports which injection path it took. Exit status is the +# result; the caller reports PASSED/FAILED. +exec "$BINARY_PATH" 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..72946195 --- /dev/null +++ b/test/standalone/tests/fips_status/test_fips_status.c @@ -0,0 +1,273 @@ +/* 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. + * Interposition assumes a shared libwolfssl. + * + * A FIPS build with neither mechanism (non-Linux, no HAVE_FORCE_FIPS_FAILURE) + * skips the failure phase. A non-FIPS build has no gate, so it checks the + * healthy path only - unless WOLFSSL_ISFIPS=1 says FIPS was intended, which + * means the build degraded and the gate would go untested. */ + +#if defined(__linux__) && !defined(_GNU_SOURCE) +/* Exposes RTLD_DEFAULT from glibc ; must precede any system header. */ +#define _GNU_SOURCE +#endif + +#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; + +/* ELF executable-first resolution lands libwolfprov's call here, so the test + * drives the status the provider sees. This exercises the provider gate, not + * the real module POST - only the SetStatus path does that. */ +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; +} + +/* Key and IV sizes of the AES-256-CBC probe below. */ +#define WP_AES256_KEY_SZ 32 +#define WP_AES_BLOCK_SZ 16 + +/* 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[WP_AES256_KEY_SZ]; + unsigned char iv[WP_AES_BLOCK_SZ]; + 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); + + /* Our definition must be first in global scope, else the provider cannot + * be seeing it. Passing does not prove libwolfprov binds here; the status + * assertion after injection is what proves that. */ + 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; +#ifndef HAVE_FIPS + const char* fipsEnv = NULL; +#endif + 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"); +#elif defined(HAVE_FIPS) + /* Non-Linux FIPS build without HAVE_FORCE_FIPS_FAILURE: no way to inject a + * failure here. A platform limit, not a regression, so skip. */ + TEST_INFO("SKIP: no FIPS failure-injection mechanism on this platform"); +#else + /* A build meant to be FIPS that lost HAVE_FIPS would pass the healthy path + * while testing no gate at all. WOLFSSL_ISFIPS says FIPS was intended. */ + fipsEnv = getenv("WOLFSSL_ISFIPS"); + if (fipsEnv != NULL && strcmp(fipsEnv, "1") == 0) { + TEST_ERROR("WOLFSSL_ISFIPS=1 but wolfSSL was built without FIPS"); + goto cleanup; + } + TEST_INFO("Not a FIPS build - only the healthy path applies"); +#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; +}