FIPS with Statically-Linked wolfSSL

Many FIPS deployments of wolfSSL’s FIPS 140-3 validated wolfCrypt module use dynamic linking to load the shared object / DLL. However, sometimes it is necessary for some types of projects to statically link the libwolfssl.a static archive directly into an executable or firmware image.

This post explains why static linking takes a little extra care, what the FIPS boundary and integrity hash actually do, and how to set up a linker script so your integrity check keeps passing across future updates to the rest of your application.

A quick refresher: the FIPS boundary and the integrity hash

A FIPS 140-3 cryptographic module is not “the whole library.” It is a precisely defined block of code — the cryptographic module boundary — containing the approved algorithm implementations and their FIPS wrappers. In wolfCrypt that boundary is a contiguous region of memory: all of the relevant object code (AES, SHA-2/3, RSA, ECC, DH, HMAC, the DRBG, the FIPS logic itself, and so on) plus the read-only constant data that those functions use.

The boundary is bracketed by two marker translation units that are deliberately linked first and last:

  • wolfcrypt_first.c defines the first code address (wolfCrypt_FIPS_first) and the start of the read-only data region (wolfCrypt_FIPS_ro_start).
  • wolfcrypt_last.c defines the last code address (wolfCrypt_FIPS_last) and the end of the read-only data region (wolfCrypt_FIPS_ro_end).

Everything that belongs to the module must land between those markers.

Why the boundary needs an integrity check

FIPS 140-3 requires the module to confirm, before it will perform any cryptographic operation, that its validated code has not been altered or corrupted. wolfCrypt does this with an in-core integrity check as part of its power-on self-test (POST):

  1. At startup the module walks the boundary — the code region from wolfCrypt_FIPS_first to wolfCrypt_FIPS_last, and the read-only data region from wolfCrypt_FIPS_ro_start to wolfCrypt_FIPS_ro_end.
  2. It computes an HMAC-SHA256 over those bytes.
  3. It compares the result against a precomputed expected value compiled into the binary (the verifyCore[] string).

If the two match, the self-test proceeds and the module becomes operational. If they do not match, the module enters an error state, and every call into wolfCrypt fails until the problem is resolved. The HMAC key here is a fixed, published value — this is a keyed integrity check for standardized self-test reasons, not a secret.

The key insight for this post is simple:

The integrity hash is computed over the bytes of the boundary as they sit in memory. Anything that changes those bytes — or moves them around relative to the boundary markers — changes the hash.

That is exactly where static vs. dynamic linking starts to matter.

Dynamic linking: the boundary travels in its own container

When wolfCrypt is built as a shared library, the shared object is the unit of delivery. The FIPS object files are placed contiguously inside the .so by link order, the boundary is self-contained within that file, and when the library is loaded, its constructor runs the POST automatically.

The happy consequence: you can rebuild your own application as often as you like and re-link against the same libwolfssl.so without ever disturbing the module. None of your application code lives inside the shared object’s boundary, so the boundary bytes — and therefore the integrity hash — do not change. The hash only needs to be regenerated when the library itself is rebuilt — and only then when the portion of the library within the boundary has changed.

This is the easiest path to a stable integrity check, and if your platform supports shared libraries, it is the lowest-friction option.

Static linking: you own the memory layout now

When wolfCrypt is statically linked (libwolfssl.a pulled directly into your executable or firmware), there is no separate module file. The linker is now free to:

  • scatter the wolfCrypt object code among your application’s own objects, and
  • place that code differently on every relink.

Without intervention, any rebuild of your application — even one that only touches code completely unrelated to cryptography — can move or interleave the FIPS objects, change the bytes between wolfCrypt_FIPS_first and wolfCrypt_FIPS_last, and break the integrity hash. The module would then fail its POST, and you would be stuck recomputing the hash after every single build. That is not a workable release process, nor would it be FIPS-compliant!

The fix is to take control of the layout with a linker script that pins the FIPS objects into a dedicated, contiguous region, in a fixed order, bracketed by wolfcrypt_first and wolfcrypt_last. This removes the biggest source of churn: the boundary code is no longer scattered through your image or reordered on every relink, so ordinary rebuilds of unrelated application code stop shuffling the module out from under its own integrity check.

But the boundary is not an island

It is tempting to conclude that, once the boundary is pinned, code outside it can never affect the hash. That is not quite true, and it is worth understanding why.

The module is not self-contained: functions inside the boundary call functions outside it — memory allocation (XMALLOC/XFREE), memcpy/memset, the OS entropy source that seeds the DRBG, threading and mutex primitives, and so on. The exact set of these calls is highly dependent on the particular wolfssl build options in use. Each of those call sites lives in the boundary’s .text, and the compiled call instruction encodes how to reach its target — either an absolute address or a PC-relative offset (the distance from the call site to the callee). Either way, the target’s location is baked into the bytes that get hashed.

