diff --git a/crypto/CMakeLists.txt b/crypto/CMakeLists.txt index 06f45de6226..d692fdfbd6f 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..630a444e58f 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/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/inject_hash/inject_hash.go b/util/fipstools/inject_hash/inject_hash.go index af1c80afa2d..436064f893a 100644 --- a/util/fipstools/inject_hash/inject_hash.go +++ b/util/fipstools/inject_hash/inject_hash.go @@ -12,12 +12,14 @@ import ( "crypto/sha256" "debug/elf" "debug/macho" + "debug/pe" "encoding/binary" "errors" "flag" "fmt" "io" "os" + "strconv" "strings" "github.com/aws/aws-lc/util/ar" @@ -297,11 +299,122 @@ func doAppleOS(objectBytes []byte) ([]byte, []byte, error) { return moduleText, moduleROData, nil } -func do(outPath, oInput string, arInput string, appleOS bool) error { +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) + // Symbol lines have format: SSSS:OOOOOOOO name RRRRRRRRRRRRRRRR Lib:Object + for _, line := range strings.Split(string(data), "\n") { + 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 + } + + return symbols, nil +} + +func doWindows(objectBytes []byte, mapPath string) ([]byte, []byte, error) { + symbolAddrs, err := parseMapFile(mapPath) + if err != nil { + return nil, nil, err + } + + peFile, err := pe.NewFile(bytes.NewReader(objectBytes)) + if err != nil { + return nil, 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, nil, errors.New("unsupported PE optional header type") + } + + resolveOffset := func(name string) (uint64, error) { + addr, ok := symbolAddrs[name] + if !ok { + return 0, fmt.Errorf("symbol %q not found in map file", name) + } + if addr < imageBase { + return 0, fmt.Errorf("symbol %q address 0x%x is below image base 0x%x", name, addr, imageBase) + } + rva := addr - imageBase + for _, s := range peFile.Sections { + start := uint64(s.VirtualAddress) + if rva >= start && rva < start+uint64(s.VirtualSize) { + return rva - start + uint64(s.Offset), nil + } + } + return 0, fmt.Errorf("RVA 0x%x for %q not found in any PE section", rva, name) + } + + extractRegion := func(startSym, endSym string) ([]byte, error) { + startOff, err := resolveOffset(startSym) + if err != nil { + return nil, err + } + endOff, err := resolveOffset(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 + } + + var moduleROData []byte + _, hasRodataStart := symbolAddrs["BORINGSSL_bcm_rodata_start"] + _, hasRodataEnd := symbolAddrs["BORINGSSL_bcm_rodata_end"] + if hasRodataStart != hasRodataEnd { + return nil, nil, errors.New("rodata marker presence inconsistent") + } + if hasRodataStart { + 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 windowsOS && len(mapFile) == 0 { + return fmt.Errorf("-map is required when -windows is set") + } + if len(arInput) > 0 { isStatic = true @@ -313,6 +426,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 +471,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 +522,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) }