diff --git a/.github/workflows/test-cpp-capture-hash.yml b/.github/workflows/test-cpp-capture-hash.yml new file mode 100644 index 0000000000..826271e4f4 --- /dev/null +++ b/.github/workflows/test-cpp-capture-hash.yml @@ -0,0 +1,44 @@ +name: Test C++ capture_hash Implementation for Windows + +on: + pull_request: + branches: [ inject-hash-cpp-experiment ] + +jobs: + test-cpp-capture-hash: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v2 + with: + submodules: 'recursive' + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: '1.20' + + - name: Setup Visual Studio Environment + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + + - name: Install Dependencies (Windows) + run: | + choco install cmake --confirm --no-progress || echo "cmake already installed" + choco install ninja --confirm --no-progress || echo "ninja already installed" + choco install strawberryperl --confirm --no-progress || echo "strawberryperl already installed" + choco install golang --confirm --no-progress || echo "golang already installed" + choco install nasm --confirm --no-progress || echo "nasm already installed" + + - name: Verify Dependencies + run: | + cmake --version + ninja --version + go version + perl --version + cl + + - name: Build and Test (Windows) + run: | + .\tests\ci\run_capture_hash_cpp.bat \ No newline at end of file diff --git a/.github/workflows/test-cpp-inject-hash.yml b/.github/workflows/test-cpp-inject-hash.yml index c11f16d76a..241afaf4e3 100644 --- a/.github/workflows/test-cpp-inject-hash.yml +++ b/.github/workflows/test-cpp-inject-hash.yml @@ -19,9 +19,31 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v2 + - name: Checkout main repository + uses: actions/checkout@v4 with: - submodules: 'recursive' + fetch-depth: 0 + # Don't initialize submodules yet + + - name: Handle LIEF submodule specifically + run: | + git config --global --add safe.directory '*' + echo "Cleaning any existing LIEF directory..." + rm -rf third_party/lief + + echo "Initializing only the LIEF submodule..." + git submodule init third_party/lief + git submodule update third_party/lief + + echo "Verifying LIEF submodule..." + ls -la third_party/lief + + echo "Now initializing remaining submodules..." + git submodule update --init --recursive + + - name: Verify all submodules + run: | + git submodule status - name: Set up Go uses: actions/setup-go@v2 diff --git a/crypto/CMakeLists.txt b/crypto/CMakeLists.txt index fd9866f6f9..0027380d2b 100644 --- a/crypto/CMakeLists.txt +++ b/crypto/CMakeLists.txt @@ -624,6 +624,15 @@ if(FIPS_SHARED) target_link_libraries(fips_empty_main PUBLIC precrypto) target_include_directories(fips_empty_main PRIVATE ${AWSLC_SOURCE_DIR}/include) target_include_directories(fips_empty_main BEFORE PRIVATE ${AWSLC_BINARY_DIR}/symbol_prefix_include) + if(USE_CPP_INJECT_HASH) + add_custom_command(OUTPUT generated_fips_shared_support.c + COMMAND ${CMAKE_BINARY_DIR}/util/fipstools/inject_hash_cpp/capture_hash_cpp + -in-executable $ + > generated_fips_shared_support.c + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS fips_empty_main capture_hash_cpp + ) + else() add_custom_command(OUTPUT generated_fips_shared_support.c COMMAND ${GO_EXECUTABLE} run ${AWSLC_SOURCE_DIR}/util/fipstools/capture_hash/capture_hash.go @@ -631,6 +640,7 @@ if(FIPS_SHARED) WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} DEPENDS fips_empty_main ${AWSLC_SOURCE_DIR}/util/fipstools/capture_hash/capture_hash.go ) + endif() add_library( generated_fipsmodule diff --git a/tests/ci/run_capture_hash_cpp.bat b/tests/ci/run_capture_hash_cpp.bat new file mode 100644 index 0000000000..3d7554c050 --- /dev/null +++ b/tests/ci/run_capture_hash_cpp.bat @@ -0,0 +1,97 @@ +@echo off +setlocal enabledelayedexpansion + +REM Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +REM SPDX-License-Identifier: Apache-2.0 OR ISC +REM Adapted from tests/ci/run_windows_tests.bat + +echo Running capture_hash.cpp tests... + +REM Setup environment (from run_windows_tests.bat) +set SRC_ROOT=%cd% +set BUILD_DIR=%SRC_ROOT%\test_build_dir + +REM Setup Visual Studio environment - using same pattern as run_windows_tests.bat +REM We'll assume GitHub Actions provides the right VS environment, but check anyway +cl >nul 2>&1 +if !errorlevel! neq 0 ( + echo Error: Visual Studio build tools not available + exit /b 1 +) + +REM Build with our specific configuration (adapted from existing :build function) +call :build RelWithDebInfo "-DFIPS=1 -DBUILD_SHARED_LIBS=1 -DUSE_CPP_INJECT_HASH=ON" || goto error + +cd /d "%BUILD_DIR%" + +REM Initialize error counter +set ERRORS=0 + +echo. +echo TESTING CAPTURE_HASH.CPP WITH EDGE CASES... + +REM Test 1: No arguments (should fail) +echo Running test: No arguments test +util\fipstools\inject_hash_cpp\capture_hash_cpp.exe >nul 2>&1 +if !errorlevel! == 0 ( + echo Test 'No arguments test' was expected to fail but succeeded + set /a ERRORS+=1 +) else ( + echo Test 'No arguments test' failed as expected +) + +REM Test 2: Invalid file (should fail) +echo Running test: Invalid file test +util\fipstools\inject_hash_cpp\capture_hash_cpp.exe -in-executable nonexistent.exe >nul 2>&1 +if !errorlevel! == 0 ( + echo Test 'Invalid file test' was expected to fail but succeeded + set /a ERRORS+=1 +) else ( + echo Test 'Invalid file test' failed as expected +) + +REM Test 3: FIPS integrity sanity check(should succeed) +echo Running test: FIPS integrity sanity check +crypto\crypto_test.exe >nul 2>&1 +if !errorlevel! neq 0 ( + echo Test 'FIPS integrity sanity check' failed - FIPS module has integrity issues + set /a ERRORS+=1 +) else ( + echo Test 'FIPS integrity sanity check' passed - FIPS module integrity OK +) + +echo. +echo === Summary === +echo Total errors: !ERRORS! + +if !ERRORS! gtr 0 ( + echo One or more tests failed + exit /b 1 +) else ( + echo All tests passed + exit /b 0 +) + +REM Build function copied from run_windows_tests.bat +REM Note: The build function is intentionally duplicated from run_windows_tests.bat +REM as Windows batch files don't support function sharing like bash scripts. +REM This keeps the script self-contained and more reliable. +:build +@echo on +@echo LOG: %date%-%time% %1 %2 build started with cmake generation +cd %SRC_ROOT% +rmdir /s /q %BUILD_DIR% +mkdir %BUILD_DIR% +cd %BUILD_DIR% + +cmake -GNinja -DCMAKE_BUILD_TYPE=%~1 %~2 %SRC_ROOT% || goto error + +@echo LOG: %date%-%time% %1 %2 cmake generation complete, starting build +ninja || goto error +@echo LOG: %date%-%time% %1 %2 build complete +@echo off +exit /b 0 + +:error +echo Failed with error #%errorlevel%. +exit /b 1 diff --git a/util/fipstools/inject_hash_cpp/CMakeLists.txt b/util/fipstools/inject_hash_cpp/CMakeLists.txt index 90a41efbe8..122849e562 100644 --- a/util/fipstools/inject_hash_cpp/CMakeLists.txt +++ b/util/fipstools/inject_hash_cpp/CMakeLists.txt @@ -1,28 +1,52 @@ if(USE_CPP_INJECT_HASH) - add_executable(inject_hash_cpp - inject_hash.cpp - ../../../tool/args.cc - ) + if(MSVC) + # On Windows, only build capture_hash_cpp + add_executable(capture_hash_cpp + capture_hash.cpp + ../../../tool/args.cc + ) - target_include_directories(inject_hash_cpp PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_BINARY_DIR} - ${CMAKE_SOURCE_DIR}/include - ${CMAKE_SOURCE_DIR} # Add this line to find tool/internal.h - ) - - # due to aws-lc's nature of converting every warning into an error, - # we need to disable some warnings that are coming from the LIEF submodule - if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") - target_compile_options(inject_hash_cpp PRIVATE - -Wno-overloaded-virtual - -Wno-unused-parameter + target_include_directories(capture_hash_cpp PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR} + ) + + target_compile_options(capture_hash_cpp PRIVATE + /wd4577 # Suppress 'noexcept' warning ) - endif() - target_link_libraries(inject_hash_cpp PRIVATE - LIEF::LIEF - fips_hashing - ) + set_target_properties(capture_hash_cpp PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + ) + else() + add_executable(inject_hash_cpp + inject_hash.cpp + ../../../tool/args.cc + ) + + target_include_directories(inject_hash_cpp PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR} # Add this line to find tool/internal.h + ) + + # due to aws-lc's nature of converting every warning into an error, + # we need to disable some warnings that are coming from the LIEF submodule + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(inject_hash_cpp PRIVATE + -Wno-overloaded-virtual + -Wno-unused-parameter + ) + endif() + + target_link_libraries(inject_hash_cpp PRIVATE + LIEF::LIEF + fips_hashing + ) + endif() endif() diff --git a/util/fipstools/inject_hash_cpp/capture_hash.cpp b/util/fipstools/inject_hash_cpp/capture_hash.cpp new file mode 100644 index 0000000000..bbc8c2feac --- /dev/null +++ b/util/fipstools/inject_hash_cpp/capture_hash.cpp @@ -0,0 +1,195 @@ +// 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. + +#include +#include +#include +#include +#include +#include +#include +#include + +#define LOG_ERROR(fmt, ...) fprintf(stderr, "Error: " fmt "\n", ##__VA_ARGS__) +#define LOG_INFO(fmt, ...) fprintf(stdout, fmt "\n", ##__VA_ARGS__) + +const std::string LINE0 = "AWS-LC FIPS failure caused by:"; +const std::string LINE1 = "FIPS integrity test failed."; +// This must match what is in crypto/fipsmodule/fips_shared_support.c +const std::string LINE2 = "Expected: ae2cea2abda6f3ec977f9bf6949afc836827cba0a09f6b6fde52cde2cdff3180"; +const int HASH_LEN = 64; + +static const argument_t kArguments[] = { + {"-in-executable", kRequiredArgument, "Input executable file path"}, + {"", kOptionalArgument, ""} // Terminating element +}; + +static void print_usage() { + LOG_INFO("Usage: capture_hash_cpp -in-executable "); + PrintUsage(kArguments); +} + +// Function declarations +static std::vector split_string(const std::string& str, const std::string& delimiter); +static std::vector split_by_space(const std::string& str); +static bool execute_command(const std::string& executable, std::string& output); + +static std::vector split_string(const std::string& str, const std::string& delimiter) { + std::vector tokens; + size_t start = 0; + size_t end = str.find(delimiter); + + while (end != std::string::npos) { + tokens.push_back(str.substr(start, end - start)); + start = end + delimiter.length(); + end = str.find(delimiter, start); + } + + tokens.push_back(str.substr(start)); + return tokens; +} + +static std::vector split_by_space(const std::string& str) { + std::vector tokens; + std::istringstream iss(str); + std::string token; + + while (iss >> token) { + tokens.push_back(token); + } + + return tokens; +} + +static bool execute_command(const std::string& executable, std::string& output) { + std::string command = executable + " 2>&1"; // Redirect stderr to stdout + std::cerr << "Executing command: " << command << std::endl; + char buffer[128]; + FILE* pipe = _popen(command.c_str(), "r"); + + if (!pipe) { + std::cerr << "Error: _popen() failed: " << strerror(errno) << std::endl; + return false; + } + + while (fgets(buffer, sizeof(buffer), pipe) != nullptr) { + output += buffer; + } + std::cerr << "Raw command output:\n" << output << std::endl; + + int status = _pclose(pipe); + bool succeeded = (status == 0); + + std::cerr << "Command exit status: " << status << std::endl; + + // Check if the command succeeded when it should have failed + if (succeeded) { + std::cerr << output; + std::cerr << "Error: Executable did not fail as expected" << std::endl; + return false; + } + + return true; +} + +int main(int argc, char* argv[]) { + std::vector args; + for (int i = 1; i < argc; i++) { + args.push_back(argv[i]); + } + + args_map_t args_map; + args_list_t extra_args; + + if (!ParseKeyValueArguments(args_map, extra_args, args, kArguments)) { + LOG_ERROR("Failed to parse arguments"); + print_usage(); + return 1; + } + + std::string executable; + if (!GetString(&executable, "-in-executable", "", args_map)) { + LOG_ERROR("Error getting executable path"); + print_usage(); + return 1; + } + + + std::string output; + if (!execute_command(executable, output)) { + return 1; + } + + // Split output into lines - checking for both Windows (\r\n) and Unix (\n) line endings + std::vector lines; + if (output.find("\r\n") != std::string::npos) { + lines = split_string(output, "\r\n"); + std::cerr << "Found Windows-style line endings" << std::endl; + } else { + lines = split_string(output, "\n"); + std::cerr << "Found Unix-style line endings" << std::endl; + } + + std::cerr << "Number of lines in output: " << lines.size() << std::endl; + + if (lines.size() != 6) { + std::cerr << output; + std::cerr << "Error: Expected 6 lines in output but got " << lines.size() << std::endl; + return 1; + } + + if (lines[0] != LINE0) { + std::cerr << output; + std::cerr << "Error: Expected \"" << LINE0 << "\" got \"" << lines[0] << "\"" << std::endl; + return 1; + } + + if (lines[1] != LINE1) { + std::cerr << output; + std::cerr << "Error: Expected \"" << LINE1 << "\" got \"" << lines[1] << "\"" << std::endl; + return 1; + } + + if (lines[2] != LINE2) { + std::cerr << output; + std::cerr << "Error: Expected \"" << LINE2 << "\" got \"" << lines[2] << "\"" << std::endl; + return 1; + } + + auto hash_parts = split_by_space(lines[3]); + if (hash_parts.size() < 2) { + std::cerr << output; + std::cerr << "Error: Could not parse hash from line: " << lines[3] << std::endl; + return 1; + } + + std::string hash = hash_parts[1]; + std::cerr << "Found hash value: " << hash << std::endl; + + if (hash.length() != HASH_LEN) { + std::cerr << output; + std::cerr << "Error: Hash \"" << hash << "\" is " << hash.length() + << " long, expected " << HASH_LEN << std::endl; + return 1; + } + + // Output the generated C code + std::cout << "// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.\n" + << "// SPDX-License-Identifier: Apache-2.0 OR ISC\n" + << "// This file is generated by: 'capture_hash_cpp -in-executable " << executable << "'\n" + << "#include \n" + << "const uint8_t BORINGSSL_bcm_text_hash[32] = {\n"; + + for (size_t i = 0; i < hash.length(); i += 2) { + std::cout << "0x" << hash.substr(i, 2) << ", "; + } + + std::cout << "\n};\n"; + + return 0; +}