wolfBoot/docs/FIPS.md

9.7 KiB

wolfBoot with wolfCrypt FIPS 140-3

This page explains how wolfBoot performs its firmware signature verification with the wolfCrypt FIPS 140-3 module, and what a fully CMVP-validated deployment additionally requires. Read it before making any FIPS claim about a wolfBoot deployment.

Two distinct things: approved algorithms vs. a validated module

FIPS 140-3 has two separate requirements that are easy to conflate:

  1. Using FIPS approved algorithms for the security-relevant operations (here: image signature verification and hashing).
  2. Performing those operations inside the CMVP-validated wolfCrypt module boundary, with the power-on self-test (POST), the in-core integrity check, and status gating (wolfCrypt_GetStatus_fips).

A stock wolfBoot build only addresses (1): it compiles individual wolfcrypt/src/*.c files selected by SIGN/HASH, with no POST or in-core integrity check. Building with FIPS=1 (this page) addresses (2): wolfBoot links the wolfCrypt FIPS module boundary, runs the POST + in-core check at boot, and refuses to boot unless the module is operational.

A production-validated deployment still requires the licensed, validated wolfCrypt FIPS bundle at the exact validated revision (not the evaluation "FIPS-ready" drop), the validated module version, and adherence to the module's Security Policy. Contact wolfSSL (facts@wolfssl.com) for the current certificate, validated module version, and integration guidance for a specific target.

Approved algorithms for image authentication

wolfBoot authenticates images with a public-key signature over a hash of the image. The default wolfBoot signature algorithm, ED25519, is NOT FIPS approved and must not be used for a FIPS configuration.

Approved pairs (select in the target .config):

  • Signature (SIGN=): ECC256 / ECC384 / ECC521 (ECDSA P-256/P-384/P-521). RSA-PSS is FIPS-approved but is not yet wired into wolfBoot's FIPS module object list (no rsa.o in the boundary); the FIPS=1 build rejects it. Use ECDSA.
  • Hash (HASH=): SHA256, SHA384. Match or exceed the signature strength (e.g. P-384 with SHA-384).
  • Set SPMATH=1 (the single-precision math backend the wolfCrypt FIPS module is validated with).

Not approved for signing: ED25519, ED448. The post-quantum options (LMS/XMSS/ML-DSA) are governed by separate NIST standards and are out of scope here.

Getting the FIPS source

Obtain a FIPS wolfCrypt source tree. For evaluation, the FIPS-ready bundle can be downloaded from wolfSSL:

https://www.wolfssl.com/wolfssl-5.9.2-gplv3-fips-ready.zip

Production use requires the licensed, validated FIPS bundle. Unpack it and point wolfBoot at it with WOLFBOOT_LIB_WOLFSSL.

Building wolfBoot with FIPS

The FIPS=1 build option (options.mk) rebuilds the wolfcrypt object list as the validated module boundary in link order (wolfcrypt_first.o first, wolfcrypt_last.o last, with fips.o/fips_test.o and the boundary crypto between them - the in-core integrity hash on GCC/ELF is enforced by this link order). Point the build at the FIPS tree and select an approved algorithm pair:

cp config/examples/sim-fips.config .config    # or config/examples/cm4.config
make FIPS=1 WOLFBOOT_LIB_WOLFSSL=/path/to/wolfssl-5.9.2-gplv3-fips-ready \
     SIGN=ECC384 HASH=SHA384 SPMATH=1

-DHAVE_FIPS is added by the FIPS=1 block. FIPS=1 also defaults FIPS_READY=1, which defines WOLFSSL_FIPS_READY (include/user_settings.h); that macro forces the evaluation bundle's HAVE_FIPS_VERSION to 7 and selects the FIPS-186-4 gating in settings.h. FIPS_READY=1 (the default) builds the evaluation FIPS-ready bundle. A production build with the licensed validated bundle (not a FIPS-ready drop) must pass both FIPS_READY=0 and FIPS_VERSION=<n> (for example FIPS_VERSION=7 for a 140-3 module), which options.mk turns into -DHAVE_FIPS_VERSION=<n> to pin the module version. This is required because under wolfBoot's -DWOLFSSL_USER_SETTINGS build settings.h includes only user_settings.h and never the configure-generated wolfssl/options.h, so the validated bundle does not self-declare HAVE_FIPS_VERSION; without it the module would silently build as FIPS v1 (140-2). options.mk therefore errors if FIPS_READY=0 is passed without FIPS_VERSION. The HAVE_FIPS block in include/user_settings.h also enables the module's algorithm set, keeps the RNG/DRBG enabled, and wires the entropy seed (below).

Entropy source (required)

The FIPS DRBG needs a seed. wolfBoot's lean configuration compiles out the OS seed paths, so a seed is provided via CUSTOM_RAND_GENERATE_SEED (the HAVE_FIPS block in include/user_settings.h keeps the RNG enabled by undoing wolfBoot's WC_NO_RNG/WC_NO_HASHDRBG). The example wiring points it at wolfBoot_fips_seed(), implemented per target: /dev/urandom on the simulator (hal/sim.c) and the BCM2711 RNG200 hardware TRNG on the CM4 (hal/cm4.c). Without a working seed, the ECDSA power-on self-test (which performs a sign) fails with ECDSA_KAT_FIPS_E because wc_GenerateSeed() returns NOT_COMPILED_IN.

Sealing the in-core integrity hash

The module verifies an in-core integrity hash (HMAC-SHA-256 over the module's code and read-only data) at startup. A fresh build ships with a placeholder, so the first run reports a mismatch; capture the runtime hash and seal it:

Console required for the seal. The runtime hash is emitted with wolfBoot_printf, which compiles to a no-op when the console is disabled (DEBUG_UART=0, the default in config/examples/cm4.config). Build the seal-capture image with DEBUG_UART=1 (bare-metal) or on the simulator, or the board just halts silently with no hash to copy. Once verifyCore[] is sealed you can rebuild with DEBUG_UART=0 for production - the seal is independent of the console setting only if it does not shift the module's link addresses (it does not on cm4, where the console code is outside the FIPS boundary; re-confirm the hash after the final production build).

  1. Build and run with a FIPS callback registered (wolfBoot does this in src/loader.c) and the console enabled (see the note above). On a mismatch the module reports the runtime hash; wolfBoot prints it (FIPS in-core hash = ..., from wolfCrypt_GetCoreHash_fips()) before halting.
  2. Copy the reported 64-hex-character hash into verifyCore[] in wolfcrypt/src/fips_test.c.
  3. Rebuild and re-run. wolfCrypt_GetStatus_fips() now returns 0 (operational).

The seal is specific to the exact binary layout: any code change that shifts the FIPS module's link addresses changes the in-core hash and requires a re-seal. Re-sealing verifyCore[] itself does not shift addresses (same-size rewrite), so once the rest of the build is fixed the seal converges in one pass.

Two practical traps when re-sealing (both cost time on the CM4 bring-up):

  • verifyCore[] can be sealed via a build define instead of editing the FIPS tree: CFLAGS_EXTRA="-DWOLFCRYPT_FIPS_CORE_HASH_VALUE=<hash>" (unquoted; fips_test.c stringifies it). But apply it by recompiling only fips_test.o - rm "$WOLFBOOT_LIB_WOLFSSL/wolfcrypt/src/fips_test.o" then rebuild. verifyCore[] lives after wolfCrypt_FIPS_last, so this leaves the hashed region byte-identical and converges in one pass. Passing the define through a full rebuild (make clean + build) recompiles the whole module and shifts its link addresses, so the hash never stabilizes.
  • make clean removes $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/*.o. If you build with WOLFBOOT_LIB_WOLFSSL=<fips-tree> but run make clean without that variable, it cleans the default lib/wolfssl instead, leaving the stale FIPS fips_test.o in place - the new seal silently never lands. Pass WOLFBOOT_LIB_WOLFSSL to clean too, or rm the object directly.

Bare-metal targets

The FIPS module targets a hosted environment; a few things must be provided on bare-metal (the CM4 does all of these):

  • POST entry. The module registers its POST via a C constructor (.init_array), which a hosted runtime runs before main(). wolfBoot's bare-metal startup does not run .init_array, so build with NO_ATTRIBUTE_CONSTRUCTOR and call fipsEntry() explicitly (src/loader.c).
  • Normal (cacheable) memory. wolfBoot's simple startup runs with the MMU off, where all memory is Device-nGnRnE and unaligned / 128-bit SIMD accesses fault (the FIPS module and newlib printf/snprintf do both). The CM4 HAL enables a minimal identity MMU with DDR mapped Normal cacheable before the POST (cm4_mmu_enable), and tears it down (clean D-cache, disable MMU/caches) before the boot handoff (cm4_mmu_disable) so the loaded image is coherent and the application starts MMU-off.
  • libc. The module uses malloc/printf; stub the newlib syscalls (--specs=nosys.specs) and provide a bounded _sbrk so the heap cannot grow into the unverified image (the CM4 HAL allocates from a fixed static buffer in hal/cm4.c).

Bring the module up on the simulator (config/examples/sim-fips.config) first - it exercises the whole flow (module boundary, POST, in-core seal, verify, A/B update) with no hardware.

Verifying operation

  • POST/CASTs run at module initialization; wc_RunAllCast_fips() runs the conditional algorithm self-tests and wolfCrypt_GetStatus_fips() reports the module status (0 = operational).
  • wolfBoot treats a non-zero FIPS status as a hard failure and refuses to boot (src/loader.c).
  • A deliberately corrupted module boundary (flip a byte) makes the in-core check fail and blocks the boot - the negative test for the integration.

See also

  • Targets.md - Raspberry Pi Compute Module 4 (BCM2711) target. FIPS 140-3 authenticated boot (module operational -> SHA-384 integrity -> ECDSA-P384 verify seeded by the BCM2711 hardware TRNG -> handoff) is validated on CM4 hardware.