From 2319a8a5b117834a7e6793b741183ad12b816af5 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 6 Aug 2026 15:15:52 -0700 Subject: [PATCH 01/11] Resolve the wolfSSL FIPS Ready bundle at CI run time --- .github/workflows/README.md | 7 +- .github/workflows/_discover-versions.yml | 43 +++++ .github/workflows/fips-ready.yml | 23 +-- .github/workflows/nightly-fips-ready.yml | 91 ++++++++++ .github/workflows/nightly-osp.yml | 5 +- .github/workflows/perf-regression.yml | 16 +- docs/FIPS_INTEGRATION_GUIDE.md | 26 +++ include/wolfprovider/settings.h | 9 +- scripts/build-wolfprovider.sh | 3 +- scripts/fetch-fips-ready.sh | 209 +++++++++++++++++++++++ src/wp_digests.c | 12 +- src/wp_internal.c | 12 +- src/wp_wolfprov.c | 10 +- test/test_digest.c | 18 ++ test/unit.c | 6 + test/unit.h | 2 + 16 files changed, 435 insertions(+), 57 deletions(-) create mode 100644 .github/workflows/nightly-fips-ready.yml create mode 100755 scripts/fetch-fips-ready.sh diff --git a/.github/workflows/README.md b/.github/workflows/README.md index b199923d..41057fbf 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -30,7 +30,7 @@ ready_for_review) and on every push to `master`, `main`, or | `simple.yml` | Builds wolfProvider against the matrix of supported wolfSSL + OpenSSL refs and runs `make check`. The baseline "did anything obvious break" check. | | `smoke-test.yml` | Minimal end-to-end: build, load the provider into stock OpenSSL, run `openssl list -providers` and a handful of `openssl` subcommands. Catches link-time and provider-registration regressions. | | `cmdline.yml` | Runs `scripts/cmd_test/do-cmd-tests.sh` — exercises every `openssl` CLI verb (genrsa, pkeyutl, enc, dgst, …) through wolfProvider. | -| `fips-ready.yml` | Same as `simple` but builds wolfSSL with `--enable-fips=ready`. Sanity check that FIPS-ready compiles and basic tests pass without the full FIPS bundle. | +| `fips-ready.yml` | Downloads FIPS Ready bundles from wolfssl.com (via `scripts/fetch-fips-ready.sh`) and builds wolfSSL with `--enable-fips=ready` against each. The matrix is resolved at run time from `_discover-versions.yml`'s `fips_ready_pr_array` — the back-compat anchors plus whatever bundle is newest — so a new FIPS Ready release is covered the day it ships. Note `ready` is not a fixed FIPS version: it was v7 through bundle 5.9.1 and is v8 from 5.9.2 on. | | `seed-src.yml` | Builds with `--enable-seed-src` (entropy seed source variant) and `-DWP_TEST_SECCOMP_SANDBOX`, then runs the unit tests including the OpenSSH fork+seccomp-sandbox regression suite. | | `multi-compiler.yml` | Cross-compiler sweep: gcc-9 through gcc-14 and clang-12 through latest. Catches toolchain-specific warnings / UB. | | `codespell.yml` | Spell-check on tracked source. `*.patch` is excluded because OSP patches mirror upstream source whose original spelling we shouldn't silently rewrite. | @@ -59,6 +59,11 @@ push and dominate the merge queue. Nightly is the right cadence for catching regressions in third-party integration that wouldn't show up in our unit tests. +`nightly-fips-ready.yml` rides along out-of-wave. It runs the same steps +as `fips-ready.yml` but over `fips_ready_all_array` — every FIPS Ready +bundle hosted on wolfssl.com at or above the 5.8.2 floor — restoring the +versions the PR set leaves out. + ### Running a nightly job on a PR (label toggles) `pr-osp-select.yml` lets you pull any nightly job into a PR on demand — diff --git a/.github/workflows/_discover-versions.yml b/.github/workflows/_discover-versions.yml index 43845662..ab2a884a 100644 --- a/.github/workflows/_discover-versions.yml +++ b/.github/workflows/_discover-versions.yml @@ -30,6 +30,15 @@ on: openssl_all_releases_array: description: 'JSON array of every upstream openssl-3.X.Y release tag, sorted ascending. Used by openssl-version.yml so the sweep tracks upstream automatically.' value: ${{ jobs.discover.outputs.openssl_all_releases_array }} + fips_ready_latest: + description: 'Plain string, newest hosted FIPS Ready bundle version (e.g. 5.9.2)' + value: ${{ jobs.discover.outputs.fips_ready_latest }} + fips_ready_pr_array: + description: 'JSON array of FIPS Ready bundle versions for PR runs: the back-compat anchors plus the newest' + value: ${{ jobs.discover.outputs.fips_ready_pr_array }} + fips_ready_all_array: + description: 'JSON array of every hosted FIPS Ready bundle version at or above the floor. Used by the nightly sweep.' + value: ${{ jobs.discover.outputs.fips_ready_all_array }} jobs: discover: @@ -49,7 +58,15 @@ jobs: openssl_latest_ref: ${{ steps.resolve.outputs.openssl_latest_ref }} openssl_latest_ref_array: ${{ steps.resolve.outputs.openssl_latest_ref_array }} openssl_all_releases_array: ${{ steps.resolve.outputs.openssl_all_releases_array }} + fips_ready_latest: ${{ steps.resolve.outputs.fips_ready_latest }} + fips_ready_pr_array: ${{ steps.resolve.outputs.fips_ready_pr_array }} + fips_ready_all_array: ${{ steps.resolve.outputs.fips_ready_all_array }} steps: + - name: Checkout wolfProvider + uses: actions/checkout@v4 + with: + fetch-depth: 1 + - name: Install ORAS (best-effort) run: | set -uo pipefail @@ -169,11 +186,34 @@ jobs: OSSL_ALL_JSON=$(printf '%s\n' "$OSSL_ALL" | jq -R . | jq -s -c .) OSSL_LATEST=$(echo "$OSSL_ALL" | tail -n 1) + # FIPS Ready: every bundle hosted on wolfssl.com at or above the floor. + # Floor 5.8.2 -- the oldest bundle wolfProvider is tested against. + FR_FLOOR="5.8.2" + FR_ALL=$(./scripts/fetch-fips-ready.sh --list --floor "$FR_FLOOR") || true + if [ -z "${FR_ALL:-}" ]; then + echo "::warning::Could not list FIPS Ready bundles; falling back to $FR_FLOOR" + FR_ALL="$FR_FLOOR" + fi + FR_ALL_JSON=$(printf '%s\n' "$FR_ALL" | jq -R . | jq -s -c .) + FR_LATEST=$(echo "$FR_ALL" | tail -n 1) + # 5.8.4 and 5.9.1 are the back-compat anchors: the last two bundles + # where --enable-fips=ready still meant FIPS v7, which 5.9.2 moved to v8. + # Intersected with the hosted set so a delisted anchor drops out. + FR_PR_JSON=$(printf '%s\n' "5.8.4" "5.9.1" "$FR_LATEST" | jq -R . \ + | jq -s -c --argjson all "$FR_ALL_JSON" \ + '[.[] | select(. as $v | $all | index($v))] | unique_by(split(".")|map(tonumber))') + if [ "$FR_PR_JSON" = "[]" ]; then + FR_PR_JSON="[\"$FR_LATEST\"]" + fi + echo "wolfSSL .deb ref (actual ghcr deb): $WOLFSSL_DEB_REF" echo "wolfSSL upstream latest -stable: $WOLFSSL_LATEST" echo "OpenSSL Bookworm stock: openssl-$OSSL (raw: $OSSL_RAW)" echo "OpenSSL upstream latest: $OSSL_LATEST" echo "OpenSSL upstream releases tracked: $(echo "$OSSL_ALL" | wc -l) tags" + echo "FIPS Ready latest bundle: $FR_LATEST" + echo "FIPS Ready PR set: $FR_PR_JSON" + echo "FIPS Ready full set (floor $FR_FLOOR): $FR_ALL_JSON" { echo "wolfssl_ref=$WOLFSSL_DEB_REF" @@ -186,4 +226,7 @@ jobs: echo "openssl_latest_ref=$OSSL_LATEST" echo "openssl_latest_ref_array=[\"$OSSL_LATEST\"]" echo "openssl_all_releases_array=$OSSL_ALL_JSON" + echo "fips_ready_latest=$FR_LATEST" + echo "fips_ready_pr_array=$FR_PR_JSON" + echo "fips_ready_all_array=$FR_ALL_JSON" } >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/fips-ready.yml b/.github/workflows/fips-ready.yml index 699fd4fa..8f2edd81 100644 --- a/.github/workflows/fips-ready.yml +++ b/.github/workflows/fips-ready.yml @@ -32,16 +32,16 @@ jobs: fips_ready_test: needs: discover_versions if: github.event_name != 'pull_request' || github.event.pull_request.draft == false - name: FIPS Ready Bundle Test + name: FIPS Ready ${{ matrix.wolfssl_bundle_ref }} (${{ matrix.openssl_ref }}) runs-on: ubuntu-22.04 permissions: contents: read packages: write - timeout-minutes: 20 + timeout-minutes: 30 strategy: fail-fast: false matrix: - wolfssl_bundle_ref: [ '5.8.2' ] + wolfssl_bundle_ref: ${{ fromJson(needs.discover_versions.outputs.fips_ready_pr_array) }} openssl_ref: ${{ fromJson(needs.discover_versions.outputs.openssl_latest_ref_array) }} # force_fail collapsed into sequential runs in the test step steps: @@ -65,21 +65,8 @@ jobs: - name: Download FIPS Ready Bundle run: | - # Download FIPS ready bundle from wolfSSL website - BUNDLE_URL="https://www.wolfssl.com/wolfssl-${{matrix.wolfssl_bundle_ref}}-gplv3-fips-ready.zip" - - wget -O wolfssl-fips-ready.zip "$BUNDLE_URL" - unzip wolfssl-fips-ready.zip - - # Find the extracted directory (build script requires directory, not zip) - BUNDLE_DIR=$(find . -maxdepth 1 -type d -name "*fips-ready*" | head -n 1) - if [ -z "$BUNDLE_DIR" ]; then - echo "ERROR: Could not find FIPS ready bundle directory after extraction" - ls -la - exit 1 - fi - - echo "FIPS_BUNDLE_PATH=$(pwd)/$BUNDLE_DIR" >> $GITHUB_ENV + BUNDLE_DIR=$(./scripts/fetch-fips-ready.sh "${{ matrix.wolfssl_bundle_ref }}") + echo "FIPS_BUNDLE_PATH=$BUNDLE_DIR" >> $GITHUB_ENV echo "Found FIPS bundle directory at: $BUNDLE_DIR" - name: Build wolfProvider with FIPS Ready Bundle diff --git a/.github/workflows/nightly-fips-ready.yml b/.github/workflows/nightly-fips-ready.yml new file mode 100644 index 00000000..7f41e48a --- /dev/null +++ b/.github/workflows/nightly-fips-ready.yml @@ -0,0 +1,91 @@ +name: Nightly FIPS Ready Bundle Tests + +# Sweeps EVERY hosted FIPS Ready bundle at or above the floor. PR-time +# fips-ready.yml runs the narrower fips_ready_pr_array (the back-compat +# anchors plus the newest) to keep PR turnaround short; this restores the +# versions it drops. Both sets are resolved at run time by +# _discover-versions.yml, so a new FIPS Ready release joins the sweep on its +# own. + +on: + workflow_call: {} + workflow_dispatch: {} + +# No concurrency group: github.workflow is the caller's here, so any group collides with sibling calls. + +jobs: + discover_versions: + uses: ./.github/workflows/_discover-versions.yml + + fips_ready_test: + needs: discover_versions + name: Nightly FIPS Ready ${{ matrix.wolfssl_bundle_ref }} (${{ matrix.openssl_ref }}) + runs-on: ubuntu-22.04 + permissions: + contents: read + packages: write + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + wolfssl_bundle_ref: ${{ fromJson(needs.discover_versions.outputs.fips_ready_all_array) }} + openssl_ref: ${{ fromJson(needs.discover_versions.outputs.openssl_latest_ref_array) }} + steps: + - name: Checkout wolfProvider + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Cache build dependencies + id: deps + uses: ./.github/actions/oras-build-deps + with: + 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 + run: | + BUNDLE_DIR=$(./scripts/fetch-fips-ready.sh "${{ matrix.wolfssl_bundle_ref }}") + echo "FIPS_BUNDLE_PATH=$BUNDLE_DIR" >> $GITHUB_ENV + echo "Found FIPS bundle directory at: $BUNDLE_DIR" + + - 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 + + - name: Push build dependencies + uses: ./.github/actions/oras-build-deps-push + with: + registry: ${{ steps.deps.outputs.registry }} + openssl_install_tag: ${{ steps.deps.outputs.openssl_install_tag }} + wolfssl_install_tag: ${{ steps.deps.outputs.wolfssl_install_tag }} + openssl_hit: ${{ steps.deps.outputs.openssl_hit }} + wolfssl_hit: ${{ steps.deps.outputs.wolfssl_hit }} + + - name: Run FIPS Command Tests + run: | + export WOLFSSL_ISFIPS=1 + source scripts/env-setup + + # --- normal mode --- + ./scripts/cmd_test/do-cmd-tests.sh + + # --- force-fail mode --- + WOLFPROV_FORCE_FAIL=1 ./scripts/cmd_test/do-cmd-tests.sh + + - name: Run FIPS Status Test + run: | + export WOLFSSL_ISFIPS=1 + source scripts/env-setup + ./test/standalone/tests/fips_status/run.sh diff --git a/.github/workflows/nightly-osp.yml b/.github/workflows/nightly-osp.yml index 32a423b9..788f485a 100644 --- a/.github/workflows/nightly-osp.yml +++ b/.github/workflows/nightly-osp.yml @@ -4,8 +4,8 @@ name: Nightly OSP Suite # wolfssl stable). Stays dynamic across version bumps. # Wave 2 = v5.8.4-stable pinned (back-compat line), gated on wave1-done # with if: always() so a single Wave 1 failure doesn't skip the -# older-line coverage. multi-compiler and static-analysis run once -# outside the waves. +# older-line coverage. multi-compiler, fips-ready and static-analysis run +# once outside the waves. # nginx-pqc is Wave 1 only: PQC has a v5.9.2-stable floor, so the # v5.8.4-stable Wave 2 line has no eligible ref to run. @@ -169,3 +169,4 @@ jobs: # === Out-of-wave: not wolfssl-version-split === static-analysis: { uses: ./.github/workflows/static-analysis.yml } multi-compiler: { uses: ./.github/workflows/nightly-multi-compiler.yml } + fips-ready: { uses: ./.github/workflows/nightly-fips-ready.yml } diff --git a/.github/workflows/perf-regression.yml b/.github/workflows/perf-regression.yml index 2f44c0e2..bfcd17d4 100644 --- a/.github/workflows/perf-regression.yml +++ b/.github/workflows/perf-regression.yml @@ -55,7 +55,9 @@ jobs: strategy: fail-fast: false matrix: - wolfssl_bundle_ref: [ '5.8.2' ] + # Newest bundle only: perf-baseline.fips.json is a single calibrated + # baseline, so fanning it across versions makes the numbers meaningless. + wolfssl_bundle_ref: [ '${{ needs.discover_versions.outputs.fips_ready_latest }}' ] openssl_ref: ${{ fromJson(needs.discover_versions.outputs.openssl_latest_ref_array) }} steps: - name: Checkout wolfProvider @@ -65,16 +67,8 @@ jobs: - name: Download FIPS Ready Bundle run: | - BUNDLE_URL="https://www.wolfssl.com/wolfssl-${{ matrix.wolfssl_bundle_ref }}-gplv3-fips-ready.zip" - wget -O wolfssl-fips-ready.zip "$BUNDLE_URL" - unzip wolfssl-fips-ready.zip - BUNDLE_DIR=$(find . -maxdepth 1 -type d -name "*fips-ready*" | head -n 1) - if [ -z "$BUNDLE_DIR" ]; then - echo "ERROR: Could not find FIPS ready bundle directory after extraction" - ls -la - exit 1 - fi - echo "FIPS_BUNDLE_PATH=$(pwd)/$BUNDLE_DIR" >> $GITHUB_ENV + BUNDLE_DIR=$(./scripts/fetch-fips-ready.sh "${{ matrix.wolfssl_bundle_ref }}") + echo "FIPS_BUNDLE_PATH=$BUNDLE_DIR" >> $GITHUB_ENV - name: Build wolfProvider with FIPS Ready Bundle run: | diff --git a/docs/FIPS_INTEGRATION_GUIDE.md b/docs/FIPS_INTEGRATION_GUIDE.md index cb2c3586..2aa24dfb 100644 --- a/docs/FIPS_INTEGRATION_GUIDE.md +++ b/docs/FIPS_INTEGRATION_GUIDE.md @@ -142,6 +142,32 @@ The `--fips-check` option tells the build system which FIPS bundle type you have | `wolfssl-5.8.4-commercial-fips-linuxv5.7z` | `linuxv5` | | `wolfssl-5.8.4-commercial-fips-linuxv5.2.1.7z` | `linuxv5.2.1` | | `wolfssl-5.8.4-commercial-fips-v6.0.0.7z` | `v6.0.0` | +| `wolfssl-5.9.2-commercial-fips-v7.0.0.7z` | `v7.0.0` | + +### What `ready` Resolves To + +`ready` is not a fixed FIPS version. It tracks one release ahead of the newest +certificate, so the module version it selects moves with the bundle: + +| FIPS Ready bundle | `--enable-fips=ready` gives | +|-------------------|-----------------------------| +| 5.6.6 – 5.7.0 | `HAVE_FIPS_VERSION=5` | +| 5.7.2 – 5.9.1 | `HAVE_FIPS_VERSION=7` | +| 5.9.2 and newer | `HAVE_FIPS_VERSION=8` | + +When you need a stable target, pin a generation with the alias the bundle +ships rather than `ready`: `v5-ready` on the older bundles, `v6-ready` on +5.7.2 – 5.9.1, `v7-ready` on 5.9.2 and newer. + +The FIPS Ready bundles are published as +`https://www.wolfssl.com/wolfssl--gplv3-fips-ready.zip`. +`scripts/fetch-fips-ready.sh` resolves, downloads and verifies them: + +```bash +./scripts/fetch-fips-ready.sh --list # every hosted bundle >= 5.8.2 +./scripts/fetch-fips-ready.sh --latest # newest hosted bundle +./scripts/fetch-fips-ready.sh 5.9.2 # download + extract, prints the path +``` --- diff --git a/include/wolfprovider/settings.h b/include/wolfprovider/settings.h index 438d5812..3a6427c5 100644 --- a/include/wolfprovider/settings.h +++ b/include/wolfprovider/settings.h @@ -70,10 +70,15 @@ #ifdef WOLFSSL_SHA512 #define WP_HAVE_SHA512 #endif -#if (LIBWOLFSSL_VERSION_HEX >= 0x05000000) && !defined(WOLFSSL_NOSHA512_224) +/* FIPS v5-v7 modules set WOLFSSL_NOSHA512_224/256; v8 exports both. */ +#if defined(WP_HAVE_SHA512) && (LIBWOLFSSL_VERSION_HEX >= 0x05000000) && \ + !defined(WOLFSSL_NOSHA512_224) && !defined(HAVE_SELFTEST) && \ + !defined(SELF_TEST) #define WP_HAVE_SHA512_224 #endif -#if (LIBWOLFSSL_VERSION_HEX >= 0x05000000) && !defined(WOLFSSL_NOSHA512_256) +#if defined(WP_HAVE_SHA512) && (LIBWOLFSSL_VERSION_HEX >= 0x05000000) && \ + !defined(WOLFSSL_NOSHA512_256) && !defined(HAVE_SELFTEST) && \ + !defined(SELF_TEST) #define WP_HAVE_SHA512_256 #endif #ifdef WOLFSSL_SHA3 diff --git a/scripts/build-wolfprovider.sh b/scripts/build-wolfprovider.sh index 41236bc9..15955430 100755 --- a/scripts/build-wolfprovider.sh +++ b/scripts/build-wolfprovider.sh @@ -17,7 +17,8 @@ show_help() { echo " --wolfssl-ver=VER Which version of wolfSSL to clone" echo " --enable-fips Build wolfProvider with a cloned FIPS bundle. Use with --fips-check to specify tag (default: v5.2.4)" echo " --fips-bundle=DIR Build wolfProvider with a directory containing a wolfSSL FIPS bundle. Use with --fips-check to specify tag" - echo " --fips-check=TAG Choose a FIPS tag (v5.2.1, v5.2.4, linuxv5.2.1, v6.0.0, ready, etc). Automatically determines configure option" + echo " --fips-check=TAG Choose a FIPS tag (v5.2.1, v5.2.4, linuxv5.2.1, v6.0.0, v7.0.0, ready, etc). Automatically determines configure option" + echo " 'ready' follows the bundle: FIPS v7 up to bundle 5.9.1, v8 from 5.9.2 on. Fetch bundles with scripts/fetch-fips-ready.sh" echo " --debian Build a Debian package" echo " --debian --enable-fips Build a Debian package with FIPS support" echo " --quicktest Disable some tests for a faster testing suite" diff --git a/scripts/fetch-fips-ready.sh b/scripts/fetch-fips-ready.sh new file mode 100755 index 00000000..c06df595 --- /dev/null +++ b/scripts/fetch-fips-ready.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Resolves and downloads wolfSSL FIPS Ready bundles from wolfssl.com. +# Usage: ./fetch-fips-ready.sh --list [--floor X.Y.Z] +# ./fetch-fips-ready.sh --latest [--floor X.Y.Z] +# ./fetch-fips-ready.sh [--dest DIR] +# Example: ./fetch-fips-ready.sh 5.9.2 --dest /tmp + +# 5.8.2 is the oldest bundle wolfProvider is tested against. +FLOOR="5.8.2" +DEST="$PWD" +MODE="" +VERSION="" + +DOWNLOAD_PAGE="https://www.wolfssl.com/download/" +WOLFSSL_GIT="https://github.com/wolfSSL/wolfssl.git" + +usage() { + echo "Usage: $0 --list|--latest [--floor X.Y.Z]" >&2 + echo " $0 [--dest DIR]" >&2 + exit 1 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --list|--latest) + MODE="${1#--}" + shift + ;; + --floor) + [[ $# -ge 2 ]] || usage + FLOOR="$2" + shift 2 + ;; + --floor=*) + FLOOR="${1#--floor=}" + shift + ;; + --dest) + [[ $# -ge 2 ]] || usage + DEST="$2" + shift 2 + ;; + --dest=*) + DEST="${1#--dest=}" + shift + ;; + -h|--help) + usage + ;; + -*) + echo "fetch-fips-ready: unknown option $1" >&2 + usage + ;; + *) + [[ -z "$VERSION" ]] || usage + VERSION="$1" + MODE="${MODE:-fetch}" + shift + ;; + esac +done + +[[ -n "$MODE" ]] || usage +[[ "$MODE" != "fetch" || -n "$VERSION" ]] || usage + +bundle_url() { + echo "https://www.wolfssl.com/wolfssl-$1-gplv3-fips-ready.zip" +} + +retry_out() { + local out attempt + for attempt in 1 2 3; do + if out=$("$@" 2>/dev/null) && [[ -n "$out" ]]; then + printf '%s' "$out" + return 0 + fi + echo "fetch-fips-ready: retry $attempt/3: '$*' failed; retrying..." >&2 + sleep $((attempt * 5)) + done + return 1 +} + +# ver_ge A B -- true when A >= B +ver_ge() { + [[ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -n1)" == "$2" ]] +} + +get_page() { + curl -fsSL "$DOWNLOAD_PAGE" +} + +# The download page advertises only the newest bundle. +page_latest() { + get_page \ + | grep -o 'wolfssl-[0-9][0-9.]*-gplv3-fips-ready\.zip' \ + | sed -E 's/^wolfssl-(.*)-gplv3-fips-ready\.zip$/\1/' \ + | sort -V | tail -n 1 +} + +page_sha256() { + get_page | tr -d '\n' \ + | grep -o "wolfssl-$1-gplv3-fips-ready\.zip[^(]*(SHA256: *[0-9a-f]\{64\}" \ + | head -n 1 | grep -o '[0-9a-f]\{64\}' +} + +ls_wolfssl_stable() { + git ls-remote --tags --refs "$WOLFSSL_GIT" 'v*-stable' \ + | awk -F/ '{print $NF}' \ + | sed -E 's/^v(.*)-stable$/\1/' \ + | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' +} + +bundle_exists() { + curl -fsI --max-time 30 "$(bundle_url "$1")" >/dev/null 2>&1 +} + +# Candidates are the union of upstream -stable tags and the advertised latest, +# because neither set alone matches what is hosted: 5.8.6 has a tag but no +# bundle, and 5.9.1 has a bundle the page never lists. +list_versions() { + local candidates ver found + candidates=$(retry_out ls_wolfssl_stable) || { + echo "fetch-fips-ready: could not list wolfSSL tags" >&2 + return 1 + } + candidates="$candidates +$(page_latest 2>/dev/null || true)" + + found="" + for ver in $(printf '%s\n' "$candidates" | grep -E '^[0-9.]+$' | sort -V -u); do + ver_ge "$ver" "$FLOOR" || continue + if bundle_exists "$ver"; then + found="$found$ver"$'\n' + fi + done + + [[ -n "$found" ]] || return 1 + printf '%s' "$found" +} + +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + shasum -a 256 "$1" | awk '{print $1}' + fi +} + +fetch_bundle() { + local ver="$1" zip dir want got attempt + zip="$DEST/wolfssl-$ver-gplv3-fips-ready.zip" + dir="$DEST/wolfssl-$ver-gplv3-fips-ready" + + mkdir -p "$DEST" + rm -rf "$dir" + + for attempt in 1 2 3; do + if curl -fsSL --max-time 900 -o "$zip" "$(bundle_url "$ver")"; then + break + fi + if [[ "$attempt" -eq 3 ]]; then + echo "fetch-fips-ready: could not download bundle $ver" >&2 + return 1 + fi + echo "fetch-fips-ready: download attempt $attempt failed; retrying..." >&2 + sleep $((attempt * 5)) + done + + want=$(page_sha256 "$ver" 2>/dev/null || true) + if [[ -n "$want" ]]; then + got=$(sha256_of "$zip") + if [[ "$got" != "$want" ]]; then + echo "fetch-fips-ready: SHA256 mismatch for $ver (want $want, got $got)" >&2 + return 1 + fi + else + # Only the newest bundle has a published hash; the rest get an integrity + # check from the archive itself. + echo "fetch-fips-ready: no published SHA256 for $ver, verifying archive" >&2 + unzip -tqq "$zip" >/dev/null + fi + + unzip -q "$zip" -d "$DEST" + rm -f "$zip" + + if [[ ! -d "$dir" ]]; then + dir=$(find "$DEST" -maxdepth 1 -type d -name '*fips-ready*' | head -n 1) + fi + if [[ -z "$dir" || ! -d "$dir" ]]; then + echo "fetch-fips-ready: no bundle directory after extracting $ver" >&2 + return 1 + fi + + cd "$(dirname "$dir")" && echo "$PWD/$(basename "$dir")" +} + +case "$MODE" in + list) + list_versions + ;; + latest) + list_versions | tail -n 1 + ;; + fetch) + fetch_bundle "$VERSION" + ;; +esac diff --git a/src/wp_digests.c b/src/wp_digests.c index 7bd3f1a7..88db8832 100644 --- a/src/wp_digests.c +++ b/src/wp_digests.c @@ -528,25 +528,21 @@ IMPLEMENT_DIGEST(wp_sha512, wc_Sha512, wc_InitSha512_ex, wc_Sha512Update, wc_Sha512Final, wc_Sha512Copy, wc_Sha512Free) -#if LIBWOLFSSL_VERSION_HEX >= 0x05000000 -#if !defined(WOLFSSL_NOSHA512_224) && \ - !defined(HAVE_FIPS) && !defined(HAVE_SELFTEST) +#ifdef WP_HAVE_SHA512_224 IMPLEMENT_DIGEST(wp_sha512_224, wc_Sha512_224, WC_SHA512_224_BLOCK_SIZE, WC_SHA512_224_DIGEST_SIZE, WP_SHA2_FLAGS, wc_InitSha512_224_ex, wc_Sha512_224Update, wc_Sha512_224Final, wc_Sha512_224Copy, wc_Sha512_224Free) -#endif /* WOLFSSL_NOSHA512_224 */ +#endif /* WP_HAVE_SHA512_224 */ -#if !defined(WOLFSSL_NOSHA512_256) && \ - !defined(HAVE_FIPS) && !defined(HAVE_SELFTEST) +#ifdef WP_HAVE_SHA512_256 IMPLEMENT_DIGEST(wp_sha512_256, wc_Sha512_256, WC_SHA512_256_BLOCK_SIZE, WC_SHA512_256_DIGEST_SIZE, WP_SHA2_FLAGS, wc_InitSha512_256_ex, wc_Sha512_256Update, wc_Sha512_256Final, wc_Sha512_256Copy, wc_Sha512_256Free) -#endif /* WOLFSSL_NOSHA512_256 */ -#endif +#endif /* WP_HAVE_SHA512_256 */ #endif /* WP_HAVE_SHA512 */ diff --git a/src/wp_internal.c b/src/wp_internal.c index 513b7349..1a813738 100644 --- a/src/wp_internal.c +++ b/src/wp_internal.c @@ -770,9 +770,7 @@ int wp_hash_copy(wc_HashAlg* src, wc_HashAlg* dst, enum wc_HashType hashType) rc = wc_Sha512Copy(&src->sha512, &dst->sha512); #endif break; -#if LIBWOLFSSL_VERSION_HEX >= 0x05000000 -#if !defined(WOLFSSL_NOSHA512_224) && !defined(HAVE_FIPS) && \ - !defined(SELF_TEST) +#ifdef WP_HAVE_SHA512_224 case WC_HASH_TYPE_SHA512_224: #if LIBWOLFSSL_VERSION_HEX >= 0x05007004 rc = wc_Sha512_224Copy(&src->alg.sha512, &dst->alg.sha512); @@ -780,9 +778,8 @@ int wp_hash_copy(wc_HashAlg* src, wc_HashAlg* dst, enum wc_HashType hashType) rc = wc_Sha512_224Copy(&src->sha512, &dst->sha512); #endif break; -#endif /* !WOLFSSL_NOSHA512_224 */ -#if !defined(WOLFSSL_NOSHA512_256) && !defined(HAVE_FIPS) && \ - !defined(SELF_TEST) +#endif /* WP_HAVE_SHA512_224 */ +#ifdef WP_HAVE_SHA512_256 case WC_HASH_TYPE_SHA512_256: #if LIBWOLFSSL_VERSION_HEX >= 0x05007004 rc = wc_Sha512_256Copy(&src->alg.sha512, &dst->alg.sha512); @@ -790,8 +787,7 @@ int wp_hash_copy(wc_HashAlg* src, wc_HashAlg* dst, enum wc_HashType hashType) rc = wc_Sha512_256Copy(&src->sha512, &dst->sha512); #endif break; -#endif /* !WOLFSSL_NOSHA512_256 */ -#endif /* LIBWOLFSSL_VERSION_HEX >= 0x05000000 */ +#endif /* WP_HAVE_SHA512_256 */ #else case WC_HASH_TYPE_SHA512: case WC_HASH_TYPE_SHA512_224: diff --git a/src/wp_wolfprov.c b/src/wp_wolfprov.c index f09c5607..ccd28ec4 100644 --- a/src/wp_wolfprov.c +++ b/src/wp_wolfprov.c @@ -430,18 +430,16 @@ static const OSSL_ALGORITHM wolfprov_digests[] = { #ifdef WP_HAVE_SHA512 { WP_NAMES_SHA2_512, WOLFPROV_PROPERTIES, wp_sha512_functions, "" }, -#if LIBWOLFSSL_VERSION_HEX >= 0x05000000 -#ifndef WOLFSSL_NOSHA512_224 +#ifdef WP_HAVE_SHA512_224 { WP_NAMES_SHA2_512_224, WOLFPROV_PROPERTIES, wp_sha512_224_functions, "" }, -#endif /* !WOLFSSL_NOSHA512_224 */ -#ifndef WOLFSSL_NOSHA512_256 +#endif /* WP_HAVE_SHA512_224 */ +#ifdef WP_HAVE_SHA512_256 { WP_NAMES_SHA2_512_256, WOLFPROV_PROPERTIES, wp_sha512_256_functions, "" }, -#endif /* !WOLFSSL_NOSHA512_256 */ -#endif +#endif /* WP_HAVE_SHA512_256 */ #endif /* WP_HAVE_SHA512 */ /* SHA-3 */ diff --git a/test/test_digest.c b/test/test_digest.c index 5b30132c..97a95b44 100644 --- a/test/test_digest.c +++ b/test/test_digest.c @@ -149,6 +149,24 @@ int test_sha512(void *data) /******************************************************************************/ +#ifdef WP_HAVE_SHA512_224 +int test_sha512_224(void *data) +{ + return test_create_digest("SHA512-224", data); +} +#endif + +/******************************************************************************/ + +#ifdef WP_HAVE_SHA512_256 +int test_sha512_256(void *data) +{ + return test_create_digest("SHA512-256", data); +} +#endif + +/******************************************************************************/ + #ifdef WP_HAVE_SHA3_224 int test_sha3_224(void *data) { diff --git a/test/unit.c b/test/unit.c index 24692402..546d755d 100644 --- a/test/unit.c +++ b/test/unit.c @@ -197,6 +197,12 @@ TEST_CASE test_case[] = { #ifdef WP_HAVE_SHA512 TEST_DECL(test_sha512, NULL), #endif +#ifdef WP_HAVE_SHA512_224 + TEST_DECL(test_sha512_224, NULL), +#endif +#ifdef WP_HAVE_SHA512_256 + TEST_DECL(test_sha512_256, NULL), +#endif #ifdef WP_HAVE_SHA3_224 TEST_DECL(test_sha3_224, NULL), #endif diff --git a/test/unit.h b/test/unit.h index e7829d29..e0635a72 100644 --- a/test/unit.h +++ b/test/unit.h @@ -137,6 +137,8 @@ int test_sha224(void *data); int test_sha256(void *data); int test_sha384(void *data); int test_sha512(void *data); +int test_sha512_224(void *data); +int test_sha512_256(void *data); int test_sha3_224(void *data); int test_sha3_256(void *data); int test_sha3_384(void *data); From 6f0754aa60aa8c361d49cba22a85ecf3684d9b6a Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 6 Aug 2026 15:36:01 -0700 Subject: [PATCH 02/11] Address skoll findings on the FIPS Ready auto-resolve PR --- .github/workflows/pr-osp-select.yml | 4 +++ scripts/fetch-fips-ready.sh | 53 +++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/.github/workflows/pr-osp-select.yml b/.github/workflows/pr-osp-select.yml index c98eb7c8..de75d9eb 100644 --- a/.github/workflows/pr-osp-select.yml +++ b/.github/workflows/pr-osp-select.yml @@ -223,6 +223,10 @@ jobs: needs: select if: contains(needs.select.outputs.run, ' all ') || contains(needs.select.outputs.run, ' multi-compiler ') uses: ./.github/workflows/nightly-multi-compiler.yml + fips-ready: + needs: select + if: contains(needs.select.outputs.run, ' all ') || contains(needs.select.outputs.run, ' fips-ready ') + uses: ./.github/workflows/nightly-fips-ready.yml perf: needs: select if: contains(needs.select.outputs.run, ' all ') || contains(needs.select.outputs.run, ' perf ') diff --git a/scripts/fetch-fips-ready.sh b/scripts/fetch-fips-ready.sh index c06df595..59448b39 100755 --- a/scripts/fetch-fips-ready.sh +++ b/scripts/fetch-fips-ready.sh @@ -120,20 +120,30 @@ bundle_exists() { # because neither set alone matches what is hosted: 5.8.6 has a tag but no # bundle, and 5.9.1 has a bundle the page never lists. list_versions() { - local candidates ver found + local candidates ver found probe_dir candidates=$(retry_out ls_wolfssl_stable) || { echo "fetch-fips-ready: could not list wolfSSL tags" >&2 return 1 } candidates="$candidates $(page_latest 2>/dev/null || true)" + candidates=$(printf '%s\n' "$candidates" | grep -E '^[0-9.]+$' | sort -V -u \ + | while read -r ver; do ver_ge "$ver" "$FLOOR" && echo "$ver"; done) + [[ -n "$candidates" ]] || return 1 + + # This runs inside the shared _discover-versions.yml job, which every + # caller pays for even when it never touches FIPS outputs -- probe + # candidates in parallel so N versions costs one round trip, not N. + probe_dir=$(mktemp -d) + trap 'rm -rf "$probe_dir"' RETURN + for ver in $candidates; do + (bundle_exists "$ver" && touch "$probe_dir/$ver") & + done + wait found="" - for ver in $(printf '%s\n' "$candidates" | grep -E '^[0-9.]+$' | sort -V -u); do - ver_ge "$ver" "$FLOOR" || continue - if bundle_exists "$ver"; then - found="$found$ver"$'\n' - fi + for ver in $(printf '%s\n' "$candidates" | sort -V); do + [[ -e "$probe_dir/$ver" ]] && found="$found$ver"$'\n' done [[ -n "$found" ]] || return 1 @@ -149,12 +159,16 @@ sha256_of() { } fetch_bundle() { - local ver="$1" zip dir want got attempt - zip="$DEST/wolfssl-$ver-gplv3-fips-ready.zip" - dir="$DEST/wolfssl-$ver-gplv3-fips-ready" + local ver="$1" zip dir want got attempt scratch scratch_dir mkdir -p "$DEST" - rm -rf "$dir" + # Download and verify into a scratch dir first; the caller's existing + # $dir is only touched after a verified download, so a failed fetch + # never destroys a bundle that was already there. + scratch=$(mktemp -d "$DEST/.fetch-fips-ready.XXXXXX") + trap 'rm -rf "$scratch"' RETURN + zip="$scratch/wolfssl-$ver-gplv3-fips-ready.zip" + dir="$DEST/wolfssl-$ver-gplv3-fips-ready" for attempt in 1 2 3; do if curl -fsSL --max-time 900 -o "$zip" "$(bundle_url "$ver")"; then @@ -179,20 +193,29 @@ fetch_bundle() { # Only the newest bundle has a published hash; the rest get an integrity # check from the archive itself. echo "fetch-fips-ready: no published SHA256 for $ver, verifying archive" >&2 - unzip -tqq "$zip" >/dev/null + if ! unzip -tqq "$zip" >/dev/null; then + echo "fetch-fips-ready: downloaded archive for $ver failed integrity check" >&2 + return 1 + fi fi - unzip -q "$zip" -d "$DEST" + unzip -q "$zip" -d "$scratch" rm -f "$zip" - if [[ ! -d "$dir" ]]; then - dir=$(find "$DEST" -maxdepth 1 -type d -name '*fips-ready*' | head -n 1) + if [[ ! -d "$scratch/wolfssl-$ver-gplv3-fips-ready" ]]; then + scratch_dir=$(find "$scratch" -maxdepth 1 -mindepth 1 -type d -name '*fips-ready*' | head -n 1) + else + scratch_dir="$scratch/wolfssl-$ver-gplv3-fips-ready" fi - if [[ -z "$dir" || ! -d "$dir" ]]; then + if [[ -z "$scratch_dir" || ! -d "$scratch_dir" ]]; then echo "fetch-fips-ready: no bundle directory after extracting $ver" >&2 return 1 fi + # Verified: safe to replace whatever was at $dir. + rm -rf "$dir" + mv "$scratch_dir" "$dir" + cd "$(dirname "$dir")" && echo "$PWD/$(basename "$dir")" } From cb5f805aeaeea362f4aff7983cee74e70b090754 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 6 Aug 2026 16:03:32 -0700 Subject: [PATCH 03/11] Fix retry semantics, add resolver tests, add wp_hash_copy coverage --- scripts/fetch-fips-ready.sh | 24 +- scripts/test-fetch-fips-ready.sh | 390 +++++++++++++++++++++++++++++++ test/test_rsa.c | 106 +++++++++ test/unit.c | 3 + test/unit.h | 3 + 5 files changed, 525 insertions(+), 1 deletion(-) create mode 100755 scripts/test-fetch-fips-ready.sh diff --git a/scripts/fetch-fips-ready.sh b/scripts/fetch-fips-ready.sh index 59448b39..44e5fe00 100755 --- a/scripts/fetch-fips-ready.sh +++ b/scripts/fetch-fips-ready.sh @@ -112,8 +112,30 @@ ls_wolfssl_stable() { | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' } +# Retries transient failures (timeout, 5xx, connection error) but treats a +# confirmed 4xx as a definitive "not hosted" with no retry -- conflating the +# two would silently drop a version on a network blip. bundle_exists() { - curl -fsI --max-time 30 "$(bundle_url "$1")" >/dev/null 2>&1 + local ver="$1" code attempt + for attempt in 1 2 3; do + # A genuine connection failure (not just a non-200 status) makes + # curl itself exit nonzero; `|| code=""` keeps that from tripping + # `set -e` before the retry loop below gets to run. + code=$(curl -sS -I -o /dev/null -w '%{http_code}' --max-time 30 \ + "$(bundle_url "$ver")" 2>/dev/null) || code="" + case "$code" in + 200) return 0 ;; + 4??) return 1 ;; + *) + if [[ "$attempt" -lt 3 ]]; then + sleep $((attempt * 3)) + fi + ;; + esac + done + echo "fetch-fips-ready: could not confirm bundle $ver after retries" \ + "(last status: ${code:-none})" >&2 + return 1 } # Candidates are the union of upstream -stable tags and the advertised latest, diff --git a/scripts/test-fetch-fips-ready.sh b/scripts/test-fetch-fips-ready.sh new file mode 100755 index 00000000..4edf3b24 --- /dev/null +++ b/scripts/test-fetch-fips-ready.sh @@ -0,0 +1,390 @@ +#!/bin/bash +# +# Copyright (C) 2006-2026 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 . +# +# Isolated, network-free tests for scripts/fetch-fips-ready.sh: mocks curl +# and git so the resolver and extraction logic -- especially its destructive +# and failure paths -- run deterministically in CI without hitting +# wolfssl.com or GitHub. +set -euo pipefail + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +TARGET="${SCRIPT_DIR}/fetch-fips-ready.sh" +WORK=$(mktemp -d) +MOCKBIN="${WORK}/bin" +FNS="${WORK}/fns.sh" +PASS=0 +FAIL=0 + +cleanup() { + rm -rf "${WORK}" +} +trap cleanup EXIT + +mkdir -p "${MOCKBIN}" + +# Source only fetch-fips-ready.sh's function definitions -- not its argv +# parser or the case-dispatch at the bottom, both of which would run +# immediately (and call exit) if sourced as-is. Boundaries are located by +# marker pattern rather than hardcoded line numbers so this test doesn't +# silently go stale as the script grows. +# shellcheck disable=SC2016 # single-quoted on purpose: matching a literal +# '$' in fetch-fips-ready.sh's source, not expanding a variable here. +while_line=$(grep -n '^while \[\[ \$# -gt 0' "${TARGET}" | head -n1 | cut -d: -f1) +bundle_url_line=$(grep -n '^bundle_url()' "${TARGET}" | head -n1 | cut -d: -f1) +# shellcheck disable=SC2016 +case_line=$(grep -n '^case "\$MODE"' "${TARGET}" | head -n1 | cut -d: -f1) +if [[ -z "${while_line}" || -z "${bundle_url_line}" || -z "${case_line}" ]]; then + echo "test-fetch-fips-ready: could not locate expected markers in" \ + "${TARGET} -- has its structure changed?" >&2 + exit 1 +fi +{ + sed -n "1,$((while_line - 1))p" "${TARGET}" + sed -n "${bundle_url_line},$((case_line - 1))p" "${TARGET}" +} > "${FNS}" + +pass() { + PASS=$((PASS + 1)) + echo "PASS: $1" +} + +fail() { + FAIL=$((FAIL + 1)) + echo "FAIL: $1" +} + +# Runs $2.. in a subshell with the mock bin dir first on PATH and the +# resolver functions sourced, so each test gets an isolated MOCKBIN/DEST +# without polluting other tests' mock scripts or fixtures. +run_case() { + local dest="$1" + shift + # DEST must be set AFTER sourcing: the sourced preamble itself sets + # DEST="$PWD", which would otherwise clobber a prefix assignment made + # before the source builtin runs. DEST is consumed inside fns.sh, not + # this file, hence the two shellcheck disables below. + # shellcheck disable=SC1090,SC2034 + ( PATH="${MOCKBIN}:${PATH}"; source "${FNS}"; DEST="${dest}"; "$@" ) +} + +# --- bundle_exists: retries a transient failure (connection error, no HTTP +# response) and succeeds once the server recovers. --- +test_bundle_exists_retries_transient() { + local d counter + d=$(mktemp -d) + counter="${d}/count" + cat > "${MOCKBIN}/curl" < "${counter}" +if [[ "\$*" == *"-I"* && "\$*" == *"%{http_code}"* ]]; then + if [[ "\$n" -lt 3 ]]; then exit 7; fi + printf '200' + exit 0 +fi +exit 1 +EOF + chmod +x "${MOCKBIN}/curl" + + if run_case "${d}" bundle_exists "9.9.9"; then + if [[ "$(cat "${counter}")" == "3" ]]; then + pass "bundle_exists retries transient failures and succeeds" + else + fail "bundle_exists succeeded but call count was $(cat "${counter}"), want 3" + fi + else + fail "bundle_exists did not recover from a transient failure" + fi + rm -rf "${d}" +} + +# --- bundle_exists: a confirmed 4xx is definitive -- no retry burned. --- +test_bundle_exists_confirmed_404_no_retry() { + local d counter + d=$(mktemp -d) + counter="${d}/count" + cat > "${MOCKBIN}/curl" < "${counter}" +if [[ "\$*" == *"-I"* && "\$*" == *"%{http_code}"* ]]; then + printf '404' + exit 0 +fi +exit 1 +EOF + chmod +x "${MOCKBIN}/curl" + + if run_case "${d}" bundle_exists "9.9.9"; then + fail "bundle_exists reported a 404 bundle as existing" + else + if [[ "$(cat "${counter}")" == "1" ]]; then + pass "bundle_exists treats a confirmed 404 as final, no retry" + else + fail "bundle_exists retried after a confirmed 404 ($(cat "${counter}") calls)" + fi + fi + rm -rf "${d}" +} + +# --- bundle_exists: exhausts retries and reports absent (not a crash) when +# every attempt is transient. --- +test_bundle_exists_exhausts_on_persistent_transient() { + local d + d=$(mktemp -d) + cat > "${MOCKBIN}/curl" <<'EOF' +#!/usr/bin/env bash +if [[ "$*" == *"-I"* && "$*" == *"%{http_code}"* ]]; then + exit 7 +fi +exit 1 +EOF + chmod +x "${MOCKBIN}/curl" + + if run_case "${d}" bundle_exists "9.9.9" 2>/dev/null; then + fail "bundle_exists reported existence despite persistent transient failure" + else + pass "bundle_exists exhausts retries and reports absent, not a crash" + fi + rm -rf "${d}" +} + +# --- list_versions: unions git tags with the page's advertised latest, +# applies the floor, and only keeps versions that probe as hosted. --- +test_list_versions_filters_and_sorts() { + local d html + d=$(mktemp -d) + html="${d}/page.html" + cat > "${html}" <<'EOF' + (SHA256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa)
+EOF + cat > "${MOCKBIN}/git" <<'EOF' +#!/usr/bin/env bash +if [[ "$1" == "ls-remote" ]]; then + cat < "${MOCKBIN}/curl" < "${MOCKBIN}/git" <<'EOF' +#!/usr/bin/env bash +exit 1 +EOF + chmod +x "${MOCKBIN}/git" + cat > "${MOCKBIN}/curl" <<'EOF' +#!/usr/bin/env bash +exit 1 +EOF + chmod +x "${MOCKBIN}/curl" + + if run_case "${d}" list_versions 2>/dev/null; then + fail "list_versions succeeded despite git ls-remote failing entirely" + else + pass "list_versions fails cleanly when upstream tags are unavailable" + fi + rm -rf "${d}" +} + +# Builds a valid zip fixture at $2 containing wolfssl-$1-gplv3-fips-ready/. +make_fixture_zip() { + local ver="$1" out="$2" stage + stage=$(mktemp -d) + mkdir -p "${stage}/wolfssl-${ver}-gplv3-fips-ready" + echo "fixture" > "${stage}/wolfssl-${ver}-gplv3-fips-ready/README" + ( cd "${stage}" && zip -q -r "${out}" "wolfssl-${ver}-gplv3-fips-ready" ) + rm -rf "${stage}" +} + +# --- fetch_bundle: a download failure must NOT destroy a bundle that was +# already at the destination (the bug the HIGH finding flagged). --- +test_fetch_bundle_preserves_existing_on_download_failure() { + local d + d=$(mktemp -d) + mkdir -p "${d}/wolfssl-9.9.9-gplv3-fips-ready" + touch "${d}/wolfssl-9.9.9-gplv3-fips-ready/CANARY" + + cat > "${MOCKBIN}/curl" <<'EOF' +#!/usr/bin/env bash +if [[ "$*" == *"-I"* && "$*" == *"%{http_code}"* ]]; then + printf '200' + exit 0 +fi +if [[ "$*" == *"--max-time 900"* || "$*" == *"-o"* ]]; then + for a in "$@"; do + if [[ "$a" == *"gplv3-fips-ready.zip"* ]]; then + exit 1 + fi + done +fi +exit 1 +EOF + chmod +x "${MOCKBIN}/curl" + + if run_case "${d}" fetch_bundle "9.9.9" 2>/dev/null; then + fail "fetch_bundle reported success despite the download failing" + elif [[ -f "${d}/wolfssl-9.9.9-gplv3-fips-ready/CANARY" ]]; then + pass "fetch_bundle preserves an existing bundle when download fails" + else + fail "fetch_bundle destroyed the existing bundle on a failed download" + fi + rm -rf "${d}" +} + +# --- fetch_bundle: a checksum mismatch must also preserve whatever was +# already there, not just a hard network failure. --- +test_fetch_bundle_checksum_mismatch_preserves_existing() { + local d html zip + d=$(mktemp -d) + mkdir -p "${d}/wolfssl-9.9.9-gplv3-fips-ready" + touch "${d}/wolfssl-9.9.9-gplv3-fips-ready/CANARY" + + zip="${d}/fixture.zip" + make_fixture_zip "9.9.9" "${zip}" + html="${d}/page.html" + # Advertise a hash that will never match the fixture's real SHA256. + cat > "${html}" <<'EOF' + (SHA256: 0000000000000000000000000000000000000000000000000000000000000000)
+EOF + cat > "${MOCKBIN}/curl" </dev/null; then + fail "fetch_bundle accepted a bundle with a mismatched checksum" + elif [[ -f "${d}/wolfssl-9.9.9-gplv3-fips-ready/CANARY" ]]; then + pass "fetch_bundle preserves an existing bundle on checksum mismatch" + else + fail "fetch_bundle destroyed the existing bundle on checksum mismatch" + fi + rm -rf "${d}" +} + +# --- fetch_bundle: a verified, successful fetch DOES replace whatever was +# there -- the fix must not make replacement impossible, only safe. --- +test_fetch_bundle_replaces_on_success() { + local d zip out + d=$(mktemp -d) + mkdir -p "${d}/wolfssl-9.9.9-gplv3-fips-ready" + touch "${d}/wolfssl-9.9.9-gplv3-fips-ready/CANARY" + + zip="${d}/fixture.zip" + make_fixture_zip "9.9.9" "${zip}" + cat > "${MOCKBIN}/curl" </dev/null); then + if [[ -f "${d}/wolfssl-9.9.9-gplv3-fips-ready/CANARY" ]]; then + fail "fetch_bundle left the stale bundle in place after a verified success" + elif [[ -f "${d}/wolfssl-9.9.9-gplv3-fips-ready/README" ]] \ + && [[ "${out}" == "${d}/wolfssl-9.9.9-gplv3-fips-ready" ]]; then + pass "fetch_bundle replaces an existing bundle on verified success" + else + fail "fetch_bundle succeeded but left an unexpected result: [${out}]" + fi + else + fail "fetch_bundle failed on a fully valid download" + fi + rm -rf "${d}" +} + +test_bundle_exists_retries_transient +test_bundle_exists_confirmed_404_no_retry +test_bundle_exists_exhausts_on_persistent_transient +test_list_versions_filters_and_sorts +test_list_versions_fails_when_tags_unavailable +test_fetch_bundle_preserves_existing_on_download_failure +test_fetch_bundle_checksum_mismatch_preserves_existing +test_fetch_bundle_replaces_on_success + +echo "" +echo "fetch-fips-ready tests: ${PASS} passed, ${FAIL} failed" +[[ "${FAIL}" -eq 0 ]] diff --git a/test/test_rsa.c b/test/test_rsa.c index b595b9c4..4357abe8 100644 --- a/test/test_rsa.c +++ b/test/test_rsa.c @@ -3361,4 +3361,110 @@ int test_rsa_sig_alg_id(void *data) return err; } +#if defined(WP_HAVE_SHA512_224) && defined(WP_HAVE_SHA512_256) +/* EVP_MD_CTX_copy_ex on a partial RSA digest_sign accumulator, using + * SHA512-224/SHA512-256 to exercise the wp_hash_copy() branches those two + * digests only gained in FIPS v8 (older FIPS modules never define + * WP_HAVE_SHA512_224/256, so this path is skipped there). Both the + * original and duplicated contexts must produce signatures that verify + * under the original key. */ +static int test_rsa_dupctx_verify(EVP_PKEY *pkey, const char *md, + const unsigned char *msg, size_t msgLen, unsigned char *sig, + size_t sigLen) +{ + int err; + EVP_MD_CTX *vCtx = EVP_MD_CTX_new(); + + err = vCtx == NULL; + if (err == 0) { + err = EVP_DigestVerifyInit_ex(vCtx, NULL, md, wpLibCtx, NULL, pkey, + NULL) != 1; + } + if (err == 0) { + err = EVP_DigestVerifyUpdate(vCtx, msg, msgLen) != 1; + } + if (err == 0) { + err = EVP_DigestVerifyFinal(vCtx, sig, sigLen) != 1; + } + + EVP_MD_CTX_free(vCtx); + + return err; +} + +int test_rsa_sha512_224_256_dupctx(void *data) +{ + static const unsigned char part1[16] = "rsa-dupctx-part1"; + static const unsigned char part2[16] = "rsa-dupctx-part2"; + const char *mds[] = { "SHA512-224", "SHA512-256" }; + int err = 0; + size_t i; + EVP_PKEY *pkey = NULL; + const unsigned char *p = rsa_key_der_2048; + EVP_MD_CTX *a = NULL; + EVP_MD_CTX *b = NULL; + unsigned char sigA[256]; + unsigned char sigB[256]; + size_t sigALen; + size_t sigBLen; + unsigned char msg[32]; + + (void)data; + + XMEMCPY(msg, part1, sizeof(part1)); + XMEMCPY(msg + sizeof(part1), part2, sizeof(part2)); + + pkey = d2i_PrivateKey(EVP_PKEY_RSA, NULL, &p, sizeof(rsa_key_der_2048)); + err = pkey == NULL; + + for (i = 0; (err == 0) && (i < sizeof(mds) / sizeof(mds[0])); i++) { + PRINT_MSG("RSA dupctx %s", mds[i]); + + sigALen = sizeof(sigA); + sigBLen = sizeof(sigB); + + a = EVP_MD_CTX_new(); + err = (a == NULL); + if (err == 0) { + err = EVP_DigestSignInit_ex(a, NULL, mds[i], wpLibCtx, NULL, + pkey, NULL) != 1; + } + if (err == 0) { + err = EVP_DigestSignUpdate(a, part1, sizeof(part1)) != 1; + } + if (err == 0) { + b = EVP_MD_CTX_new(); + err = (b == NULL); + } + if (err == 0) { + err = EVP_MD_CTX_copy_ex(b, a) != 1; + } + if (err == 0) { + err = EVP_DigestSignUpdate(a, part2, sizeof(part2)) != 1 + || EVP_DigestSignUpdate(b, part2, sizeof(part2)) != 1; + } + if (err == 0) { + err = EVP_DigestSignFinal(a, sigA, &sigALen) != 1 + || EVP_DigestSignFinal(b, sigB, &sigBLen) != 1; + } + if (err == 0) { + err = test_rsa_dupctx_verify(pkey, mds[i], msg, sizeof(msg), + sigA, sigALen) != 0 + || test_rsa_dupctx_verify(pkey, mds[i], msg, sizeof(msg), + sigB, sigBLen) != 0; + if (err) { + PRINT_ERR_MSG("RSA dupctx sig verify failed"); + } + } + + EVP_MD_CTX_free(a); a = NULL; + EVP_MD_CTX_free(b); b = NULL; + } + + EVP_PKEY_free(pkey); + + return err; +} +#endif /* WP_HAVE_SHA512_224 && WP_HAVE_SHA512_256 */ + #endif /* WP_HAVE_RSA */ diff --git a/test/unit.c b/test/unit.c index 546d755d..7770de43 100644 --- a/test/unit.c +++ b/test/unit.c @@ -368,6 +368,9 @@ TEST_CASE test_case[] = { TEST_DECL(test_rsa_get_params, NULL), #endif TEST_DECL(test_rsa_sig_alg_id, NULL), +#if defined(WP_HAVE_SHA512_224) && defined(WP_HAVE_SHA512_256) + TEST_DECL(test_rsa_sha512_224_256_dupctx, NULL), +#endif TEST_DECL(test_rsa_pss_salt, NULL), TEST_DECL(test_rsa_pss_restrictions, NULL), TEST_DECL(test_rsa_load_key, NULL), diff --git a/test/unit.h b/test/unit.h index e0635a72..82dfc4e5 100644 --- a/test/unit.h +++ b/test/unit.h @@ -343,6 +343,9 @@ int test_rsa_pkey_keygen(void *data); int test_rsa_pkey_invalid_key_size(void *data); int test_rsa_get_params(void *data); int test_rsa_sig_alg_id(void *data); +#if defined(WP_HAVE_SHA512_224) && defined(WP_HAVE_SHA512_256) +int test_rsa_sha512_224_256_dupctx(void *data); +#endif int test_rsa_pss_salt(void *date); int test_rsa_pss_restrictions(void *data); From 2201d671eb1ea3b652c08ffb2b01c33eca1c36d7 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 6 Aug 2026 16:27:32 -0700 Subject: [PATCH 04/11] Fix bundle_exists 4xx classification, portable sort, dist manifest, CI wiring --- .github/workflows/fips-ready.yml | 14 +++ .github/workflows/nightly-fips-ready.yml | 13 +++ scripts/fetch-fips-ready.sh | 58 +++++++++--- scripts/include.am | 1 + scripts/test-fetch-fips-ready.sh | 108 ++++++++++++++++++++--- 5 files changed, 169 insertions(+), 25 deletions(-) diff --git a/.github/workflows/fips-ready.yml b/.github/workflows/fips-ready.yml index 8f2edd81..28c5f440 100644 --- a/.github/workflows/fips-ready.yml +++ b/.github/workflows/fips-ready.yml @@ -25,7 +25,21 @@ concurrency: # END OF COMMON SECTION jobs: + test_fetch_script: + if: github.event_name != 'pull_request' || github.event.pull_request.draft == false + name: Test fetch-fips-ready.sh + runs-on: ubuntu-22.04 + timeout-minutes: 5 + steps: + - name: Checkout wolfProvider + uses: actions/checkout@v4 + with: + fetch-depth: 1 + - name: Run network-free resolver tests + run: ./scripts/test-fetch-fips-ready.sh + discover_versions: + needs: test_fetch_script if: github.event_name != 'pull_request' || github.event.pull_request.draft == false uses: ./.github/workflows/_discover-versions.yml diff --git a/.github/workflows/nightly-fips-ready.yml b/.github/workflows/nightly-fips-ready.yml index 7f41e48a..c7c63e53 100644 --- a/.github/workflows/nightly-fips-ready.yml +++ b/.github/workflows/nightly-fips-ready.yml @@ -14,7 +14,20 @@ on: # No concurrency group: github.workflow is the caller's here, so any group collides with sibling calls. jobs: + test_fetch_script: + name: Test fetch-fips-ready.sh + runs-on: ubuntu-22.04 + timeout-minutes: 5 + steps: + - name: Checkout wolfProvider + uses: actions/checkout@v4 + with: + fetch-depth: 1 + - name: Run network-free resolver tests + run: ./scripts/test-fetch-fips-ready.sh + discover_versions: + needs: test_fetch_script uses: ./.github/workflows/_discover-versions.yml fips_ready_test: diff --git a/scripts/fetch-fips-ready.sh b/scripts/fetch-fips-ready.sh index 44e5fe00..3c1f0007 100755 --- a/scripts/fetch-fips-ready.sh +++ b/scripts/fetch-fips-ready.sh @@ -82,9 +82,20 @@ retry_out() { return 1 } +# Sorts newline-separated X.Y.Z version strings ascending. `sort -V` is a +# GNU extension and unavailable on some BSD/macOS sort builds; this script +# is documented for local (including macOS) use, so version ordering can't +# depend on it. Zero-pads each numeric field to a fixed width and sorts +# lexicographically instead, which every `sort` supports. +ver_sort() { + awk -F. '{ printf "%05d.%05d.%05d %s\n", $1, $2, $3, $0 }' \ + | sort \ + | awk '{ print $2 }' +} + # ver_ge A B -- true when A >= B ver_ge() { - [[ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -n1)" == "$2" ]] + [[ "$(printf '%s\n%s\n' "$2" "$1" | ver_sort | head -n1)" == "$2" ]] } get_page() { @@ -96,7 +107,7 @@ page_latest() { get_page \ | grep -o 'wolfssl-[0-9][0-9.]*-gplv3-fips-ready\.zip' \ | sed -E 's/^wolfssl-(.*)-gplv3-fips-ready\.zip$/\1/' \ - | sort -V | tail -n 1 + | ver_sort | tail -n 1 } page_sha256() { @@ -112,9 +123,12 @@ ls_wolfssl_stable() { | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' } -# Retries transient failures (timeout, 5xx, connection error) but treats a -# confirmed 4xx as a definitive "not hosted" with no retry -- conflating the -# two would silently drop a version on a network blip. +# Retries transient failures (timeout, 429, 5xx, connection error) but +# treats a confirmed 404/410 as definitive "not hosted" with no retry -- +# conflating the two would silently drop a version on a network blip or a +# rate limit. Returns 0 exists / 1 confirmed absent / 2 indeterminate (every +# attempt was transient) -- callers must not treat 1 and 2 the same, or an +# indeterminate result silently reads as "not hosted". bundle_exists() { local ver="$1" code attempt for attempt in 1 2 3; do @@ -125,7 +139,7 @@ bundle_exists() { "$(bundle_url "$ver")" 2>/dev/null) || code="" case "$code" in 200) return 0 ;; - 4??) return 1 ;; + 404|410) return 1 ;; *) if [[ "$attempt" -lt 3 ]]; then sleep $((attempt * 3)) @@ -135,7 +149,7 @@ bundle_exists() { done echo "fetch-fips-ready: could not confirm bundle $ver after retries" \ "(last status: ${code:-none})" >&2 - return 1 + return 2 } # Candidates are the union of upstream -stable tags and the advertised latest, @@ -143,13 +157,15 @@ bundle_exists() { # bundle, and 5.9.1 has a bundle the page never lists. list_versions() { local candidates ver found probe_dir + local -a indeterminate=() candidates=$(retry_out ls_wolfssl_stable) || { echo "fetch-fips-ready: could not list wolfSSL tags" >&2 return 1 } candidates="$candidates $(page_latest 2>/dev/null || true)" - candidates=$(printf '%s\n' "$candidates" | grep -E '^[0-9.]+$' | sort -V -u \ + candidates=$(printf '%s\n' "$candidates" | grep -E '^[0-9.]+$' | sort -u \ + | ver_sort \ | while read -r ver; do ver_ge "$ver" "$FLOOR" && echo "$ver"; done) [[ -n "$candidates" ]] || return 1 @@ -159,13 +175,33 @@ $(page_latest 2>/dev/null || true)" probe_dir=$(mktemp -d) trap 'rm -rf "$probe_dir"' RETURN for ver in $candidates; do - (bundle_exists "$ver" && touch "$probe_dir/$ver") & + ( + bundle_exists "$ver" + case "$?" in + 0) touch "$probe_dir/$ver.exists" ;; + 2) touch "$probe_dir/$ver.indeterminate" ;; + # 1 (confirmed absent): no marker, correctly omitted below. + esac + ) & done wait + # An indeterminate probe must fail the whole resolution, not just drop + # that one candidate -- a rate-limited or flaky bundle would otherwise + # silently vanish from a list that still reports success. + while IFS= read -r ver; do + indeterminate+=("$ver") + done < <(find "$probe_dir" -maxdepth 1 -name '*.indeterminate' \ + -exec basename {} .indeterminate \;) + if [[ "${#indeterminate[@]}" -gt 0 ]]; then + echo "fetch-fips-ready: could not determine availability for:" \ + "${indeterminate[*]}" >&2 + return 1 + fi + found="" - for ver in $(printf '%s\n' "$candidates" | sort -V); do - [[ -e "$probe_dir/$ver" ]] && found="$found$ver"$'\n' + for ver in $(printf '%s\n' "$candidates" | ver_sort); do + [[ -e "$probe_dir/$ver.exists" ]] && found="$found$ver"$'\n' done [[ -n "$found" ]] || return 1 diff --git a/scripts/include.am b/scripts/include.am index f192d304..40f58f64 100644 --- a/scripts/include.am +++ b/scripts/include.am @@ -1,5 +1,6 @@ dist_noinst_SCRIPTS += scripts/build-wolfprovider.sh dist_noinst_SCRIPTS += scripts/env-setup +dist_noinst_SCRIPTS += scripts/fetch-fips-ready.sh dist_noinst_SCRIPTS += scripts/patch-libcrypto-exports.sh dist_noinst_SCRIPTS += scripts/patch-openssl-fips.sh dist_noinst_SCRIPTS += scripts/resolve-ref.sh diff --git a/scripts/test-fetch-fips-ready.sh b/scripts/test-fetch-fips-ready.sh index 4edf3b24..80f0114d 100755 --- a/scripts/test-fetch-fips-ready.sh +++ b/scripts/test-fetch-fips-ready.sh @@ -118,7 +118,7 @@ EOF # --- bundle_exists: a confirmed 4xx is definitive -- no retry burned. --- test_bundle_exists_confirmed_404_no_retry() { - local d counter + local d counter rc d=$(mktemp -d) counter="${d}/count" cat > "${MOCKBIN}/curl" < "${MOCKBIN}/curl" < "${counter}" +if [[ "\$*" == *"-I"* && "\$*" == *"%{http_code}"* ]]; then + if [[ "\$n" -lt 2 ]]; then printf '429'; exit 0; fi + printf '200' + exit 0 +fi +exit 1 +EOF + chmod +x "${MOCKBIN}/curl" + + rc=0 + run_case "${d}" bundle_exists "9.9.9" || rc=$? + if [[ "${rc}" -eq 0 ]]; then + pass "bundle_exists retries a 429 instead of treating it as absent" + else + fail "bundle_exists returned ${rc} for a 429-then-200 sequence, want 0 (exists)" + fi + rm -rf "${d}" +} + +# --- bundle_exists: exhausts retries and reports INDETERMINATE (rc=2, not +# the same as confirmed-absent rc=1) when every attempt is transient. A +# caller that treats 1 and 2 the same silently drops a real bundle. --- test_bundle_exists_exhausts_on_persistent_transient() { - local d + local d rc d=$(mktemp -d) cat > "${MOCKBIN}/curl" <<'EOF' #!/usr/bin/env bash @@ -161,10 +195,12 @@ exit 1 EOF chmod +x "${MOCKBIN}/curl" - if run_case "${d}" bundle_exists "9.9.9" 2>/dev/null; then - fail "bundle_exists reported existence despite persistent transient failure" + rc=0 + run_case "${d}" bundle_exists "9.9.9" 2>/dev/null || rc=$? + if [[ "${rc}" -eq 2 ]]; then + pass "bundle_exists reports indeterminate (rc=2), distinct from confirmed absent" else - pass "bundle_exists exhausts retries and reports absent, not a crash" + fail "bundle_exists returned ${rc} for persistent transient failure, want 2" fi rm -rf "${d}" } @@ -247,6 +283,48 @@ EOF rm -rf "${d}" } +# --- list_versions: one candidate hosted, one persistently indeterminate +# (5xx/timeout throughout) -- the whole resolution must fail rather than +# silently publishing a list that quietly omits the flaky candidate. --- +test_list_versions_fails_on_indeterminate_candidate() { + local d rc + d=$(mktemp -d) + cat > "${MOCKBIN}/git" <<'EOF' +#!/usr/bin/env bash +if [[ "$1" == "ls-remote" ]]; then + cat < "${MOCKBIN}/curl" <<'EOF' +#!/usr/bin/env bash +if [[ "$*" == *"-I"* && "$*" == *"%{http_code}"* ]]; then + for a in "$@"; do + # 5.8.2 is persistently flaky (503 forever); 5.9.2 is healthy. + if [[ "$a" == *"5.8.2"* ]]; then printf '503'; exit 0; fi + done + printf '200' + exit 0 +fi +exit 1 +EOF + chmod +x "${MOCKBIN}/curl" + + rc=0 + run_case "${d}" list_versions 2>/dev/null || rc=$? + if [[ "${rc}" -ne 0 ]]; then + pass "list_versions fails when a real candidate is indeterminate" + else + fail "list_versions succeeded despite an indeterminate candidate (5.8.2 always 503)" + fi + rm -rf "${d}" +} + # Builds a valid zip fixture at $2 containing wolfssl-$1-gplv3-fips-ready/. make_fixture_zip() { local ver="$1" out="$2" stage @@ -378,9 +456,11 @@ EOF test_bundle_exists_retries_transient test_bundle_exists_confirmed_404_no_retry +test_bundle_exists_429_is_transient test_bundle_exists_exhausts_on_persistent_transient test_list_versions_filters_and_sorts test_list_versions_fails_when_tags_unavailable +test_list_versions_fails_on_indeterminate_candidate test_fetch_bundle_preserves_existing_on_download_failure test_fetch_bundle_checksum_mismatch_preserves_existing test_fetch_bundle_replaces_on_success From e848a26cea564a27d067637529fa13d6d99d7c94 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 6 Aug 2026 16:50:16 -0700 Subject: [PATCH 05/11] Fix errexit masking the indeterminate-bundle marker in list_versions --- scripts/fetch-fips-ready.sh | 10 ++++-- scripts/test-fetch-fips-ready.sh | 61 +++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/scripts/fetch-fips-ready.sh b/scripts/fetch-fips-ready.sh index 3c1f0007..6353408a 100755 --- a/scripts/fetch-fips-ready.sh +++ b/scripts/fetch-fips-ready.sh @@ -176,8 +176,14 @@ $(page_latest 2>/dev/null || true)" trap 'rm -rf "$probe_dir"' RETURN for ver in $candidates; do ( - bundle_exists "$ver" - case "$?" in + # `|| rc=$?` is load-bearing under `set -e`: an unguarded + # `bundle_exists "$ver"` returning 2 would kill this subshell + # before the case below ever runs, silently losing the + # indeterminate marker (and with it, the whole-resolution + # failure this is supposed to trigger). + rc=0 + bundle_exists "$ver" || rc=$? + case "$rc" in 0) touch "$probe_dir/$ver.exists" ;; 2) touch "$probe_dir/$ver.indeterminate" ;; # 1 (confirmed absent): no marker, correctly omitted below. diff --git a/scripts/test-fetch-fips-ready.sh b/scripts/test-fetch-fips-ready.sh index 80f0114d..a85908c7 100755 --- a/scripts/test-fetch-fips-ready.sh +++ b/scripts/test-fetch-fips-ready.sh @@ -79,7 +79,9 @@ run_case() { # DEST="$PWD", which would otherwise clobber a prefix assignment made # before the source builtin runs. DEST is consumed inside fns.sh, not # this file, hence the two shellcheck disables below. - # shellcheck disable=SC1090,SC2034 + # SC2030: the PATH override is deliberately scoped to this subshell, + # so each call gets an isolated mock PATH without polluting later tests. + # shellcheck disable=SC1090,SC2030,SC2034 ( PATH="${MOCKBIN}:${PATH}"; source "${FNS}"; DEST="${dest}"; "$@" ) } @@ -325,6 +327,62 @@ EOF rm -rf "${d}" } +# --- Entrypoint-level regression for the indeterminate-candidate case. +# Bash disables errexit checking for the ENTIRE call tree beneath a compound +# command that sits on the left side of ||/&&/if -- including subshells +# spawned deep inside a sourced function -- not just that top-level command. +# Every other test in this file calls list_versions/bundle_exists through +# run_case, which is itself always invoked as `run_case ... || rc=$?`; that +# masked a real errexit bug (a bare `bundle_exists "$ver"` inside +# list_versions' background probe subshell) that only showed up when the +# real script binary was run as a genuine, unconditioned subprocess. This +# test exercises exactly that: `bash "$TARGET" --list`, not sourced, not +# wrapped in an if/||, so it has the same errexit semantics production CI +# actually gets. --- +test_entrypoint_list_fails_on_indeterminate_candidate() { + local d out status + d=$(mktemp -d) + cat > "${MOCKBIN}/git" <<'EOF' +#!/usr/bin/env bash +if [[ "$1" == "ls-remote" ]]; then + cat < "${MOCKBIN}/curl" <<'EOF' +#!/usr/bin/env bash +if [[ "$*" == *"-I"* && "$*" == *"%{http_code}"* ]]; then + for a in "$@"; do + # 5.8.2 is persistently flaky (503 forever); 5.9.2 is healthy. + if [[ "$a" == *"5.8.2"* ]]; then printf '503'; exit 0; fi + done + printf '200' + exit 0 +fi +exit 1 +EOF + chmod +x "${MOCKBIN}/curl" + + status=0 + # SC2031: the PATH override only needs to reach the `bash "$TARGET"` + # call on this same line -- it isn't meant to escape the subshell. + # shellcheck disable=SC2031 + out=$(cd "${d}" && PATH="${MOCKBIN}:${PATH}" bash "${TARGET}" --list \ + --floor 5.8.2 2>&1) || status=$? + + if [[ "${status}" -ne 0 ]]; then + pass "entrypoint --list fails on an indeterminate candidate (real subprocess, no errexit masking)" + else + fail "entrypoint --list exited 0 despite an indeterminate candidate; got: [${out}]" + fi + rm -rf "${d}" +} + # Builds a valid zip fixture at $2 containing wolfssl-$1-gplv3-fips-ready/. make_fixture_zip() { local ver="$1" out="$2" stage @@ -461,6 +519,7 @@ test_bundle_exists_exhausts_on_persistent_transient test_list_versions_filters_and_sorts test_list_versions_fails_when_tags_unavailable test_list_versions_fails_on_indeterminate_candidate +test_entrypoint_list_fails_on_indeterminate_candidate test_fetch_bundle_preserves_existing_on_download_failure test_fetch_bundle_checksum_mismatch_preserves_existing test_fetch_bundle_replaces_on_success From 193c4a98d73058ca3535a339e88bbbc01ec2a582 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 6 Aug 2026 17:12:05 -0700 Subject: [PATCH 06/11] Fail loudly on degraded FIPS discovery, add v7 tag mapping, split dupctx tests --- .github/workflows/_discover-versions.yml | 16 +++- .github/workflows/fips-ready.yml | 19 +++- .github/workflows/nightly-fips-ready.yml | 15 +++- .github/workflows/perf-regression.yml | 17 +++- scripts/utils-wolfssl.sh | 3 + test/test_rsa.c | 108 +++++++++++++---------- test/unit.c | 7 +- test/unit.h | 7 +- 8 files changed, 137 insertions(+), 55 deletions(-) diff --git a/.github/workflows/_discover-versions.yml b/.github/workflows/_discover-versions.yml index ab2a884a..2e7de215 100644 --- a/.github/workflows/_discover-versions.yml +++ b/.github/workflows/_discover-versions.yml @@ -39,6 +39,9 @@ on: fips_ready_all_array: description: 'JSON array of every hosted FIPS Ready bundle version at or above the floor. Used by the nightly sweep.' value: ${{ jobs.discover.outputs.fips_ready_all_array }} + fips_ready_resolved: + description: '"true" if FIPS Ready bundle discovery succeeded, "false" if the outputs above are a degraded floor-only fallback. FIPS-specific callers must fail on "false" rather than silently testing a stale matrix.' + value: ${{ jobs.discover.outputs.fips_ready_resolved }} jobs: discover: @@ -61,6 +64,7 @@ jobs: fips_ready_latest: ${{ steps.resolve.outputs.fips_ready_latest }} fips_ready_pr_array: ${{ steps.resolve.outputs.fips_ready_pr_array }} fips_ready_all_array: ${{ steps.resolve.outputs.fips_ready_all_array }} + fips_ready_resolved: ${{ steps.resolve.outputs.fips_ready_resolved }} steps: - name: Checkout wolfProvider uses: actions/checkout@v4 @@ -189,10 +193,19 @@ jobs: # FIPS Ready: every bundle hosted on wolfssl.com at or above the floor. # Floor 5.8.2 -- the oldest bundle wolfProvider is tested against. FR_FLOOR="5.8.2" + FR_OK=true FR_ALL=$(./scripts/fetch-fips-ready.sh --list --floor "$FR_FLOOR") || true if [ -z "${FR_ALL:-}" ]; then - echo "::warning::Could not list FIPS Ready bundles; falling back to $FR_FLOOR" + # This job is shared by ~40 workflows that never touch FIPS + # outputs, so a resolution failure here must not fail the whole + # job -- but silently falling back to a one-version floor would + # let fips-ready.yml / nightly-fips-ready.yml / perf-regression.yml + # report green while testing a stale, near-empty matrix. FR_OK + # lets those FIPS-specific callers fail loudly instead; every + # other caller of this workflow ignores it. + echo "::warning::Could not list FIPS Ready bundles; falling back to $FR_FLOOR (FIPS callers will fail this run)" FR_ALL="$FR_FLOOR" + FR_OK=false fi FR_ALL_JSON=$(printf '%s\n' "$FR_ALL" | jq -R . | jq -s -c .) FR_LATEST=$(echo "$FR_ALL" | tail -n 1) @@ -229,4 +242,5 @@ jobs: echo "fips_ready_latest=$FR_LATEST" echo "fips_ready_pr_array=$FR_PR_JSON" echo "fips_ready_all_array=$FR_ALL_JSON" + echo "fips_ready_resolved=$FR_OK" } >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/fips-ready.yml b/.github/workflows/fips-ready.yml index 28c5f440..5522013d 100644 --- a/.github/workflows/fips-ready.yml +++ b/.github/workflows/fips-ready.yml @@ -43,9 +43,26 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.draft == false uses: ./.github/workflows/_discover-versions.yml - fips_ready_test: + # A resolution failure makes discover_versions fall back to testing only + # the floor version -- fine for the ~40 unrelated workflows that share + # that job, but here it would let this workflow report green while + # silently skipping most of the FIPS Ready matrix. Fail loudly instead. + require_fips_resolved: needs: discover_versions if: github.event_name != 'pull_request' || github.event.pull_request.draft == false + name: Require FIPS Ready bundle discovery succeeded + runs-on: ubuntu-22.04 + timeout-minutes: 2 + steps: + - name: Fail if discovery fell back to the floor + if: needs.discover_versions.outputs.fips_ready_resolved != 'true' + run: | + echo "::error::FIPS Ready bundle discovery failed and fell back to a single floor version; refusing to report success on a silently degraded matrix. See the discover_versions job log." + exit 1 + + fips_ready_test: + needs: [discover_versions, require_fips_resolved] + if: github.event_name != 'pull_request' || github.event.pull_request.draft == false name: FIPS Ready ${{ matrix.wolfssl_bundle_ref }} (${{ matrix.openssl_ref }}) runs-on: ubuntu-22.04 permissions: diff --git a/.github/workflows/nightly-fips-ready.yml b/.github/workflows/nightly-fips-ready.yml index c7c63e53..b927733a 100644 --- a/.github/workflows/nightly-fips-ready.yml +++ b/.github/workflows/nightly-fips-ready.yml @@ -30,8 +30,21 @@ jobs: needs: test_fetch_script uses: ./.github/workflows/_discover-versions.yml - fips_ready_test: + # See fips-ready.yml for why this can't be left to a silent floor fallback. + require_fips_resolved: needs: discover_versions + name: Require FIPS Ready bundle discovery succeeded + runs-on: ubuntu-22.04 + timeout-minutes: 2 + steps: + - name: Fail if discovery fell back to the floor + if: needs.discover_versions.outputs.fips_ready_resolved != 'true' + run: | + echo "::error::FIPS Ready bundle discovery failed and fell back to a single floor version; refusing to report success on a silently degraded matrix. See the discover_versions job log." + exit 1 + + fips_ready_test: + needs: [discover_versions, require_fips_resolved] name: Nightly FIPS Ready ${{ matrix.wolfssl_bundle_ref }} (${{ matrix.openssl_ref }}) runs-on: ubuntu-22.04 permissions: diff --git a/.github/workflows/perf-regression.yml b/.github/workflows/perf-regression.yml index bfcd17d4..bac4179d 100644 --- a/.github/workflows/perf-regression.yml +++ b/.github/workflows/perf-regression.yml @@ -47,8 +47,23 @@ jobs: path: perf_outputs/results.json retention-days: 7 - perf_fips: + # A discovery failure falls back to the 5.8.2 floor -- without this gate, + # perf_fips would silently benchmark a stale bundle under the "latest" + # label instead of failing. See fips-ready.yml for the fuller rationale. + require_fips_resolved: needs: discover_versions + name: Require FIPS Ready bundle discovery succeeded + runs-on: ubuntu-22.04 + timeout-minutes: 2 + steps: + - name: Fail if discovery fell back to the floor + if: needs.discover_versions.outputs.fips_ready_resolved != 'true' + run: | + echo "::error::FIPS Ready bundle discovery failed and fell back to a single floor version; refusing to benchmark a stale bundle under the 'latest' label." + exit 1 + + perf_fips: + needs: [discover_versions, require_fips_resolved] name: Overhead regression (FIPS) runs-on: ubuntu-22.04 timeout-minutes: 30 diff --git a/scripts/utils-wolfssl.sh b/scripts/utils-wolfssl.sh index 48b5c204..caf79726 100644 --- a/scripts/utils-wolfssl.sh +++ b/scripts/utils-wolfssl.sh @@ -203,6 +203,9 @@ install_wolfssl() { v6.*|linuxv6.*) fips_configure_arg="v6" ;; + v7.*|linuxv7.*) + fips_configure_arg="v7" + ;; *) # For ready, v5, v6, or other tags, use as-is fips_configure_arg="$fips_tag" diff --git a/test/test_rsa.c b/test/test_rsa.c index 4357abe8..5659b255 100644 --- a/test/test_rsa.c +++ b/test/test_rsa.c @@ -3361,13 +3361,19 @@ int test_rsa_sig_alg_id(void *data) return err; } -#if defined(WP_HAVE_SHA512_224) && defined(WP_HAVE_SHA512_256) +#if defined(WP_HAVE_SHA512_224) || defined(WP_HAVE_SHA512_256) /* EVP_MD_CTX_copy_ex on a partial RSA digest_sign accumulator, using * SHA512-224/SHA512-256 to exercise the wp_hash_copy() branches those two * digests only gained in FIPS v8 (older FIPS modules never define * WP_HAVE_SHA512_224/256, so this path is skipped there). Both the * original and duplicated contexts must produce signatures that verify - * under the original key. */ + * under the original key. + * + * WP_HAVE_SHA512_224 and WP_HAVE_SHA512_256 are independently defined + * (wolfSSL toggles WOLFSSL_NOSHA512_224/256 separately), and wp_hash_copy() + * has independent branches for each, so this is two separately-gated + * one-digest tests rather than one combined test -- a build enabling only + * one of the two must still exercise its copy branch. */ static int test_rsa_dupctx_verify(EVP_PKEY *pkey, const char *md, const unsigned char *msg, size_t msgLen, unsigned char *sig, size_t sigLen) @@ -3392,21 +3398,19 @@ static int test_rsa_dupctx_verify(EVP_PKEY *pkey, const char *md, return err; } -int test_rsa_sha512_224_256_dupctx(void *data) +static int test_rsa_dupctx_one_digest(const char *md, void *data) { static const unsigned char part1[16] = "rsa-dupctx-part1"; static const unsigned char part2[16] = "rsa-dupctx-part2"; - const char *mds[] = { "SHA512-224", "SHA512-256" }; int err = 0; - size_t i; EVP_PKEY *pkey = NULL; const unsigned char *p = rsa_key_der_2048; EVP_MD_CTX *a = NULL; EVP_MD_CTX *b = NULL; unsigned char sigA[256]; unsigned char sigB[256]; - size_t sigALen; - size_t sigBLen; + size_t sigALen = sizeof(sigA); + size_t sigBLen = sizeof(sigB); unsigned char msg[32]; (void)data; @@ -3414,57 +3418,67 @@ int test_rsa_sha512_224_256_dupctx(void *data) XMEMCPY(msg, part1, sizeof(part1)); XMEMCPY(msg + sizeof(part1), part2, sizeof(part2)); + PRINT_MSG("RSA dupctx %s", md); + pkey = d2i_PrivateKey(EVP_PKEY_RSA, NULL, &p, sizeof(rsa_key_der_2048)); err = pkey == NULL; - for (i = 0; (err == 0) && (i < sizeof(mds) / sizeof(mds[0])); i++) { - PRINT_MSG("RSA dupctx %s", mds[i]); - - sigALen = sizeof(sigA); - sigBLen = sizeof(sigB); - + if (err == 0) { a = EVP_MD_CTX_new(); err = (a == NULL); - if (err == 0) { - err = EVP_DigestSignInit_ex(a, NULL, mds[i], wpLibCtx, NULL, - pkey, NULL) != 1; - } - if (err == 0) { - err = EVP_DigestSignUpdate(a, part1, sizeof(part1)) != 1; - } - if (err == 0) { - b = EVP_MD_CTX_new(); - err = (b == NULL); - } - if (err == 0) { - err = EVP_MD_CTX_copy_ex(b, a) != 1; - } - if (err == 0) { - err = EVP_DigestSignUpdate(a, part2, sizeof(part2)) != 1 - || EVP_DigestSignUpdate(b, part2, sizeof(part2)) != 1; - } - if (err == 0) { - err = EVP_DigestSignFinal(a, sigA, &sigALen) != 1 - || EVP_DigestSignFinal(b, sigB, &sigBLen) != 1; - } - if (err == 0) { - err = test_rsa_dupctx_verify(pkey, mds[i], msg, sizeof(msg), - sigA, sigALen) != 0 - || test_rsa_dupctx_verify(pkey, mds[i], msg, sizeof(msg), - sigB, sigBLen) != 0; - if (err) { - PRINT_ERR_MSG("RSA dupctx sig verify failed"); - } + } + if (err == 0) { + err = EVP_DigestSignInit_ex(a, NULL, md, wpLibCtx, NULL, pkey, + NULL) != 1; + } + if (err == 0) { + err = EVP_DigestSignUpdate(a, part1, sizeof(part1)) != 1; + } + if (err == 0) { + b = EVP_MD_CTX_new(); + err = (b == NULL); + } + if (err == 0) { + err = EVP_MD_CTX_copy_ex(b, a) != 1; + } + if (err == 0) { + err = EVP_DigestSignUpdate(a, part2, sizeof(part2)) != 1 + || EVP_DigestSignUpdate(b, part2, sizeof(part2)) != 1; + } + if (err == 0) { + err = EVP_DigestSignFinal(a, sigA, &sigALen) != 1 + || EVP_DigestSignFinal(b, sigB, &sigBLen) != 1; + } + if (err == 0) { + err = test_rsa_dupctx_verify(pkey, md, msg, sizeof(msg), sigA, + sigALen) != 0 + || test_rsa_dupctx_verify(pkey, md, msg, sizeof(msg), sigB, + sigBLen) != 0; + if (err) { + PRINT_ERR_MSG("RSA dupctx sig verify failed"); } - - EVP_MD_CTX_free(a); a = NULL; - EVP_MD_CTX_free(b); b = NULL; } + EVP_MD_CTX_free(a); + EVP_MD_CTX_free(b); EVP_PKEY_free(pkey); return err; } -#endif /* WP_HAVE_SHA512_224 && WP_HAVE_SHA512_256 */ +#endif /* WP_HAVE_SHA512_224 || WP_HAVE_SHA512_256 */ + +#ifdef WP_HAVE_SHA512_224 +int test_rsa_sha512_224_dupctx(void *data) +{ + return test_rsa_dupctx_one_digest("SHA512-224", data); +} +#endif /* WP_HAVE_SHA512_224 */ + +#ifdef WP_HAVE_SHA512_256 +int test_rsa_sha512_256_dupctx(void *data) +{ + return test_rsa_dupctx_one_digest("SHA512-256", data); +} +#endif /* WP_HAVE_SHA512_256 */ #endif /* WP_HAVE_RSA */ diff --git a/test/unit.c b/test/unit.c index 7770de43..8eca1d19 100644 --- a/test/unit.c +++ b/test/unit.c @@ -368,8 +368,11 @@ TEST_CASE test_case[] = { TEST_DECL(test_rsa_get_params, NULL), #endif TEST_DECL(test_rsa_sig_alg_id, NULL), -#if defined(WP_HAVE_SHA512_224) && defined(WP_HAVE_SHA512_256) - TEST_DECL(test_rsa_sha512_224_256_dupctx, NULL), +#ifdef WP_HAVE_SHA512_224 + TEST_DECL(test_rsa_sha512_224_dupctx, NULL), +#endif +#ifdef WP_HAVE_SHA512_256 + TEST_DECL(test_rsa_sha512_256_dupctx, NULL), #endif TEST_DECL(test_rsa_pss_salt, NULL), TEST_DECL(test_rsa_pss_restrictions, NULL), diff --git a/test/unit.h b/test/unit.h index 82dfc4e5..f93c2949 100644 --- a/test/unit.h +++ b/test/unit.h @@ -343,8 +343,11 @@ int test_rsa_pkey_keygen(void *data); int test_rsa_pkey_invalid_key_size(void *data); int test_rsa_get_params(void *data); int test_rsa_sig_alg_id(void *data); -#if defined(WP_HAVE_SHA512_224) && defined(WP_HAVE_SHA512_256) -int test_rsa_sha512_224_256_dupctx(void *data); +#ifdef WP_HAVE_SHA512_224 +int test_rsa_sha512_224_dupctx(void *data); +#endif +#ifdef WP_HAVE_SHA512_256 +int test_rsa_sha512_256_dupctx(void *data); #endif int test_rsa_pss_salt(void *date); int test_rsa_pss_restrictions(void *data); From 770167db47973bbafc78b443a65e538eb0afd644 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 7 Aug 2026 09:32:05 -0700 Subject: [PATCH 07/11] Remove em-dash and shorten long comments --- .github/workflows/README.md | 10 +-- .github/workflows/_discover-versions.yml | 13 ++-- .github/workflows/fips-ready.yml | 5 +- .github/workflows/perf-regression.yml | 6 +- docs/FIPS_INTEGRATION_GUIDE.md | 6 +- scripts/fetch-fips-ready.sh | 37 +++++------ scripts/test-fetch-fips-ready.sh | 82 +++++++++++------------- test/test_rsa.c | 15 +---- 8 files changed, 72 insertions(+), 102 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 41057fbf..489571ef 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -30,7 +30,7 @@ ready_for_review) and on every push to `master`, `main`, or | `simple.yml` | Builds wolfProvider against the matrix of supported wolfSSL + OpenSSL refs and runs `make check`. The baseline "did anything obvious break" check. | | `smoke-test.yml` | Minimal end-to-end: build, load the provider into stock OpenSSL, run `openssl list -providers` and a handful of `openssl` subcommands. Catches link-time and provider-registration regressions. | | `cmdline.yml` | Runs `scripts/cmd_test/do-cmd-tests.sh` — exercises every `openssl` CLI verb (genrsa, pkeyutl, enc, dgst, …) through wolfProvider. | -| `fips-ready.yml` | Downloads FIPS Ready bundles from wolfssl.com (via `scripts/fetch-fips-ready.sh`) and builds wolfSSL with `--enable-fips=ready` against each. The matrix is resolved at run time from `_discover-versions.yml`'s `fips_ready_pr_array` — the back-compat anchors plus whatever bundle is newest — so a new FIPS Ready release is covered the day it ships. Note `ready` is not a fixed FIPS version: it was v7 through bundle 5.9.1 and is v8 from 5.9.2 on. | +| `fips-ready.yml` | Downloads FIPS Ready bundles from wolfssl.com (via `scripts/fetch-fips-ready.sh`) and builds wolfSSL with `--enable-fips=ready` against each. Matrix resolved at run time from `_discover-versions.yml`'s `fips_ready_pr_array`: the back-compat anchors plus the newest bundle. `ready` is not a fixed FIPS version: v7 through bundle 5.9.1, v8 from 5.9.2 on. | | `seed-src.yml` | Builds with `--enable-seed-src` (entropy seed source variant) and `-DWP_TEST_SECCOMP_SANDBOX`, then runs the unit tests including the OpenSSH fork+seccomp-sandbox regression suite. | | `multi-compiler.yml` | Cross-compiler sweep: gcc-9 through gcc-14 and clang-12 through latest. Catches toolchain-specific warnings / UB. | | `codespell.yml` | Spell-check on tracked source. `*.patch` is excluded because OSP patches mirror upstream source whose original spelling we shouldn't silently rewrite. | @@ -59,10 +59,10 @@ push and dominate the merge queue. Nightly is the right cadence for catching regressions in third-party integration that wouldn't show up in our unit tests. -`nightly-fips-ready.yml` rides along out-of-wave. It runs the same steps -as `fips-ready.yml` but over `fips_ready_all_array` — every FIPS Ready -bundle hosted on wolfssl.com at or above the 5.8.2 floor — restoring the -versions the PR set leaves out. +`nightly-fips-ready.yml` rides along out-of-wave, running the same steps +as `fips-ready.yml` over `fips_ready_all_array`: every FIPS Ready bundle +hosted at or above the 5.8.2 floor, restoring the versions the PR set +leaves out. ### Running a nightly job on a PR (label toggles) diff --git a/.github/workflows/_discover-versions.yml b/.github/workflows/_discover-versions.yml index 2e7de215..a3ec7070 100644 --- a/.github/workflows/_discover-versions.yml +++ b/.github/workflows/_discover-versions.yml @@ -191,18 +191,15 @@ jobs: OSSL_LATEST=$(echo "$OSSL_ALL" | tail -n 1) # FIPS Ready: every bundle hosted on wolfssl.com at or above the floor. - # Floor 5.8.2 -- the oldest bundle wolfProvider is tested against. + # Floor 5.8.2: the oldest bundle wolfProvider is tested against. FR_FLOOR="5.8.2" FR_OK=true FR_ALL=$(./scripts/fetch-fips-ready.sh --list --floor "$FR_FLOOR") || true if [ -z "${FR_ALL:-}" ]; then - # This job is shared by ~40 workflows that never touch FIPS - # outputs, so a resolution failure here must not fail the whole - # job -- but silently falling back to a one-version floor would - # let fips-ready.yml / nightly-fips-ready.yml / perf-regression.yml - # report green while testing a stale, near-empty matrix. FR_OK - # lets those FIPS-specific callers fail loudly instead; every - # other caller of this workflow ignores it. + # ~40 workflows share this job for wolfssl/openssl outputs only, + # so a FIPS resolution failure must not fail the whole job. FR_OK + # lets the FIPS-specific callers fail loudly on their own instead + # of reporting green on a stale, near-empty fallback matrix. echo "::warning::Could not list FIPS Ready bundles; falling back to $FR_FLOOR (FIPS callers will fail this run)" FR_ALL="$FR_FLOOR" FR_OK=false diff --git a/.github/workflows/fips-ready.yml b/.github/workflows/fips-ready.yml index 5522013d..59f1f596 100644 --- a/.github/workflows/fips-ready.yml +++ b/.github/workflows/fips-ready.yml @@ -44,9 +44,8 @@ jobs: uses: ./.github/workflows/_discover-versions.yml # A resolution failure makes discover_versions fall back to testing only - # the floor version -- fine for the ~40 unrelated workflows that share - # that job, but here it would let this workflow report green while - # silently skipping most of the FIPS Ready matrix. Fail loudly instead. + # the floor version. Fine for the ~40 unrelated workflows sharing that + # job, but here it would report green while skipping most of the matrix. require_fips_resolved: needs: discover_versions if: github.event_name != 'pull_request' || github.event.pull_request.draft == false diff --git a/.github/workflows/perf-regression.yml b/.github/workflows/perf-regression.yml index bac4179d..511079e5 100644 --- a/.github/workflows/perf-regression.yml +++ b/.github/workflows/perf-regression.yml @@ -47,9 +47,9 @@ jobs: path: perf_outputs/results.json retention-days: 7 - # A discovery failure falls back to the 5.8.2 floor -- without this gate, - # perf_fips would silently benchmark a stale bundle under the "latest" - # label instead of failing. See fips-ready.yml for the fuller rationale. + # Without this gate, a discovery failure (falling back to the 5.8.2 floor) + # would let perf_fips silently benchmark a stale bundle labeled "latest". + # See fips-ready.yml for the fuller rationale. require_fips_resolved: needs: discover_versions name: Require FIPS Ready bundle discovery succeeded diff --git a/docs/FIPS_INTEGRATION_GUIDE.md b/docs/FIPS_INTEGRATION_GUIDE.md index 2aa24dfb..579a526c 100644 --- a/docs/FIPS_INTEGRATION_GUIDE.md +++ b/docs/FIPS_INTEGRATION_GUIDE.md @@ -151,13 +151,13 @@ certificate, so the module version it selects moves with the bundle: | FIPS Ready bundle | `--enable-fips=ready` gives | |-------------------|-----------------------------| -| 5.6.6 – 5.7.0 | `HAVE_FIPS_VERSION=5` | -| 5.7.2 – 5.9.1 | `HAVE_FIPS_VERSION=7` | +| 5.6.6 to 5.7.0 | `HAVE_FIPS_VERSION=5` | +| 5.7.2 to 5.9.1 | `HAVE_FIPS_VERSION=7` | | 5.9.2 and newer | `HAVE_FIPS_VERSION=8` | When you need a stable target, pin a generation with the alias the bundle ships rather than `ready`: `v5-ready` on the older bundles, `v6-ready` on -5.7.2 – 5.9.1, `v7-ready` on 5.9.2 and newer. +5.7.2 to 5.9.1, `v7-ready` on 5.9.2 and newer. The FIPS Ready bundles are published as `https://www.wolfssl.com/wolfssl--gplv3-fips-ready.zip`. diff --git a/scripts/fetch-fips-ready.sh b/scripts/fetch-fips-ready.sh index 6353408a..a77aa877 100755 --- a/scripts/fetch-fips-ready.sh +++ b/scripts/fetch-fips-ready.sh @@ -82,18 +82,16 @@ retry_out() { return 1 } -# Sorts newline-separated X.Y.Z version strings ascending. `sort -V` is a -# GNU extension and unavailable on some BSD/macOS sort builds; this script -# is documented for local (including macOS) use, so version ordering can't -# depend on it. Zero-pads each numeric field to a fixed width and sorts -# lexicographically instead, which every `sort` supports. +# Sorts newline-separated X.Y.Z version strings ascending without `sort -V`, +# a GNU extension this script (used on macOS too) can't rely on. Zero-pads +# each numeric field so plain lexicographic sort orders them correctly. ver_sort() { awk -F. '{ printf "%05d.%05d.%05d %s\n", $1, $2, $3, $0 }' \ | sort \ | awk '{ print $2 }' } -# ver_ge A B -- true when A >= B +# ver_ge A B: true when A >= B ver_ge() { [[ "$(printf '%s\n%s\n' "$2" "$1" | ver_sort | head -n1)" == "$2" ]] } @@ -123,12 +121,9 @@ ls_wolfssl_stable() { | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' } -# Retries transient failures (timeout, 429, 5xx, connection error) but -# treats a confirmed 404/410 as definitive "not hosted" with no retry -- -# conflating the two would silently drop a version on a network blip or a -# rate limit. Returns 0 exists / 1 confirmed absent / 2 indeterminate (every -# attempt was transient) -- callers must not treat 1 and 2 the same, or an -# indeterminate result silently reads as "not hosted". +# Retries transient failures (timeout, 429, 5xx, connection error); only a +# confirmed 404/410 means "not hosted" and skips retry. Returns 0 exists, +# 1 confirmed absent, 2 indeterminate. Callers must not treat 1 and 2 alike. bundle_exists() { local ver="$1" code attempt for attempt in 1 2 3; do @@ -169,18 +164,16 @@ $(page_latest 2>/dev/null || true)" | while read -r ver; do ver_ge "$ver" "$FLOOR" && echo "$ver"; done) [[ -n "$candidates" ]] || return 1 - # This runs inside the shared _discover-versions.yml job, which every - # caller pays for even when it never touches FIPS outputs -- probe - # candidates in parallel so N versions costs one round trip, not N. + # Runs inside the shared _discover-versions.yml job that every caller + # pays for even without touching FIPS outputs, so probe candidates in + # parallel: N versions costs one round trip, not N. probe_dir=$(mktemp -d) trap 'rm -rf "$probe_dir"' RETURN for ver in $candidates; do ( - # `|| rc=$?` is load-bearing under `set -e`: an unguarded - # `bundle_exists "$ver"` returning 2 would kill this subshell - # before the case below ever runs, silently losing the - # indeterminate marker (and with it, the whole-resolution - # failure this is supposed to trigger). + # `|| rc=$?` is load-bearing under `set -e`: an unguarded call + # returning 2 kills this subshell before the case below runs, + # silently losing the indeterminate marker. rc=0 bundle_exists "$ver" || rc=$? case "$rc" in @@ -193,8 +186,8 @@ $(page_latest 2>/dev/null || true)" wait # An indeterminate probe must fail the whole resolution, not just drop - # that one candidate -- a rate-limited or flaky bundle would otherwise - # silently vanish from a list that still reports success. + # that candidate: otherwise a flaky bundle vanishes silently from a + # list that still reports success. while IFS= read -r ver; do indeterminate+=("$ver") done < <(find "$probe_dir" -maxdepth 1 -name '*.indeterminate' \ diff --git a/scripts/test-fetch-fips-ready.sh b/scripts/test-fetch-fips-ready.sh index a85908c7..364cc891 100755 --- a/scripts/test-fetch-fips-ready.sh +++ b/scripts/test-fetch-fips-ready.sh @@ -18,8 +18,8 @@ # along with wolfProvider. If not, see . # # Isolated, network-free tests for scripts/fetch-fips-ready.sh: mocks curl -# and git so the resolver and extraction logic -- especially its destructive -# and failure paths -- run deterministically in CI without hitting +# and git so the resolver and extraction logic, especially its destructive +# and failure paths, run deterministically in CI without hitting # wolfssl.com or GitHub. set -euo pipefail @@ -38,11 +38,11 @@ trap cleanup EXIT mkdir -p "${MOCKBIN}" -# Source only fetch-fips-ready.sh's function definitions -- not its argv -# parser or the case-dispatch at the bottom, both of which would run -# immediately (and call exit) if sourced as-is. Boundaries are located by -# marker pattern rather than hardcoded line numbers so this test doesn't -# silently go stale as the script grows. +# Source only fetch-fips-ready.sh's function definitions, not its argv +# parser or the case-dispatch at the bottom (both would run immediately, +# and call exit, if sourced as-is). Boundaries are located by marker +# pattern, not hardcoded line numbers, so this stays in sync as the +# script grows. # shellcheck disable=SC2016 # single-quoted on purpose: matching a literal # '$' in fetch-fips-ready.sh's source, not expanding a variable here. while_line=$(grep -n '^while \[\[ \$# -gt 0' "${TARGET}" | head -n1 | cut -d: -f1) @@ -51,7 +51,7 @@ bundle_url_line=$(grep -n '^bundle_url()' "${TARGET}" | head -n1 | cut -d: -f1) case_line=$(grep -n '^case "\$MODE"' "${TARGET}" | head -n1 | cut -d: -f1) if [[ -z "${while_line}" || -z "${bundle_url_line}" || -z "${case_line}" ]]; then echo "test-fetch-fips-ready: could not locate expected markers in" \ - "${TARGET} -- has its structure changed?" >&2 + "${TARGET}; has its structure changed?" >&2 exit 1 fi { @@ -85,8 +85,8 @@ run_case() { ( PATH="${MOCKBIN}:${PATH}"; source "${FNS}"; DEST="${dest}"; "$@" ) } -# --- bundle_exists: retries a transient failure (connection error, no HTTP -# response) and succeeds once the server recovers. --- +# bundle_exists: retries a transient failure (connection error, no HTTP +# response) and succeeds once the server recovers. test_bundle_exists_retries_transient() { local d counter d=$(mktemp -d) @@ -118,7 +118,7 @@ EOF rm -rf "${d}" } -# --- bundle_exists: a confirmed 4xx is definitive -- no retry burned. --- +# bundle_exists: a confirmed 4xx is definitive, no retry burned. test_bundle_exists_confirmed_404_no_retry() { local d counter rc d=$(mktemp -d) @@ -149,10 +149,8 @@ EOF rm -rf "${d}" } -# --- bundle_exists: a 429 (rate limit) is transient, NOT a confirmed -# absence -- conflating the two was the exact regression a prior review -# round flagged (HIGH: 4xx classified all 4xx, including 429/408, as -# permanently absent with no retry). --- +# bundle_exists: a 429 (rate limit) is transient, not a confirmed absence. +# A prior review round flagged classifying all 4xx as permanently absent. test_bundle_exists_429_is_transient() { local d counter rc d=$(mktemp -d) @@ -182,9 +180,8 @@ EOF rm -rf "${d}" } -# --- bundle_exists: exhausts retries and reports INDETERMINATE (rc=2, not -# the same as confirmed-absent rc=1) when every attempt is transient. A -# caller that treats 1 and 2 the same silently drops a real bundle. --- +# bundle_exists: exhausts retries and reports indeterminate (rc=2), not +# confirmed-absent (rc=1), when every attempt is transient. test_bundle_exists_exhausts_on_persistent_transient() { local d rc d=$(mktemp -d) @@ -207,8 +204,8 @@ EOF rm -rf "${d}" } -# --- list_versions: unions git tags with the page's advertised latest, -# applies the floor, and only keeps versions that probe as hosted. --- +# list_versions: unions git tags with the page's advertised latest, +# applies the floor, and only keeps versions that probe as hosted. test_list_versions_filters_and_sorts() { local d html d=$(mktemp -d) @@ -234,7 +231,7 @@ EOF cat > "${MOCKBIN}/curl" <&1) || status=$? @@ -393,8 +383,8 @@ make_fixture_zip() { rm -rf "${stage}" } -# --- fetch_bundle: a download failure must NOT destroy a bundle that was -# already at the destination (the bug the HIGH finding flagged). --- +# fetch_bundle: a download failure must not destroy a bundle already at +# the destination (the bug the HIGH finding flagged). test_fetch_bundle_preserves_existing_on_download_failure() { local d d=$(mktemp -d) @@ -428,8 +418,8 @@ EOF rm -rf "${d}" } -# --- fetch_bundle: a checksum mismatch must also preserve whatever was -# already there, not just a hard network failure. --- +# fetch_bundle: a checksum mismatch must also preserve whatever was +# already there, not just a hard network failure. test_fetch_bundle_checksum_mismatch_preserves_existing() { local d html zip d=$(mktemp -d) @@ -470,8 +460,8 @@ EOF rm -rf "${d}" } -# --- fetch_bundle: a verified, successful fetch DOES replace whatever was -# there -- the fix must not make replacement impossible, only safe. --- +# fetch_bundle: a verified, successful fetch does replace whatever was +# there; the fix must not make replacement impossible, only safe. test_fetch_bundle_replaces_on_success() { local d zip out d=$(mktemp -d) @@ -492,7 +482,7 @@ for a in "\$@"; do exit 0 fi done -# No SHA256 advertised for this version -- falls back to archive-integrity check. +# No SHA256 advertised for this version; falls back to archive-integrity check. exit 0 EOF chmod +x "${MOCKBIN}/curl" diff --git a/test/test_rsa.c b/test/test_rsa.c index 5659b255..c6ff774a 100644 --- a/test/test_rsa.c +++ b/test/test_rsa.c @@ -3362,18 +3362,9 @@ int test_rsa_sig_alg_id(void *data) } #if defined(WP_HAVE_SHA512_224) || defined(WP_HAVE_SHA512_256) -/* EVP_MD_CTX_copy_ex on a partial RSA digest_sign accumulator, using - * SHA512-224/SHA512-256 to exercise the wp_hash_copy() branches those two - * digests only gained in FIPS v8 (older FIPS modules never define - * WP_HAVE_SHA512_224/256, so this path is skipped there). Both the - * original and duplicated contexts must produce signatures that verify - * under the original key. - * - * WP_HAVE_SHA512_224 and WP_HAVE_SHA512_256 are independently defined - * (wolfSSL toggles WOLFSSL_NOSHA512_224/256 separately), and wp_hash_copy() - * has independent branches for each, so this is two separately-gated - * one-digest tests rather than one combined test -- a build enabling only - * one of the two must still exercise its copy branch. */ +/* EVP_MD_CTX_copy_ex on a partial RSA digest_sign accumulator, exercising + * the wp_hash_copy() SHA512-224/256 branches. Split into two tests since + * WP_HAVE_SHA512_224 and WP_HAVE_SHA512_256 are independently defined. */ static int test_rsa_dupctx_verify(EVP_PKEY *pkey, const char *md, const unsigned char *msg, size_t msgLen, unsigned char *sig, size_t sigLen) From db4b0c0b141a6e9e94653074e82b594bbccace22 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 7 Aug 2026 10:37:35 -0700 Subject: [PATCH 08/11] Show the resolved FIPS module version in job names --- .github/workflows/_discover-versions.yml | 24 ++++++++++++++++++++++++ .github/workflows/fips-ready.yml | 2 +- .github/workflows/nightly-fips-ready.yml | 2 +- .github/workflows/perf-regression.yml | 2 +- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/.github/workflows/_discover-versions.yml b/.github/workflows/_discover-versions.yml index a3ec7070..1bcb84f4 100644 --- a/.github/workflows/_discover-versions.yml +++ b/.github/workflows/_discover-versions.yml @@ -42,6 +42,9 @@ on: fips_ready_resolved: description: '"true" if FIPS Ready bundle discovery succeeded, "false" if the outputs above are a degraded floor-only fallback. FIPS-specific callers must fail on "false" rather than silently testing a stale matrix.' value: ${{ jobs.discover.outputs.fips_ready_resolved }} + fips_ready_version_map: + description: 'JSON object mapping each FIPS Ready bundle version to the FIPS module (v5/v7/v8) that --enable-fips=ready actually resolves to for that bundle.' + value: ${{ jobs.discover.outputs.fips_ready_version_map }} jobs: discover: @@ -65,6 +68,7 @@ jobs: fips_ready_pr_array: ${{ steps.resolve.outputs.fips_ready_pr_array }} fips_ready_all_array: ${{ steps.resolve.outputs.fips_ready_all_array }} fips_ready_resolved: ${{ steps.resolve.outputs.fips_ready_resolved }} + fips_ready_version_map: ${{ steps.resolve.outputs.fips_ready_version_map }} steps: - name: Checkout wolfProvider uses: actions/checkout@v4 @@ -206,6 +210,24 @@ jobs: fi FR_ALL_JSON=$(printf '%s\n' "$FR_ALL" | jq -R . | jq -s -c .) FR_LATEST=$(echo "$FR_ALL" | tail -n 1) + + # `ready` does not name a fixed FIPS module; map each hosted bundle + # to the module it actually builds (from wolfSSL's configure.ac + # `ready|vN-ready` case, checked per tag). Extend the boundaries + # below when a new FIPS generation ships. + fips_module_for() { + local key + key=$(printf '%s' "$1" | awk -F. '{ printf "%05d.%05d.%05d", $1, $2, $3 }') + if [[ "$key" > "00005.00009.00001" ]]; then echo "v8" # > 5.9.1 + elif [[ "$key" > "00005.00007.00000" ]]; then echo "v7" # > 5.7.0 + else echo "v5" + fi + } + FR_MAP_JSON="{}" + while IFS= read -r ver; do + FR_MAP_JSON=$(jq --arg v "$ver" --arg f "$(fips_module_for "$ver")" \ + '. + {($v): $f}' <<< "$FR_MAP_JSON") + done <<< "$FR_ALL" # 5.8.4 and 5.9.1 are the back-compat anchors: the last two bundles # where --enable-fips=ready still meant FIPS v7, which 5.9.2 moved to v8. # Intersected with the hosted set so a delisted anchor drops out. @@ -224,6 +246,7 @@ jobs: echo "FIPS Ready latest bundle: $FR_LATEST" echo "FIPS Ready PR set: $FR_PR_JSON" echo "FIPS Ready full set (floor $FR_FLOOR): $FR_ALL_JSON" + echo "FIPS Ready version map: $FR_MAP_JSON" { echo "wolfssl_ref=$WOLFSSL_DEB_REF" @@ -240,4 +263,5 @@ jobs: echo "fips_ready_pr_array=$FR_PR_JSON" echo "fips_ready_all_array=$FR_ALL_JSON" echo "fips_ready_resolved=$FR_OK" + echo "fips_ready_version_map=$FR_MAP_JSON" } >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/fips-ready.yml b/.github/workflows/fips-ready.yml index 59f1f596..0a267ffb 100644 --- a/.github/workflows/fips-ready.yml +++ b/.github/workflows/fips-ready.yml @@ -62,7 +62,7 @@ jobs: fips_ready_test: needs: [discover_versions, require_fips_resolved] if: github.event_name != 'pull_request' || github.event.pull_request.draft == false - name: FIPS Ready ${{ matrix.wolfssl_bundle_ref }} (${{ matrix.openssl_ref }}) + name: FIPS Ready ${{ matrix.wolfssl_bundle_ref }} (FIPS ${{ fromJson(needs.discover_versions.outputs.fips_ready_version_map)[matrix.wolfssl_bundle_ref] }}, ${{ matrix.openssl_ref }}) runs-on: ubuntu-22.04 permissions: contents: read diff --git a/.github/workflows/nightly-fips-ready.yml b/.github/workflows/nightly-fips-ready.yml index b927733a..a7de97c2 100644 --- a/.github/workflows/nightly-fips-ready.yml +++ b/.github/workflows/nightly-fips-ready.yml @@ -45,7 +45,7 @@ jobs: fips_ready_test: needs: [discover_versions, require_fips_resolved] - name: Nightly FIPS Ready ${{ matrix.wolfssl_bundle_ref }} (${{ matrix.openssl_ref }}) + name: Nightly FIPS Ready ${{ matrix.wolfssl_bundle_ref }} (FIPS ${{ fromJson(needs.discover_versions.outputs.fips_ready_version_map)[matrix.wolfssl_bundle_ref] }}, ${{ matrix.openssl_ref }}) runs-on: ubuntu-22.04 permissions: contents: read diff --git a/.github/workflows/perf-regression.yml b/.github/workflows/perf-regression.yml index 511079e5..e9755919 100644 --- a/.github/workflows/perf-regression.yml +++ b/.github/workflows/perf-regression.yml @@ -64,7 +64,7 @@ jobs: perf_fips: needs: [discover_versions, require_fips_resolved] - name: Overhead regression (FIPS) + name: Overhead regression (FIPS ${{ fromJson(needs.discover_versions.outputs.fips_ready_version_map)[needs.discover_versions.outputs.fips_ready_latest] }}) runs-on: ubuntu-22.04 timeout-minutes: 30 strategy: From 5a731640a6ee8eed5867578aeaf1b6969640dd82 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 7 Aug 2026 11:01:36 -0700 Subject: [PATCH 09/11] Fix multi-line JSON breaking GITHUB_OUTPUT in FIPS version map --- .github/workflows/_discover-versions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_discover-versions.yml b/.github/workflows/_discover-versions.yml index 1bcb84f4..4be5f654 100644 --- a/.github/workflows/_discover-versions.yml +++ b/.github/workflows/_discover-versions.yml @@ -225,7 +225,7 @@ jobs: } FR_MAP_JSON="{}" while IFS= read -r ver; do - FR_MAP_JSON=$(jq --arg v "$ver" --arg f "$(fips_module_for "$ver")" \ + FR_MAP_JSON=$(jq -c --arg v "$ver" --arg f "$(fips_module_for "$ver")" \ '. + {($v): $f}' <<< "$FR_MAP_JSON") done <<< "$FR_ALL" # 5.8.4 and 5.9.1 are the back-compat anchors: the last two bundles From 516ba15d5042eb6720656b6075da5a1f86cadf8b Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Sat, 8 Aug 2026 09:56:58 -0700 Subject: [PATCH 10/11] Harden FIPS Ready bundle fetch and gate SHA-512/224 on FIPS v8 --- .github/workflows/README.md | 7 +++--- .github/workflows/_discover-versions.yml | 4 ++- docs/FIPS_INTEGRATION_GUIDE.md | 3 ++- include/wolfprovider/settings.h | 9 ++++--- scripts/fetch-fips-ready.sh | 32 ++++++++++++++++++------ scripts/include.am | 1 + scripts/utils-wolfssl.sh | 3 --- src/wp_internal.c | 4 +-- test/unit.h | 4 --- 9 files changed, 43 insertions(+), 24 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 489571ef..33769303 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -74,7 +74,7 @@ per-app workflows stay untouched. | Label | Effect | |-------|--------| | `ci:` | Run that one job (e.g. `ci:hostap`, `ci:curl`, `ci:static-analysis`). Add several to run several. | -| `ci:all` | Run the whole fan-out (all 43 jobs). | +| `ci:all` | Run the whole fan-out (all 44 jobs). | | (no label) | Nothing runs — a normal PR is unaffected. | `` is the job key in the table below (the workflow base name, e.g. @@ -101,8 +101,9 @@ gh workflow run pr-osp-select.yml --ref -f jobs="all" ### What runs in the nightly fan-out -43 workflows total: 40 third-party OSS integrations, 2 internal -validations, and the static-analysis suite. Every one of these patches +44 workflows total: 40 third-party OSS integrations, 3 internal +validations (including nightly-fips-ready.yml, reachable via +`ci:fips-ready`), and the static-analysis suite. Every one of these patches the upstream project (where needed) via `osp/wolfProvider//*.patch` from [wolfssl/osp](https://github.com/wolfssl/osp), builds it against the replace-default wolfProvider `.deb` stack, and runs the project's diff --git a/.github/workflows/_discover-versions.yml b/.github/workflows/_discover-versions.yml index 4be5f654..4ff13b5a 100644 --- a/.github/workflows/_discover-versions.yml +++ b/.github/workflows/_discover-versions.yml @@ -198,7 +198,9 @@ jobs: # Floor 5.8.2: the oldest bundle wolfProvider is tested against. FR_FLOOR="5.8.2" FR_OK=true - FR_ALL=$(./scripts/fetch-fips-ready.sh --list --floor "$FR_FLOOR") || true + # Bound the probe so a hung wolfssl.com cannot eat this shared job's + # timeout and take all ~40 callers down; a timeout degrades to FR_OK=false. + FR_ALL=$(timeout 240 ./scripts/fetch-fips-ready.sh --list --floor "$FR_FLOOR") || true if [ -z "${FR_ALL:-}" ]; then # ~40 workflows share this job for wolfssl/openssl outputs only, # so a FIPS resolution failure must not fail the whole job. FR_OK diff --git a/docs/FIPS_INTEGRATION_GUIDE.md b/docs/FIPS_INTEGRATION_GUIDE.md index 579a526c..55a19ca8 100644 --- a/docs/FIPS_INTEGRATION_GUIDE.md +++ b/docs/FIPS_INTEGRATION_GUIDE.md @@ -156,7 +156,8 @@ certificate, so the module version it selects moves with the bundle: | 5.9.2 and newer | `HAVE_FIPS_VERSION=8` | When you need a stable target, pin a generation with the alias the bundle -ships rather than `ready`: `v5-ready` on the older bundles, `v6-ready` on +ships rather than `ready`. Each alias is only accepted by the bundle that +ships it, not simultaneously: `v5-ready` on the older bundles, `v6-ready` on 5.7.2 to 5.9.1, `v7-ready` on 5.9.2 and newer. The FIPS Ready bundles are published as diff --git a/include/wolfprovider/settings.h b/include/wolfprovider/settings.h index 3a6427c5..d7a0dc7e 100644 --- a/include/wolfprovider/settings.h +++ b/include/wolfprovider/settings.h @@ -70,15 +70,18 @@ #ifdef WOLFSSL_SHA512 #define WP_HAVE_SHA512 #endif -/* FIPS v5-v7 modules set WOLFSSL_NOSHA512_224/256; v8 exports both. */ +/* SHA-512/224 and /256 only enter the FIPS module boundary at v8; earlier FIPS + * modules omit the symbols (WOLFSSL_NOSHA512_*) or lack wc_HashInit_ex support. */ #if defined(WP_HAVE_SHA512) && (LIBWOLFSSL_VERSION_HEX >= 0x05000000) && \ !defined(WOLFSSL_NOSHA512_224) && !defined(HAVE_SELFTEST) && \ - !defined(SELF_TEST) + !defined(SELF_TEST) && (!defined(HAVE_FIPS) || \ + (defined(HAVE_FIPS_VERSION_MAJOR) && HAVE_FIPS_VERSION_MAJOR >= 8)) #define WP_HAVE_SHA512_224 #endif #if defined(WP_HAVE_SHA512) && (LIBWOLFSSL_VERSION_HEX >= 0x05000000) && \ !defined(WOLFSSL_NOSHA512_256) && !defined(HAVE_SELFTEST) && \ - !defined(SELF_TEST) + !defined(SELF_TEST) && (!defined(HAVE_FIPS) || \ + (defined(HAVE_FIPS_VERSION_MAJOR) && HAVE_FIPS_VERSION_MAJOR >= 8)) #define WP_HAVE_SHA512_256 #endif #ifdef WOLFSSL_SHA3 diff --git a/scripts/fetch-fips-ready.sh b/scripts/fetch-fips-ready.sh index a77aa877..305b33f4 100755 --- a/scripts/fetch-fips-ready.sh +++ b/scripts/fetch-fips-ready.sh @@ -76,8 +76,10 @@ retry_out() { printf '%s' "$out" return 0 fi - echo "fetch-fips-ready: retry $attempt/3: '$*' failed; retrying..." >&2 - sleep $((attempt * 5)) + if [[ "$attempt" -lt 3 ]]; then + echo "fetch-fips-ready: retry $attempt/3: '$*' failed; retrying..." >&2 + sleep $((attempt * 5)) + fi done return 1 } @@ -130,7 +132,7 @@ bundle_exists() { # A genuine connection failure (not just a non-200 status) makes # curl itself exit nonzero; `|| code=""` keeps that from tripping # `set -e` before the retry loop below gets to run. - code=$(curl -sS -I -o /dev/null -w '%{http_code}' --max-time 30 \ + code=$(curl -sS -L -I -o /dev/null -w '%{http_code}' --max-time 30 \ "$(bundle_url "$ver")" 2>/dev/null) || code="" case "$code" in 200) return 0 ;; @@ -161,7 +163,8 @@ list_versions() { $(page_latest 2>/dev/null || true)" candidates=$(printf '%s\n' "$candidates" | grep -E '^[0-9.]+$' | sort -u \ | ver_sort \ - | while read -r ver; do ver_ge "$ver" "$FLOOR" && echo "$ver"; done) + | while read -r ver; do ver_ge "$ver" "$FLOOR" && echo "$ver"; done) \ + || true [[ -n "$candidates" ]] || return 1 # Runs inside the shared _discover-versions.yml job that every caller @@ -170,6 +173,11 @@ $(page_latest 2>/dev/null || true)" probe_dir=$(mktemp -d) trap 'rm -rf "$probe_dir"' RETURN for ver in $candidates; do + # Cap concurrent probes so a growing candidate set does not hit + # wolfssl.com with one big HEAD burst, the most likely 429 trigger. + while [[ "$(jobs -rp | wc -l)" -ge 6 ]]; do + sleep 0.2 + done ( # `|| rc=$?` is load-bearing under `set -e`: an unguarded call # returning 2 kills this subshell before the case below runs, @@ -216,7 +224,7 @@ sha256_of() { } fetch_bundle() { - local ver="$1" zip dir want got attempt scratch scratch_dir + local ver="$1" zip dir want got attempt scratch scratch_dir latest mkdir -p "$DEST" # Download and verify into a scratch dir first; the caller's existing @@ -240,15 +248,25 @@ fetch_bundle() { done want=$(page_sha256 "$ver" 2>/dev/null || true) + latest=$(page_latest 2>/dev/null || true) + if [[ -z "$want" && -n "$latest" && "$ver" == "$latest" ]]; then + # The newest bundle always publishes a hash, so an empty scrape here is + # a transient page/markup failure, not "verification is optional". Retry + # before deciding, and never fall through to the CRC-only path. + want=$(retry_out page_sha256 "$ver" 2>/dev/null || true) + fi if [[ -n "$want" ]]; then got=$(sha256_of "$zip") if [[ "$got" != "$want" ]]; then echo "fetch-fips-ready: SHA256 mismatch for $ver (want $want, got $got)" >&2 return 1 fi + elif [[ -n "$latest" && "$ver" == "$latest" ]]; then + echo "fetch-fips-ready: expected a published SHA256 for newest bundle" \ + "$ver but found none; refusing unverified download" >&2 + return 1 else - # Only the newest bundle has a published hash; the rest get an integrity - # check from the archive itself. + # Older bundles publish no hash; verify the archive itself instead. echo "fetch-fips-ready: no published SHA256 for $ver, verifying archive" >&2 if ! unzip -tqq "$zip" >/dev/null; then echo "fetch-fips-ready: downloaded archive for $ver failed integrity check" >&2 diff --git a/scripts/include.am b/scripts/include.am index 40f58f64..566e62eb 100644 --- a/scripts/include.am +++ b/scripts/include.am @@ -1,6 +1,7 @@ dist_noinst_SCRIPTS += scripts/build-wolfprovider.sh dist_noinst_SCRIPTS += scripts/env-setup dist_noinst_SCRIPTS += scripts/fetch-fips-ready.sh +dist_noinst_SCRIPTS += scripts/test-fetch-fips-ready.sh dist_noinst_SCRIPTS += scripts/patch-libcrypto-exports.sh dist_noinst_SCRIPTS += scripts/patch-openssl-fips.sh dist_noinst_SCRIPTS += scripts/resolve-ref.sh diff --git a/scripts/utils-wolfssl.sh b/scripts/utils-wolfssl.sh index caf79726..48b5c204 100644 --- a/scripts/utils-wolfssl.sh +++ b/scripts/utils-wolfssl.sh @@ -203,9 +203,6 @@ install_wolfssl() { v6.*|linuxv6.*) fips_configure_arg="v6" ;; - v7.*|linuxv7.*) - fips_configure_arg="v7" - ;; *) # For ready, v5, v6, or other tags, use as-is fips_configure_arg="$fips_tag" diff --git a/src/wp_internal.c b/src/wp_internal.c index 1a813738..e5a0dae4 100644 --- a/src/wp_internal.c +++ b/src/wp_internal.c @@ -591,12 +591,12 @@ enum wc_HashType wp_nid_to_wc_hash_type(int nid) hashType = WC_HASH_TYPE_SHA512; break; #if LIBWOLFSSL_VERSION_HEX >= 0x05000000 - #ifndef WOLFSSL_NOSHA512_224 + #ifdef WP_HAVE_SHA512_224 case NID_sha512_224: hashType = WC_HASH_TYPE_SHA512_224; break; #endif - #ifndef WOLFSSL_NOSHA512_256 + #ifdef WP_HAVE_SHA512_256 case NID_sha512_256: hashType = WC_HASH_TYPE_SHA512_256; break; diff --git a/test/unit.h b/test/unit.h index f93c2949..8d6dc82c 100644 --- a/test/unit.h +++ b/test/unit.h @@ -343,12 +343,8 @@ int test_rsa_pkey_keygen(void *data); int test_rsa_pkey_invalid_key_size(void *data); int test_rsa_get_params(void *data); int test_rsa_sig_alg_id(void *data); -#ifdef WP_HAVE_SHA512_224 int test_rsa_sha512_224_dupctx(void *data); -#endif -#ifdef WP_HAVE_SHA512_256 int test_rsa_sha512_256_dupctx(void *data); -#endif int test_rsa_pss_salt(void *date); int test_rsa_pss_restrictions(void *data); From b345b14b207e7e166e8016c47bc58bd10c0f185f Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 10 Aug 2026 07:33:35 -0700 Subject: [PATCH 11/11] Fail closed on FIPS bundle verify and fix SHA-512/224 FIPS-v7 gate --- .github/workflows/README.md | 1 + docs/FIPS_INTEGRATION_GUIDE.md | 10 ++-- include/wolfprovider/settings.h | 8 +-- scripts/fetch-fips-ready.sh | 35 ++++++++----- scripts/test-fetch-fips-ready.sh | 90 +++++++++++++++++++++++++++++++- scripts/utils-wolfssl.sh | 3 ++ src/wp_internal.c | 2 - 7 files changed, 126 insertions(+), 23 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 33769303..49842623 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -182,6 +182,7 @@ exercised, with and without `WOLFPROV_FORCE_FAIL=1`. | `debian-package.yml` | End-to-end check: builds the wolfprov `.deb`s and confirms they install cleanly on a fresh container and the provider loads. | | `openssl-version.yml` | Sweeps every upstream `openssl-3.X.Y` release tag — catches breakage from OpenSSL point releases before they hit our matrix defaults. | | `static-analysis.yml` | cppcheck, clang scan-build, Facebook Infer. Heavy enough that it lives in the nightly fan-out rather than per-PR. | +| `nightly-fips-ready.yml` | Builds and tests every resolved FIPS Ready bundle — the nightly superset of the per-PR `fips-ready.yml` gate. Toggle on a PR with `ci:fips-ready`. | Sanitizers (ASan+UBSan, TSan) run on every PR/push — see the PR table above. They're fast enough with caching to gate merges, so they don't diff --git a/docs/FIPS_INTEGRATION_GUIDE.md b/docs/FIPS_INTEGRATION_GUIDE.md index 55a19ca8..734cdff4 100644 --- a/docs/FIPS_INTEGRATION_GUIDE.md +++ b/docs/FIPS_INTEGRATION_GUIDE.md @@ -155,10 +155,12 @@ certificate, so the module version it selects moves with the bundle: | 5.7.2 to 5.9.1 | `HAVE_FIPS_VERSION=7` | | 5.9.2 and newer | `HAVE_FIPS_VERSION=8` | -When you need a stable target, pin a generation with the alias the bundle -ships rather than `ready`. Each alias is only accepted by the bundle that -ships it, not simultaneously: `v5-ready` on the older bundles, `v6-ready` on -5.7.2 to 5.9.1, `v7-ready` on 5.9.2 and newer. +When you need to pin a specific generation rather than tracking `ready`, use +the alias that the bundle's own `configure.ac` accepts; these differ per +release, so check the bundle you have. As of this writing: 5.8.2 accepts +`ready` and `v6-ready`; 5.9.1 accepts `ready` only; 5.9.2 accepts `ready` and +`v7-ready`. The `v5-ready` arm remains a separate, valid option across these +releases. The FIPS Ready bundles are published as `https://www.wolfssl.com/wolfssl--gplv3-fips-ready.zip`. diff --git a/include/wolfprovider/settings.h b/include/wolfprovider/settings.h index d7a0dc7e..cbf8d156 100644 --- a/include/wolfprovider/settings.h +++ b/include/wolfprovider/settings.h @@ -70,18 +70,18 @@ #ifdef WOLFSSL_SHA512 #define WP_HAVE_SHA512 #endif -/* SHA-512/224 and /256 only enter the FIPS module boundary at v8; earlier FIPS - * modules omit the symbols (WOLFSSL_NOSHA512_*) or lack wc_HashInit_ex support. */ +/* SHA-512/224 and /256 are in-boundary from FIPS v7 (the [v7|ready|dev] arm); + * earlier modules set WOLFSSL_NOSHA512_* and lack wc_HashInit_ex support. */ #if defined(WP_HAVE_SHA512) && (LIBWOLFSSL_VERSION_HEX >= 0x05000000) && \ !defined(WOLFSSL_NOSHA512_224) && !defined(HAVE_SELFTEST) && \ !defined(SELF_TEST) && (!defined(HAVE_FIPS) || \ - (defined(HAVE_FIPS_VERSION_MAJOR) && HAVE_FIPS_VERSION_MAJOR >= 8)) + (defined(HAVE_FIPS_VERSION_MAJOR) && HAVE_FIPS_VERSION_MAJOR >= 7)) #define WP_HAVE_SHA512_224 #endif #if defined(WP_HAVE_SHA512) && (LIBWOLFSSL_VERSION_HEX >= 0x05000000) && \ !defined(WOLFSSL_NOSHA512_256) && !defined(HAVE_SELFTEST) && \ !defined(SELF_TEST) && (!defined(HAVE_FIPS) || \ - (defined(HAVE_FIPS_VERSION_MAJOR) && HAVE_FIPS_VERSION_MAJOR >= 8)) + (defined(HAVE_FIPS_VERSION_MAJOR) && HAVE_FIPS_VERSION_MAJOR >= 7)) #define WP_HAVE_SHA512_256 #endif #ifdef WOLFSSL_SHA3 diff --git a/scripts/fetch-fips-ready.sh b/scripts/fetch-fips-ready.sh index 305b33f4..5fe8dad0 100755 --- a/scripts/fetch-fips-ready.sh +++ b/scripts/fetch-fips-ready.sh @@ -224,9 +224,13 @@ sha256_of() { } fetch_bundle() { - local ver="$1" zip dir want got attempt scratch scratch_dir latest + local ver="$1" zip dir want got attempt scratch scratch_dir latest page mkdir -p "$DEST" + # Absolutize so the RETURN trap and the printed path stay valid even if a + # later cd changes the shell's cwd (a relative --dest would otherwise leak + # the scratch dir on cleanup). + DEST=$(cd "$DEST" && pwd) # Download and verify into a scratch dir first; the caller's existing # $dir is only touched after a verified download, so a failed fetch # never destroys a bundle that was already there. @@ -247,23 +251,30 @@ fetch_bundle() { sleep $((attempt * 5)) done - want=$(page_sha256 "$ver" 2>/dev/null || true) - latest=$(page_latest 2>/dev/null || true) - if [[ -z "$want" && -n "$latest" && "$ver" == "$latest" ]]; then - # The newest bundle always publishes a hash, so an empty scrape here is - # a transient page/markup failure, not "verification is optional". Retry - # before deciding, and never fall through to the CRC-only path. - want=$(retry_out page_sha256 "$ver" 2>/dev/null || true) + # Fetch the download page once, with retry, so verification can never fall + # open on a transient page failure. An unreachable page refuses every + # bundle rather than downgrading to the CRC-only path. + if ! page=$(retry_out get_page); then + echo "fetch-fips-ready: could not reach download page to verify $ver;" \ + "refusing unverified download" >&2 + return 1 fi + want=$(printf '%s' "$page" | tr -d '\n' \ + | grep -o "wolfssl-$ver-gplv3-fips-ready\.zip[^(]*(SHA256: *[0-9a-f]\{64\}" \ + | head -n 1 | grep -o '[0-9a-f]\{64\}' || true) + latest=$(printf '%s' "$page" \ + | grep -o 'wolfssl-[0-9][0-9.]*-gplv3-fips-ready\.zip' \ + | sed -E 's/^wolfssl-(.*)-gplv3-fips-ready\.zip$/\1/' \ + | ver_sort | tail -n 1 || true) if [[ -n "$want" ]]; then got=$(sha256_of "$zip") if [[ "$got" != "$want" ]]; then echo "fetch-fips-ready: SHA256 mismatch for $ver (want $want, got $got)" >&2 return 1 fi - elif [[ -n "$latest" && "$ver" == "$latest" ]]; then - echo "fetch-fips-ready: expected a published SHA256 for newest bundle" \ - "$ver but found none; refusing unverified download" >&2 + elif [[ "$ver" == "$latest" ]]; then + echo "fetch-fips-ready: no published SHA256 for newest bundle $ver;" \ + "refusing unverified download" >&2 return 1 else # Older bundles publish no hash; verify the archive itself instead. @@ -291,7 +302,7 @@ fetch_bundle() { rm -rf "$dir" mv "$scratch_dir" "$dir" - cd "$(dirname "$dir")" && echo "$PWD/$(basename "$dir")" + echo "$dir" } case "$MODE" in diff --git a/scripts/test-fetch-fips-ready.sh b/scripts/test-fetch-fips-ready.sh index 364cc891..942bc679 100755 --- a/scripts/test-fetch-fips-ready.sh +++ b/scripts/test-fetch-fips-ready.sh @@ -470,6 +470,16 @@ test_fetch_bundle_replaces_on_success() { zip="${d}/fixture.zip" make_fixture_zip "9.9.9" "${zip}" + if command -v sha256sum >/dev/null 2>&1; then + real=$(sha256sum "${zip}" | awk '{print $1}') + else + real=$(shasum -a 256 "${zip}" | awk '{print $1}') + fi + html="${d}/page.html" + # 9.9.9 is the newest bundle and its published hash matches the fixture. + cat > "${html}" < (SHA256: ${real})
+EOF cat > "${MOCKBIN}/curl" < "${html}" <<'EOF' +
+EOF + cat > "${MOCKBIN}/curl" </dev/null; then + fail "fetch_bundle accepted the newest bundle with no published SHA256" + else + pass "fetch_bundle refuses the newest bundle when no SHA256 is published" + fi + rm -rf "${d}" +} + +# fetch_bundle: an unreachable download page must fail closed, not downgrade +# the newest bundle to a CRC-only check. Regression test for the fail-open. +test_fetch_bundle_unreachable_page_fails() { + local d zip + d=$(mktemp -d) + mkdir -p "${d}/wolfssl-9.9.9-gplv3-fips-ready" + touch "${d}/wolfssl-9.9.9-gplv3-fips-ready/CANARY" + + zip="${d}/fixture.zip" + make_fixture_zip "9.9.9" "${zip}" + cat > "${MOCKBIN}/curl" </dev/null; then + fail "fetch_bundle accepted a bundle while the download page was unreachable" + elif [[ -f "${d}/wolfssl-9.9.9-gplv3-fips-ready/CANARY" ]]; then + pass "fetch_bundle refuses when the download page is unreachable" + else + fail "fetch_bundle destroyed the existing bundle on an unreachable page" + fi + rm -rf "${d}" +} + test_bundle_exists_retries_transient test_bundle_exists_confirmed_404_no_retry test_bundle_exists_429_is_transient @@ -513,6 +599,8 @@ test_entrypoint_list_fails_on_indeterminate_candidate test_fetch_bundle_preserves_existing_on_download_failure test_fetch_bundle_checksum_mismatch_preserves_existing test_fetch_bundle_replaces_on_success +test_fetch_bundle_newest_no_hash_fails +test_fetch_bundle_unreachable_page_fails echo "" echo "fetch-fips-ready tests: ${PASS} passed, ${FAIL} failed" diff --git a/scripts/utils-wolfssl.sh b/scripts/utils-wolfssl.sh index 48b5c204..caf79726 100644 --- a/scripts/utils-wolfssl.sh +++ b/scripts/utils-wolfssl.sh @@ -203,6 +203,9 @@ install_wolfssl() { v6.*|linuxv6.*) fips_configure_arg="v6" ;; + v7.*|linuxv7.*) + fips_configure_arg="v7" + ;; *) # For ready, v5, v6, or other tags, use as-is fips_configure_arg="$fips_tag" diff --git a/src/wp_internal.c b/src/wp_internal.c index e5a0dae4..1596b0a0 100644 --- a/src/wp_internal.c +++ b/src/wp_internal.c @@ -590,7 +590,6 @@ enum wc_HashType wp_nid_to_wc_hash_type(int nid) case NID_sha512: hashType = WC_HASH_TYPE_SHA512; break; -#if LIBWOLFSSL_VERSION_HEX >= 0x05000000 #ifdef WP_HAVE_SHA512_224 case NID_sha512_224: hashType = WC_HASH_TYPE_SHA512_224; @@ -601,7 +600,6 @@ enum wc_HashType wp_nid_to_wc_hash_type(int nid) hashType = WC_HASH_TYPE_SHA512_256; break; #endif -#endif case NID_sha3_224: hashType = WC_HASH_TYPE_SHA3_224; break;