diff --git a/crypto/CMakeLists.txt b/crypto/CMakeLists.txt index b8b6148ba3..f581aa4ce1 100644 --- a/crypto/CMakeLists.txt +++ b/crypto/CMakeLists.txt @@ -628,9 +628,34 @@ endfunction() 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 to capture the computed integrity value that bcm.o prints to generate the - # correct value in generated_fips_shared_support.c. See FIPS.md for a full explanation of the process + if(MSVC AND ARCH STREQUAL "aarch64") + # On ARM64 Windows, ASLR is mandatory and cannot be disabled. The linker + # resolves ADRP immediates differently when building two separate DLLs + # (precrypto.dll vs crypto.dll), even when the FIPS module code comes from + # the same bcm.lib. This makes the two-DLL capture_hash approach produce a + # hash mismatch. Instead we use a single-DLL approach: build crypto.dll + # once with a placeholder hash, run fips_empty_main.exe (which triggers the + # integrity check, computes the real hash, and prints it), then binary-patch + # the placeholder in crypto.dll with the captured hash. + 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) + + add_custom_command( + TARGET fips_empty_main POST_BUILD + COMMAND ${GO_EXECUTABLE} run + ${AWSLC_SOURCE_DIR}/util/fipstools/capture_hash/capture_hash.go + -in-executable $ + -patch-dll $ + WORKING_DIRECTORY ${AWSLC_SOURCE_DIR} + ) + elseif(MSVC) + # On x64 Windows we use capture_hash.go to capture the computed integrity + # value that the module prints on its first (failing) run, then embed that + # value in generated_fips_shared_support.c for the final crypto.dll. + # See FIPS.md for a full explanation of the process. build_libcrypto(NAME precrypto MODULE_SOURCE $) add_executable(fips_empty_main fipsmodule/fips_empty_main.c) target_link_libraries(fips_empty_main PUBLIC precrypto) @@ -638,8 +663,8 @@ if(FIPS_SHARED) add_custom_command(OUTPUT generated_fips_shared_support.c COMMAND ${GO_EXECUTABLE} run ${AWSLC_SOURCE_DIR}/util/fipstools/capture_hash/capture_hash.go - -in-executable $ > generated_fips_shared_support.c - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + -in-executable $ > ${CMAKE_CURRENT_BINARY_DIR}/generated_fips_shared_support.c + WORKING_DIRECTORY ${AWSLC_SOURCE_DIR} DEPENDS fips_empty_main ${AWSLC_SOURCE_DIR}/util/fipstools/capture_hash/capture_hash.go ) add_library( @@ -650,6 +675,7 @@ if(FIPS_SHARED) generated_fips_shared_support.c ${AWSLC_SOURCE_DIR}/crypto/fipsmodule/cpucap/cpucap.c ) + target_compile_definitions(generated_fipsmodule PRIVATE BORINGSSL_IMPLEMENTATION S2N_BN_HIDE_SYMBOLS) target_add_awslc_include_paths(TARGET generated_fipsmodule SCOPE PRIVATE) build_libcrypto(NAME crypto MODULE_SOURCE $ SET_OUTPUT_NAME) diff --git a/crypto/fipsmodule/bcm.c b/crypto/fipsmodule/bcm.c index 248c2c8a42..3c437383a5 100644 --- a/crypto/fipsmodule/bcm.c +++ b/crypto/fipsmodule/bcm.c @@ -32,6 +32,10 @@ #pragma data_seg(".fipsda$b") #pragma const_seg(".fipsco$b") #pragma bss_seg(".fipsbs$b") +// Explicitly declare the FIPS rodata section with correct attributes. This +// ensures the section is known to the compiler/linker even if #pragma const_seg +// is not fully supported (e.g. clang-cl on ARM64). +#pragma section(".fipsco$b", read) #endif #include diff --git a/crypto/fipsmodule/fips_shared_library_marker.c b/crypto/fipsmodule/fips_shared_library_marker.c index 504260d42d..9f7cde8d8f 100644 --- a/crypto/fipsmodule/fips_shared_library_marker.c +++ b/crypto/fipsmodule/fips_shared_library_marker.c @@ -23,6 +23,10 @@ #pragma data_seg(".fipsda$a") #pragma const_seg(".fipsco$a") #pragma bss_seg(".fipsbs$a") +// Declare the FIPS rodata section so that __declspec(allocate()) can reference +// it. This provides an explicit fallback in case #pragma const_seg is not fully +// supported by the compiler (e.g. clang-cl on ARM64). +#pragma section(".fipsco$a", read) #endif // Dummy but not empty function and array to avoid the compiler completely @@ -30,8 +34,11 @@ const uint8_t *BORINGSSL_bcm_text_start(void) { return NULL; } +#if defined(_MSC_VER) +__declspec(allocate(".fipsco$a")) +#endif const uint8_t BORINGSSL_bcm_rodata_start[16] = - {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; #elif defined(AWSLC_FIPS_SHARED_END) #if defined(_MSC_VER) @@ -39,6 +46,10 @@ const uint8_t BORINGSSL_bcm_rodata_start[16] = #pragma data_seg(".fipsda$z") #pragma const_seg(".fipsco$z") #pragma bss_seg(".fipsbs$z") +// Declare the FIPS rodata section so that __declspec(allocate()) can reference +// it. This provides an explicit fallback in case #pragma const_seg is not fully +// supported by the compiler (e.g. clang-cl on ARM64). +#pragma section(".fipsco$z", read) #endif // Dummy but not empty function and array to avoid the compiler completely @@ -46,12 +57,14 @@ const uint8_t BORINGSSL_bcm_rodata_start[16] = const uint8_t *BORINGSSL_bcm_text_end(void){ return NULL; } +#if defined(_MSC_VER) +__declspec(allocate(".fipsco$z")) +#endif const uint8_t BORINGSSL_bcm_rodata_end[16] = - {16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + {16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; #else #error "This file should be compiled only as part of the Shared FIPS build on macOS/iOS/Windows." #endif - diff --git a/crypto/fipsmodule/fips_shared_support.c b/crypto/fipsmodule/fips_shared_support.c index 54aa34b934..1aa049439e 100644 --- a/crypto/fipsmodule/fips_shared_support.c +++ b/crypto/fipsmodule/fips_shared_support.c @@ -14,6 +14,14 @@ #include +// Ensure that no ambient #pragma const_seg is active. BORINGSSL_bcm_text_hash +// MUST be placed in the default .rdata section, outside the FIPS module rodata +// boundary (.fipsco). If it were placed inside the FIPS rodata boundary, the +// integrity check would hash the expected value itself, creating a circular +// dependency that can never be satisfied. +#if defined(_MSC_VER) +#pragma const_seg() +#endif #if defined(BORINGSSL_FIPS) && defined(BORINGSSL_SHARED_LIBRARY) // BORINGSSL_bcm_text_hash is is default hash value for the FIPS integrity check diff --git a/util/fipstools/capture_hash/capture_hash.go b/util/fipstools/capture_hash/capture_hash.go index 18babd3654..7c34eee2ce 100644 --- a/util/fipstools/capture_hash/capture_hash.go +++ b/util/fipstools/capture_hash/capture_hash.go @@ -1,73 +1,165 @@ // 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, takes the correct -// fingerprint value, and generates a new C file that contains the correct fingerprint which is used to build the -// final libcrypto. +// 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. +// +// By default it generates a C source file on stdout containing the correct +// hash, which is used to build the final libcrypto (the two-DLL approach used +// on Windows x64). +// +// When -patch-dll is specified, it instead reads the given 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 might differ (as happens on ARM64 Windows where +// mandatory ASLR causes ADRP immediate differences between DLLs). package main import ( + "bytes" + "encoding/hex" "flag" "fmt" "os" "os/exec" "strings" + + "github.com/aws/aws-lc/util/fipstools/fipscommon" ) -const line0 = "AWS-LC FIPS failure caused by:" -const line1 = "FIPS integrity test failed." +const expectedFailureMsg = "FIPS integrity test failed." // This must match what is in crypto/fipsmodule/fips_shared_support.c -const line2 = "Expected: ae2cea2abda6f3ec977f9bf6949afc836827cba0a09f6b6fde52cde2cdff3180" -const hash_len = 64 +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) + } + cmd := exec.Command(*executable) out, err := cmd.CombinedOutput() if err == nil { - fmt.Fprintf(os.Stderr, string(out)) - panic("Executable did not fail as expected") + 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") - if len(lines) != 6 { - fmt.Fprintf(os.Stderr, string(out)) - panic(fmt.Sprintf("Expected 6 lines in output but got %d", len(lines))) + + 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 lines[0] != line0 { - fmt.Fprintf(os.Stderr, string(out)) - panic(fmt.Sprintf("Expected \"%s\" got \"%s\"", line0, lines[0])) + 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 lines[1] != line1 { - fmt.Fprintf(os.Stderr, string(out)) - panic(fmt.Sprintf("Expected \"%s\" got \"%s\"", line1, lines[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 lines[2] != line2 { - fmt.Fprintf(os.Stderr, string(out)) - panic(fmt.Sprintf("Expected \"%s\" got \"%s\"", line1, lines[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) } - hash := strings.Split(lines[3], " ")[1] - if len(hash) != hash_len { - fmt.Fprintf(os.Stderr, string(out)) - panic(fmt.Sprintf("Hash \"%s\" is %d long, expected %d", hash, len(hash), hash_len)) + 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.Printf(`// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. + 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 { + // Two-DLL mode: generate a C source file with the correct hash on stdout. + 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(hash); i += 2 { - fmt.Printf("0x%s, ", hash[i:i+2]) - } - fmt.Printf(` + for i := 0; i < len(hashHex); i += 2 { + fmt.Printf("0x%s, ", hashHex[i:i+2]) + } + fmt.Printf(` }; `) + } }