So if you change code outside the boundary in a way that moves one of those callees — add a function ahead of it, change its size, or simply relink in a different order — the address (or relative offset) embedded in the in-boundary call instruction changes too. The contents of the boundary change, and the integrity hash changes, even though you never touched a line of crypto code.

The linker script does not eliminate this coupling; it minimizes and stabilizes it. By keeping the boundary objects contiguous and in a fixed internal order, the intra-boundary calls (crypto calling crypto) stay stable, and only the handful of genuine cross-boundary calls remain sensitive to outside changes. To keep the hash stable in practice, treat the placement of the functions the module calls outward as part of your FIPS configuration: keep that set of out-of-boundary callees, and their relative layout, as stable as you can across builds. This could involve ordering these functions first or perhaps even assigning fixed addresses to them based on a certified build to pin them to a fixed location, allowing code not called from within the FIPS boundary to be placed as needed by the linker.

Example linker script

Below is a trimmed example, adapted from the linker scripts shipped with the wolfSSL FIPS sources. The principle is always the same regardless of target:

  1. Open a dedicated section for the FIPS module.
  2. List the boundary object files in a fixed order, beginning with wolfcrypt_first and ending with wolfcrypt_last.
  3. Group both .text (code) and .rodata (constants) for those objects — the integrity check covers both.
  4. Use KEEP() so the linker’s garbage collection cannot drop them.
  5. Place everything else (your application, the OS, the rest of wolfSSL) in the normal sections after the FIPS region.

You can run ar t libwolfssl.a on a static libwolfssl archive that you’ve built with your desired build options to see the exact list of object file names it contains. From this list, you can determine which ones should be placed in the FIPS boundary by examining the DoInCoreCheck() function and looking for calls to wolfCrypt_FIPS_sanity. Each of these calls will pass in a “sanity” function name and variable name specific to the module being tested. The location of the definition of these functions will be a source file (for example aes.c for wolfCrypt_FIPS_AES_sanity) which can then be translated to the object file name.

Here is an example linker script with a FIPS boundary definition for a statically-linked libwolfssl.a:

SECTIONS
{
    /* ---- FIPS module: code ---- */
    .wolfCryptFIPSModule_text :
    {
        . = ALIGN(4);
        KEEP(*src_libwolfssl_la-wolfcrypt_first.o (.text .text*))
        KEEP(*src_libwolfssl_la-aes.o    (.text .text*))
        KEEP(*src_libwolfssl_la-cmac.o   (.text .text*))
        KEEP(*src_libwolfssl_la-des3.o   (.text .text*))
        KEEP(*src_libwolfssl_la-dh.o     (.text .text*))
        KEEP(*src_libwolfssl_la-ecc.o    (.text .text*))
        KEEP(*src_libwolfssl_la-hmac.o   (.text .text*))
        KEEP(*src_libwolfssl_la-random.o (.text .text*))
        KEEP(*src_libwolfssl_la-rsa.o    (.text .text*))
        KEEP(*src_libwolfssl_la-sha.o    (.text .text*))
        KEEP(*src_libwolfssl_la-sha256.o (.text .text*))
        KEEP(*src_libwolfssl_la-sha512.o (.text .text*))
        KEEP(*src_libwolfssl_la-sha3.o   (.text .text*))
        KEEP(*src_libwolfssl_la-fips.o      (.text .text*))
        KEEP(*src_libwolfssl_la-fips_test.o (.text .text*))
        KEEP(*src_libwolfssl_la-wolfcrypt_last.o (.text .text*))
        . = ALIGN(4);
    } > REGION_FIPS_TEXT

    /* ---- FIPS module: read-only data ---- */
    .wolfCryptFIPSModule_rodata :
    {
        . = ALIGN(4);
        KEEP(*src_libwolfssl_la-wolfcrypt_first.o (.rodata .rodata*))
        /* ... same object list, in the same order ... */
        KEEP(*src_libwolfssl_la-wolfcrypt_last.o  (.rodata .rodata*))
        . = ALIGN(4);
    } > REGION_FIPS_RODATA

    /* ---- Everything else goes here, AFTER the boundary ---- */
    .text   : { *(.text .text*) }   > REGION_APP
    .rodata : { *(.rodata .rodata*) } > REGION_APP
    .data   : { *(.data .data*) }   > REGION_APP
    .bss    : { *(.bss .bss*) COMMON } > REGION_APP
}

A few things to watch out for, because they can trip people up:

  1. The object-file names are build-system specific. The autotools build prepends a per-target prefix and changes the basename, so the SHA-512 object is src_libwolfssl_la-sha512.o, not sha512.o. A different build system (a kernel module, an IDE project, CMake, a hand-rolled Makefile) will produce different object names, and your script must match whatever your build actually emits.
  2. The object list depends on your enabled algorithms. Include only the crypto sources your configuration actually builds, and include the assembly sibling of any source that has one (for example, an aes_asm object belongs with aes.o). Hardware/architecture backends rename the object too — a RISC-V build, for instance, compiles riscv-64-sha512.o instead of sha512.o, so pin that name instead.
  3. The markers must stay strictly first and last. If a toolchain reorders them, the boundary is wrong and integrity verification fails.
  4. Keep the order identical between the .text and .rodata blocks.

