diff --git a/.github/workflows/windows-alt.yml b/.github/workflows/windows-alt.yml index bfdac615fb3..ca908e7298c 100644 --- a/.github/workflows/windows-alt.yml +++ b/.github/workflows/windows-alt.yml @@ -207,6 +207,32 @@ jobs: - name: x86_64-w64-mingw32 Build/Test run: ./tests/ci/run_cross_mingw_tests.sh x86_64 w64-mingw32 "-DCMAKE_BUILD_TYPE=Release" + cross-mingw-fips: + if: github.repository_owner == 'aws' + runs-on: ubuntu-24.04 + steps: + - name: Install Tools + run: | + set -ex + sudo apt-get update -o Acquire::Languages=none -o Acquire::Translation=none + sudo apt-get install --assume-yes --no-install-recommends software-properties-common + sudo add-apt-repository --yes ppa:longsleep/golang-backports + sudo dpkg --add-architecture i386 + sudo mkdir -pm755 /etc/apt/keyrings + sudo wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key + sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/noble/winehq-noble.sources + sudo apt-get update -o Acquire::Languages=none -o Acquire::Translation=none + sudo apt-get install --assume-yes --no-install-recommends build-essential cmake golang-go nasm clang wget mingw-w64 + sudo apt-get install --assume-yes --install-recommends winehq-stable wine-binfmt binfmt-support + sudo update-binfmts --display + sudo update-binfmts --disable + sudo update-binfmts --enable wine + sudo update-binfmts --display + sudo rm -rf /tmp/* + - uses: actions/checkout@v6 + - name: x86_64-w64-mingw32 FIPS shared Build/Test + run: + ./tests/ci/run_cross_mingw_fips_tests.sh x86_64 w64-mingw32 "-DCMAKE_BUILD_TYPE=Release" cross-clang-cl-ninja: # Cross-compile from Linux to x86_64-pc-windows-msvc with Ninja + clang-cl, # mirroring the default aws-lc-rs setup. This catches command-line quoting diff --git a/CMakeLists.txt b/CMakeLists.txt index 2305bdd7f8a..675b02736b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1063,15 +1063,31 @@ if(FIPS) add_definitions(-DBORINGSSL_FIPS) endif() -# The Android CMake files set -ffunction-sections and -fdata-sections, which -# is incompatible with FIPS_SHARED. -if(FIPS_SHARED AND ANDROID) +# -ffunction-sections / -fdata-sections are incompatible with FIPS_SHARED. +# On ELF, gcc_fips_shared.lds already handles this: it gathers known split +# sections into .text and /DISCARD/s anything unexpected. Android and MinGW +# lack that linker-script safety net (Android's NDK toolchain injects these +# flags externally; MinGW/PE has no linker script support), so we override +# at the compiler level. +if(FIPS_SHARED AND (ANDROID OR MINGW)) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-function-sections -fno-data-sections") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-function-sections -fno-data-sections") endif() +# GCC (-O2+) splits cold code into separate .text.unlikely/.text.* sections. +# On ELF, gcc_fips_shared.lds gathers these between the bcm text markers, but +# the MinGW path partial-links with a plain `ld -r` (PE has no linker script +# support), so any split-out section would land outside the integrity-checked +# region. Keep all module code in one contiguous .text bracketed by the markers. +if(FIPS_SHARED AND MINGW AND GCC) + set(CMAKE_C_FLAGS + "${CMAKE_C_FLAGS} -fno-reorder-functions -fno-reorder-blocks-and-partition") + set(CMAKE_CXX_FLAGS + "${CMAKE_CXX_FLAGS} -fno-reorder-functions -fno-reorder-blocks-and-partition") +endif() + if(OPENSSL_SMALL) add_definitions(-DOPENSSL_SMALL) # OPENSSL_SMALL implies disabling AVX-512 optimizations on x86_64 for size. @@ -1369,6 +1385,14 @@ endif() # via Web Workers and SharedArrayBuffer, configured through compiler flags. if(NOT CMAKE_SYSTEM_NAME MATCHES "^(Generic|Android|Emscripten)$") find_package(Threads REQUIRED) + if(MINGW AND FIPS) + # Statically link winpthread so the FIPS module has no DLL dependency on it. + # This overwrites (not appends) INTERFACE_LINK_LIBRARIES because + # find_package(Threads) already set it to -lpthread (dynamic); appending + # would leave the dynamic entry on the link line. + set_property(TARGET Threads::Threads PROPERTY + INTERFACE_LINK_LIBRARIES "-Wl,-Bstatic,-lwinpthread;-Wl,-Bdynamic") + endif() set(AWSLC_LINK_THREADS TRUE) # CMAKE_THREAD_LIBS_INIT contains the actual linker flags (e.g., -lpthread) # set by find_package(Threads). Use this for pkgconfig instead of the diff --git a/crypto/CMakeLists.txt b/crypto/CMakeLists.txt index ade407902fb..54bad19f6ef 100644 --- a/crypto/CMakeLists.txt +++ b/crypto/CMakeLists.txt @@ -673,12 +673,15 @@ if(FIPS_SHARED) if (APPLE) set(INJECT_HASH_APPLE_FLAG "-apple") endif() + if (MINGW) + set(INJECT_HASH_MINGW_FLAG "-mingw") + endif() add_custom_command( TARGET crypto POST_BUILD COMMAND ${GO_EXECUTABLE} run ${AWSLC_SOURCE_DIR}/util/fipstools/inject_hash/inject_hash.go - -o $ -in-object $ ${INJECT_HASH_APPLE_FLAG} + -o $ -in-object $ ${INJECT_HASH_APPLE_FLAG} ${INJECT_HASH_MINGW_FLAG} WORKING_DIRECTORY ${AWSLC_SOURCE_DIR} ) diff --git a/crypto/fipsmodule/CMakeLists.txt b/crypto/fipsmodule/CMakeLists.txt index cee9476d891..595051958f0 100644 --- a/crypto/fipsmodule/CMakeLists.txt +++ b/crypto/fipsmodule/CMakeLists.txt @@ -735,6 +735,72 @@ elseif(FIPS_SHARED) DEPENDS fips_msvc_start.obj fips_msvc_end.obj bcm_library WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) + elseif(MINGW) + # The PE linker does not support linker scripts. Use the same start/end + # marker object approach as macOS by compiling the marker file twice + # (start and end), then partial-link the three pieces in order so + # BORINGSSL_bcm_text_start/end bracket all BCM code. + set(BCM_NAME bcm.o) + # These custom commands invoke the compiler directly (outside CMake's normal + # target compilation), so we must forward CMAKE_C_FLAGS manually. This is + # necessary for cross-compilation where the flags contain --target and + # system include paths (e.g. clang targeting MinGW). + separate_arguments(FIPS_MARKER_C_FLAGS NATIVE_COMMAND "${CMAKE_C_FLAGS}") + + # Resolve an objdump for the target toolchain. It is used below to verify + # that the assembled module object keeps all code/rodata in the single + # .text/.rdata sections bracketed by the integrity markers. + get_filename_component(FIPS_LD_DIR "${CMAKE_LINKER}" DIRECTORY) + get_filename_component(FIPS_LD_NAME "${CMAKE_LINKER}" NAME) + + set(FIPS_MINGW_OBJDUMP "${CMAKE_OBJDUMP}") + if(NOT FIPS_MINGW_OBJDUMP) + # Derive from the linker (e.g. -ld -> -objdump). + string(REGEX REPLACE "ld(\\.exe)?$" "objdump" FIPS_OD_NAME "${FIPS_LD_NAME}") + find_program(FIPS_MINGW_OBJDUMP NAMES "${FIPS_OD_NAME}" objdump llvm-objdump + HINTS "${FIPS_LD_DIR}") + endif() + if(NOT FIPS_MINGW_OBJDUMP) + message(FATAL_ERROR + "FIPS MinGW build: could not find objdump to verify the module " + "integrity section layout. Set CMAKE_OBJDUMP to the target objdump.") + endif() + + set(FIPS_MINGW_OBJCOPY "${CMAKE_OBJCOPY}") + if(NOT FIPS_MINGW_OBJCOPY) + # Derive from the linker (e.g. -ld -> -objcopy). + string(REGEX REPLACE "ld(\\.exe)?$" "objcopy" FIPS_OC_NAME "${FIPS_LD_NAME}") + find_program(FIPS_MINGW_OBJCOPY NAMES "${FIPS_OC_NAME}" objcopy llvm-objcopy + HINTS "${FIPS_LD_DIR}") + endif() + if(NOT FIPS_MINGW_OBJCOPY) + message(FATAL_ERROR + "FIPS MinGW build: could not find objcopy to merge the module " + "read-only data sections. Set CMAKE_OBJCOPY to the target objcopy.") + endif() + + add_custom_command( + OUTPUT fips_mingw_start.o + COMMAND ${CMAKE_C_COMPILER} ${FIPS_MARKER_C_FLAGS} -w -c ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c -DAWSLC_FIPS_SHARED_START -o fips_mingw_start.o + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c + ) + add_custom_command( + OUTPUT fips_mingw_end.o + COMMAND ${CMAKE_C_COMPILER} ${FIPS_MARKER_C_FLAGS} -w -c ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c -DAWSLC_FIPS_SHARED_END -o fips_mingw_end.o + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c + ) + set(BCM_UNMERGED_NAME bcm.unmerged.o) + add_custom_command( + OUTPUT ${BCM_NAME} + COMMAND ${CMAKE_LINKER} -r fips_mingw_start.o --whole-archive $ --no-whole-archive fips_mingw_end.o -o ${BCM_UNMERGED_NAME} + COMMAND ${CMAKE_COMMAND} -DOBJDUMP=${FIPS_MINGW_OBJDUMP} -DOBJCOPY=${FIPS_MINGW_OBJCOPY} -DLINKER=${CMAKE_LINKER} -DINPUT_OBJECT=${BCM_UNMERGED_NAME} -DOUTPUT_OBJECT=${BCM_NAME} -P ${CMAKE_CURRENT_SOURCE_DIR}/merge_fips_mingw_rodata.cmake + # Fail the build if GCC split any module code/rodata into sections that + # would fall outside the integrity markers in the final DLL. + COMMAND ${CMAKE_COMMAND} -DOBJDUMP=${FIPS_MINGW_OBJDUMP} -DBCM_OBJECT=${BCM_NAME} -P ${CMAKE_CURRENT_SOURCE_DIR}/check_fips_mingw_sections.cmake + DEPENDS fips_mingw_start.o fips_mingw_end.o bcm_library ${CMAKE_CURRENT_SOURCE_DIR}/check_fips_mingw_sections.cmake ${CMAKE_CURRENT_SOURCE_DIR}/merge_fips_mingw_rodata.cmake + BYPRODUCTS ${BCM_UNMERGED_NAME} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) else() set(BCM_NAME bcm.o) # fips_shared.lds does not have 'clang' prefix because we want to keep merging any changes from upstream. diff --git a/crypto/fipsmodule/bcm.c b/crypto/fipsmodule/bcm.c index 00a58c284be..dea55b85ef9 100644 --- a/crypto/fipsmodule/bcm.c +++ b/crypto/fipsmodule/bcm.c @@ -8,6 +8,9 @@ #include #include +#if defined(__MINGW32__) && defined(BORINGSSL_SHARED_LIBRARY) +#include +#endif #if defined(BORINGSSL_FIPS) && !defined(OPENSSL_WINDOWS) #include #include @@ -15,8 +18,10 @@ // On Windows place the bcm code in a specific section that uses Grouped Sections // to control the order. $b section will place bcm in between the start/end markers -// which are in $a and $z. -#if defined(BORINGSSL_FIPS) && defined(OPENSSL_WINDOWS) +// which are in $a and $z. These #pragma directives are MSVC-specific, so this is +// gated on _MSC_VER (matches fips_shared_library_marker.c); the MinGW FIPS build +// instead brackets the module via marker objects partial-linked with `ld -r`. +#if defined(BORINGSSL_FIPS) && defined(_MSC_VER) #pragma code_seg(".fipstx$b") #pragma data_seg(".fipsda$b") #pragma const_seg(".fipsco$b") @@ -179,6 +184,136 @@ extern const uint8_t BORINGSSL_bcm_rodata_start[]; extern const uint8_t BORINGSSL_bcm_rodata_end[]; #endif +#if defined(__MINGW32__) && defined(BORINGSSL_SHARED_LIBRARY) +#if !defined(OPENSSL_64_BIT) +#error "FIPS shared MinGW builds are only supported on 64-bit (x86_64) targets." +#endif +// Defined in fips_shared_support.c and filled in by inject_hash.go with the +// link-time preferred PE image base used to normalize ASLR relocations. +extern const uint64_t BORINGSSL_bcm_preferred_base; +#define BORINGSSL_BCM_PREFERRED_BASE_UNSET UINT64_C(0xBADC0FFEE0DDF00D) + +// MinGW is the only supported FIPS target whose hashed module region contains +// base relocations: GCC emits .refptr indirection cells (and genuine imports +// such as the CRT's free) into .rdata, inside the integrity boundary. Every +// other toolchain keeps the hashed region relocation-free by construction +// (ELF/Mach-O segregate relocated read-only pointers into separate sections; +// MSVC does not emit .refptr cells), so they hash the region directly via the +// plain definition below. Here we recompute the hash as it would appear at the +// preferred image base by un-applying the ASLR load delta to each relocation. +static int hmac_update_module_region(HMAC_CTX *hmac_ctx, const uint8_t *start, + size_t len) { + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery(start, &mbi, sizeof(mbi)) == 0) { + return 0; + } + + const uint8_t *const image_base = (const uint8_t *)mbi.AllocationBase; + const IMAGE_DOS_HEADER *const dos_header = + (const IMAGE_DOS_HEADER *)image_base; + if (dos_header->e_magic != IMAGE_DOS_SIGNATURE) { + return 0; + } + + const IMAGE_NT_HEADERS *const nt_headers = + (const IMAGE_NT_HEADERS *)(image_base + dos_header->e_lfanew); + if (nt_headers->Signature != IMAGE_NT_SIGNATURE || + nt_headers->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) { + return 0; + } + + // Use the same link-time preferred base that inject_hash.go used when it + // hashed the on-disk PE image. Keeping this value in .fipshash avoids relying + // on loader-specific treatment of PE headers that are outside the module + // boundary. + if (BORINGSSL_bcm_preferred_base == BORINGSSL_BCM_PREFERRED_BASE_UNSET || + BORINGSSL_bcm_preferred_base == 0) { + return 0; + } + const uintptr_t preferred_base = (uintptr_t)BORINGSSL_bcm_preferred_base; + const uintptr_t load_delta = (uintptr_t)image_base - preferred_base; + const IMAGE_DATA_DIRECTORY *const reloc_dir = + &nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; + if (load_delta == 0 || reloc_dir->VirtualAddress == 0 || + reloc_dir->Size == 0) { + return HMAC_Update(hmac_ctx, start, len); + } + + const uintptr_t region_start = (uintptr_t)start; + const uintptr_t region_end = region_start + len; + if (region_end < region_start) { + return 0; + } + + const uint8_t *cursor = start; + const uint8_t *const end = start + len; + const uint8_t *reloc_block_ptr = image_base + reloc_dir->VirtualAddress; + const uint8_t *const reloc_end = reloc_block_ptr + reloc_dir->Size; + while (reloc_block_ptr + sizeof(IMAGE_BASE_RELOCATION) <= reloc_end) { + const IMAGE_BASE_RELOCATION *const reloc_block = + (const IMAGE_BASE_RELOCATION *)reloc_block_ptr; + if (reloc_block->SizeOfBlock < sizeof(IMAGE_BASE_RELOCATION) || + reloc_block_ptr + reloc_block->SizeOfBlock > reloc_end) { + return 0; + } + + const WORD *const entries = + (const WORD *)(reloc_block_ptr + sizeof(IMAGE_BASE_RELOCATION)); + const size_t entry_count = + (reloc_block->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / + sizeof(WORD); + for (size_t i = 0; i < entry_count; i++) { + const WORD entry = entries[i]; + const WORD type = entry >> 12; + const WORD offset = entry & 0x0fff; + + // IMAGE_REL_BASED_ABSOLUTE entries are block padding, not relocations. + if (type == IMAGE_REL_BASED_ABSOLUTE) { + continue; + } + const uintptr_t reloc_addr = + (uintptr_t)image_base + reloc_block->VirtualAddress + offset; + if (reloc_addr < region_start || reloc_addr >= region_end) { + continue; + } + // Inside the hashed region only 64-bit absolute relocations are expected. + // Anything else (or one that would run past the region end) cannot be + // reconciled, so fail closed rather than hash load-dependent bytes. + if (type != IMAGE_REL_BASED_DIR64 || + (size_t)(region_end - reloc_addr) < sizeof(uint64_t)) { + return 0; + } + + // PE base relocations are emitted in ascending address order; the cursor + // check enforces that and bounds the gap length below. + const uint8_t *const reloc_ptr = (const uint8_t *)reloc_addr; + if (reloc_ptr < cursor || + !HMAC_Update(hmac_ctx, cursor, (size_t)(reloc_ptr - cursor))) { + return 0; + } + + uint8_t adjusted[sizeof(uint64_t)]; + const uint64_t value = + CRYPTO_load_u64_le(reloc_ptr) - (uint64_t)load_delta; + CRYPTO_store_u64_le(adjusted, value); + if (!HMAC_Update(hmac_ctx, adjusted, sizeof(uint64_t))) { + return 0; + } + cursor = reloc_ptr + sizeof(uint64_t); + } + + reloc_block_ptr += reloc_block->SizeOfBlock; + } + + return HMAC_Update(hmac_ctx, cursor, (size_t)(end - cursor)); +} +#else +static int hmac_update_module_region(HMAC_CTX *hmac_ctx, const uint8_t *start, + size_t len) { + return HMAC_Update(hmac_ctx, start, len); +} +#endif + #define STRING_POINTER_LENGTH 18 #define MAX_FUNCTION_NAME 32 #define ASSERT_WITHIN_MSG "FIPS module doesn't span expected symbol (%s). Expected %p <= %p < %p\n" @@ -348,18 +483,29 @@ int BORINGSSL_integrity_test(void) { BORINGSSL_maybe_set_module_text_permissions(PROT_READ | PROT_EXEC); #endif #if defined(BORINGSSL_SHARED_LIBRARY) - uint64_t length = end - start; + size_t region_len = (size_t)(end - start); + uint64_t length = region_len; uint8_t buffer[sizeof(length)]; CRYPTO_store_u64_le(buffer, length); - HMAC_Update(&hmac_ctx, buffer, sizeof(length)); - HMAC_Update(&hmac_ctx, start, length); + if (!HMAC_Update(&hmac_ctx, buffer, sizeof(length)) || + !hmac_update_module_region(&hmac_ctx, start, region_len)) { + fprintf(stderr, "HMAC failed.\n"); + return 0; + } - length = rodata_end - rodata_start; + region_len = (size_t)(rodata_end - rodata_start); + length = region_len; CRYPTO_store_u64_le(buffer, length); - HMAC_Update(&hmac_ctx, buffer, sizeof(length)); - HMAC_Update(&hmac_ctx, rodata_start, length); + if (!HMAC_Update(&hmac_ctx, buffer, sizeof(length)) || + !hmac_update_module_region(&hmac_ctx, rodata_start, region_len)) { + fprintf(stderr, "HMAC failed.\n"); + return 0; + } #else - HMAC_Update(&hmac_ctx, start, end - start); + if (!hmac_update_module_region(&hmac_ctx, start, (size_t)(end - start))) { + fprintf(stderr, "HMAC failed.\n"); + return 0; + } #endif #if !defined(OPENSSL_WINDOWS) BORINGSSL_maybe_set_module_text_permissions(PROT_EXEC); diff --git a/crypto/fipsmodule/check_fips_mingw_sections.cmake b/crypto/fipsmodule/check_fips_mingw_sections.cmake new file mode 100644 index 00000000000..3108b18efef --- /dev/null +++ b/crypto/fipsmodule/check_fips_mingw_sections.cmake @@ -0,0 +1,97 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 OR ISC + +# Verifies that the partial-linked FIPS module object (bcm.o) does not contain +# any code or read-only data in sections that would fall outside the +# [BORINGSSL_bcm_text_start, BORINGSSL_bcm_text_end] (and rodata) integrity +# markers in the final DLL. +# +# Background: at -O2+ GCC partitions functions into hot/cold paths and moves +# cold code into .text.unlikely (and constructors/destructors into +# .text.startup/.text.exit). On ELF the FIPS linker script (gcc_fips_shared.lds) +# gathers these back between the markers and /DISCARD/s stray sections. The +# PE/COFF MinGW path has no such net: it partial-links with a plain `ld -r` and +# relies on GCC not emitting split sections (see -fno-reorder-functions / +# -fno-reorder-blocks-and-partition in the top-level CMakeLists.txt). If a split +# section ever reappears, the module would still pass its self-test (injection +# and the runtime check measure the same window) but would silently leave that +# code/data outside the integrity check. This guard turns that silent gap into a +# hard build failure. +# +# Required arguments (passed via -D): +# OBJDUMP - path to a binutils/llvm objdump for the target +# BCM_OBJECT - path to the partial-linked module object to inspect + +if(NOT OBJDUMP) + message(FATAL_ERROR "FIPS MinGW section check: OBJDUMP not provided") +endif() +if(NOT BCM_OBJECT) + message(FATAL_ERROR "FIPS MinGW section check: BCM_OBJECT not provided") +endif() + +execute_process( + COMMAND "${OBJDUMP}" -h "${BCM_OBJECT}" + OUTPUT_VARIABLE objdump_output + ERROR_VARIABLE objdump_error + RESULT_VARIABLE objdump_result +) +if(NOT objdump_result EQUAL 0) + message(FATAL_ERROR + "FIPS MinGW section check: failed to run '${OBJDUMP} -h ${BCM_OBJECT}'\n${objdump_error}") +endif() + +# Extract section names (the second whitespace-delimited column of `objdump -h`) +# and flag any that indicate split-out module code or data. Dollar-suffixed +# grouped .rdata sections contain MinGW refptr cells, so the build must merge +# them into .rdata before this check runs. +# +# MinGW may also emit .ctors/.dtors records for module constructors and +# destructors. Those are loader registration metadata analogous to ELF +# .init_array/.fini_array or MSVC .CRT$XCU records, and are intentionally not +# treated as module rodata by this check. +string(REPLACE "\n" ";" objdump_lines "${objdump_output}") +set(forbidden_sections "") +set(forbidden_section_metadata "") +set(current_section_name "") +foreach(line IN LISTS objdump_lines) + string(REGEX REPLACE "^[ \t]+" "" line "${line}") + if(line MATCHES "^[0-9]+[ \t]+([^ \t]+)") + set(section_name "${CMAKE_MATCH_1}") + set(current_section_name "${section_name}") + if(section_name MATCHES "^\\.text[\\.$]" OR section_name MATCHES "^\\.rdata[\\.$]") + list(APPEND forbidden_sections "${section_name}") + endif() + elseif(current_section_name STREQUAL ".rdata" AND line MATCHES "LINK_ONCE|COMDAT") + list(APPEND forbidden_section_metadata ".rdata: ${line}") + endif() +endforeach() + +if(forbidden_sections OR forbidden_section_metadata) + list(REMOVE_DUPLICATES forbidden_sections) + list(REMOVE_DUPLICATES forbidden_section_metadata) + string(REPLACE ";" ", " forbidden_pretty "${forbidden_sections}") + string(REPLACE ";" ", " forbidden_metadata_pretty "${forbidden_section_metadata}") + set(failure_details "") + if(forbidden_sections) + string(APPEND failure_details + "split-out section(s) outside the integrity markers: ${forbidden_pretty}.") + endif() + if(forbidden_section_metadata) + if(failure_details) + string(APPEND failure_details " ") + endif() + string(APPEND failure_details + "merged .rdata still has grouped-section metadata: ${forbidden_metadata_pretty}.") + endif() + message(FATAL_ERROR + "FIPS integrity check would be incomplete: '${BCM_OBJECT}' contains " + "${failure_details}\n" + "All module code must live in a single contiguous .text (and rodata in " + ".rdata) bracketed by BORINGSSL_bcm_text_start/end. Ensure the module is " + "built with -fno-reorder-functions -fno-reorder-blocks-and-partition " + "(and -fno-function-sections -fno-data-sections), and ensure grouped " + ".rdata$ sections are merged by merge_fips_mingw_rodata.cmake; see the " + "FIPS_SHARED MinGW blocks in the top-level CMakeLists.txt.") +endif() + +message(STATUS "FIPS MinGW section check passed for ${BCM_OBJECT}") diff --git a/crypto/fipsmodule/fips_shared_library_marker.c b/crypto/fipsmodule/fips_shared_library_marker.c index 3c8df98e76e..95c401e216a 100644 --- a/crypto/fipsmodule/fips_shared_library_marker.c +++ b/crypto/fipsmodule/fips_shared_library_marker.c @@ -13,6 +13,12 @@ #include #include +#if defined(__MINGW32__) +#define AWSLC_MINGW_RODATA_END __attribute__((section(".rdata$zzzz"), used)) +#else +#define AWSLC_MINGW_RODATA_END +#endif + #if defined(AWSLC_FIPS_SHARED_START) #if defined(_MSC_VER) #pragma code_seg(".fipstx$a") @@ -56,6 +62,7 @@ const uint8_t *BORINGSSL_bcm_text_end(void){ #if defined(_MSC_VER) __declspec(allocate(".fipsco$z")) #endif +AWSLC_MINGW_RODATA_END const uint8_t BORINGSSL_bcm_rodata_end[16] = {16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; diff --git a/crypto/fipsmodule/fips_shared_support.c b/crypto/fipsmodule/fips_shared_support.c index 65bd0ea90f2..0e3c1aab9ee 100644 --- a/crypto/fipsmodule/fips_shared_support.c +++ b/crypto/fipsmodule/fips_shared_support.c @@ -2,10 +2,9 @@ // SPDX-License-Identifier: ISC // 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. +// MUST be placed outside the FIPS module rodata boundary. 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 @@ -13,15 +12,40 @@ #if defined(BORINGSSL_FIPS) && defined(BORINGSSL_SHARED_LIBRARY) #include -// BORINGSSL_bcm_text_hash is is default hash value for the FIPS integrity check -// that must be replaced with the real value during the build process. This -// value need only be distinct, i.e. so that we can safely search-and-replace it -// in an object file. -const uint8_t BORINGSSL_bcm_text_hash[32] = { +#if defined(__MINGW32__) +// MinGW brackets the module rodata with symbols in .rdata. Keep the expected +// hash in a separate read-only PE section so it cannot be included in the +// integrity input. +#define AWSLC_FIPS_HASH_SECTION __attribute__((section(".fipshash"), used)) +#else +#define AWSLC_FIPS_HASH_SECTION +#endif + +// BORINGSSL_bcm_text_hash is the default hash value for the FIPS integrity +// check that must be replaced with the real value during the build process. +// This value need only be distinct, i.e. so that we can safely +// search-and-replace it in an object file. +const uint8_t BORINGSSL_bcm_text_hash[32] AWSLC_FIPS_HASH_SECTION = { 0xae, 0x2c, 0xea, 0x2a, 0xbd, 0xa6, 0xf3, 0xec, 0x97, 0x7f, 0x9b, 0xf6, 0x94, 0x9a, 0xfc, 0x83, 0x68, 0x27, 0xcb, 0xa0, 0xa0, 0x9f, 0x6b, 0x6f, 0xde, 0x52, 0xcd, 0xe2, 0xcd, 0xff, 0x31, 0x80, }; + +#if defined(__MINGW32__) +// BORINGSSL_bcm_preferred_base records the link-time (preferred) PE image base +// of the crypto DLL. When a module is relocated for ASLR, the runtime integrity +// check subtracts the load delta from relocated cells before hashing, matching +// the on-disk preferred-base bytes that inject_hash.go measured. Like +// BORINGSSL_bcm_text_hash it lives outside the hashed module boundary, and +// because it is an integer (not a pointer) it carries no base relocation of its +// own, so its value is identical on disk and in memory. +// +// The initializer is a recognizable sentinel: if it survives to runtime, base +// injection did not run and the integrity check fails closed. +#define BORINGSSL_BCM_PREFERRED_BASE_UNSET UINT64_C(0xBADC0FFEE0DDF00D) +const uint64_t BORINGSSL_bcm_preferred_base AWSLC_FIPS_HASH_SECTION = + BORINGSSL_BCM_PREFERRED_BASE_UNSET; +#endif #else // C requires a translation unit to contain at least one declaration. Since // BORINGSSL_FIPS or BORINGSSL_SHARED_LIBRARY is not defined, this file is diff --git a/crypto/fipsmodule/merge_fips_mingw_rodata.cmake b/crypto/fipsmodule/merge_fips_mingw_rodata.cmake new file mode 100644 index 00000000000..1d91406b2a0 --- /dev/null +++ b/crypto/fipsmodule/merge_fips_mingw_rodata.cmake @@ -0,0 +1,97 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 OR ISC + +# MinGW emits external address cells in grouped .rdata$* sections. They are part +# of the FIPS module's read-only data, so merge them into .rdata before the final +# DLL link. The end marker is compiled into .rdata$zzzz, so it remains after the +# merged grouped sections. + +foreach(required_var OBJDUMP OBJCOPY LINKER INPUT_OBJECT OUTPUT_OBJECT) + if(NOT DEFINED ${required_var} OR "${${required_var}}" STREQUAL "") + message(FATAL_ERROR + "FIPS MinGW rodata merge: ${required_var} not provided") + endif() +endforeach() + +execute_process( + COMMAND "${OBJDUMP}" -h "${INPUT_OBJECT}" + OUTPUT_VARIABLE objdump_output + ERROR_VARIABLE objdump_error + RESULT_VARIABLE objdump_result +) +if(NOT objdump_result EQUAL 0) + message(FATAL_ERROR + "FIPS MinGW rodata merge: failed to run '${OBJDUMP} -h ${INPUT_OBJECT}'\n${objdump_error}") +endif() + +string(REPLACE "\n" ";" objdump_lines "${objdump_output}") +set(objcopy_args "") +foreach(line IN LISTS objdump_lines) + string(REGEX REPLACE "^[ \t]+" "" line "${line}") + string(REGEX REPLACE "[ \t]+" ";" fields "${line}") + list(LENGTH fields field_count) + if(field_count GREATER 1) + list(GET fields 1 section_name) + if(section_name MATCHES "^\\.rdata\\$") + list(APPEND objcopy_args "--rename-section" "${section_name}=.rdata") + endif() + endif() +endforeach() + +if(NOT objcopy_args) + execute_process( + COMMAND "${CMAKE_COMMAND}" -E copy "${INPUT_OBJECT}" "${OUTPUT_OBJECT}" + ERROR_VARIABLE copy_error + RESULT_VARIABLE copy_result + ) + if(NOT copy_result EQUAL 0) + message(FATAL_ERROR + "FIPS MinGW rodata merge: failed to copy '${INPUT_OBJECT}' to '${OUTPUT_OBJECT}'\n${copy_error}") + endif() + return() +endif() + +set(renamed_object "${OUTPUT_OBJECT}.rdata_renamed.tmp") +set(cleaned_object "${OUTPUT_OBJECT}.rdata_cleaned.tmp") +file(REMOVE "${renamed_object}" "${cleaned_object}" "${OUTPUT_OBJECT}") + +execute_process( + COMMAND "${OBJCOPY}" ${objcopy_args} --wildcard --localize-symbol=.refptr.* "${INPUT_OBJECT}" "${renamed_object}" + ERROR_VARIABLE objcopy_error + RESULT_VARIABLE objcopy_result +) +if(NOT objcopy_result EQUAL 0) + message(FATAL_ERROR + "FIPS MinGW rodata merge: failed to rename grouped .rdata sections\n${objcopy_error}") +endif() + +# Re-link the renamed object to coalesce the duplicate .rdata sections into one +# contiguous section. Warnings from intermediate COMDAT section names are +# expected after the rename and are intentionally not surfaced on success. +execute_process( + COMMAND "${LINKER}" -r "${renamed_object}" -o "${OUTPUT_OBJECT}" + ERROR_VARIABLE linker_error + RESULT_VARIABLE linker_result +) +file(REMOVE "${renamed_object}") +if(NOT linker_result EQUAL 0) + message(FATAL_ERROR + "FIPS MinGW rodata merge: failed to merge grouped .rdata sections\n${linker_error}") +endif() + +# The rename can leave the merged .rdata section marked LINK_ONCE/COMDAT. Clear +# that metadata so the final DLL link emits base-relocation entries for absolute +# refptr cells inside the FIPS rodata window. +execute_process( + COMMAND "${OBJCOPY}" + --set-section-flags ".rdata=alloc,load,readonly,data,contents" + "${OUTPUT_OBJECT}" "${cleaned_object}" + ERROR_VARIABLE objcopy_flags_error + RESULT_VARIABLE objcopy_flags_result +) +if(NOT objcopy_flags_result EQUAL 0) + message(FATAL_ERROR + "FIPS MinGW rodata merge: failed to clear merged .rdata flags\n${objcopy_flags_error}") +endif() +file(REMOVE "${OUTPUT_OBJECT}") +file(RENAME "${cleaned_object}" "${OUTPUT_OBJECT}") diff --git a/crypto/internal.h b/crypto/internal.h index 1f7ad67816a..2de8195be82 100644 --- a/crypto/internal.h +++ b/crypto/internal.h @@ -34,9 +34,7 @@ #endif #endif -#if defined(OPENSSL_THREADS) && \ - (!defined(OPENSSL_WINDOWS) || \ - (defined(__MINGW32__) && !defined(__clang__))) +#if defined(OPENSSL_THREADS) && !defined(OPENSSL_WINDOWS) #include #define OPENSSL_PTHREADS #endif diff --git a/crypto/thread_win.c b/crypto/thread_win.c index 10082fbc05f..b3c64230d5f 100644 --- a/crypto/thread_win.c +++ b/crypto/thread_win.c @@ -183,6 +183,7 @@ __pragma(comment( #ifdef _WIN64 // .CRT section is merged with .rdata on x64 so it must be constant data. +#if defined(_MSC_VER) || defined(__clang__) #pragma const_seg(".CRT$XLC") // When defining a const variable, it must have external linkage to be sure the // linker doesn't discard it. @@ -190,13 +191,27 @@ extern const PIMAGE_TLS_CALLBACK p_thread_callback_boringssl; const PIMAGE_TLS_CALLBACK p_thread_callback_boringssl = thread_local_destructor; // Reset the default section. #pragma const_seg() +#else +// GCC (e.g. MinGW-GCC) does not support the const_seg/data_seg pragmas, so use +// the section attribute to place the callback in the .CRT$XLC callback array. +// 'used' keeps the optimizer from discarding it (the linker /INCLUDE pragmas +// above are likewise MSVC-only). +extern const PIMAGE_TLS_CALLBACK p_thread_callback_boringssl; +const PIMAGE_TLS_CALLBACK p_thread_callback_boringssl + __attribute__((section(".CRT$XLC"), used)) = thread_local_destructor; +#endif // _MSC_VER || __clang__ #else +#if defined(_MSC_VER) || defined(__clang__) #pragma data_seg(".CRT$XLC") PIMAGE_TLS_CALLBACK p_thread_callback_boringssl = thread_local_destructor; // Reset the default section. #pragma data_seg() +#else +PIMAGE_TLS_CALLBACK p_thread_callback_boringssl + __attribute__((section(".CRT$XLC"), used)) = thread_local_destructor; +#endif // _MSC_VER || __clang__ #endif // _WIN64 diff --git a/tests/ci/run_cross_mingw_fips_tests.sh b/tests/ci/run_cross_mingw_fips_tests.sh new file mode 100755 index 00000000000..e33a494bd40 --- /dev/null +++ b/tests/ci/run_cross_mingw_fips_tests.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 OR ISC + +set -ex + +# Cross-compiles a FIPS *shared* (DLL) build of AWS-LC for Windows using the +# mingw-w64 toolchain, then exercises the FIPS integrity self-test under Wine. +# This is the cross-compilation analogue of the native clang-cl FIPS shared +# coverage, and validates the PE/COFF hash-injection path in inject_hash.go as +# well as the MinGW-specific CMake plumbing (bcm marker objects, statically +# linked winpthread). + +TARGET_CPU="${1}" +TARGET_PLATFORM="${2}" +BUILD_OPTIONS=("${@:3:5}") +# MinGW FIPS shared requires a reasonably recent binutils (>= ~2.41, as shipped +# by Ubuntu 24.04 / mingw-w64 gcc-13). Older linkers (e.g. binutils 2.38 on +# Ubuntu 22.04) drop the dllexport directives (.drectve) for the FIPS module's +# symbols during the bcm.o partial link, leaving libcrypto.dll under-exported. +# This is a new build target with no existing consumers, so we set the floor +# here rather than fall back to -Wl,--export-all-symbols. +GCC_VERSION="13" +THREAD_MODEL="posix" + +if [ "${#BUILD_OPTIONS[@]}" -lt 1 ]; then + echo "Must pass build parameters" + exit 1 +fi + +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +SCRIPT_DIR="$(readlink -f "${SCRIPT_DIR}")" + +source "${SCRIPT_DIR}/common_posix_setup.sh" +source "${SCRIPT_DIR}/gtest_util.sh" + +# Assumes script is executed from the root of aws-lc directory +SCRATCH_FOLDER="${SYS_ROOT}/SCRATCH_${TARGET_CPU}_FIPS" + +if [ -e "${SCRATCH_FOLDER}" ]; then + # Some directories in the archive lack write permission, preventing deletion of files + chmod +w -R "${SCRATCH_FOLDER}" + rm -rf "${SCRATCH_FOLDER:?}" +fi +mkdir -p "${SCRATCH_FOLDER}" + +pushd "${SCRATCH_FOLDER}" + +# Note: unlike the static cross-mingw build, we do NOT set +# CMAKE_EXE_LINKER_FLAGS=--static here. A FIPS shared build links the test +# executables against the crypto DLL, so the executables (and the DLL) must be +# able to locate their runtime dependencies at load time. winpthread is pulled +# statically into the crypto DLL itself (see the MINGW AND FIPS branch in the +# top-level CMakeLists.txt); the remaining mingw runtime DLLs are made +# discoverable through WINEPATH below. +cat < ${TARGET_CPU}-${TARGET_PLATFORM}.cmake +# Specify the target system +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR ${TARGET_CPU}) + +# Specify the cross-compiler +set(CMAKE_C_COMPILER /usr/bin/${TARGET_CPU}-${TARGET_PLATFORM}-gcc-${THREAD_MODEL}) +set(CMAKE_CXX_COMPILER /usr/bin/${TARGET_CPU}-${TARGET_PLATFORM}-g++-${THREAD_MODEL}) + +# Specify the sysroot for the target system +set(CMAKE_SYSROOT /usr/lib/gcc/${TARGET_CPU}-${TARGET_PLATFORM}/${GCC_VERSION}-${THREAD_MODEL}/) +set(CMAKE_SYSTEM_INCLUDE_PATH /usr/lib/gcc/${TARGET_CPU}-${TARGET_PLATFORM}/${GCC_VERSION}-${THREAD_MODEL}/include) + +set(CMAKE_FIND_ROOT_PATH /usr/lib/gcc/${TARGET_CPU}-${TARGET_PLATFORM}/${GCC_VERSION}-${THREAD_MODEL}/) +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + +set(CMAKE_GENERATOR Ninja) +EOF + +cat ${TARGET_CPU}-${TARGET_PLATFORM}.cmake + +function wine_z_path { + local unix_path="${1%/}" + printf 'Z:%s' "${unix_path//\//\\}" +} + +# Make the cross-compiled crypto/ssl DLLs, generated test DLLs, and the mingw +# runtime DLLs (libstdc++, libgcc_s, libwinpthread) discoverable by Wine when it +# loads the test executables. WINEPATH is a semicolon-separated list of +# Windows-style paths. +export WINEPATH="$(wine_z_path "${BUILD_ROOT}")" +WINEPATH+=";$(wine_z_path "${BUILD_ROOT}/crypto")" +WINEPATH+=";$(wine_z_path "${BUILD_ROOT}/ssl")" +WINEPATH+=";$(wine_z_path "/usr/lib/gcc/${TARGET_CPU}-${TARGET_PLATFORM}/${GCC_VERSION}-${THREAD_MODEL}")" +WINEPATH+=";$(wine_z_path "/usr/${TARGET_CPU}-${TARGET_PLATFORM}/lib")" +# Keep Wine quiet and non-interactive in CI. +export WINEDEBUG="-all" + +echo "Testing AWS-LC FIPS shared library for ${TARGET_CPU}." + +for BO in "${BUILD_OPTIONS[@]}"; do + run_build \ + -DCMAKE_TOOLCHAIN_FILE="${SCRATCH_FOLDER}/${TARGET_CPU}-${TARGET_PLATFORM}.cmake" \ + -DFIPS=1 \ + -DBUILD_SHARED_LIBS=1 \ + ${BO} + + # The module's FIPS status can be verified by 'tool/bssl isfips'. This must + # report FIPS mode is enabled for a successful shared FIPS build. + module_status=$(${BUILD_ROOT}/tool/bssl.exe isfips) + [[ "${module_status}" == "1" ]] || { echo >&2 "FIPS Mode validation failed."; exit 1; } + + # Positive test: test_fips exercises the power-on self-test, including the + # runtime integrity check that recomputes the module hash. This validates that + # inject_hash.go wrote a correct hash into the PE/COFF DLL. + ${BUILD_ROOT}/util/fipstools/test_fips.exe + + # Negative test: corrupt the FIPS module inside the crypto DLL and confirm the + # runtime integrity check detects it (test_fips must now FAIL). This proves the + # integrity check actually runs and has teeth on the MinGW PE/COFF DLL, rather + # than merely being self-consistent with inject_hash.go (both measure the same + # window, so a passing positive test alone cannot distinguish a working check + # from a no-op one). break-hash.go -mingw locates the module via the PE/COFF + # symbol table (no .map file) and flips a byte inside + # [BORINGSSL_bcm_text_start, BORINGSSL_bcm_text_end]. + crypto_dll="${BUILD_ROOT}/crypto/libcrypto.dll" + if [ ! -f "${crypto_dll}" ]; then + echo >&2 "Expected crypto DLL not found at ${crypto_dll}"; exit 1 + fi + cp "${crypto_dll}" "${crypto_dll}.bak" + (cd "${SRC_ROOT}" && go run util/fipstools/break-hash.go -mingw "${crypto_dll}" "${crypto_dll}.corrupted") + cp "${crypto_dll}.corrupted" "${crypto_dll}" + # test_fips is expected to fail here; disable errexit so we can capture its + # exit code instead of aborting the script. + set +e + ${BUILD_ROOT}/util/fipstools/test_fips.exe + fips_negative_rc=$? + set -e + # Restore the pristine DLL before asserting, so a failure does not leave a + # corrupted DLL behind for the gtest runs below or for local re-runs. + cp "${crypto_dll}.bak" "${crypto_dll}" + rm -f "${crypto_dll}.bak" "${crypto_dll}.corrupted" + if [ "${fips_negative_rc}" -eq 0 ]; then + echo >&2 "FIPS integrity negative test failed: test_fips should have failed with a corrupted crypto DLL." + exit 1 + fi + + # Confirm the module loads and basic functionality works through the DLL. + shard_gtest "${BUILD_ROOT}/crypto/crypto_test.exe --gtest_also_run_disabled_tests" + shard_gtest ${BUILD_ROOT}/ssl/ssl_test.exe +done +popd diff --git a/third_party/jitterentropy/CMakeLists.txt b/third_party/jitterentropy/CMakeLists.txt index ce4825fb98a..7d5326b184e 100644 --- a/third_party/jitterentropy/CMakeLists.txt +++ b/third_party/jitterentropy/CMakeLists.txt @@ -20,7 +20,10 @@ if(WIN32) if(MSVC) set(JITTER_COMPILE_FLAGS "/Od /W4 /DYNAMICBASE /DAWSLC") else() - set(JITTER_COMPILE_FLAGS "-DAWSLC -fwrapv --param ssp-buffer-size=4 -fvisibility=hidden -Wcast-align -Wmissing-field-initializers -Wshadow -Wswitch-enum -Wextra -Wall -pedantic -O0 -fwrapv -Wconversion") + # Without BORINGSSL_IMPLEMENTATION, on MinGW shared builds, + # symbols are treated as imported rather than exported, causing + # undefined references at link time. + set(JITTER_COMPILE_FLAGS "-DAWSLC -DBORINGSSL_IMPLEMENTATION -fwrapv --param ssp-buffer-size=4 -fvisibility=hidden -Wcast-align -Wmissing-field-initializers -Wshadow -Wswitch-enum -Wextra -Wall -pedantic -O0 -fwrapv -Wconversion") endif() else() set(CMAKE_POSITION_INDEPENDENT_CODE true) diff --git a/third_party/jitterentropy/jitterentropy-library/jitterentropy.h b/third_party/jitterentropy/jitterentropy-library/jitterentropy.h index ec6f5bc2832..52e79ef6af0 100644 --- a/third_party/jitterentropy/jitterentropy-library/jitterentropy.h +++ b/third_party/jitterentropy/jitterentropy-library/jitterentropy.h @@ -412,7 +412,7 @@ struct rand_data #ifdef JENT_PRIVATE_COMPILE # define JENT_PRIVATE_STATIC static #else /* JENT_PRIVATE_COMPILE */ -#if defined(_MSC_VER) +#if defined(_WIN32) #define JENT_PRIVATE_STATIC __declspec(dllexport) #else #define JENT_PRIVATE_STATIC __attribute__((visibility("default"))) diff --git a/util/fipstools/break-hash.go b/util/fipstools/break-hash.go index 6d12e0a52e3..37202a777d2 100644 --- a/util/fipstools/break-hash.go +++ b/util/fipstools/break-hash.go @@ -12,6 +12,7 @@ import ( "crypto/hmac" "crypto/sha512" "debug/elf" + "debug/pe" "encoding/hex" "errors" "flag" @@ -140,7 +141,80 @@ func doPE(objectBytes []byte, mapPath string) (int, []byte, error) { return int(startOffset), moduleText, nil } -func do(outPath, inPath, mapPath string) error { +func doMingw(objectBytes []byte) (int, []byte, error) { + // The MinGW FIPS DLL is not built with a linker .map file, so (unlike + // doPE) we locate the module from the PE/COFF symbol table directly, the + // same way inject_hash.go does for MinGW. COFF symbol values are offsets + // from the start of their section, which translates directly to an on-disk + // location via the section's raw-data file offset. + object, err := pe.NewFile(bytes.NewReader(objectBytes)) + if err != nil { + return 0, nil, errors.New("failed to parse object: " + err.Error()) + } + + var textSection *pe.Section + var textSectionIndex int + for i, section := range object.Sections { + if section.Name == ".text" { + textSection = section + // COFF section numbers are 1-based. + textSectionIndex = i + 1 + break + } + } + if textSection == nil { + return 0, nil, errors.New("failed to find .text section in object") + } + + symbols := object.Symbols + if symbols == nil { + return 0, nil, errors.New("no symbol table found in object") + } + + var startSeen, endSeen bool + var start, end uint64 + for _, symbol := range symbols { + if int(symbol.SectionNumber) != textSectionIndex { + continue + } + switch symbol.Name { + case "BORINGSSL_bcm_text_start": + if startSeen { + return 0, nil, errors.New("duplicate start symbol found") + } + startSeen = true + start = uint64(symbol.Value) + case "BORINGSSL_bcm_text_end": + if endSeen { + return 0, nil, errors.New("duplicate end symbol found") + } + endSeen = true + end = uint64(symbol.Value) + } + } + + if !startSeen || !endSeen { + return 0, nil, errors.New("could not find module in object") + } + if start >= end || end > uint64(textSection.Size) { + return 0, nil, fmt.Errorf("invalid module boundaries: start=0x%x end=0x%x textsize=0x%x", start, end, textSection.Size) + } + + // Section.Offset is the file offset of the section's raw data + // (PointerToRawData), so the module's on-disk start is that plus the + // section-relative symbol offset. + fileOffset := uint64(textSection.Offset) + start + if fileOffset+(end-start) > uint64(len(objectBytes)) { + return 0, nil, errors.New("module extends past end of file") + } + + moduleText := make([]byte, end-start) + copy(moduleText, objectBytes[fileOffset:fileOffset+(end-start)]) + + return int(fileOffset), moduleText, nil +} + +func do(outPath, inPath, mapPath string, mingw bool) error { objectBytes, err := os.ReadFile(inPath) if err != nil { return err @@ -149,7 +223,9 @@ func do(outPath, inPath, mapPath string) error { var fileOffset int var moduleText []byte - if mapPath != "" { + if mingw { + fileOffset, moduleText, err = doMingw(objectBytes) + } else if mapPath != "" { fileOffset, moduleText, err = doPE(objectBytes, mapPath) } else { fileOffset, moduleText, err = doELF(objectBytes) @@ -182,9 +258,10 @@ func do(outPath, inPath, mapPath string) error { } func main() { - mapPath := flag.String("map", "", "Path to linker .map file (required for Windows PE/DLL)") + mapPath := flag.String("map", "", "Path to linker .map file (required for Windows MSVC PE/DLL)") + mingw := flag.Bool("mingw", false, "Whether the FIPS module is a Windows MinGW DLL (uses the PE/COFF symbol table instead of a .map file)") flag.Usage = func() { - fmt.Fprintf(os.Stderr, "Usage: %s [-map mapfile] \n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Usage: %s [-map mapfile] [-mingw] \n", os.Args[0]) flag.PrintDefaults() } flag.Parse() @@ -195,7 +272,7 @@ func main() { os.Exit(1) } - if err := do(args[1], args[0], *mapPath); err != nil { + if err := do(args[1], args[0], *mapPath, *mingw); err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) os.Exit(1) } diff --git a/util/fipstools/inject_hash/inject_hash.go b/util/fipstools/inject_hash/inject_hash.go index dd351af266c..3d3c2a59277 100644 --- a/util/fipstools/inject_hash/inject_hash.go +++ b/util/fipstools/inject_hash/inject_hash.go @@ -12,6 +12,7 @@ import ( "crypto/sha256" "debug/elf" "debug/macho" + "debug/pe" "encoding/binary" "errors" "flag" @@ -349,18 +350,179 @@ func doWindows(objectBytes []byte, mapPath string) ([]byte, []byte, error) { return moduleText, moduleROData, nil } -func do(outPath, oInput string, arInput string, appleOS bool, windowsOS bool, mapFile string) error { +func doMingw(objectBytes []byte) ([]byte, []byte, error) { + object, err := pe.NewFile(bytes.NewReader(objectBytes)) + if err != nil { + return nil, nil, errors.New("failed to parse object: " + err.Error()) + } + + // Find .text and .rdata sections. Windows uses .rdata for read-only data. + var textSection, rodataSection *pe.Section + var textSectionIndex, rodataSectionIndex int + + for i, section := range object.Sections { + switch section.Name { + case ".text": + textSection = section + textSectionIndex = i + 1 + case ".rdata": + rodataSection = section + rodataSectionIndex = i + 1 + } + } + + if textSection == nil { + return nil, nil, errors.New("failed to find .text section in object") + } + + var textStart, textEnd, rodataStart, rodataEnd *uint64 + + symbols := object.Symbols + if symbols == nil { + return nil, nil, errors.New("no symbol table found in object") + } + + for _, symbol := range symbols { + // Only consider symbols defined in .text or, if present, .rdata. COFF + // section numbers are 1-based; undefined symbols carry section number 0, + // which must not be mistaken for a real section when .rdata is absent. + sn := int(symbol.SectionNumber) + if sn != textSectionIndex && (rodataSection == nil || sn != rodataSectionIndex) { + continue + } + + // In COFF, symbol.Value is the offset from the start of the section, + // not an RVA. + offset := uint64(symbol.Value) + + switch symbol.Name { + case "BORINGSSL_bcm_text_start": + if textStart != nil { + return nil, nil, errors.New("duplicate start symbol found") + } + textStart = &offset + case "BORINGSSL_bcm_text_end": + if textEnd != nil { + return nil, nil, errors.New("duplicate end symbol found") + } + textEnd = &offset + case "BORINGSSL_bcm_rodata_start": + if rodataStart != nil { + return nil, nil, errors.New("duplicate rodata start symbol found") + } + rodataStart = &offset + case "BORINGSSL_bcm_rodata_end": + if rodataEnd != nil { + return nil, nil, errors.New("duplicate rodata end symbol found") + } + rodataEnd = &offset + } + } + + if textStart == nil || textEnd == nil { + return nil, nil, errors.New("could not find .text module boundaries in object") + } + + if (rodataStart == nil) != (rodataSection == nil) { + return nil, nil, errors.New("rodata start marker inconsistent with .rdata section presence") + } + + if (rodataStart != nil) != (rodataEnd != nil) { + return nil, nil, errors.New("rodata marker presence inconsistent") + } + + if max := uint64(textSection.Size); *textStart > max || *textStart > *textEnd || *textEnd > max { + return nil, nil, fmt.Errorf("invalid module .text boundaries: start: %x, end: %x, max: %x", *textStart, *textEnd, max) + } + + if rodataStart != nil { + if max := uint64(rodataSection.Size); *rodataStart > max || *rodataStart > *rodataEnd || *rodataEnd > max { + return nil, nil, fmt.Errorf("invalid module .rdata boundaries: start: %x, end: %x, max: %x", *rodataStart, *rodataEnd, max) + } + } + + text, err := textSection.Data() + if err != nil { + return nil, nil, errors.New("failed to read .text data: " + err.Error()) + } + moduleText := text[*textStart:*textEnd] + + var moduleROData []byte + if rodataStart != nil { + rodata, err := rodataSection.Data() + if err != nil { + return nil, nil, errors.New("failed to read .rdata data: " + err.Error()) + } + moduleROData = rodata[*rodataStart:*rodataEnd] + } + + // Record the link-time preferred PE image base in BORINGSSL_bcm_preferred_base + // so the runtime integrity check computes the ASLR load delta against the + // same preferred-base image bytes hashed here. + var imageBase uint64 + switch oh := object.OptionalHeader.(type) { + case *pe.OptionalHeader64: + imageBase = oh.ImageBase + case *pe.OptionalHeader32: + imageBase = uint64(oh.ImageBase) + default: + return nil, nil, errors.New("unsupported or missing PE optional header") + } + + var baseSym *pe.Symbol + for _, symbol := range symbols { + if symbol.Name == "BORINGSSL_bcm_preferred_base" { + if baseSym != nil { + return nil, nil, errors.New("duplicate preferred base symbol found") + } + baseSym = symbol + } + } + if baseSym == nil { + return nil, nil, errors.New("could not find BORINGSSL_bcm_preferred_base symbol") + } + sn := int(baseSym.SectionNumber) + if sn < 1 || sn > len(object.Sections) { + return nil, nil, errors.New("BORINGSSL_bcm_preferred_base has invalid section number") + } + baseSection := object.Sections[sn-1] + if baseSection.Offset == 0 { + return nil, nil, errors.New("BORINGSSL_bcm_preferred_base section has no file data") + } + baseFileOffset := int64(baseSection.Offset) + int64(baseSym.Value) + if baseFileOffset < 0 || baseFileOffset+8 > int64(len(objectBytes)) { + return nil, nil, errors.New("BORINGSSL_bcm_preferred_base lies outside the object file") + } + binary.LittleEndian.PutUint64(objectBytes[baseFileOffset:], imageBase) + + return moduleText, moduleROData, nil +} + +func do(outPath, oInput string, arInput string, appleOS bool, windowsOS bool, mapFile string, mingw bool) error { var objectBytes []byte var isStatic bool var perm os.FileMode - if appleOS && windowsOS { - return fmt.Errorf("-apple and -windows are mutually exclusive") + modeCount := 0 + if appleOS { + modeCount++ + } + if windowsOS { + modeCount++ + } + if mingw { + modeCount++ + } + if modeCount > 1 { + return fmt.Errorf("-apple, -windows, and -mingw are mutually exclusive") } if windowsOS && len(mapFile) == 0 { return fmt.Errorf("-map is required when -windows is set") } + if !windowsOS && len(mapFile) != 0 { + return fmt.Errorf("-map is only valid when -windows is set") + } if len(arInput) > 0 { isStatic = true @@ -376,6 +538,9 @@ func do(outPath, oInput string, arInput string, appleOS bool, windowsOS bool, ma if windowsOS { return fmt.Errorf("only shared libraries can be handled on Windows") } + if mingw { + return fmt.Errorf("only shared libraries can be handled on MinGW") + } fi, err := os.Stat(arInput) if err != nil { @@ -422,6 +587,8 @@ func do(outPath, oInput string, arInput string, appleOS bool, windowsOS bool, ma moduleText, moduleROData, err = doWindows(objectBytes, mapFile) } else if appleOS { moduleText, moduleROData, err = doAppleOS(objectBytes) + } else if mingw { + moduleText, moduleROData, err = doMingw(objectBytes) } else { moduleText, moduleROData, err = doLinux(objectBytes, isStatic) } @@ -471,10 +638,11 @@ func main() { 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)") + mingw := flag.Bool("mingw", false, "Whether the FIPS module is built for a Windows MinGW toolchain or not.") flag.Parse() - if err := do(*outPath, *oInput, *arInput, *appleOS, *windowsOS, *mapFile); err != nil { + if err := do(*outPath, *oInput, *arInput, *appleOS, *windowsOS, *mapFile, *mingw); err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) os.Exit(1) }