diff --git a/.github/workflows/aws-lc-rs.yml b/.github/workflows/aws-lc-rs.yml index 0eeb0bbe1fe..ec43689b0ac 100644 --- a/.github/workflows/aws-lc-rs.yml +++ b/.github/workflows/aws-lc-rs.yml @@ -310,8 +310,11 @@ jobs: run: | set -ex # Wine binfmt allows the kernel to transparently run .exe files through - # Wine. This is needed for the FIPS build, which runs fips_empty_main.exe - # at build time to capture the integrity hash. + # Wine. The FIPS build itself no longer needs Wine (inject_hash.go + # patches the integrity hash directly into crypto.dll using the + # linker map), but we still need Wine to run the FIPS sanity test + # below, which loads the cross-built DLL so the .CRT$XCU initializer + # can trigger the power-on self-test. # # Ubuntu 24.04's wine64 (9.0) does not properly execute .CRT$XCU # initializers in cross-compiled DLLs, which prevents the FIPS diff --git a/crypto/CMakeLists.txt b/crypto/CMakeLists.txt index c7f3c22f4c3..534f1060d89 100644 --- a/crypto/CMakeLists.txt +++ b/crypto/CMakeLists.txt @@ -629,34 +629,22 @@ if(FIPS_SHARED) # Rewrite libcrypto.so, libcrypto.dylib, or crypto.dll to inject the correct module # hash value. For now we support the FIPS build only on Linux, macOS, iOS, and Windows. if(MSVC) - # On Windows we use capture_hash.go: build crypto.dll with a placeholder - # hash, then run fips_empty_main.exe (which triggers the integrity check - # and prints the correct hash), then patch the placeholder in crypto.dll. - # - # The fips_integrity target (marked ALL) ensures the hash injection runs - # before 'install' copies crypto.dll. Without this, Cargo builds (which - # run 'cmake --build --target install') would skip the hash injection - # because fips_empty_main is not in crypto's dependency chain. + # On Windows, inject_hash.go parses the linker map file and the PE to locate + # module boundaries, computes the integrity hash, and patches it directly + # into the DLL. This matches the Linux/Apple approach and does not require + # running a binary, enabling cross-compilation without Wine. + set(CRYPTO_MAP_FILE "${CMAKE_CURRENT_BINARY_DIR}/fips_crypto.map") build_libcrypto(NAME crypto MODULE_SOURCE $ SET_OUTPUT_NAME) - - add_executable(fips_empty_main fipsmodule/fips_empty_main.c) - target_link_libraries(fips_empty_main PUBLIC crypto) - target_add_awslc_include_paths(TARGET fips_empty_main SCOPE PRIVATE) + target_link_options(crypto PRIVATE "/MAP:${CRYPTO_MAP_FILE}") add_custom_command( - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/fips_hash_injected.stamp + TARGET crypto POST_BUILD COMMAND ${GO_EXECUTABLE} run - ${AWSLC_SOURCE_DIR}/util/fipstools/capture_hash/capture_hash.go - -in-executable $ - -patch-dll $ - COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/fips_hash_injected.stamp - DEPENDS fips_empty_main crypto - ${AWSLC_SOURCE_DIR}/util/fipstools/capture_hash/capture_hash.go + ${AWSLC_SOURCE_DIR}/util/fipstools/inject_hash/inject_hash.go + -o $ -in-object $ + -map ${CRYPTO_MAP_FILE} -windows WORKING_DIRECTORY ${AWSLC_SOURCE_DIR} ) - add_custom_target(fips_integrity ALL - DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/fips_hash_injected.stamp - ) else() # On Apple and Linux platforms inject_hash.go can parse libcrypto and inject # the hash directly into the final library. diff --git a/crypto/fipsmodule/CMakeLists.txt b/crypto/fipsmodule/CMakeLists.txt index 07859817d34..adc451235ef 100644 --- a/crypto/fipsmodule/CMakeLists.txt +++ b/crypto/fipsmodule/CMakeLists.txt @@ -619,26 +619,30 @@ elseif(FIPS_SHARED) separate_arguments(FIPS_MARKER_C_FLAGS NATIVE_COMMAND "${CMAKE_C_FLAGS}") add_custom_command( OUTPUT fips_msvc_start.obj - COMMAND ${CMAKE_C_COMPILER} ${FIPS_MARKER_C_FLAGS} -w /nologo /c /DAWSLC_FIPS_SHARED_START /Fo:fips_msvc_start.obj ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c + COMMAND ${CMAKE_C_COMPILER} ${FIPS_MARKER_C_FLAGS} -w /nologo /c /DAWSLC_FIPS_SHARED_START /Fofips_msvc_start.obj ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c ) add_custom_command( OUTPUT fips_msvc_end.obj - COMMAND ${CMAKE_C_COMPILER} ${FIPS_MARKER_C_FLAGS} -w /nologo /c /DAWSLC_FIPS_SHARED_END /Fo:fips_msvc_end.obj ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c + COMMAND ${CMAKE_C_COMPILER} ${FIPS_MARKER_C_FLAGS} -w /nologo /c /DAWSLC_FIPS_SHARED_END /Fofips_msvc_end.obj ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c ) - if(CMAKE_AR) - set(MSVC_LIB "${CMAKE_AR}") - else() - get_filename_component(MSVC_BIN ${CMAKE_LINKER} DIRECTORY) - set(MSVC_LIB "${MSVC_BIN}/lib.exe") + get_filename_component(MSVC_BIN ${CMAKE_LINKER} DIRECTORY) + find_program(MSVC_LIB NAMES lib lib.exe llvm-lib llvm-lib.exe + HINTS ${MSVC_BIN} NO_DEFAULT_PATH) + if(NOT MSVC_LIB) + find_program(MSVC_LIB NAMES lib lib.exe llvm-lib llvm-lib.exe) + endif() + if(NOT MSVC_LIB) + message(FATAL_ERROR "Could not find lib.exe or llvm-lib for creating bcm.lib") endif() + file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/bcm_objects.rsp" + CONTENT "\"$,\"\n\">\"") add_custom_command( OUTPUT ${BCM_NAME} - COMMAND ${MSVC_LIB} /nologo fips_msvc_start.obj $ fips_msvc_end.obj /OUT:${BCM_NAME} - COMMAND_EXPAND_LISTS + COMMAND ${MSVC_LIB} /nologo fips_msvc_start.obj @bcm_objects.rsp fips_msvc_end.obj /OUT:${BCM_NAME} DEPENDS fips_msvc_start.obj fips_msvc_end.obj bcm_library WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) diff --git a/crypto/fipsmodule/FIPS.md b/crypto/fipsmodule/FIPS.md index f2753674c10..dab4ef26a7b 100644 --- a/crypto/fipsmodule/FIPS.md +++ b/crypto/fipsmodule/FIPS.md @@ -163,20 +163,10 @@ The Shared Windows FIPS integrity test differs in two key ways: 2. How the correct integrity hash is calculated Microsoft Visual C compiler (MSVC) does not support linker scripts that add symbols to mark the start and end of the text and rodata sections, as is done on Linux. Instead, `fips_shared_library_marker.c` is compiled twice to generate two object files that contain start/end functions and variables. MSVC `pragma` segment definitions are used to place the markers in specific sections (e.g. `.fipstx$a`). This particular name format uses [Portable Executable Grouped Sections](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#grouped-sections-object-only) to control what section the code is placed in and the order within the section. With the start and end markers placed at `$a` and `$z` respectively, BCM puts everything in the `$b` section. When the final crypto.dll is built, all the code is in the `.fipstx` section, all data is in `.fipsda`, all constants are in `.fipsco`, all uninitialized items in `.fipsbs`, and everything is in the correct order. -The process to generate the expected integrity fingerprint is also different from Linux. We use a single-DLL capture-and-patch approach: build `crypto.dll` once with a placeholder hash, run it to compute the real hash, then binary-patch the placeholder directly in the DLL. This avoids building two separate DLLs whose linker output may differ (e.g. mandatory ASLR on ARM64 causes different ADRP immediates, and `lld-link` used by clang-cl is not guaranteed to produce byte-identical output across two independent link operations). - -1. Build the required object files once: `bcm.obj` from `bcm.c` and the start/end object files - 1. `bcm.obj` places the power-on self tests in the `.CRT$XCU` section which is run automatically by the Windows Common Runtime library (CRT) startup code -2. Use MSVC's `lib.exe` (or `llvm-lib` for clang-cl) to combine the start/end object files with `bcm.obj` to create the static library `bcm.lib`. - 1. MSVC does not support combining multiple object files into another object file like the Apple build. -3. Build `fipsmodule` which contains the placeholder integrity hash -4. Build `crypto.dll` with `bcm.lib` and `fipsmodule` -5. Build the small application `fips_empty_main.exe` and link it with `crypto.dll` -6. `capture_hash.go` runs `fips_empty_main.exe` - 1. The CRT runs all functions in the `.CRT$XC*` sections in order starting with `.CRT$XCA` - 2. The BCM power-on tests are in `.CRT$XCU` and are run after all other Windows initialization is complete - 3. BCM calculates the correct integrity value which will not match the placeholder value. Before aborting the process the correct value is printed - 4. `capture_hash.go` reads the correct integrity value and binary-patches the 32-byte placeholder directly in `crypto.dll` +The process to generate the expected integrity fingerprint follows the same approach as Linux and Apple, using `inject_hash.go` to patch the hash directly into the final binary: + +1. Build `crypto.dll` from `bcm.c`, the start/end marker objects, and the rest of `fipsmodule`, using the `/MAP:` linker flag to produce a linker map file +2. `inject_hash.go -windows` parses the linker map file and the PE to locate the FIPS module boundaries via the marker symbols, computes the integrity hash over the module text and rodata, and patches the correct value directly into `crypto.dll` ### Linux Static build diff --git a/crypto/fipsmodule/fips_empty_main.c b/crypto/fipsmodule/fips_empty_main.c deleted file mode 100644 index a1264ad9d91..00000000000 --- a/crypto/fipsmodule/fips_empty_main.c +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 OR ISC - -#include -#include - -#include -/* - * This program is used during the FIPS libcrypto build on Windows. It is the - * smallest possible executable that links with libcrypto and can trigger the - * power-on self tests. - */ -int main(int argc, char *argv[]) { - fprintf(stderr, "This will only print if the power-on self-tests pass.\n"); - // To ensure the linker links libcrypto call something - fprintf(stderr, "FIPS mode is %d\n", FIPS_mode()); - - exit(1); -} - diff --git a/tests/ci/run_windows_tests.bat b/tests/ci/run_windows_tests.bat index 0f01de21c90..cb38b493e9d 100644 --- a/tests/ci/run_windows_tests.bat +++ b/tests/ci/run_windows_tests.bat @@ -43,10 +43,10 @@ call :build_and_test Release "-DBUILD_SHARED_LIBS=1" || goto error @rem Reuse the build tree from the preceding shared build to verify that @rem `cmake --install` places DLLs in bin/ and import libraries in lib/. call :verify_install || goto error -call :build_and_test Release "-DBUILD_SHARED_LIBS=1 -DFIPS=1" || goto error +call :fips_build_and_test Release "-DBUILD_SHARED_LIBS=1 -DFIPS=1" || goto error if /i not "%ARCH_OPTION%" == "arm64" ( @rem For FIPS on Windows/x86-64 we also have a RelWithDebInfo build to generate debug symbols. - call :build_and_test RelWithDebInfo "-DBUILD_SHARED_LIBS=1 -DFIPS=1" || goto error + call :fips_build_and_test RelWithDebInfo "-DBUILD_SHARED_LIBS=1 -DFIPS=1" || goto error ) @rem On Windows, CMake defaults to dynamically linking to the Windows C-runtime. @@ -57,6 +57,40 @@ call :build_and_test Release "-DBUILD_SHARED_LIBS=0 -DCMAKE_MSVC_RUNTIME_LIBRARY exit /b 0 +@rem %1 is the build type (e.g. Release/Debug) +@rem %2 is the additional full CMake args containing -DFIPS=1 +:fips_build_and_test +@echo on +call :build_and_test %1 %2 || goto error +@echo LOG: %date%-%time% %1 %2 running FIPS validation + +@rem Positive test: run test_fips.exe to verify the integrity check passes and KATs succeed. +@rem test_fips.exe links crypto.dll, so the CRT .CRT$XCU initializer triggers the +@rem integrity check before main. If the hash is wrong, the process aborts. +%BUILD_DIR%\util\fipstools\test_fips.exe || goto error + +@rem Negative test: corrupt the FIPS module in crypto.dll and verify the integrity +@rem check detects it. This proves the check actually runs on DLL load. +copy /y %BUILD_DIR%\crypto\crypto.dll %BUILD_DIR%\crypto\crypto.dll.bak || goto error +cd /d %SRC_ROOT% +go run util/fipstools/break-hash.go -map %BUILD_DIR%\crypto\fips_crypto.map %BUILD_DIR%\crypto\crypto.dll %BUILD_DIR%\crypto\crypto_corrupted.dll || goto error +copy /y %BUILD_DIR%\crypto\crypto_corrupted.dll %BUILD_DIR%\crypto\crypto.dll || goto error +%BUILD_DIR%\util\fipstools\test_fips.exe 2>nul +set FIPS_NEGATIVE_RC=%ERRORLEVEL% +@rem Restore the unmodified DLL before we decide whether the test passed or +@rem failed, so that a failure here does not leave a corrupted crypto.dll +@rem behind for any subsequent local invocation. +copy /y %BUILD_DIR%\crypto\crypto.dll.bak %BUILD_DIR%\crypto\crypto.dll || goto error +del /q %BUILD_DIR%\crypto\crypto.dll.bak +del /q %BUILD_DIR%\crypto\crypto_corrupted.dll +if %FIPS_NEGATIVE_RC% equ 0 ( + echo FIPS integrity negative test failed: test_fips should have failed with corrupted crypto.dll + goto error +) + +@echo LOG: %date%-%time% %1 %2 FIPS validation complete +exit /b 0 + :run_sde_tests @rem Run and test the same dimensions as our Linux SDE tests. call :build_and_test_with_sde Debug "" || goto error diff --git a/util/fipstools/break-hash.go b/util/fipstools/break-hash.go index feaf06d4364..6d12e0a52e3 100644 --- a/util/fipstools/break-hash.go +++ b/util/fipstools/break-hash.go @@ -3,8 +3,8 @@ //go:build ignore -// break-hash parses an ELF binary containing the FIPS module and corrupts the -// first byte of the module. This should cause the integrity check to fail. +// break-hash parses an ELF or PE binary containing the FIPS module and corrupts +// the first byte of the module. This should cause the integrity check to fail. package main import ( @@ -14,19 +14,17 @@ import ( "debug/elf" "encoding/hex" "errors" + "flag" "fmt" "os" -) -func do(outPath, inPath string) error { - objectBytes, err := os.ReadFile(inPath) - if err != nil { - return err - } + "github.com/aws/aws-lc/util/fipstools/fipscommon" +) +func doELF(objectBytes []byte) (int, []byte, error) { object, err := elf.NewFile(bytes.NewReader(objectBytes)) if err != nil { - return errors.New("failed to parse object: " + err.Error()) + return 0, nil, errors.New("failed to parse object: " + err.Error()) } // Find the .text section. @@ -41,7 +39,7 @@ func do(outPath, inPath string) error { } if textSection == nil { - return errors.New("failed to find .text section in object") + return 0, nil, errors.New("failed to find .text section in object") } symbols, err := object.Symbols() @@ -50,7 +48,7 @@ func do(outPath, inPath string) error { symbols, err = object.DynamicSymbols() } if err != nil { - return errors.New("failed to parse symbols: " + err.Error()) + return 0, nil, errors.New("failed to parse symbols: " + err.Error()) } // Find the start and end markers of the module. @@ -65,13 +63,13 @@ func do(outPath, inPath string) error { switch symbol.Name { case "BORINGSSL_bcm_text_start": if startSeen { - return errors.New("duplicate start symbol found") + return 0, nil, errors.New("duplicate start symbol found") } startSeen = true start = symbol.Value case "BORINGSSL_bcm_text_end": if endSeen { - return errors.New("duplicate end symbol found") + return 0, nil, errors.New("duplicate end symbol found") } endSeen = true end = symbol.Value @@ -81,42 +79,101 @@ func do(outPath, inPath string) error { } if !startSeen || !endSeen { - return errors.New("could not find module in object") + return 0, nil, errors.New("could not find module in object") } moduleText := make([]byte, end-start) if n, err := textSection.ReadAt(moduleText, int64(start-textSection.Addr)); err != nil { - return fmt.Errorf("failed to read from module start (at %d of %d) in .text: %s", start, textSection.Size, err) + return 0, nil, fmt.Errorf("failed to read from module start (at %d of %d) in .text: %s", start, textSection.Size, err) } else if n != len(moduleText) { - return fmt.Errorf("short read from .text: wanted %d, got %d", len(moduleText), n) + return 0, nil, fmt.Errorf("short read from .text: wanted %d, got %d", len(moduleText), n) } // In order to match up the module start with the raw ELF contents, // search for the first 256 bytes and assume that will be unique. - offset := bytes.Index(objectBytes, moduleText[:256]) - if offset < 0 { - return errors.New("did not find module prefix in object file") + fileOffset := bytes.Index(objectBytes, moduleText[:256]) + if fileOffset < 0 { + return 0, nil, errors.New("did not find module prefix in object file") + } + + if bytes.Index(objectBytes[fileOffset+1:], moduleText[:256]) >= 0 { + return 0, nil, errors.New("found two occurrences of prefix in object file") + } + + return fileOffset, moduleText, nil +} + +func doPE(objectBytes []byte, mapPath string) (int, []byte, error) { + // Unlike doELF, we do not need to search the raw file for a unique + // 256-byte prefix of the module text to find its file offset: the map + // file gives us the exact virtual address of BORINGSSL_bcm_text_start, + // and the PE section table lets us translate that directly to a file + // offset. The uniqueness check doELF performs is a workaround for the + // fact that ELF symbol values alone do not pinpoint the on-disk location + // of the module; the PE path does not have that limitation. + symbolAddrs, err := fipscommon.ParseMapFile(mapPath) + if err != nil { + return 0, nil, err + } + + peInfo, err := fipscommon.ParsePE(objectBytes) + if err != nil { + return 0, nil, err } - if bytes.Index(objectBytes[offset+1:], moduleText[:256]) >= 0 { - return errors.New("found two occurrences of prefix in object file") + startOffset, err := peInfo.ResolveSymbolFileOffset(symbolAddrs, "BORINGSSL_bcm_text_start") + if err != nil { + return 0, nil, err + } + endOffset, err := peInfo.ResolveSymbolFileOffset(symbolAddrs, "BORINGSSL_bcm_text_end") + if err != nil { + return 0, nil, err } - // Corrupt the module in the ELF. - objectBytes[offset] ^= 1 + if startOffset >= endOffset || endOffset > uint64(len(objectBytes)) { + return 0, nil, fmt.Errorf("invalid boundaries: start=0x%x end=0x%x filesize=%d", startOffset, endOffset, len(objectBytes)) + } - // Calculate the before and after hash of the module. + moduleText := make([]byte, endOffset-startOffset) + copy(moduleText, objectBytes[startOffset:endOffset]) + + return int(startOffset), moduleText, nil +} + +func do(outPath, inPath, mapPath string) error { + objectBytes, err := os.ReadFile(inPath) + if err != nil { + return err + } + + var fileOffset int + var moduleText []byte + + if mapPath != "" { + fileOffset, moduleText, err = doPE(objectBytes, mapPath) + } else { + fileOffset, moduleText, err = doELF(objectBytes) + } + if err != nil { + return err + } + + // Calculate the before hash of the module. var zeroKey [64]byte mac := hmac.New(sha512.New, zeroKey[:]) mac.Write(moduleText) hashWas := mac.Sum(nil) + // Corrupt the module in the binary. + objectBytes[fileOffset] ^= 1 + + // Calculate the after hash of the module. moduleText[0] ^= 1 mac.Reset() mac.Write(moduleText) newHash := mac.Sum(nil) - fmt.Printf("Found start of module at offset 0x%x (VMA 0x%x):\n", start-textSection.Addr, start) + fmt.Printf("Found start of module at file offset 0x%x:\n", fileOffset) fmt.Println(hex.Dump(moduleText[:128])) fmt.Printf("\nHash of module was: %x\n", hashWas) fmt.Printf("Hash of corrupted module is: %x\n", newHash) @@ -125,17 +182,21 @@ func do(outPath, inPath string) error { } func main() { - if len(os.Args) != 3 { - usage() + mapPath := flag.String("map", "", "Path to linker .map file (required for Windows PE/DLL)") + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: %s [-map mapfile] \n", os.Args[0]) + flag.PrintDefaults() + } + flag.Parse() + + args := flag.Args() + if len(args) != 2 { + flag.Usage() os.Exit(1) } - if err := do(os.Args[2], os.Args[1]); err != nil { + if err := do(args[1], args[0], *mapPath); err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) os.Exit(1) } } - -func usage() { - fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) -} diff --git a/util/fipstools/capture_hash/capture_hash.go b/util/fipstools/capture_hash/capture_hash.go deleted file mode 100644 index 04fba431765..00000000000 --- a/util/fipstools/capture_hash/capture_hash.go +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 OR ISC - -// capture_hash runs another executable that has been linked with libcrypto. It -// expects the libcrypto to run the power-on self-tests and fail due to a -// fingerprint mismatch. capture_hash parses the output to extract the correct -// fingerprint value. -// -// The -patch-dll flag (required for the Windows FIPS build) specifies a DLL to -// binary-patch: the tool reads the DLL, finds the placeholder hash value, -// replaces it with the captured hash, and writes the patched DLL back. This -// single-DLL approach avoids building two separate DLLs whose linker output -// may differ — e.g. mandatory ASLR on ARM64 causes ADRP immediate differences, -// and lld-link (clang-cl) is not guaranteed to produce byte-identical output -// across two independent link operations. -// -// Without -patch-dll, the tool writes a C source file to stdout containing the -// correct hash. This mode is retained for debugging but is no longer used by -// the build system. - -package main - -import ( - "bytes" - "encoding/hex" - "flag" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/aws/aws-lc/util/fipstools/fipscommon" -) - -const expectedFailureMsg = "FIPS integrity test failed." - -// This must match what is in crypto/fipsmodule/fips_shared_support.c -const expectedHashLine = "Expected: ae2cea2abda6f3ec977f9bf6949afc836827cba0a09f6b6fde52cde2cdff3180" -const calculatedPrefix = "Calculated: " -const hashHexLen = 64 - -func main() { - executable := flag.String("in-executable", "", "Path to the executable file") - patchDll := flag.String("patch-dll", "", "Path to a DLL to binary-patch with the captured hash (single-DLL mode)") - flag.Parse() - - if *executable == "" { - fmt.Fprintf(os.Stderr, "capture_hash: -in-executable is required\n") - os.Exit(1) - } - - // When -patch-dll is specified, check whether the DLL still contains the - // placeholder hash. If it has already been patched (e.g. by a previous - // build invocation in the same output directory), there is nothing to do. - // Running the executable against an already-patched DLL would cause the - // FIPS self-test to pass, and capture_hash would fail because it expects - // the self-test to report a mismatch. - if *patchDll != "" { - dllBytes, err := os.ReadFile(*patchDll) - if err != nil { - fmt.Fprintf(os.Stderr, "capture_hash: failed to read DLL: %v\n", err) - os.Exit(1) - } - if bytes.Index(dllBytes, fipscommon.UninitHashValue[:]) < 0 { - fmt.Fprintf(os.Stderr, "capture_hash: %s already patched (placeholder not found), skipping\n", *patchDll) - return - } - } - - cmd := exec.Command(*executable) - - // When -patch-dll is specified, the executable links against a DLL that - // may reside in a different directory. Under Wine binfmt (cross-compiling - // from Linux), Wine needs WINEPATH to locate the DLL. We replace any - // existing WINEPATH rather than appending, because a stale WINEPATH - // (e.g. from a previous build) could cause Wine to load an already- - // patched copy of the DLL instead of the one we need to patch. - if *patchDll != "" { - dllDir := filepath.Dir(*patchDll) - env := os.Environ() - found := false - for i, e := range env { - if strings.HasPrefix(e, "WINEPATH=") { - env[i] = "WINEPATH=" + dllDir - found = true - break - } - } - if !found { - env = append(env, "WINEPATH="+dllDir) - } - cmd.Env = env - } - - out, err := cmd.CombinedOutput() - if err == nil { - fmt.Fprintf(os.Stderr, "%s", out) - fmt.Fprintf(os.Stderr, "capture_hash: executable did not fail as expected\n") - os.Exit(1) - } - - // Search for the expected lines by content rather than by strict line - // numbers. This makes the parser tolerant of additional diagnostic output - // that may be printed before or between the FIPS integrity test messages. - lines := strings.Split(string(out), "\r\n") - - foundFailureMsg := false - foundExpectedHash := false - hashHex := "" - - for _, line := range lines { - line = strings.TrimSpace(line) - if line == expectedFailureMsg { - foundFailureMsg = true - } - if line == expectedHashLine { - foundExpectedHash = true - } - if strings.HasPrefix(line, calculatedPrefix) { - parts := strings.Fields(line) - if len(parts) >= 2 { - hashHex = parts[1] - } - } - } - - if !foundFailureMsg { - fmt.Fprintf(os.Stderr, "%s", out) - fmt.Fprintf(os.Stderr, "capture_hash: did not find %q in output\n", expectedFailureMsg) - os.Exit(1) - } - - if !foundExpectedHash { - fmt.Fprintf(os.Stderr, "%s", out) - fmt.Fprintf(os.Stderr, "capture_hash: did not find %q in output\n", expectedHashLine) - os.Exit(1) - } - - if hashHex == "" { - fmt.Fprintf(os.Stderr, "%s", out) - fmt.Fprintf(os.Stderr, "capture_hash: did not find %q line in output\n", calculatedPrefix) - os.Exit(1) - } - - if len(hashHex) != hashHexLen { - fmt.Fprintf(os.Stderr, "%s", out) - fmt.Fprintf(os.Stderr, "capture_hash: hash %q is %d chars, expected %d\n", hashHex, len(hashHex), hashHexLen) - os.Exit(1) - } - - fmt.Fprintf(os.Stderr, "capture_hash: captured hash = %s\n", hashHex) - - if *patchDll != "" { - // Single-DLL mode: binary-patch the placeholder hash in the DLL. - hashBytes, err := hex.DecodeString(hashHex) - if err != nil { - fmt.Fprintf(os.Stderr, "capture_hash: failed to decode hash hex: %v\n", err) - os.Exit(1) - } - - fi, err := os.Stat(*patchDll) - if err != nil { - fmt.Fprintf(os.Stderr, "capture_hash: %v\n", err) - os.Exit(1) - } - perm := fi.Mode() & 0777 - - dllBytes, err := os.ReadFile(*patchDll) - if err != nil { - fmt.Fprintf(os.Stderr, "capture_hash: failed to read DLL: %v\n", err) - os.Exit(1) - } - - offset := bytes.Index(dllBytes, fipscommon.UninitHashValue[:]) - if offset < 0 { - fmt.Fprintf(os.Stderr, "capture_hash: placeholder hash not found in %s\n", *patchDll) - os.Exit(1) - } - - // Verify uniqueness — the placeholder must appear exactly once. - if bytes.Index(dllBytes[offset+len(fipscommon.UninitHashValue):], fipscommon.UninitHashValue[:]) >= 0 { - fmt.Fprintf(os.Stderr, "capture_hash: found multiple occurrences of placeholder hash in %s\n", *patchDll) - os.Exit(1) - } - - copy(dllBytes[offset:], hashBytes) - - if err := os.WriteFile(*patchDll, dllBytes, perm); err != nil { - fmt.Fprintf(os.Stderr, "capture_hash: failed to write patched DLL: %v\n", err) - os.Exit(1) - } - - fmt.Fprintf(os.Stderr, "capture_hash: patched %s at offset 0x%x\n", *patchDll, offset) - } else { - // Stdout mode (not used by the build system): generate a C source file. - fmt.Printf(`// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 OR ISC -// This file is generated by: 'go run util/fipstools/capture_hash/capture_hash.go -in-executable %s' -#include -const uint8_t BORINGSSL_bcm_text_hash[32] = { -`, *executable) - for i := 0; i < len(hashHex); i += 2 { - fmt.Printf("0x%s, ", hashHex[i:i+2]) - } - fmt.Printf(` -}; -`) - } -} diff --git a/util/fipstools/fipscommon/pe.go b/util/fipstools/fipscommon/pe.go new file mode 100644 index 00000000000..64e2f258a10 --- /dev/null +++ b/util/fipstools/fipscommon/pe.go @@ -0,0 +1,138 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR ISC + +package fipscommon + +import ( + "bytes" + "debug/pe" + "errors" + "fmt" + "os" + "strconv" + "strings" +) + +// PEInfo holds parsed PE file information needed for symbol resolution. +type PEInfo struct { + File *pe.File + ImageBase uint64 +} + +// ParseMapFile reads a Windows linker map file and returns a map of +// BORINGSSL_bcm_* symbol names to their Rva+Base addresses. +// +// MSVC-style map files (also produced by lld-link with /MAP:) contain several +// sections. Symbol addresses we care about live under a column-header line of +// the form: +// +// Address Publics by Value Rva+Base Lib:Object +// +// followed by rows of the form: +// +// SSSS:OOOOOOOO name RRRRRRRRRRRRRRRR [f] Lib:Object +// +// A later "Static symbols" heading begins a section that reuses the row format, +// so anchoring parsing to the Publics header avoids accidentally picking up a +// same-named static symbol from some other translation unit. The +// BORINGSSL_bcm_* markers are `extern` and therefore only ever appear in +// Publics by Value in practice, but the anchor makes the intent explicit and +// the parser less fragile. +func ParseMapFile(mapPath string) (map[string]uint64, error) { + data, err := os.ReadFile(mapPath) + if err != nil { + return nil, fmt.Errorf("failed to read map file: %s", err.Error()) + } + + symbols := make(map[string]uint64) + inPublics := false + sawPublicsHeading := false + for _, line := range strings.Split(string(data), "\n") { + trimmed := strings.TrimSpace(line) + // Section headings are column-header / label lines with no leading + // address column. Detect the Publics by Value column-header line + // (which also contains the word "Address") and the Static symbols + // label that terminates the public section. + if strings.Contains(trimmed, "Publics by Value") { + inPublics = true + sawPublicsHeading = true + continue + } + if trimmed == "Static symbols" { + inPublics = false + continue + } + if !inPublics { + continue + } + fields := strings.Fields(line) + if len(fields) < 3 || !strings.Contains(fields[0], ":") { + continue + } + name := fields[1] + if !strings.HasPrefix(name, "BORINGSSL_bcm_") { + continue + } + rvaBase, err := strconv.ParseUint(fields[2], 16, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse Rva+Base for symbol %q: %s", name, err.Error()) + } + if _, exists := symbols[name]; exists { + return nil, fmt.Errorf("duplicate symbol %q in map file", name) + } + symbols[name] = rvaBase + } + + // Guard against silently-malformed map files: if we never saw the + // expected heading the caller will get confusing "symbol not found" + // errors downstream. Surface the real problem here instead. + if !sawPublicsHeading { + return nil, fmt.Errorf("map file %q does not contain a \"Publics by Value\" section; is this an MSVC-style linker map?", mapPath) + } + + return symbols, nil +} + +// ParsePE parses a PE file from raw bytes and extracts the image base. +func ParsePE(objectBytes []byte) (*PEInfo, error) { + peFile, err := pe.NewFile(bytes.NewReader(objectBytes)) + if err != nil { + return nil, fmt.Errorf("failed to parse PE: %s", err.Error()) + } + + var imageBase uint64 + switch oh := peFile.OptionalHeader.(type) { + case *pe.OptionalHeader64: + imageBase = oh.ImageBase + case *pe.OptionalHeader32: + imageBase = uint64(oh.ImageBase) + default: + return nil, errors.New("unsupported PE optional header type") + } + + return &PEInfo{File: peFile, ImageBase: imageBase}, nil +} + +// ResolveSymbolFileOffset converts a symbol's Rva+Base address (from a linker +// map file) to a file offset within the PE binary. +func (p *PEInfo) ResolveSymbolFileOffset(symbolAddrs map[string]uint64, name string) (uint64, error) { + addr, ok := symbolAddrs[name] + if !ok { + return 0, fmt.Errorf("symbol %q not found in map file", name) + } + if addr < p.ImageBase { + return 0, fmt.Errorf("symbol %q address 0x%x is below image base 0x%x", name, addr, p.ImageBase) + } + rva := addr - p.ImageBase + for _, s := range p.File.Sections { + start := uint64(s.VirtualAddress) + if rva >= start && rva < start+uint64(s.VirtualSize) { + offsetInSection := rva - start + if offsetInSection >= uint64(s.Size) { + return 0, fmt.Errorf("RVA 0x%x for %q is in zero-fill/BSS region of section %s (offset 0x%x exceeds raw data size 0x%x)", rva, name, s.Name, offsetInSection, s.Size) + } + return offsetInSection + uint64(s.Offset), nil + } + } + return 0, fmt.Errorf("RVA 0x%x for %q not found in any PE section", rva, name) +} diff --git a/util/fipstools/inject_hash/inject_hash.go b/util/fipstools/inject_hash/inject_hash.go index af1c80afa2d..dd351af266c 100644 --- a/util/fipstools/inject_hash/inject_hash.go +++ b/util/fipstools/inject_hash/inject_hash.go @@ -297,11 +297,71 @@ func doAppleOS(objectBytes []byte) ([]byte, []byte, error) { return moduleText, moduleROData, nil } -func do(outPath, oInput string, arInput string, appleOS bool) error { +func doWindows(objectBytes []byte, mapPath string) ([]byte, []byte, error) { + symbolAddrs, err := fipscommon.ParseMapFile(mapPath) + if err != nil { + return nil, nil, err + } + + peInfo, err := fipscommon.ParsePE(objectBytes) + if err != nil { + return nil, nil, err + } + + extractRegion := func(startSym, endSym string) ([]byte, error) { + startOff, err := peInfo.ResolveSymbolFileOffset(symbolAddrs, startSym) + if err != nil { + return nil, err + } + endOff, err := peInfo.ResolveSymbolFileOffset(symbolAddrs, endSym) + if err != nil { + return nil, err + } + if startOff >= endOff || endOff > uint64(len(objectBytes)) { + return nil, fmt.Errorf("invalid boundaries: start=0x%x end=0x%x filesize=%d", startOff, endOff, len(objectBytes)) + } + buf := make([]byte, endOff-startOff) + copy(buf, objectBytes[startOff:endOff]) + return buf, nil + } + + moduleText, err := extractRegion("BORINGSSL_bcm_text_start", "BORINGSSL_bcm_text_end") + if err != nil { + return nil, nil, err + } + + // The Windows FIPS build is always a shared library (see the `windowsOS` + // branch of `do` below, which rejects static inputs). In shared builds the + // runtime integrity check in bcm.c hashes the rodata region in addition + // to the text region, so the map file must contain both rodata markers. + // Silently skipping rodata here would produce a hash that disagrees with + // what the runtime computes and the DLL would fail the power-on self-test. + _, hasRodataStart := symbolAddrs["BORINGSSL_bcm_rodata_start"] + _, hasRodataEnd := symbolAddrs["BORINGSSL_bcm_rodata_end"] + if !hasRodataStart || !hasRodataEnd { + return nil, nil, errors.New("rodata markers missing from map file; Windows FIPS shared build requires both BORINGSSL_bcm_rodata_start and BORINGSSL_bcm_rodata_end") + } + moduleROData, err := extractRegion("BORINGSSL_bcm_rodata_start", "BORINGSSL_bcm_rodata_end") + if err != nil { + return nil, nil, err + } + + return moduleText, moduleROData, nil +} + +func do(outPath, oInput string, arInput string, appleOS bool, windowsOS bool, mapFile string) error { var objectBytes []byte var isStatic bool var perm os.FileMode + if appleOS && windowsOS { + return fmt.Errorf("-apple and -windows are mutually exclusive") + } + + if windowsOS && len(mapFile) == 0 { + return fmt.Errorf("-map is required when -windows is set") + } + if len(arInput) > 0 { isStatic = true @@ -313,6 +373,10 @@ func do(outPath, oInput string, arInput string, appleOS bool) error { return fmt.Errorf("only shared libraries can be handled on macOS/iOS") } + if windowsOS { + return fmt.Errorf("only shared libraries can be handled on Windows") + } + fi, err := os.Stat(arInput) if err != nil { return err @@ -354,7 +418,9 @@ func do(outPath, oInput string, arInput string, appleOS bool) error { var moduleText, moduleROData []byte var err error - if appleOS == true { + if windowsOS { + moduleText, moduleROData, err = doWindows(objectBytes, mapFile) + } else if appleOS { moduleText, moduleROData, err = doAppleOS(objectBytes) } else { moduleText, moduleROData, err = doLinux(objectBytes, isStatic) @@ -403,10 +469,12 @@ func main() { oInput := flag.String("in-object", "", "Path to a .o file") outPath := flag.String("o", "", "Path to output object") appleOS := flag.Bool("apple", false, "Whether the FIPS module is built for macOS/iOS or not.") + windowsOS := flag.Bool("windows", false, "Whether the FIPS module is built for Windows or not.") + mapFile := flag.String("map", "", "Path to linker .map file (required for Windows)") flag.Parse() - if err := do(*outPath, *oInput, *arInput, *appleOS); err != nil { + if err := do(*outPath, *oInput, *arInput, *appleOS, *windowsOS, *mapFile); err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) os.Exit(1) }