The wolfSSL FIPS sources ship several real, ready-to-adapt linker scripts in the example-linker-scripts/ directory of the FIPS bundle. Start from the one closest to your platform rather than from scratch:

File Target
linker.ld x86-64 ELF (elf64-x86-64), dedicated FIPS text/rodata/bss/data regions
wolf-fips-linkerscript.ld 32-bit ELF (elf32-i386, NetBSD-style), full system script with the FIPS objects ordered into .text
wolf_fips.ld Embedded ARM (Atmel SAM4Lx8, elf32-littlearm), bare-metal flash/SRAM layout
CR2700DECODE_BL.ld Bare-metal ARM board (CR8200 decode board) link descriptor
fetch-the-default-linker-script.txt Not a script — instructions for dumping and modifying your toolchain’s default script

If you do not have a target-specific script to start from, follow fetch-the-default-linker-script.txt: dump your toolchain’s default script with $CC myapp.c -o myapp -Wl,-verbose, save the output, insert the FIPS section near the top, and feed the modified script back with $CC myapp.c -o myapp -Wl,-T modified-linker-script.ld.

Generating and locking in the integrity hash

Because the hash depends on the exact compiled bytes of the boundary, a fresh build will not match the placeholder value shipped in verifyCore[]. The first run tells you the value it actually computed, so you can paste it back in:

  1. Build your statically-linked application (with the linker script above).
  2. Run the self-test once. On a hash mismatch the registered FIPS callback (the myFipsCb handler in wolfcrypt/test/test.c is the canonical example) prints the freshly computed hash:
  3. in my Fips callback, ok = 0, err = -203
    message = In Core Integrity check FIPS error
    hash = 
    In core integrity hash check failure, copy above hash
    into verifyCore[] in fips_test.c and rebuild
    
  4. Copy that hash into verifyCore[] in fips_test.c, or supply it at compile time through the WOLFCRYPT_FIPS_CORE_HASH_VALUE define instead of editing the source.
  5. Rebuild. Now the stored value matches the in-memory module, and the POST passes.

Startup: making sure the self-test actually runs

With a shared library, the library’s load-time constructor runs the POST entry point (fipsEntry()) for you. With static linking on a normal hosted platform the same constructor mechanism still fires before main() — the entry point is registered with __attribute__((constructor)) (or the .CRT$XCU section on Windows) — provided a standard C runtime starts your program.

In constructor-less environments (e.g. bare metal, some RTOSes, firmware, UEFI/BIOS), nothing runs it automatically. There, call the module’s public integrity-test API at startup and confirm success before using any crypto:

if (wolfCrypt_IntegrityTest_fips() != 0 || wolfCrypt_GetStatus_fips() != 0) {
    /* module failed POST -- do not use crypto */
}

wolfCrypt_IntegrityTest_fips() runs the self-test (in-core integrity check plus the conditional known-answer tests), and wolfCrypt_GetStatus_fips() returns 0 only once the module is operational. Note that fipsEntry() itself is internal and not the public entry point — it is only callable directly under the NO_ATTRIBUTE_CONSTRUCTOR debug path. Plan for this explicitly in embedded static builds; do not assume the constructor will run everywhere.

Summary

  • The FIPS boundary is the contiguous block of validated code and read-only data that the certificate covers; the integrity hash is an HMAC-SHA256 over that block, checked at startup to prove the module is intact.
  • Dynamic linking keeps the boundary self-contained inside the shared object, so updating your application never disturbs the hash.
  • Static linking hands you control of the memory layout, which means you must use a linker script to pin the FIPS objects contiguously between wolfcrypt_first and wolfcrypt_last.
  • Do that correctly and lock down addresses of any out-of-boundary functions called from within the boundary and then updates to other application code outside the boundary leave the hash valid. A locked down linker script can make hash regeneration the exception, but not a guarantee you will never need it for any out-of-boundary change.
  • Mind the build-system-specific object names and the enabled-algorithm and architecture-specific object set, keep the markers first and last, and make sure the POST actually runs at startup on your target.

Static linking with FIPS is entirely practical — it just asks you to be deliberate about where the module lives in memory, and to understand the few places where it still reaches outside itself. Once the linker script is in place, the rest of your development cycle proceeds normally, and regenerating the integrity hash becomes a rare, well-understood step rather than an every-build chore.

If you have questions about any of the above, please contact us at facts@wolfssl.com or call us at +1 425 245 8247.

Download wolfSSL Now