From f86f96409070a2d56845281f892d5b53f1abbf12 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Tue, 11 Aug 2026 20:32:50 -0500 Subject: [PATCH 01/45] Add native macOS CMake build support --- BUILDING.md | 66 ++++++++++++++ CMakeLists.txt | 208 ++++++++++++++++++++++++++++++++++++++++++ INSTALL.txt | 18 +++- Makefile | 11 ++- README.md | 13 ++- utils/src/cdo-alloc.c | 2 + utils/src/cdo-npi.c | 2 + 7 files changed, 311 insertions(+), 9 deletions(-) create mode 100644 BUILDING.md create mode 100644 CMakeLists.txt diff --git a/BUILDING.md b/BUILDING.md new file mode 100644 index 0000000..8cb8c9a --- /dev/null +++ b/BUILDING.md @@ -0,0 +1,66 @@ +# Building Bootgen + +Bootgen's CMake build supports native Linux and macOS hosts. It uses checked-in +Bison and Flex output, so parser generators are not required for a normal +build. + +## Requirements + +- A C++14 compiler +- CMake 3.20 or newer +- OpenSSL development headers and libraries +- A threading implementation supported by CMake + +On macOS, install the build prerequisites with Homebrew: + +```sh +brew install cmake ninja openssl@3 +``` + +## CMake build + +Configure with the normal CMake OpenSSL hint when the dependency is not in a +default search path. The path is deliberately supplied by the caller; Bootgen +does not embed Homebrew installation paths. + +```sh +cmake -S . -B build/macos-arm64 -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_OSX_ARCHITECTURES=arm64 \ + -DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" +cmake --build build/macos-arm64 --parallel +ctest --test-dir build/macos-arm64 --output-on-failure +``` + +For an Intel build, run the same configuration on an Intel Mac and set +`CMAKE_OSX_ARCHITECTURES=x86_64`. Test each architecture before any universal +binary packaging. A merged executable requires OpenSSL runtime libraries that +also support both architectures. + +Install into a staging directory with: + +```sh +cmake --install build/macos-arm64 --prefix "$PWD/stage" +``` + +The compatibility Makefile provides a `macos` target that delegates to CMake: + +```sh +make macos CMAKE_ARGS="-DOPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" +``` + +## Sanitizers + +Use the following configuration for the initial host smoke tests: + +```sh +cmake -S . -B build/macos-asan -G Ninja \ + -DBOOTGEN_ENABLE_SANITIZERS=ON \ + -DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" +cmake --build build/macos-asan --parallel +ctest --test-dir build/macos-asan --output-on-failure +``` + +Sanitizer coverage and the current smoke test do not establish boot-image +compatibility. Golden-image, cryptographic, malformed-input, and hardware +tests remain required before a release-quality macOS port can be claimed. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..02109b6 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,208 @@ +# Copyright 2015-2026 Advanced Micro Devices, Inc. +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20) + +project(bootgen LANGUAGES C CXX) + +option(BOOTGEN_BUILD_TESTS "Build Bootgen smoke tests" ON) +option(BOOTGEN_ENABLE_SANITIZERS "Enable AddressSanitizer and UndefinedBehaviorSanitizer" OFF) + +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +find_package(OpenSSL REQUIRED COMPONENTS SSL Crypto) +find_package(Threads REQUIRED) + +set(BOOTGEN_INCLUDE_DIRS + bisonflex + common/include + spartanup/include + versal/include + versal_2ve_2vm/include + zynq/include + zynqmp/include + utils/include + lms-hash-sigs + win_include) + +set(BOOTGEN_PARSER_SOURCES + bisonflex/bif.tab.cpp + bisonflex/bif.yy.cpp + bisonflex/reginit.tab.cpp + bisonflex/reginit.yy.cpp + bisonflex/cmdoptions.tab.cpp + bisonflex/cmdoptions.yy.cpp) + +set(BOOTGEN_SOURCES + common/src/Keccak-compact.cpp + common/src/authentication.cpp + common/src/authkeys.cpp + common/src/bifoptions.cpp + common/src/binary.cpp + common/src/binfile.cpp + common/src/bitutils.cpp + common/src/bootheader.cpp + common/src/bootimage.cpp + common/src/checksum.cpp + common/src/encryption.cpp + common/src/hash.cpp + common/src/imageheadertable.cpp + common/src/kdf.cpp + common/src/logger.cpp + common/src/main.cpp + common/src/mcsfile.cpp + common/src/options.cpp + common/src/outputfile.cpp + common/src/parsing.cpp + common/src/partition.cpp + common/src/partitionheadertable.cpp + common/src/postprocess.cpp + common/src/readimage.cpp + common/src/reginit.cpp + spartanup/src/Keccak-compact-spartanup.cpp + spartanup/src/authentication-spartanup.cpp + spartanup/src/authkeys-spartanup.cpp + spartanup/src/binary-spartanup.cpp + spartanup/src/bitutils-spartanup.cpp + spartanup/src/bootheader-spartanup.cpp + spartanup/src/bootimage-spartanup.cpp + spartanup/src/checksum-spartanup.cpp + spartanup/src/encryption-spartanup.cpp + spartanup/src/imageheadertable-spartanup.cpp + spartanup/src/outputfile-spartanup.cpp + spartanup/src/partition-spartanup.cpp + spartanup/src/partitionheadertable-spartanup.cpp + spartanup/src/readimage-spartanup.cpp + spartanup/src/verifyimage-spartanup.cpp + utils/src/cJSON.c + utils/src/cdo-alloc.c + utils/src/cdo-binary.c + utils/src/cdo-command.c + utils/src/cdo-load.c + utils/src/cdo-metadata.c + utils/src/cdo-npi.c + utils/src/cdo-overlay.c + utils/src/cdo-raw.c + utils/src/cdo-source.c + utils/src/elftools.cpp + utils/src/encryptutils.cpp + utils/src/fips202.c + versal/src/Keccak-compact-versal.cpp + versal/src/authentication-versal.cpp + versal/src/authkeys-versal.cpp + versal/src/binary-versal.cpp + versal/src/bitutils-versal.cpp + versal/src/bootheader-versal.cpp + versal/src/bootimage-versal.cpp + versal/src/checksum-versal.cpp + versal/src/encryption-versal.cpp + versal/src/encryption-versalnet.cpp + versal/src/imageheadertable-versal.cpp + versal/src/outputfile-versal.cpp + versal/src/partition-versal.cpp + versal/src/partitionheadertable-versal.cpp + versal/src/readimage-versal.cpp + versal/src/verifyimage-versal.cpp + versal_2ve_2vm/src/Keccak-compact-versal_2ve_2vm.cpp + versal_2ve_2vm/src/authentication-versal_2ve_2vm.cpp + versal_2ve_2vm/src/authkeys-versal_2ve_2vm.cpp + versal_2ve_2vm/src/binary-versal_2ve_2vm.cpp + versal_2ve_2vm/src/bitutils-versal_2ve_2vm.cpp + versal_2ve_2vm/src/bootheader-versal_2ve_2vm.cpp + versal_2ve_2vm/src/bootimage-versal_2ve_2vm.cpp + versal_2ve_2vm/src/checksum-versal_2ve_2vm.cpp + versal_2ve_2vm/src/encryption-versal_2ve_2vm.cpp + versal_2ve_2vm/src/imageheadertable-versal_2ve_2vm.cpp + versal_2ve_2vm/src/outputfile-versal_2ve_2vm.cpp + versal_2ve_2vm/src/partition-versal_2ve_2vm.cpp + versal_2ve_2vm/src/partitionheadertable-versal_2ve_2vm.cpp + versal_2ve_2vm/src/postprocess_versal_2ve_2vm.cpp + versal_2ve_2vm/src/readimage-versal_2ve_2vm.cpp + versal_2ve_2vm/src/tlbingeneration.cpp + versal_2ve_2vm/src/verifyimage-versal_2ve_2vm.cpp + zynq/src/authentication-zynq.cpp + zynq/src/bootheader-zynq.cpp + zynq/src/bootimage-zynq.cpp + zynq/src/encryption-zynq.cpp + zynq/src/imageheadertable-zynq.cpp + zynq/src/partitionheadertable-zynq.cpp + zynq/src/readimage-zynq.cpp + zynqmp/src/authentication-zynqmp.cpp + zynqmp/src/bootheader-zynqmp.cpp + zynqmp/src/bootimage-zynqmp.cpp + zynqmp/src/encryption-zynqmp.cpp + zynqmp/src/imageheadertable-zynqmp.cpp + zynqmp/src/partitionheadertable-zynqmp.cpp + zynqmp/src/readimage-zynqmp.cpp + zynqmp/src/verifyimage-zynqmp.cpp) + +set(LMS_HASH_SIGS_SOURCES + lms-hash-sigs/endian.c + lms-hash-sigs/fips202.c + lms-hash-sigs/hss.c + lms-hash-sigs/hss_alloc.c + lms-hash-sigs/hss_aux.c + lms-hash-sigs/hss_common.c + lms-hash-sigs/hss_compute.c + lms-hash-sigs/hss_derive.c + lms-hash-sigs/hss_generate.c + lms-hash-sigs/hss_keygen.c + lms-hash-sigs/hss_param.c + lms-hash-sigs/hss_reserve.c + lms-hash-sigs/hss_sign.c + lms-hash-sigs/hss_sign_inc.c + lms-hash-sigs/hss_thread_pthread.c + lms-hash-sigs/hss_verify.c + lms-hash-sigs/hss_verify_inc.c + lms-hash-sigs/hss_zeroize.c + lms-hash-sigs/lm_common.c + lms-hash-sigs/lm_ots_common.c + lms-hash-sigs/lm_ots_sign.c + lms-hash-sigs/lm_ots_verify.c + lms-hash-sigs/lm_verify.c + lms-hash-sigs/lms_hash.c + lms-hash-sigs/lms_keys.c + lms-hash-sigs/lms_sign.c + lms-hash-sigs/sha256.c) + +add_library(lms-hash-sigs STATIC ${LMS_HASH_SIGS_SOURCES}) +target_include_directories(lms-hash-sigs PRIVATE lms-hash-sigs) +target_link_libraries(lms-hash-sigs PRIVATE OpenSSL::Crypto Threads::Threads) + +add_executable(bootgen ${BOOTGEN_PARSER_SOURCES} ${BOOTGEN_SOURCES}) +target_include_directories(bootgen PRIVATE ${BOOTGEN_INCLUDE_DIRS}) +target_link_libraries(bootgen PRIVATE + lms-hash-sigs + OpenSSL::SSL + OpenSSL::Crypto + Threads::Threads + ${CMAKE_DL_LIBS}) + +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + target_compile_options(bootgen PRIVATE -Wall -Wno-reorder -Wno-deprecated-declarations) + set_source_files_properties(${BOOTGEN_PARSER_SOURCES} PROPERTIES + COMPILE_OPTIONS "-Wno-deprecated-register") +endif() + +if(BOOTGEN_ENABLE_SANITIZERS) + if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + message(FATAL_ERROR "BOOTGEN_ENABLE_SANITIZERS requires Clang or GCC") + endif() + target_compile_options(lms-hash-sigs PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer) + target_compile_options(bootgen PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer) + target_link_options(bootgen PRIVATE -fsanitize=address,undefined) +endif() + +include(GNUInstallDirs) +install(TARGETS bootgen RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +install(FILES LICENSE README.md INSTALL.txt BUILDING.md DESTINATION ${CMAKE_INSTALL_DOCDIR}) + +if(BOOTGEN_BUILD_TESTS) + include(CTest) + if(BUILD_TESTING) + add_test(NAME bootgen.help COMMAND bootgen -help) + set_tests_properties(bootgen.help PROPERTIES PASS_REGULAR_EXPRESSION "Bootgen v") + endif() +endif() diff --git a/INSTALL.txt b/INSTALL.txt index 00820c7..98a7470 100755 --- a/INSTALL.txt +++ b/INSTALL.txt @@ -1,8 +1,9 @@ Required 3rd Party Packages -* OpenSSL v1.1.1b libraries +* OpenSSL libraries Windows : http://slproweb.com/products/Win32OpenSSL.html Linux : libssl-dev package + macOS : Homebrew openssl@3 ================================================================================ @@ -19,6 +20,20 @@ Linux Builds Assuming OpenSSL libraries are located at /home/$USER/local/openssl make "LIBS=/home/$USER/local/openssl/lib/libssl.a /home/$USER/local/openssl/lib/libcrypto.a -ldl" "INCLUDE_USER=-I/home/$USER/local/openssl/include" + +For portable Linux and macOS builds, use the CMake instructions in BUILDING.md. + +macOS Builds + +1. Install CMake, Ninja, and OpenSSL 3: + + brew install cmake ninja openssl@3 + +2. Configure and build without hard-coding the Homebrew prefix: + + cmake -S . -B build/macos -G Ninja -DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" + cmake --build build/macos + ctest --test-dir build/macos --output-on-failure ================================================================================ @@ -30,4 +45,3 @@ Windows Builds 2. Use Microsoft Visual Studio 2015 to build Bootgen source code and link it with OpenSSL libraries downloaded in previous step - diff --git a/Makefile b/Makefile index 8ac6077..90af35a 100755 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ EXEC = build/bin/bootgen OBJDIR = build/obj UNAME := $(shell uname) -ifeq ($(UNAME), Linux) +ifneq (,$(filter Linux Darwin,$(UNAME))) INCLUDE_SYS = -I bisonflex -I common/include -I spartanup/include -I versal/include -I versal_2ve_2vm/include -I zynq/include -I zynqmp/include -I utils/include -I lms-hash-sigs -I win_include LIBS = -lssl -lcrypto RTLIBS = @@ -83,13 +83,18 @@ ${OBJDIR}/%.${OBJ} : */src/%.c ${EXEC}: $(OBJECTS) echo Building executable file: $@... - cd ${LMS_HASH_DIR} && $(MAKE) ${LMS_LIB} + cd ${LMS_HASH_DIR} && $(MAKE) ${LMS_LIB} CFLAGS="$(CFLAGS) $(INCLUDE)" ${CXX} $(CXXFLAGS) $(LDFLAGS) $(OPTIONS_USER) -o $@ $(OBJECTS) ${LMS_HASH_DIR}/${LMS_LIB} $(OPTIONS)$(LIBS) execs: ${EXEC} +.PHONY: macos +macos: + cmake -S . -B build/macos -G Ninja $(CMAKE_ARGS) + cmake --build build/macos + clean: echo rm -rf ${EXEC} rm -f $(OBJECTS) - cd ${LMS_HASH_DIR} && $(MAKE) clean \ No newline at end of file + cd ${LMS_HASH_DIR} && $(MAKE) clean diff --git a/README.md b/README.md index 213a16b..bbd0a69 100755 --- a/README.md +++ b/README.md @@ -16,15 +16,21 @@ In general, Bootgen supports obfuscated key generation through a Xilinx propriet # Build instructions ## Platform Support -Bootgen can be built for both Linux and Windows operating systems. It can be compiled natively for -x86-based computers as well as cross-compiled for ARM-based platforms. +Bootgen can be built on Linux, macOS, and Windows hosts. The CMake build is the +portable build interface for Linux and macOS; the existing Makefile remains +available for compatibility. It can be compiled natively for x86-based +computers as well as cross-compiled for ARM-based platforms. The following 3rd party packages are required to build Bootgen: -* OpenSSL v1.1.1b libraries +* OpenSSL libraries * Windows libraries are available at http://slproweb.com/products/Win32OpenSSL.html * Linux libraries are part of libssl-dev package + * macOS builds are tested with Homebrew OpenSSL 3 + +See [BUILDING.md](BUILDING.md) for CMake, macOS, installation, and sanitizer +instructions. ## Linux Builds @@ -112,4 +118,3 @@ Bootgen links with openssl libraries. Below is openssl copyright. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` - diff --git a/utils/src/cdo-alloc.c b/utils/src/cdo-alloc.c index 3d2cdc8..7c14bff 100755 --- a/utils/src/cdo-alloc.c +++ b/utils/src/cdo-alloc.c @@ -18,7 +18,9 @@ #include #include #include +#if !defined(__APPLE__) #include +#endif #include #include "cdo-alloc.h" diff --git a/utils/src/cdo-npi.c b/utils/src/cdo-npi.c index d357724..c34df9f 100755 --- a/utils/src/cdo-npi.c +++ b/utils/src/cdo-npi.c @@ -18,7 +18,9 @@ #include #include #include +#if !defined(__APPLE__) #include +#endif #include #include #include From 9fb0edcc2d3a052046e03695a4435dd9a888bcb9 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Wed, 12 Aug 2026 21:54:49 -0500 Subject: [PATCH 02/45] Add cross-platform build smoke CI --- .github/workflows/build.yml | 86 +++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..534773d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,86 @@ +name: Build and test + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - name: Ubuntu GCC + os: ubuntu-24.04 + cc: gcc + cxx: g++ + cmake_options: '' + - name: Ubuntu Clang + os: ubuntu-24.04 + cc: clang + cxx: clang++ + cmake_options: '' + - name: Ubuntu Clang sanitizers + os: ubuntu-24.04 + cc: clang + cxx: clang++ + cmake_options: -DBOOTGEN_ENABLE_SANITIZERS=ON + - name: macOS Apple Silicon + os: macos-15 + cc: clang + cxx: clang++ + cmake_options: '' + + defaults: + run: + shell: bash + + env: + CC: ${{ matrix.cc }} + CXX: ${{ matrix.cxx }} + CMAKE_OPTIONS: ${{ matrix.cmake_options }} + + steps: + - uses: actions/checkout@v4 + + - name: Install Linux dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install --yes ninja-build libssl-dev + + - name: Install macOS dependencies + if: runner.os == 'macOS' + run: | + brew install ninja openssl@3 + echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" >> "$GITHUB_ENV" + + - name: Configure + run: | + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ + ${OPENSSL_ROOT_DIR:+-DOPENSSL_ROOT_DIR="$OPENSSL_ROOT_DIR"} \ + ${CMAKE_OPTIONS:-} + + - name: Build + run: cmake --build build --parallel + + - name: Test + run: ctest --test-dir build --output-on-failure --output-junit build/ctest.xml + + - name: Upload artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: bootgen-${{ matrix.name }} + if-no-files-found: error + path: | + build/bootgen + build/ctest.xml + LICENSE + BUILDING.md From 44adebac43f7fe1541389cf5cdfc55acb68973aa Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Thu, 13 Aug 2026 07:41:46 -0500 Subject: [PATCH 03/45] Add Zynq boot image smoke tests --- CMakeLists.txt | 13 ++++++++++ tests/cmake/invalid_bif_test.cmake | 18 +++++++++++++ tests/cmake/zynq_smoke.cmake | 40 +++++++++++++++++++++++++++++ tests/fixtures/README.md | 18 +++++++++++++ tests/fixtures/invalid.bif | 4 +++ tests/fixtures/zynq-smoke.S | 9 +++++++ tests/fixtures/zynq-smoke.bif | 4 +++ tests/fixtures/zynq-smoke.elf | Bin 0 -> 4548 bytes tests/fixtures/zynq-smoke.ld | 7 +++++ utils/include/elftools.h | 2 +- 10 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 tests/cmake/invalid_bif_test.cmake create mode 100644 tests/cmake/zynq_smoke.cmake create mode 100644 tests/fixtures/README.md create mode 100644 tests/fixtures/invalid.bif create mode 100644 tests/fixtures/zynq-smoke.S create mode 100644 tests/fixtures/zynq-smoke.bif create mode 100755 tests/fixtures/zynq-smoke.elf create mode 100644 tests/fixtures/zynq-smoke.ld diff --git a/CMakeLists.txt b/CMakeLists.txt index 02109b6..9d1b0ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -204,5 +204,18 @@ if(BOOTGEN_BUILD_TESTS) if(BUILD_TESTING) add_test(NAME bootgen.help COMMAND bootgen -help) set_tests_properties(bootgen.help PROPERTIES PASS_REGULAR_EXPRESSION "Bootgen v") + add_test( + NAME bootgen.zynq-smoke + COMMAND ${CMAKE_COMMAND} + -DBOOTGEN=$ + -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures + -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/zynq-smoke + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/zynq_smoke.cmake) + add_test( + NAME bootgen.invalid-bif + COMMAND ${CMAKE_COMMAND} + -DBOOTGEN=$ + -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/invalid_bif_test.cmake) endif() endif() diff --git a/tests/cmake/invalid_bif_test.cmake b/tests/cmake/invalid_bif_test.cmake new file mode 100644 index 0000000..225cf76 --- /dev/null +++ b/tests/cmake/invalid_bif_test.cmake @@ -0,0 +1,18 @@ +if(NOT DEFINED BOOTGEN OR NOT DEFINED FIXTURE_DIR) + message(FATAL_ERROR "BOOTGEN and FIXTURE_DIR are required") +endif() + +execute_process( + COMMAND "${BOOTGEN}" -arch zynq -image invalid.bif + WORKING_DIRECTORY "${FIXTURE_DIR}" + RESULT_VARIABLE RESULT + OUTPUT_VARIABLE OUTPUT + ERROR_VARIABLE ERROR) +if(RESULT EQUAL 0) + message(FATAL_ERROR "Malformed BIF unexpectedly succeeded:\n${OUTPUT}\n${ERROR}") +endif() + +string(FIND "${OUTPUT}${ERROR}" "syntax error" ERROR_OFFSET) +if(ERROR_OFFSET EQUAL -1) + message(FATAL_ERROR "Malformed BIF did not report a syntax error:\n${OUTPUT}\n${ERROR}") +endif() diff --git a/tests/cmake/zynq_smoke.cmake b/tests/cmake/zynq_smoke.cmake new file mode 100644 index 0000000..3a4fdf0 --- /dev/null +++ b/tests/cmake/zynq_smoke.cmake @@ -0,0 +1,40 @@ +if(NOT DEFINED BOOTGEN OR NOT DEFINED FIXTURE_DIR OR NOT DEFINED OUTPUT_DIR) + message(FATAL_ERROR "BOOTGEN, FIXTURE_DIR, and OUTPUT_DIR are required") +endif() + +file(MAKE_DIRECTORY "${OUTPUT_DIR}") +set(TEST_FIXTURE_DIR "${OUTPUT_DIR}/fixture") +file(MAKE_DIRECTORY "${TEST_FIXTURE_DIR}") +file(COPY "${FIXTURE_DIR}/zynq-smoke.bif" "${FIXTURE_DIR}/zynq-smoke.elf" DESTINATION "${TEST_FIXTURE_DIR}") +set(BOOT_IMAGE "${OUTPUT_DIR}/BOOT.BIN") + +execute_process( + COMMAND "${BOOTGEN}" -arch zynq -image "${TEST_FIXTURE_DIR}/zynq-smoke.bif" -o "${BOOT_IMAGE}" -w on + WORKING_DIRECTORY "${TEST_FIXTURE_DIR}" + RESULT_VARIABLE CREATE_RESULT + OUTPUT_VARIABLE CREATE_OUTPUT + ERROR_VARIABLE CREATE_ERROR) +if(NOT CREATE_RESULT EQUAL 0) + message(FATAL_ERROR "Zynq smoke image creation failed (${CREATE_RESULT}):\n${CREATE_OUTPUT}\n${CREATE_ERROR}") +endif() + +file(SIZE "${BOOT_IMAGE}" BOOT_IMAGE_SIZE) +if(BOOT_IMAGE_SIZE LESS 2048) + message(FATAL_ERROR "Zynq smoke image is unexpectedly small: ${BOOT_IMAGE_SIZE} bytes") +endif() + +execute_process( + COMMAND "${BOOTGEN}" -arch zynq -read "${BOOT_IMAGE}" + RESULT_VARIABLE READ_RESULT + OUTPUT_VARIABLE READ_OUTPUT + ERROR_VARIABLE READ_ERROR) +if(NOT READ_RESULT EQUAL 0) + message(FATAL_ERROR "Zynq smoke image inspection failed (${READ_RESULT}):\n${READ_OUTPUT}\n${READ_ERROR}") +endif() + +string(FIND "${READ_OUTPUT}" "zynq-smoke.elf" IMAGE_NAME_OFFSET) +string(FIND "${READ_OUTPUT}" "fsbl_load_address (0x38) : 0x00100000" LOAD_ADDRESS_OFFSET) +string(FIND "${READ_OUTPUT}" "fsbl_exec_address (0x3C) : 0x00100000" EXEC_ADDRESS_OFFSET) +if(IMAGE_NAME_OFFSET EQUAL -1 OR LOAD_ADDRESS_OFFSET EQUAL -1 OR EXEC_ADDRESS_OFFSET EQUAL -1) + message(FATAL_ERROR "Zynq smoke image metadata does not match the input ELF:\n${READ_OUTPUT}") +endif() diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md new file mode 100644 index 0000000..13262d8 --- /dev/null +++ b/tests/fixtures/README.md @@ -0,0 +1,18 @@ +# Zynq smoke fixture + +`zynq-smoke.elf` is a tiny, non-secure Cortex-A9 ELF used to exercise Bootgen's +Zynq-7000 ELF import, image construction, and image-read paths. It contains one +four-byte branch-to-self instruction at load and entry address `0x00100000`. +It is not bootable firmware and must never be used on a board. + +The ELF is generated from `zynq-smoke.S` and `zynq-smoke.ld` with: + +```sh +arm-none-eabi-gcc -nostdlib \ + -Wl,-T,tests/fixtures/zynq-smoke.ld,-e,_start,--build-id=none \ + -o tests/fixtures/zynq-smoke.elf tests/fixtures/zynq-smoke.S +``` + +The checked-in binary removes a cross-compiler dependency from host-side test +runs. Its ELF header and program header are intentionally small enough to be +inspected in reviews. diff --git a/tests/fixtures/invalid.bif b/tests/fixtures/invalid.bif new file mode 100644 index 0000000..4070363 --- /dev/null +++ b/tests/fixtures/invalid.bif @@ -0,0 +1,4 @@ +the_ROM_image: +{ + [bootloader zynq-smoke.elf +} diff --git a/tests/fixtures/zynq-smoke.S b/tests/fixtures/zynq-smoke.S new file mode 100644 index 0000000..d32c084 --- /dev/null +++ b/tests/fixtures/zynq-smoke.S @@ -0,0 +1,9 @@ +/* Minimal Cortex-A9 image used only for host-side Bootgen smoke testing. */ +.syntax unified +.cpu cortex-a9 + +.section .text +.global _start +.type _start, %function +_start: + b _start diff --git a/tests/fixtures/zynq-smoke.bif b/tests/fixtures/zynq-smoke.bif new file mode 100644 index 0000000..0eb05f6 --- /dev/null +++ b/tests/fixtures/zynq-smoke.bif @@ -0,0 +1,4 @@ +the_ROM_image: +{ + [bootloader] zynq-smoke.elf +} diff --git a/tests/fixtures/zynq-smoke.elf b/tests/fixtures/zynq-smoke.elf new file mode 100755 index 0000000000000000000000000000000000000000..bb4c5c51a92703ab47c1b3d3d62057c19e5f73ab GIT binary patch literal 4548 zcmeI0u}Z{15QhIvE;&yGqlJwY3ZjQ-KuBX5^sKZ{>~(Vlyh2Wr749luz^Cw;d=DMbDd3~ws;Lrr-jL9 zN9hT|<$k)P2Ht@>Tg(W4HeU|{;D1jIT+Qdup!-G-AJs}-Z2Vw#wUe(~_jr7BRV4FS zJjJfYZE192fViAL7@aYiqKWtJ#x4!w^!hT^#uSt6$>gQ$;P1Fei|sM2t?Y}BonyG= z?mHBG;IY-|+jj}0`%fFaj($kr9dkDuv5^u6-27g Date: Thu, 13 Aug 2026 17:01:26 -0500 Subject: [PATCH 04/45] Preserve UTF-8 and space-containing command paths --- CMakeLists.txt | 2 +- bisonflex/cmdoptions.yy.cpp | 3 +-- common/include/cmdoptionsscanner.h | 12 ++++++++++++ common/src/options.cpp | 28 +++++++++++++++++++++++++++- parser/cmdoptions.l | 2 +- 5 files changed, 42 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d1b0ce..39be084 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -209,7 +209,7 @@ if(BOOTGEN_BUILD_TESTS) COMMAND ${CMAKE_COMMAND} -DBOOTGEN=$ -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures - -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/zynq-smoke + -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/zynq smoke-ü -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/zynq_smoke.cmake) add_test( NAME bootgen.invalid-bif diff --git a/bisonflex/cmdoptions.yy.cpp b/bisonflex/cmdoptions.yy.cpp index 094b714..4c3ac05 100644 --- a/bisonflex/cmdoptions.yy.cpp +++ b/bisonflex/cmdoptions.yy.cpp @@ -2553,7 +2553,7 @@ yylval->cstring=strdup(yytext); return tok::HEXSTRING; case 204: YY_RULE_SETUP #line 269 "parser/cmdoptions.l" -yylval->cstring=strdup(yytext); return tok::IDENTIFIER; +yylval->cstring=strdup(ResolveCommandLineArgument(yytext).c_str()); return tok::IDENTIFIER; YY_BREAK case 205: YY_RULE_SETUP @@ -3655,4 +3655,3 @@ void CO::FlexScanner::comment() goto loop; } } - diff --git a/common/include/cmdoptionsscanner.h b/common/include/cmdoptionsscanner.h index 78fa7cf..e68ca56 100755 --- a/common/include/cmdoptionsscanner.h +++ b/common/include/cmdoptionsscanner.h @@ -39,6 +39,8 @@ // Include Bison for types / tokens #include "cmdoptions.tab.hpp" +#include +#include #include "options.h" #include "parsing.h" @@ -64,6 +66,15 @@ namespace CO } void comment(); + void AddCommandLineArgument(std::string placeholder, std::string value) + { + commandLineArguments.emplace(std::move(placeholder), std::move(value)); + } + std::string ResolveCommandLineArgument(const char* value) const + { + const auto argument = commandLineArguments.find(value); + return argument == commandLineArguments.end() ? value : argument->second; + } std::string commandline; void ReportError(const CO::BisonParser::location_type &loc, const std::string &msg) { @@ -79,6 +90,7 @@ namespace CO // point to yylval (provided by Bison in overloaded yylex) CO::BisonParser::semantic_type * yylval; CO::BisonParser::location_type * yylloc; + std::map commandLineArguments; }; } #endif diff --git a/common/src/options.cpp b/common/src/options.cpp index 67c0168..8f8a468 100755 --- a/common/src/options.cpp +++ b/common/src/options.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include "options.h" #include "cmdoptionsscanner.h" @@ -41,6 +42,22 @@ ***************************************************** F U N C T I O N S *** ------------------------------------------------------------------------------- */ +/******************************************************************************/ +static bool RequiresCommandLinePlaceholder(const char* argument) +{ + for (const unsigned char* character = reinterpret_cast(argument); + *character != '\0'; + ++character) + { + if (std::isspace(*character) || *character >= 0x80) + { + return true; + } + } + + return false; +} + /******************************************************************************/ void Options::ParseArgs(int argc, const char * argv[]) { @@ -54,7 +71,16 @@ void Options::ParseArgs(int argc, const char * argv[]) { merged.append(" "); } - merged.append(argv[i]); + if (RequiresCommandLinePlaceholder(argv[i])) + { + std::string placeholder = "__bootgen_arg_" + std::to_string(i) + "__"; + scanner.AddCommandLineArgument(placeholder, argv[i]); + merged.append(placeholder); + } + else + { + merged.append(argv[i]); + } } /* No arguments, assume "-h" */ diff --git a/parser/cmdoptions.l b/parser/cmdoptions.l index 22f2975..88d54f7 100755 --- a/parser/cmdoptions.l +++ b/parser/cmdoptions.l @@ -266,7 +266,7 @@ imagestore return tok::HV_BIF_IMAGESTORE; [a-fA-F0-9]+ yylval->cstring=strdup(yytext); return tok::HEXSTRING; -[0-9]?[a-zA-Z0-9.,_-]+ yylval->cstring=strdup(yytext); return tok::IDENTIFIER; +[0-9]?[a-zA-Z0-9.,_-]+ yylval->cstring=strdup(ResolveCommandLineArgument(yytext).c_str()); return tok::IDENTIFIER; ([a-zA-Z]:[\/\\])?[a-zA-Z0-9\/\\\\(\)@!#$%^&+._-]+ yylval->cstring=strdup(yytext); return tok::FILENAME; \"([a-zA-Z]:[\/\\])?[a-zA-Z0-9\/\\\\(\)@!#$%^&+._ -]+\" yylval->cstring=strdup(yytext+1); yylval->cstring[strlen(yytext)-2]=0; return tok::QFILENAME; From d4fa0fc36ec8206258896e4f8225d3e4453dec5c Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Thu, 13 Aug 2026 22:08:33 -0500 Subject: [PATCH 05/45] Support UTF-8 filenames in BIF files --- bisonflex/bif.yy.cpp | 18 ++++++++++++++++-- parser/bif.l | 17 ++++++++++++++++- tests/cmake/zynq_smoke.cmake | 11 +++++++++-- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/bisonflex/bif.yy.cpp b/bisonflex/bif.yy.cpp index c0c34e5..22b2aae 100644 --- a/bisonflex/bif.yy.cpp +++ b/bisonflex/bif.yy.cpp @@ -3416,7 +3416,22 @@ YY_RULE_SETUP case 287: YY_RULE_SETUP #line 470 "parser/bif.l" -LOG_ERROR("BIF parsing error : Invalid character (0x%02X) in BIF file", (unsigned char)yytext[0]); +{ + // The legacy QFILENAME rule only accepts ASCII; consume a quoted UTF-8 path here. + if (yytext[0] == '"') { + std::string filename; + int character; + while ((character = yyinput()) != '"' && character != 0 && character != '\n' && character != '\r') { + filename += static_cast(character); + } + if (character == '"') { + yylloc->columns(filename.length() + 1); + yylval->string = strdup(filename.c_str()); + return tok::QFILENAME; + } + } + LOG_ERROR("BIF parsing error : Invalid character (0x%02X) in BIF file", (unsigned char)yytext[0]); + } YY_BREAK case 288: YY_RULE_SETUP @@ -4522,4 +4537,3 @@ void BIF::FlexScanner::cleanup_include_stacks() scanner_filename_stack.pop(); } } - diff --git a/parser/bif.l b/parser/bif.l index 243ce72..66c3cd4 100755 --- a/parser/bif.l +++ b/parser/bif.l @@ -467,7 +467,22 @@ lms_key_params yylval->number = tok::LMS_KEY_PARAMS; return tok:: [ \t]+ yylloc->step (); [\n]+ yylloc->lines (yyleng); yylloc->step (); \r /* ignore (Linux only) */ -. LOG_ERROR("BIF parsing error : Invalid character (0x%02X) in BIF file", (unsigned char)yytext[0]); +. { + // The legacy QFILENAME rule only accepts ASCII; consume a quoted UTF-8 path here. + if (yytext[0] == '"') { + std::string filename; + int character; + while ((character = yyinput()) != '"' && character != 0 && character != '\n' && character != '\r') { + filename += static_cast(character); + } + if (character == '"') { + yylloc->columns(filename.length() + 1); + yylval->string = strdup(filename.c_str()); + return tok::QFILENAME; + } + } + LOG_ERROR("BIF parsing error : Invalid character (0x%02X) in BIF file", (unsigned char)yytext[0]); + } %% diff --git a/tests/cmake/zynq_smoke.cmake b/tests/cmake/zynq_smoke.cmake index 3a4fdf0..93a4ed5 100644 --- a/tests/cmake/zynq_smoke.cmake +++ b/tests/cmake/zynq_smoke.cmake @@ -5,7 +5,14 @@ endif() file(MAKE_DIRECTORY "${OUTPUT_DIR}") set(TEST_FIXTURE_DIR "${OUTPUT_DIR}/fixture") file(MAKE_DIRECTORY "${TEST_FIXTURE_DIR}") -file(COPY "${FIXTURE_DIR}/zynq-smoke.bif" "${FIXTURE_DIR}/zynq-smoke.elf" DESTINATION "${TEST_FIXTURE_DIR}") +set(TEST_ELF "${TEST_FIXTURE_DIR}/zynq smoke-ü.elf") +execute_process( + COMMAND "${CMAKE_COMMAND}" -E copy "${FIXTURE_DIR}/zynq-smoke.elf" "${TEST_ELF}" + RESULT_VARIABLE COPY_RESULT) +if(NOT COPY_RESULT EQUAL 0) + message(FATAL_ERROR "Unable to create the Zynq smoke ELF fixture") +endif() +file(WRITE "${TEST_FIXTURE_DIR}/zynq-smoke.bif" "the_ROM_image:\n{\n [bootloader] \"zynq smoke-ü.elf\"\n}\n") set(BOOT_IMAGE "${OUTPUT_DIR}/BOOT.BIN") execute_process( @@ -32,7 +39,7 @@ if(NOT READ_RESULT EQUAL 0) message(FATAL_ERROR "Zynq smoke image inspection failed (${READ_RESULT}):\n${READ_OUTPUT}\n${READ_ERROR}") endif() -string(FIND "${READ_OUTPUT}" "zynq-smoke.elf" IMAGE_NAME_OFFSET) +string(FIND "${READ_OUTPUT}" "zynq smoke-ü.elf" IMAGE_NAME_OFFSET) string(FIND "${READ_OUTPUT}" "fsbl_load_address (0x38) : 0x00100000" LOAD_ADDRESS_OFFSET) string(FIND "${READ_OUTPUT}" "fsbl_exec_address (0x3C) : 0x00100000" EXEC_ADDRESS_OFFSET) if(IMAGE_NAME_OFFSET EQUAL -1 OR LOAD_ADDRESS_OFFSET EQUAL -1 OR EXEC_ADDRESS_OFFSET EQUAL -1) From 0e0f2384c654d4842754d304b0c8766a88fd577a Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Fri, 14 Aug 2026 01:26:29 -0500 Subject: [PATCH 06/45] Add opt-in parser regeneration target --- BUILDING.md | 16 ++++++++++++++++ CMakeLists.txt | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/BUILDING.md b/BUILDING.md index 8cb8c9a..7da0c7e 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -61,6 +61,22 @@ cmake --build build/macos-asan --parallel ctest --test-dir build/macos-asan --output-on-failure ``` +## Parser regeneration (maintainers only) + +The `bisonflex` directory is checked in. Regenerate it only when changing a +grammar or lexer, using Bison 2.7 or newer and Flex 2.5.35 or newer. This is an +explicit opt-in target because generator upgrades can create large reviewable +diffs. + +```sh +cmake -S . -B build/parser-maintenance -DBOOTGEN_REGENERATE_PARSERS=ON +cmake --build build/parser-maintenance --target regenerate-parsers +``` + +The target updates checked-in files in `bisonflex`; inspect those changes, +rebuild, and run the complete test suite before committing them. Normal CMake +and Make builds do not configure or invoke parser generators. + Sanitizer coverage and the current smoke test do not establish boot-image compatibility. Golden-image, cryptographic, malformed-input, and hardware tests remain required before a release-quality macOS port can be claimed. diff --git a/CMakeLists.txt b/CMakeLists.txt index 39be084..80b3305 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,7 @@ project(bootgen LANGUAGES C CXX) option(BOOTGEN_BUILD_TESTS "Build Bootgen smoke tests" ON) option(BOOTGEN_ENABLE_SANITIZERS "Enable AddressSanitizer and UndefinedBehaviorSanitizer" OFF) +option(BOOTGEN_REGENERATE_PARSERS "Enable the parser regeneration maintainer target" OFF) set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -35,6 +36,37 @@ set(BOOTGEN_PARSER_SOURCES bisonflex/cmdoptions.tab.cpp bisonflex/cmdoptions.yy.cpp) +if(BOOTGEN_REGENERATE_PARSERS) + find_package(BISON 2.7 REQUIRED) + find_package(FLEX 2.5.35 REQUIRED) + + add_custom_target(regenerate-parsers + COMMAND ${BISON_EXECUTABLE} --verbose + --defines=${CMAKE_CURRENT_SOURCE_DIR}/bisonflex/bif.tab.hpp + --output=${CMAKE_CURRENT_SOURCE_DIR}/bisonflex/bif.tab.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/parser/bif.y + COMMAND ${FLEX_EXECUTABLE} --prefix=bif + --outfile=${CMAKE_CURRENT_SOURCE_DIR}/bisonflex/bif.yy.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/parser/bif.l + COMMAND ${BISON_EXECUTABLE} --verbose + --defines=${CMAKE_CURRENT_SOURCE_DIR}/bisonflex/reginit.tab.hpp + --output=${CMAKE_CURRENT_SOURCE_DIR}/bisonflex/reginit.tab.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/parser/reginit.y + COMMAND ${FLEX_EXECUTABLE} --prefix=reginit + --outfile=${CMAKE_CURRENT_SOURCE_DIR}/bisonflex/reginit.yy.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/parser/reginit.l + COMMAND ${BISON_EXECUTABLE} --verbose + --defines=${CMAKE_CURRENT_SOURCE_DIR}/bisonflex/cmdoptions.tab.hpp + --output=${CMAKE_CURRENT_SOURCE_DIR}/bisonflex/cmdoptions.tab.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/parser/cmdoptions.y + COMMAND ${FLEX_EXECUTABLE} --prefix=cmdoptions + --outfile=${CMAKE_CURRENT_SOURCE_DIR}/bisonflex/cmdoptions.yy.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/parser/cmdoptions.l + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Regenerating checked-in Bison and Flex parser sources" + VERBATIM) +endif() + set(BOOTGEN_SOURCES common/src/Keccak-compact.cpp common/src/authentication.cpp From 0829cc2b29eb4187fb3f9b3c951bbcec6711b5a4 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Fri, 14 Aug 2026 05:04:53 -0500 Subject: [PATCH 07/45] Fix Versal 2VE/2VM hash block bounds --- versal_2ve_2vm/src/partition-versal_2ve_2vm.cpp | 3 +-- versal_2ve_2vm/src/readimage-versal_2ve_2vm.cpp | 11 ++++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/versal_2ve_2vm/src/partition-versal_2ve_2vm.cpp b/versal_2ve_2vm/src/partition-versal_2ve_2vm.cpp index 8fe7be3..b0f54de 100644 --- a/versal_2ve_2vm/src/partition-versal_2ve_2vm.cpp +++ b/versal_2ve_2vm/src/partition-versal_2ve_2vm.cpp @@ -1008,7 +1008,7 @@ void Versal_2ve_2vmPartition::Build(BootImage& bi, Binary& cache) auto newDataPtr = std::make_unique(newLength); memset(newDataPtr.get(), 0, newLength); - uint8_t* newDataPtrRaw = newDataPtr.get() + newLength; + uint8_t* newDataPtrRaw = newDataPtr.get(); newDataPtrRaw += hashBlockLength; if ((encryptCtx->Type() != Encryption::None) && currentAuthCtx->authAlgorithm->Type() == Authentication::None) { @@ -1318,4 +1318,3 @@ void Versal_2ve_2vmPartition::Link(BootImage &bi) #endif } } - diff --git a/versal_2ve_2vm/src/readimage-versal_2ve_2vm.cpp b/versal_2ve_2vm/src/readimage-versal_2ve_2vm.cpp index e3ecb48..b7fd3a1 100755 --- a/versal_2ve_2vm/src/readimage-versal_2ve_2vm.cpp +++ b/versal_2ve_2vm/src/readimage-versal_2ve_2vm.cpp @@ -1047,6 +1047,12 @@ void Versal_2ve_2vmReadImage::DisplayAuthenicationCertificates(void) if(bH && hash_block_itr == Hashblock_record.begin()) { + constexpr uint32_t hashSlotSize = 4 + 48; + constexpr uint32_t minimumHashBlockSize = 4 * hashSlotSize; + if (hash_block_itr->second < minimumHashBlockSize) + { + LOG_ERROR("Hash Block0 is too small: %u bytes", hash_block_itr->second); + } Separator(); std::cout << " Hash Block0" << std::endl; Separator(); @@ -1058,8 +1064,8 @@ void Versal_2ve_2vmReadImage::DisplayAuthenicationCertificates(void) DisplayLongValues("PMC data index : ", ((*hash_block_itr).first), 4); ((*hash_block_itr).first) += 4; DisplayLongValues("PMC data digest : ", ((*hash_block_itr).first), 48); ((*hash_block_itr).first) += 48; DisplayLongValues("hash block1 index : ", ((*hash_block_itr).first), 4); ((*hash_block_itr).first) += 4; - DisplayLongValues("hash block1 digest : ", ((*hash_block_itr).first), 48); ((*hash_block_itr).first) += 152; - DisplayLongValues("padding : ", ((*hash_block_itr).first), 16); + DisplayLongValues("hash block1 digest : ", ((*hash_block_itr).first), 48); ((*hash_block_itr).first) += 48; + DisplayLongValues("padding : ", ((*hash_block_itr).first), hash_block_itr->second - minimumHashBlockSize); hash_block_itr++; if(auth_type != Authentication::None) DisplayLongValues("HASH Block0 signture : ", ((*hash_block_itr).first), hash_block_itr->second); @@ -1681,4 +1687,3 @@ std::string Versal_2ve_2vmReadImage::GetPartitionType(uint32_t value) return val; } - From 5ee28e28ab8a48efef6522d939a85cce12125794 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Fri, 14 Aug 2026 06:40:26 -0500 Subject: [PATCH 08/45] Add cross-family boot image smoke tests --- CMakeLists.txt | 34 ++++++++++++++++++++++++++++ tests/cmake/versal_smoke.cmake | 38 +++++++++++++++++++++++++++++++ tests/cmake/zynqmp_smoke.cmake | 40 +++++++++++++++++++++++++++++++++ tests/fixtures/README.md | 8 +++++++ tests/fixtures/versal-smoke.bif | 8 +++++++ tests/fixtures/zynqmp-smoke.bif | 4 ++++ 6 files changed, 132 insertions(+) create mode 100644 tests/cmake/versal_smoke.cmake create mode 100644 tests/cmake/zynqmp_smoke.cmake create mode 100644 tests/fixtures/versal-smoke.bif create mode 100644 tests/fixtures/zynqmp-smoke.bif diff --git a/CMakeLists.txt b/CMakeLists.txt index 80b3305..7688274 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -243,6 +243,40 @@ if(BOOTGEN_BUILD_TESTS) -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/zynq smoke-ü -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/zynq_smoke.cmake) + add_test( + NAME bootgen.zynqmp-smoke + COMMAND ${CMAKE_COMMAND} + -DBOOTGEN=$ + -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures + -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/zynqmp-smoke + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/zynqmp_smoke.cmake) + add_test( + NAME bootgen.versal-smoke + COMMAND ${CMAKE_COMMAND} + -DBOOTGEN=$ + -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures + -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/versal-smoke + -DARCH=versal + -DEXPECTED_READ_OUTPUT=partition_type[elf] + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/versal_smoke.cmake) + add_test( + NAME bootgen.versal-2ve-2vm-smoke + COMMAND ${CMAKE_COMMAND} + -DBOOTGEN=$ + -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures + -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/versal-2ve-2vm-smoke + -DARCH=versal_2ve_2vm + -DEXPECTED_READ_OUTPUT=partition_type[elf] + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/versal_smoke.cmake) + add_test( + NAME bootgen.spartanup-smoke + COMMAND ${CMAKE_COMMAND} + -DBOOTGEN=$ + -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures + -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/spartanup-smoke + -DARCH=spartanup + -DEXPECTED_READ_OUTPUT=plm_length + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/versal_smoke.cmake) add_test( NAME bootgen.invalid-bif COMMAND ${CMAKE_COMMAND} diff --git a/tests/cmake/versal_smoke.cmake b/tests/cmake/versal_smoke.cmake new file mode 100644 index 0000000..0180d4a --- /dev/null +++ b/tests/cmake/versal_smoke.cmake @@ -0,0 +1,38 @@ +if(NOT DEFINED BOOTGEN OR NOT DEFINED FIXTURE_DIR OR NOT DEFINED OUTPUT_DIR OR NOT DEFINED ARCH OR NOT DEFINED EXPECTED_READ_OUTPUT) + message(FATAL_ERROR "BOOTGEN, FIXTURE_DIR, OUTPUT_DIR, ARCH, and EXPECTED_READ_OUTPUT are required") +endif() + +file(MAKE_DIRECTORY "${OUTPUT_DIR}") +set(TEST_FIXTURE_DIR "${OUTPUT_DIR}/fixture") +file(MAKE_DIRECTORY "${TEST_FIXTURE_DIR}") +file(COPY "${FIXTURE_DIR}/versal-smoke.bif" "${FIXTURE_DIR}/zynq-smoke.elf" DESTINATION "${TEST_FIXTURE_DIR}") +set(BOOT_IMAGE "${OUTPUT_DIR}/BOOT.BIN") + +execute_process( + COMMAND "${BOOTGEN}" -arch "${ARCH}" -image "${TEST_FIXTURE_DIR}/versal-smoke.bif" -o "${BOOT_IMAGE}" -w on + WORKING_DIRECTORY "${TEST_FIXTURE_DIR}" + RESULT_VARIABLE CREATE_RESULT + OUTPUT_VARIABLE CREATE_OUTPUT + ERROR_VARIABLE CREATE_ERROR) +if(NOT CREATE_RESULT EQUAL 0) + message(FATAL_ERROR "${ARCH} smoke image creation failed (${CREATE_RESULT}):\n${CREATE_OUTPUT}\n${CREATE_ERROR}") +endif() + +file(SIZE "${BOOT_IMAGE}" BOOT_IMAGE_SIZE) +if(BOOT_IMAGE_SIZE LESS 512) + message(FATAL_ERROR "${ARCH} smoke image is unexpectedly small: ${BOOT_IMAGE_SIZE} bytes") +endif() + +execute_process( + COMMAND "${BOOTGEN}" -arch "${ARCH}" -read "${BOOT_IMAGE}" + RESULT_VARIABLE READ_RESULT + OUTPUT_VARIABLE READ_OUTPUT + ERROR_VARIABLE READ_ERROR) +if(NOT READ_RESULT EQUAL 0) + message(FATAL_ERROR "${ARCH} smoke image inspection failed (${READ_RESULT}):\n${READ_OUTPUT}\n${READ_ERROR}") +endif() + +string(FIND "${READ_OUTPUT}" "${EXPECTED_READ_OUTPUT}" EXPECTED_OUTPUT_OFFSET) +if(EXPECTED_OUTPUT_OFFSET EQUAL -1) + message(FATAL_ERROR "${ARCH} smoke image metadata does not match the input ELF:\n${READ_OUTPUT}") +endif() diff --git a/tests/cmake/zynqmp_smoke.cmake b/tests/cmake/zynqmp_smoke.cmake new file mode 100644 index 0000000..a30b54f --- /dev/null +++ b/tests/cmake/zynqmp_smoke.cmake @@ -0,0 +1,40 @@ +if(NOT DEFINED BOOTGEN OR NOT DEFINED FIXTURE_DIR OR NOT DEFINED OUTPUT_DIR) + message(FATAL_ERROR "BOOTGEN, FIXTURE_DIR, and OUTPUT_DIR are required") +endif() + +file(MAKE_DIRECTORY "${OUTPUT_DIR}") +set(TEST_FIXTURE_DIR "${OUTPUT_DIR}/fixture") +file(MAKE_DIRECTORY "${TEST_FIXTURE_DIR}") +file(COPY "${FIXTURE_DIR}/zynqmp-smoke.bif" "${FIXTURE_DIR}/zynq-smoke.elf" DESTINATION "${TEST_FIXTURE_DIR}") +set(BOOT_IMAGE "${OUTPUT_DIR}/BOOT.BIN") + +execute_process( + COMMAND "${BOOTGEN}" -arch zynqmp -image "${TEST_FIXTURE_DIR}/zynqmp-smoke.bif" -o "${BOOT_IMAGE}" -w on + WORKING_DIRECTORY "${TEST_FIXTURE_DIR}" + RESULT_VARIABLE CREATE_RESULT + OUTPUT_VARIABLE CREATE_OUTPUT + ERROR_VARIABLE CREATE_ERROR) +if(NOT CREATE_RESULT EQUAL 0) + message(FATAL_ERROR "ZynqMP smoke image creation failed (${CREATE_RESULT}):\n${CREATE_OUTPUT}\n${CREATE_ERROR}") +endif() + +file(SIZE "${BOOT_IMAGE}" BOOT_IMAGE_SIZE) +if(BOOT_IMAGE_SIZE LESS 2048) + message(FATAL_ERROR "ZynqMP smoke image is unexpectedly small: ${BOOT_IMAGE_SIZE} bytes") +endif() + +execute_process( + COMMAND "${BOOTGEN}" -arch zynqmp -read "${BOOT_IMAGE}" + RESULT_VARIABLE READ_RESULT + OUTPUT_VARIABLE READ_OUTPUT + ERROR_VARIABLE READ_ERROR) +if(NOT READ_RESULT EQUAL 0) + message(FATAL_ERROR "ZynqMP smoke image inspection failed (${READ_RESULT}):\n${READ_OUTPUT}\n${READ_ERROR}") +endif() + +string(FIND "${READ_OUTPUT}" "zynq-smoke.elf" IMAGE_NAME_OFFSET) +string(FIND "${READ_OUTPUT}" "exec_addr_lo (0x10) : 0x00100000" EXEC_ADDRESS_OFFSET) +string(FIND "${READ_OUTPUT}" "core [a53-0]" CORE_OFFSET) +if(IMAGE_NAME_OFFSET EQUAL -1 OR EXEC_ADDRESS_OFFSET EQUAL -1 OR CORE_OFFSET EQUAL -1) + message(FATAL_ERROR "ZynqMP smoke image metadata does not match the input ELF:\n${READ_OUTPUT}") +endif() diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md index 13262d8..66dd835 100644 --- a/tests/fixtures/README.md +++ b/tests/fixtures/README.md @@ -16,3 +16,11 @@ arm-none-eabi-gcc -nostdlib \ The checked-in binary removes a cross-compiler dependency from host-side test runs. Its ELF header and program header are intentionally small enough to be inspected in reviews. + +The ZynqMP smoke test reuses this ELF as an AArch32 A53 bootloader. It verifies +that Bootgen produces and reads ZynqMP image and partition headers without +requiring vendor firmware. + +`versal-smoke.bif` uses the same four-byte ELF as a synthetic PLM payload. It +is sufficient to exercise non-secure Versal and Versal 2VE/2VM image creation +and readback; it is not valid firmware for either device family. diff --git a/tests/fixtures/versal-smoke.bif b/tests/fixtures/versal-smoke.bif new file mode 100644 index 0000000..2503372 --- /dev/null +++ b/tests/fixtures/versal-smoke.bif @@ -0,0 +1,8 @@ +all: +{ + image + { + name = image1, id = 0x1c000001 + { type = bootloader, file = zynq-smoke.elf } + } +} diff --git a/tests/fixtures/zynqmp-smoke.bif b/tests/fixtures/zynqmp-smoke.bif new file mode 100644 index 0000000..15acb17 --- /dev/null +++ b/tests/fixtures/zynqmp-smoke.bif @@ -0,0 +1,4 @@ +the_ROM_image: +{ + [bootloader, destination_cpu=a53-0] zynq-smoke.elf +} From b138743b0b69015cbd31f5494389c49c26156339 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Fri, 14 Aug 2026 19:24:50 -0500 Subject: [PATCH 09/45] Test native Intel macOS builds in CI --- .github/workflows/build.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 534773d..0b87e0f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,7 +35,12 @@ jobs: os: macos-15 cc: clang cxx: clang++ - cmake_options: '' + cmake_options: -DCMAKE_OSX_ARCHITECTURES=arm64 + - name: macOS Intel + os: macos-15-intel + cc: clang + cxx: clang++ + cmake_options: -DCMAKE_OSX_ARCHITECTURES=x86_64 defaults: run: @@ -73,6 +78,16 @@ jobs: - name: Test run: ctest --test-dir build --output-on-failure --output-junit build/ctest.xml + - name: Confirm macOS binary architecture + if: runner.os == 'macOS' + run: | + file build/bootgen + if [[ "${{ matrix.name }}" == "macOS Apple Silicon" ]]; then + file build/bootgen | grep -q 'arm64' + else + file build/bootgen | grep -q 'x86_64' + fi + - name: Upload artifacts if: always() uses: actions/upload-artifact@v4 From baef1381d62adb7a834a38102525e37824e91e00 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Fri, 14 Aug 2026 21:20:52 -0500 Subject: [PATCH 10/45] Set the default macOS deployment target --- BUILDING.md | 5 +++++ CMakeLists.txt | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/BUILDING.md b/BUILDING.md index 7da0c7e..36d3b6d 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -32,6 +32,11 @@ cmake --build build/macos-arm64 --parallel ctest --test-dir build/macos-arm64 --output-on-failure ``` +CMake defaults macOS builds to deployment target 15.0, matching the native +macOS CI runners. Override +`CMAKE_OSX_DEPLOYMENT_TARGET` only when all dependencies, including OpenSSL, +support the selected target. + For an Intel build, run the same configuration on an Intel Mac and set `CMAKE_OSX_ARCHITECTURES=x86_64`. Test each architecture before any universal binary packaging. A merged executable requires OpenSSL runtime libraries that diff --git a/CMakeLists.txt b/CMakeLists.txt index 7688274..cb0a8db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,10 @@ cmake_minimum_required(VERSION 3.20) +if(APPLE AND NOT CMAKE_OSX_DEPLOYMENT_TARGET) + set(CMAKE_OSX_DEPLOYMENT_TARGET "15.0" CACHE STRING "Minimum supported macOS version" FORCE) +endif() + project(bootgen LANGUAGES C CXX) option(BOOTGEN_BUILD_TESTS "Build Bootgen smoke tests" ON) From 1d873b842ecc7c9847ecc1a71f54cea3975301e1 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Fri, 14 Aug 2026 21:48:10 -0500 Subject: [PATCH 11/45] Add macOS package validation --- .github/workflows/build.yml | 5 +++++ BUILDING.md | 25 +++++++++++++++++++++++++ CMakeLists.txt | 9 +++++++++ 3 files changed, 39 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b87e0f..dcdd373 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -88,6 +88,10 @@ jobs: file build/bootgen | grep -q 'x86_64' fi + - name: Package macOS tarball + if: runner.os == 'macOS' + run: cmake --build build --target package + - name: Upload artifacts if: always() uses: actions/upload-artifact@v4 @@ -97,5 +101,6 @@ jobs: path: | build/bootgen build/ctest.xml + build/*.tar.gz LICENSE BUILDING.md diff --git a/BUILDING.md b/BUILDING.md index 36d3b6d..b142254 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -48,6 +48,31 @@ Install into a staging directory with: cmake --install build/macos-arm64 --prefix "$PWD/stage" ``` +## macOS packaging and runtime dependencies + +The CMake `package` target creates an architecture-specific tarball containing +`bootgen`, its Apache-2.0 license, and build documentation: + +```sh +cmake --build build/macos-arm64 --target package +``` + +Bootgen intentionally links to the caller-supplied OpenSSL installation; the +tarball does not bundle or sign OpenSSL. Install the matching-architecture +Homebrew `openssl@3` package before running it, then inspect the result: + +```sh +file build/macos-arm64/bootgen +otool -L build/macos-arm64/bootgen +codesign --force --sign - build/macos-arm64/bootgen +codesign --verify --deep --strict build/macos-arm64/bootgen +``` + +Ad-hoc signing is appropriate for local development. A released universal +package requires compatible universal OpenSSL libraries (or separately tested +architecture-specific OpenSSL dependencies); it must not merge executables +against incompatible runtime libraries. + The compatibility Makefile provides a `macos` target that delegates to CMake: ```sh diff --git a/CMakeLists.txt b/CMakeLists.txt index cb0a8db..d14e591 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -235,6 +235,15 @@ include(GNUInstallDirs) install(TARGETS bootgen RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) install(FILES LICENSE README.md INSTALL.txt BUILDING.md DESTINATION ${CMAKE_INSTALL_DOCDIR}) +set(CPACK_GENERATOR TGZ) +set(CPACK_PACKAGE_NAME bootgen) +set(CPACK_PACKAGE_VENDOR "Advanced Micro Devices, Inc.") +set(CPACK_PACKAGE_VERSION "2026.2") +set(CPACK_PACKAGE_FILE_NAME "bootgen-${CPACK_PACKAGE_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}") +set(CPACK_PACKAGING_INSTALL_PREFIX "/") +set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE") +include(CPack) + if(BOOTGEN_BUILD_TESTS) include(CTest) if(BUILD_TESTING) From 03cfd47b732522b38eba101f5bceb63ce3e754d7 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Fri, 14 Aug 2026 23:23:43 -0500 Subject: [PATCH 12/45] Add ZynqMP OpenSSL authentication smoke test --- CMakeLists.txt | 7 ++++ tests/cmake/zynqmp_auth_smoke.cmake | 53 +++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 tests/cmake/zynqmp_auth_smoke.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index d14e591..d80d2c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -263,6 +263,13 @@ if(BOOTGEN_BUILD_TESTS) -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/zynqmp-smoke -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/zynqmp_smoke.cmake) + add_test( + NAME bootgen.zynqmp-auth-smoke + COMMAND ${CMAKE_COMMAND} + -DBOOTGEN=$ + -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures + -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/zynqmp-auth-smoke + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/zynqmp_auth_smoke.cmake) add_test( NAME bootgen.versal-smoke COMMAND ${CMAKE_COMMAND} diff --git a/tests/cmake/zynqmp_auth_smoke.cmake b/tests/cmake/zynqmp_auth_smoke.cmake new file mode 100644 index 0000000..29fbda3 --- /dev/null +++ b/tests/cmake/zynqmp_auth_smoke.cmake @@ -0,0 +1,53 @@ +if(NOT DEFINED BOOTGEN OR NOT DEFINED FIXTURE_DIR OR NOT DEFINED OUTPUT_DIR) + message(FATAL_ERROR "BOOTGEN, FIXTURE_DIR, and OUTPUT_DIR are required") +endif() + +file(MAKE_DIRECTORY "${OUTPUT_DIR}") +set(TEST_FIXTURE_DIR "${OUTPUT_DIR}/fixture") +file(MAKE_DIRECTORY "${TEST_FIXTURE_DIR}") +file(CHMOD "${TEST_FIXTURE_DIR}" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE) +file(COPY "${FIXTURE_DIR}/zynq-smoke.elf" DESTINATION "${TEST_FIXTURE_DIR}") + +set(PRIMARY_KEY "${TEST_FIXTURE_DIR}/primary.pem") +set(SECONDARY_KEY "${TEST_FIXTURE_DIR}/secondary.pem") +function(cleanup_test_keys) + file(REMOVE "${PRIMARY_KEY}" "${SECONDARY_KEY}") +endfunction() + +file(WRITE "${TEST_FIXTURE_DIR}/keygen.bif" "all:\n{\n [pskfile] primary.pem\n [sskfile] secondary.pem\n}\n") +execute_process( + COMMAND "${BOOTGEN}" -arch zynqmp -image "${TEST_FIXTURE_DIR}/keygen.bif" -generate_keys pem + WORKING_DIRECTORY "${TEST_FIXTURE_DIR}" + RESULT_VARIABLE KEYGEN_RESULT + OUTPUT_VARIABLE KEYGEN_OUTPUT + ERROR_VARIABLE KEYGEN_ERROR) +if(NOT KEYGEN_RESULT EQUAL 0 OR NOT EXISTS "${PRIMARY_KEY}" OR NOT EXISTS "${SECONDARY_KEY}") + cleanup_test_keys() + message(FATAL_ERROR "RSA key generation failed (${KEYGEN_RESULT}):\n${KEYGEN_OUTPUT}\n${KEYGEN_ERROR}") +endif() +file(CHMOD "${PRIMARY_KEY}" "${SECONDARY_KEY}" PERMISSIONS OWNER_READ OWNER_WRITE) + +file(WRITE "${TEST_FIXTURE_DIR}/authenticated.bif" "all:\n{\n [pskfile] primary.pem\n [sskfile] secondary.pem\n [bootloader, destination_cpu=a53-0, authentication=rsa] zynq-smoke.elf\n}\n") +set(BOOT_IMAGE "${OUTPUT_DIR}/BOOT.BIN") +execute_process( + COMMAND "${BOOTGEN}" -arch zynqmp -image "${TEST_FIXTURE_DIR}/authenticated.bif" -o "${BOOT_IMAGE}" -w on + WORKING_DIRECTORY "${TEST_FIXTURE_DIR}" + RESULT_VARIABLE CREATE_RESULT + OUTPUT_VARIABLE CREATE_OUTPUT + ERROR_VARIABLE CREATE_ERROR) +if(NOT CREATE_RESULT EQUAL 0) + cleanup_test_keys() + message(FATAL_ERROR "Authenticated ZynqMP smoke image creation failed (${CREATE_RESULT}):\n${CREATE_OUTPUT}\n${CREATE_ERROR}") +endif() + +execute_process( + COMMAND "${BOOTGEN}" -arch zynqmp -verify "${BOOT_IMAGE}" + RESULT_VARIABLE VERIFY_RESULT + OUTPUT_VARIABLE VERIFY_OUTPUT + ERROR_VARIABLE VERIFY_ERROR) +if(NOT VERIFY_RESULT EQUAL 0) + cleanup_test_keys() + message(FATAL_ERROR "Authenticated ZynqMP smoke image verification failed (${VERIFY_RESULT}):\n${VERIFY_OUTPUT}\n${VERIFY_ERROR}") +endif() + +cleanup_test_keys() From 63e6009857e06f65e2f117998b198622b5264621 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Sat, 15 Aug 2026 06:13:13 -0500 Subject: [PATCH 13/45] Secure LMS key generation on POSIX hosts --- CMakeLists.txt | 6 +++ lms-hash-sigs/lms_keys.c | 67 ++---------------------------- lms-hash-sigs/lms_sign.c | 27 ++++++++---- tests/cmake/lms_keygen_smoke.cmake | 47 +++++++++++++++++++++ 4 files changed, 77 insertions(+), 70 deletions(-) create mode 100644 tests/cmake/lms_keygen_smoke.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index d80d2c6..5789c76 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -288,6 +288,12 @@ if(BOOTGEN_BUILD_TESTS) -DARCH=versal_2ve_2vm -DEXPECTED_READ_OUTPUT=partition_type[elf] -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/versal_smoke.cmake) + add_test( + NAME bootgen.versal-2ve-2vm-lms-keygen + COMMAND ${CMAKE_COMMAND} + -DBOOTGEN=$ + -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/versal-2ve-2vm-lms-keygen + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/lms_keygen_smoke.cmake) add_test( NAME bootgen.spartanup-smoke COMMAND ${CMAKE_COMMAND} diff --git a/lms-hash-sigs/lms_keys.c b/lms-hash-sigs/lms_keys.c index 940d201..8587a4c 100755 --- a/lms-hash-sigs/lms_keys.c +++ b/lms-hash-sigs/lms_keys.c @@ -24,8 +24,9 @@ #include #include #include -#include +#include #include +#include #include "hss.h" #include "hss_verify_inc.h" #include "hss_sign_inc.h" @@ -59,11 +60,7 @@ static const char *i_value = 0; static bool convert_specified_seed_i_value( void *, size_t ); -/* - * This is a function that is supposed to generate truly random values. - * This is a hideous version of this; this needs to be replaced by something - * secure in a real product - */ +/* Generate key material from the OpenSSL CSPRNG. */ bool do_rand( void *output, size_t len ) { if (seedbits) { /* The seed was specified on the command line */ @@ -71,63 +68,7 @@ bool do_rand( void *output, size_t len ) { /* This is not something a real application should do */ return convert_specified_seed_i_value( output, len ); } - struct { - unsigned char dev_random_output[32]; - int rand_output[16]; - /* Potentially more random sources here */ - unsigned count; - } buffer; - int i; - - /* Try to grab a sammple of /dev/urandom output */ - /* We use /dev/urandom because there's no point in blocking; this is a */ - /* demo program */ - FILE *f = fopen( "/dev/urandom", "r" ); - if (f) { - (void)fread( buffer.dev_random_output, 1, 32, f ); - fclose(f); - } - - /* Also try to grab some output from rand */ - /* It's not great, but if the /dev/urandom output fails, at least we */ - /* have something */ - /* In a real program, we'd want to fail if we don't have enough */ - /* entropy, but hey, this is a demo */ - static int set_seed = 0; - if (!set_seed) { - srand( time(0) ); - set_seed = 1; - } - for (i = 0; i<16; i++) { - buffer.rand_output[i] = rand(); - } - - - /* If we had more random sources, we'd sample them here */ - - unsigned output_buffer[32]; - for (i=0; len>0; i++) { - buffer.count = i; - - /* Ok, hash all our random samples together to generate the random */ - /* string that was asked for */ - hss_hash( output_buffer, HASH_SHA256, &buffer, sizeof buffer ); - - /* Copy that hash to the output buffer */ - int this_len = 32; - if (this_len > len) this_len = len; - memcpy( output, output_buffer, this_len ); - - /* Advance pointers */ - output = (unsigned char *)output + this_len; len -= this_len; - } - - /* Clean up after ourselves. Yes, this is a demo program; doesn't mean */ - /* we get to be sloppy */ - hss_zeroize( output_buffer, sizeof output_buffer ); - hss_zeroize( &buffer, sizeof buffer ); - - return true; + return len <= INT_MAX && RAND_bytes(output, (int)len) == 1; } static int fromhex(char c) { diff --git a/lms-hash-sigs/lms_sign.c b/lms-hash-sigs/lms_sign.c index 651abe4..d8035ca 100755 --- a/lms-hash-sigs/lms_sign.c +++ b/lms-hash-sigs/lms_sign.c @@ -25,6 +25,11 @@ #include #include #include +#if !defined(_WIN32) +#include +#include +#include +#endif #include "hss.h" #include "hss_verify_inc.h" #include "hss_sign_inc.h" @@ -83,15 +88,23 @@ void *read_file(const char *filename, size_t *len) { */ bool update_private_key(unsigned char *private_key, size_t len_private_key, void *filename) { - FILE *f = fopen(filename, "r+"); +#if defined(_WIN32) + FILE *f = fopen(filename, "wb"); + if (!f) return false; +#else + int fd = open((const char *)filename, O_WRONLY | O_CREAT | O_TRUNC, + S_IRUSR | S_IWUSR); + if (fd < 0) return false; + if (fchmod(fd, S_IRUSR | S_IWUSR) != 0) { + close(fd); + return false; + } + FILE *f = fdopen(fd, "wb"); if (!f) { - /* Open failed, possibly because the file didn't exist */ - f = fopen(filename, "w"); - if (!f) { - /* Unable to open file */ - return false; - } + close(fd); + return false; } +#endif if (1 != fwrite(private_key, len_private_key, 1, f)) { /* Write failed */ fclose(f); diff --git a/tests/cmake/lms_keygen_smoke.cmake b/tests/cmake/lms_keygen_smoke.cmake new file mode 100644 index 0000000..9ba4836 --- /dev/null +++ b/tests/cmake/lms_keygen_smoke.cmake @@ -0,0 +1,47 @@ +if(NOT DEFINED BOOTGEN OR NOT DEFINED OUTPUT_DIR) + message(FATAL_ERROR "BOOTGEN and OUTPUT_DIR are required") +endif() + +file(MAKE_DIRECTORY "${OUTPUT_DIR}") +file(CHMOD "${OUTPUT_DIR}" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE) +set(PRIMARY_KEY "${OUTPUT_DIR}/primary.key") +set(SECONDARY_KEY "${OUTPUT_DIR}/secondary.key") +function(cleanup_test_keys) + file(REMOVE + "${PRIMARY_KEY}.prv" "${PRIMARY_KEY}.pub" + "${SECONDARY_KEY}.prv" "${SECONDARY_KEY}.pub") +endfunction() + +file(WRITE "${OUTPUT_DIR}/keygen.bif" "all:\n{\n lms_key_params\n {\n primary {lms_shake256_h5_w2}\n secondary {lms_shake256_h5_w2}\n }\n [pskfile] primary.key\n [sskfile] secondary.key\n}\n") +execute_process( + COMMAND "${BOOTGEN}" -arch versal_2ve_2vm -image "${OUTPUT_DIR}/keygen.bif" -generate_keys lms + WORKING_DIRECTORY "${OUTPUT_DIR}" + RESULT_VARIABLE KEYGEN_RESULT + OUTPUT_VARIABLE KEYGEN_OUTPUT + ERROR_VARIABLE KEYGEN_ERROR) +if(NOT KEYGEN_RESULT EQUAL 0 OR NOT EXISTS "${PRIMARY_KEY}.prv" OR NOT EXISTS "${PRIMARY_KEY}.pub" OR NOT EXISTS "${SECONDARY_KEY}.prv" OR NOT EXISTS "${SECONDARY_KEY}.pub") + cleanup_test_keys() + message(FATAL_ERROR "LMS key generation failed (${KEYGEN_RESULT}):\n${KEYGEN_OUTPUT}\n${KEYGEN_ERROR}") +endif() + +if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin") + set(STAT_COMMAND /usr/bin/stat -f %Lp) +elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") + set(STAT_COMMAND /usr/bin/stat -c %a) +endif() +if(DEFINED STAT_COMMAND) + foreach(PRIVATE_KEY "${PRIMARY_KEY}.prv" "${SECONDARY_KEY}.prv") + execute_process( + COMMAND ${STAT_COMMAND} "${PRIVATE_KEY}" + RESULT_VARIABLE STAT_RESULT + OUTPUT_VARIABLE PRIVATE_KEY_MODE + ERROR_VARIABLE STAT_ERROR + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT STAT_RESULT EQUAL 0 OR NOT PRIVATE_KEY_MODE STREQUAL "600") + cleanup_test_keys() + message(FATAL_ERROR "LMS private key ${PRIVATE_KEY} must have mode 600 (got ${PRIVATE_KEY_MODE}): ${STAT_ERROR}") + endif() + endforeach() +endif() + +cleanup_test_keys() From 23e76bdcd8837e69020791d6d6ec25320e8bf8b5 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Sat, 15 Aug 2026 22:56:30 -0500 Subject: [PATCH 14/45] Harden LMS signing file handling --- CMakeLists.txt | 12 +++++++ lms-hash-sigs/lms-utils.h | 6 +++- lms-hash-sigs/lms_keys.c | 40 ++++++++++++++-------- lms-hash-sigs/lms_sign.c | 65 +++++++++++++++++------------------ tests/lms_sign_smoke.c | 71 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 144 insertions(+), 50 deletions(-) create mode 100644 tests/lms_sign_smoke.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 5789c76..22eb4ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -247,6 +247,15 @@ include(CPack) if(BOOTGEN_BUILD_TESTS) include(CTest) if(BUILD_TESTING) + add_executable(lms-sign-smoke tests/lms_sign_smoke.c) + target_include_directories(lms-sign-smoke PRIVATE lms-hash-sigs) + target_link_libraries(lms-sign-smoke PRIVATE lms-hash-sigs Threads::Threads) + if(BOOTGEN_ENABLE_SANITIZERS) + target_compile_options(lms-sign-smoke PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer) + target_link_options(lms-sign-smoke PRIVATE -fsanitize=address,undefined) + endif() + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/tests/lms-sign-smoke") + add_test(NAME bootgen.help COMMAND bootgen -help) set_tests_properties(bootgen.help PROPERTIES PASS_REGULAR_EXPRESSION "Bootgen v") add_test( @@ -294,6 +303,9 @@ if(BOOTGEN_BUILD_TESTS) -DBOOTGEN=$ -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/versal-2ve-2vm-lms-keygen -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/lms_keygen_smoke.cmake) + add_test(NAME bootgen.lms-sign-smoke COMMAND lms-sign-smoke) + set_tests_properties(bootgen.lms-sign-smoke PROPERTIES + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/tests/lms-sign-smoke") add_test( NAME bootgen.spartanup-smoke COMMAND ${CMAKE_COMMAND} diff --git a/lms-hash-sigs/lms-utils.h b/lms-hash-sigs/lms-utils.h index cbaf328..b12778b 100755 --- a/lms-hash-sigs/lms-utils.h +++ b/lms-hash-sigs/lms-utils.h @@ -1,6 +1,10 @@ #if !defined( LMS_UTILS_H_ ) #define LMS_UTILS_H_ +#include +#include +#include + extern int LmsSign(const char *keyname, const unsigned char *buffer, unsigned long int buffer_len, unsigned char* result0, size_t* sig_len, uint32_t* err); extern int LmsVerify(const char *keyname, const unsigned char *buffer, size_t buffer_len, unsigned char* sig, size_t sig_len); @@ -12,4 +16,4 @@ extern bool update_private_key(unsigned char *private_key, size_t len_private_ke //extern int LmsKeyGeneration(const char *keyname, const char *parm_set); extern int LmsKeyGeneration(const char *keyname, int* hash, int* h, int* w, int levels); -#endif /* LMS_UTILS_H_ */ \ No newline at end of file +#endif /* LMS_UTILS_H_ */ diff --git a/lms-hash-sigs/lms_keys.c b/lms-hash-sigs/lms_keys.c index 8587a4c..894b0e2 100755 --- a/lms-hash-sigs/lms_keys.c +++ b/lms-hash-sigs/lms_keys.c @@ -59,6 +59,18 @@ static const char *seedbits = 0; static const char *i_value = 0; static bool convert_specified_seed_i_value( void *, size_t ); +static char *make_key_filename(const char *keyname, const char *suffix) { + size_t keyname_len = strlen(keyname); + size_t suffix_len = strlen(suffix); + if (keyname_len > (size_t)-1 - suffix_len - 1) return NULL; + + char *filename = malloc(keyname_len + suffix_len + 1); + if (!filename) return NULL; + + memcpy(filename, keyname, keyname_len); + memcpy(filename + keyname_len, suffix, suffix_len + 1); + return filename; +} /* Generate key material from the OpenSSL CSPRNG. */ bool do_rand( void *output, size_t len ) { @@ -218,10 +230,8 @@ int LmsKeyGeneration(const char *keyname, int* hash, int* h, int* w, int levels) list_parameter_set(levels, lm_array, ots_array, aux_size, hash); /* We'll place the private key here */ - size_t private_key_filename_len = strlen(keyname) + sizeof(".prv") + 1; - char *private_key_filename = malloc(private_key_filename_len); + char *private_key_filename = make_key_filename(keyname, ".prv"); if (!private_key_filename) return 0; - sprintf(private_key_filename, "%s.prv", keyname); /* We'll place the public key in this array */ unsigned len_public_key = hss_get_public_key_len(levels, @@ -262,34 +272,35 @@ int LmsKeyGeneration(const char *keyname, int* hash, int* h, int* w, int levels) } free(private_key_filename); private_key_filename = 0; - size_t public_key_filename_len = strlen(keyname) + sizeof(".pub") + 1; - char *public_key_filename = malloc(public_key_filename_len); + char *public_key_filename = make_key_filename(keyname, ".pub"); if (!public_key_filename) { free(aux); return 0; } - sprintf(public_key_filename, "%s.pub", keyname); printf("[INFO] : Generating public key %s\n", public_key_filename); - FILE *f = fopen(public_key_filename, "w"); - free(public_key_filename); public_key_filename = 0; + FILE *f = fopen(public_key_filename, "wb"); if (!f) { fprintf(stderr, "[ERROR] : Unable to write public key - %s\n", public_key_filename); + free(public_key_filename); free(aux); return 0; } if (1 != fwrite(public_key, len_public_key, 1, f)) { /* Write failed */ fclose(f); + free(public_key_filename); free(aux); return 0; } if (0 != fclose(f)) { fprintf(stderr, "[ERROR] : Unable to close public key file - %s\n", public_key_filename); /* Close failed (possibly because pending write failed) */ + free(public_key_filename); free(aux); return 0; } + free(public_key_filename); /* If the key was specified manually, put in our warning */ if (seedbits) { @@ -299,22 +310,20 @@ int LmsKeyGeneration(const char *keyname, int* hash, int* h, int* w, int levels) } if (aux_size > 0) { - size_t aux_filename_len = strlen(keyname) + sizeof(".aux") + 1; - char *aux_filename = malloc(aux_filename_len); + char *aux_filename = make_key_filename(keyname, ".aux"); if (!aux_filename) { - fprintf(stderr, "[WARNING] : malloc failure writing to aux file - %s\n", aux_filename); + fprintf(stderr, "[WARNING] : malloc failure writing aux file\n"); free(aux); return 1; } - sprintf(aux_filename, "%s.aux", keyname); /* Attempt to write the aux file. Note that if we fail, we'll still */ /* claim to have succeeded (as the aux file is optional) */ printf("[INFO] : Generating aux data %s\n", aux_filename); - f = fopen(aux_filename, "w"); - free(aux_filename); aux_filename = 0; + f = fopen(aux_filename, "wb"); if (!f) { fprintf(stderr, "[WARNING] : Unable to write aux file - %s\n", aux_filename); + free(aux_filename); free(aux); return 1; } @@ -322,15 +331,18 @@ int LmsKeyGeneration(const char *keyname, int* hash, int* h, int* w, int levels) fprintf(stderr, "[WARNING] : Unable to write aux file - %s\n", aux_filename); /* Write failed */ fclose(f); + free(aux_filename); free(aux); return 1; } if (0 != fclose(f)) { fprintf(stderr, "[WARNING] : Close failed writing aux file - %s\n", aux_filename); /* Close failed (possibly because pending write failed) */ + free(aux_filename); free(aux); return 1; } + free(aux_filename); } free(aux); diff --git a/lms-hash-sigs/lms_sign.c b/lms-hash-sigs/lms_sign.c index d8035ca..0afcfc1 100755 --- a/lms-hash-sigs/lms_sign.c +++ b/lms-hash-sigs/lms_sign.c @@ -52,13 +52,16 @@ * those in chunks within the sign()/verify() routines below. */ void *read_file(const char *filename, size_t *len) { - FILE *f = fopen(filename, "r"); + FILE *f = fopen(filename, "rb"); if (!f) return 0; #define FILE_INCREMENT 20000 unsigned alloc_len = FILE_INCREMENT; unsigned char *p = malloc(FILE_INCREMENT); - if (!p) return 0; + if (!p) { + fclose(f); + return 0; + } unsigned cur_len = 0; for (;;) { @@ -67,6 +70,7 @@ void *read_file(const char *filename, size_t *len) { unsigned char *q = realloc(p, alloc_len + FILE_INCREMENT); if (!q) { free(p); + fclose(f); return 0; } p = q; @@ -78,6 +82,7 @@ void *read_file(const char *filename, size_t *len) { cur_len += n; } + fclose(f); if (len) *len = cur_len; return p; } @@ -125,7 +130,7 @@ bool update_private_key(unsigned char *private_key, */ static bool read_private_key(unsigned char *private_key, size_t len_private_key, void *filename) { - FILE *f = fopen(filename, "r"); + FILE *f = fopen(filename, "rb"); if (!f) { return false; } @@ -140,30 +145,29 @@ static bool read_private_key(unsigned char *private_key, return true; } +static char *make_key_filename(const char *keyname, const char *suffix) { + size_t keyname_len = strlen(keyname); + size_t suffix_len = strlen(suffix); + if (keyname_len > (size_t)-1 - suffix_len - 1) return NULL; + + char *filename = malloc(keyname_len + suffix_len + 1); + if (!filename) return NULL; + + memcpy(filename, keyname, keyname_len); + memcpy(filename + keyname_len, suffix, suffix_len + 1); + return filename; +} + int LmsVerify(const char *keyname, const unsigned char *buffer, size_t buffer_len, unsigned char* sig, size_t sig_len) { - size_t public_key_filename_len = strlen(keyname) + 1; - char *public_key_filename = malloc(public_key_filename_len); - if (!public_key_filename) { - printf("Error: malloc failure\n"); - return 0; - } - sprintf(public_key_filename, "%s", keyname); - unsigned char *pub = read_file(public_key_filename, 0); + unsigned char *pub = read_file(keyname, 0); if (!pub) { printf("Error: unable to read %s\n", keyname); - free(public_key_filename); return 0; } bool success = hss_validate_signature(pub, buffer, buffer_len, sig, sig_len, NULL); - if (success) { - //printf("Verifed"); - return 1; - } - else { - //printf("Not Verifed"); - return 0; - } + free(pub); + return success ? 1 : 0; } /* @@ -172,24 +176,20 @@ int LmsVerify(const char *keyname, const unsigned char *buffer, size_t buffer_le writes the signature out to disk */ int LmsSign(const char *keyname, const unsigned char *buffer, unsigned long int buffer_len, unsigned char* sig, size_t* sig_len, uint32_t* err) { - int private_key_filename_len = strlen(keyname) + 1; - char *private_key_filename = malloc(private_key_filename_len); + char *private_key_filename = make_key_filename(keyname, ""); if (!private_key_filename) { printf("Malloc failure\n"); return 0; } - sprintf(private_key_filename, "%s", keyname); struct hss_extra_info info = { 0 }; /* Read in the auxilliary file */ - size_t aux_filename_len = strlen(keyname) + sizeof(".aux") + 1; - char *aux_filename = malloc(aux_filename_len); + char *aux_filename = make_key_filename(keyname, ".aux"); if (!aux_filename) { printf("Malloc failure\n"); free(private_key_filename); return 0; } - sprintf(aux_filename, "%s.aux", keyname); size_t len_aux_data = 0; void *aux_data = read_file(aux_filename, &len_aux_data); if (aux_data != 0) { @@ -254,6 +254,9 @@ int LmsVerify(const char *keyname, const unsigned char *buffer, size_t buffer_le if (!success){ printf("Error signing"); *err = info.error_code; + hss_free_working_key(w); + free(aux_filename); + free(private_key_filename); return 0; } #if 0 @@ -281,18 +284,10 @@ int LmsVerify(const char *keyname, const unsigned char *buffer, size_t buffer_le int GetLmsSignLength(const char *keyname, bool lmsOnly) { - int private_key_filename_len = strlen(keyname) + 1; - char *private_key_filename = malloc(private_key_filename_len); - if (!private_key_filename) { - printf("Malloc failure\n"); - return 0; - } - sprintf(private_key_filename, "%s", keyname); - unsigned levels; param_set_t lm[MAX_HSS_LEVELS]; param_set_t ots[MAX_HSS_LEVELS]; - if (!hss_get_parameter_set(&levels, lm, ots, read_private_key, private_key_filename, NULL)) { + if (!hss_get_parameter_set(&levels, lm, ots, read_private_key, (void *)keyname, NULL)) { /* Can't read private key, or private key invalid */ return 0; } diff --git a/tests/lms_sign_smoke.c b/tests/lms_sign_smoke.c new file mode 100644 index 0000000..a8996cf --- /dev/null +++ b/tests/lms_sign_smoke.c @@ -0,0 +1,71 @@ +#include +#include +#include +#include +#include + +#include "lms-utils.h" + +static void cleanup(void) { + remove("lms-smoke.prv"); + remove("lms-smoke.pub"); + remove("lms-smoke.aux"); + remove("lms-smoke.prv.aux"); +} + +int main(void) { + const char *key_base = "lms-smoke"; + const char *private_key = "lms-smoke.prv"; + const char *public_key = "lms-smoke.pub"; + const unsigned char message[] = "Bootgen LMS signing smoke test"; + int hash[] = { 2 }; + int height[] = { 5 }; + int width[] = { 2 }; + int result = EXIT_FAILURE; + uint32_t error_code = 0; + size_t signature_length; + unsigned char *signature = NULL; + + cleanup(); + if (!LmsKeyGeneration(key_base, hash, height, width, 1)) { + fprintf(stderr, "LMS key generation failed\n"); + goto done; + } + + signature_length = GetLmsSignLength(private_key, false); + if (signature_length == 0) { + fprintf(stderr, "LMS signature length lookup failed\n"); + goto done; + } + + signature = calloc(signature_length, 1); + if (!signature) { + fprintf(stderr, "LMS signature allocation failed\n"); + goto done; + } + + if (!LmsSign(private_key, message, sizeof(message) - 1, signature, + &signature_length, &error_code)) { + fprintf(stderr, "LMS signing failed: %u\n", error_code); + goto done; + } + if (!LmsVerify(public_key, message, sizeof(message) - 1, signature, + signature_length)) { + fprintf(stderr, "LMS signature verification failed\n"); + goto done; + } + + signature[0] ^= 1; + if (LmsVerify(public_key, message, sizeof(message) - 1, signature, + signature_length)) { + fprintf(stderr, "Modified LMS signature unexpectedly verified\n"); + goto done; + } + + result = EXIT_SUCCESS; + +done: + free(signature); + cleanup(); + return result; +} From 0de374a8b0605591af30b8c97b586d0dba613e1b Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Sun, 16 Aug 2026 21:00:34 -0500 Subject: [PATCH 15/45] Use EVP for LMS SHA-256 hashing --- lms-hash-sigs/lms_hash.c | 52 ++++++++++++++++++++++++++++++---------- lms-hash-sigs/lms_hash.h | 2 +- lms-hash-sigs/sha256.h | 3 +-- tests/lms_sign_smoke.c | 2 +- 4 files changed, 43 insertions(+), 16 deletions(-) diff --git a/lms-hash-sigs/lms_hash.c b/lms-hash-sigs/lms_hash.c index 7ec583d..4f776d9 100755 --- a/lms-hash-sigs/lms_hash.c +++ b/lms-hash-sigs/lms_hash.c @@ -26,6 +26,18 @@ bool hss_verbose = false; #endif +static bool sha256_digest(void *result, const void *message, size_t message_len) { + EVP_MD_CTX *context = EVP_MD_CTX_new(); + unsigned int output_len = 0; + bool success = context + && EVP_DigestInit_ex(context, EVP_sha256(), NULL) == 1 + && EVP_DigestUpdate(context, message, message_len) == 1 + && EVP_DigestFinal_ex(context, result, &output_len) == 1 + && output_len == SHA256_LEN; + EVP_MD_CTX_free(context); + return success; +} + /* * This will hash the message, given the hash type. It assumes that the result * buffer is large enough for the hash @@ -41,17 +53,17 @@ void hss_hash_ctx(void *result, int hash_type, union hash_context *ctx, int output_len; switch (hash_type) { case HASH_SHA256: { - SHA256_Init(&ctx->sha256); - SHA256_Update(&ctx->sha256, message, message_len); - SHA256_Final(result, &ctx->sha256); + if (!sha256_digest(result, message, message_len)) { + memset(result, 0, SHA256_LEN); + } output_len = 32; break; } case HASH_SHA256_24: { unsigned char temp[SHA256_LEN]; - SHA256_Init(&ctx->sha256); - SHA256_Update(&ctx->sha256, message, message_len); - SHA256_Final(temp, &ctx->sha256); + if (!sha256_digest(temp, message, message_len)) { + memset(temp, 0, sizeof temp); + } memcpy(result, temp, 24 ); hss_zeroize(temp, sizeof temp); output_len = 24; @@ -89,7 +101,11 @@ void hss_hash(void *result, int hash_type, void hss_init_hash_context(int h, union hash_context *ctx) { switch (h) { case HASH_SHA256: case HASH_SHA256_24: - SHA256_Init( &ctx->sha256 ); + ctx->sha256 = EVP_MD_CTX_new(); + if (!ctx->sha256 || EVP_DigestInit_ex(ctx->sha256, EVP_sha256(), NULL) != 1) { + EVP_MD_CTX_free(ctx->sha256); + ctx->sha256 = NULL; + } break; case HASH_SHAKE256: case HASH_SHAKE256_24: shake256_inc_init(ctx->shake256); @@ -106,7 +122,9 @@ void hss_update_hash_context(int h, union hash_context *ctx, #endif switch (h) { case HASH_SHA256: case HASH_SHA256_24: - SHA256_Update(&ctx->sha256, msg, len_msg); + if (ctx->sha256) { + (void)EVP_DigestUpdate(ctx->sha256, msg, len_msg); + } break; case HASH_SHAKE256: case HASH_SHAKE256_24: shake256_inc_absorb(ctx->shake256, msg, len_msg); @@ -117,13 +135,24 @@ void hss_update_hash_context(int h, union hash_context *ctx, void hss_finalize_hash_context(int h, union hash_context *ctx, void *buffer) { int output_len; switch (h) { - case HASH_SHA256: - SHA256_Final(buffer, &ctx->sha256); + case HASH_SHA256: { + unsigned int digest_len = 0; + if (!ctx->sha256 || EVP_DigestFinal_ex(ctx->sha256, buffer, &digest_len) != 1 || digest_len != SHA256_LEN) { + memset(buffer, 0, SHA256_LEN); + } + EVP_MD_CTX_free(ctx->sha256); + ctx->sha256 = NULL; output_len = 32; break; + } case HASH_SHA256_24: { unsigned char temp[SHA256_LEN]; - SHA256_Final(temp, &ctx->sha256); + unsigned int digest_len = 0; + if (!ctx->sha256 || EVP_DigestFinal_ex(ctx->sha256, temp, &digest_len) != 1 || digest_len != SHA256_LEN) { + memset(temp, 0, sizeof temp); + } + EVP_MD_CTX_free(ctx->sha256); + ctx->sha256 = NULL; memcpy(buffer, temp, 24); hss_zeroize(temp, sizeof temp); output_len = 24; @@ -169,4 +198,3 @@ unsigned hss_hash_blocksize(int hash_type) { } return 0; } - diff --git a/lms-hash-sigs/lms_hash.h b/lms-hash-sigs/lms_hash.h index ff615d9..aeb646c 100755 --- a/lms-hash-sigs/lms_hash.h +++ b/lms-hash-sigs/lms_hash.h @@ -22,7 +22,7 @@ enum { }; union hash_context { - SHA256_CTX sha256; + EVP_MD_CTX *sha256; keccak_state shake256; /* Any other hash contexts would go here */ }; diff --git a/lms-hash-sigs/sha256.h b/lms-hash-sigs/sha256.h index 152419a..7d85ae7 100755 --- a/lms-hash-sigs/sha256.h +++ b/lms-hash-sigs/sha256.h @@ -10,7 +10,7 @@ #if USE_OPENSSL -#include +#include #else @@ -34,4 +34,3 @@ void SHA256_Final(unsigned char *, #endif #endif /* ifdef(SHA256_H_) */ - diff --git a/tests/lms_sign_smoke.c b/tests/lms_sign_smoke.c index a8996cf..f148f9e 100644 --- a/tests/lms_sign_smoke.c +++ b/tests/lms_sign_smoke.c @@ -18,7 +18,7 @@ int main(void) { const char *private_key = "lms-smoke.prv"; const char *public_key = "lms-smoke.pub"; const unsigned char message[] = "Bootgen LMS signing smoke test"; - int hash[] = { 2 }; + int hash[] = { 1 }; int height[] = { 5 }; int width[] = { 2 }; int result = EXIT_FAILURE; From c57dccf4a13c2185547efd20910f6767d819967d Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 17 Aug 2026 03:43:15 -0500 Subject: [PATCH 16/45] Test incremental LMS hashing --- CMakeLists.txt | 6 ++++++ tests/lms_hash_smoke.c | 49 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 tests/lms_hash_smoke.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 22eb4ee..be12dae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -250,9 +250,14 @@ if(BOOTGEN_BUILD_TESTS) add_executable(lms-sign-smoke tests/lms_sign_smoke.c) target_include_directories(lms-sign-smoke PRIVATE lms-hash-sigs) target_link_libraries(lms-sign-smoke PRIVATE lms-hash-sigs Threads::Threads) + add_executable(lms-hash-smoke tests/lms_hash_smoke.c) + target_include_directories(lms-hash-smoke PRIVATE lms-hash-sigs) + target_link_libraries(lms-hash-smoke PRIVATE lms-hash-sigs OpenSSL::Crypto Threads::Threads) if(BOOTGEN_ENABLE_SANITIZERS) target_compile_options(lms-sign-smoke PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer) target_link_options(lms-sign-smoke PRIVATE -fsanitize=address,undefined) + target_compile_options(lms-hash-smoke PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer) + target_link_options(lms-hash-smoke PRIVATE -fsanitize=address,undefined) endif() file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/tests/lms-sign-smoke") @@ -306,6 +311,7 @@ if(BOOTGEN_BUILD_TESTS) add_test(NAME bootgen.lms-sign-smoke COMMAND lms-sign-smoke) set_tests_properties(bootgen.lms-sign-smoke PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/tests/lms-sign-smoke") + add_test(NAME bootgen.lms-hash-smoke COMMAND lms-hash-smoke) add_test( NAME bootgen.spartanup-smoke COMMAND ${CMAKE_COMMAND} diff --git a/tests/lms_hash_smoke.c b/tests/lms_hash_smoke.c new file mode 100644 index 0000000..27abb27 --- /dev/null +++ b/tests/lms_hash_smoke.c @@ -0,0 +1,49 @@ +#include +#include + +#include "lms_hash.h" + +static int check_sha256(void) { + static const unsigned char expected[SHA256_LEN] = { + 0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, + 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23, + 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, + 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad + }; + unsigned char digest[SHA256_LEN]; + union hash_context context; + + hss_hash(digest, HASH_SHA256, "abc", 3); + if (memcmp(digest, expected, sizeof digest) != 0) return 0; + + hss_init_hash_context(HASH_SHA256, &context); + hss_update_hash_context(HASH_SHA256, &context, "a", 1); + hss_update_hash_context(HASH_SHA256, &context, "bc", 2); + hss_finalize_hash_context(HASH_SHA256, &context, digest); + return memcmp(digest, expected, sizeof digest) == 0; +} + +static int check_shake256(void) { + unsigned char one_shot[SHA256_LEN]; + unsigned char incremental[SHA256_LEN]; + union hash_context context; + + hss_hash(one_shot, HASH_SHAKE256, "abc", 3); + hss_init_hash_context(HASH_SHAKE256, &context); + hss_update_hash_context(HASH_SHAKE256, &context, "a", 1); + hss_update_hash_context(HASH_SHAKE256, &context, "bc", 2); + hss_finalize_hash_context(HASH_SHAKE256, &context, incremental); + return memcmp(one_shot, incremental, sizeof one_shot) == 0; +} + +int main(void) { + if (!check_sha256()) { + fprintf(stderr, "LMS SHA-256 hash result did not match the expected digest\n"); + return 1; + } + if (!check_shake256()) { + fprintf(stderr, "LMS SHAKE-256 one-shot and incremental hashes differ\n"); + return 1; + } + return 0; +} From 7ea7a230d9c8cd14b0d275e3d1f9a55a34ab87ba Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 17 Aug 2026 09:58:38 -0500 Subject: [PATCH 17/45] Add guarded universal macOS packaging workflow --- BUILDING.md | 26 +++++++-- cmake/package_universal_macos.cmake | 82 +++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 cmake/package_universal_macos.cmake diff --git a/BUILDING.md b/BUILDING.md index b142254..ae94146 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -39,8 +39,25 @@ support the selected target. For an Intel build, run the same configuration on an Intel Mac and set `CMAKE_OSX_ARCHITECTURES=x86_64`. Test each architecture before any universal -binary packaging. A merged executable requires OpenSSL runtime libraries that -also support both architectures. +binary packaging. Assemble separately tested slices with the checked +compatibility workflow: + +```sh +cmake \ + -DSOURCE_DIR="$PWD" \ + -DARM64_BINARY="$PWD/build/macos-arm64/bootgen" \ + -DX86_64_BINARY="$PWD/build/macos-x86_64/bootgen" \ + -DOUTPUT_DIR="$PWD/build/universal" \ + -P cmake/package_universal_macos.cmake +file build/universal/bootgen-universal/bin/bootgen +codesign --force --sign - build/universal/bootgen-universal/bin/bootgen +codesign --verify --deep --strict build/universal/bootgen-universal/bin/bootgen +``` + +The script verifies both executable slices, requires their `otool -L` runtime +dependencies to match, and checks non-system absolute dependencies (including +OpenSSL) are themselves universal. The resulting staging tree includes the +binary, license, and build documentation. Install into a staging directory with: @@ -69,9 +86,8 @@ codesign --verify --deep --strict build/macos-arm64/bootgen ``` Ad-hoc signing is appropriate for local development. A released universal -package requires compatible universal OpenSSL libraries (or separately tested -architecture-specific OpenSSL dependencies); it must not merge executables -against incompatible runtime libraries. +package requires compatible universal OpenSSL libraries; it must not merge +executables against incompatible runtime libraries. The compatibility Makefile provides a `macos` target that delegates to CMake: diff --git a/cmake/package_universal_macos.cmake b/cmake/package_universal_macos.cmake new file mode 100644 index 0000000..42ffa0e --- /dev/null +++ b/cmake/package_universal_macos.cmake @@ -0,0 +1,82 @@ +if(NOT APPLE) + message(FATAL_ERROR "A universal macOS package can only be assembled on macOS") +endif() + +foreach(REQUIRED_VARIABLE SOURCE_DIR ARM64_BINARY X86_64_BINARY OUTPUT_DIR) + if(NOT DEFINED ${REQUIRED_VARIABLE} OR "${${REQUIRED_VARIABLE}}" STREQUAL "") + message(FATAL_ERROR "${REQUIRED_VARIABLE} is required") + endif() +endforeach() + +find_program(LIPO_EXECUTABLE lipo REQUIRED) +find_program(OTOOL_EXECUTABLE otool REQUIRED) + +function(require_architecture BINARY ARCHITECTURE) + execute_process( + COMMAND "${LIPO_EXECUTABLE}" -archs "${BINARY}" + RESULT_VARIABLE LIPO_RESULT + OUTPUT_VARIABLE BINARY_ARCHITECTURES + ERROR_VARIABLE LIPO_ERROR + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT LIPO_RESULT EQUAL 0 OR NOT BINARY_ARCHITECTURES MATCHES "(^| )${ARCHITECTURE}( |$)") + message(FATAL_ERROR "${BINARY} does not contain ${ARCHITECTURE}: ${LIPO_ERROR}") + endif() +endfunction() + +function(read_dependencies BINARY OUTPUT_VARIABLE) + execute_process( + COMMAND "${OTOOL_EXECUTABLE}" -L "${BINARY}" + RESULT_VARIABLE OTOOL_RESULT + OUTPUT_VARIABLE OTOOL_OUTPUT + ERROR_VARIABLE OTOOL_ERROR) + if(NOT OTOOL_RESULT EQUAL 0) + message(FATAL_ERROR "Unable to inspect ${BINARY}: ${OTOOL_ERROR}") + endif() + string(REGEX REPLACE "^[^\n]*\n" "" OTOOL_OUTPUT "${OTOOL_OUTPUT}") + set(${OUTPUT_VARIABLE} "${OTOOL_OUTPUT}" PARENT_SCOPE) +endfunction() + +require_architecture("${ARM64_BINARY}" arm64) +require_architecture("${X86_64_BINARY}" x86_64) +read_dependencies("${ARM64_BINARY}" ARM64_DEPENDENCIES) +read_dependencies("${X86_64_BINARY}" X86_64_DEPENDENCIES) +if(NOT "${ARM64_DEPENDENCIES}" STREQUAL "${X86_64_DEPENDENCIES}") + message(FATAL_ERROR "The architecture slices use different runtime dependencies; rebuild both against the same universal dependency installation") +endif() + +string(REPLACE "\n" ";" DEPENDENCY_LINES "${ARM64_DEPENDENCIES}") +foreach(DEPENDENCY_LINE IN LISTS DEPENDENCY_LINES) + string(STRIP "${DEPENDENCY_LINE}" DEPENDENCY_LINE) + if(DEPENDENCY_LINE STREQUAL "") + continue() + endif() + string(REGEX REPLACE "^[^ ]+" "" DEPENDENCY_TRAILER "${DEPENDENCY_LINE}") + string(REGEX REPLACE "^[ ]*" "" DEPENDENCY_TRAILER "${DEPENDENCY_TRAILER}") + string(REPLACE "${DEPENDENCY_TRAILER}" "" DEPENDENCY_PATH "${DEPENDENCY_LINE}") + if(DEPENDENCY_PATH MATCHES "^/usr/lib/" OR DEPENDENCY_PATH MATCHES "^@") + continue() + endif() + if(NOT IS_ABSOLUTE "${DEPENDENCY_PATH}" OR NOT EXISTS "${DEPENDENCY_PATH}") + message(FATAL_ERROR "Cannot verify universal runtime dependency ${DEPENDENCY_PATH}") + endif() + require_architecture("${DEPENDENCY_PATH}" arm64) + require_architecture("${DEPENDENCY_PATH}" x86_64) +endforeach() + +set(PACKAGE_ROOT "${OUTPUT_DIR}/bootgen-universal") +if(EXISTS "${PACKAGE_ROOT}") + message(FATAL_ERROR "Refusing to overwrite existing package directory ${PACKAGE_ROOT}") +endif() +file(MAKE_DIRECTORY "${PACKAGE_ROOT}/bin" "${PACKAGE_ROOT}/share/doc/bootgen") +execute_process( + COMMAND "${LIPO_EXECUTABLE}" -create "${ARM64_BINARY}" "${X86_64_BINARY}" -output "${PACKAGE_ROOT}/bin/bootgen" + RESULT_VARIABLE CREATE_RESULT + ERROR_VARIABLE CREATE_ERROR) +if(NOT CREATE_RESULT EQUAL 0) + message(FATAL_ERROR "Unable to create universal Bootgen executable: ${CREATE_ERROR}") +endif() +require_architecture("${PACKAGE_ROOT}/bin/bootgen" arm64) +require_architecture("${PACKAGE_ROOT}/bin/bootgen" x86_64) +file(COPY "${SOURCE_DIR}/LICENSE" "${SOURCE_DIR}/README.md" "${SOURCE_DIR}/INSTALL.txt" "${SOURCE_DIR}/BUILDING.md" + DESTINATION "${PACKAGE_ROOT}/share/doc/bootgen") +message(STATUS "Created universal macOS staging tree: ${PACKAGE_ROOT}") From 5b04ba2833e481ec4c86d0a39ac4eb1eb0e0aba5 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 17 Aug 2026 19:18:17 -0500 Subject: [PATCH 18/45] Include third-party notices in macOS packages --- BUILDING.md | 7 ++++++- CMakeLists.txt | 5 ++++- THIRD_PARTY_NOTICES.md | 19 +++++++++++++++++++ cmake/package_universal_macos.cmake | 4 +++- 4 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 THIRD_PARTY_NOTICES.md diff --git a/BUILDING.md b/BUILDING.md index ae94146..a61d52a 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -68,7 +68,7 @@ cmake --install build/macos-arm64 --prefix "$PWD/stage" ## macOS packaging and runtime dependencies The CMake `package` target creates an architecture-specific tarball containing -`bootgen`, its Apache-2.0 license, and build documentation: +`bootgen`, its license notices, and build documentation: ```sh cmake --build build/macos-arm64 --target package @@ -85,6 +85,11 @@ codesign --force --sign - build/macos-arm64/bootgen codesign --verify --deep --strict build/macos-arm64/bootgen ``` +See `THIRD_PARTY_NOTICES.md` for the source and binary distribution inventory. +The package includes Bootgen's aggregate `LICENSE` and the separate Cisco +BSD-3-Clause notice for the LMS/HSS implementation. OpenSSL remains a dynamic, +caller-supplied dependency and is not redistributed in the package. + Ad-hoc signing is appropriate for local development. A released universal package requires compatible universal OpenSSL libraries; it must not merge executables against incompatible runtime libraries. diff --git a/CMakeLists.txt b/CMakeLists.txt index be12dae..5bf7327 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -233,7 +233,10 @@ endif() include(GNUInstallDirs) install(TARGETS bootgen RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) -install(FILES LICENSE README.md INSTALL.txt BUILDING.md DESTINATION ${CMAKE_INSTALL_DOCDIR}) +install(FILES LICENSE README.md INSTALL.txt BUILDING.md THIRD_PARTY_NOTICES.md + DESTINATION ${CMAKE_INSTALL_DOCDIR}) +install(FILES lms-hash-sigs/license.txt DESTINATION ${CMAKE_INSTALL_DOCDIR} + RENAME LMS-HSS-LICENSE.txt) set(CPACK_GENERATOR TGZ) set(CPACK_PACKAGE_NAME bootgen) diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..6f486de --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,19 @@ +# Third-party notices + +This inventory applies to Bootgen source releases and the binary packages +created by the CMake packaging workflows. + +| Component | Location | Notice supplied in package | +| --- | --- | --- | +| Bootgen source | Repository source files | `LICENSE` (Apache-2.0) | +| GNU Bison 2.7 parser skeleton | `bisonflex/` generated parser files | `LICENSE` (Bison special exception) | +| Flex 2.5.35 scanner | `bisonflex/` generated scanner files and `win_include/FlexLexer.h` | `LICENSE` (BSD-style notice) | +| Keccak 3.3 | `common/src/Keccak-compact.cpp` and architecture variants | `LICENSE` (CC0/public-domain dedication) | +| LMS/HSS implementation | `lms-hash-sigs/` | `LMS-HSS-LICENSE.txt` (Cisco BSD-3-Clause) | +| OpenSSL | Dynamically linked host dependency | Not bundled; obtain its notices from the installed OpenSSL distribution | + +The root `LICENSE` retains the notices already distributed by upstream. The +LMS/HSS notice is installed separately because it is maintained in +`lms-hash-sigs/license.txt` in the source tree. Release maintainers must review +this inventory whenever adding, updating, bundling, or statically linking a +third-party component. diff --git a/cmake/package_universal_macos.cmake b/cmake/package_universal_macos.cmake index 42ffa0e..e5f9653 100644 --- a/cmake/package_universal_macos.cmake +++ b/cmake/package_universal_macos.cmake @@ -77,6 +77,8 @@ if(NOT CREATE_RESULT EQUAL 0) endif() require_architecture("${PACKAGE_ROOT}/bin/bootgen" arm64) require_architecture("${PACKAGE_ROOT}/bin/bootgen" x86_64) -file(COPY "${SOURCE_DIR}/LICENSE" "${SOURCE_DIR}/README.md" "${SOURCE_DIR}/INSTALL.txt" "${SOURCE_DIR}/BUILDING.md" +file(COPY "${SOURCE_DIR}/LICENSE" "${SOURCE_DIR}/README.md" "${SOURCE_DIR}/INSTALL.txt" "${SOURCE_DIR}/BUILDING.md" "${SOURCE_DIR}/THIRD_PARTY_NOTICES.md" DESTINATION "${PACKAGE_ROOT}/share/doc/bootgen") +file(COPY "${SOURCE_DIR}/lms-hash-sigs/license.txt" DESTINATION "${PACKAGE_ROOT}/share/doc/bootgen") +file(RENAME "${PACKAGE_ROOT}/share/doc/bootgen/license.txt" "${PACKAGE_ROOT}/share/doc/bootgen/LMS-HSS-LICENSE.txt") message(STATUS "Created universal macOS staging tree: ${PACKAGE_ROOT}") From 88be003b3d01f777b17246f85ff2a3feaded3a64 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 17 Aug 2026 20:19:42 -0500 Subject: [PATCH 19/45] Test deterministic boot image generation --- tests/cmake/versal_smoke.cmake | 17 +++++++++++++++++ tests/cmake/zynq_smoke.cmake | 17 +++++++++++++++++ tests/cmake/zynqmp_smoke.cmake | 17 +++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/tests/cmake/versal_smoke.cmake b/tests/cmake/versal_smoke.cmake index 0180d4a..dc43367 100644 --- a/tests/cmake/versal_smoke.cmake +++ b/tests/cmake/versal_smoke.cmake @@ -18,6 +18,23 @@ if(NOT CREATE_RESULT EQUAL 0) message(FATAL_ERROR "${ARCH} smoke image creation failed (${CREATE_RESULT}):\n${CREATE_OUTPUT}\n${CREATE_ERROR}") endif() +set(REPEAT_BOOT_IMAGE "${OUTPUT_DIR}/BOOT-repeat.BIN") +execute_process( + COMMAND "${BOOTGEN}" -arch "${ARCH}" -image "${TEST_FIXTURE_DIR}/versal-smoke.bif" -o "${REPEAT_BOOT_IMAGE}" -w on + WORKING_DIRECTORY "${TEST_FIXTURE_DIR}" + RESULT_VARIABLE REPEAT_RESULT + OUTPUT_VARIABLE REPEAT_OUTPUT + ERROR_VARIABLE REPEAT_ERROR) +if(NOT REPEAT_RESULT EQUAL 0) + message(FATAL_ERROR "Repeated ${ARCH} smoke image creation failed (${REPEAT_RESULT}):\n${REPEAT_OUTPUT}\n${REPEAT_ERROR}") +endif() +execute_process( + COMMAND "${CMAKE_COMMAND}" -E compare_files "${BOOT_IMAGE}" "${REPEAT_BOOT_IMAGE}" + RESULT_VARIABLE COMPARE_RESULT) +if(NOT COMPARE_RESULT EQUAL 0) + message(FATAL_ERROR "Repeated ${ARCH} smoke images are not byte-identical") +endif() + file(SIZE "${BOOT_IMAGE}" BOOT_IMAGE_SIZE) if(BOOT_IMAGE_SIZE LESS 512) message(FATAL_ERROR "${ARCH} smoke image is unexpectedly small: ${BOOT_IMAGE_SIZE} bytes") diff --git a/tests/cmake/zynq_smoke.cmake b/tests/cmake/zynq_smoke.cmake index 93a4ed5..dd6e7cf 100644 --- a/tests/cmake/zynq_smoke.cmake +++ b/tests/cmake/zynq_smoke.cmake @@ -25,6 +25,23 @@ if(NOT CREATE_RESULT EQUAL 0) message(FATAL_ERROR "Zynq smoke image creation failed (${CREATE_RESULT}):\n${CREATE_OUTPUT}\n${CREATE_ERROR}") endif() +set(REPEAT_BOOT_IMAGE "${OUTPUT_DIR}/BOOT-repeat.BIN") +execute_process( + COMMAND "${BOOTGEN}" -arch zynq -image "${TEST_FIXTURE_DIR}/zynq-smoke.bif" -o "${REPEAT_BOOT_IMAGE}" -w on + WORKING_DIRECTORY "${TEST_FIXTURE_DIR}" + RESULT_VARIABLE REPEAT_RESULT + OUTPUT_VARIABLE REPEAT_OUTPUT + ERROR_VARIABLE REPEAT_ERROR) +if(NOT REPEAT_RESULT EQUAL 0) + message(FATAL_ERROR "Repeated Zynq smoke image creation failed (${REPEAT_RESULT}):\n${REPEAT_OUTPUT}\n${REPEAT_ERROR}") +endif() +execute_process( + COMMAND "${CMAKE_COMMAND}" -E compare_files "${BOOT_IMAGE}" "${REPEAT_BOOT_IMAGE}" + RESULT_VARIABLE COMPARE_RESULT) +if(NOT COMPARE_RESULT EQUAL 0) + message(FATAL_ERROR "Repeated Zynq smoke images are not byte-identical") +endif() + file(SIZE "${BOOT_IMAGE}" BOOT_IMAGE_SIZE) if(BOOT_IMAGE_SIZE LESS 2048) message(FATAL_ERROR "Zynq smoke image is unexpectedly small: ${BOOT_IMAGE_SIZE} bytes") diff --git a/tests/cmake/zynqmp_smoke.cmake b/tests/cmake/zynqmp_smoke.cmake index a30b54f..7d9796f 100644 --- a/tests/cmake/zynqmp_smoke.cmake +++ b/tests/cmake/zynqmp_smoke.cmake @@ -18,6 +18,23 @@ if(NOT CREATE_RESULT EQUAL 0) message(FATAL_ERROR "ZynqMP smoke image creation failed (${CREATE_RESULT}):\n${CREATE_OUTPUT}\n${CREATE_ERROR}") endif() +set(REPEAT_BOOT_IMAGE "${OUTPUT_DIR}/BOOT-repeat.BIN") +execute_process( + COMMAND "${BOOTGEN}" -arch zynqmp -image "${TEST_FIXTURE_DIR}/zynqmp-smoke.bif" -o "${REPEAT_BOOT_IMAGE}" -w on + WORKING_DIRECTORY "${TEST_FIXTURE_DIR}" + RESULT_VARIABLE REPEAT_RESULT + OUTPUT_VARIABLE REPEAT_OUTPUT + ERROR_VARIABLE REPEAT_ERROR) +if(NOT REPEAT_RESULT EQUAL 0) + message(FATAL_ERROR "Repeated ZynqMP smoke image creation failed (${REPEAT_RESULT}):\n${REPEAT_OUTPUT}\n${REPEAT_ERROR}") +endif() +execute_process( + COMMAND "${CMAKE_COMMAND}" -E compare_files "${BOOT_IMAGE}" "${REPEAT_BOOT_IMAGE}" + RESULT_VARIABLE COMPARE_RESULT) +if(NOT COMPARE_RESULT EQUAL 0) + message(FATAL_ERROR "Repeated ZynqMP smoke images are not byte-identical") +endif() + file(SIZE "${BOOT_IMAGE}" BOOT_IMAGE_SIZE) if(BOOT_IMAGE_SIZE LESS 2048) message(FATAL_ERROR "ZynqMP smoke image is unexpectedly small: ${BOOT_IMAGE_SIZE} bytes") From b42e4838ea35b2c84d0c7a6eb36b1ca494653172 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Tue, 18 Aug 2026 02:07:47 -0500 Subject: [PATCH 20/45] Reject tampered authenticated ZynqMP images --- CMakeLists.txt | 4 +++ tests/cmake/zynqmp_auth_smoke.cmake | 24 +++++++++++-- tests/tamper_file.c | 53 +++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 tests/tamper_file.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 5bf7327..389d4c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -256,11 +256,14 @@ if(BOOTGEN_BUILD_TESTS) add_executable(lms-hash-smoke tests/lms_hash_smoke.c) target_include_directories(lms-hash-smoke PRIVATE lms-hash-sigs) target_link_libraries(lms-hash-smoke PRIVATE lms-hash-sigs OpenSSL::Crypto Threads::Threads) + add_executable(tamper-file tests/tamper_file.c) if(BOOTGEN_ENABLE_SANITIZERS) target_compile_options(lms-sign-smoke PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer) target_link_options(lms-sign-smoke PRIVATE -fsanitize=address,undefined) target_compile_options(lms-hash-smoke PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer) target_link_options(lms-hash-smoke PRIVATE -fsanitize=address,undefined) + target_compile_options(tamper-file PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer) + target_link_options(tamper-file PRIVATE -fsanitize=address,undefined) endif() file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/tests/lms-sign-smoke") @@ -284,6 +287,7 @@ if(BOOTGEN_BUILD_TESTS) NAME bootgen.zynqmp-auth-smoke COMMAND ${CMAKE_COMMAND} -DBOOTGEN=$ + -DTAMPER_FILE=$ -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/zynqmp-auth-smoke -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/zynqmp_auth_smoke.cmake) diff --git a/tests/cmake/zynqmp_auth_smoke.cmake b/tests/cmake/zynqmp_auth_smoke.cmake index 29fbda3..6afc841 100644 --- a/tests/cmake/zynqmp_auth_smoke.cmake +++ b/tests/cmake/zynqmp_auth_smoke.cmake @@ -1,5 +1,5 @@ -if(NOT DEFINED BOOTGEN OR NOT DEFINED FIXTURE_DIR OR NOT DEFINED OUTPUT_DIR) - message(FATAL_ERROR "BOOTGEN, FIXTURE_DIR, and OUTPUT_DIR are required") +if(NOT DEFINED BOOTGEN OR NOT DEFINED TAMPER_FILE OR NOT DEFINED FIXTURE_DIR OR NOT DEFINED OUTPUT_DIR) + message(FATAL_ERROR "BOOTGEN, TAMPER_FILE, FIXTURE_DIR, and OUTPUT_DIR are required") endif() file(MAKE_DIRECTORY "${OUTPUT_DIR}") @@ -50,4 +50,24 @@ if(NOT VERIFY_RESULT EQUAL 0) message(FATAL_ERROR "Authenticated ZynqMP smoke image verification failed (${VERIFY_RESULT}):\n${VERIFY_OUTPUT}\n${VERIFY_ERROR}") endif() +set(TAMPERED_BOOT_IMAGE "${OUTPUT_DIR}/BOOT-tampered.BIN") +execute_process( + COMMAND "${TAMPER_FILE}" "${BOOT_IMAGE}" "${TAMPERED_BOOT_IMAGE}" + RESULT_VARIABLE TAMPER_RESULT + OUTPUT_VARIABLE TAMPER_OUTPUT + ERROR_VARIABLE TAMPER_ERROR) +if(NOT TAMPER_RESULT EQUAL 0) + cleanup_test_keys() + message(FATAL_ERROR "Unable to modify authenticated ZynqMP smoke image (${TAMPER_RESULT}):\n${TAMPER_OUTPUT}\n${TAMPER_ERROR}") +endif() +execute_process( + COMMAND "${BOOTGEN}" -arch zynqmp -verify "${TAMPERED_BOOT_IMAGE}" + RESULT_VARIABLE TAMPER_VERIFY_RESULT + OUTPUT_VARIABLE TAMPER_VERIFY_OUTPUT + ERROR_VARIABLE TAMPER_VERIFY_ERROR) +if(TAMPER_VERIFY_RESULT EQUAL 0) + cleanup_test_keys() + message(FATAL_ERROR "Modified authenticated ZynqMP image unexpectedly verified:\n${TAMPER_VERIFY_OUTPUT}\n${TAMPER_VERIFY_ERROR}") +endif() + cleanup_test_keys() diff --git a/tests/tamper_file.c b/tests/tamper_file.c new file mode 100644 index 0000000..fa6a79d --- /dev/null +++ b/tests/tamper_file.c @@ -0,0 +1,53 @@ +#include +#include + +int main(int argc, char **argv) { + unsigned char buffer[4096]; + size_t bytes_read; + FILE *input; + FILE *output; + long input_size; + int last_byte; + + if (argc != 3) { + fprintf(stderr, "usage: tamper-file \n"); + return EXIT_FAILURE; + } + input = fopen(argv[1], "rb"); + if (!input) { + perror("opening input"); + return EXIT_FAILURE; + } + output = fopen(argv[2], "wb"); + if (!output) { + perror("opening output"); + fclose(input); + return EXIT_FAILURE; + } + while ((bytes_read = fread(buffer, 1, sizeof buffer, input)) != 0) { + if (fwrite(buffer, 1, bytes_read, output) != bytes_read) { + perror("writing output"); + fclose(output); + fclose(input); + return EXIT_FAILURE; + } + } + if (ferror(input) || fclose(input) != 0 || fclose(output) != 0) { + fprintf(stderr, "copying input failed\n"); + return EXIT_FAILURE; + } + + output = fopen(argv[2], "r+b"); + if (!output) { + perror("reopening output"); + return EXIT_FAILURE; + } + if (fseek(output, 0, SEEK_END) != 0 || (input_size = ftell(output)) < 1 + || fseek(output, -1, SEEK_END) != 0 || (last_byte = fgetc(output)) == EOF + || fseek(output, -1, SEEK_CUR) != 0 || fputc(last_byte ^ 1, output) == EOF + || fclose(output) != 0) { + fprintf(stderr, "modifying output failed\n"); + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} From cb9817cf01e97f495c2562ec5ceb4cb6b1eb4e63 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Tue, 18 Aug 2026 11:20:37 -0500 Subject: [PATCH 21/45] Record native macOS port baseline --- UPSTREAM_BASELINE.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 UPSTREAM_BASELINE.md diff --git a/UPSTREAM_BASELINE.md b/UPSTREAM_BASELINE.md new file mode 100644 index 0000000..c2f1f07 --- /dev/null +++ b/UPSTREAM_BASELINE.md @@ -0,0 +1,23 @@ +# Upstream baseline + +`feat/native-macos` was created from the following Xilinx Bootgen upstream +commit: + +```text +remote: https://github.com/Xilinx/bootgen.git +branch: upstream/master +commit: 510dc3a4ffb2bd29fcfb8f8d7a1c5aa2bb1f39f1 +subject: presign function broken in HSM flow +``` + +Verify the branch ancestry before opening or refreshing an upstream pull +request: + +```sh +git fetch upstream +git merge-base --is-ancestor 510dc3a4ffb2bd29fcfb8f8d7a1c5aa2bb1f39f1 HEAD +git log --oneline 510dc3a4ffb2bd29fcfb8f8d7a1c5aa2bb1f39f1..HEAD +``` + +This branch deliberately retains small, reviewable commits so porting, +security, and test changes can be proposed upstream independently when useful. From 5a06bf8948fb1cdaebbbc947477078b6db467415 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Tue, 18 Aug 2026 11:20:37 -0500 Subject: [PATCH 22/45] Handle POSIX separators in split output names --- CMakeLists.txt | 5 +++++ common/src/bootimage.cpp | 6 +++--- tests/stringutils_smoke.cpp | 17 +++++++++++++++++ utils/include/stringutils.h | 36 ++++-------------------------------- 4 files changed, 29 insertions(+), 35 deletions(-) create mode 100644 tests/stringutils_smoke.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 389d4c7..2afd780 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -257,6 +257,8 @@ if(BOOTGEN_BUILD_TESTS) target_include_directories(lms-hash-smoke PRIVATE lms-hash-sigs) target_link_libraries(lms-hash-smoke PRIVATE lms-hash-sigs OpenSSL::Crypto Threads::Threads) add_executable(tamper-file tests/tamper_file.c) + add_executable(stringutils-smoke tests/stringutils_smoke.cpp) + target_include_directories(stringutils-smoke PRIVATE utils/include) if(BOOTGEN_ENABLE_SANITIZERS) target_compile_options(lms-sign-smoke PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer) target_link_options(lms-sign-smoke PRIVATE -fsanitize=address,undefined) @@ -264,6 +266,8 @@ if(BOOTGEN_BUILD_TESTS) target_link_options(lms-hash-smoke PRIVATE -fsanitize=address,undefined) target_compile_options(tamper-file PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer) target_link_options(tamper-file PRIVATE -fsanitize=address,undefined) + target_compile_options(stringutils-smoke PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer) + target_link_options(stringutils-smoke PRIVATE -fsanitize=address,undefined) endif() file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/tests/lms-sign-smoke") @@ -319,6 +323,7 @@ if(BOOTGEN_BUILD_TESTS) set_tests_properties(bootgen.lms-sign-smoke PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/tests/lms-sign-smoke") add_test(NAME bootgen.lms-hash-smoke COMMAND lms-hash-smoke) + add_test(NAME bootgen.stringutils-smoke COMMAND stringutils-smoke) add_test( NAME bootgen.spartanup-smoke COMMAND ${CMAKE_COMMAND} diff --git a/common/src/bootimage.cpp b/common/src/bootimage.cpp index 9be8c1f..f760f85 100755 --- a/common/src/bootimage.cpp +++ b/common/src/bootimage.cpp @@ -317,9 +317,9 @@ void BIF_File::Output(Options& options, uint8_t index) if (options.GetOutType() != File::Unknown) { fName.clear(); - } + } fName = StringUtils::RemoveExtension(biffilename); - fName = fName.substr(fName.find_last_of("\\")+1, std::string::npos); + fName = StringUtils::BaseName(fName); if(splitFileType == File::Unknown) { splitFileType = options.GetOutputFormat(); @@ -1027,4 +1027,4 @@ void BIF_File::AppendAndReplaceFilesinBIF() } } } -} \ No newline at end of file +} diff --git a/tests/stringutils_smoke.cpp b/tests/stringutils_smoke.cpp new file mode 100644 index 0000000..01535b1 --- /dev/null +++ b/tests/stringutils_smoke.cpp @@ -0,0 +1,17 @@ +#include +#include +#include + +#include "stringutils.h" + +int main() +{ + if (StringUtils::BaseName("parent/child\\image.bif") != "image.bif" + || StringUtils::BaseName("parent\\child/image.bif") != "image.bif" + || StringUtils::FolderPath("parent/child\\image.bif") != "parent/child" + || StringUtils::FolderPath("parent\\child/image.bif") != "parent\\child") { + std::cerr << "mixed path separators were not handled consistently\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} diff --git a/utils/include/stringutils.h b/utils/include/stringutils.h index d436a61..9e80a5a 100755 --- a/utils/include/stringutils.h +++ b/utils/include/stringutils.h @@ -83,22 +83,8 @@ class StringUtils static std::string BaseName(const std::string& fullpath) { - size_t i1 = fullpath.rfind("/"); - size_t i2 = fullpath.rfind("\\"); - std::string x; - if (i1 != std::string::npos) - { - x = fullpath.substr(i1+1); - } - else if (i2 != std::string::npos) - { - x = fullpath.substr(i2+1); - } - else - { - x = fullpath; - } - return x; + size_t separator = fullpath.find_last_of("/\\"); + return separator == std::string::npos ? fullpath : fullpath.substr(separator + 1); } static std::string Format(const std::string fmt, ...) @@ -153,22 +139,8 @@ class StringUtils static std::string FolderPath(const std::string& fullpath) { - size_t i1 = fullpath.rfind("/"); - size_t i2 = fullpath.rfind("\\"); - std::string x; - if (i1 != std::string::npos) - { - x = fullpath.substr(0, i1); - } - else if (i2 != std::string::npos) - { - x = fullpath.substr(0, i2); - } - else - { - x = "./"; - } - return x; + size_t separator = fullpath.find_last_of("/\\"); + return separator == std::string::npos ? "./" : fullpath.substr(0, separator); } }; #endif From 85ecb93b973348ef81a6a6172e5dde3ea5aeeb20 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Tue, 18 Aug 2026 18:37:25 -0500 Subject: [PATCH 23/45] Test split output paths from nested BIF files --- CMakeLists.txt | 7 ++++++ tests/cmake/split_output_smoke.cmake | 37 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 tests/cmake/split_output_smoke.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 2afd780..c8b2c19 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -280,6 +280,13 @@ if(BOOTGEN_BUILD_TESTS) -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/zynq smoke-ü -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/zynq_smoke.cmake) + add_test( + NAME bootgen.split-output-smoke + COMMAND ${CMAKE_COMMAND} + -DBOOTGEN=$ + -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures + -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/split-output-smoke + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/split_output_smoke.cmake) add_test( NAME bootgen.zynqmp-smoke COMMAND ${CMAKE_COMMAND} diff --git a/tests/cmake/split_output_smoke.cmake b/tests/cmake/split_output_smoke.cmake new file mode 100644 index 0000000..cc2dd3e --- /dev/null +++ b/tests/cmake/split_output_smoke.cmake @@ -0,0 +1,37 @@ +if(NOT DEFINED BOOTGEN OR NOT DEFINED FIXTURE_DIR OR NOT DEFINED OUTPUT_DIR) + message(FATAL_ERROR "BOOTGEN, FIXTURE_DIR, and OUTPUT_DIR are required") +endif() + +set(WORK_DIR "${OUTPUT_DIR}/work") +set(INPUT_DIR "${WORK_DIR}/input") +file(MAKE_DIRECTORY "${INPUT_DIR}") +execute_process( + COMMAND "${CMAKE_COMMAND}" -E copy "${FIXTURE_DIR}/zynq-smoke.elf" "${WORK_DIR}/zynq-smoke.elf" + RESULT_VARIABLE COPY_RESULT) +if(NOT COPY_RESULT EQUAL 0) + message(FATAL_ERROR "Unable to create the split-output ELF fixture") +endif() +file(WRITE "${INPUT_DIR}/zynq-split.bif" "the_ROM_image:\n{\n [bootloader] zynq-smoke.elf\n}\n") + +execute_process( + COMMAND "${BOOTGEN}" -arch zynq -image input/zynq-split.bif -split bin -w on + WORKING_DIRECTORY "${WORK_DIR}" + RESULT_VARIABLE CREATE_RESULT + OUTPUT_VARIABLE CREATE_OUTPUT + ERROR_VARIABLE CREATE_ERROR) +if(NOT CREATE_RESULT EQUAL 0) + message(FATAL_ERROR "Split output creation failed (${CREATE_RESULT}):\n${CREATE_OUTPUT}\n${CREATE_ERROR}") +endif() + +set(EXPECTED_OUTPUT "${WORK_DIR}/zynq-split.bin") +set(UNEXPECTED_OUTPUT "${INPUT_DIR}/zynq-split.bin") +if(NOT EXISTS "${EXPECTED_OUTPUT}") + message(FATAL_ERROR "Split output was not created in the working directory") +endif() +if(EXISTS "${UNEXPECTED_OUTPUT}") + message(FATAL_ERROR "Split output incorrectly retained the BIF input directory") +endif() +file(SIZE "${EXPECTED_OUTPUT}" OUTPUT_SIZE) +if(OUTPUT_SIZE LESS 2048) + message(FATAL_ERROR "Split output is unexpectedly small: ${OUTPUT_SIZE} bytes") +endif() From 8549a50cb2cf834a4e8252a272a742b4bf4e3e07 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Tue, 18 Aug 2026 23:37:43 -0500 Subject: [PATCH 24/45] Document GitHub Actions contribution workflow --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index bbd0a69..6ac8bf5 100755 --- a/README.md +++ b/README.md @@ -32,6 +32,20 @@ The following 3rd party packages are required to build Bootgen: See [BUILDING.md](BUILDING.md) for CMake, macOS, installation, and sanitizer instructions. +## Continuous integration + +The GitHub Actions workflow in [`.github/workflows/build.yml`](.github/workflows/build.yml) +configures, builds, tests, and packages Bootgen on Ubuntu with GCC and Clang +(including an AddressSanitizer/UndefinedBehaviorSanitizer build), macOS Apple +Silicon, and macOS Intel. macOS jobs install Homebrew `openssl@3` and verify +that the resulting executable has the expected architecture. + +To run the workflow for a contribution, fork the repository, add the fork as +your `origin` remote, push a branch to that fork, and open a pull request. The +workflow is intentionally available on both pushes and pull requests; a +universal macOS package must still be assembled from independently tested +native `arm64` and `x86_64` builds as described in `BUILDING.md`. + ## Linux Builds A Makefile is included along with the code to build Bootgen for Linux. This From 253e581bd31cbde1af16184a7132417eb87bf832 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Tue, 18 Aug 2026 23:37:43 -0500 Subject: [PATCH 25/45] Use public OpenSSL APIs for ZynqMP RSA verification --- common/src/authkeys.cpp | 87 +++++++++++------------------ zynqmp/src/verifyimage-zynqmp.cpp | 91 ++++++------------------------- 2 files changed, 49 insertions(+), 129 deletions(-) diff --git a/common/src/authkeys.cpp b/common/src/authkeys.cpp index faa563b..0d0f492 100755 --- a/common/src/authkeys.cpp +++ b/common/src/authkeys.cpp @@ -232,38 +232,22 @@ uint8_t Key::ParseOpenSSLKey(FILE* f) { return 1; } -#if OPENSSL_VERSION_NUMBER > 0x10100000L const BIGNUM* dCheck = RSA_get0_d(rsaInst.rsa); const BIGNUM* nCheck = RSA_get0_n(rsaInst.rsa); -#else - const BIGNUM* dCheck = rsaInst.rsa->d; - const BIGNUM* nCheck = rsaInst.rsa->n; -#endif if (dCheck == NULL || nCheck == NULL) { LOG_ERROR("Invalid secret key file - private RSA key not found"); } -#if OPENSSL_VERSION_NUMBER > 0x10100000L keySzRd = BN_num_bytes(RSA_get0_n(rsaInst.rsa)); -#else - keySzRd = BN_num_bytes(rsaInst.rsa->n); -#endif if (keySzRd != keySize) { LOG_ERROR("Incorrect Key Size !!!\n\t Key Size is %d bits. Expected key size is %d bits", keySzRd * 8, keySize * 8); } -#if OPENSSL_VERSION_NUMBER > 0x10100000L -#ifdef _MSC_VER - /* Use BN_bn2lebinpad for safe BIGNUM-to-buffer conversion on MSVC, - direct ->d access is unsafe due to BIGNUM struct layout mismatch */ - BN_bn2lebinpad(RSA_get0_d(rsaInst.rsa), D.get(), keySize); -#else - memcpy(D.get(), RSA_get0_d(rsaInst.rsa)->d, keySize); -#endif -#else - memcpy(D.get(), rsaInst.rsa->d->d, keySize); -#endif + if (BN_bn2lebinpad(dCheck, D.get(), keySize) != keySize) + { + LOG_ERROR("Unable to export RSA private exponent"); + } } else { @@ -272,31 +256,20 @@ uint8_t Key::ParseOpenSSLKey(FILE* f) { return 1; } -#if OPENSSL_VERSION_NUMBER > 0x10100000L keySzRd = BN_num_bytes(RSA_get0_n(rsaInst.rsa)); -#else - keySzRd = BN_num_bytes(rsaInst.rsa->n); -#endif if (keySzRd != keySize) { LOG_ERROR("Incorrect Key Size !!!\n\t Key Size is %d bits. Expected key size is %d bits", keySzRd * 8, keySize * 8); } memset(D.get(),0,keySize); } -#if OPENSSL_VERSION_NUMBER > 0x10100000L -#ifdef _MSC_VER - /* Use BN_bn2lebinpad for safe BIGNUM-to-buffer conversion on MSVC, - direct ->d access is unsafe due to BIGNUM struct layout mismatch */ - BN_bn2lebinpad(RSA_get0_n(rsaInst.rsa), N.get(), keySize); - BN_bn2lebinpad(RSA_get0_e(rsaInst.rsa), E.get(), sizeof(uint32_t)); -#else - memcpy(N.get(), RSA_get0_n(rsaInst.rsa)->d, keySize); - memcpy(E.get(), RSA_get0_e(rsaInst.rsa)->d, sizeof(uint32_t)); -#endif -#else - memcpy(N.get(), rsaInst.rsa->n->d, keySize); - memcpy(E.get(), rsaInst.rsa->e->d, sizeof(uint32_t)); -#endif + // Bootgen keeps RSA components in little-endian buffers until the family + // authentication code serializes them into an image. + if (BN_bn2lebinpad(RSA_get0_n(rsaInst.rsa), N.get(), keySize) != keySize + || BN_bn2lebinpad(RSA_get0_e(rsaInst.rsa), E.get(), sizeof(uint32_t)) != sizeof(uint32_t)) + { + LOG_ERROR("Unable to export RSA public key"); + } return 0; } @@ -458,34 +431,38 @@ void Key::WriteRsaFile(std::string filename, const RSA* rsa, bool secret, uint16 if (file) { auto temp = std::make_unique(keyLength); + const BIGNUM* modulus = RSA_get0_n(rsa); + const BIGNUM* exponent = RSA_get0_e(rsa); + const BIGNUM* privateExponent = RSA_get0_d(rsa); + if (modulus == NULL || exponent == NULL || (secret && privateExponent == NULL)) + { + LOG_ERROR("Invalid RSA key while writing authentication key file"); + } file << "N = "; - -#if OPENSSL_VERSION_NUMBER > 0x10100000L - memcpy(temp.get(), RSA_get0_n(rsa)->d, keyLength); -#else - memcpy(temp.get(), rsa->n->d, keyLength); -#endif + if (BN_bn2lebinpad(modulus, temp.get(), keyLength) != keyLength) + { + LOG_ERROR("Unable to export RSA modulus"); + } for (uint32_t index = keyLength; index != 0; index--) { file << std::uppercase << std::hex << std::setfill('0') << std::setw(2) << int(temp[index - 1]); } file << "\n\nE = "; -#if OPENSSL_VERSION_NUMBER > 0x10100000L - uint32_t* temp_e = (uint32_t*)RSA_get0_e(rsa)->d; -#else - uint32_t* temp_e = (uint32_t*)rsa->e->d; -#endif - file << std::uppercase << std::hex << std::setfill('0') << std::setw(8) << *temp_e; + if (BN_num_bits(exponent) > 32) + { + LOG_ERROR("RSA public exponent does not fit in the Bootgen key format"); + } + file << std::uppercase << std::hex << std::setfill('0') << std::setw(8) + << static_cast(BN_get_word(exponent)); if (secret) { file << "\n\nD = "; -#if OPENSSL_VERSION_NUMBER > 0x10100000L - memcpy(temp.get(), RSA_get0_d(rsa)->d, keyLength); -#else - memcpy(temp.get(), rsa->d->d, keyLength); -#endif + if (BN_bn2lebinpad(privateExponent, temp.get(), keyLength) != keyLength) + { + LOG_ERROR("Unable to export RSA private exponent"); + } for (uint32_t index = keyLength; index != 0; index--) { file << std::uppercase << std::hex << std::setfill('0') << std::setw(2) << int(temp[index - 1]); diff --git a/zynqmp/src/verifyimage-zynqmp.cpp b/zynqmp/src/verifyimage-zynqmp.cpp index 361665a..f291590 100755 --- a/zynqmp/src/verifyimage-zynqmp.cpp +++ b/zynqmp/src/verifyimage-zynqmp.cpp @@ -27,26 +27,6 @@ -/*******************************************************************************/ -static void RearrangeEndianess(uint8_t *array, uint32_t size) -{ - uint32_t lastIndex = size - 1; - char tempInt = 0; - - // If array is NULL, return - if (!array) - { - return; - } - - for (uint32_t loop = 0; loop <= (lastIndex / 2); loop++) - { - tempInt = array[loop]; - array[loop] = array[lastIndex - loop]; - array[lastIndex - loop] = tempInt; - } -} - /*******************************************************************************/ void ZynqMpReadImage::VerifyAuthentication(bool verifyImageOption) { @@ -91,73 +71,37 @@ bool ZynqMpReadImage::VerifySignature(bool nist, uint8_t * data, size_t dataLeng LOG_TRACE("Hash from data"); LOG_DUMP_BYTES(shaHash.get(), hashLength); - rsa = RSA_new(); + RSA_Class rsaInstance; + rsaInstance.rsa = RSA_new(); + if (rsaInstance.rsa == NULL) + { + LOG_ERROR("Failed to allocate RSA verification key"); + } -#ifdef _MSC_VER - /* On Windows/MSVC, use BN_bin2bn to construct BIGNUMs from raw key data - instead of directly assigning to internal struct fields (d, dmax, top). Direct struct - access causes Montgomery reduction failure due to GCC/MSVC struct layout mismatch. */ + // ZynqMP authentication certificates serialize RSA components in big-endian + // order, unlike Bootgen's internal key buffers. BIGNUM* n = BN_bin2bn(acKey->N, RSA_4096_KEY_LENGTH, NULL); BIGNUM* e = BN_bin2bn(acKey->E, sizeof(uint32_t), NULL); if (n == NULL || e == NULL) { + BN_free(n); + BN_free(e); LOG_ERROR("Failed to create BIGNUMs from AC key data"); } - -#if OPENSSL_VERSION_NUMBER > 0x10100000L - RSA_set0_key(rsa, n, e, NULL); -#else - rsa->n = n; - rsa->e = e; -#endif -#else - BIGNUM* n = BN_new(); - n->d = (BN_ULONG*)acKey->N; - n->dmax = RSA_4096_KEY_LENGTH / sizeof(BN_ULONG); - n->top = RSA_4096_KEY_LENGTH / sizeof(BN_ULONG); - n->flags = 0; - n->neg = 0; - - BIGNUM* e = BN_new(); - e->d = (BN_ULONG*)acKey->E; - e->dmax = 1; - e->top = 1; - e->flags = 0; - e->neg = 0; - -#if OPENSSL_VERSION_NUMBER > 0x10100000L - BIGNUM *d = NULL; - RSA_set0_key(rsa, n, e, d); - RearrangeEndianess((uint8_t*)RSA_get0_e(rsa)->d, sizeof(uint32_t)); - RearrangeEndianess((uint8_t*)RSA_get0_n(rsa)->d, RSA_4096_KEY_LENGTH); -#else - rsa->n = n; - rsa->e = e; - RearrangeEndianess((uint8_t*)rsa->e->d, sizeof(uint32_t)); - RearrangeEndianess((uint8_t*)rsa->n->d, RSA_4096_KEY_LENGTH); -#endif -#endif + if (RSA_set0_key(rsaInstance.rsa, n, e, NULL) != 1) + { + BN_free(n); + BN_free(e); + LOG_ERROR("Failed to configure RSA verification key"); + } auto opensslHashPadded = std::make_unique(RSA_4096_KEY_LENGTH); - if (RSA_public_encrypt(RSA_4096_KEY_LENGTH, signature, (unsigned char*)opensslHashPadded.get(), rsa, RSA_NO_PADDING) < 0) + if (RSA_public_encrypt(RSA_4096_KEY_LENGTH, signature, (unsigned char*)opensslHashPadded.get(), rsaInstance.rsa, RSA_NO_PADDING) < 0) { LOG_ERROR("RSA_public_encrypt error"); } - /* RearrangeEndianess is only needed for the GCC path where BIGNUMs were - constructed via direct struct access. On MSVC, BN_bin2bn handles - endianness correctly so no post-encrypt fixup is needed. */ -#ifndef _MSC_VER -#if OPENSSL_VERSION_NUMBER > 0x10100000L - RearrangeEndianess((uint8_t*)RSA_get0_n(rsa)->d, RSA_4096_KEY_LENGTH); - RearrangeEndianess((uint8_t*)RSA_get0_e(rsa)->d, sizeof(uint32_t)); -#else - RearrangeEndianess((uint8_t*)rsa->n->d, RSA_4096_KEY_LENGTH); - RearrangeEndianess((uint8_t*)rsa->e->d, sizeof(uint32_t)); -#endif -#endif - auto opensslHash = std::make_unique(hashLength); memcpy(opensslHash.get(),opensslHashPadded.get() + RSA_4096_KEY_LENGTH - hashLength, hashLength); LOG_TRACE("Hash from signature"); @@ -404,4 +348,3 @@ void ZynqMpReadImage::VerifyPartitionSignature(void) fclose(binFile); Separator(); } - From 560f2664788211d5ee4f5b1795c2be842e761f6b Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Wed, 19 Aug 2026 19:52:35 -0500 Subject: [PATCH 26/45] Test authenticated Versal ECDSA images --- CMakeLists.txt | 7 ++++ tests/cmake/versal_auth_smoke.cmake | 58 +++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 tests/cmake/versal_auth_smoke.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index c8b2c19..26fa25b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -302,6 +302,13 @@ if(BOOTGEN_BUILD_TESTS) -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/zynqmp-auth-smoke -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/zynqmp_auth_smoke.cmake) + add_test( + NAME bootgen.versal-auth-smoke + COMMAND ${CMAKE_COMMAND} + -DBOOTGEN=$ + -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures + -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/versal-auth-smoke + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/versal_auth_smoke.cmake) add_test( NAME bootgen.versal-smoke COMMAND ${CMAKE_COMMAND} diff --git a/tests/cmake/versal_auth_smoke.cmake b/tests/cmake/versal_auth_smoke.cmake new file mode 100644 index 0000000..1fe7e52 --- /dev/null +++ b/tests/cmake/versal_auth_smoke.cmake @@ -0,0 +1,58 @@ +if(NOT DEFINED BOOTGEN OR NOT DEFINED FIXTURE_DIR OR NOT DEFINED OUTPUT_DIR) + message(FATAL_ERROR "BOOTGEN, FIXTURE_DIR, and OUTPUT_DIR are required") +endif() + +file(MAKE_DIRECTORY "${OUTPUT_DIR}") +set(TEST_FIXTURE_DIR "${OUTPUT_DIR}/fixture") +file(MAKE_DIRECTORY "${TEST_FIXTURE_DIR}") +file(CHMOD "${TEST_FIXTURE_DIR}" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE) +file(COPY "${FIXTURE_DIR}/zynq-smoke.elf" DESTINATION "${TEST_FIXTURE_DIR}") + +set(PRIMARY_KEY "${TEST_FIXTURE_DIR}/primary.pem") +set(SECONDARY_KEY "${TEST_FIXTURE_DIR}/secondary.pem") +function(cleanup_test_keys) + file(REMOVE "${PRIMARY_KEY}" "${SECONDARY_KEY}") +endfunction() + +file(WRITE "${TEST_FIXTURE_DIR}/keygen.bif" "all:\n{\n [pskfile] primary.pem\n [sskfile] secondary.pem\n}\n") +execute_process( + COMMAND "${BOOTGEN}" -arch versal -image "${TEST_FIXTURE_DIR}/keygen.bif" -generate_keys ecdsa + WORKING_DIRECTORY "${TEST_FIXTURE_DIR}" + RESULT_VARIABLE KEYGEN_RESULT + OUTPUT_VARIABLE KEYGEN_OUTPUT + ERROR_VARIABLE KEYGEN_ERROR) +if(NOT KEYGEN_RESULT EQUAL 0 OR NOT EXISTS "${PRIMARY_KEY}" OR NOT EXISTS "${SECONDARY_KEY}") + cleanup_test_keys() + message(FATAL_ERROR "Versal ECDSA key generation failed (${KEYGEN_RESULT}):\n${KEYGEN_OUTPUT}\n${KEYGEN_ERROR}") +endif() +file(CHMOD "${PRIMARY_KEY}" "${SECONDARY_KEY}" PERMISSIONS OWNER_READ OWNER_WRITE) + +file(WRITE "${TEST_FIXTURE_DIR}/authenticated.bif" "all:\n{\n pskfile = primary.pem\n sskfile = secondary.pem\n image\n {\n name = image1, id = 0x1c000001\n { type = bootloader, authentication = ecdsa, file = zynq-smoke.elf }\n }\n}\n") +set(BOOT_IMAGE "${OUTPUT_DIR}/BOOT.BIN") +execute_process( + COMMAND "${BOOTGEN}" -arch versal -image "${TEST_FIXTURE_DIR}/authenticated.bif" -o "${BOOT_IMAGE}" -w on + WORKING_DIRECTORY "${TEST_FIXTURE_DIR}" + RESULT_VARIABLE CREATE_RESULT + OUTPUT_VARIABLE CREATE_OUTPUT + ERROR_VARIABLE CREATE_ERROR) +if(NOT CREATE_RESULT EQUAL 0) + cleanup_test_keys() + message(FATAL_ERROR "Authenticated Versal smoke image creation failed (${CREATE_RESULT}):\n${CREATE_OUTPUT}\n${CREATE_ERROR}") +endif() + +execute_process( + COMMAND "${BOOTGEN}" -arch versal -verify "${BOOT_IMAGE}" + RESULT_VARIABLE VERIFY_RESULT + OUTPUT_VARIABLE VERIFY_OUTPUT + ERROR_VARIABLE VERIFY_ERROR) +if(NOT VERIFY_RESULT EQUAL 0) + cleanup_test_keys() + message(FATAL_ERROR "Authenticated Versal smoke image verification failed (${VERIFY_RESULT}):\n${VERIFY_OUTPUT}\n${VERIFY_ERROR}") +endif() +string(FIND "${VERIFY_OUTPUT}" "Authentication is verified" AUTHENTICATION_OFFSET) +if(AUTHENTICATION_OFFSET EQUAL -1) + cleanup_test_keys() + message(FATAL_ERROR "Authenticated Versal image did not report successful verification:\n${VERIFY_OUTPUT}") +endif() + +cleanup_test_keys() From be6238d246c15f195a3ce1a8fe65171b5f868ec3 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Thu, 20 Aug 2026 03:16:13 -0500 Subject: [PATCH 27/45] Use public OpenSSL APIs for Versal ECDSA keys --- versal/src/authentication-versal.cpp | 75 +++++++--------------- versal/src/authkeys-versal.cpp | 95 +++++++--------------------- 2 files changed, 46 insertions(+), 124 deletions(-) diff --git a/versal/src/authentication-versal.cpp b/versal/src/authentication-versal.cpp index 7c288a7..526ef83 100755 --- a/versal/src/authentication-versal.cpp +++ b/versal/src/authentication-versal.cpp @@ -1173,23 +1173,13 @@ void ECDSAAuthenticationAlgorithm::ECDSASignature(const uint8_t *base, EC_KEY *e if (signature != NULL) { -#if OPENSSL_VERSION_NUMBER > 0x10100000L const BIGNUM *sig_r = NULL, *sig_s = NULL; ECDSA_SIG_get0(signature, &sig_r, &sig_s); - - memcpy(result0, sig_r->d, EC_P384_KEY_LENGTH); - RearrangeEndianess(result0, EC_P384_KEY_LENGTH); - - memcpy(result0 + EC_P384_KEY_LENGTH, sig_s->d, EC_P384_KEY_LENGTH); - RearrangeEndianess(result0 + EC_P384_KEY_LENGTH, EC_P384_KEY_LENGTH); -#else - - memcpy(result0, signature->r->d, EC_P384_KEY_LENGTH); - RearrangeEndianess(result0, EC_P384_KEY_LENGTH); - - memcpy(result0 + EC_P384_KEY_LENGTH, signature->s->d, EC_P384_KEY_LENGTH); - RearrangeEndianess(result0 + EC_P384_KEY_LENGTH, EC_P384_KEY_LENGTH); -#endif + if (BN_bn2binpad(sig_r, result0, EC_P384_KEY_LENGTH) != EC_P384_KEY_LENGTH + || BN_bn2binpad(sig_s, result0 + EC_P384_KEY_LENGTH, EC_P384_KEY_LENGTH) != EC_P384_KEY_LENGTH) + { + LOG_ERROR("Failed to export ECDSA signature"); + } } EC_GROUP_free(ecgroup); @@ -1234,7 +1224,6 @@ void ECDSAP521AuthenticationAlgorithm::ECDSASignature(const uint8_t *base, EC_KE uint32_t signSzR; uint32_t signSzS; -#if OPENSSL_VERSION_NUMBER > 0x10100000L const BIGNUM *sig_r = NULL, *sig_s = NULL; ECDSA_SIG_get0(signature, &sig_r, &sig_s); @@ -1243,55 +1232,35 @@ void ECDSAP521AuthenticationAlgorithm::ECDSASignature(const uint8_t *base, EC_KE if (signSzR == EC_P521_KEY_LENGTH1) { - memcpy(x1.get() + 1, sig_r->d, signSzR); - RearrangeEndianess(x1.get() + 1, signSzR); - } - else - { - memcpy(x1.get(), sig_r->d, signSzR); - RearrangeEndianess(x1.get(), signSzR); - } - if (signSzS == EC_P521_KEY_LENGTH1) - { - memcpy(y1.get() + 1, sig_s->d, signSzS); - RearrangeEndianess(y1.get() + 1, signSzS); - } - else - { - memcpy(y1.get(), sig_s->d, signSzS); - RearrangeEndianess(y1.get(), signSzS); - } - - memcpy(result0, x1.get(), EC_P521_KEY_LENGTH2); - memcpy(result0 + EC_P521_KEY_LENGTH2, y1.get(), EC_P521_KEY_LENGTH2); -#else - signSzR = BN_num_bytes(signature->r); - signSzS = BN_num_bytes(signature->s); - - if (signSzR == EC_P521_KEY_LENGTH1) - { - memcpy(x1.get() + 1, signature->r->d, signSzR); - RearrangeEndianess(x1.get() + 1, signSzR); + if (BN_bn2binpad(sig_r, x1.get() + 1, signSzR) != signSzR) + { + LOG_ERROR("Failed to export ECDSAp521 signature R component"); + } } else { - memcpy(x1.get(), signature->r->d, signSzR); - RearrangeEndianess(x1.get(), signSzR); + if (BN_bn2binpad(sig_r, x1.get(), signSzR) != signSzR) + { + LOG_ERROR("Failed to export ECDSAp521 signature R component"); + } } if (signSzS == EC_P521_KEY_LENGTH1) { - memcpy(y1.get() + 1, signature->s->d, signSzS); - RearrangeEndianess(y1.get() + 1, signSzS); + if (BN_bn2binpad(sig_s, y1.get() + 1, signSzS) != signSzS) + { + LOG_ERROR("Failed to export ECDSAp521 signature S component"); + } } else { - memcpy(y1.get(), signature->s->d, signSzS); - RearrangeEndianess(y1.get(), signSzS); + if (BN_bn2binpad(sig_s, y1.get(), signSzS) != signSzS) + { + LOG_ERROR("Failed to export ECDSAp521 signature S component"); + } } memcpy(result0, x1.get(), EC_P521_KEY_LENGTH2); memcpy(result0 + EC_P521_KEY_LENGTH2, y1.get(), EC_P521_KEY_LENGTH2); -#endif } EC_GROUP_free(ecgroup); @@ -1742,4 +1711,4 @@ void VersalAuthenticationContext::SetKeyLength(Authentication::Type type) { //AuthenticationContext::authKeyLength = EC_P521_KEY_LENGTH; } -} \ No newline at end of file +} diff --git a/versal/src/authkeys-versal.cpp b/versal/src/authkeys-versal.cpp index 92af68b..d5ba31d 100755 --- a/versal/src/authkeys-versal.cpp +++ b/versal/src/authkeys-versal.cpp @@ -212,26 +212,6 @@ void KeyECDSAp521_versal::Import(const void * acKey, const std::string & name0) memset(D.get(), 0, keySize); } -/******************************************************************************/ -static void RearrangeEndianess(uint8_t *array, uint32_t size) -{ - uint32_t lastIndex = size - 1; - char tempInt = 0; - - // If array is NULL, return - if (!array) - { - return; - } - - for (uint32_t loop = 0; loop <= (lastIndex / 2); loop++) - { - tempInt = array[loop]; - array[loop] = array[lastIndex - loop]; - array[lastIndex - loop] = tempInt; - } -} - /******************************************************************************/ /* BN_num_bytes may return fewer bytes than the curve's key size when coordinates have leading zeros - use upper-bound check (>) not equality */ @@ -239,13 +219,7 @@ uint8_t VersalKey::ParseECDSAOpenSSLKey(const std::string& filename) { OpenSSL_add_all_algorithms(); BIGNUM *X = BN_new(); - X->flags = 0; - X->neg = 0; - X->top = 0; BIGNUM *Y = BN_new(); - Y->flags = 0; - Y->neg = 0; - Y->top = 0; uint32_t keySzRdX; uint32_t keySzRdY; EC_GROUP *ecgroup = NULL; // Track EC_GROUP to free it at the end @@ -272,10 +246,6 @@ uint8_t VersalKey::ParseECDSAOpenSSLKey(const std::string& filename) y = std::make_unique(keySize); memset(x.get(), 0, keySize); memset(y.get(), 0, keySize); - X->d = (BN_ULONG*)x.get(); - Y->d = (BN_ULONG*)y.get(); - X->dmax = keySize / sizeof(BN_ULONG); - Y->dmax = keySize / sizeof(BN_ULONG); ecgroup = EC_GROUP_new_by_curve_name(NID_secp384r1); const EC_POINT *pub = EC_KEY_get0_public_key(eckey); if (EC_POINT_get_affine_coordinates_GFp(ecgroup, pub, X, Y, NULL)) @@ -290,8 +260,11 @@ uint8_t VersalKey::ParseECDSAOpenSSLKey(const std::string& filename) { LOG_ERROR("Incorrect Key Size !!!\n\t Key Size is %d bits. Expected key size is %d bits", BN_num_bits(Y), keySize * 8); } - RearrangeEndianess(x.get(), keySize); - RearrangeEndianess(y.get(), keySize); + if (BN_bn2binpad(X, x.get(), keySize) != keySize + || BN_bn2binpad(Y, y.get(), keySize) != keySize) + { + LOG_ERROR("Failed to export ECDSAp384 public key"); + } } else { @@ -319,11 +292,11 @@ uint8_t VersalKey::ParseECDSAOpenSSLKey(const std::string& filename) y = std::make_unique(keySizeY); memset(x.get(), 0, keySizeX); memset(y.get(), 0, keySizeY); - memcpy(x.get(), X->d, keySizeX); - memcpy(y.get(), Y->d, keySizeY); - - RearrangeEndianess(x.get(), keySizeX); - RearrangeEndianess(y.get(), keySizeY); + if (BN_bn2binpad(X, x.get(), keySizeX) != keySizeX + || BN_bn2binpad(Y, y.get(), keySizeY) != keySizeY) + { + LOG_ERROR("Failed to export ECDSAp521 public key"); + } } else { @@ -347,10 +320,6 @@ uint8_t VersalKey::ParseECDSAOpenSSLKey(const std::string& filename) y = std::make_unique(keySize); memset(x.get(), 0, keySize); memset(y.get(), 0, keySize); - X->d = (BN_ULONG*)x.get(); - Y->d = (BN_ULONG*)y.get(); - X->dmax = keySize / sizeof(BN_ULONG); - Y->dmax = keySize / sizeof(BN_ULONG); ecgroup = EC_GROUP_new_by_curve_name(NID_secp384r1); const EC_POINT *pub = EC_KEY_get0_public_key(eckey); if (EC_POINT_get_affine_coordinates_GFp(ecgroup, pub, X, Y, NULL)) @@ -365,8 +334,11 @@ uint8_t VersalKey::ParseECDSAOpenSSLKey(const std::string& filename) { LOG_ERROR("Incorrect Key Size !!!\n\t Key Size is %d bits. Expected key size is %d bits", BN_num_bits(Y), keySize * 8); } - RearrangeEndianess(x.get(), keySize); - RearrangeEndianess(y.get(), keySize); + if (BN_bn2binpad(X, x.get(), keySize) != keySize + || BN_bn2binpad(Y, y.get(), keySize) != keySize) + { + LOG_ERROR("Failed to export ECDSAp384 public key"); + } } else { @@ -394,11 +366,11 @@ uint8_t VersalKey::ParseECDSAOpenSSLKey(const std::string& filename) y = std::make_unique(keySizeY); memset(x.get(), 0, keySizeX); memset(y.get(), 0, keySizeY); - memcpy(x.get(), X->d, keySizeX); - memcpy(y.get(), Y->d, keySizeY); - - RearrangeEndianess(x.get(), keySizeX); - RearrangeEndianess(y.get(), keySizeY); + if (BN_bn2binpad(X, x.get(), keySizeX) != keySizeX + || BN_bn2binpad(Y, y.get(), keySizeY) != keySizeY) + { + LOG_ERROR("Failed to export ECDSAp521 public key"); + } } else { @@ -409,28 +381,9 @@ uint8_t VersalKey::ParseECDSAOpenSSLKey(const std::string& filename) } fclose(file); - // Free BIGNUM structures - // For P384: X->d and Y->d point to our memory (x.get(), y.get()), so clear before freeing - // For P521: X->d and Y->d still have OpenSSL memory, so BN_free will free them normally - if (X) { - if (keySize == EC_P384_KEY_LENGTH) { - X->d = NULL; // Prevent OpenSSL from freeing our memory (x smart pointer owns it) - X->dmax = 0; - } - BN_free(X); - } - if (Y) { - if (keySize == EC_P384_KEY_LENGTH) { - Y->d = NULL; // Prevent OpenSSL from freeing our memory (y smart pointer owns it) - Y->dmax = 0; - } - BN_free(Y); - } - - // Free EC_GROUP object - if (ecgroup) { - EC_GROUP_free(ecgroup); - } + BN_free(X); + BN_free(Y); + EC_GROUP_free(ecgroup); return 0; } @@ -654,4 +607,4 @@ void VersalKey::Parse(const std::string& filename, bool isSecret0) } } } -} \ No newline at end of file +} From 3d5fae3aa64471c5474a5d192f27fea0896aed41 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Thu, 20 Aug 2026 22:36:29 -0500 Subject: [PATCH 28/45] Preserve Versal key ownership during copies --- versal/src/authkeys-versal.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/versal/src/authkeys-versal.cpp b/versal/src/authkeys-versal.cpp index d5ba31d..fef2223 100755 --- a/versal/src/authkeys-versal.cpp +++ b/versal/src/authkeys-versal.cpp @@ -15,9 +15,6 @@ * limitations under the License. ******************************************************************************/ -#pragma GCC diagnostic pop -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wclass-memaccess" /* ------------------------------------------------------------------------------- *********************************************** H E A D E R F I L E S *** @@ -73,10 +70,6 @@ VersalKey::VersalKey(const Key& otherKey) eckey = EC_KEY_new_by_curve_name(NID_secp521r1); } - // Note: memcpy on object with unique_ptr members is a design limitation - // This shallow copy is intentional for the legacy Key class design - - memcpy(this, &otherKey, sizeof(Key)); } /******************************************************************************/ From 79e827db10eef0245447b70a27c411def8f30075 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Thu, 20 Aug 2026 23:24:15 -0500 Subject: [PATCH 29/45] Preserve sibling-family key ownership during copies --- spartanup/src/authkeys-spartanup.cpp | 8 -------- versal_2ve_2vm/src/authkeys-versal_2ve_2vm.cpp | 7 ------- 2 files changed, 15 deletions(-) diff --git a/spartanup/src/authkeys-spartanup.cpp b/spartanup/src/authkeys-spartanup.cpp index 8086df2..6a3e4a2 100755 --- a/spartanup/src/authkeys-spartanup.cpp +++ b/spartanup/src/authkeys-spartanup.cpp @@ -21,9 +21,6 @@ *********************************************** H E A D E R F I L E S *** ------------------------------------------------------------------------------- */ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wclass-memaccess" -#pragma GCC diagnostic pop #include #include @@ -76,11 +73,6 @@ SpartanupKey::SpartanupKey(const Key& otherKey) eckey = EC_KEY_new_by_curve_name(NID_secp521r1); } - // Note: memcpy on object with unique_ptr members is a design limitation - // This shallow copy is intentional for the legacy Key class design - - memcpy(this, &otherKey, sizeof(Key)); - } /******************************************************************************/ diff --git a/versal_2ve_2vm/src/authkeys-versal_2ve_2vm.cpp b/versal_2ve_2vm/src/authkeys-versal_2ve_2vm.cpp index 489b7c8..b5a2087 100755 --- a/versal_2ve_2vm/src/authkeys-versal_2ve_2vm.cpp +++ b/versal_2ve_2vm/src/authkeys-versal_2ve_2vm.cpp @@ -15,9 +15,6 @@ * limitations under the License. ******************************************************************************/ - #pragma GCC diagnostic pop - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wclass-memaccess" /* ------------------------------------------------------------------------------- *********************************************** H E A D E R F I L E S *** @@ -75,10 +72,6 @@ Versal_2ve_2vmKey::Versal_2ve_2vmKey(const Key& otherKey) eckey = EC_KEY_new_by_curve_name(NID_secp521r1); } - // Note: memcpy on object with unique_ptr members is a design limitation - // This shallow copy is intentional for the legacy Key class design - - memcpy(this, &otherKey, sizeof(Key)); } /******************************************************************************/ From 71fd0a86989db23f8e8e9900c7fec9e75a6956f1 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Fri, 21 Aug 2026 01:06:38 -0500 Subject: [PATCH 30/45] Test authenticated Spartan UltraScale+ image builds --- CMakeLists.txt | 7 ++++ tests/cmake/spartanup_auth_smoke.cmake | 48 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 tests/cmake/spartanup_auth_smoke.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 26fa25b..f17a29e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -347,6 +347,13 @@ if(BOOTGEN_BUILD_TESTS) -DARCH=spartanup -DEXPECTED_READ_OUTPUT=plm_length -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/versal_smoke.cmake) + add_test( + NAME bootgen.spartanup-auth-smoke + COMMAND ${CMAKE_COMMAND} + -DBOOTGEN=$ + -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures + -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/spartanup-auth-smoke + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/spartanup_auth_smoke.cmake) add_test( NAME bootgen.invalid-bif COMMAND ${CMAKE_COMMAND} diff --git a/tests/cmake/spartanup_auth_smoke.cmake b/tests/cmake/spartanup_auth_smoke.cmake new file mode 100644 index 0000000..bb7070f --- /dev/null +++ b/tests/cmake/spartanup_auth_smoke.cmake @@ -0,0 +1,48 @@ +if(NOT DEFINED BOOTGEN OR NOT DEFINED FIXTURE_DIR OR NOT DEFINED OUTPUT_DIR) + message(FATAL_ERROR "BOOTGEN, FIXTURE_DIR, and OUTPUT_DIR are required") +endif() + +file(MAKE_DIRECTORY "${OUTPUT_DIR}") +set(TEST_FIXTURE_DIR "${OUTPUT_DIR}/fixture") +file(MAKE_DIRECTORY "${TEST_FIXTURE_DIR}") +file(CHMOD "${TEST_FIXTURE_DIR}" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE) +file(COPY "${FIXTURE_DIR}/zynq-smoke.elf" DESTINATION "${TEST_FIXTURE_DIR}") + +set(PRIMARY_KEY "${TEST_FIXTURE_DIR}/primary.pem") +set(SECONDARY_KEY "${TEST_FIXTURE_DIR}/secondary.pem") +function(cleanup_test_keys) + file(REMOVE "${PRIMARY_KEY}" "${SECONDARY_KEY}") +endfunction() + +file(WRITE "${TEST_FIXTURE_DIR}/keygen.bif" "all:\n{\n [pskfile] primary.pem\n [sskfile] secondary.pem\n}\n") +execute_process( + COMMAND "${BOOTGEN}" -arch spartanup -image "${TEST_FIXTURE_DIR}/keygen.bif" -generate_keys ecdsa + WORKING_DIRECTORY "${TEST_FIXTURE_DIR}" + RESULT_VARIABLE KEYGEN_RESULT + OUTPUT_VARIABLE KEYGEN_OUTPUT + ERROR_VARIABLE KEYGEN_ERROR) +if(NOT KEYGEN_RESULT EQUAL 0 OR NOT EXISTS "${PRIMARY_KEY}" OR NOT EXISTS "${SECONDARY_KEY}") + cleanup_test_keys() + message(FATAL_ERROR "Spartan UltraScale+ ECDSA key generation failed (${KEYGEN_RESULT}):\n${KEYGEN_OUTPUT}\n${KEYGEN_ERROR}") +endif() +file(CHMOD "${PRIMARY_KEY}" "${SECONDARY_KEY}" PERMISSIONS OWNER_READ OWNER_WRITE) + +file(WRITE "${TEST_FIXTURE_DIR}/authenticated.bif" "all:\n{\n pskfile = primary.pem\n sskfile = secondary.pem\n image\n {\n name = image1, id = 0x1c000001\n { type = bootloader, authentication = ecdsa, file = zynq-smoke.elf }\n }\n}\n") +set(BOOT_IMAGE "${OUTPUT_DIR}/BOOT.BIN") +execute_process( + COMMAND "${BOOTGEN}" -arch spartanup -image "${TEST_FIXTURE_DIR}/authenticated.bif" -o "${BOOT_IMAGE}" -w on + WORKING_DIRECTORY "${TEST_FIXTURE_DIR}" + RESULT_VARIABLE CREATE_RESULT + OUTPUT_VARIABLE CREATE_OUTPUT + ERROR_VARIABLE CREATE_ERROR) +if(NOT CREATE_RESULT EQUAL 0) + cleanup_test_keys() + message(FATAL_ERROR "Authenticated Spartan UltraScale+ image creation failed (${CREATE_RESULT}):\n${CREATE_OUTPUT}\n${CREATE_ERROR}") +endif() +file(SIZE "${BOOT_IMAGE}" BOOT_IMAGE_SIZE) +if(BOOT_IMAGE_SIZE LESS 1024) + cleanup_test_keys() + message(FATAL_ERROR "Authenticated Spartan UltraScale+ image is unexpectedly small: ${BOOT_IMAGE_SIZE} bytes") +endif() + +cleanup_test_keys() From fc74f5e35b274a50843340a9cd5a89d417316c86 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Fri, 21 Aug 2026 07:22:00 -0500 Subject: [PATCH 31/45] Use public OpenSSL APIs for Spartan ECDSA keys --- spartanup/src/authkeys-spartanup.cpp | 99 +++++++--------------------- 1 file changed, 23 insertions(+), 76 deletions(-) diff --git a/spartanup/src/authkeys-spartanup.cpp b/spartanup/src/authkeys-spartanup.cpp index 6a3e4a2..f52c7f7 100755 --- a/spartanup/src/authkeys-spartanup.cpp +++ b/spartanup/src/authkeys-spartanup.cpp @@ -239,26 +239,6 @@ void KeyLMS_spartanup::Import(const void * acKey, const std::string & name0) { } -/******************************************************************************/ -static void RearrangeEndianess(uint8_t *array, uint32_t size) -{ - uint32_t lastIndex = size - 1; - char tempInt = 0; - - // If array is NULL, return - if (!array) - { - return; - } - - for (uint32_t loop = 0; loop <= (lastIndex / 2); loop++) - { - tempInt = array[loop]; - array[loop] = array[lastIndex - loop]; - array[lastIndex - loop] = tempInt; - } -} - /******************************************************************************/ /* BN_num_bytes may return fewer bytes than the curve's key size when coordinates have leading zeros - use upper-bound check (>) not equality */ @@ -266,9 +246,9 @@ uint8_t SpartanupKey::ParseECDSAOpenSSLKey(const std::string& filename) { OpenSSL_add_all_algorithms(); - // Use smart pointers for exception-safe cleanup - auto X_deleter = [](BIGNUM* bn) { if (bn) { bn->d = NULL; bn->dmax = 0; BN_free(bn); } }; - auto Y_deleter = [](BIGNUM* bn) { if (bn) { bn->d = NULL; bn->dmax = 0; BN_free(bn); } }; + // Use smart pointers for exception-safe cleanup. + auto X_deleter = [](BIGNUM* bn) { BN_free(bn); }; + auto Y_deleter = [](BIGNUM* bn) { BN_free(bn); }; auto group_deleter = [](EC_GROUP* g) { if (g) EC_GROUP_free(g); }; std::unique_ptr X_ptr(BN_new(), X_deleter); @@ -278,13 +258,6 @@ uint8_t SpartanupKey::ParseECDSAOpenSSLKey(const std::string& filename) BIGNUM* X = X_ptr.get(); BIGNUM* Y = Y_ptr.get(); - X->flags = 0; - X->neg = 0; - X->top = 0; - Y->flags = 0; - Y->neg = 0; - Y->top = 0; - uint32_t keySzRdX; uint32_t keySzRdY; EC_GROUP *ecgroup = NULL; // Will be managed by ecgroup_ptr @@ -312,20 +285,6 @@ uint8_t SpartanupKey::ParseECDSAOpenSSLKey(const std::string& filename) memset(x.get(), 0, keySize); memset(y.get(), 0, keySize); - // Free original d buffers allocated by BN_new() before replacing - if (X->d) { - OPENSSL_free(X->d); - X->d = NULL; - } - if (Y->d) { - OPENSSL_free(Y->d); - Y->d = NULL; - } - - X->d = (BN_ULONG*)x.get(); - Y->d = (BN_ULONG*)y.get(); - X->dmax = keySize / sizeof(BN_ULONG); - Y->dmax = keySize / sizeof(BN_ULONG); ecgroup = EC_GROUP_new_by_curve_name(NID_secp384r1); ecgroup_ptr.reset(ecgroup); // Transfer ownership to smart pointer const EC_POINT *pub = EC_KEY_get0_public_key(eckey); @@ -341,8 +300,11 @@ uint8_t SpartanupKey::ParseECDSAOpenSSLKey(const std::string& filename) { LOG_ERROR("Incorrect Key Size !!!\n\t Key Size is %d bits. Expected key size is %d bits", BN_num_bits(Y), keySize * 8); } - RearrangeEndianess(x.get(), keySize); - RearrangeEndianess(y.get(), keySize); + if (BN_bn2binpad(X, x.get(), keySize) != keySize + || BN_bn2binpad(Y, y.get(), keySize) != keySize) + { + LOG_ERROR("Failed to export ECDSAp384 public key"); + } } else { @@ -371,11 +333,11 @@ uint8_t SpartanupKey::ParseECDSAOpenSSLKey(const std::string& filename) y = std::make_unique(keySizeY); memset(x.get(), 0, keySizeX); memset(y.get(), 0, keySizeY); - memcpy(x.get(), X->d, keySizeX); - memcpy(y.get(), Y->d, keySizeY); - - RearrangeEndianess(x.get(), keySizeX); - RearrangeEndianess(y.get(), keySizeY); + if (BN_bn2binpad(X, x.get(), keySizeX) != keySizeX + || BN_bn2binpad(Y, y.get(), keySizeY) != keySizeY) + { + LOG_ERROR("Failed to export ECDSAp521 public key"); + } } else { @@ -400,20 +362,6 @@ uint8_t SpartanupKey::ParseECDSAOpenSSLKey(const std::string& filename) memset(x.get(), 0, keySize); memset(y.get(), 0, keySize); - // Free original d buffers allocated by BN_new() before replacing - if (X->d) { - OPENSSL_free(X->d); - X->d = NULL; - } - if (Y->d) { - OPENSSL_free(Y->d); - Y->d = NULL; - } - - X->d = (BN_ULONG*)x.get(); - Y->d = (BN_ULONG*)y.get(); - X->dmax = keySize / sizeof(BN_ULONG); - Y->dmax = keySize / sizeof(BN_ULONG); ecgroup = EC_GROUP_new_by_curve_name(NID_secp384r1); ecgroup_ptr.reset(ecgroup); // Transfer ownership to smart pointer const EC_POINT *pub = EC_KEY_get0_public_key(eckey); @@ -429,8 +377,11 @@ uint8_t SpartanupKey::ParseECDSAOpenSSLKey(const std::string& filename) { LOG_ERROR("Incorrect Key Size !!!\n\t Key Size is %d bits. Expected key size is %d bits", BN_num_bits(Y), keySize * 8); } - RearrangeEndianess(x.get(), keySize); - RearrangeEndianess(y.get(), keySize); + if (BN_bn2binpad(X, x.get(), keySize) != keySize + || BN_bn2binpad(Y, y.get(), keySize) != keySize) + { + LOG_ERROR("Failed to export ECDSAp384 public key"); + } } else { @@ -459,11 +410,11 @@ uint8_t SpartanupKey::ParseECDSAOpenSSLKey(const std::string& filename) y = std::make_unique(keySizeY); memset(x.get(), 0, keySizeX); memset(y.get(), 0, keySizeY); - memcpy(x.get(), X->d, keySizeX); - memcpy(y.get(), Y->d, keySizeY); - - RearrangeEndianess(x.get(), keySizeX); - RearrangeEndianess(y.get(), keySizeY); + if (BN_bn2binpad(X, x.get(), keySizeX) != keySizeX + || BN_bn2binpad(Y, y.get(), keySizeY) != keySizeY) + { + LOG_ERROR("Failed to export ECDSAp521 public key"); + } } else { @@ -474,10 +425,6 @@ uint8_t SpartanupKey::ParseECDSAOpenSSLKey(const std::string& filename) } fclose(file); - // Smart pointers X_ptr, Y_ptr, and ecgroup_ptr will automatically clean up - // Their custom deleters set d=NULL and dmax=0 before calling BN_free to prevent - // freeing our memory buffers (x.get(), y.get()) which are managed by unique_ptr - return 0; } From cfd659f10267a2ef6df991f57a59e6bfd1a51201 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Fri, 21 Aug 2026 18:37:41 -0500 Subject: [PATCH 32/45] Remove 2VE/2VM internal debug output --- versal_2ve_2vm/src/bootheader-versal_2ve_2vm.cpp | 12 ------------ .../src/partitionheadertable-versal_2ve_2vm.cpp | 10 ---------- 2 files changed, 22 deletions(-) diff --git a/versal_2ve_2vm/src/bootheader-versal_2ve_2vm.cpp b/versal_2ve_2vm/src/bootheader-versal_2ve_2vm.cpp index b327a59..aa242c0 100644 --- a/versal_2ve_2vm/src/bootheader-versal_2ve_2vm.cpp +++ b/versal_2ve_2vm/src/bootheader-versal_2ve_2vm.cpp @@ -102,18 +102,6 @@ void Versal_2ve_2vmBootHeader::RefreshStructPointers() { bHTable = (Versal_2ve_2vmBootHeaderStructure*)section->Data.get(); smapTable = (Versal_2ve_2vmSmapWidthTable*)bHTable->smapWords; - fprintf(stderr, "[BH-REFRESH] Updated bHTable=%p, smapTable=%p, section->Data=%p, section->Length=%lu\n", - (void*)bHTable, (void*)smapTable, (void*)section->Data.get(), section->Length); - fprintf(stderr, "[BH-REFRESH] smapWords[3] value=0x%08x, address=%p, offset from Data=0x%lx\n", - bHTable->smapWords[3], (void*)&bHTable->smapWords[3], - (uint8_t*)&bHTable->smapWords[3] - section->Data.get()); - if (section->Length > 0x113C) { - fprintf(stderr, "[BH-REFRESH] Bytes at 0x113C: "); - for (size_t i = 0x113C; i < 0x1144 && i < section->Length; i++) { - fprintf(stderr, "%02x", section->Data.get()[i]); - } - fprintf(stderr, "\n"); - } } } diff --git a/versal_2ve_2vm/src/partitionheadertable-versal_2ve_2vm.cpp b/versal_2ve_2vm/src/partitionheadertable-versal_2ve_2vm.cpp index 2bca0dc..d843a4e 100644 --- a/versal_2ve_2vm/src/partitionheadertable-versal_2ve_2vm.cpp +++ b/versal_2ve_2vm/src/partitionheadertable-versal_2ve_2vm.cpp @@ -1657,23 +1657,13 @@ void Versal_2ve_2vmPartitionHeaderTable::Link(BootImage & bi) bi.hash->CalculateVersalHash(true, headers->Data.get(), size, sha_hash.get()); - - fprintf(stderr, "[META-HASH-RESULT] Meta header SHA3 hash (THIS IS THE CRITICAL VALUE):\n"); - for (size_t i = 0; i < bi.hash->GetHashLength(); i++) { - fprintf(stderr, "%02x", sha_hash.get()[i]); - if ((i+1) % 16 == 0) fprintf(stderr, "\n"); - } - fprintf(stderr, "\n"); // headers is now managed by sections container /* Copy Meta Header Hash into Hash Block 1 */ - fprintf(stderr, "[LINK-IHT-HB] imageHeaderTable->hashBlockSection=%p\n", (void*)bi.imageHeaderTable->hashBlockSection); if (bi.imageHeaderTable->hashBlockSection) { - fprintf(stderr, "[LINK-IHT-HB] Zeroing and copying meta header hash, sectionLength=%u\n", bi.imageHeaderTable->hashBlockSectionLength); memset(bi.imageHeaderTable->hashBlockSection->Data.get(), 0, bi.imageHeaderTable->hashBlockSectionLength); memcpy(bi.imageHeaderTable->hashBlockSection->Data.get() + HASH_BLOCK_INDEX_BYTES, sha_hash.get(), bi.hash->GetHashLength()); - fprintf(stderr, "[LINK-IHT-HB] Meta header hash copied to offset %u\n", HASH_BLOCK_INDEX_BYTES); } #ifdef DEBUG LOG_TRACE("Meta Header Length %d", bi.imageHeaderTable->metaHeaderLength); From 2ecc58f58837c5b50c976aa077c44365304f45f9 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Fri, 21 Aug 2026 21:07:50 -0500 Subject: [PATCH 33/45] Test authenticated Versal 2VE/2VM image builds --- CMakeLists.txt | 7 +++ tests/cmake/versal_2ve_2vm_auth_smoke.cmake | 48 +++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 tests/cmake/versal_2ve_2vm_auth_smoke.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index f17a29e..e750148 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -333,6 +333,13 @@ if(BOOTGEN_BUILD_TESTS) -DBOOTGEN=$ -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/versal-2ve-2vm-lms-keygen -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/lms_keygen_smoke.cmake) + add_test( + NAME bootgen.versal-2ve-2vm-auth-smoke + COMMAND ${CMAKE_COMMAND} + -DBOOTGEN=$ + -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures + -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/versal-2ve-2vm-auth-smoke + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/versal_2ve_2vm_auth_smoke.cmake) add_test(NAME bootgen.lms-sign-smoke COMMAND lms-sign-smoke) set_tests_properties(bootgen.lms-sign-smoke PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/tests/lms-sign-smoke") diff --git a/tests/cmake/versal_2ve_2vm_auth_smoke.cmake b/tests/cmake/versal_2ve_2vm_auth_smoke.cmake new file mode 100644 index 0000000..d8dfe57 --- /dev/null +++ b/tests/cmake/versal_2ve_2vm_auth_smoke.cmake @@ -0,0 +1,48 @@ +if(NOT DEFINED BOOTGEN OR NOT DEFINED FIXTURE_DIR OR NOT DEFINED OUTPUT_DIR) + message(FATAL_ERROR "BOOTGEN, FIXTURE_DIR, and OUTPUT_DIR are required") +endif() + +file(MAKE_DIRECTORY "${OUTPUT_DIR}") +set(TEST_FIXTURE_DIR "${OUTPUT_DIR}/fixture") +file(MAKE_DIRECTORY "${TEST_FIXTURE_DIR}") +file(CHMOD "${TEST_FIXTURE_DIR}" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE) +file(COPY "${FIXTURE_DIR}/zynq-smoke.elf" DESTINATION "${TEST_FIXTURE_DIR}") + +set(PRIMARY_KEY "${TEST_FIXTURE_DIR}/primary.pem") +set(SECONDARY_KEY "${TEST_FIXTURE_DIR}/secondary.pem") +function(cleanup_test_keys) + file(REMOVE "${PRIMARY_KEY}" "${SECONDARY_KEY}") +endfunction() + +file(WRITE "${TEST_FIXTURE_DIR}/keygen.bif" "all:\n{\n [pskfile] primary.pem\n [sskfile] secondary.pem\n}\n") +execute_process( + COMMAND "${BOOTGEN}" -arch versal_2ve_2vm -image "${TEST_FIXTURE_DIR}/keygen.bif" -generate_keys ecdsa + WORKING_DIRECTORY "${TEST_FIXTURE_DIR}" + RESULT_VARIABLE KEYGEN_RESULT + OUTPUT_VARIABLE KEYGEN_OUTPUT + ERROR_VARIABLE KEYGEN_ERROR) +if(NOT KEYGEN_RESULT EQUAL 0 OR NOT EXISTS "${PRIMARY_KEY}" OR NOT EXISTS "${SECONDARY_KEY}") + cleanup_test_keys() + message(FATAL_ERROR "Versal 2VE/2VM ECDSA key generation failed (${KEYGEN_RESULT}):\n${KEYGEN_OUTPUT}\n${KEYGEN_ERROR}") +endif() +file(CHMOD "${PRIMARY_KEY}" "${SECONDARY_KEY}" PERMISSIONS OWNER_READ OWNER_WRITE) + +file(WRITE "${TEST_FIXTURE_DIR}/authenticated.bif" "all:\n{\n pskfile = primary.pem\n sskfile = secondary.pem\n image\n {\n name = image1, id = 0x1c000001\n { type = bootloader, authentication = ecdsa, file = zynq-smoke.elf }\n }\n}\n") +set(BOOT_IMAGE "${OUTPUT_DIR}/BOOT.BIN") +execute_process( + COMMAND "${BOOTGEN}" -arch versal_2ve_2vm -image "${TEST_FIXTURE_DIR}/authenticated.bif" -o "${BOOT_IMAGE}" -w on + WORKING_DIRECTORY "${TEST_FIXTURE_DIR}" + RESULT_VARIABLE CREATE_RESULT + OUTPUT_VARIABLE CREATE_OUTPUT + ERROR_VARIABLE CREATE_ERROR) +if(NOT CREATE_RESULT EQUAL 0) + cleanup_test_keys() + message(FATAL_ERROR "Authenticated Versal 2VE/2VM image creation failed (${CREATE_RESULT}):\n${CREATE_OUTPUT}\n${CREATE_ERROR}") +endif() +file(SIZE "${BOOT_IMAGE}" BOOT_IMAGE_SIZE) +if(BOOT_IMAGE_SIZE LESS 2048) + cleanup_test_keys() + message(FATAL_ERROR "Authenticated Versal 2VE/2VM image is unexpectedly small: ${BOOT_IMAGE_SIZE} bytes") +endif() + +cleanup_test_keys() From 64c324aaaf880f993f71ce791b81f22cae7e0440 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Sat, 22 Aug 2026 03:23:13 -0500 Subject: [PATCH 34/45] Use public OpenSSL APIs for 2VE/2VM ECDSA keys --- .../src/authentication-versal_2ve_2vm.cpp | 73 +++++---------- .../src/authkeys-versal_2ve_2vm.cpp | 93 +++++-------------- 2 files changed, 44 insertions(+), 122 deletions(-) diff --git a/versal_2ve_2vm/src/authentication-versal_2ve_2vm.cpp b/versal_2ve_2vm/src/authentication-versal_2ve_2vm.cpp index 14283d0..a031aad 100755 --- a/versal_2ve_2vm/src/authentication-versal_2ve_2vm.cpp +++ b/versal_2ve_2vm/src/authentication-versal_2ve_2vm.cpp @@ -1958,23 +1958,13 @@ void ECDSAHBAuthenticationAlgorithm::ECDSASignature(const uint8_t *base, EC_KEY if (signature != NULL) { -#if OPENSSL_VERSION_NUMBER > 0x10100000L const BIGNUM *sig_r = NULL, *sig_s = NULL; ECDSA_SIG_get0(signature, &sig_r, &sig_s); - - memcpy(result0, sig_r->d, EC_P384_KEY_LENGTH); - RearrangeEndianess(result0, EC_P384_KEY_LENGTH); - - memcpy(result0 + EC_P384_KEY_LENGTH, sig_s->d, EC_P384_KEY_LENGTH); - RearrangeEndianess(result0 + EC_P384_KEY_LENGTH, EC_P384_KEY_LENGTH); -#else - - memcpy(result0, signature->r->d, EC_P384_KEY_LENGTH); - RearrangeEndianess(result0, EC_P384_KEY_LENGTH); - - memcpy(result0 + EC_P384_KEY_LENGTH, signature->s->d, EC_P384_KEY_LENGTH); - RearrangeEndianess(result0 + EC_P384_KEY_LENGTH, EC_P384_KEY_LENGTH); -#endif + if (BN_bn2binpad(sig_r, result0, EC_P384_KEY_LENGTH) != EC_P384_KEY_LENGTH + || BN_bn2binpad(sig_s, result0 + EC_P384_KEY_LENGTH, EC_P384_KEY_LENGTH) != EC_P384_KEY_LENGTH) + { + LOG_ERROR("Failed to export ECDSA signature"); + } } EC_GROUP_free(ecgroup); @@ -2017,7 +2007,6 @@ void ECDSAP521HBAuthenticationAlgorithm::ECDSASignature(const uint8_t *base, EC_ uint32_t signSzR; uint32_t signSzS; -#if OPENSSL_VERSION_NUMBER > 0x10100000L const BIGNUM *sig_r = NULL, *sig_s = NULL; ECDSA_SIG_get0(signature, &sig_r, &sig_s); @@ -2026,55 +2015,35 @@ void ECDSAP521HBAuthenticationAlgorithm::ECDSASignature(const uint8_t *base, EC_ if (signSzR == EC_P521_KEY_LENGTH1) { - memcpy(x1.get() + 1, sig_r->d, signSzR); - RearrangeEndianess(x1.get() + 1, signSzR); - } - else - { - memcpy(x1.get(), sig_r->d, signSzR); - RearrangeEndianess(x1.get(), signSzR); - } - if (signSzS == EC_P521_KEY_LENGTH1) - { - memcpy(y1.get() + 1, sig_s->d, signSzS); - RearrangeEndianess(y1.get() + 1, signSzS); - } - else - { - memcpy(y1.get(), sig_s->d, signSzS); - RearrangeEndianess(y1.get(), signSzS); - } - - memcpy(result0, x1.get(), EC_P521_KEY_LENGTH2); - memcpy(result0 + EC_P521_KEY_LENGTH2, y1.get(), EC_P521_KEY_LENGTH2); -#else - signSzR = BN_num_bytes(signature->r); - signSzS = BN_num_bytes(signature->s); - - if (signSzR == EC_P521_KEY_LENGTH1) - { - memcpy(x1.get() + 1, signature->r->d, signSzR); - RearrangeEndianess(x1.get() + 1, signSzR); + if (BN_bn2binpad(sig_r, x1.get() + 1, signSzR) != signSzR) + { + LOG_ERROR("Failed to export ECDSAp521 signature R component"); + } } else { - memcpy(x1.get(), signature->r->d, signSzR); - RearrangeEndianess(x1.get(), signSzR); + if (BN_bn2binpad(sig_r, x1.get(), signSzR) != signSzR) + { + LOG_ERROR("Failed to export ECDSAp521 signature R component"); + } } if (signSzS == EC_P521_KEY_LENGTH1) { - memcpy(y1.get() + 1, signature->s->d, signSzS); - RearrangeEndianess(y1.get() + 1, signSzS); + if (BN_bn2binpad(sig_s, y1.get() + 1, signSzS) != signSzS) + { + LOG_ERROR("Failed to export ECDSAp521 signature S component"); + } } else { - memcpy(y1.get(), signature->s->d, signSzS); - RearrangeEndianess(y1.get(), signSzS); + if (BN_bn2binpad(sig_s, y1.get(), signSzS) != signSzS) + { + LOG_ERROR("Failed to export ECDSAp521 signature S component"); + } } memcpy(result0, x1.get(), EC_P521_KEY_LENGTH2); memcpy(result0 + EC_P521_KEY_LENGTH2, y1.get(), EC_P521_KEY_LENGTH2); -#endif if (x1 != NULL) { // unique_ptr handles deletion diff --git a/versal_2ve_2vm/src/authkeys-versal_2ve_2vm.cpp b/versal_2ve_2vm/src/authkeys-versal_2ve_2vm.cpp index b5a2087..4effe0f 100755 --- a/versal_2ve_2vm/src/authkeys-versal_2ve_2vm.cpp +++ b/versal_2ve_2vm/src/authkeys-versal_2ve_2vm.cpp @@ -239,26 +239,6 @@ void KeyLMS_versal_2ve_2vm::Import(const void * acKey, const std::string & name0 { } -/******************************************************************************/ -static void RearrangeEndianess(uint8_t *array, uint32_t size) -{ - uint32_t lastIndex = size - 1; - char tempInt = 0; - - // If array is NULL, return - if (!array) - { - return; - } - - for (uint32_t loop = 0; loop <= (lastIndex / 2); loop++) - { - tempInt = array[loop]; - array[loop] = array[lastIndex - loop]; - array[lastIndex - loop] = tempInt; - } -} - /******************************************************************************/ /* BN_num_bytes may return fewer bytes than the curve's key size when coordinates have leading zeros - use upper-bound check (>) not equality */ @@ -266,13 +246,7 @@ uint8_t Versal_2ve_2vmKey::ParseECDSAOpenSSLKey(const std::string& filename) { OpenSSL_add_all_algorithms(); BIGNUM *X = BN_new(); - X->flags = 0; - X->neg = 0; - X->top = 0; BIGNUM *Y = BN_new(); - Y->flags = 0; - Y->neg = 0; - Y->top = 0; uint32_t keySzRdX; uint32_t keySzRdY; EC_GROUP *ecgroup = NULL; // Track EC_GROUP to free it at the end @@ -299,10 +273,6 @@ uint8_t Versal_2ve_2vmKey::ParseECDSAOpenSSLKey(const std::string& filename) y = std::make_unique(keySize); memset(x.get(), 0, keySize); memset(y.get(), 0, keySize); - X->d = (BN_ULONG*)x.get(); - Y->d = (BN_ULONG*)y.get(); - X->dmax = keySize / sizeof(BN_ULONG); - Y->dmax = keySize / sizeof(BN_ULONG); ecgroup = EC_GROUP_new_by_curve_name(NID_secp384r1); const EC_POINT *pub = EC_KEY_get0_public_key(eckey); if (EC_POINT_get_affine_coordinates_GFp(ecgroup, pub, X, Y, NULL)) @@ -317,8 +287,11 @@ uint8_t Versal_2ve_2vmKey::ParseECDSAOpenSSLKey(const std::string& filename) { LOG_ERROR("Incorrect Key Size !!!\n\t Key Size is %d bits. Expected key size is %d bits", BN_num_bits(Y), keySize * 8); } - RearrangeEndianess(x.get(), keySize); - RearrangeEndianess(y.get(), keySize); + if (BN_bn2binpad(X, x.get(), keySize) != keySize + || BN_bn2binpad(Y, y.get(), keySize) != keySize) + { + LOG_ERROR("Failed to export ECDSAp384 public key"); + } } else { @@ -346,11 +319,11 @@ uint8_t Versal_2ve_2vmKey::ParseECDSAOpenSSLKey(const std::string& filename) y = std::make_unique(keySizeY); memset(x.get(), 0, keySizeX); memset(y.get(), 0, keySizeY); - memcpy(x.get(), X->d, keySizeX); - memcpy(y.get(), Y->d, keySizeY); - - RearrangeEndianess(x.get(), keySizeX); - RearrangeEndianess(y.get(), keySizeY); + if (BN_bn2binpad(X, x.get(), keySizeX) != keySizeX + || BN_bn2binpad(Y, y.get(), keySizeY) != keySizeY) + { + LOG_ERROR("Failed to export ECDSAp521 public key"); + } } else { @@ -374,10 +347,6 @@ uint8_t Versal_2ve_2vmKey::ParseECDSAOpenSSLKey(const std::string& filename) y = std::make_unique(keySize); memset(x.get(), 0, keySize); memset(y.get(), 0, keySize); - X->d = (BN_ULONG*)x.get(); - Y->d = (BN_ULONG*)y.get(); - X->dmax = keySize / sizeof(BN_ULONG); - Y->dmax = keySize / sizeof(BN_ULONG); ecgroup = EC_GROUP_new_by_curve_name(NID_secp384r1); const EC_POINT *pub = EC_KEY_get0_public_key(eckey); if (EC_POINT_get_affine_coordinates_GFp(ecgroup, pub, X, Y, NULL)) @@ -392,8 +361,11 @@ uint8_t Versal_2ve_2vmKey::ParseECDSAOpenSSLKey(const std::string& filename) { LOG_ERROR("Incorrect Key Size !!!\n\t Key Size is %d bits. Expected key size is %d bits", BN_num_bits(Y), keySize * 8); } - RearrangeEndianess(x.get(), keySize); - RearrangeEndianess(y.get(), keySize); + if (BN_bn2binpad(X, x.get(), keySize) != keySize + || BN_bn2binpad(Y, y.get(), keySize) != keySize) + { + LOG_ERROR("Failed to export ECDSAp384 public key"); + } } else { @@ -421,11 +393,11 @@ uint8_t Versal_2ve_2vmKey::ParseECDSAOpenSSLKey(const std::string& filename) y = std::make_unique(keySizeY); memset(x.get(), 0, keySizeX); memset(y.get(), 0, keySizeY); - memcpy(x.get(), X->d, keySizeX); - memcpy(y.get(), Y->d, keySizeY); - - RearrangeEndianess(x.get(), keySizeX); - RearrangeEndianess(y.get(), keySizeY); + if (BN_bn2binpad(X, x.get(), keySizeX) != keySizeX + || BN_bn2binpad(Y, y.get(), keySizeY) != keySizeY) + { + LOG_ERROR("Failed to export ECDSAp521 public key"); + } } else { @@ -436,28 +408,9 @@ uint8_t Versal_2ve_2vmKey::ParseECDSAOpenSSLKey(const std::string& filename) } fclose(file); - // Free BIGNUM structures - // For P384: X->d and Y->d point to our memory (x.get(), y.get()), so clear before freeing - // For P521: X->d and Y->d still have OpenSSL memory, so BN_free will free them normally - if (X) { - if (keySize == EC_P384_KEY_LENGTH) { - X->d = NULL; // Prevent OpenSSL from freeing our memory (x smart pointer owns it) - X->dmax = 0; - } - BN_free(X); - } - if (Y) { - if (keySize == EC_P384_KEY_LENGTH) { - Y->d = NULL; // Prevent OpenSSL from freeing our memory (y smart pointer owns it) - Y->dmax = 0; - } - BN_free(Y); - } - - // Free EC_GROUP object - if (ecgroup) { - EC_GROUP_free(ecgroup); - } + BN_free(X); + BN_free(Y); + EC_GROUP_free(ecgroup); return 0; } From 8563eceb418ab9931ad2c4a6a49771358d3954cb Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Sat, 22 Aug 2026 22:16:11 -0500 Subject: [PATCH 35/45] Remove OpenSSL BIGNUM structure dependency --- common/include/authentication.h | 41 +++++------- common/include/authkeys.h | 4 -- common/src/authentication.cpp | 67 +++---------------- common/src/authkeys.cpp | 21 +----- spartanup/src/authkeys-spartanup.cpp | 34 +--------- spartanup/src/verifyimage-spartanup.cpp | 14 ++-- versal/src/authkeys-versal.cpp | 34 +--------- versal/src/verifyimage-versal.cpp | 14 ++-- .../src/authkeys-versal_2ve_2vm.cpp | 34 +--------- .../src/verifyimage-versal_2ve_2vm.cpp | 14 ++-- 10 files changed, 51 insertions(+), 226 deletions(-) diff --git a/common/include/authentication.h b/common/include/authentication.h index 5ed9141..4b2f05b 100755 --- a/common/include/authentication.h +++ b/common/include/authentication.h @@ -46,10 +46,6 @@ #include "hash.h" #include "systemutils.h" -#if OPENSSL_VERSION_NUMBER > 0x10100000L -#include "xil-bignum.h" -#endif - /* Forward class references */ class BaseThing; class Section; @@ -218,31 +214,26 @@ class BN_MONT_CTX_Class void GetModulusExtension(uint8_t* ptr, BIGNUM& m, size_t len) { - if(len == RSA_2048_KEY_LENGTH) + BIGNUM *r = BN_new(); + BIGNUM *rr = BN_new(); + if (r == NULL || rr == NULL) { - if (len != mont->RR.top * sizeof(BN_ULONG)) - { - LOG_ERROR("Sanity check in GetModulusExtension Failed"); - } - memcpy(ptr,mont->RR.d,len); + BN_free(r); + BN_free(rr); + LOG_ERROR("Failed to allocate BIGNUMs for modulus extension"); } - else + const int rBits = len == RSA_4096_KEY_LENGTH ? 4160 : static_cast(len * 8); + if (BN_one(r) != 1 + || BN_lshift(r, r, rBits) != 1 + || BN_mod_sqr(rr, r, &m, ctx) != 1 + || BN_bn2lebinpad(rr, ptr, static_cast(len)) != static_cast(len)) { - BIGNUM *r = BN_new(); - BIGNUM *res = BN_new(); - BIGNUM *m_x = BN_new(); - if( r == NULL || res == NULL || m_x == NULL) - { - LOG_ERROR("Failed to allocate BN_new"); - } - BN_one(r); - BN_lshift(res, r, 4160); - BN_mod_mul(m_x, res, res, &m, ctx); - memcpy(ptr, m_x->d, RSA_4096_KEY_LENGTH); BN_free(r); - BN_free(res); - BN_free(m_x); + BN_free(rr); + LOG_ERROR("Failed to compute modulus extension"); } + BN_free(r); + BN_free(rr); } BN_CTX* ctx; @@ -502,4 +493,4 @@ class RSA4096AuthenticationCertificate : public AuthenticationCertificate RSA4096AuthenticationCertificate(AuthenticationContext* context) : AuthenticationCertificate(context) { } RSA4096AuthenticationCertificate() { }; }; -#endif \ No newline at end of file +#endif diff --git a/common/include/authkeys.h b/common/include/authkeys.h index 4cd3f1a..b018eff 100755 --- a/common/include/authkeys.h +++ b/common/include/authkeys.h @@ -34,10 +34,6 @@ #include "logger.h" #include //#include "bifoptions.h" -#if OPENSSL_VERSION_NUMBER > 0x10100000L -#include "xil-bignum.h" -#endif - /* ------------------------------------------------------------------------------- ********************************************* P R E P R O C E S S O R S *** diff --git a/common/src/authentication.cpp b/common/src/authentication.cpp index 08635b7..a6409c6 100755 --- a/common/src/authentication.cpp +++ b/common/src/authentication.cpp @@ -716,10 +716,6 @@ Section* RSA2048AuthenticationCertificate::AttachBootHeaderToFsbl(BootImage& bi) /******************************************************************************/ void AuthenticationAlgorithm::RSA_Exponentiation(const uint8_t *base, const uint8_t* modular, const uint8_t *modular_ext, const uint8_t *exponent, uint8_t *result0) { -#ifdef _MSC_VER - /* On Windows/MSVC, direct BIGNUM struct field access (d, dmax, top, etc.) - causes BN_MONT_CTX_set failure due to struct layout differences between GCC and MSVC. - Use public OpenSSL API (BN_lebin2bn, BN_mod_exp_mont, BN_bn2lebinpad) instead. */ uint16_t keyLength = AuthenticationContext::GetRsaKeyLength(); BIGNUM *a = BN_lebin2bn(base, keyLength, NULL); @@ -767,63 +763,18 @@ void AuthenticationAlgorithm::RSA_Exponentiation(const uint8_t *base, const uint LOG_ERROR("Authentication Error !!!"); } - BN_bn2lebinpad(result, result0, keyLength); + if (BN_bn2lebinpad(result, result0, keyLength) != keyLength) + { + BN_free(result); + BN_free(a); + BN_free(power); + BN_free(m); + LOG_ERROR("Failed to export RSA exponentiation result"); + } BN_free(result); BN_free(a); BN_free(power); BN_free(m); -#else - BIGNUM result; - BIGNUM a; // base - BIGNUM power; // exponent - BIGNUM m; // modulus - BN_CTX_Class ctxInst; - BN_MONT_CTX_Class montClass(ctxInst); - - uint16_t keyLength = AuthenticationContext::GetRsaKeyLength(); - - a.d = (BN_ULONG*)base; - a.dmax = keyLength / sizeof(BN_ULONG); - a.top = keyLength / sizeof(BN_ULONG); - a.flags = 0; - a.neg = 0; - - power.d = (BN_ULONG*)exponent; - power.dmax = keyLength / sizeof(BN_ULONG); - power.top = keyLength / sizeof(BN_ULONG); - power.flags = 0; - power.neg = 0; - - m.d = (BN_ULONG*)modular; - m.dmax = keyLength / sizeof(BN_ULONG); - m.top = keyLength / sizeof(BN_ULONG); - m.flags = 0; - m.neg = 0; - - montClass.Set(m); - auto sanityExtension = std::make_unique(keyLength); - montClass.GetModulusExtension(sanityExtension.get(), m, keyLength); - int comp = memcmp(sanityExtension.get(), modular_ext, keyLength); - - if (comp) - { - LOG_ERROR("Internal Error : Montgomery Reduction is not same as externally calculated value."); - } - - result.d = (BN_ULONG*)result0; - result.dmax = keyLength / sizeof(BN_ULONG); - result.flags = 0; - result.neg = 0; - result.top = 0; - - int ret = BN_mod_exp_mont(&result, &a, &power, &m, ctxInst.ctx, montClass.mont); - - if (ret != 1) - { - LOG_DEBUG(DEBUG_STAMP, "Error in calculating Modulus Exponent"); - LOG_ERROR("Authentication Error !!!"); - } -#endif } /******************************************************************************/ @@ -836,4 +787,4 @@ void RSAAuthenticationAlgorithm::CreateSignature(const uint8_t* base, uint8_t* p NoneAuthenticationContext::NoneAuthenticationContext() { authAlgorithm = std::make_unique(); -} \ No newline at end of file +} diff --git a/common/src/authkeys.cpp b/common/src/authkeys.cpp index 0d0f492..c03a3e2 100755 --- a/common/src/authkeys.cpp +++ b/common/src/authkeys.cpp @@ -150,24 +150,7 @@ void Key::Parse(const std::string& filename, bool isSecret0) /* Calculate the modulus extension, i.e. Montgomery Reduction term RR and some sanity check for the keys passed */ { -#ifdef _MSC_VER - /* On Windows/MSVC, direct BIGNUM struct field access (d, dmax, top) - causes BN_MONT_CTX_set to fail due to struct layout differences - between GCC and MSVC OpenSSL builds. Use public API instead. */ ComputeModulusExtension(N.get(), N_ext.get(), keySize); -#else - BIGNUM m; - m.d = (BN_ULONG*)N.get(); - m.dmax = keySize / sizeof(BN_ULONG); - m.top = keySize / sizeof(BN_ULONG); - m.flags = 0; - m.neg = 0; - - BN_CTX_Class ctxInst; - BN_MONT_CTX_Class montClass(ctxInst); - montClass.Set(m); - montClass.GetModulusExtension(N_ext.get(), m, keySize); -#endif } Loaded = true; } @@ -208,8 +191,8 @@ void Key::ComputeModulusExtension(const uint8_t* modulus, uint8_t* extension, si LOG_ERROR("Failed to allocate BIGNUMs for modulus extension"); } - int bits = BN_num_bits(m); - BN_set_bit(R, bits); + const int rBits = keyLen == RSA_4096_KEY_LENGTH ? 4160 : static_cast(keyLen * 8); + BN_set_bit(R, rBits); BN_mod_mul(RR, R, R, m, ctx); BN_bn2lebinpad(RR, extension, (int)keyLen); diff --git a/spartanup/src/authkeys-spartanup.cpp b/spartanup/src/authkeys-spartanup.cpp index f52c7f7..ddc3ccc 100755 --- a/spartanup/src/authkeys-spartanup.cpp +++ b/spartanup/src/authkeys-spartanup.cpp @@ -536,22 +536,7 @@ void SpartanupKey::Parse(const std::string& filename, bool isSecret0) LOG_ERROR("RSA authentication key parsing failed - %s", basefile.c_str()); } - /* Calculate the modulus extension, i.e. Montgomery Reduction term RR - and some sanity check for the keys passed */ - { - BIGNUM m; // modulus - m.d = (BN_ULONG*)N.get(); - m.dmax = keySize / sizeof(BN_ULONG); - m.top = keySize / sizeof(BN_ULONG); - m.flags = 0; - m.neg = 0; - - BN_CTX_Class ctxInst; - BN_MONT_CTX_Class montClass(ctxInst); - - montClass.Set(m); - montClass.GetModulusExtension(N_ext.get(), m, keySize); - } + Key::ComputeModulusExtension(N.get(), N_ext.get(), keySize); Loaded = true; } @@ -659,22 +644,7 @@ void SpartanupKey::Parse(const std::string& filename, bool isSecret0) LOG_ERROR("RSA authentication key parsing failed - %s", basefile.c_str()); } - /* Calculate the modulus extension, i.e. Montgomery Reduction term RR - and some sanity check for the keys passed */ - { - BIGNUM m; // modulus - m.d = (BN_ULONG*)N.get(); - m.dmax = keySize / sizeof(BN_ULONG); - m.top = keySize / sizeof(BN_ULONG); - m.flags = 0; - m.neg = 0; - - BN_CTX_Class ctxInst; - BN_MONT_CTX_Class montClass(ctxInst); - - montClass.Set(m); - montClass.GetModulusExtension(N_ext.get(), m, keySize); - } + Key::ComputeModulusExtension(N.get(), N_ext.get(), keySize); Loaded = true; } diff --git a/spartanup/src/verifyimage-spartanup.cpp b/spartanup/src/verifyimage-spartanup.cpp index 66561c4..cbc5735 100755 --- a/spartanup/src/verifyimage-spartanup.cpp +++ b/spartanup/src/verifyimage-spartanup.cpp @@ -187,13 +187,12 @@ bool SpartanupReadImage::VerifySignature(bool nist, uint8_t * data, size_t dataL BIGNUM *n = BN_bin2bn(acKey->N, sizeof(acKey->N), NULL); BIGNUM *e = BN_bin2bn(acKey->E, sizeof(acKey->E), NULL); -#if OPENSSL_VERSION_NUMBER > 0x10100000L - BIGNUM *d = NULL; - RSA_set0_key(rsa, n, e, d); -#else - rsa->n = n; - rsa->e = e; -#endif + if (RSA_set0_key(rsa, n, e, NULL) != 1) + { + BN_free(n); + BN_free(e); + LOG_ERROR("Failed to configure RSA verification key"); + } /* Find SHA-384 hash from signature */ uint8_t opensslHashPadded[SIGN_LENGTH_VERSAL] = { 0 }; @@ -677,4 +676,3 @@ void SpartanupReadImage::VerifyPartitionSignature(void) fclose(binFile); Separator(); } - diff --git a/versal/src/authkeys-versal.cpp b/versal/src/authkeys-versal.cpp index fef2223..ebcd657 100755 --- a/versal/src/authkeys-versal.cpp +++ b/versal/src/authkeys-versal.cpp @@ -456,22 +456,7 @@ void VersalKey::Parse(const std::string& filename, bool isSecret0) LOG_ERROR("RSA authentication key parsing failed - %s", basefile.c_str()); } - /* Calculate the modulus extension, i.e. Montgomery Reduction term RR - and some sanity check for the keys passed */ - { - BIGNUM m; // modulus - m.d = (BN_ULONG*)N.get(); - m.dmax = keySize / sizeof(BN_ULONG); - m.top = keySize / sizeof(BN_ULONG); - m.flags = 0; - m.neg = 0; - - BN_CTX_Class ctxInst; - BN_MONT_CTX_Class montClass(ctxInst); - - montClass.Set(m); - montClass.GetModulusExtension(N_ext.get(), m, keySize); - } + Key::ComputeModulusExtension(N.get(), N_ext.get(), keySize); Loaded = true; } @@ -569,22 +554,7 @@ void VersalKey::Parse(const std::string& filename, bool isSecret0) LOG_ERROR("RSA authentication key parsing failed - %s", basefile.c_str()); } - /* Calculate the modulus extension, i.e. Montgomery Reduction term RR - and some sanity check for the keys passed */ - { - BIGNUM m; // modulus - m.d = (BN_ULONG*)N.get(); - m.dmax = keySize / sizeof(BN_ULONG); - m.top = keySize / sizeof(BN_ULONG); - m.flags = 0; - m.neg = 0; - - BN_CTX_Class ctxInst; - BN_MONT_CTX_Class montClass(ctxInst); - - montClass.Set(m); - montClass.GetModulusExtension(N_ext.get(), m, keySize); - } + Key::ComputeModulusExtension(N.get(), N_ext.get(), keySize); Loaded = true; } diff --git a/versal/src/verifyimage-versal.cpp b/versal/src/verifyimage-versal.cpp index 41cb357..9483840 100755 --- a/versal/src/verifyimage-versal.cpp +++ b/versal/src/verifyimage-versal.cpp @@ -190,13 +190,12 @@ bool VersalReadImage::VerifySignature(bool nist, uint8_t * data, size_t dataLeng BIGNUM *n = BN_bin2bn(acKey->N, sizeof(acKey->N), NULL); BIGNUM *e = BN_bin2bn(acKey->E, sizeof(acKey->E), NULL); -#if OPENSSL_VERSION_NUMBER > 0x10100000L - BIGNUM *d = NULL; - RSA_set0_key(rsa, n, e, d); -#else - rsa->n = n; - rsa->e = e; -#endif + if (RSA_set0_key(rsa, n, e, NULL) != 1) + { + BN_free(n); + BN_free(e); + LOG_ERROR("Failed to configure RSA verification key"); + } /* Find SHA-384 hash from signature */ uint8_t opensslHashPadded[SIGN_LENGTH_VERSAL] = { 0 }; @@ -756,4 +755,3 @@ void VersalReadImage::VerifyPartitionSignature(void) fclose(binFile); Separator(); } - diff --git a/versal_2ve_2vm/src/authkeys-versal_2ve_2vm.cpp b/versal_2ve_2vm/src/authkeys-versal_2ve_2vm.cpp index 4effe0f..5f2bc95 100755 --- a/versal_2ve_2vm/src/authkeys-versal_2ve_2vm.cpp +++ b/versal_2ve_2vm/src/authkeys-versal_2ve_2vm.cpp @@ -523,22 +523,7 @@ void Versal_2ve_2vmKey::Parse(const std::string& filename, bool isSecret0) LOG_ERROR("RSA authentication key parsing failed - %s", basefile.c_str()); } - /* Calculate the modulus extension, i.e. Montgomery Reduction term RR - and some sanity check for the keys passed */ - { - BIGNUM m; // modulus - m.d = (BN_ULONG*)N.get(); - m.dmax = keySize / sizeof(BN_ULONG); - m.top = keySize / sizeof(BN_ULONG); - m.flags = 0; - m.neg = 0; - - BN_CTX_Class ctxInst; - BN_MONT_CTX_Class montClass(ctxInst); - - montClass.Set(m); - montClass.GetModulusExtension(N_ext.get(), m, keySize); - } + Key::ComputeModulusExtension(N.get(), N_ext.get(), keySize); Loaded = true; } @@ -646,22 +631,7 @@ void Versal_2ve_2vmKey::Parse(const std::string& filename, bool isSecret0) LOG_ERROR("RSA authentication key parsing failed - %s", basefile.c_str()); } - /* Calculate the modulus extension, i.e. Montgomery Reduction term RR - and some sanity check for the keys passed */ - { - BIGNUM m; // modulus - m.d = (BN_ULONG*)N.get(); - m.dmax = keySize / sizeof(BN_ULONG); - m.top = keySize / sizeof(BN_ULONG); - m.flags = 0; - m.neg = 0; - - BN_CTX_Class ctxInst; - BN_MONT_CTX_Class montClass(ctxInst); - - montClass.Set(m); - montClass.GetModulusExtension(N_ext.get(), m, keySize); - } + Key::ComputeModulusExtension(N.get(), N_ext.get(), keySize); Loaded = true; } diff --git a/versal_2ve_2vm/src/verifyimage-versal_2ve_2vm.cpp b/versal_2ve_2vm/src/verifyimage-versal_2ve_2vm.cpp index 5a4f24b..f3e868d 100755 --- a/versal_2ve_2vm/src/verifyimage-versal_2ve_2vm.cpp +++ b/versal_2ve_2vm/src/verifyimage-versal_2ve_2vm.cpp @@ -189,13 +189,12 @@ bool Versal_2ve_2vmReadImage::VerifySignature(bool nist, uint8_t * data, size_t BIGNUM *n = BN_bin2bn(acKey->N, sizeof(acKey->N), NULL); BIGNUM *e = BN_bin2bn(acKey->E, sizeof(acKey->E), NULL); -#if OPENSSL_VERSION_NUMBER > 0x10100000L - BIGNUM *d = NULL; - RSA_set0_key(rsa, n, e, d); -#else - rsa->n = n; - rsa->e = e; -#endif + if (RSA_set0_key(rsa, n, e, NULL) != 1) + { + BN_free(n); + BN_free(e); + LOG_ERROR("Failed to configure RSA verification key"); + } /* Find SHA-384 hash from signature */ uint8_t opensslHashPadded[SIGN_LENGTH_VERSAL] = { 0 }; @@ -680,4 +679,3 @@ void Versal_2ve_2vmReadImage::VerifyPartitionSignature(void) fclose(binFile); Separator(); } - From 91d2aa68c166f93f28f790ed9bd9807435d02b16 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 24 Aug 2026 05:46:43 -0500 Subject: [PATCH 36/45] Document release validation evidence --- BUILDING.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/BUILDING.md b/BUILDING.md index a61d52a..04cdce1 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -112,6 +112,25 @@ cmake --build build/macos-asan --parallel ctest --test-dir build/macos-asan --output-on-failure ``` +## Release validation matrix + +A successful local build proves only the host and dependency architecture used +for that build. Before publishing a macOS binary or proposing these changes +upstream, run and retain the results of the following checks: + +| Gate | Required evidence | +| --- | --- | +| Apple Silicon | Native `arm64` release and sanitizer builds with the complete CTest suite. | +| Intel macOS | Native `x86_64` release and sanitizer builds with the complete CTest suite and Intel Homebrew OpenSSL. | +| Universal package | Independently tested arm64 and x86_64 binaries, universal compatible OpenSSL dependencies, and a successful `package_universal_macos.cmake` run. | +| Linux compatibility | GCC, Clang, and sanitizer builds using the project workflow or an equivalent native Linux environment. | +| Image compatibility | Comparisons with a pinned upstream reference for deterministic images; structural and signature verification for randomized authenticated images. | +| Hosted CI | A clean GitHub Actions run from the contributor fork for every matrix job. | + +The current GitHub workflow covers the Linux compiler/sanitizer and native macOS +architecture jobs after the branch is pushed to a fork. It intentionally does +not synthesize a universal package from untested slices. + ## Parser regeneration (maintainers only) The `bisonflex` directory is checked in. Regenerate it only when changing a From 93cdd8a7dc5c38e8bc9d48797640b93565575c62 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 24 Aug 2026 05:46:43 -0500 Subject: [PATCH 37/45] Require a supported OpenSSL version --- BUILDING.md | 2 +- CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 04cdce1..3b9a0b1 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -8,7 +8,7 @@ build. - A C++14 compiler - CMake 3.20 or newer -- OpenSSL development headers and libraries +- OpenSSL 1.1.1 or newer development headers and libraries (OpenSSL 3 on macOS) - A threading implementation supported by CMake On macOS, install the build prerequisites with Homebrew: diff --git a/CMakeLists.txt b/CMakeLists.txt index e750148..987cfd6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,7 @@ set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -find_package(OpenSSL REQUIRED COMPONENTS SSL Crypto) +find_package(OpenSSL 1.1.1 REQUIRED COMPONENTS SSL Crypto) find_package(Threads REQUIRED) set(BOOTGEN_INCLUDE_DIRS From c604c7bf9dbebc66f0a40e3c25fdb5924b286264 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 24 Aug 2026 06:00:22 -0500 Subject: [PATCH 38/45] Reject truncated boot images in all readers --- CMakeLists.txt | 6 ++++++ tests/cmake/malformed_image_read_test.cmake | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 tests/cmake/malformed_image_read_test.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 987cfd6..b25b9c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -367,5 +367,11 @@ if(BOOTGEN_BUILD_TESTS) -DBOOTGEN=$ -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/invalid_bif_test.cmake) + add_test( + NAME bootgen.malformed-image-read + COMMAND ${CMAKE_COMMAND} + -DBOOTGEN=$ + -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/malformed-image-read + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/malformed_image_read_test.cmake) endif() endif() diff --git a/tests/cmake/malformed_image_read_test.cmake b/tests/cmake/malformed_image_read_test.cmake new file mode 100644 index 0000000..0887f88 --- /dev/null +++ b/tests/cmake/malformed_image_read_test.cmake @@ -0,0 +1,20 @@ +if(NOT DEFINED BOOTGEN OR NOT DEFINED OUTPUT_DIR) + message(FATAL_ERROR "BOOTGEN and OUTPUT_DIR are required") +endif() + +file(MAKE_DIRECTORY "${OUTPUT_DIR}") +set(MALFORMED_IMAGE "${OUTPUT_DIR}/truncated-boot-image.bin") +# A boot image contains headers larger than this input. Keeping the fixture +# intentionally short exercises bounds checks before any image data is used. +file(WRITE "${MALFORMED_IMAGE}" "BOOTGEN") + +foreach(ARCHITECTURE zynq zynqmp versal versal_2ve_2vm spartanup) + execute_process( + COMMAND "${BOOTGEN}" -arch "${ARCHITECTURE}" -read "${MALFORMED_IMAGE}" + RESULT_VARIABLE READ_RESULT + OUTPUT_VARIABLE READ_OUTPUT + ERROR_VARIABLE READ_ERROR) + if(READ_RESULT EQUAL 0) + message(FATAL_ERROR "${ARCHITECTURE} accepted a truncated boot image:\n${READ_OUTPUT}\n${READ_ERROR}") + endif() +endforeach() From 9fa7c62fd826318bc514b7bc948b3c7e0725a0a1 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 24 Aug 2026 08:57:49 -0500 Subject: [PATCH 39/45] Detect malformed image reader crashes --- tests/cmake/malformed_image_read_test.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cmake/malformed_image_read_test.cmake b/tests/cmake/malformed_image_read_test.cmake index 0887f88..a2654a2 100644 --- a/tests/cmake/malformed_image_read_test.cmake +++ b/tests/cmake/malformed_image_read_test.cmake @@ -14,7 +14,7 @@ foreach(ARCHITECTURE zynq zynqmp versal versal_2ve_2vm spartanup) RESULT_VARIABLE READ_RESULT OUTPUT_VARIABLE READ_OUTPUT ERROR_VARIABLE READ_ERROR) - if(READ_RESULT EQUAL 0) - message(FATAL_ERROR "${ARCHITECTURE} accepted a truncated boot image:\n${READ_OUTPUT}\n${READ_ERROR}") + if(READ_RESULT EQUAL 0 OR NOT READ_RESULT MATCHES "^[1-9][0-9]*$") + message(FATAL_ERROR "${ARCHITECTURE} did not cleanly reject a truncated boot image (result: ${READ_RESULT}):\n${READ_OUTPUT}\n${READ_ERROR}") endif() endforeach() From f4aa8d5505153491e6c39f71b06b8474d0373f17 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 24 Aug 2026 09:25:07 -0500 Subject: [PATCH 40/45] Enable CDO buffer loading on macOS --- utils/src/cdo-load.c | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/utils/src/cdo-load.c b/utils/src/cdo-load.c index 96a479a..f4975a7 100755 --- a/utils/src/cdo-load.c +++ b/utils/src/cdo-load.c @@ -130,7 +130,7 @@ CdoSequence * cdoseq_load_cdo_from_buffer(char * data, size_t size) { return sequence; #endif - #if defined(__linux__) + #if !defined(_WIN64) && !defined(_WIN32) CdoSequence * seq = NULL; CdoRawInfo * raw = NULL; @@ -146,11 +146,18 @@ CdoSequence * cdoseq_load_cdo_from_buffer(char * data, size_t size) { fprintf(stderr, "cannot decode binary cdo buffer\n"); goto done; } - cdometa_add_markers(&raw->meta, seq); + if (raw != NULL) { + cdometa_add_markers(&raw->meta, seq); + } } else { - FILE *memfile = fmemopen(data, size, "r"); + FILE *memfile = tmpfile(); if (memfile == NULL) { - fprintf(stderr, "fmemopen failed\n"); + fprintf(stderr, "tmpfile failed\n"); + goto done; + } + if (fwrite(data, 1, size, memfile) != size || fseek(memfile, 0, SEEK_SET) != 0) { + fprintf(stderr, "cannot prepare CDO buffer stream\n"); + fclose(memfile); goto done; } seq = cdoseq_from_source(memfile); From a19f1d1f4ebf7b2cc7311db11ff19319778fc8e4 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 24 Aug 2026 11:07:30 -0500 Subject: [PATCH 41/45] Record upstream macOS proposal audit --- UPSTREAM_BASELINE.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/UPSTREAM_BASELINE.md b/UPSTREAM_BASELINE.md index c2f1f07..e2217e6 100644 --- a/UPSTREAM_BASELINE.md +++ b/UPSTREAM_BASELINE.md @@ -21,3 +21,17 @@ git log --oneline 510dc3a4ffb2bd29fcfb8f8d7a1c5aa2bb1f39f1..HEAD This branch deliberately retains small, reviewable commits so porting, security, and test changes can be proposed upstream independently when useful. + +## Existing macOS proposal + +[PR #52](https://github.com/Xilinx/bootgen/pull/52) was open when this branch +was audited on 2026-08-22. Its single commit (`c815d8f`) adds a Makefile path +that calls Homebrew directly and omits `malloc.h` on macOS. This branch includes +the required `malloc.h` portability guards, but does not depend on that proposal: +dependency discovery is caller-supplied through CMake, and the port also covers +OpenSSL 3 compatibility, native CI, family smoke tests, packaging, and +documentation. + +When proposing this work upstream, reference PR #52 to avoid duplicate review +and present these commits as a superset with independently reproducible build +and test evidence. From 8164597e70737574da5b7db7bfd1870339a87808 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 24 Aug 2026 11:07:30 -0500 Subject: [PATCH 42/45] Harden Versal CDO partition decoding --- CMakeLists.txt | 7 ++++ tests/cmake/versal_cdo_smoke.cmake | 52 ++++++++++++++++++++++++++++++ versal/src/partition-versal.cpp | 4 +++ 3 files changed, 63 insertions(+) create mode 100644 tests/cmake/versal_cdo_smoke.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index b25b9c3..88eb430 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -318,6 +318,13 @@ if(BOOTGEN_BUILD_TESTS) -DARCH=versal -DEXPECTED_READ_OUTPUT=partition_type[elf] -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/versal_smoke.cmake) + add_test( + NAME bootgen.versal-cdo-smoke + COMMAND ${CMAKE_COMMAND} + -DBOOTGEN=$ + -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures + -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/versal-cdo-smoke + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/versal_cdo_smoke.cmake) add_test( NAME bootgen.versal-2ve-2vm-smoke COMMAND ${CMAKE_COMMAND} diff --git a/tests/cmake/versal_cdo_smoke.cmake b/tests/cmake/versal_cdo_smoke.cmake new file mode 100644 index 0000000..4904ad2 --- /dev/null +++ b/tests/cmake/versal_cdo_smoke.cmake @@ -0,0 +1,52 @@ +if(NOT DEFINED BOOTGEN OR NOT DEFINED FIXTURE_DIR OR NOT DEFINED OUTPUT_DIR) + message(FATAL_ERROR "BOOTGEN, FIXTURE_DIR, and OUTPUT_DIR are required") +endif() + +file(MAKE_DIRECTORY "${OUTPUT_DIR}") +set(TEST_FIXTURE_DIR "${OUTPUT_DIR}/fixture") +file(MAKE_DIRECTORY "${TEST_FIXTURE_DIR}") +file(COPY "${FIXTURE_DIR}/zynq-smoke.elf" DESTINATION "${TEST_FIXTURE_DIR}") +file(WRITE "${TEST_FIXTURE_DIR}/minimal.cdo" "version 2.0\nwrite 0xf1260050 0\n") +file(WRITE "${TEST_FIXTURE_DIR}/versal-cdo.bif" "all:\n{\n image\n {\n name = image1, id = 0x1c000001\n { type = bootloader, file = zynq-smoke.elf }\n { type = cdo, file = minimal.cdo }\n }\n}\n") + +set(BOOT_IMAGE "${OUTPUT_DIR}/BOOT.BIN") +execute_process( + COMMAND "${BOOTGEN}" -arch versal -image "${TEST_FIXTURE_DIR}/versal-cdo.bif" -o "${BOOT_IMAGE}" -w on + WORKING_DIRECTORY "${TEST_FIXTURE_DIR}" + RESULT_VARIABLE CREATE_RESULT + OUTPUT_VARIABLE CREATE_OUTPUT + ERROR_VARIABLE CREATE_ERROR) +if(NOT CREATE_RESULT EQUAL 0) + message(FATAL_ERROR "Versal CDO image creation failed (${CREATE_RESULT}):\n${CREATE_OUTPUT}\n${CREATE_ERROR}") +endif() + +file(SIZE "${BOOT_IMAGE}" BOOT_IMAGE_SIZE) +if(BOOT_IMAGE_SIZE LESS 512) + message(FATAL_ERROR "Versal CDO smoke image is unexpectedly small: ${BOOT_IMAGE_SIZE} bytes") +endif() + +execute_process( + COMMAND "${BOOTGEN}" -arch versal -read "${BOOT_IMAGE}" + RESULT_VARIABLE READ_RESULT + OUTPUT_VARIABLE READ_OUTPUT + ERROR_VARIABLE READ_ERROR) +if(NOT READ_RESULT EQUAL 0) + message(FATAL_ERROR "Versal CDO image inspection failed (${READ_RESULT}):\n${READ_OUTPUT}\n${READ_ERROR}") +endif() + +string(FIND "${READ_OUTPUT}" "partition_type[cdo]" CDO_OFFSET) +if(CDO_OFFSET EQUAL -1) + message(FATAL_ERROR "Versal CDO smoke image does not contain a CDO partition:\n${READ_OUTPUT}") +endif() + +file(WRITE "${TEST_FIXTURE_DIR}/legacy.cdo" "version 1.0\nsection 0\nwrite 0xf1260050 0\n") +file(WRITE "${TEST_FIXTURE_DIR}/versal-legacy-cdo.bif" "all:\n{\n image\n {\n name = image1, id = 0x1c000001\n { type = bootloader, file = zynq-smoke.elf }\n { type = cdo, file = legacy.cdo }\n }\n}\n") +execute_process( + COMMAND "${BOOTGEN}" -arch versal -image "${TEST_FIXTURE_DIR}/versal-legacy-cdo.bif" -o "${OUTPUT_DIR}/legacy-BOOT.BIN" -w on + WORKING_DIRECTORY "${TEST_FIXTURE_DIR}" + RESULT_VARIABLE LEGACY_RESULT + OUTPUT_VARIABLE LEGACY_OUTPUT + ERROR_VARIABLE LEGACY_ERROR) +if(LEGACY_RESULT EQUAL 0 OR NOT LEGACY_RESULT MATCHES "^[1-9][0-9]*$") + message(FATAL_ERROR "Versal accepted or crashed on an unsupported legacy CDO (result: ${LEGACY_RESULT}):\n${LEGACY_OUTPUT}\n${LEGACY_ERROR}") +endif() diff --git a/versal/src/partition-versal.cpp b/versal/src/partition-versal.cpp index d68443f..6bd0165 100644 --- a/versal/src/partition-versal.cpp +++ b/versal/src/partition-versal.cpp @@ -539,6 +539,10 @@ void VersalPartition::Build(BootImage& bi, Binary& cache) CdoSequence * cdo_seq; cdo_seq = decode_cdo_binary(header->partition->section->Data.get(), header->partition->section->Length); + if (cdo_seq == NULL) + { + LOG_ERROR("Unable to decode CDO partition '%s'", header->partition->section->Name.c_str()); + } /* Enable the search for sync points - only needs to be done for SSIT devices */ search_for_sync_points(); From f24a444a7b338666cae1980af9b9ea481412a707 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 24 Aug 2026 18:17:29 -0500 Subject: [PATCH 43/45] Harden encrypted CDO decoding in sibling families --- CMakeLists.txt | 20 +++++++ spartanup/src/partition-spartanup.cpp | 5 +- tests/cmake/versal_cdo_smoke.cmake | 58 ++++++++++--------- .../src/partition-versal_2ve_2vm.cpp | 4 ++ 4 files changed, 59 insertions(+), 28 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 88eb430..6148cd9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -324,6 +324,9 @@ if(BOOTGEN_BUILD_TESTS) -DBOOTGEN=$ -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/versal-cdo-smoke + -DARCH=versal + -DEXPECT_READ_CDO_PARTITION=ON + -DEXPECT_LEGACY_FAILURE=ON -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/versal_cdo_smoke.cmake) add_test( NAME bootgen.versal-2ve-2vm-smoke @@ -334,6 +337,15 @@ if(BOOTGEN_BUILD_TESTS) -DARCH=versal_2ve_2vm -DEXPECTED_READ_OUTPUT=partition_type[elf] -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/versal_smoke.cmake) + add_test( + NAME bootgen.versal-2ve-2vm-cdo-smoke + COMMAND ${CMAKE_COMMAND} + -DBOOTGEN=$ + -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures + -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/versal-2ve-2vm-cdo-smoke + -DARCH=versal_2ve_2vm + -DEXPECT_READ_CDO_PARTITION=ON + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/versal_cdo_smoke.cmake) add_test( NAME bootgen.versal-2ve-2vm-lms-keygen COMMAND ${CMAKE_COMMAND} @@ -361,6 +373,14 @@ if(BOOTGEN_BUILD_TESTS) -DARCH=spartanup -DEXPECTED_READ_OUTPUT=plm_length -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/versal_smoke.cmake) + add_test( + NAME bootgen.spartanup-cdo-smoke + COMMAND ${CMAKE_COMMAND} + -DBOOTGEN=$ + -DFIXTURE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures + -DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/tests/spartanup-cdo-smoke + -DARCH=spartanup + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/cmake/versal_cdo_smoke.cmake) add_test( NAME bootgen.spartanup-auth-smoke COMMAND ${CMAKE_COMMAND} diff --git a/spartanup/src/partition-spartanup.cpp b/spartanup/src/partition-spartanup.cpp index 9ac25e8..295a6cc 100644 --- a/spartanup/src/partition-spartanup.cpp +++ b/spartanup/src/partition-spartanup.cpp @@ -826,6 +826,10 @@ void SpartanupPartition::Build(BootImage& bi, Binary& cache) CdoSequence * cdo_seq; cdo_seq = decode_cdo_binary(header->partition->section->Data.get(), header->partition->section->Length); + if (cdo_seq == NULL) + { + LOG_ERROR("Unable to decode CDO partition '%s'", header->partition->section->Name.c_str()); + } /* Enable the search for sync points - only needs to be done for SSIT devices */ search_for_sync_points(); @@ -1124,4 +1128,3 @@ void SpartanupPartition::Link(BootImage &bi) } } } - diff --git a/tests/cmake/versal_cdo_smoke.cmake b/tests/cmake/versal_cdo_smoke.cmake index 4904ad2..8bb187f 100644 --- a/tests/cmake/versal_cdo_smoke.cmake +++ b/tests/cmake/versal_cdo_smoke.cmake @@ -1,5 +1,5 @@ -if(NOT DEFINED BOOTGEN OR NOT DEFINED FIXTURE_DIR OR NOT DEFINED OUTPUT_DIR) - message(FATAL_ERROR "BOOTGEN, FIXTURE_DIR, and OUTPUT_DIR are required") +if(NOT DEFINED BOOTGEN OR NOT DEFINED FIXTURE_DIR OR NOT DEFINED OUTPUT_DIR OR NOT DEFINED ARCH) + message(FATAL_ERROR "BOOTGEN, FIXTURE_DIR, OUTPUT_DIR, and ARCH are required") endif() file(MAKE_DIRECTORY "${OUTPUT_DIR}") @@ -7,46 +7,50 @@ set(TEST_FIXTURE_DIR "${OUTPUT_DIR}/fixture") file(MAKE_DIRECTORY "${TEST_FIXTURE_DIR}") file(COPY "${FIXTURE_DIR}/zynq-smoke.elf" DESTINATION "${TEST_FIXTURE_DIR}") file(WRITE "${TEST_FIXTURE_DIR}/minimal.cdo" "version 2.0\nwrite 0xf1260050 0\n") -file(WRITE "${TEST_FIXTURE_DIR}/versal-cdo.bif" "all:\n{\n image\n {\n name = image1, id = 0x1c000001\n { type = bootloader, file = zynq-smoke.elf }\n { type = cdo, file = minimal.cdo }\n }\n}\n") +file(WRITE "${TEST_FIXTURE_DIR}/versal-cdo.bif" "all:\n{\n image\n {\n name = image1, id = 0x1c000001\n { type = bootloader, encryption = aes, keysrc = bbram_red_key, aeskeyfile = boot.nky, file = zynq-smoke.elf }\n { type = cdo, encryption = aes, keysrc = bbram_red_key, aeskeyfile = cdo.nky, file = minimal.cdo }\n }\n}\n") set(BOOT_IMAGE "${OUTPUT_DIR}/BOOT.BIN") execute_process( - COMMAND "${BOOTGEN}" -arch versal -image "${TEST_FIXTURE_DIR}/versal-cdo.bif" -o "${BOOT_IMAGE}" -w on + COMMAND "${BOOTGEN}" -arch "${ARCH}" -image "${TEST_FIXTURE_DIR}/versal-cdo.bif" -o "${BOOT_IMAGE}" -w on -p xcvc1902 WORKING_DIRECTORY "${TEST_FIXTURE_DIR}" RESULT_VARIABLE CREATE_RESULT OUTPUT_VARIABLE CREATE_OUTPUT ERROR_VARIABLE CREATE_ERROR) if(NOT CREATE_RESULT EQUAL 0) - message(FATAL_ERROR "Versal CDO image creation failed (${CREATE_RESULT}):\n${CREATE_OUTPUT}\n${CREATE_ERROR}") + message(FATAL_ERROR "${ARCH} CDO image creation failed (${CREATE_RESULT}):\n${CREATE_OUTPUT}\n${CREATE_ERROR}") endif() file(SIZE "${BOOT_IMAGE}" BOOT_IMAGE_SIZE) if(BOOT_IMAGE_SIZE LESS 512) - message(FATAL_ERROR "Versal CDO smoke image is unexpectedly small: ${BOOT_IMAGE_SIZE} bytes") + message(FATAL_ERROR "${ARCH} CDO smoke image is unexpectedly small: ${BOOT_IMAGE_SIZE} bytes") endif() -execute_process( - COMMAND "${BOOTGEN}" -arch versal -read "${BOOT_IMAGE}" - RESULT_VARIABLE READ_RESULT - OUTPUT_VARIABLE READ_OUTPUT - ERROR_VARIABLE READ_ERROR) -if(NOT READ_RESULT EQUAL 0) - message(FATAL_ERROR "Versal CDO image inspection failed (${READ_RESULT}):\n${READ_OUTPUT}\n${READ_ERROR}") -endif() +if(DEFINED EXPECT_READ_CDO_PARTITION AND EXPECT_READ_CDO_PARTITION) + execute_process( + COMMAND "${BOOTGEN}" -arch "${ARCH}" -read "${BOOT_IMAGE}" + RESULT_VARIABLE READ_RESULT + OUTPUT_VARIABLE READ_OUTPUT + ERROR_VARIABLE READ_ERROR) + if(NOT READ_RESULT EQUAL 0) + message(FATAL_ERROR "${ARCH} CDO image inspection failed (${READ_RESULT}):\n${READ_OUTPUT}\n${READ_ERROR}") + endif() -string(FIND "${READ_OUTPUT}" "partition_type[cdo]" CDO_OFFSET) -if(CDO_OFFSET EQUAL -1) - message(FATAL_ERROR "Versal CDO smoke image does not contain a CDO partition:\n${READ_OUTPUT}") + string(FIND "${READ_OUTPUT}" "partition_type[cdo]" CDO_OFFSET) + if(CDO_OFFSET EQUAL -1) + message(FATAL_ERROR "${ARCH} CDO smoke image does not contain a CDO partition:\n${READ_OUTPUT}") + endif() endif() -file(WRITE "${TEST_FIXTURE_DIR}/legacy.cdo" "version 1.0\nsection 0\nwrite 0xf1260050 0\n") -file(WRITE "${TEST_FIXTURE_DIR}/versal-legacy-cdo.bif" "all:\n{\n image\n {\n name = image1, id = 0x1c000001\n { type = bootloader, file = zynq-smoke.elf }\n { type = cdo, file = legacy.cdo }\n }\n}\n") -execute_process( - COMMAND "${BOOTGEN}" -arch versal -image "${TEST_FIXTURE_DIR}/versal-legacy-cdo.bif" -o "${OUTPUT_DIR}/legacy-BOOT.BIN" -w on - WORKING_DIRECTORY "${TEST_FIXTURE_DIR}" - RESULT_VARIABLE LEGACY_RESULT - OUTPUT_VARIABLE LEGACY_OUTPUT - ERROR_VARIABLE LEGACY_ERROR) -if(LEGACY_RESULT EQUAL 0 OR NOT LEGACY_RESULT MATCHES "^[1-9][0-9]*$") - message(FATAL_ERROR "Versal accepted or crashed on an unsupported legacy CDO (result: ${LEGACY_RESULT}):\n${LEGACY_OUTPUT}\n${LEGACY_ERROR}") +if(DEFINED EXPECT_LEGACY_FAILURE AND EXPECT_LEGACY_FAILURE) + file(WRITE "${TEST_FIXTURE_DIR}/legacy.cdo" "version 1.0\nsection 0\nwrite 0xf1260050 0\n") + file(WRITE "${TEST_FIXTURE_DIR}/versal-legacy-cdo.bif" "all:\n{\n image\n {\n name = image1, id = 0x1c000001\n { type = bootloader, file = zynq-smoke.elf }\n { type = cdo, file = legacy.cdo }\n }\n}\n") + execute_process( + COMMAND "${BOOTGEN}" -arch "${ARCH}" -image "${TEST_FIXTURE_DIR}/versal-legacy-cdo.bif" -o "${OUTPUT_DIR}/legacy-BOOT.BIN" -w on + WORKING_DIRECTORY "${TEST_FIXTURE_DIR}" + RESULT_VARIABLE LEGACY_RESULT + OUTPUT_VARIABLE LEGACY_OUTPUT + ERROR_VARIABLE LEGACY_ERROR) + if(LEGACY_RESULT EQUAL 0 OR NOT LEGACY_RESULT MATCHES "^[1-9][0-9]*$") + message(FATAL_ERROR "${ARCH} accepted or crashed on an unsupported legacy CDO (result: ${LEGACY_RESULT}):\n${LEGACY_OUTPUT}\n${LEGACY_ERROR}") + endif() endif() diff --git a/versal_2ve_2vm/src/partition-versal_2ve_2vm.cpp b/versal_2ve_2vm/src/partition-versal_2ve_2vm.cpp index b0f54de..43776ec 100644 --- a/versal_2ve_2vm/src/partition-versal_2ve_2vm.cpp +++ b/versal_2ve_2vm/src/partition-versal_2ve_2vm.cpp @@ -909,6 +909,10 @@ void Versal_2ve_2vmPartition::Build(BootImage& bi, Binary& cache) CdoSequence * cdo_seq; cdo_seq = decode_cdo_binary(header->partition->section->Data.get(), header->partition->section->Length); + if (cdo_seq == NULL) + { + LOG_ERROR("Unable to decode CDO partition '%s'", header->partition->section->Name.c_str()); + } /* Enable the search for sync points - only needs to be done for SSIT devices */ search_for_sync_points(); From d3ef97e9adec0278ef878563c8d0350d8f31559d Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Tue, 25 Aug 2026 04:11:15 -0500 Subject: [PATCH 44/45] Document signing macOS packages before CPack --- BUILDING.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 3b9a0b1..264a4c8 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -68,21 +68,31 @@ cmake --install build/macos-arm64 --prefix "$PWD/stage" ## macOS packaging and runtime dependencies The CMake `package` target creates an architecture-specific tarball containing -`bootgen`, its license notices, and build documentation: +`bootgen`, its license notices, and build documentation. Sign the executable +before invoking CPack so the signed Mach-O is the one installed into the +tarball: ```sh +file build/macos-arm64/bootgen +otool -L build/macos-arm64/bootgen +codesign --force --sign - build/macos-arm64/bootgen +codesign --verify --deep --strict build/macos-arm64/bootgen cmake --build build/macos-arm64 --target package ``` -Bootgen intentionally links to the caller-supplied OpenSSL installation; the -tarball does not bundle or sign OpenSSL. Install the matching-architecture -Homebrew `openssl@3` package before running it, then inspect the result: +For a release, replace `-` with the intended Developer ID identity. Verify the +artifact itself after extracting it; a signature applied after packaging does +not modify an existing tarball: ```sh -file build/macos-arm64/bootgen -otool -L build/macos-arm64/bootgen -codesign --force --sign - build/macos-arm64/bootgen -codesign --verify --deep --strict build/macos-arm64/bootgen +PACKAGE_DIR="$(mktemp -d)" +tar -xzf build/macos-arm64/bootgen-*-Darwin-arm64.tar.gz -C "$PACKAGE_DIR" +codesign --verify --deep --strict "$PACKAGE_DIR"/bootgen-*/bin/bootgen +``` + +Bootgen intentionally links to the caller-supplied OpenSSL installation; the +tarball does not bundle or sign OpenSSL. Install the matching-architecture +Homebrew `openssl@3` package before running it. ``` See `THIRD_PARTY_NOTICES.md` for the source and binary distribution inventory. From c3af7cdf221bd77ae9c7b586af56dc792c728b0a Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Tue, 25 Aug 2026 04:17:16 -0500 Subject: [PATCH 45/45] Fix macOS package signing documentation formatting --- BUILDING.md | 1 - 1 file changed, 1 deletion(-) diff --git a/BUILDING.md b/BUILDING.md index 264a4c8..c160a06 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -93,7 +93,6 @@ codesign --verify --deep --strict "$PACKAGE_DIR"/bootgen-*/bin/bootgen Bootgen intentionally links to the caller-supplied OpenSSL installation; the tarball does not bundle or sign OpenSSL. Install the matching-architecture Homebrew `openssl@3` package before running it. -``` See `THIRD_PARTY_NOTICES.md` for the source and binary distribution inventory. The package includes Bootgen's aggregate `LICENSE` and the separate Cisco