diff --git a/.github/workflows/signed-plugins.yml b/.github/workflows/signed-plugins.yml new file mode 100644 index 00000000000..402ba2c9a2a --- /dev/null +++ b/.github/workflows/signed-plugins.yml @@ -0,0 +1,138 @@ +name: Test Signed Plugins + +on: + push: + branches: [ develop ] + pull_request: + branches: [ develop ] + +env: + CTEST_OUTPUT_ON_FAILURE: 1 + +jobs: + # Test signature verification across platforms and configurations + test-signed-plugins: + name: "${{ matrix.config.name }}" + runs-on: ${{ matrix.config.os }} + permissions: + contents: read + strategy: + fail-fast: false + matrix: + config: + # Linux configurations + - name: "Linux Serial (Debug + Shared)" + os: ubuntu-latest + build_type: Debug + shared: ON + parallel: OFF + generator: "" + + - name: "Linux Serial (Release + Static)" + os: ubuntu-latest + build_type: Release + shared: OFF + parallel: OFF + generator: "" + + - name: "Linux Parallel (Debug + Shared)" + os: ubuntu-latest + build_type: Debug + shared: ON + parallel: ON + generator: "" + + # macOS configuration + - name: "macOS Serial (Release + Shared)" + os: macos-latest + build_type: Release + shared: ON + parallel: OFF + generator: "" + + # Windows configuration + - name: "Windows Serial (Release + Shared)" + os: windows-latest + build_type: Release + shared: ON + parallel: OFF + generator: "-A x64" + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + libssl-dev \ + zlib1g-dev \ + libaec-dev + + - name: Install MPI dependencies (Linux) + if: runner.os == 'Linux' && matrix.config.parallel == 'ON' + run: | + sudo apt-get install -y \ + libopenmpi-dev \ + openmpi-bin + + - name: Install dependencies (macOS) + if: runner.os == 'macOS' + run: | + brew install openssl@3 + + - name: Generate test RSA key pair (Unix) + if: runner.os != 'Windows' + run: | + openssl genrsa -out ci-test-private.pem 2048 + openssl rsa -in ci-test-private.pem -pubout -out ci-test-public.pem + mkdir -p ci-keystore + cp ci-test-public.pem ci-keystore/ + + - name: Generate test RSA key pair (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + & openssl genrsa -out ci-test-private.pem 2048 + & openssl rsa -in ci-test-private.pem -pubout -out ci-test-public.pem + New-Item -ItemType Directory -Force -Path ci-keystore + Copy-Item ci-test-public.pem ci-keystore/ + + - name: Configure CMake + shell: bash + run: | + EXTRA_FLAGS="" + if [ "${{ matrix.config.parallel }}" == "ON" ]; then + EXTRA_FLAGS="-DMPIEXEC_PREFLAGS=--oversubscribe" + fi + cmake -B build \ + ${{ matrix.config.generator }} \ + -DCMAKE_BUILD_TYPE=${{ matrix.config.build_type }} \ + -DHDF5_REQUIRE_SIGNED_PLUGINS:BOOL=ON \ + -DHDF5_PLUGIN_KEYSTORE_DIR="${PWD}/ci-keystore" \ + -DHDF5_ENABLE_PARALLEL:BOOL=${{ matrix.config.parallel }} \ + -DBUILD_SHARED_LIBS:BOOL=${{ matrix.config.shared }} \ + -DBUILD_STATIC_LIBS:BOOL=ON \ + -DBUILD_TESTING:BOOL=ON \ + -DHDF5_BUILD_TOOLS:BOOL=ON \ + -DHDF5_ENABLE_ZLIB_SUPPORT:BOOL=${{ runner.os == 'Linux' }} \ + -DHDF5_ENABLE_SZIP_SUPPORT:BOOL=${{ runner.os == 'Linux' }} \ + $EXTRA_FLAGS + + - name: Build + run: cmake --build build --parallel 4 --config ${{ matrix.config.build_type }} + + - name: Copy OpenSSL DLLs (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Copy-Item "C:\Program Files\OpenSSL\bin\libcrypto-3-x64.dll" build\bin\${{ matrix.config.build_type }}\ + Copy-Item "C:\Program Files\OpenSSL\bin\libssl-3-x64.dll" build\bin\${{ matrix.config.build_type }}\" + + - name: Run Tests + shell: bash + run: | + cd build + ctest --build-config ${{ matrix.config.build_type }} --parallel 4 --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index fe35faaa31f..cfc428e9535 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -781,6 +781,88 @@ if (HDF5_ENABLE_HDFS) endif () endif () +#----------------------------------------------------------------------------- +# Option to Require Digitally Signed plugins +#----------------------------------------------------------------------------- +option (HDF5_REQUIRE_SIGNED_PLUGINS "Require digitally signed plugins" OFF) + +cmake_dependent_option (HDF5_LOCK_PLUGIN_KEYSTORE "Disable HDF5_PLUGIN_KEYSTORE environment variable override (security hardening)" OFF "HDF5_REQUIRE_SIGNED_PLUGINS" OFF) +mark_as_advanced(HDF5_LOCK_PLUGIN_KEYSTORE) + +if (HDF5_REQUIRE_SIGNED_PLUGINS) + # KeyStore directory for multiple trusted public keys + set(HDF5_PLUGIN_KEYSTORE_DIR "" CACHE PATH + "Directory containing trusted public keys (.pem files) for plugin verification") + # Find OpenSSL for RSA signature verification + find_package(OpenSSL REQUIRED) + if (NOT OPENSSL_FOUND) + message(FATAL_ERROR "OpenSSL is required for HDF5_REQUIRE_SIGNED_PLUGINS but was not found") + endif () + + # Check minimum OpenSSL version + # The signature verification implementation uses modern EVP API (EVP_DigestVerifyInit, + # EVP_DigestVerifyUpdate, EVP_DigestVerifyFinal) which requires OpenSSL 1.1.0+ + if (OPENSSL_VERSION VERSION_LESS "1.1.0") + message(FATAL_ERROR + "OpenSSL 1.1.0 or later is required for HDF5_REQUIRE_SIGNED_PLUGINS\n" + " Found: OpenSSL ${OPENSSL_VERSION}\n" + " Required: OpenSSL 1.1.0 or later\n" + "\n" + "The signature verification implementation uses modern EVP API which is not\n" + "available in OpenSSL 1.0.2 and earlier versions.\n" + "\n" + "Solutions:\n" + " 1. Upgrade to OpenSSL 3.0 or later (recommended)\n" + " - OpenSSL 3.0 is LTS (supported until 2026-09-07)\n" + " - OpenSSL 3.4+ is also supported\n" + " 2. Use LibreSSL 2.7.0 or later (compatible alternative)\n" + " 3. Disable signed plugins: -DHDF5_REQUIRE_SIGNED_PLUGINS=OFF\n" + "\n" + "Note: OpenSSL 1.0.2 reached end-of-life in December 2019\n" + " CentOS 7 users should install openssl11 package") + endif () + + # Informational message for OpenSSL 3.0+ (APIs are compatible, not deprecated) + if (OPENSSL_VERSION VERSION_GREATER_EQUAL "3.0.0") + message(STATUS "OpenSSL 3.0+ detected - all EVP_* APIs are compatible (not deprecated)") + endif () + + # KeyStore directory is optional at build time; the HDF5_PLUGIN_KEYSTORE + # environment variable can be used at runtime instead. Require a compile-time + # directory only when the environment variable override is locked out. + if (HDF5_LOCK_PLUGIN_KEYSTORE AND NOT HDF5_PLUGIN_KEYSTORE_DIR) + message(FATAL_ERROR + "HDF5_LOCK_PLUGIN_KEYSTORE=ON requires a compile-time KeyStore directory:\n" + " -DHDF5_PLUGIN_KEYSTORE_DIR=/etc/hdf5/trusted_keys") + endif () + + # Configure KeyStore directory if provided. + # Note: the path is embedded as a string literal in the library binary. + # Use the HDF5_PLUGIN_KEYSTORE environment variable instead if the path + # should not be visible in the binary. + if (HDF5_PLUGIN_KEYSTORE_DIR) + add_compile_definitions(H5PL_KEYSTORE_DIR="${HDF5_PLUGIN_KEYSTORE_DIR}") + else () + message(NOTICE "No compile-time KeyStore directory configured; " + "set HDF5_PLUGIN_KEYSTORE environment variable at runtime.") + endif () + + # Enable digital signature verification (goes into H5pubconf.h) + set(H5_REQUIRE_DIGITAL_SIGNATURE 1) + + # Security: Disable environment variable override if requested + if (HDF5_LOCK_PLUGIN_KEYSTORE) + add_compile_definitions(H5PL_DISABLE_ENV_KEYSTORE) + message(VERBOSE "HDF5_PLUGIN_KEYSTORE environment variable override: DISABLED (security hardening)") + endif () + + # Add OpenSSL to link libraries for the HDF5 library + # Only libcrypto is needed (EVP, PEM, BIO, ERR APIs); libssl (TLS) is not used + list(APPEND LINK_LIBS OpenSSL::Crypto) + + message(VERBOSE "Digital signature verification enabled (OpenSSL ${OPENSSL_VERSION})") +endif () + #----------------------------------------------------------------------------- # Option to Enable MPI Parallel #----------------------------------------------------------------------------- diff --git a/config/cmake/SignPlugin.cmake b/config/cmake/SignPlugin.cmake new file mode 100644 index 00000000000..814cbb81c87 --- /dev/null +++ b/config/cmake/SignPlugin.cmake @@ -0,0 +1,52 @@ +# +# Copyright by The HDF Group. +# All rights reserved. +# +# This file is part of HDF5. The full HDF5 copyright notice, including +# terms governing use, modification, and redistribution, is contained in +# the COPYING file, which can be found at the root of the source code +# distribution tree, or in https://www.hdfgroup.org/licenses. +# If you do not have access to either file, you may request a copy from +# help@hdfgroup.org. +# + +#[=======================================================================[.rst: +SignPlugin +---------- + +Provides a CMake function to sign plugin libraries when HDF5_REQUIRE_SIGNED_PLUGINS is enabled. + +.. command:: sign_plugin_target + + Signs a plugin target using the h5sign tool. + + .. code-block:: cmake + + sign_plugin_target( ) + + ``target`` + The CMake target to sign (must be a shared library plugin) + + ``plugin_dir`` + The directory where the plugin will be located after build + + This function adds a post-build command that: + - Signs the plugin using the h5sign tool + - Uses the test private key (${CMAKE_BINARY_DIR}/private.pem) + - Only executes if HDF5_REQUIRE_SIGNED_PLUGINS is enabled + +#]=======================================================================] + +function(sign_plugin_target TARGET PLUGIN_DIR) + if (HDF5_REQUIRE_SIGNED_PLUGINS) + add_dependencies(${TARGET} h5sign) + add_custom_command( + TARGET ${TARGET} + POST_BUILD + COMMAND $ + ARGS -p "${PLUGIN_DIR}/$" + -k "${CMAKE_BINARY_DIR}/private.pem" + COMMENT "Signing test plugin ${TARGET} for signature verification" + ) + endif() +endfunction() diff --git a/config/cmake/runExecute.cmake b/config/cmake/runExecute.cmake index 4c4f0ea7669..474b11d2bf3 100644 --- a/config/cmake/runExecute.cmake +++ b/config/cmake/runExecute.cmake @@ -47,7 +47,7 @@ macro (STREAM_STRINGS stream strings_out) endmacro() macro (EXECUTE_TEST) - cmake_parse_arguments (TEST "" "NOERRDISPLAY;EXPECT;JAVA;CLASSPATH;PROGRAM;FOLDER;OUTPUT;LIBRARY_DIRECTORY;INPUT;ENV_VAR;ENV_VALUE;EMULATOR;ARGS" "TEST_" ${ARGN}) + cmake_parse_arguments (TEST "" "NOERRDISPLAY;EXPECT;JAVA;CLASSPATH;PROGRAM;FOLDER;OUTPUT;LIBRARY_DIRECTORY;INPUT;ENV_VAR;ENV_VALUE;KEYSTORE_DIR;EMULATOR;ARGS" "TEST_" ${ARGN}) if (NOT TEST_PROGRAM) message (FATAL_ERROR "Require TEST_PROGRAM to be defined") endif () @@ -88,6 +88,11 @@ if (TEST_ENV_VAR) message (TRACE "ENV:${TEST_ENV_VAR}=$ENV{${TEST_ENV_VAR}}") endif () +if (TEST_KEYSTORE_DIR) + set (ENV{HDF5_PLUGIN_KEYSTORE} "${TEST_KEYSTORE_DIR}") + message (TRACE "ENV:HDF5_PLUGIN_KEYSTORE=$ENV{HDF5_PLUGIN_KEYSTORE}") +endif () + if (NOT TEST_JAVA) message (STATUS "COMMAND: ${TEST_EMULATOR} ${TEST_PROGRAM} ${TEST_ARGS}") if (NOT TEST_INPUT) diff --git a/config/cmake/runTest.cmake b/config/cmake/runTest.cmake index c596c9481d8..5a0172880bf 100644 --- a/config/cmake/runTest.cmake +++ b/config/cmake/runTest.cmake @@ -42,6 +42,7 @@ EXECUTE_TEST (TEST_FOLDER ${TEST_FOLDER} TEST_LIBRARY_DIRECTORY ${TEST_LIBRARY_DIRECTORY} TEST_ENV_VAR ${TEST_ENV_VAR} TEST_ENV_VALUE ${TEST_ENV_VALUE} + TEST_KEYSTORE_DIR ${TEST_KEYSTORE_DIR} TEST_INPUT ${TEST_INPUT} TEST_CLASSPATH ${TEST_CLASSPATH} TEST_NOERRDISPLAY ${TEST_NOERRDISPLAY} diff --git a/release_docs/CHANGELOG.md b/release_docs/CHANGELOG.md index 7d77b2d64e2..1d9a529263f 100644 --- a/release_docs/CHANGELOG.md +++ b/release_docs/CHANGELOG.md @@ -92,6 +92,10 @@ We would like to thank the many HDF5 community members who contributed to this r ## Library +### Added optional digital signature verification for dynamically loaded plugins + + When built with `-DHDF5_REQUIRE_SIGNED_PLUGINS=ON` and OpenSSL, HDF5 will cryptographically verify each plugin before loading it. Plugins are signed with the new `h5sign` tool, which appends an RSA signature and a compact footer to the plugin binary. Verification uses a keystore directory of trusted public keys, configurable at compile time (`-DHDF5_PLUGIN_KEYSTORE_DIR=`) or at runtime via the `HDF5_PLUGIN_KEYSTORE` environment variable. Individual signatures can be revoked without removing the entire public key by listing their SHA-256 hashes in a `revoked_signatures.txt` file in the keystore directory. Supported algorithms include SHA-256, SHA-384, and SHA-512 with both PKCS#1 v1.5 and PSS padding. See `release_docs/PLUGIN_SIGNATURE_README.md` for details. + ### Improve performance of H5Ovisit() with deeply nested group structures `H5Ovisit()` would previously internally traverse each object's path name from the iteration root group in order to retrieve information about that object, causing severe performance degradation with a deeply nested group structure. Modified the algorithm to instead retrieve information directly from the object. To get this benefit, users should use `H5Ovisit3()`, or use `H5Ovisit2()` with neither `H5O_INFO_HDR` nor `H5O_INFO_META_SIZE` selected in the `fields` parameter. Performance of `H5Ocopy()`, `H5Iget_name()`, and external links with a callback set should also improve in similar situations. @@ -106,6 +110,10 @@ We would like to thank the many HDF5 community members who contributed to this r ## Tools +### Added `h5sign` tool for signing plugins with RSA digital signatures + + The `h5sign` command-line tool signs HDF5 plugin shared libraries by appending an RSA signature and a 12-byte footer. It supports SHA-256, SHA-384, SHA-512, and their PSS variants, and accepts passphrase-protected private keys. Use `-f` / `--force` to strip an existing signature before re-signing. The tool is built automatically when `HDF5_REQUIRE_SIGNED_PLUGINS` is enabled. + ## High-Level APIs ## C Packet Table API diff --git a/release_docs/PLUGIN_SIGNATURE_README.md b/release_docs/PLUGIN_SIGNATURE_README.md new file mode 100644 index 00000000000..78b5421b3e2 --- /dev/null +++ b/release_docs/PLUGIN_SIGNATURE_README.md @@ -0,0 +1,277 @@ +# HDF5 Plugin Digital Signature Guide + +## Table of Contents + +1. [Overview](#overview) +2. [Quick Start](#quick-start) +3. [For Plugin Users](#for-plugin-users) +4. [For Plugin Developers](#for-plugin-developers) +5. [Security Considerations](#security-considerations) +6. [Troubleshooting](#troubleshooting) +7. [Technical Details](#technical-details) + +--- + +## Overview + +HDF5 plugin digital signatures provide cryptographic verification of plugin authenticity and integrity. When enabled, HDF5 verifies that each plugin was signed by a trusted developer before loading it. + +### Key Features + +- RSA-based digital signatures (4096-bit recommended, 2048-bit minimum) +- Multiple hash algorithms (SHA-256, SHA-384, SHA-512) with PSS padding support +- Multi-key keystore for accepting plugins from multiple trusted developers +- Plugins verified once per load (already cached by plugin loader) + +--- + +## Quick Start + +### For Plugin Users + +1. Obtain the public key from your plugin developer +2. Create a keystore directory and place the public key in it +3. Set the `HDF5_PLUGIN_KEYSTORE` environment variable to the keystore directory path +4. Use HDF5 normally — signed plugins are verified automatically + +### For Plugin Developers + +1. Generate an RSA key pair (see OpenSSL documentation) +2. Build your plugin as usual +3. Sign your plugin with `h5sign -p my_plugin.so -k my_private_key.pem` +4. Distribute the signed plugin and public key to users + +--- + +## For Plugin Users + +### Setting Up the Keystore + +1. Obtain the public key from your plugin developer through a trusted channel +2. Create a directory to serve as your keystore +3. Place the public key `.pem` file(s) in the keystore directory +4. Set the `HDF5_PLUGIN_KEYSTORE` environment variable to the keystore path + +The keystore can contain public keys from multiple developers. HDF5 will try +all keys and accept the plugin if any key verifies successfully. + +### Compile-Time Keystore + +Alternatively, the keystore path can be set at compile time: + +```bash +cmake -DHDF5_REQUIRE_SIGNED_PLUGINS=ON \ + -DHDF5_PLUGIN_KEYSTORE_DIR=/path/to/keystore \ + /path/to/hdf5/source +``` + +If `HDF5_PLUGIN_KEYSTORE_DIR` is set at compile time, it is used as the +default. The `HDF5_PLUGIN_KEYSTORE` environment variable takes precedence +at runtime unless the keystore is locked (see below). + +### Locking the Keystore + +To prevent runtime override of the keystore path via environment variable, +build with `-DHDF5_LOCK_PLUGIN_KEYSTORE=ON`. When locked, only the +compile-time `HDF5_PLUGIN_KEYSTORE_DIR` is used. + +--- + +## For Plugin Developers + +### Key Generation + +Generate an RSA key pair using OpenSSL (4096-bit recommended). Refer to the +[OpenSSL documentation](https://www.openssl.org/docs/) for key generation +commands and best practices. + +**Key security**: Store your private key securely and never share it. + +### Signing Plugins + +Use the `h5sign` tool to add a digital signature: + +```bash +# Basic signing (defaults to SHA-512) +h5sign -p my_plugin.so -k my_private_key.pem + +# Choose a specific algorithm +h5sign -p my_plugin.so -k my_private_key.pem -a sha256 + +# PSS padding variant +h5sign -p my_plugin.so -k my_private_key.pem -a sha512-pss + +# Verbose output +h5sign -p my_plugin.so -k my_private_key.pem -v +``` + +The tool appends the RSA signature and a 12-byte footer to the end of the +plugin file. The binary loader ignores trailing data, so the signed plugin +loads normally on all platforms. + +### Re-signing a Plugin + +To update the signature (e.g., after rebuilding or to change the algorithm): + +```bash +h5sign -p my_plugin.so -k my_private_key.pem -f +``` + +The `-f` / `--force` flag strips the existing signature before re-signing. + +### Distributing Plugins + +Provide users with: + +1. The signed plugin binary +2. Your public key (`.pem` file) +3. Instructions to add the public key to their keystore + +### Passphrase-Protected Keys + +h5sign supports passphrase-protected private keys. OpenSSL will prompt for the +passphrase interactively. + +--- + +## Security Considerations + +### Key Management + +Public keys in the keystore are not secret, but their integrity must be +protected — anyone who can add a key to the keystore can make HDF5 trust +their plugins. + +Plugin developers are responsible for keeping their private keys secure. + +### Security Model + +HDF5 plugin signatures protect against: + +- **Unsigned malicious plugins**: Blocked (signature required) +- **Tampered plugins**: Detected (signature invalidates) +- **Untrusted sources**: Rejected (keystore verification) + +### Signature Revocation + +Individual signatures can be revoked without removing the entire public key. +Place a file named `revoked_signatures.txt` in the keystore directory. Each +line is the 64-character hex-encoded SHA-256 hash of the raw signature bytes +to revoke. Lines starting with `#` are comments; empty lines are ignored. + +```text +# Example revoked_signatures.txt +# SHA-256 hash of a compromised plugin's signature +a1b2c3d4e5f6... (64 hex characters) +``` + +The revocation file is optional. If absent, no signatures are revoked. + +### Known Limitations + +- **No rollback protection**: Signatures prove authenticity, not freshness +- **No expiration**: Signed plugins remain valid indefinitely +- **Manual trust management**: Users must manage keystore contents + +### Air-Gapped Environments + +All cryptographic operations are performed locally using OpenSSL. No +internet connectivity is required for signing or verification. + +--- + +## Troubleshooting + +### Common Error Messages + +| Error | Cause | Solution | +| ----- | ----- | -------- | +| "keystore is empty" | No public keys in keystore directory | Add the developer's public key to the keystore | +| "plugin signature verification failed" | Wrong key, tampered plugin, or corrupted download | Verify you have the correct public key; re-download the plugin | +| "plugin signature has been revoked" | Signature listed in `revoked_signatures.txt` | Remove the hash from the revocation file, or re-sign the plugin | +| "invalid signature magic number" | Plugin is not signed | Sign the plugin with `h5sign` | +| Keystore not found | `HDF5_PLUGIN_KEYSTORE` not set or path does not exist | Set the environment variable to a valid keystore directory | + +### Verification Test Suite + +`h5signverifytest` is the HDF5 internal test harness for the plugin signature +system. It is intended for HDF5 developers only and requires pre-generated test +data from the HDF5 build tree. + +--- + +## Technical Details + +### Signature Format + +Signed plugins have this structure: + +```text ++-----------------------------+ +| Original Plugin Binary | +| (unchanged) | ++-----------------------------+ +| RSA Signature (256-1024B) | ++-----------------------------+ +| Footer (12 bytes): | +| - Magic: HDF5 (4B) | +| - Signature length (4B) | +| - Algorithm ID (1B) | +| - Format version (1B) | +| - Reserved (2B) | ++-----------------------------+ +``` + +### Supported Algorithms + +| Algorithm | Padding | Security Level | +| --------- | ------- | -------------- | +| SHA-256 | PKCS#1 v1.5 | Good | +| SHA-384 | PKCS#1 v1.5 | Better | +| SHA-512 (default) | PKCS#1 v1.5 | Best | +| SHA-256-PSS | PSS | Enhanced | +| SHA-384-PSS | PSS | Enhanced | +| SHA-512-PSS | PSS | Maximum | + +### Performance + +Verification time is dominated by I/O to read the plugin file for hashing, +plus ~1-5ms for the RSA operation. Plugins are cached by the HDF5 plugin +loader, so each plugin is verified only once per process. + +--- + +## FAQ + +**Q: Do I need to sign plugins?** +A: Only if HDF5 was built with `HDF5_REQUIRE_SIGNED_PLUGINS=ON`. Otherwise, +signing is optional but recommended. + +**Q: Can I use the same key for multiple plugins?** +A: Yes. Users only need your single public key. + +**Q: What happens if verification fails?** +A: HDF5 refuses to load the plugin and returns an error. + +**Q: Does signing increase plugin size?** +A: Minimally — 256-512 bytes for the signature plus 12 bytes for the footer. + +**Q: Are signatures platform-specific?** +A: No. A signed plugin retains its signature across platforms (though the +plugin binary itself may be platform-specific). + +**Q: Do signatures work in air-gapped environments?** +A: Yes. All operations are local; no internet required. + +--- + +## Additional Resources + +- **OpenSSL Documentation**: +- **HDF5 Plugin Documentation**: + +--- + +**Document Version**: 1.2 +**Last Updated**: 2026-03-20 +**HDF5 Version**: 2.2.0+ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b5b1245ca60..8ae3b0becc1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -705,6 +705,7 @@ set (H5PL_SOURCES ${HDF5_SRC_DIR}/H5PLint.c ${HDF5_SRC_DIR}/H5PLpath.c ${HDF5_SRC_DIR}/H5PLplugin_cache.c + ${HDF5_SRC_DIR}/H5PLsig.c ) set (H5PL_PUBLIC_HDRS @@ -716,6 +717,7 @@ IDE_GENERATED_PROPERTIES ("H5PL" "${H5PL_HDRS}" "${H5PL_SOURCES}" ) set (H5PL_PRIVATE_HDRS ${HDF5_SRC_DIR}/H5PLpkg.h ${HDF5_SRC_DIR}/H5PLprivate.h + ${HDF5_SRC_DIR}/H5PLsig.h ) diff --git a/src/H5.c b/src/H5.c index 8b8748b5b81..fd68e042f03 100644 --- a/src/H5.c +++ b/src/H5.c @@ -209,6 +209,7 @@ H5_init_library(void) H5_debug_g.pkg[H5_PKG_MM].name = "mm"; H5_debug_g.pkg[H5_PKG_O].name = "o"; H5_debug_g.pkg[H5_PKG_P].name = "p"; + H5_debug_g.pkg[H5_PKG_PL].name = "pl"; H5_debug_g.pkg[H5_PKG_S].name = "s"; H5_debug_g.pkg[H5_PKG_T].name = "t"; H5_debug_g.pkg[H5_PKG_V].name = "v"; diff --git a/src/H5PLint.c b/src/H5PLint.c index 18c759c5096..35c02ea8011 100644 --- a/src/H5PLint.c +++ b/src/H5PLint.c @@ -195,6 +195,12 @@ H5PL_term_package(void) if (H5PL__close_path_table() < 0) HGOTO_ERROR(H5E_PLUGIN, H5E_CANTFREE, (-1), "problem closing search path table"); +#ifdef H5_REQUIRE_DIGITAL_SIGNATURE + /* Clean up signature verification resources */ + if (H5PL__cleanup_signature_resources() < 0) + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTFREE, (-1), "problem cleaning up signature resources"); +#endif + /* Mark the interface as uninitialized */ if (0 == ret_value) H5_PKG_INIT_VAR = false; @@ -340,6 +346,12 @@ H5PL__open(const char *path, H5PL_type_t type, const H5PL_key_t *key, bool *succ if (plugin_type) *plugin_type = H5PL_TYPE_ERROR; +#ifdef H5_REQUIRE_DIGITAL_SIGNATURE + /* Verify plugin signature before loading */ + if (H5PL__verify_signature_appended(path) < 0) + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTGET, FAIL, "plugin signature verification failed for: %s", path); +#endif + /* There are different reasons why a library can't be open, e.g. wrong architecture. * If we can't open the library, just return. */ diff --git a/src/H5PLpkg.h b/src/H5PLpkg.h index f7830504df9..cd4c6eec1df 100644 --- a/src/H5PLpkg.h +++ b/src/H5PLpkg.h @@ -108,6 +108,28 @@ typedef H5PL_type_t (*H5PL_get_plugin_type_t)(void); typedef const void *(*H5PL_get_plugin_info_t)(void); #endif /* H5_HAVE_WIN32_API */ +/************************************/ +/* Digital Signature Platform Macros */ +/************************************/ +#ifdef H5_REQUIRE_DIGITAL_SIGNATURE + +/* Keystore directory string for error messages */ +#ifdef H5PL_KEYSTORE_DIR +#define H5PL_SIG_KEYSTORE_DIR_STR H5PL_KEYSTORE_DIR +#else +#define H5PL_SIG_KEYSTORE_DIR_STR "(not configured)" +#endif + +#define H5PL_SIG_DEBUG_PRINT(...) \ + do { \ + if (H5DEBUG(PL)) { \ + fprintf(H5DEBUG(PL), __VA_ARGS__); \ + fflush(H5DEBUG(PL)); \ + } \ + } while (0) + +#endif /* H5_REQUIRE_DIGITAL_SIGNATURE */ + /****************************/ /* Package Private Typedefs */ /****************************/ @@ -156,4 +178,10 @@ H5_DLL herr_t H5PL__path_table_iterate(H5PL_iterate_type_t iter_type, H5PL_itera H5_DLL herr_t H5PL__find_plugin_in_path_table(const H5PL_search_params_t *search_params, bool *found /*out*/, const void **plugin_info /*out*/); +/* Digital signature verification */ +#ifdef H5_REQUIRE_DIGITAL_SIGNATURE +H5_DLL herr_t H5PL__verify_signature_appended(const char *plugin_path); +H5_DLL herr_t H5PL__cleanup_signature_resources(void); +#endif + #endif /* H5PLpkg_H */ diff --git a/src/H5PLsig.c b/src/H5PLsig.c new file mode 100644 index 00000000000..cc6b890762d --- /dev/null +++ b/src/H5PLsig.c @@ -0,0 +1,1311 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Copyright by The HDF Group. * + * All rights reserved. * + * * + * This file is part of HDF5. The full HDF5 copyright notice, including * + * terms governing use, modification, and redistribution, is contained in * + * the LICENSE file, which can be found at the root of the source code * + * distribution tree, or in https://www.hdfgroup.org/licenses. * + * If you do not have access to either file, you may request a copy from * + * help@hdfgroup.org. * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +/* + * Purpose: Digital signature verification for HDF5 plugins + */ + +/****************/ +/* Module Setup */ +/****************/ + +#include "H5PLmodule.h" /* This source code file is part of the H5PL module */ + +/***********/ +/* Headers */ +/***********/ +#include "H5private.h" /* Generic Functions */ +#include "H5Eprivate.h" /* Error handling */ +#include "H5PLpkg.h" /* Plugin */ +#include "H5PLsig.h" /* Signature format */ +#include "H5MMprivate.h" /* Memory management */ +#include "H5encode.h" /* Endianness conversion */ + +#ifdef H5_REQUIRE_DIGITAL_SIGNATURE + +#include +#include +#include +#include + +/* For directory operations */ +#ifndef H5_HAVE_WIN32_API +#include +#else +/* S_ISDIR may not be defined on Windows */ +#ifndef S_ISDIR +#define S_ISDIR(m) (((m) & _S_IFMT) == _S_IFDIR) +#endif +#endif + +/*******************/ +/* Local Variables */ +/*******************/ + +/* + * Thread Safety Note: + * All file-scope static variables below (keystore and revocation list) + * are accessed without explicit synchronization. When HDF5_ENABLE_THREADSAFE is enabled, + * these variables are protected by the HDF5 library-wide global lock that guards plugin + * operations. Concurrent plugin loads are serialized at the H5PL__load level, ensuring + * that keystore initialization and revocation list checks cannot race. + */ + +/* KeyStore entry for storing multiple trusted public keys */ +typedef struct H5PL_keystore_entry_t { + EVP_PKEY *key; /* OpenSSL public key object */ + char *source; /* Key source (filename or "embedded") for debugging */ +} H5PL_keystore_entry_t; + +/* KeyStore for signature verification + * TODO (Thread Safety): Requires mutex protection if global lock is removed + */ +static H5PL_keystore_entry_t *H5PL_keystore_g = NULL; +static size_t H5PL_keystore_count_g = 0; +static size_t H5PL_keystore_capacity_g = 0; +static bool H5PL_keystore_initialized_g = false; + +/* Revocation list for blocking specific signatures. + * The file /H5PL_REVOKED_SIGS_FILENAME is read at keystore + * init time. Each line is a 64-hex-char SHA-256 hash of a signature blob. + * Lines starting with '#' are comments; empty lines are ignored. + * The file is optional — if absent, no signatures are revoked. */ +#define H5PL_REVOKED_SIGS_FILENAME "revoked_signatures.txt" + +/* Size of the SHA-256 hash used to identify revoked signatures. + * This is the hash of the raw signature bytes, independent of the + * plugin's signing algorithm (SHA-256/384/512). */ +#define H5PL_SIGNATURE_HASH_SIZE 32 /* SHA-256 = 32 bytes */ +#define H5PL_SIGNATURE_HASH_HEX_LEN (H5PL_SIGNATURE_HASH_SIZE * 2) /* 64 hex chars in text file */ +typedef struct H5PL_revoked_signature_t { + unsigned char hash[H5PL_SIGNATURE_HASH_SIZE]; /* SHA-256 hash of signature */ +} H5PL_revoked_signature_t; + +/* TODO (Thread Safety): Requires mutex protection if global lock is removed */ +static H5PL_revoked_signature_t *H5PL_revoked_sigs_g = NULL; +static size_t H5PL_revoked_sigs_count_g = 0; +static size_t H5PL_revoked_sigs_capacity_g = 0; +static bool H5PL_revoked_sigs_initialized_g = false; + +/* Initial capacity for keystore array */ +#define H5PL_KEYSTORE_INITIAL_CAPACITY 4 + +/* I/O chunk size for verification (1MB - optimized for modern I/O subsystems) */ +#define H5PL_VERIFY_CHUNK_SIZE ((size_t)(1024 * 1024)) + +/*********************/ +/* Local Prototypes */ +/*********************/ +static int H5PL__compare_signature_hashes(const void *a, const void *b); +static herr_t H5PL__load_revoked_signatures(const char *keystore_dir); +static bool H5PL__is_signature_revoked(const unsigned char *signature, size_t signature_len); +static void H5PL__free_keystore(void); +static herr_t H5PL__process_key_file(const char *file_path); + +/*------------------------------------------------------------------------- + * Function: H5PL__compare_signature_hashes + * + * Purpose: Comparison function for sorting and binary searching + * revoked signature hashes + * + * Return: <0 if a < b, 0 if a == b, >0 if a > b + *------------------------------------------------------------------------- + */ +static int +H5PL__compare_signature_hashes(const void *a, const void *b) +{ + const H5PL_revoked_signature_t *hash_a = (const H5PL_revoked_signature_t *)a; + const H5PL_revoked_signature_t *hash_b = (const H5PL_revoked_signature_t *)b; + + return memcmp(hash_a->hash, hash_b->hash, H5PL_SIGNATURE_HASH_SIZE); +} /* end H5PL__compare_signature_hashes() */ + +/*------------------------------------------------------------------------- + * Function: H5PL__read_file_data + * + * Purpose: Portable file read with EINTR retry + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +H5PL__read_file_data(int fd, HDoff_t offset, void *buf, size_t size, const char *filename) +{ + size_t left_to_read = size; + unsigned char *read_ptr = (unsigned char *)buf; + herr_t ret_value = SUCCEED; + + FUNC_ENTER_PACKAGE + + assert(buf); + assert(filename); + +#ifndef H5_HAVE_PREADWRITE + /* Seek to the correct location (if we don't have pread) */ + if (HDlseek(fd, offset, SEEK_SET) < 0) + HGOTO_ERROR(H5E_PLUGIN, H5E_SEEKERROR, FAIL, "unable to seek to offset %llu in plugin file '%s'", + (unsigned long long)offset, filename); +#endif /* H5_HAVE_PREADWRITE */ + + /* Read data in chunks, following HDF5's established I/O pattern from H5FDsec2.c */ + while (left_to_read > 0) { + h5_posix_io_t bytes_in = 0; + h5_posix_io_ret_t bytes_read = -1; + + /* Respect platform I/O size limits to avoid undefined behavior */ + if (left_to_read > H5_POSIX_MAX_IO_BYTES) + bytes_in = H5_POSIX_MAX_IO_BYTES; + else + bytes_in = (h5_posix_io_t)left_to_read; + + /* Retry on EINTR (interrupted system call), use pread if available */ + do { +#ifdef H5_HAVE_PREADWRITE + bytes_read = HDpread(fd, read_ptr, bytes_in, offset); + if (bytes_read > 0) + offset += bytes_read; +#else + bytes_read = HDread(fd, read_ptr, bytes_in); + if (bytes_read > 0) + offset += bytes_read; /* track offset for error reporting */ +#endif /* H5_HAVE_PREADWRITE */ + } while (-1 == bytes_read && EINTR == errno); + + if (bytes_read < 0) { + int myerrno = errno; + + HGOTO_ERROR(H5E_PLUGIN, H5E_READERROR, FAIL, + "plugin file read failed: filename='%s', errno=%d (%s), offset=%llu, size=%llu", + filename, myerrno, strerror(myerrno), (unsigned long long)offset, + (unsigned long long)bytes_in); + } + + if (0 == bytes_read) + HGOTO_ERROR(H5E_PLUGIN, H5E_READERROR, FAIL, + "unexpected end of file while reading plugin '%s' at offset %llu", filename, + (unsigned long long)offset); + + assert(bytes_read >= 0); + assert((size_t)bytes_read <= left_to_read); + + left_to_read -= (size_t)bytes_read; + read_ptr += bytes_read; + } + +done: + FUNC_LEAVE_NOAPI(ret_value) +} /* end H5PL__read_file_data() */ + +/*------------------------------------------------------------------------- + * Function: H5PL__get_hash_algorithm + * + * Purpose: Convert algorithm ID to OpenSSL EVP_MD + * + * Return: Success: Pointer to EVP_MD + * Failure: NULL + *------------------------------------------------------------------------- + */ +static const EVP_MD * +H5PL__get_hash_algorithm(H5PL_sig_algo_t algorithm_id) +{ + const EVP_MD *ret_value = NULL; + + FUNC_ENTER_PACKAGE_NOERR + + switch (algorithm_id) { + case H5PL_SIG_ALGO_SHA256: + case H5PL_SIG_ALGO_SHA256_PSS: + ret_value = EVP_sha256(); + break; + + case H5PL_SIG_ALGO_SHA384: + case H5PL_SIG_ALGO_SHA384_PSS: + ret_value = EVP_sha384(); + break; + + case H5PL_SIG_ALGO_SHA512: + case H5PL_SIG_ALGO_SHA512_PSS: + ret_value = EVP_sha512(); + break; + + case H5PL_SIG_ALGO_SHA3_256: + /* SHA3-256 is reserved for a future HDF5 release */ + H5PL_SIG_DEBUG_PRINT("Algorithm SHA3-256 (0x%02X) is reserved for future use\n", algorithm_id); + ret_value = NULL; + break; + + case H5PL_SIG_ALGO_BLAKE3: + /* BLAKE3 is reserved for a future HDF5 release */ + H5PL_SIG_DEBUG_PRINT("Algorithm BLAKE3 (0x%02X) is reserved for future use\n", algorithm_id); + ret_value = NULL; + break; + + default: + /* Completely unknown algorithm - return NULL */ + ret_value = NULL; + break; + } + + FUNC_LEAVE_NOAPI(ret_value) +} /* end H5PL__get_hash_algorithm() */ + +/*------------------------------------------------------------------------- + * Function: H5PL__add_key_to_keystore + * + * Purpose: Add a public key to the keystore with source tracking + * + * Return: SUCCEED/FAIL + * + *------------------------------------------------------------------------- + */ +static herr_t +H5PL__add_key_to_keystore(EVP_PKEY *key, const char *source) +{ + herr_t ret_value = SUCCEED; + + FUNC_ENTER_PACKAGE + + assert(key); + assert(source); + + /* Expand keystore if needed */ + if (H5PL_keystore_count_g >= H5PL_keystore_capacity_g) { + size_t new_capacity = + H5PL_keystore_capacity_g == 0 ? H5PL_KEYSTORE_INITIAL_CAPACITY : H5PL_keystore_capacity_g * 2; + H5PL_keystore_entry_t *new_keystore = (H5PL_keystore_entry_t *)H5MM_realloc( + H5PL_keystore_g, new_capacity * sizeof(H5PL_keystore_entry_t)); + + if (NULL == new_keystore) + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTALLOC, FAIL, "cannot expand keystore array"); + + H5PL_keystore_g = new_keystore; + H5PL_keystore_capacity_g = new_capacity; + } + + /* Duplicate source string before committing the entry, so a strdup + * failure doesn't leave an orphaned key pointer in the array. */ + { + char *dup_source = H5MM_strdup(source); + if (NULL == dup_source) + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTALLOC, FAIL, "cannot duplicate key source string"); + + H5PL_keystore_g[H5PL_keystore_count_g].key = key; + H5PL_keystore_g[H5PL_keystore_count_g].source = dup_source; + H5PL_keystore_count_g++; + } + +done: + FUNC_LEAVE_NOAPI(ret_value) +} /* end H5PL__add_key_to_keystore() */ + +/*------------------------------------------------------------------------- + * Function: H5PL__create_public_RSA_from_file + * + * Purpose: Create EVP public key from PEM file + * + * Return: Success: Pointer to EVP_PKEY + * Failure: NULL + * + *------------------------------------------------------------------------- + */ +static EVP_PKEY * +H5PL__create_public_RSA_from_file(const char *file_path) +{ + BIO *bio = NULL; + EVP_PKEY *pkey = NULL; + EVP_PKEY *ret_value = NULL; + + FUNC_ENTER_PACKAGE_NOERR + + assert(file_path); + + /* Open key file using BIO (avoids OPENSSL_Applink issue on Windows) */ + if (NULL == (bio = BIO_new_file(file_path, "r"))) { + /* Don't error - just skip invalid files */ + goto done; + } + + /* Read public key using modern EVP API */ + if (NULL == (pkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL))) { + /* Don't error - just skip invalid PEM files */ + goto done; + } + + /* Validate key type - only RSA keys are supported */ + { + int key_type = EVP_PKEY_base_id(pkey); + if (key_type != EVP_PKEY_RSA && key_type != EVP_PKEY_RSA_PSS) { + /* Don't error - just skip unsupported key types */ + goto done; + } + } + + ret_value = pkey; + pkey = NULL; /* Prevent cleanup */ + +done: + if (bio) + BIO_free(bio); + if (pkey) + EVP_PKEY_free(pkey); + + /* Clear any remaining OpenSSL errors from the error queue */ + ERR_clear_error(); + + FUNC_LEAVE_NOAPI(ret_value) +} /* end H5PL__create_public_RSA_from_file() */ + +/*------------------------------------------------------------------------- + * Function: H5PL__process_key_file + * + * Purpose: Load a PEM key file and add it to the keystore + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +H5PL__process_key_file(const char *file_path) +{ + EVP_PKEY *key = NULL; + herr_t ret_value = SUCCEED; + + FUNC_ENTER_PACKAGE + + assert(file_path); + + /* Try to load key; skip files that fail to load (invalid PEM, etc.) */ + if (NULL != (key = H5PL__create_public_RSA_from_file(file_path))) { + /* Add to keystore (transfers ownership of key on success) */ + if (H5PL__add_key_to_keystore(key, file_path) < 0) + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTALLOC, FAIL, "cannot add key to keystore"); + key = NULL; /* Ownership transferred to keystore */ + } + +done: + if (key) + EVP_PKEY_free(key); + FUNC_LEAVE_NOAPI(ret_value) +} /* end H5PL__process_key_file() */ + +/*------------------------------------------------------------------------- + * Function: H5PL__load_keys_from_directory + * + * Purpose: Load all .pem files from a directory into the keystore + * + * Return: SUCCEED/FAIL (fails if directory invalid, but skips bad files) + * + *------------------------------------------------------------------------- + */ +#ifndef H5_HAVE_WIN32_API +static herr_t +H5PL__load_keys_from_directory(const char *dir_path) +{ + DIR *dir = NULL; + struct dirent *entry = NULL; + size_t dirlen = 0; + herr_t ret_value = SUCCEED; + + FUNC_ENTER_PACKAGE + + assert(dir_path); + + /* Open directory */ + if (NULL == (dir = opendir(dir_path))) { + /* Non-existent directory is an error */ + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTOPENFILE, FAIL, "cannot open keystore directory: %s", dir_path); + } + + dirlen = strlen(dir_path); + + /* Iterate through directory entries */ + while (NULL != (entry = readdir(dir))) { + char *file_path = NULL; + size_t namelen = strlen(entry->d_name); + size_t path_len; + + /* Skip . and .. */ + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) + continue; + + /* Only process .pem files */ + if (namelen < 5 || strcmp(entry->d_name + namelen - 4, ".pem") != 0) + continue; + + /* Validate filename doesn't contain path separators (defense in depth) */ + if (strchr(entry->d_name, '/') != NULL) { + H5PL_SIG_DEBUG_PRINT("WARNING: Skipping file with path separator in name: %s\n", entry->d_name); + continue; + } + + /* Build full path */ + path_len = dirlen + namelen + 2; + if (NULL == (file_path = (char *)H5MM_malloc(path_len))) { + H5PL_SIG_DEBUG_PRINT("WARNING: Cannot allocate path buffer for %s\n", entry->d_name); + continue; + } + + snprintf(file_path, path_len, "%s/%s", dir_path, entry->d_name); + + /* Canonicalize and verify path stays within keystore directory (path traversal protection) */ + { + char *canonical_dir = NULL; + char *canonical_file = NULL; + + canonical_dir = HDrealpath(dir_path, NULL); + if (NULL == canonical_dir) { + H5PL_SIG_DEBUG_PRINT("WARNING: Cannot resolve keystore directory path: %s\n", + strerror(errno)); + H5MM_xfree(file_path); + continue; + } + + canonical_file = HDrealpath(file_path, NULL); + if (NULL == canonical_file) { + /* File might not exist yet in some cases, but for key files it must exist */ + H5PL_SIG_DEBUG_PRINT("WARNING: Cannot resolve key file path %s: %s\n", file_path, + strerror(errno)); + free(canonical_dir); + H5MM_xfree(file_path); + continue; + } + + /* Verify canonical file path starts with canonical directory path */ + { + size_t dir_len = strlen(canonical_dir); + if (strncmp(canonical_file, canonical_dir, dir_len) != 0 || + (canonical_file[dir_len] != '/' && canonical_file[dir_len] != '\0')) { + H5PL_SIG_DEBUG_PRINT( + "WARNING: Path traversal detected - %s resolves outside keystore directory\n", + entry->d_name); + free(canonical_dir); + free(canonical_file); + H5MM_xfree(file_path); + continue; + } + } + + free(canonical_dir); + free(canonical_file); + } + + /* Skip symlinks */ + { + h5_stat_t file_stat; + if (HDlstat(file_path, &file_stat) < 0) { + H5PL_SIG_DEBUG_PRINT("WARNING: Cannot stat key file %s: %s\n", file_path, strerror(errno)); + H5MM_xfree(file_path); + continue; + } + + if (S_ISLNK(file_stat.st_mode)) { + H5PL_SIG_DEBUG_PRINT("WARNING: Skipping symlink %s (security policy)\n", file_path); + H5MM_xfree(file_path); + continue; + } + } + + /* Load key and add to keystore */ + if (H5PL__process_key_file(file_path) < 0) { + H5MM_xfree(file_path); + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTALLOC, FAIL, "cannot process key file"); + } + + /* Clean up file path */ + H5MM_xfree(file_path); + } + +done: + if (dir) + closedir(dir); + FUNC_LEAVE_NOAPI(ret_value) +} /* end H5PL__load_keys_from_directory() */ +#else /* H5_HAVE_WIN32_API */ +static herr_t +H5PL__load_keys_from_directory(const char *dir_path) +{ + HANDLE dir_handle = INVALID_HANDLE_VALUE; + herr_t ret_value = SUCCEED; + + FUNC_ENTER_PACKAGE + + assert(dir_path); + + { + WIN32_FIND_DATAA find_data; + char search_pattern[MAX_PATH]; + + /* Build search pattern: dir\*.pem */ + snprintf(search_pattern, sizeof(search_pattern), "%s\\*.pem", dir_path); + + dir_handle = FindFirstFileA(search_pattern, &find_data); + if (INVALID_HANDLE_VALUE == dir_handle) { + /* Empty directory is OK */ + goto done; + } + + do { + char file_path[MAX_PATH]; + char canonical_dir[MAX_PATH]; + char canonical_file[MAX_PATH]; + + /* Skip directories */ + if (find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + continue; + + /* Skip symlinks and reparse points (NTFS junctions, symlinks) */ + if (find_data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) + continue; + + /* Build full path */ + snprintf(file_path, sizeof(file_path), "%s\\%s", dir_path, find_data.cFileName); + + /* Path traversal protection: verify file resolves within the keystore directory */ + if (GetFullPathNameA(dir_path, MAX_PATH, canonical_dir, NULL) == 0 || + GetFullPathNameA(file_path, MAX_PATH, canonical_file, NULL) == 0) { + H5PL_SIG_DEBUG_PRINT("WARNING: Cannot resolve path for %s\n", find_data.cFileName); + continue; + } + { + size_t dir_len = strlen(canonical_dir); + if (_strnicmp(canonical_file, canonical_dir, dir_len) != 0 || + (canonical_file[dir_len] != '\\' && canonical_file[dir_len] != '\0')) { + H5PL_SIG_DEBUG_PRINT( + "WARNING: Path traversal detected - %s resolves outside keystore directory\n", + find_data.cFileName); + continue; + } + } + + /* Load key and add to keystore */ + if (H5PL__process_key_file(file_path) < 0) + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTALLOC, FAIL, "cannot process key file"); + + } while (FindNextFileA(dir_handle, &find_data) != 0); + } + +done: + if (dir_handle != INVALID_HANDLE_VALUE) + FindClose(dir_handle); + + FUNC_LEAVE_NOAPI(ret_value) +} /* end H5PL__load_keys_from_directory() */ +#endif /* H5_HAVE_WIN32_API */ + +/*------------------------------------------------------------------------- + * Function: H5PL__free_keystore + * + * Purpose: Free all keys and revocation entries in the keystore + * + * Return: void + *------------------------------------------------------------------------- + */ +static void +H5PL__free_keystore(void) +{ + if (H5PL_keystore_g) { + size_t i; + for (i = 0; i < H5PL_keystore_count_g; i++) { + if (H5PL_keystore_g[i].key) + EVP_PKEY_free(H5PL_keystore_g[i].key); + if (H5PL_keystore_g[i].source) + H5MM_xfree(H5PL_keystore_g[i].source); + } + H5MM_xfree(H5PL_keystore_g); + H5PL_keystore_g = NULL; + } + H5PL_keystore_count_g = 0; + H5PL_keystore_capacity_g = 0; + + if (H5PL_revoked_sigs_g) { + H5MM_xfree(H5PL_revoked_sigs_g); + H5PL_revoked_sigs_g = NULL; + } + H5PL_revoked_sigs_count_g = 0; + H5PL_revoked_sigs_capacity_g = 0; + + H5PL_keystore_initialized_g = false; + H5PL_revoked_sigs_initialized_g = false; +} /* end H5PL__free_keystore() */ + +/*------------------------------------------------------------------------- + * Function: H5PL__init_keystore + * + * Purpose: Initialize keystore + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +H5PL__init_keystore(void) +{ + const char *env_keystore = NULL; + bool keys_loaded = false; + herr_t ret_value = SUCCEED; + + FUNC_ENTER_PACKAGE + + /* Already initialized? */ + if (H5PL_keystore_initialized_g) + HGOTO_DONE(SUCCEED); + + /* Initialize keystore */ + H5PL_keystore_g = NULL; + H5PL_keystore_count_g = 0; + H5PL_keystore_capacity_g = 0; + H5PL_keystore_initialized_g = true; + + /* Initialize revocation list */ + H5PL_revoked_sigs_g = NULL; + H5PL_revoked_sigs_count_g = 0; + H5PL_revoked_sigs_capacity_g = 0; + H5PL_revoked_sigs_initialized_g = true; + + /* 1. Check environment variable (highest priority) */ +#ifndef H5PL_DISABLE_ENV_KEYSTORE + if (NULL != (env_keystore = getenv("HDF5_PLUGIN_KEYSTORE"))) { + if (H5PL__load_keys_from_directory(env_keystore) < 0) + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTLOAD, FAIL, "failed to load keys from HDF5_PLUGIN_KEYSTORE: %s", + env_keystore); + keys_loaded = true; + + /* Load revoked signatures from same directory */ + if (H5PL__load_revoked_signatures(env_keystore) < 0) { + /* Non-fatal - continue even if revoked signatures fail to load */ + } + } +#else + /* Environment variable override disabled at compile time (security hardening) */ + env_keystore = NULL; /* Suppress unused variable warning */ +#endif + +/* 2. Check CMake-configured directory */ +#ifdef H5PL_KEYSTORE_DIR + if (!keys_loaded) { + /* Only try if directory was configured */ + h5_stat_t st; + if (HDstat(H5PL_KEYSTORE_DIR, &st) == 0) { + /* Directory exists, try to load. Pause the error stack so that + * a load failure here does not pollute the stack — the generic + * "no valid public keys" error below is more informative. */ + H5E_pause_stack(); + if (H5PL__load_keys_from_directory(H5PL_KEYSTORE_DIR) < 0) { + H5PL_SIG_DEBUG_PRINT("WARNING: Failed to load keys from configured keystore: %s\n", + H5PL_KEYSTORE_DIR); + } + else { + keys_loaded = true; + + /* Load revoked signatures from same directory */ + if (H5PL__load_revoked_signatures(H5PL_KEYSTORE_DIR) < 0) { + /* Non-fatal - continue even if revoked signatures fail to load */ + } + } + H5E_resume_stack(); + } + } +#endif + + /* Must have at least one key */ + if (!keys_loaded || H5PL_keystore_count_g == 0) { + const char *attempted_source = env_keystore ? env_keystore : H5PL_SIG_KEYSTORE_DIR_STR; + bool keystore_configured = (env_keystore != NULL); + +#ifdef H5PL_KEYSTORE_DIR + keystore_configured = true; +#endif + + HGOTO_ERROR(H5E_PLUGIN, H5E_BADVALUE, FAIL, + "no valid public keys found for plugin signature verification\n" + " %s%s\n" + " Keys found: 0\n" + "\n" + "Configure keys via:\n" + " - Environment: export HDF5_PLUGIN_KEYSTORE=/path/to/keys\n" + " - CMake: -DHDF5_PLUGIN_KEYSTORE_DIR=/path/to/keys\n" + "\n" + "Verify:\n" + " - Directory exists and is readable\n" + " - Directory contains .pem files\n" + " - .pem files are valid RSA public keys", + keystore_configured ? "Attempted to load from: " : "No keystore configured", + keystore_configured ? attempted_source : ""); + } + + if (H5PL_keystore_count_g > 0) { + H5PL_SIG_DEBUG_PRINT("HDF5 Plugin KeyStore initialized:\n"); + H5PL_SIG_DEBUG_PRINT(" Keys loaded: %zu\n", H5PL_keystore_count_g); + for (size_t i = 0; i < H5PL_keystore_count_g; i++) { + H5PL_SIG_DEBUG_PRINT(" [%zu] %s\n", i + 1, H5PL_keystore_g[i].source); + } + } + if (H5PL_revoked_sigs_count_g > 0) { + H5PL_SIG_DEBUG_PRINT(" Revoked signatures loaded: %zu\n", H5PL_revoked_sigs_count_g); + } + +done: + /* Cleanup on initialization failure */ + if (ret_value < 0) + H5PL__free_keystore(); + + FUNC_LEAVE_NOAPI(ret_value) +} /* end H5PL__init_keystore() */ + +/*------------------------------------------------------------------------- + * Function: H5PL__parse_hex_hash + * + * Purpose: Parse a hexadecimal string into a byte array + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +H5PL__parse_hex_hash(const char *hex_string, unsigned char *hash) +{ + herr_t ret_value = SUCCEED; + + FUNC_ENTER_PACKAGE + + assert(hex_string); + assert(hash); + + /* Convert hex string to bytes */ + for (size_t i = 0; i < H5PL_SIGNATURE_HASH_SIZE; i++) { + unsigned int byte; + if (sscanf(hex_string + (i * 2), "%2x", &byte) != 1) + HGOTO_ERROR(H5E_PLUGIN, H5E_BADVALUE, FAIL, "invalid hex character in hash string"); + hash[i] = (unsigned char)byte; + } + +done: + FUNC_LEAVE_NOAPI(ret_value) +} /* end H5PL__parse_hex_hash() */ + +/*------------------------------------------------------------------------- + * Function: H5PL__load_revoked_signatures + * + * Purpose: Load revoked signature hashes from blocklist file + * + * File format: One SHA-256 hash per line (64 hex chars) + * Comments start with '#', empty lines ignored + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +H5PL__load_revoked_signatures(const char *keystore_dir) +{ + char *filepath = NULL; + FILE *fp = NULL; + char line[256]; + size_t path_len; + herr_t ret_value = SUCCEED; + + FUNC_ENTER_PACKAGE + + assert(keystore_dir); + + /* Build path to revoked signatures file */ + path_len = strlen(keystore_dir) + 1 + strlen(H5PL_REVOKED_SIGS_FILENAME) + 1; + if (NULL == (filepath = (char *)H5MM_malloc(path_len))) + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTALLOC, FAIL, "cannot allocate filepath buffer"); + + if (snprintf(filepath, path_len, "%s/%s", keystore_dir, H5PL_REVOKED_SIGS_FILENAME) >= (int)path_len) + HGOTO_ERROR(H5E_PLUGIN, H5E_NOSPACE, FAIL, "revoked signatures file path too long"); + + /* Try to open revoked signatures file (optional - not an error if missing) */ + if (NULL == (fp = fopen(filepath, "r"))) { + /* File doesn't exist - not an error, just means no revoked signatures */ + HGOTO_DONE(SUCCEED); + } + + /* Read file line by line */ + while (fgets(line, sizeof(line), fp) != NULL) { + unsigned char hash[H5PL_SIGNATURE_HASH_SIZE]; + size_t line_len; + char *trimmed; + bool line_truncated = false; + + /* Detect truncated reads: fgets fills the buffer without finding a + * newline, meaning the physical line exceeds sizeof(line)-1 chars. + * Drain the remainder so the next fgets starts on a fresh line, then + * skip this chunk — a trailing fragment could otherwise be mistaken + * for a valid 64-hex-char hash. */ + if (strchr(line, '\n') == NULL && !feof(fp)) { + int ch; + line_truncated = true; + while ((ch = fgetc(fp)) != EOF && ch != '\n') + ; + } + if (line_truncated) { + H5PL_SIG_DEBUG_PRINT("WARNING: Skipping oversized line in revoked signatures file\n"); + continue; + } + + /* Trim whitespace */ + trimmed = line; + while (*trimmed == ' ' || *trimmed == '\t') + trimmed++; + + line_len = strlen(trimmed); + while (line_len > 0 && (trimmed[line_len - 1] == '\n' || trimmed[line_len - 1] == '\r' || + trimmed[line_len - 1] == ' ' || trimmed[line_len - 1] == '\t')) { + trimmed[line_len - 1] = '\0'; + line_len--; + } + + /* Skip empty lines and comments */ + if (line_len == 0 || trimmed[0] == '#') + continue; + + /* Each SHA-256 byte is two hex characters → 64 chars per hash */ + if (line_len != H5PL_SIGNATURE_HASH_HEX_LEN) { + H5PL_SIG_DEBUG_PRINT( + "WARNING: Ignoring invalid revoked signature hash (expected %d hex chars): %s\n", + H5PL_SIGNATURE_HASH_HEX_LEN, trimmed); + continue; + } + + /* Convert hex string to bytes */ + if (H5PL__parse_hex_hash(trimmed, hash) < 0) { + H5PL_SIG_DEBUG_PRINT("WARNING: Invalid hex in revoked signature hash: %s\n", trimmed); + continue; + } + + /* Expand revoked signatures array if needed */ + if (H5PL_revoked_sigs_count_g >= H5PL_revoked_sigs_capacity_g) { + size_t new_capacity = H5PL_revoked_sigs_capacity_g == 0 ? 8 : H5PL_revoked_sigs_capacity_g * 2; + H5PL_revoked_signature_t *new_array = (H5PL_revoked_signature_t *)H5MM_realloc( + H5PL_revoked_sigs_g, new_capacity * sizeof(H5PL_revoked_signature_t)); + + if (NULL == new_array) + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTALLOC, FAIL, "cannot expand revoked signatures array"); + + H5PL_revoked_sigs_g = new_array; + H5PL_revoked_sigs_capacity_g = new_capacity; + } + + /* Add hash to revoked list */ + memcpy(H5PL_revoked_sigs_g[H5PL_revoked_sigs_count_g].hash, hash, H5PL_SIGNATURE_HASH_SIZE); + H5PL_revoked_sigs_count_g++; + } + + /* Sort the revocation list for binary search (improves O(n) to O(log n) lookup) */ + if (H5PL_revoked_sigs_count_g > 1) { + qsort(H5PL_revoked_sigs_g, H5PL_revoked_sigs_count_g, sizeof(H5PL_revoked_signature_t), + H5PL__compare_signature_hashes); + } + +done: + if (fp) + fclose(fp); + if (filepath) + H5MM_xfree(filepath); + + FUNC_LEAVE_NOAPI(ret_value) +} /* end H5PL__load_revoked_signatures() */ + +/*------------------------------------------------------------------------- + * Function: H5PL__is_signature_revoked + * + * Purpose: Check if a signature hash is in the revocation list + * + * Return: true if revoked, false otherwise + *------------------------------------------------------------------------- + */ +static bool +H5PL__is_signature_revoked(const unsigned char *signature, size_t signature_len) +{ + unsigned char hash[H5PL_SIGNATURE_HASH_SIZE]; + EVP_MD_CTX *mdctx = NULL; + bool ret_value = false; + + FUNC_ENTER_PACKAGE_NOERR + + assert(signature); + + /* Compute SHA-256 hash of signature */ + if (NULL == (mdctx = EVP_MD_CTX_new())) + HGOTO_DONE(false); + + if (1 != EVP_DigestInit_ex(mdctx, EVP_sha256(), NULL)) + HGOTO_DONE(false); + + if (1 != EVP_DigestUpdate(mdctx, signature, signature_len)) + HGOTO_DONE(false); + + if (1 != EVP_DigestFinal_ex(mdctx, hash, NULL)) + HGOTO_DONE(false); + + /* Check if hash is in revoked list using binary search + * (array is sorted in H5PL__load_revoked_signatures). + * hash[] can be passed directly as the bsearch key because + * H5PL_revoked_signature_t contains only a hash array at offset 0. + */ + if (H5PL_revoked_sigs_count_g > 0) { + if (NULL != bsearch(hash, H5PL_revoked_sigs_g, H5PL_revoked_sigs_count_g, + sizeof(H5PL_revoked_signature_t), H5PL__compare_signature_hashes)) + HGOTO_DONE(true); + } + +done: + if (mdctx) + EVP_MD_CTX_free(mdctx); + + FUNC_LEAVE_NOAPI(ret_value) +} /* end H5PL__is_signature_revoked() */ + +/*------------------------------------------------------------------------- + * Function: H5PL__hash_file_binary + * + * Purpose: Compute the message digest of the plugin binary data. + * Reads the first binary_size bytes of fd in 1MB chunks + * and feeds them into hash_algorithm. The raw digest is + * written to digest_out (caller must supply EVP_MAX_MD_SIZE + * bytes) and its byte length to digest_len_out. + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +H5PL__hash_file_binary(int fd, HDoff_t binary_size, const EVP_MD *hash_algorithm, unsigned char *digest_out, + unsigned int *digest_len_out, const char *plugin_path) +{ + EVP_MD_CTX *mdctx = NULL; + unsigned char *chunk_buf = NULL; + HDoff_t bytes_read = 0; + herr_t ret_value = SUCCEED; + + FUNC_ENTER_PACKAGE + + assert(fd >= 0); + assert(hash_algorithm); + assert(digest_out); + assert(digest_len_out); + assert(plugin_path); + + /* Allocate chunk buffer */ + if (NULL == (chunk_buf = (unsigned char *)H5MM_malloc(H5PL_VERIFY_CHUNK_SIZE))) + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTALLOC, FAIL, "cannot allocate chunk buffer for hashing"); + + /* Create and initialize digest context */ + if (NULL == (mdctx = EVP_MD_CTX_new())) + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTCREATE, FAIL, "cannot create digest context"); + + if (1 != EVP_DigestInit_ex(mdctx, hash_algorithm, NULL)) + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTINIT, FAIL, "cannot initialize digest context"); + + /* Read and hash file in chunks */ + while (bytes_read < binary_size) { + size_t chunk_size = (size_t)((binary_size - bytes_read) > (HDoff_t)H5PL_VERIFY_CHUNK_SIZE + ? H5PL_VERIFY_CHUNK_SIZE + : (size_t)(binary_size - bytes_read)); + + if (H5PL__read_file_data(fd, bytes_read, chunk_buf, chunk_size, plugin_path) < 0) + HGOTO_ERROR(H5E_PLUGIN, H5E_READERROR, FAIL, "cannot read plugin data for hashing"); + + if (1 != EVP_DigestUpdate(mdctx, chunk_buf, chunk_size)) + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTGET, FAIL, "cannot update digest"); + + bytes_read += (HDoff_t)chunk_size; + } + + /* Finalize digest */ + if (1 != EVP_DigestFinal_ex(mdctx, digest_out, digest_len_out)) + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTGET, FAIL, "cannot finalize digest"); + +done: + if (chunk_buf) + H5MM_xfree(chunk_buf); + if (mdctx) + EVP_MD_CTX_free(mdctx); + ERR_clear_error(); + + FUNC_LEAVE_NOAPI(ret_value) +} /* end H5PL__hash_file_binary() */ + +/*------------------------------------------------------------------------- + * Function: H5PL__read_and_validate_footer + * + * Purpose: Read and validate the signature footer from a plugin file + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +H5PL__read_and_validate_footer(int fd, HDoff_t file_size, const char *plugin_path, + H5PL_sig_footer_t *footer_out, size_t *binary_size_out) +{ + uint8_t footer_buf[H5PL_SIG_FOOTER_SIZE]; + herr_t ret_value = SUCCEED; + + FUNC_ENTER_PACKAGE + + assert(fd >= 0); + assert(plugin_path); + assert(footer_out); + assert(binary_size_out); + + /* File must be large enough for footer */ + if (file_size < (HDoff_t)H5PL_SIG_FOOTER_SIZE) + HGOTO_ERROR(H5E_PLUGIN, H5E_BADVALUE, FAIL, "file too small to contain signature footer"); + + /* Read footer from end of file */ + if (H5PL__read_file_data(fd, file_size - (HDoff_t)H5PL_SIG_FOOTER_SIZE, footer_buf, H5PL_SIG_FOOTER_SIZE, + plugin_path) < 0) + HGOTO_ERROR(H5E_PLUGIN, H5E_READERROR, FAIL, "cannot read signature footer"); + + /* Decode and validate footer (magic and format version checked inside) */ + if (!H5PL_sig_decode_footer(footer_buf, sizeof(footer_buf), footer_out)) + HGOTO_ERROR(H5E_PLUGIN, H5E_BADVALUE, FAIL, + "not a signed HDF5 plugin (bad magic or unsupported format version)"); + + /* Validate algorithm ID */ + if (NULL == H5PL__get_hash_algorithm(footer_out->algorithm_id)) + HGOTO_ERROR(H5E_PLUGIN, H5E_BADVALUE, FAIL, + "unsupported or unknown hash algorithm ID 0x%02X in plugin signature", + (unsigned)footer_out->algorithm_id); + + /* Validate signature length */ + if (footer_out->signature_length == 0 || footer_out->signature_length > H5PL_MAX_SIGNATURE_SIZE) + HGOTO_ERROR(H5E_PLUGIN, H5E_BADVALUE, FAIL, + "invalid signature length %u bytes (valid range: 1-%u bytes)", + footer_out->signature_length, H5PL_MAX_SIGNATURE_SIZE); + + /* Calculate binary data size with overflow protection */ + { + /* Use uint64_t to prevent any theoretical overflow in addition */ + uint64_t sig_and_footer_size = + (uint64_t)footer_out->signature_length + (uint64_t)H5PL_SIG_FOOTER_SIZE; + + /* Validate file size can contain signature and footer */ + if (file_size < (HDoff_t)sig_and_footer_size) + HGOTO_ERROR(H5E_PLUGIN, H5E_BADVALUE, FAIL, + "file too small to contain claimed signature and footer"); + + /* Calculate binary size - mathematically guaranteed non-negative after above check */ + HDoff_t binary_size_off = file_size - (HDoff_t)sig_and_footer_size; + + /* Practical size limit: 1GB for plugin files */ + if (binary_size_off > H5PL_MAX_PLUGIN_SIZE) + HGOTO_ERROR(H5E_PLUGIN, H5E_BADVALUE, FAIL, + "plugin binary size %llu exceeds maximum allowed size (%llu bytes) - " + "file too large to verify", + (unsigned long long)binary_size_off, (unsigned long long)H5PL_MAX_PLUGIN_SIZE); + + *binary_size_out = (size_t)binary_size_off; + } + +done: + FUNC_LEAVE_NOAPI(ret_value) +} /* end H5PL__read_and_validate_footer() */ + +/*------------------------------------------------------------------------- + * Function: H5PL__verify_with_all_keys + * + * Purpose: Try verifying the plugin signature with each key in the + * keystore. The binary is hashed exactly once; the digest + * is then checked against the stored signature for every + * key using EVP_PKEY_verify (no per-key file re-read). + * + * Return: SUCCEED if signature verified with at least one key + * FAIL otherwise + *------------------------------------------------------------------------- + */ +static herr_t +H5PL__verify_with_all_keys(int fd, size_t binary_size, const unsigned char *signature, + const H5PL_sig_footer_t *footer, const char *plugin_path) +{ + const EVP_MD *hash_algorithm = NULL; + unsigned char digest[EVP_MAX_MD_SIZE]; + unsigned int digest_len = 0; + bool verified = false; + herr_t ret_value = SUCCEED; + + FUNC_ENTER_PACKAGE + + assert(fd >= 0); + assert(signature); + assert(footer); + assert(plugin_path); + + /* Get hash algorithm from footer (crypto-agile verification) */ + hash_algorithm = H5PL__get_hash_algorithm(footer->algorithm_id); + if (NULL == hash_algorithm) + HGOTO_ERROR(H5E_PLUGIN, H5E_BADVALUE, FAIL, "cannot get hash algorithm for ID 0x%02X", + (unsigned)footer->algorithm_id); + + /* Hash the binary exactly once - shared across all key verification attempts */ + if (H5PL__hash_file_binary(fd, (HDoff_t)binary_size, hash_algorithm, digest, &digest_len, plugin_path) < + 0) + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTGET, FAIL, "cannot compute hash of plugin binary"); + + /* Try each key in keystore (OR logic - first match wins) */ + for (size_t key_idx = 0; key_idx < H5PL_keystore_count_g; key_idx++) { + EVP_PKEY *public_key = H5PL_keystore_g[key_idx].key; + EVP_PKEY_CTX *pkey_ctx = NULL; + int verify_result = -1; + + /* Create per-key verification context */ + if (NULL == (pkey_ctx = EVP_PKEY_CTX_new(public_key, NULL))) { + ERR_clear_error(); + continue; + } + + if (1 != EVP_PKEY_verify_init(pkey_ctx)) { + EVP_PKEY_CTX_free(pkey_ctx); + ERR_clear_error(); + continue; + } + + /* Bind hash algorithm to the context */ + if (1 != EVP_PKEY_CTX_set_signature_md(pkey_ctx, hash_algorithm)) { + EVP_PKEY_CTX_free(pkey_ctx); + ERR_clear_error(); + continue; + } + + /* Configure PSS padding if needed */ + if (H5PL_SIG_ALGO_IS_PSS(footer->algorithm_id)) { + if (1 != EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, RSA_PKCS1_PSS_PADDING) || + 1 != EVP_PKEY_CTX_set_rsa_pss_saltlen(pkey_ctx, RSA_PSS_SALTLEN_DIGEST)) { + EVP_PKEY_CTX_free(pkey_ctx); + ERR_clear_error(); + continue; + } + } + + /* Verify pre-computed digest against the stored signature */ + verify_result = + EVP_PKEY_verify(pkey_ctx, signature, footer->signature_length, digest, (size_t)digest_len); + EVP_PKEY_CTX_free(pkey_ctx); + ERR_clear_error(); + + if (verify_result == 1) { + /* SUCCESS - signature matches this key */ + verified = true; + H5PL_SIG_DEBUG_PRINT("Plugin '%s' verified with key from: %s\n", plugin_path, + H5PL_keystore_g[key_idx].source); + break; + } + } + + if (!verified) + HGOTO_ERROR(H5E_PLUGIN, H5E_BADVALUE, FAIL, + "plugin signature verification failed: no key in keystore matched"); + +done: + FUNC_LEAVE_NOAPI(ret_value) +} /* end H5PL__verify_with_all_keys() */ + +/*------------------------------------------------------------------------- + * Function: H5PL__verify_signature_appended + * + * Purpose: Verify plugin digital signature + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +herr_t +H5PL__verify_signature_appended(const char *plugin_path) +{ + int fd = -1; + h5_stat_t st; + HDoff_t file_size = 0; + H5PL_sig_footer_t footer; + unsigned char *signature = NULL; + size_t binary_size = 0; + herr_t ret_value = SUCCEED; + + FUNC_ENTER_PACKAGE + + assert(plugin_path); + + /* Open plugin file */ + { + int open_flags = O_RDONLY; +#ifdef O_CLOEXEC + open_flags |= O_CLOEXEC; +#endif + fd = HDopen(plugin_path, open_flags, 0); + } + if (fd < 0) + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTOPENFILE, FAIL, "cannot open plugin file"); + + /* Get file size */ + if (HDfstat(fd, &st) < 0) + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTGET, FAIL, "cannot get file size"); + file_size = (HDoff_t)st.st_size; + + /* Read and validate footer */ + if (H5PL__read_and_validate_footer(fd, file_size, plugin_path, &footer, &binary_size) < 0) + HGOTO_ERROR(H5E_PLUGIN, H5E_READERROR, FAIL, "cannot read or validate signature footer"); + + /* Read signature data */ + if (NULL == (signature = (unsigned char *)H5MM_malloc(footer.signature_length))) + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTALLOC, FAIL, "cannot allocate signature buffer"); + + if (H5PL__read_file_data(fd, (HDoff_t)binary_size, signature, footer.signature_length, plugin_path) < 0) + HGOTO_ERROR(H5E_PLUGIN, H5E_READERROR, FAIL, "cannot read signature data"); + + /* Initialize keystore on first use */ + if (!H5PL_keystore_initialized_g) { + if (H5PL__init_keystore() < 0) + HGOTO_ERROR(H5E_PLUGIN, H5E_CANTINIT, FAIL, "cannot initialize keystore"); + } + + /* Check if signature is revoked */ + if (H5PL__is_signature_revoked(signature, footer.signature_length)) + HGOTO_ERROR(H5E_PLUGIN, H5E_BADVALUE, FAIL, "plugin signature has been revoked: %s", plugin_path); + + /* Must have at least one key */ + if (H5PL_keystore_count_g == 0) + HGOTO_ERROR(H5E_PLUGIN, H5E_BADVALUE, FAIL, "keystore is empty - no keys available for verification"); + + /* Verify signature with all keys in keystore */ + if (H5PL__verify_with_all_keys(fd, binary_size, signature, &footer, plugin_path) < 0) + HGOTO_ERROR(H5E_PLUGIN, H5E_BADVALUE, FAIL, "signature verification failed"); + + /* Close file after verification */ + HDclose(fd); + fd = -1; + +done: + if (fd >= 0) + HDclose(fd); + if (signature) + H5MM_xfree(signature); + + ERR_clear_error(); + + FUNC_LEAVE_NOAPI(ret_value) +} /* end H5PL__verify_signature_appended() */ + +/*------------------------------------------------------------------------- + * Function: H5PL__cleanup_signature_resources + * + * Purpose: Clean up keystore and revocation list + * + * Return: SUCCEED + *------------------------------------------------------------------------- + */ +herr_t +H5PL__cleanup_signature_resources(void) +{ + FUNC_ENTER_PACKAGE_NOERR + + /* Free all keys in the keystore and revocation list */ + H5PL__free_keystore(); + + FUNC_LEAVE_NOAPI(SUCCEED) +} /* end H5PL__cleanup_signature_resources() */ + +#endif /* H5_REQUIRE_DIGITAL_SIGNATURE */ diff --git a/src/H5PLsig.h b/src/H5PLsig.h new file mode 100644 index 00000000000..fff3d829dac --- /dev/null +++ b/src/H5PLsig.h @@ -0,0 +1,157 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Copyright by The HDF Group. * + * All rights reserved. * + * * + * This file is part of HDF5. The full HDF5 copyright notice, including * + * terms governing use, modification, and redistribution, is contained in * + * the LICENSE file, which can be found at the root of the source code * + * distribution tree, or in https://www.hdfgroup.org/licenses. * + * If you do not have access to either file, you may request a copy from * + * help@hdfgroup.org. * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef H5PLsig_H +#define H5PLsig_H + +/* + * Appended Signature Format + * ========================= + * + * Plugin files use an appended signature format: + * + * [ Binary Data (ELF/DLL/Mach-O) ] [ RSA Signature ] [ Footer ] + * + * The footer contains metadata about the signature and a magic number + * to identify signed plugins. The binary loader ignores trailing data, + * so the plugin loads normally. + * + * This approach: + * - Works on all platforms (Linux, Windows, macOS) + * - No ELF parsing required + * - No external tools needed (objcopy, etc.) + * - Simple append operation for signing + * - Simple read-from-end for verification + */ + +/* Magic number to identify HDF5 signed plugins */ +#define H5PL_SIG_MAGIC 0x48444635 /* "HDF5" in hex */ + +/* Current signature format version. + * If future versions change the footer layout, the decoder should be + * updated to accept older versions so that already-signed plugins + * remain loadable without re-signing. */ +#define H5PL_SIG_FORMAT_VERSION_CURRENT 1 + +/* Hash Algorithm Identifiers (on-disk values, stored as uint8_t) */ +typedef enum { + H5PL_SIG_ALGO_SHA256 = 0x01, /* SHA-256 with RSA-PKCS1 */ + H5PL_SIG_ALGO_SHA384 = 0x02, /* SHA-384 with RSA-PKCS1 */ + H5PL_SIG_ALGO_SHA512 = 0x03, /* SHA-512 with RSA-PKCS1 (default) */ + H5PL_SIG_ALGO_SHA256_PSS = 0x11, /* SHA-256 with RSA-PSS */ + H5PL_SIG_ALGO_SHA384_PSS = 0x12, /* SHA-384 with RSA-PSS */ + H5PL_SIG_ALGO_SHA512_PSS = 0x13, /* SHA-512 with RSA-PSS */ + H5PL_SIG_ALGO_SHA3_256 = 0x20, /* SHA3-256 (future) */ + H5PL_SIG_ALGO_BLAKE3 = 0x30 /* BLAKE3 (future) */ +} H5PL_sig_algo_t; + +/* Signature footer on-disk size (12 bytes) */ +#define H5PL_SIG_FOOTER_SIZE 12 + +/* True when algo id selects an RSA-PSS padding variant */ +#define H5PL_SIG_ALGO_IS_PSS(id) ((id) >= H5PL_SIG_ALGO_SHA256_PSS && (id) <= H5PL_SIG_ALGO_SHA512_PSS) + +/* Maximum RSA signature size in bytes. + * A 4096-bit RSA key produces a 512-byte signature; 1024 bytes allows + * headroom for 8192-bit keys. Used by both the signer and verifier. */ +#define H5PL_MAX_SIGNATURE_SIZE 1024 + +/* Maximum plugin file size (1GB). Shared between the library verifier + * and the h5sign tool to keep the limit in sync. */ +#define H5PL_MAX_PLUGIN_SIZE (1024LL * 1024LL * 1024LL) + +/* Signature footer structure + * + * On-disk layout (12 bytes, little-endian): + * [magic: 4][sig_len: 4][algo_id: 1][format_ver: 1][reserved: 2] + * + * Note: Magic is encoded first so it can be verified before interpreting + * remaining fields. Always decode from byte buffer using + * little-endian byte order. Never read directly into this struct + * due to endianness portability (the on-disk format is always + * little-endian, but host byte order varies). + */ +typedef struct H5PL_sig_footer_t { + uint32_t magic; /* Magic number H5PL_SIG_MAGIC */ + uint32_t signature_length; /* Length of RSA signature in bytes */ + H5PL_sig_algo_t algorithm_id; /* Hash algorithm identifier */ + uint8_t format_version; /* Footer format version */ + uint16_t reserved; /* Reserved for future use */ +} H5PL_sig_footer_t; + +/*------------------------------------------------------------------------- + * Function: H5PL_sig_encode_footer + * + * Purpose: Encode a signature footer struct into a little-endian buffer + * suitable for appending to a signed plugin file. + * + * Note: Requires H5encode.h for UINT32ENCODE / UINT16ENCODE. + * buf_size must be >= H5PL_SIG_FOOTER_SIZE (12). + *------------------------------------------------------------------------- + */ +static inline void +H5PL_sig_encode_footer(uint8_t *buf, size_t buf_size, const H5PL_sig_footer_t *footer) +{ + uint8_t *p = buf; + + assert(buf_size >= H5PL_SIG_FOOTER_SIZE); + (void)buf_size; /* used only by assert */ + + UINT32ENCODE(p, footer->magic); /* bytes 0-3 */ + UINT32ENCODE(p, footer->signature_length); /* bytes 4-7 */ + *p++ = (uint8_t)footer->algorithm_id; /* byte 8 */ + *p++ = footer->format_version; /* byte 9 */ + UINT16ENCODE(p, footer->reserved); /* bytes 10-11 */ +} /* end H5PL_sig_encode_footer() */ + +/*------------------------------------------------------------------------- + * Function: H5PL_sig_decode_footer + * + * Purpose: Decode a little-endian buffer into a footer struct and + * perform minimal validation (magic and format version). + * + * Return: true — footer decoded and valid + * false — magic mismatch or unsupported format version + * + * Note: Requires H5encode.h for UINT32DECODE / UINT16DECODE. + * buf_size must be >= H5PL_SIG_FOOTER_SIZE (12). + *------------------------------------------------------------------------- + */ +static inline bool +H5PL_sig_decode_footer(const uint8_t *buf, size_t buf_size, H5PL_sig_footer_t *footer) +{ + const uint8_t *p = buf; + + if (buf_size < H5PL_SIG_FOOTER_SIZE) + return false; + + /* Decode and verify magic first */ + UINT32DECODE(p, footer->magic); /* bytes 0-3 */ + if (footer->magic != H5PL_SIG_MAGIC) + return false; + + UINT32DECODE(p, footer->signature_length); /* bytes 4-7 */ + footer->algorithm_id = (H5PL_sig_algo_t)*p++; /* byte 8 */ + footer->format_version = *p++; /* byte 9 */ + UINT16DECODE(p, footer->reserved); /* bytes 10-11 */ + + /* Verify format version. + * Currently only version 1 exists. When a new version is introduced, + * add backward-compatible decoding here (e.g. accept versions 1..N) + * so that plugins signed with an older format remain loadable. */ + if (footer->format_version < 1 || footer->format_version > H5PL_SIG_FORMAT_VERSION_CURRENT) + return false; + + return true; +} /* end H5PL_sig_decode_footer() */ + +#endif /* H5PLsig_H */ diff --git a/src/H5private.h b/src/H5private.h index fd613214771..e34f3f80b9b 100644 --- a/src/H5private.h +++ b/src/H5private.h @@ -990,6 +990,7 @@ typedef enum { H5_PKG_MM, /* Core memory management */ H5_PKG_O, /* Object headers */ H5_PKG_P, /* Property lists */ + H5_PKG_PL, /* Plugins */ H5_PKG_S, /* Dataspaces */ H5_PKG_T, /* Datatypes */ H5_PKG_V, /* Vector functions */ diff --git a/src/H5pubconf.h.in b/src/H5pubconf.h.in index 82fc2f5f804..afeb50056f9 100644 --- a/src/H5pubconf.h.in +++ b/src/H5pubconf.h.in @@ -667,4 +667,7 @@ /* Define to `long' if does not define. */ #cmakedefine H5_ssize_t +/* Define if plugin digital signature verification is required */ +#cmakedefine H5_REQUIRE_DIGITAL_SIGNATURE @H5_REQUIRE_DIGITAL_SIGNATURE@ + #endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7ee9dd28b5d..ea13fb5f83f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -7,6 +7,9 @@ cmake_minimum_required (VERSION 3.26) project (HDF5_TEST C) +# Include plugin signing helper function +include(${HDF5_SOURCE_DIR}/config/cmake/SignPlugin.cmake) + #----------------------------------------------------------------------------- # Generate the H5srcdir_str.h file containing user settings needed by compilation #----------------------------------------------------------------------------- @@ -138,6 +141,64 @@ if (BUILD_SHARED_LIBS) file (MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/filter_plugin_dir1") file (MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/filter_plugin_dir2") + #----------------------------------------------------------------------------- + # Generate test RSA key pair for plugin signing if signature verification is enabled + #----------------------------------------------------------------------------- + if (HDF5_REQUIRE_SIGNED_PLUGINS) + find_program(OPENSSL_EXECUTABLE openssl) + if (NOT OPENSSL_EXECUTABLE) + message(FATAL_ERROR + "OpenSSL executable not found but is required for HDF5_REQUIRE_SIGNED_PLUGINS.\n" + "Please install OpenSSL or disable HDF5_REQUIRE_SIGNED_PLUGINS.") + endif () + + set(TEST_PRIVATE_KEY "${CMAKE_BINARY_DIR}/private.pem") + set(TEST_PUBLIC_KEY "${CMAKE_BINARY_DIR}/public.pem") + + # Check if keys already exist + if (NOT EXISTS "${TEST_PRIVATE_KEY}" OR NOT EXISTS "${TEST_PUBLIC_KEY}") + message(STATUS "Generating test RSA key pair for plugin signing...") + + # Generate private key + execute_process( + COMMAND ${OPENSSL_EXECUTABLE} genrsa -out "${TEST_PRIVATE_KEY}" 2048 + RESULT_VARIABLE GENKEY_RESULT + OUTPUT_QUIET + ERROR_QUIET + ) + + if (NOT GENKEY_RESULT EQUAL 0) + message(FATAL_ERROR "Failed to generate test private key") + endif () + + # Generate public key + execute_process( + COMMAND ${OPENSSL_EXECUTABLE} rsa -in "${TEST_PRIVATE_KEY}" -pubout -out "${TEST_PUBLIC_KEY}" + RESULT_VARIABLE GENPUB_RESULT + OUTPUT_QUIET + ERROR_QUIET + ) + + if (NOT GENPUB_RESULT EQUAL 0) + message(FATAL_ERROR "Failed to generate test public key") + endif () + + message(STATUS "Test RSA key pair generated successfully:") + message(STATUS " Private key: ${TEST_PRIVATE_KEY}") + message(STATUS " Public key: ${TEST_PUBLIC_KEY}") + else () + message(STATUS "Using existing test RSA key pair:") + message(STATUS " Private key: ${TEST_PRIVATE_KEY}") + message(STATUS " Public key: ${TEST_PUBLIC_KEY}") + endif () + + # Create a test keystore directory containing the test public key + set(TEST_KEYSTORE_DIR "${CMAKE_BINARY_DIR}/test_keystore") + file(MAKE_DIRECTORY "${TEST_KEYSTORE_DIR}") + file(COPY_FILE "${TEST_PUBLIC_KEY}" "${TEST_KEYSTORE_DIR}/test_public.pem") + message(STATUS "Test keystore: ${TEST_KEYSTORE_DIR}") + endif () + #----------------------------------------------------------------------------- # Define Filter Plugin Test Sources #----------------------------------------------------------------------------- @@ -180,6 +241,11 @@ if (BUILD_SHARED_LIBS) "$" "${CMAKE_BINARY_DIR}/filter_plugin_dir1/$" ) + + #----------------------------------------------------------------------------- + # Sign the filter plugin if signature verification is enabled + #----------------------------------------------------------------------------- + sign_plugin_target(${HDF5_TEST_PLUGIN_TARGET} "${CMAKE_BINARY_DIR}/filter_plugin_dir1") endforeach () foreach (plugin_name ${FILTER_PLUGINS_FOR_DIR2}) @@ -212,6 +278,11 @@ if (BUILD_SHARED_LIBS) "$" "${CMAKE_BINARY_DIR}/filter_plugin_dir2/$" ) + + #----------------------------------------------------------------------------- + # Sign the filter plugin if signature verification is enabled + #----------------------------------------------------------------------------- + sign_plugin_target(${HDF5_TEST_PLUGIN_TARGET} "${CMAKE_BINARY_DIR}/filter_plugin_dir2") endforeach () ################################################################################# @@ -256,6 +327,11 @@ if (BUILD_SHARED_LIBS) "$" "${CMAKE_BINARY_DIR}/null_vfd_plugin_dir/$" ) + + #----------------------------------------------------------------------------- + # Sign the VFD plugin if signature verification is enabled + #----------------------------------------------------------------------------- + sign_plugin_target(${HDF5_VFD_PLUGIN_LIB_TARGET} "${CMAKE_BINARY_DIR}/null_vfd_plugin_dir") endforeach () ################################################################################# @@ -300,6 +376,11 @@ if (BUILD_SHARED_LIBS) "$" "${CMAKE_BINARY_DIR}/null_vol_plugin_dir/$" ) + + #----------------------------------------------------------------------------- + # Sign the VOL plugin if signature verification is enabled + #----------------------------------------------------------------------------- + sign_plugin_target(${HDF5_VOL_PLUGIN_LIB_TARGET} "${CMAKE_BINARY_DIR}/null_vol_plugin_dir") endforeach () endif () @@ -786,6 +867,25 @@ if (BUILD_SHARED_LIBS) if (HDF5_ENABLE_FORMATTERS) clang_format (HDF5_TEST_vol_plugin_FORMAT vol_plugin) endif () + + # Plugin signature verification test (only when signature verification is enabled) + if (HDF5_REQUIRE_SIGNED_PLUGINS) + add_executable (test_plugin_signature ${HDF5_TEST_SOURCE_DIR}/test_plugin_signature.c) + target_include_directories (test_plugin_signature PRIVATE "${HDF5_SRC_INCLUDE_DIRS};${HDF5_SRC_BINARY_DIR};$<$:${MPI_C_INCLUDE_DIRS}>") + TARGET_C_PROPERTIES (test_plugin_signature SHARED) + target_link_libraries (test_plugin_signature PRIVATE ${HDF5_TEST_LIBSH_TARGET}) + set_target_properties (test_plugin_signature PROPERTIES FOLDER test) + + # Link OpenSSL for signature verification + target_link_libraries (test_plugin_signature PRIVATE OpenSSL::Crypto) + + #----------------------------------------------------------------------------- + # Add Target to clang-format + #----------------------------------------------------------------------------- + if (HDF5_ENABLE_FORMATTERS) + clang_format (HDF5_TEST_test_plugin_signature_FORMAT test_plugin_signature) + endif () + endif () endif () ############################################################################## diff --git a/test/CMakeTests.cmake b/test/CMakeTests.cmake index e954e137b0c..f3972aba498 100644 --- a/test/CMakeTests.cmake +++ b/test/CMakeTests.cmake @@ -990,13 +990,31 @@ if (BUILD_SHARED_LIBS) endif () add_test (NAME H5PLUGIN-filter_plugin COMMAND $) + set (H5PLUGIN_FILTER_ENV "HDF5_PLUGIN_PATH=${CMAKE_BINARY_DIR}/filter_plugin_dir1${CMAKE_SEP}${CMAKE_BINARY_DIR}/filter_plugin_dir2${CMAKE_SEP};HDF5_VOL_CONNECTOR=;srcdir=${HDF5_TEST_BINARY_DIR}") + if (HDF5_REQUIRE_SIGNED_PLUGINS) + list (APPEND H5PLUGIN_FILTER_ENV "HDF5_PLUGIN_KEYSTORE=${CMAKE_BINARY_DIR}/test_keystore") + endif () set_tests_properties (H5PLUGIN-filter_plugin PROPERTIES - ENVIRONMENT "HDF5_PLUGIN_PATH=${CMAKE_BINARY_DIR}/filter_plugin_dir1${CMAKE_SEP}${CMAKE_BINARY_DIR}/filter_plugin_dir2${CMAKE_SEP};HDF5_VOL_CONNECTOR=;srcdir=${HDF5_TEST_BINARY_DIR}" + ENVIRONMENT "${H5PLUGIN_FILTER_ENV}" WORKING_DIRECTORY ${HDF5_TEST_BINARY_DIR} ) if ("H5PLUGIN-filter_plugin" MATCHES "${HDF5_DISABLE_TESTS_REGEX}") set_tests_properties (H5PLUGIN-filter_plugin PROPERTIES DISABLED true) endif () + + # Add plugin signature verification test (only when signature verification is enabled) + if (HDF5_REQUIRE_SIGNED_PLUGINS) + add_test (NAME H5PLUGIN-signature-verification COMMAND $) + set_tests_properties (H5PLUGIN-signature-verification PROPERTIES + ENVIRONMENT "srcdir=${HDF5_TEST_BINARY_DIR};HDF5_TEST_PRIVATE_KEY=${CMAKE_BINARY_DIR}/private.pem;HDF5_PLUGIN_KEYSTORE=${CMAKE_BINARY_DIR}/test_keystore" + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:${CMAKE_TEST_OUTPUT_DIRECTORY}" + WORKING_DIRECTORY ${HDF5_TEST_BINARY_DIR} + LABELS "H5PLUGIN" + ) + if ("H5PLUGIN-signature-verification" MATCHES "${HDF5_DISABLE_TESTS_REGEX}") + set_tests_properties (H5PLUGIN-signature-verification PROPERTIES DISABLED true) + endif () + endif () endif () option (HDF5_TEST_SHELL_SCRIPTS "Enable shell script tests" ON) @@ -1088,8 +1106,12 @@ if (BUILD_SHARED_LIBS) endif () add_test (NAME H5PLUGIN-vol_plugin COMMAND $) + set (H5PLUGIN_VOL_ENV "HDF5_PLUGIN_PATH=${CMAKE_BINARY_DIR}/null_vol_plugin_dir;srcdir=${HDF5_TEST_BINARY_DIR};HDF5_VOL_CONNECTOR=") + if (HDF5_REQUIRE_SIGNED_PLUGINS) + list (APPEND H5PLUGIN_VOL_ENV "HDF5_PLUGIN_KEYSTORE=${CMAKE_BINARY_DIR}/test_keystore") + endif () set_tests_properties (H5PLUGIN-vol_plugin PROPERTIES - ENVIRONMENT "HDF5_PLUGIN_PATH=${CMAKE_BINARY_DIR}/null_vol_plugin_dir;srcdir=${HDF5_TEST_BINARY_DIR};HDF5_VOL_CONNECTOR=" + ENVIRONMENT "${H5PLUGIN_VOL_ENV}" WORKING_DIRECTORY ${HDF5_TEST_BINARY_DIR} ) if ("H5PLUGIN-vol_plugin" MATCHES "${HDF5_DISABLE_TESTS_REGEX}") diff --git a/test/test_plugin_signature.c b/test/test_plugin_signature.c new file mode 100644 index 00000000000..7e92a1aa584 --- /dev/null +++ b/test/test_plugin_signature.c @@ -0,0 +1,1305 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Copyright by The HDF Group. * + * All rights reserved. * + * * + * This file is part of HDF5. The full HDF5 copyright notice, including * + * terms governing use, modification, and redistribution, is contained in * + * the LICENSE file, which can be found at the root of the source code * + * distribution tree, or in https://www.hdfgroup.org/licenses. * + * If you do not have access to either file, you may request a copy from * + * help@hdfgroup.org. * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +/* + * Purpose: Comprehensive tests for HDF5 plugin signature verification + * + * This test suite verifies that the plugin signature verification + * system correctly handles: + * 1. Valid signed plugins (should load successfully) + * 2. Unsigned plugins (should be rejected) + * 3. Tampered plugins (should be rejected) + * 4. Plugins with invalid signatures (should be rejected) + */ + +#include "h5test.h" +#include "H5srcdir.h" + +/* + * This file needs to access private datatypes from the H5PL package. + */ +#define H5PL_FRIEND +#include "H5PLpkg.h" +#include "H5PLsig.h" + +#ifdef H5_REQUIRE_DIGITAL_SIGNATURE + +#include +#include + +/* Test filter ID */ +#define TEST_SIGNATURE_FILTER_ID 260 + +/* Test files */ +static const char *PLUGIN_DIR = "test_plugin_signature_dir"; +static const char *SIGNED_PLUGIN = "libh5test_sig_filter.so"; +static const char *UNSIGNED_PLUGIN = "libh5test_sig_filter_unsigned.so"; +static const char *TAMPERED_PLUGIN = "libh5test_sig_filter_tampered.so"; +static const char *BAD_SIG_PLUGIN = "libh5test_sig_filter_badsig.so"; +static const char *NO_FOOTER_PLUGIN = "libh5test_sig_filter_nofooter.so"; +static const char *CORRUPT_MAGIC_PLUGIN = "libh5test_sig_filter_badmagic.so"; + +/* Test key paths (set via environment or compile-time) */ +static char test_private_key[1024] = ""; +static char test_public_key[1024] = ""; + +/*------------------------------------------------------------------------- + * Function: create_dummy_plugin + * + * Purpose: Create a minimal valid plugin binary for testing + * This creates a simple binary file that can be used as + * a base for signature testing. + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +create_dummy_plugin(const char *path) +{ + int fd; + herr_t ret_value = SUCCEED; + + /* Create minimal plugin file - just some dummy binary data */ + const unsigned char dummy_data[] = {/* ELF header magic for shared library (simplified) */ + 0x7f, 'E', 'L', 'F', /* Magic number */ + 0x02, 0x01, 0x01, 0x00, /* 64-bit, little-endian, current version */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* Padding */ + /* Some dummy content to make it a reasonable size */ + 'T', 'E', 'S', 'T', ' ', 'P', 'L', 'U', 'G', 'I', 'N', '\0'}; + + if ((fd = HDopen(path, O_WRONLY | O_CREAT | O_TRUNC, 0644)) < 0) { + fprintf(stderr, "Failed to create plugin file: %s\n", path); + return FAIL; + } + + if (HDwrite(fd, dummy_data, sizeof(dummy_data)) < 0) { + fprintf(stderr, "Failed to write plugin data: %s\n", path); + HDclose(fd); + return FAIL; + } + + HDclose(fd); + return SUCCEED; +} + +/*------------------------------------------------------------------------- + * Function: sign_plugin_file + * + * Purpose: Sign a plugin file using the h5sign tool + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +sign_plugin_file(const char *plugin_path, const char *private_key_path) +{ + char cmd[2048]; + int result; + herr_t ret_value = SUCCEED; + + /* Build command to sign the plugin using h5sign tool (quote paths for safety) */ + snprintf(cmd, sizeof(cmd), "h5sign -p \"%s\" -k \"%s\" 2>&1", plugin_path, private_key_path); + + result = system(cmd); + if (result != 0) { + fprintf(stderr, "Failed to sign plugin: %s\n", plugin_path); + return FAIL; + } + + return SUCCEED; +} + +/*------------------------------------------------------------------------- + * Function: append_bad_signature + * + * Purpose: Append an invalid signature to a plugin file + * This creates a plugin that has a signature footer but + * with an incorrect signature value. + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +append_bad_signature(const char *plugin_path) +{ + int fd; + H5PL_sig_footer_t footer; + unsigned char bad_signature[256]; + size_t i; + herr_t ret_value = SUCCEED; + + /* Create a dummy bad signature (just random bytes) */ + for (i = 0; i < sizeof(bad_signature); i++) + bad_signature[i] = (unsigned char)(i * 7 + 13); /* Arbitrary pattern */ + + /* Open plugin file in append mode */ + if ((fd = HDopen(plugin_path, O_WRONLY | O_APPEND, 0)) < 0) { + fprintf(stderr, "Failed to open plugin for bad signature: %s\n", plugin_path); + return FAIL; + } + + /* Write bad signature */ + if (HDwrite(fd, bad_signature, sizeof(bad_signature)) < 0) { + fprintf(stderr, "Failed to write bad signature\n"); + HDclose(fd); + return FAIL; + } + + /* Write footer with correct format but pointing to bad signature */ + footer.signature_length = sizeof(bad_signature); + footer.algorithm_id = H5PL_SIG_ALGO_SHA256; + footer.format_version = 1; + footer.reserved = 0; + footer.magic = H5PL_SIG_MAGIC; + + /* Encode footer in little-endian (as expected by verification code) */ + { + unsigned char footer_bytes[H5PL_SIG_FOOTER_SIZE]; + + H5PL_sig_encode_footer(footer_bytes, sizeof(footer_bytes), &footer); + + if (HDwrite(fd, footer_bytes, sizeof(footer_bytes)) < 0) { + fprintf(stderr, "Failed to write footer\n"); + HDclose(fd); + return FAIL; + } + } + + HDclose(fd); + return SUCCEED; +} + +/*------------------------------------------------------------------------- + * Function: append_corrupt_footer + * + * Purpose: Append a footer with corrupted magic number + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +append_corrupt_footer(const char *plugin_path) +{ + int fd; + unsigned char footer_bytes[H5PL_SIG_FOOTER_SIZE]; + herr_t ret_value = SUCCEED; + + if ((fd = HDopen(plugin_path, O_WRONLY | O_APPEND, 0)) < 0) { + fprintf(stderr, "Failed to open plugin for corrupt footer: %s\n", plugin_path); + return FAIL; + } + + /* Write footer with wrong magic number */ + { + H5PL_sig_footer_t footer; + + footer.magic = 0xDEADBEEF; /* Wrong magic */ + footer.signature_length = 256; + footer.algorithm_id = H5PL_SIG_ALGO_SHA256; + footer.format_version = 1; + footer.reserved = 0; + + H5PL_sig_encode_footer(footer_bytes, sizeof(footer_bytes), &footer); + } + + if (HDwrite(fd, footer_bytes, sizeof(footer_bytes)) < 0) { + fprintf(stderr, "Failed to write corrupt footer\n"); + HDclose(fd); + return FAIL; + } + + HDclose(fd); + return SUCCEED; +} + +/*------------------------------------------------------------------------- + * Function: tamper_with_plugin + * + * Purpose: Modify a signed plugin to invalidate its signature + * This simulates an attacker tampering with a signed plugin. + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +tamper_with_plugin(const char *plugin_path) +{ + int fd; + char byte; + herr_t ret_value = SUCCEED; + + /* Open plugin and modify the first byte of content */ + if ((fd = HDopen(plugin_path, O_RDWR, 0)) < 0) { + fprintf(stderr, "Failed to open plugin for tampering: %s\n", plugin_path); + return FAIL; + } + + /* Read first byte */ + if (HDread(fd, &byte, 1) < 0) { + fprintf(stderr, "Failed to read plugin byte\n"); + HDclose(fd); + return FAIL; + } + + /* Modify it */ + byte ^= 0xFF; + + /* Seek back and write modified byte */ + if (HDlseek(fd, 0, SEEK_SET) < 0) { + fprintf(stderr, "Failed to seek plugin\n"); + HDclose(fd); + return FAIL; + } + + if (HDwrite(fd, &byte, 1) < 0) { + fprintf(stderr, "Failed to write modified byte\n"); + HDclose(fd); + return FAIL; + } + + HDclose(fd); + return SUCCEED; +} + +/*------------------------------------------------------------------------- + * Function: generate_rsa_keypair + * + * Purpose: Generate RSA key pair using OpenSSL command line + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +generate_rsa_keypair(int bits, const char *private_path, const char *public_path) +{ + char cmd[2048]; + int result; + + /* Generate private key */ +#ifdef H5_HAVE_WIN32_API + snprintf(cmd, sizeof(cmd), "openssl genrsa -out %s %d >NUL 2>&1", private_path, bits); +#else + snprintf(cmd, sizeof(cmd), "openssl genrsa -out %s %d 2>&1 >/dev/null", private_path, bits); +#endif + result = system(cmd); + if (result != 0) { + fprintf(stderr, "Failed to generate RSA-%d private key: %s\n", bits, private_path); + return FAIL; + } + + /* Extract public key */ +#ifdef H5_HAVE_WIN32_API + snprintf(cmd, sizeof(cmd), "openssl rsa -in %s -pubout -out %s >NUL 2>&1", private_path, public_path); +#else + snprintf(cmd, sizeof(cmd), "openssl rsa -in %s -pubout -out %s 2>&1 >/dev/null", private_path, + public_path); +#endif + result = system(cmd); + if (result != 0) { + fprintf(stderr, "Failed to extract RSA-%d public key: %s\n", bits, public_path); + return FAIL; + } + + return SUCCEED; +} + +/*------------------------------------------------------------------------- + * Function: create_keystore_directory + * + * Purpose: Create a KeyStore directory with specific permissions + * + * Return: Allocated path string (caller must free), NULL on failure + *------------------------------------------------------------------------- + */ +static char * +create_keystore_directory(const char *base_dir, const char *dir_name, unsigned permissions) +{ + char full_path[1024]; + char *ret_value = NULL; + + snprintf(full_path, sizeof(full_path), "%s/%s", base_dir, dir_name); + + if (HDmkdir(full_path, permissions) < 0) { + fprintf(stderr, "Failed to create keystore directory: %s\n", full_path); + return NULL; + } + +#ifdef H5_HAVE_WIN32_API + /* Restrict ACLs so the keystore permission check passes. + * Remove inherited permissions and grant write only to Administrators + * and the current user (mimicking a secure keystore on Windows). */ + { + char cmd[1280]; + snprintf(cmd, sizeof(cmd), + "icacls \"%s\" /inheritance:r /grant \"Administrators:(OI)(CI)F\"" + " /grant \"%%USERNAME%%:(OI)(CI)F\" >NUL 2>&1", + full_path); + (void)system(cmd); + } +#endif + + ret_value = strdup(full_path); + return ret_value; +} + +/*------------------------------------------------------------------------- + * Function: add_key_to_keystore + * + * Purpose: Copy a PEM key file into a KeyStore directory + * + * Return: Allocated destination path (caller must free), NULL on failure + *------------------------------------------------------------------------- + */ +static char * +add_key_to_keystore(const char *keystore_dir, const char *key_name, const char *key_source) +{ + char dest_path[1024]; + char cmd[2048]; + char *ret_value = NULL; + + snprintf(dest_path, sizeof(dest_path), "%s/%s", keystore_dir, key_name); + + /* Copy file using C standard I/O (portable across all platforms) */ + { + FILE *src, *dst; + unsigned char buf[4096]; + size_t n; + int copy_ok = 1; + + if (NULL == (src = fopen(key_source, "rb"))) { + fprintf(stderr, "Failed to open source key: %s\n", key_source); + return NULL; + } + if (NULL == (dst = fopen(dest_path, "wb"))) { + fprintf(stderr, "Failed to open dest key: %s\n", dest_path); + fclose(src); + return NULL; + } + while ((n = fread(buf, 1, sizeof(buf), src)) > 0) { + if (fwrite(buf, 1, n, dst) != n) { + copy_ok = 0; + break; + } + } + fclose(src); + fclose(dst); + if (!copy_ok) { + fprintf(stderr, "Failed to copy key to keystore: %s -> %s\n", key_source, dest_path); + return NULL; + } + } + + ret_value = strdup(dest_path); + return ret_value; +} + +/*------------------------------------------------------------------------- + * Function: create_corrupted_pem + * + * Purpose: Create corrupted/invalid PEM files for testing + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +typedef enum { + PEM_CORRUPT_TRUNCATED, /* Cut off mid-key */ + PEM_CORRUPT_GARBAGE, /* Random binary data */ + PEM_CORRUPT_WRONG_FORMAT, /* Missing BEGIN/END markers */ + PEM_CORRUPT_WRONG_KEY_TYPE /* ECDSA instead of RSA */ +} corruption_type_t; + +static herr_t +create_corrupted_pem(const char *path, corruption_type_t type) +{ + int fd; + herr_t ret_value = SUCCEED; + + switch (type) { + case PEM_CORRUPT_TRUNCATED: { + /* Create truncated PEM - write partial header */ + const char truncated[] = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB"; + if ((fd = HDopen(path, O_WRONLY | O_CREAT | O_TRUNC, 0644)) < 0) + return FAIL; + HDwrite(fd, truncated, strlen(truncated)); + HDclose(fd); + break; + } + + case PEM_CORRUPT_GARBAGE: { + /* Write random binary garbage */ + unsigned char garbage[256]; + size_t i; + for (i = 0; i < sizeof(garbage); i++) + garbage[i] = (unsigned char)(i * 13 + 7); + if ((fd = HDopen(path, O_WRONLY | O_CREAT | O_TRUNC, 0644)) < 0) + return FAIL; + HDwrite(fd, garbage, sizeof(garbage)); + HDclose(fd); + break; + } + + case PEM_CORRUPT_WRONG_FORMAT: { + /* Write text without PEM markers */ + const char wrong[] = "This is not a PEM file\nJust some random text\nNo markers here\n"; + if ((fd = HDopen(path, O_WRONLY | O_CREAT | O_TRUNC, 0644)) < 0) + return FAIL; + HDwrite(fd, wrong, strlen(wrong)); + HDclose(fd); + break; + } + + case PEM_CORRUPT_WRONG_KEY_TYPE: { + /* Generate ECDSA key instead of RSA */ + char cmd[2048]; +#ifdef H5_HAVE_WIN32_API + snprintf(cmd, sizeof(cmd), + "openssl ecparam -genkey -name prime256v1 -noout | openssl ec -pubout -out %s >NUL 2>&1", + path); +#else + snprintf(cmd, sizeof(cmd), + "openssl ecparam -genkey -name prime256v1 -noout | openssl ec -pubout -out %s 2>&1 " + ">/dev/null", + path); +#endif + if (system(cmd) != 0) { + /* If ECDSA generation fails, just write invalid RSA-like content */ + const char fake_ecdsa[] = + "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE\n-----END PUBLIC " + "KEY-----\n"; + if ((fd = HDopen(path, O_WRONLY | O_CREAT | O_TRUNC, 0644)) < 0) + return FAIL; + HDwrite(fd, fake_ecdsa, strlen(fake_ecdsa)); + HDclose(fd); + } + break; + } + + default: + return FAIL; + } + + return SUCCEED; +} + +/*------------------------------------------------------------------------- + * Function: reset_keystore_state + * + * Purpose: Reset KeyStore global state between tests + * This ensures test isolation + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +reset_keystore_state(void) +{ + /* Cleanup keystore to force reinitialization. + * This allows tests to use different KeyStore directories. + */ + if (H5PL__cleanup_signature_resources() < 0) { + fprintf(stderr, "Failed to cleanup signature resources\n"); + return FAIL; + } + return SUCCEED; +} + +/*------------------------------------------------------------------------- + * Function: setup_test_environment + * + * Purpose: Set up the test environment with various test plugins + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +setup_test_environment(void) +{ + char plugin_path[1024]; + char temp_path[1024]; + + /* Create plugin directory */ + HDmkdir(PLUGIN_DIR, 0755); + + /* Get test keys from environment or use defaults */ + if (getenv("HDF5_TEST_PRIVATE_KEY")) { + snprintf(test_private_key, sizeof(test_private_key), "%s", getenv("HDF5_TEST_PRIVATE_KEY")); + } + else { + /* Try to find test keys in common locations */ + snprintf(test_private_key, sizeof(test_private_key), "%s/ci-test-private.pem", H5_get_srcdir()); + } + + if (getenv("HDF5_TEST_PUBLIC_KEY")) { + snprintf(test_public_key, sizeof(test_public_key), "%s", getenv("HDF5_TEST_PUBLIC_KEY")); + } + else { + snprintf(test_public_key, sizeof(test_public_key), "%s/ci-test-public.pem", H5_get_srcdir()); + } + + /* Verify keys exist */ + if (access(test_private_key, R_OK) != 0) { + fprintf(stderr, "Test private key not found: %s\n", test_private_key); + fprintf(stderr, "Set HDF5_TEST_PRIVATE_KEY environment variable or generate keys\n"); + return FAIL; + } + + /* 1. Create and sign a valid plugin */ + snprintf(plugin_path, sizeof(plugin_path), "%s/%s", PLUGIN_DIR, SIGNED_PLUGIN); + if (create_dummy_plugin(plugin_path) < 0) + return FAIL; + if (sign_plugin_file(plugin_path, test_private_key) < 0) + return FAIL; + + /* 2. Create an unsigned plugin */ + snprintf(plugin_path, sizeof(plugin_path), "%s/%s", PLUGIN_DIR, UNSIGNED_PLUGIN); + if (create_dummy_plugin(plugin_path) < 0) + return FAIL; + + /* 3. Create a signed plugin then tamper with it */ + snprintf(plugin_path, sizeof(plugin_path), "%s/%s", PLUGIN_DIR, TAMPERED_PLUGIN); + if (create_dummy_plugin(plugin_path) < 0) + return FAIL; + if (sign_plugin_file(plugin_path, test_private_key) < 0) + return FAIL; + if (tamper_with_plugin(plugin_path) < 0) + return FAIL; + + /* 4. Create plugin with bad signature */ + snprintf(plugin_path, sizeof(plugin_path), "%s/%s", PLUGIN_DIR, BAD_SIG_PLUGIN); + if (create_dummy_plugin(plugin_path) < 0) + return FAIL; + if (append_bad_signature(plugin_path) < 0) + return FAIL; + + /* 5. Create plugin with no footer */ + snprintf(plugin_path, sizeof(plugin_path), "%s/%s", PLUGIN_DIR, NO_FOOTER_PLUGIN); + if (create_dummy_plugin(plugin_path) < 0) + return FAIL; + + /* 6. Create plugin with corrupted magic number */ + snprintf(plugin_path, sizeof(plugin_path), "%s/%s", PLUGIN_DIR, CORRUPT_MAGIC_PLUGIN); + if (create_dummy_plugin(plugin_path) < 0) + return FAIL; + if (append_corrupt_footer(plugin_path) < 0) + return FAIL; + + return SUCCEED; +} + +/*------------------------------------------------------------------------- + * Function: cleanup_test_environment + * + * Purpose: Clean up test files + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +cleanup_test_environment(void) +{ + char cmd[1024]; + + /* Remove plugin directory (includes all KeyStore subdirectories) */ +#ifdef H5_HAVE_WIN32_API + snprintf(cmd, sizeof(cmd), "rmdir /s /q %s >NUL 2>&1", PLUGIN_DIR); + system(cmd); + system("del /q org*_*.pem test_*_4096.pem *_private.pem *_public.pem >NUL 2>&1"); +#else + snprintf(cmd, sizeof(cmd), "rm -rf %s", PLUGIN_DIR); + system(cmd); + system("rm -f org*_*.pem test_*_4096.pem *_private.pem *_public.pem 2>/dev/null"); +#endif + + return SUCCEED; +} + +/*------------------------------------------------------------------------- + * Function: test_valid_signed_plugin + * + * Purpose: Test that a properly signed plugin is accepted + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +test_valid_signed_plugin(void) +{ + char plugin_path[1024]; + herr_t ret_value = SUCCEED; + + TESTING("valid signed plugin verification"); + + snprintf(plugin_path, sizeof(plugin_path), "%s/%s", PLUGIN_DIR, SIGNED_PLUGIN); + + /* Verify the signed plugin */ + if (H5PL__verify_signature_appended(plugin_path) < 0) { + H5_FAILED(); + fprintf(stderr, " Valid signed plugin was rejected\n"); + return FAIL; + } + + PASSED(); + return SUCCEED; +} + +/*------------------------------------------------------------------------- + * Function: test_unsigned_plugin_rejected + * + * Purpose: Test that an unsigned plugin is rejected + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +test_unsigned_plugin_rejected(void) +{ + char plugin_path[1024]; + herr_t status; + + TESTING("unsigned plugin rejection"); + + snprintf(plugin_path, sizeof(plugin_path), "%s/%s", PLUGIN_DIR, UNSIGNED_PLUGIN); + + /* Verification should fail for unsigned plugin */ + H5E_BEGIN_TRY + { + status = H5PL__verify_signature_appended(plugin_path); + } + H5E_END_TRY; + + if (status >= 0) { + H5_FAILED(); + fprintf(stderr, " Unsigned plugin was incorrectly accepted\n"); + return FAIL; + } + + PASSED(); + return SUCCEED; +} + +/*------------------------------------------------------------------------- + * Function: test_tampered_plugin_rejected + * + * Purpose: Test that a tampered plugin is rejected + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +test_tampered_plugin_rejected(void) +{ + char plugin_path[1024]; + herr_t status; + + TESTING("tampered plugin rejection"); + + snprintf(plugin_path, sizeof(plugin_path), "%s/%s", PLUGIN_DIR, TAMPERED_PLUGIN); + + /* Verification should fail for tampered plugin */ + H5E_BEGIN_TRY + { + status = H5PL__verify_signature_appended(plugin_path); + } + H5E_END_TRY; + + if (status >= 0) { + H5_FAILED(); + fprintf(stderr, " Tampered plugin was incorrectly accepted\n"); + return FAIL; + } + + PASSED(); + return SUCCEED; +} + +/*------------------------------------------------------------------------- + * Function: test_bad_signature_rejected + * + * Purpose: Test that a plugin with wrong signature is rejected + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +test_bad_signature_rejected(void) +{ + char plugin_path[1024]; + herr_t status; + + TESTING("plugin with invalid signature rejection"); + + snprintf(plugin_path, sizeof(plugin_path), "%s/%s", PLUGIN_DIR, BAD_SIG_PLUGIN); + + /* Verification should fail for plugin with bad signature */ + H5E_BEGIN_TRY + { + status = H5PL__verify_signature_appended(plugin_path); + } + H5E_END_TRY; + + if (status >= 0) { + H5_FAILED(); + fprintf(stderr, " Plugin with bad signature was incorrectly accepted\n"); + return FAIL; + } + + PASSED(); + return SUCCEED; +} + +/*------------------------------------------------------------------------- + * Function: test_no_footer_rejected + * + * Purpose: Test that a plugin without signature footer is rejected + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +test_no_footer_rejected(void) +{ + char plugin_path[1024]; + herr_t status; + + TESTING("plugin without signature footer rejection"); + + snprintf(plugin_path, sizeof(plugin_path), "%s/%s", PLUGIN_DIR, NO_FOOTER_PLUGIN); + + /* Verification should fail for plugin without footer */ + H5E_BEGIN_TRY + { + status = H5PL__verify_signature_appended(plugin_path); + } + H5E_END_TRY; + + if (status >= 0) { + H5_FAILED(); + fprintf(stderr, " Plugin without footer was incorrectly accepted\n"); + return FAIL; + } + + PASSED(); + return SUCCEED; +} + +/*------------------------------------------------------------------------- + * Function: test_corrupt_magic_rejected + * + * Purpose: Test that a plugin with corrupted magic number is rejected + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +test_corrupt_magic_rejected(void) +{ + char plugin_path[1024]; + herr_t status; + + TESTING("plugin with corrupt magic number rejection"); + + snprintf(plugin_path, sizeof(plugin_path), "%s/%s", PLUGIN_DIR, CORRUPT_MAGIC_PLUGIN); + + /* Verification should fail for plugin with corrupt magic */ + H5E_BEGIN_TRY + { + status = H5PL__verify_signature_appended(plugin_path); + } + H5E_END_TRY; + + if (status >= 0) { + H5_FAILED(); + fprintf(stderr, " Plugin with corrupt magic was incorrectly accepted\n"); + return FAIL; + } + + PASSED(); + return SUCCEED; +} + +/*------------------------------------------------------------------------- + * Function: test_keystore_multiple_keys + * + * Purpose: Test that KeyStore can load and use multiple trusted keys + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +test_keystore_multiple_keys(void) +{ + char *keystore_dir = NULL; + char keystore_path[1024]; + char plugin1_path[1024], plugin2_path[1024], plugin3_path[1024]; + char priv1[1024], pub1[1024]; + char priv2[1024], pub2[1024]; + char priv3[1024], pub3[1024]; + char *key_path = NULL; + herr_t ret_value = SUCCEED; + + TESTING("multiple keys in keystore verification"); + + /* Create KeyStore directory */ + keystore_dir = create_keystore_directory(PLUGIN_DIR, "test_keystore_multiple", 0755); + if (!keystore_dir) { + H5_FAILED(); + fprintf(stderr, "Failed to create keystore directory\n"); + return FAIL; + } + + /* Generate 3 separate key pairs */ + snprintf(priv1, sizeof(priv1), "%s/org1_private.pem", PLUGIN_DIR); + snprintf(pub1, sizeof(pub1), "%s/org1_public.pem", PLUGIN_DIR); + snprintf(priv2, sizeof(priv2), "%s/org2_private.pem", PLUGIN_DIR); + snprintf(pub2, sizeof(pub2), "%s/org2_public.pem", PLUGIN_DIR); + snprintf(priv3, sizeof(priv3), "%s/org3_private.pem", PLUGIN_DIR); + snprintf(pub3, sizeof(pub3), "%s/org3_public.pem", PLUGIN_DIR); + + if (generate_rsa_keypair(2048, priv1, pub1) < 0 || generate_rsa_keypair(2048, priv2, pub2) < 0 || + generate_rsa_keypair(2048, priv3, pub3) < 0) { + H5_FAILED(); + fprintf(stderr, "Failed to generate key pairs\n"); + goto error; + } + + /* Add all 3 public keys to KeyStore */ + key_path = add_key_to_keystore(keystore_dir, "org1.pem", pub1); + if (key_path) + free(key_path); + key_path = add_key_to_keystore(keystore_dir, "org2.pem", pub2); + if (key_path) + free(key_path); + key_path = add_key_to_keystore(keystore_dir, "org3.pem", pub3); + if (key_path) + free(key_path); + + /* Create and sign 3 plugins with different keys */ + snprintf(plugin1_path, sizeof(plugin1_path), "%s/plugin_org1.so", PLUGIN_DIR); + snprintf(plugin2_path, sizeof(plugin2_path), "%s/plugin_org2.so", PLUGIN_DIR); + snprintf(plugin3_path, sizeof(plugin3_path), "%s/plugin_org3.so", PLUGIN_DIR); + + if (create_dummy_plugin(plugin1_path) < 0 || sign_plugin_file(plugin1_path, priv1) < 0) { + H5_FAILED(); + fprintf(stderr, "Failed to create/sign plugin1\n"); + goto error; + } + + if (create_dummy_plugin(plugin2_path) < 0 || sign_plugin_file(plugin2_path, priv2) < 0) { + H5_FAILED(); + fprintf(stderr, "Failed to create/sign plugin2\n"); + goto error; + } + + if (create_dummy_plugin(plugin3_path) < 0 || sign_plugin_file(plugin3_path, priv3) < 0) { + H5_FAILED(); + fprintf(stderr, "Failed to create/sign plugin3\n"); + goto error; + } + + /* Set environment variable to use this KeyStore */ + snprintf(keystore_path, sizeof(keystore_path), "%s", keystore_dir); + HDsetenv("HDF5_PLUGIN_KEYSTORE", keystore_path, 1); + reset_keystore_state(); + + /* Verify all 3 plugins (each should match a different key) */ + if (H5PL__verify_signature_appended(plugin1_path) < 0) { + H5_FAILED(); + fprintf(stderr, "Plugin1 (org1 key) was rejected\n"); + HDunsetenv("HDF5_PLUGIN_KEYSTORE"); + goto error; + } + + if (H5PL__verify_signature_appended(plugin2_path) < 0) { + H5_FAILED(); + fprintf(stderr, "Plugin2 (org2 key) was rejected\n"); + HDunsetenv("HDF5_PLUGIN_KEYSTORE"); + goto error; + } + + if (H5PL__verify_signature_appended(plugin3_path) < 0) { + H5_FAILED(); + fprintf(stderr, "Plugin3 (org3 key) was rejected\n"); + HDunsetenv("HDF5_PLUGIN_KEYSTORE"); + goto error; + } + + /* Cleanup */ + HDunsetenv("HDF5_PLUGIN_KEYSTORE"); + free(keystore_dir); + + PASSED(); + return SUCCEED; + +error: + if (keystore_dir) + free(keystore_dir); + return FAIL; +} + +/*------------------------------------------------------------------------- + * Function: test_invalid_pem_file_handling + * + * Purpose: Test that corrupted PEM files are silently skipped + * and valid keys still work + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +test_invalid_pem_file_handling(void) +{ + char *keystore_dir = NULL; + char keystore_path[1024]; + char valid_priv[1024], valid_pub[1024]; + char plugin_path[1024]; + char corrupt_path[1024]; + char *key_path = NULL; + herr_t ret_value = SUCCEED; + + TESTING("invalid PEM file handling"); + + /* Create KeyStore directory */ + keystore_dir = create_keystore_directory(PLUGIN_DIR, "test_keystore_corrupted", 0755); + if (!keystore_dir) { + H5_FAILED(); + fprintf(stderr, "Failed to create keystore directory\n"); + return FAIL; + } + + /* Generate 1 valid key pair */ + snprintf(valid_priv, sizeof(valid_priv), "%s/valid_private.pem", PLUGIN_DIR); + snprintf(valid_pub, sizeof(valid_pub), "%s/valid_public.pem", PLUGIN_DIR); + if (generate_rsa_keypair(2048, valid_priv, valid_pub) < 0) { + H5_FAILED(); + fprintf(stderr, "Failed to generate valid key pair\n"); + goto error; + } + + /* Add valid key to KeyStore */ + key_path = add_key_to_keystore(keystore_dir, "validkey.pem", valid_pub); + if (key_path) + free(key_path); + + /* Create 4 corrupted PEM files */ + snprintf(corrupt_path, sizeof(corrupt_path), "%s/truncated.pem", keystore_dir); + create_corrupted_pem(corrupt_path, PEM_CORRUPT_TRUNCATED); + + snprintf(corrupt_path, sizeof(corrupt_path), "%s/garbage.pem", keystore_dir); + create_corrupted_pem(corrupt_path, PEM_CORRUPT_GARBAGE); + + snprintf(corrupt_path, sizeof(corrupt_path), "%s/nomarkers.pem", keystore_dir); + create_corrupted_pem(corrupt_path, PEM_CORRUPT_WRONG_FORMAT); + + snprintf(corrupt_path, sizeof(corrupt_path), "%s/ecdsa.pem", keystore_dir); + create_corrupted_pem(corrupt_path, PEM_CORRUPT_WRONG_KEY_TYPE); + + /* Create plugin signed with valid key */ + snprintf(plugin_path, sizeof(plugin_path), "%s/plugin_valid.so", PLUGIN_DIR); + if (create_dummy_plugin(plugin_path) < 0 || sign_plugin_file(plugin_path, valid_priv) < 0) { + H5_FAILED(); + fprintf(stderr, "Failed to create/sign plugin\n"); + goto error; + } + + /* Set environment variable to use this KeyStore */ + snprintf(keystore_path, sizeof(keystore_path), "%s", keystore_dir); + HDsetenv("HDF5_PLUGIN_KEYSTORE", keystore_path, 1); + reset_keystore_state(); + + /* Verify plugin (should succeed - corrupted files silently skipped) */ + if (H5PL__verify_signature_appended(plugin_path) < 0) { + H5_FAILED(); + fprintf(stderr, "Valid plugin was rejected despite corrupted PEM files in KeyStore\n"); + HDunsetenv("HDF5_PLUGIN_KEYSTORE"); + goto error; + } + + /* Cleanup */ + HDunsetenv("HDF5_PLUGIN_KEYSTORE"); + free(keystore_dir); + + PASSED(); + return SUCCEED; + +error: + if (keystore_dir) + free(keystore_dir); + return FAIL; +} + +/*------------------------------------------------------------------------- + * Function: test_rsa4096_signature + * + * Purpose: Test support for RSA-4096 keys (512-byte signatures) + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +test_rsa4096_signature(void) +{ + char priv4096[1024], pub4096[1024]; + char plugin_path[1024]; + char *keystore_dir = NULL; + char keystore_path[1024]; + char *key_path = NULL; + herr_t ret_value = SUCCEED; + + TESTING("RSA-4096 signature verification"); + + /* Create KeyStore directory */ + keystore_dir = create_keystore_directory(PLUGIN_DIR, "test_keystore_rsa4096", 0755); + if (!keystore_dir) { + H5_FAILED(); + fprintf(stderr, "Failed to create keystore directory\n"); + return FAIL; + } + + /* Generate RSA-4096 key pair */ + snprintf(priv4096, sizeof(priv4096), "%s/test_private_4096.pem", PLUGIN_DIR); + snprintf(pub4096, sizeof(pub4096), "%s/test_public_4096.pem", PLUGIN_DIR); + if (generate_rsa_keypair(4096, priv4096, pub4096) < 0) { + H5_FAILED(); + fprintf(stderr, "Failed to generate RSA-4096 key pair\n"); + goto error; + } + + /* Add public key to KeyStore */ + key_path = add_key_to_keystore(keystore_dir, "rsa4096.pem", pub4096); + if (key_path) + free(key_path); + + /* Create and sign plugin with RSA-4096 key */ + snprintf(plugin_path, sizeof(plugin_path), "%s/plugin_rsa4096.so", PLUGIN_DIR); + if (create_dummy_plugin(plugin_path) < 0 || sign_plugin_file(plugin_path, priv4096) < 0) { + H5_FAILED(); + fprintf(stderr, "Failed to create/sign plugin with RSA-4096\n"); + goto error; + } + + /* Set environment variable to use this KeyStore */ + snprintf(keystore_path, sizeof(keystore_path), "%s", keystore_dir); + HDsetenv("HDF5_PLUGIN_KEYSTORE", keystore_path, 1); + reset_keystore_state(); + + /* Verify plugin (RSA-4096 signature is 512 bytes, within 1024-byte limit) */ + if (H5PL__verify_signature_appended(plugin_path) < 0) { + H5_FAILED(); + fprintf(stderr, "RSA-4096 signed plugin was rejected\n"); + HDunsetenv("HDF5_PLUGIN_KEYSTORE"); + goto error; + } + + /* Cleanup */ + HDunsetenv("HDF5_PLUGIN_KEYSTORE"); + free(keystore_dir); + + PASSED(); + return SUCCEED; + +error: + if (keystore_dir) + free(keystore_dir); + return FAIL; +} + +/*------------------------------------------------------------------------- + * Function: test_keystore_symlink_rejection + * + * Purpose: Test that symbolic links in KeyStore are rejected + * (Unix/Linux only - prevents symlink attacks) + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +test_keystore_symlink_rejection(void) +{ +#ifndef H5_HAVE_WIN32_API + char *keystore_dir = NULL; + char keystore_path[1024]; + char trusted_priv[1024], trusted_pub[1024]; + char attacker_priv[1024], attacker_pub[1024]; + char symlink_path[1024]; + char plugin_trusted[1024], plugin_attacker[1024]; + char *key_path = NULL; + herr_t status; + herr_t ret_value = SUCCEED; + + TESTING("symlink rejection in keystore"); + + /* Create KeyStore directory */ + keystore_dir = create_keystore_directory(PLUGIN_DIR, "test_keystore_symlinks", 0755); + if (!keystore_dir) { + H5_FAILED(); + fprintf(stderr, "Failed to create keystore directory\n"); + return FAIL; + } + + /* Generate trusted key pair */ + snprintf(trusted_priv, sizeof(trusted_priv), "%s/trusted_private.pem", PLUGIN_DIR); + snprintf(trusted_pub, sizeof(trusted_pub), "%s/trusted_public.pem", PLUGIN_DIR); + if (generate_rsa_keypair(2048, trusted_priv, trusted_pub) < 0) { + H5_FAILED(); + fprintf(stderr, "Failed to generate trusted key pair\n"); + goto error; + } + + /* Generate attacker key pair (outside KeyStore) */ + snprintf(attacker_priv, sizeof(attacker_priv), "%s/attacker_private.pem", PLUGIN_DIR); + snprintf(attacker_pub, sizeof(attacker_pub), "%s/attacker_public.pem", PLUGIN_DIR); + if (generate_rsa_keypair(2048, attacker_priv, attacker_pub) < 0) { + H5_FAILED(); + fprintf(stderr, "Failed to generate attacker key pair\n"); + goto error; + } + + /* Add legitimate key to KeyStore */ + key_path = add_key_to_keystore(keystore_dir, "trusted.pem", trusted_pub); + if (key_path) + free(key_path); + + /* Create symlink in KeyStore pointing to attacker key */ + snprintf(symlink_path, sizeof(symlink_path), "%s/bad.pem", keystore_dir); + if (symlink(attacker_pub, symlink_path) < 0) { + H5_FAILED(); + fprintf(stderr, "Failed to create symlink\n"); + goto error; + } + + /* Create 2 plugins */ + snprintf(plugin_trusted, sizeof(plugin_trusted), "%s/plugin_trusted.so", PLUGIN_DIR); + snprintf(plugin_attacker, sizeof(plugin_attacker), "%s/plugin_attacker.so", PLUGIN_DIR); + + if (create_dummy_plugin(plugin_trusted) < 0 || sign_plugin_file(plugin_trusted, trusted_priv) < 0) { + H5_FAILED(); + fprintf(stderr, "Failed to create/sign trusted plugin\n"); + goto error; + } + + if (create_dummy_plugin(plugin_attacker) < 0 || sign_plugin_file(plugin_attacker, attacker_priv) < 0) { + H5_FAILED(); + fprintf(stderr, "Failed to create/sign attacker plugin\n"); + goto error; + } + + /* Set environment variable to use this KeyStore */ + snprintf(keystore_path, sizeof(keystore_path), "%s", keystore_dir); + HDsetenv("HDF5_PLUGIN_KEYSTORE", keystore_path, 1); + reset_keystore_state(); + + /* Verify trusted plugin should succeed */ + if (H5PL__verify_signature_appended(plugin_trusted) < 0) { + H5_FAILED(); + fprintf(stderr, "Trusted plugin was rejected\n"); + HDunsetenv("HDF5_PLUGIN_KEYSTORE"); + goto error; + } + + /* Verify attacker plugin should fail (symlink was skipped) */ + H5E_BEGIN_TRY + { + status = H5PL__verify_signature_appended(plugin_attacker); + } + H5E_END_TRY; + + if (status >= 0) { + H5_FAILED(); + fprintf(stderr, "Attacker plugin was incorrectly accepted (symlink not skipped)\n"); + HDunsetenv("HDF5_PLUGIN_KEYSTORE"); + goto error; + } + + /* Cleanup */ + HDunsetenv("HDF5_PLUGIN_KEYSTORE"); + free(keystore_dir); + + PASSED(); + return SUCCEED; + +error: + if (keystore_dir) + free(keystore_dir); + return FAIL; + +#else + /* Windows - skip test */ + TESTING("symlink rejection in keystore (Unix only)"); + SKIPPED(); + return SUCCEED; +#endif +} + +/*------------------------------------------------------------------------- + * Function: main + * + * Purpose: Run plugin signature verification tests + * + * Return: EXIT_SUCCESS/EXIT_FAILURE + *------------------------------------------------------------------------- + */ +int +main(void) +{ + int nerrors = 0; + + printf("Testing HDF5 Plugin Signature Verification\n"); + printf("==========================================\n\n"); + + /* Open the HDF5 library explicitly */ + H5open(); + + /* Set up test environment */ + if (setup_test_environment() < 0) { + fprintf(stderr, "Failed to set up test environment\n"); + return EXIT_FAILURE; + } + + /* Run tests */ + nerrors += test_valid_signed_plugin() < 0 ? 1 : 0; + nerrors += test_unsigned_plugin_rejected() < 0 ? 1 : 0; + nerrors += test_tampered_plugin_rejected() < 0 ? 1 : 0; + nerrors += test_bad_signature_rejected() < 0 ? 1 : 0; + nerrors += test_no_footer_rejected() < 0 ? 1 : 0; + nerrors += test_corrupt_magic_rejected() < 0 ? 1 : 0; + + /* Run KeyStore tests */ + nerrors += test_keystore_multiple_keys() < 0 ? 1 : 0; + nerrors += test_invalid_pem_file_handling() < 0 ? 1 : 0; + nerrors += test_rsa4096_signature() < 0 ? 1 : 0; + nerrors += test_keystore_symlink_rejection() < 0 ? 1 : 0; + + /* Clean up */ + cleanup_test_environment(); + + /* Report results */ + if (nerrors) { + printf("\n***** %d PLUGIN SIGNATURE VERIFICATION TEST%s FAILED *****\n", nerrors, + nerrors > 1 ? "S" : ""); + return EXIT_FAILURE; + } + + printf("\nAll plugin signature verification tests passed.\n"); + return EXIT_SUCCESS; +} + +#else /* H5_REQUIRE_DIGITAL_SIGNATURE */ + +int +main(void) +{ + printf("Plugin signature verification is not enabled.\n"); + printf("Reconfigure with -DHDF5_REQUIRE_SIGNED_PLUGINS=ON to enable these tests.\n"); + return EXIT_SUCCESS; /* Not a failure - feature not enabled */ +} + +#endif /* H5_REQUIRE_DIGITAL_SIGNATURE */ diff --git a/tools/src/CMakeLists.txt b/tools/src/CMakeLists.txt index 9c3ab4a9d22..9e2fb32cf6e 100644 --- a/tools/src/CMakeLists.txt +++ b/tools/src/CMakeLists.txt @@ -67,3 +67,11 @@ add_subdirectory (h5format_convert) #-- h5perf executables add_subdirectory (h5perf) + +#-- Add the h5sign executable (plugin signing tool) +# Build h5sign when plugin signature verification is enabled +# Note: This is separate from HDF5_ENABLE_PLUGIN_SUPPORT which is for external filter plugins. +# h5sign is needed to sign test plugins and any dynamically loaded plugins. +if (HDF5_REQUIRE_SIGNED_PLUGINS) + add_subdirectory (h5sign) +endif () diff --git a/tools/src/h5sign/CMakeLists.txt b/tools/src/h5sign/CMakeLists.txt new file mode 100644 index 00000000000..abb8ff5fd7e --- /dev/null +++ b/tools/src/h5sign/CMakeLists.txt @@ -0,0 +1,82 @@ +cmake_minimum_required (VERSION 3.26) +project (HDF5_TOOLS_SRC_H5SIGN C) + +#----------------------------------------------------------------------------- +# CMake configuration for HDF5 h5sign tool +# This file sets up the build, formatting, and installation rules for the h5sign executable. +# h5sign is used to sign HDF5 plugin files with RSA digital signatures. +#----------------------------------------------------------------------------- + +# -------------------------------------------------------------------- +# Find OpenSSL (required for RSA signing) +# -------------------------------------------------------------------- +find_package(OpenSSL REQUIRED) + +# -------------------------------------------------------------------- +# Add the h5sign executable +# -------------------------------------------------------------------- +add_executable (h5sign ${HDF5_TOOLS_SRC_H5SIGN_SOURCE_DIR}/h5sign.c) + +# Include directories: tools library, HDF5 source, and OpenSSL +target_include_directories (h5sign + PRIVATE + "${HDF5_TOOLS_ROOT_DIR}/lib" + "${HDF5_SRC_INCLUDE_DIRS}" + "${HDF5_SRC_BINARY_DIR}" + "${HDF5_SRC_DIR}" # For H5PLsig.h + $<$:${MPI_C_INCLUDE_DIRS}> +) + +# Link against HDF5 tools library, HDF5 library, and OpenSSL +if (HDF5_BUILD_STATIC_TOOLS) + TARGET_C_PROPERTIES (h5sign STATIC) + target_link_libraries (h5sign + PRIVATE + ${HDF5_TOOLS_LIB_TARGET} + ${HDF5_LIB_TARGET} + OpenSSL::Crypto + ) +else () + TARGET_C_PROPERTIES (h5sign SHARED) + target_link_libraries (h5sign + PRIVATE + ${HDF5_TOOLS_LIBSH_TARGET} + ${HDF5_LIBSH_TARGET} + OpenSSL::Crypto + ) +endif () + +set_target_properties (h5sign PROPERTIES FOLDER tools) +set_global_variable (HDF5_UTILS_TO_EXPORT "${HDF5_UTILS_TO_EXPORT};h5sign") + +set (H5_DEP_EXECUTABLES h5sign) + +# -------------------------------------------------------------------- +# Add Target to clang-format +# -------------------------------------------------------------------- +if (HDF5_ENABLE_FORMATTERS) + clang_format (HDF5_H5SIGN_SRC_FORMAT h5sign) +endif () + +############################################################################## +############################################################################## +### I N S T A L L A T I O N ### +############################################################################## +############################################################################## + +# -------------------------------------------------------------------- +# Rules for Installation of tools using make Install target +# -------------------------------------------------------------------- +if (HDF5_EXPORTED_TARGETS) + foreach (exec ${H5_DEP_EXECUTABLES}) + INSTALL_PROGRAM_PDB (${exec} ${HDF5_INSTALL_BIN_DIR} toolsapplications) + endforeach () + + install ( + TARGETS + ${H5_DEP_EXECUTABLES} + EXPORT + ${HDF5_EXPORTED_TARGETS} + RUNTIME DESTINATION ${HDF5_INSTALL_BIN_DIR} COMPONENT toolsapplications + ) +endif () diff --git a/tools/src/h5sign/h5sign.c b/tools/src/h5sign/h5sign.c new file mode 100644 index 00000000000..483781eabdd --- /dev/null +++ b/tools/src/h5sign/h5sign.c @@ -0,0 +1,855 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Copyright by The HDF Group. * + * All rights reserved. * + * * + * This file is part of HDF5. The full HDF5 copyright notice, including * + * terms governing use, modification, and redistribution, is contained in * + * the LICENSE file, which can be found at the root of the source code * + * distribution tree, or in https://www.hdfgroup.org/licenses. * + * If you do not have access to either file, you may request a copy from * + * help@hdfgroup.org. * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +/* + * Purpose: Sign HDF5 plugin files with RSA digital signatures + * + * This tool appends an RSA signature to a plugin binary file using + * the format expected by HDF5's plugin signature verification system. + * + * File format after signing: + * [ Plugin Binary ] [ RSA Signature ] [ Footer ] + * + * Footer (12 bytes, little-endian): + * - Magic number 0x48444635 "HDF5" (4 bytes) + * - Signature length (4 bytes) + * - Algorithm ID (1 byte) + * - Format version (1 byte) + * - Reserved (2 bytes) + * + * The plugin binary loader ignores trailing data, so signed plugins + * load normally on all platforms. + */ + +#include "hdf5.h" +#include "H5private.h" +#include "h5tools.h" +#include "h5tools_utils.h" + +/* Include signature header for footer format and magic number */ +#include "H5PLsig.h" +#include "H5encode.h" + +/* OpenSSL headers for RSA signing */ +#include +#include +#include +#include + +/* On Windows, OpenSSL requires applink to bridge different CRT versions */ +#ifdef _MSC_VER +#include +#endif + +/* Name of tool */ +#define PROGRAMNAME "h5sign" + +/* Use the shared maximum plugin size from H5PLsig.h */ + +/* I/O chunk size for hashing (64KB) */ +#define HASH_CHUNK_SIZE ((size_t)(64 * 1024)) + +/* Global options */ +static char *plugin_file = NULL; +static char *privkey_file = NULL; +static char *opt_algorithm = NULL; +static int opt_verbose = 0; +static int opt_force = 0; + +/* + * Command-line options: The user can specify short or long-named + * parameters. + */ +static const char *s_opts = "hp:k:a:fvV"; +static struct h5_long_options l_opts[] = {{"help", no_arg, 'h'}, + {"plugin", require_arg, 'p'}, + {"key", require_arg, 'k'}, + {"algorithm", require_arg, 'a'}, + {"force", no_arg, 'f'}, + {"verbose", no_arg, 'v'}, + {NULL, 0, '\0'}}; + +/*------------------------------------------------------------------------- + * Function: write_with_retry + * + * Purpose: Write data to a file descriptor, handling partial writes + * and EINTR interrupts. On failure, truncate the file to + * rollback_size to restore it to its pre-signing state. + * + * Return: SUCCEED/FAIL + *------------------------------------------------------------------------- + */ +static herr_t +write_with_retry(int fd, const unsigned char *data, size_t total, const char *what, const char *plugin_path, + hsize_t rollback_size) +{ + size_t written = 0; + + while (written < total) { + h5_posix_io_ret_t wr; + do { + wr = HDwrite(fd, data + written, total - written); + } while (-1 == wr && EINTR == errno); + + if (wr <= 0) { + fprintf(rawerrorstream, "Error: Cannot write %s to '%s': %s\n", what, plugin_path, + strerror(errno)); + /* Attempt rollback: restore file to its pre-signing state */ + (void)HDftruncate(fd, (HDoff_t)rollback_size); + return FAIL; + } + + written += (size_t)wr; + } + + return SUCCEED; +} + +/*------------------------------------------------------------------------- + * Function: usage + * + * Purpose: Print usage message + * + * Return: void + *------------------------------------------------------------------------- + */ +static void +usage(const char *prog) +{ + fflush(rawoutstream); + fprintf(rawoutstream, "usage: %s -p -k [OPTIONS]\n", prog); + fprintf(rawoutstream, "\n"); + fprintf(rawoutstream, "Sign an HDF5 plugin with RSA digital signature.\n"); + fprintf(rawoutstream, "\n"); + fprintf(rawoutstream, "REQUIRED OPTIONS\n"); + fprintf(rawoutstream, " -p, --plugin Plugin binary to sign (.so, .dll, .dylib)\n"); + fprintf(rawoutstream, " -k, --key RSA private key in PEM format\n"); + fprintf(rawoutstream, "\n"); + fprintf(rawoutstream, "OTHER OPTIONS\n"); + fprintf(rawoutstream, " -a, --algorithm Hash algorithm: sha256, sha384, sha512,\n"); + fprintf(rawoutstream, " sha256-pss, sha384-pss, sha512-pss\n"); + fprintf(rawoutstream, " (default: sha512)\n"); + fprintf(rawoutstream, " -f, --force Re-sign an already-signed plugin by stripping\n"); + fprintf(rawoutstream, " the existing signature before signing\n"); + fprintf(rawoutstream, " -v, --verbose Verbose output (show signature details)\n"); + fprintf(rawoutstream, " -h, --help Print this help message\n"); + fprintf(rawoutstream, " -V Print HDF5 library version\n"); + fprintf(rawoutstream, "\n"); + fprintf(rawoutstream, "DESCRIPTION\n"); + fprintf(rawoutstream, " This tool appends an RSA signature to a plugin file. The signature\n"); + fprintf(rawoutstream, " allows HDF5 to verify the plugin's authenticity when loading.\n"); + fprintf(rawoutstream, "\n"); + fprintf(rawoutstream, " The plugin file is modified in-place by appending:\n"); + fprintf(rawoutstream, " 1. RSA signature of the plugin binary (configurable hash algorithm)\n"); + fprintf(rawoutstream, " 2. Footer with signature metadata and magic number\n"); + fprintf(rawoutstream, "\n"); + fprintf(rawoutstream, " The binary loader ignores trailing data, so the signed plugin\n"); + fprintf(rawoutstream, " loads normally on all platforms.\n"); + fprintf(rawoutstream, "\n"); + fprintf(rawoutstream, "EXAMPLES\n"); + fprintf(rawoutstream, " # Sign a plugin with a private key (default SHA-512)\n"); + fprintf(rawoutstream, " %s -p libmyplugin.so -k private.pem\n", prog); + fprintf(rawoutstream, "\n"); + fprintf(rawoutstream, " # Sign with SHA-256 hash algorithm\n"); + fprintf(rawoutstream, " %s -p libmyplugin.so -k private.pem -a sha256\n", prog); + fprintf(rawoutstream, "\n"); + fprintf(rawoutstream, " # Sign with verbose output\n"); + fprintf(rawoutstream, " %s -p libmyplugin.so -k private.pem -v\n", prog); + fprintf(rawoutstream, "\n"); + fprintf(rawoutstream, "KEY GENERATION\n"); + fprintf(rawoutstream, " To generate an RSA key pair:\n"); + fprintf(rawoutstream, " openssl genrsa -out private.pem 4096\n"); + fprintf(rawoutstream, " openssl rsa -in private.pem -pubout -out public.pem\n"); + fprintf(rawoutstream, "\n"); + fprintf(rawoutstream, " Keep the private key secure! Use the public key when building HDF5\n"); + fprintf(rawoutstream, " with signature verification enabled.\n"); + fprintf(rawoutstream, "\n"); + fprintf(rawoutstream, "SECURITY NOTES\n"); + fprintf(rawoutstream, " - Keep your private key secure (chmod 600 private.pem)\n"); + fprintf(rawoutstream, " - Never share or commit your private key to version control\n"); + fprintf(rawoutstream, " - Verify plugin code before signing\n"); + fprintf(rawoutstream, " - Use strong keys (2048-bit minimum, 4096-bit recommended)\n"); + fprintf(rawoutstream, "\n"); + fprintf(rawoutstream, "Exit Status:\n"); + fprintf(rawoutstream, " 0 Successfully signed the plugin\n"); + fprintf(rawoutstream, " 1 An error occurred\n"); +} + +/*------------------------------------------------------------------------- + * Function: leave + * + * Purpose: Shutdown and call exit() + * + * Return: Does not return + *------------------------------------------------------------------------- + */ +static void +leave(int ret) +{ + if (plugin_file) + free(plugin_file); + if (privkey_file) + free(privkey_file); + if (opt_algorithm) + free(opt_algorithm); + + h5tools_close(); + exit(ret); +} + +/*------------------------------------------------------------------------- + * Function: report_openssl_error + * + * Purpose: Print an OpenSSL error message with context + * + * Return: void + *------------------------------------------------------------------------- + */ +static void +report_openssl_error(const char *context) +{ + unsigned long ssl_err = ERR_get_error(); + char err_buf[256]; + ERR_error_string_n(ssl_err, err_buf, sizeof(err_buf)); + fprintf(rawerrorstream, "Error: %s: %s\n", context, err_buf); +} + +/*------------------------------------------------------------------------- + * Function: parse_command_line + * + * Purpose: Parse command line arguments + * + * Return: Success: SUCCEED + * Failure: FAIL (exits program) + *------------------------------------------------------------------------- + */ +static herr_t +parse_command_line(int argc, const char *const *argv) +{ + int opt; + + /* Parse command line options */ + while ((opt = H5_get_option(argc, argv, s_opts, l_opts)) != EOF) { + switch ((char)opt) { + case 'p': + if (plugin_file) + free(plugin_file); + plugin_file = strdup(H5_optarg); + if (!plugin_file) { + fprintf(rawerrorstream, "Error: Out of memory\n"); + leave(EXIT_FAILURE); + } + break; + case 'k': + if (privkey_file) + free(privkey_file); + privkey_file = strdup(H5_optarg); + if (!privkey_file) { + fprintf(rawerrorstream, "Error: Out of memory\n"); + leave(EXIT_FAILURE); + } + break; + case 'a': + if (opt_algorithm) + free(opt_algorithm); + opt_algorithm = strdup(H5_optarg); + if (!opt_algorithm) { + fprintf(rawerrorstream, "Error: Out of memory\n"); + leave(EXIT_FAILURE); + } + break; + case 'f': + opt_force = 1; + break; + case 'v': + opt_verbose = 1; + break; + case 'h': + usage(h5tools_getprogname()); + leave(EXIT_SUCCESS); + break; + case 'V': + print_version(h5tools_getprogname()); + leave(EXIT_SUCCESS); + break; + case '?': + default: + usage(h5tools_getprogname()); + leave(EXIT_FAILURE); + } + } + + /* Check required arguments */ + if (!plugin_file) { + fprintf(rawerrorstream, "Error: Plugin file (-p) is required\n\n"); + usage(h5tools_getprogname()); + leave(EXIT_FAILURE); + } + + if (!privkey_file) { + fprintf(rawerrorstream, "Error: Private key file (-k) is required\n\n"); + usage(h5tools_getprogname()); + leave(EXIT_FAILURE); + } + + return SUCCEED; +} + +/*------------------------------------------------------------------------- + * Function: read_private_key + * + * Purpose: Read RSA private key from PEM file + * + * Return: Success: EVP_PKEY pointer + * Failure: NULL + *------------------------------------------------------------------------- + */ +static EVP_PKEY * +read_private_key(const char *keyfile) +{ + BIO *bio = NULL; + EVP_PKEY *pkey = NULL; + EVP_PKEY *ret_pkey = NULL; + + /* Open key file using BIO (avoids OPENSSL_Applink issue on Windows) */ + if (NULL == (bio = BIO_new_file(keyfile, "r"))) { + fprintf(rawerrorstream, "Error: Cannot open private key file '%s'\n", keyfile); + goto done; + } + + /* Read private key using OpenSSL's PEM reader */ + if (NULL == (pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL))) { + report_openssl_error("Cannot read private key"); + fprintf(rawerrorstream, " Key file: '%s'\n", keyfile); + fprintf(rawerrorstream, " Make sure the file is in PEM format.\n"); + fprintf(rawerrorstream, + " If the key is passphrase-protected, re-run interactively so OpenSSL\n"); + fprintf(rawerrorstream, " can prompt for the passphrase (non-interactive use will fail).\n"); + goto done; + } + + /* Verify it's an RSA key. + * EVP_PKEY_is_a() is the preferred API on OpenSSL 3.0+; fall back to + * EVP_PKEY_base_id() for older versions to avoid deprecation warnings. */ +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + if (!EVP_PKEY_is_a(pkey, "RSA") && !EVP_PKEY_is_a(pkey, "RSA-PSS")) { +#else + if (EVP_PKEY_base_id(pkey) != EVP_PKEY_RSA && EVP_PKEY_base_id(pkey) != EVP_PKEY_RSA_PSS) { +#endif + fprintf(rawerrorstream, "Error: Key file '%s' is not an RSA key\n", keyfile); + fprintf(rawerrorstream, " Only RSA keys are supported for plugin signing.\n"); + goto done; + } + + /* Enforce minimum RSA key size (2048-bit minimum; 4096-bit recommended) + * Use EVP_PKEY_bits() for OpenSSL 1.1.x compatibility; EVP_PKEY_get_bits() + * was introduced in OpenSSL 3.0 and EVP_PKEY_bits() remains available as + * a backward-compatible alias. */ + { + int key_bits = EVP_PKEY_bits(pkey); + if (key_bits < 2048) { + fprintf(rawerrorstream, "Error: RSA key in '%s' is too small (%d bits)\n", keyfile, key_bits); + fprintf(rawerrorstream, " Minimum required: 2048 bits (4096-bit recommended)\n"); + goto done; + } + } + + ret_pkey = pkey; + pkey = NULL; /* Prevent cleanup */ + +done: + if (bio) + BIO_free(bio); + if (pkey) + EVP_PKEY_free(pkey); + + /* Clear OpenSSL error queue */ + ERR_clear_error(); + + return ret_pkey; +} + +/* + * Algorithm lookup table: maps CLI name / algorithm ID to display name + * and OpenSSL EVP_MD getter. Shared by parse_algorithm_name() and the + * success-message printer in sign_plugin_file(). + */ +typedef const EVP_MD *(*evp_md_getter_t)(void); + +typedef struct { + const char *cli_name; /* Name accepted on the command line */ + const char *display_name; /* Human-readable name for messages */ + H5PL_sig_algo_t algo_id; /* H5PL_SIG_ALGO_* constant */ + evp_md_getter_t md_getter; /* OpenSSL EVP_MD factory function */ +} h5sign_algo_entry_t; + +/* clang-format off */ +static const h5sign_algo_entry_t algo_table[] = { + {"sha256", "SHA-256", H5PL_SIG_ALGO_SHA256, EVP_sha256}, + {"sha384", "SHA-384", H5PL_SIG_ALGO_SHA384, EVP_sha384}, + {"sha512", "SHA-512", H5PL_SIG_ALGO_SHA512, EVP_sha512}, + {"sha256-pss", "SHA-256/RSA-PSS", H5PL_SIG_ALGO_SHA256_PSS, EVP_sha256}, + {"sha384-pss", "SHA-384/RSA-PSS", H5PL_SIG_ALGO_SHA384_PSS, EVP_sha384}, + {"sha512-pss", "SHA-512/RSA-PSS", H5PL_SIG_ALGO_SHA512_PSS, EVP_sha512}, +}; +/* clang-format on */ + +static const size_t algo_table_size = sizeof(algo_table) / sizeof(algo_table[0]); + +/*------------------------------------------------------------------------- + * Function: algo_display_name + * + * Purpose: Return the display name for a given algorithm ID + * + * Return: Display name string, or NULL if unknown + *------------------------------------------------------------------------- + */ +static const char * +algo_display_name(H5PL_sig_algo_t algo_id) +{ + for (size_t i = 0; i < algo_table_size; i++) { + if (algo_table[i].algo_id == algo_id) + return algo_table[i].display_name; + } + return NULL; +} + +/*------------------------------------------------------------------------- + * Function: parse_algorithm_name + * + * Purpose: Parse algorithm name string and return corresponding + * EVP_MD and algorithm ID + * + * Return: Success: SUCCEED + * Failure: FAIL + *------------------------------------------------------------------------- + */ +static herr_t +parse_algorithm_name(const char *name, const EVP_MD **md_out, H5PL_sig_algo_t *algo_id_out) +{ + for (size_t i = 0; i < algo_table_size; i++) { + if (HDstrcasecmp(name, algo_table[i].cli_name) == 0) { + *md_out = algo_table[i].md_getter(); + *algo_id_out = algo_table[i].algo_id; + return SUCCEED; + } + } + + fprintf(rawerrorstream, "Error: Unknown algorithm '%s'\n", name); + fprintf(rawerrorstream, "Supported: sha256, sha384, sha512, sha256-pss, sha384-pss, sha512-pss\n"); + return FAIL; +} + +/*------------------------------------------------------------------------- + * Function: sign_plugin_file + * + * Purpose: Sign a plugin file by computing hash and creating + * RSA signature, then appending signature and footer to file + * + * Return: Success: SUCCEED + * Failure: FAIL + *------------------------------------------------------------------------- + */ +static herr_t +sign_plugin_file(const char *plugin_path, EVP_PKEY *private_key, const EVP_MD *hash_algorithm, + H5PL_sig_algo_t algorithm_id) +{ + int fd = -1; + h5_stat_t st; + hsize_t file_size = 0; + unsigned char *hash_buffer = NULL; + unsigned char *signature = NULL; + size_t sig_len = 0; + EVP_MD_CTX *mdctx = NULL; + EVP_PKEY_CTX *pkey_ctx = NULL; + herr_t ret_value = SUCCEED; + hsize_t bytes_read = 0; + + /* Open plugin file for reading and writing. + * Keeping a single fd open throughout hashing and appending eliminates + * the TOCTOU window that would exist if we closed and reopened the file. */ + if ((fd = HDopen(plugin_path, O_RDWR, 0)) < 0) { + fprintf(rawerrorstream, "Error: Cannot open plugin file '%s': %s\n", plugin_path, strerror(errno)); + ret_value = FAIL; + goto done; + } + + /* Get file size */ + if (HDfstat(fd, &st) < 0) { + fprintf(rawerrorstream, "Error: Cannot get file size for '%s': %s\n", plugin_path, strerror(errno)); + ret_value = FAIL; + goto done; + } + + file_size = (hsize_t)st.st_size; + + /* Sanity check file size */ + if (file_size == 0) { + fprintf(rawerrorstream, "Error: Plugin file '%s' is empty\n", plugin_path); + ret_value = FAIL; + goto done; + } + + /* Detect already-signed files: check for HDF5 magic number in the footer */ + if (file_size >= (hsize_t)H5PL_SIG_FOOTER_SIZE) { + uint8_t check_buf[H5PL_SIG_FOOTER_SIZE]; + H5PL_sig_footer_t check_footer; + + if (HDlseek(fd, (HDoff_t)(file_size - (hsize_t)H5PL_SIG_FOOTER_SIZE), SEEK_SET) >= 0) { + h5_posix_io_ret_t nr = HDread(fd, check_buf, H5PL_SIG_FOOTER_SIZE); + if (nr == (h5_posix_io_ret_t)H5PL_SIG_FOOTER_SIZE) { + if (H5PL_sig_decode_footer(check_buf, sizeof(check_buf), &check_footer)) { + if (!opt_force) { + fprintf(rawerrorstream, "Error: Plugin file '%s' is already signed\n", plugin_path); + fprintf(rawerrorstream, + " Use -f/--force to strip the existing signature and re-sign,\n" + " or start from the original unsigned binary\n"); + ret_value = FAIL; + goto done; + } + + /* --force: strip the existing signature and footer so we can re-sign */ + { + uint32_t existing_sig_len = check_footer.signature_length; + hsize_t binary_size; + + if (existing_sig_len == 0 || + (hsize_t)existing_sig_len + (hsize_t)H5PL_SIG_FOOTER_SIZE > file_size) { + fprintf(rawerrorstream, "Error: Corrupt signature footer in '%s' (sig_len=%u)\n", + plugin_path, existing_sig_len); + ret_value = FAIL; + goto done; + } + + binary_size = file_size - (hsize_t)existing_sig_len - (hsize_t)H5PL_SIG_FOOTER_SIZE; + + if (HDftruncate(fd, (HDoff_t)binary_size) < 0) { + fprintf(rawerrorstream, "Error: Cannot strip existing signature from '%s': %s\n", + plugin_path, strerror(errno)); + ret_value = FAIL; + goto done; + } + + if (opt_verbose) + fprintf(rawoutstream, "Existing signature stripped (%u bytes + %d byte footer)\n", + existing_sig_len, H5PL_SIG_FOOTER_SIZE); + + /* Update file_size to reflect the stripped binary */ + file_size = binary_size; + } + } + } + } + + /* Seek back to the beginning for hashing */ + if (HDlseek(fd, 0, SEEK_SET) < 0) { + fprintf(rawerrorstream, "Error: Cannot seek in plugin file '%s': %s\n", plugin_path, + strerror(errno)); + ret_value = FAIL; + goto done; + } + } + + /* Check binary size after any existing signature has been stripped. + * The verifier enforces this same limit against the binary portion of the + * file, so a signed file whose total on-disk size exceeds the limit but + * whose binary is within range must still be signable. */ + if (file_size > (hsize_t)H5PL_MAX_PLUGIN_SIZE) { + fprintf(rawerrorstream, "Error: Plugin binary '%s' is too large (%llu bytes)\n", plugin_path, + (unsigned long long)file_size); + fprintf(rawerrorstream, " Maximum binary size is %llu bytes (1GB)\n", + (unsigned long long)H5PL_MAX_PLUGIN_SIZE); + ret_value = FAIL; + goto done; + } + + if (opt_verbose) { + fprintf(rawoutstream, "Plugin file: %s\n", plugin_path); + fprintf(rawoutstream, "File size: %llu bytes\n", (unsigned long long)file_size); + } + + /* Create message digest context */ + if (NULL == (mdctx = EVP_MD_CTX_new())) { + report_openssl_error("Cannot create message digest context"); + ret_value = FAIL; + goto done; + } + + /* Initialize signing context with selected hash algorithm */ + if (1 != EVP_DigestSignInit(mdctx, &pkey_ctx, hash_algorithm, NULL, private_key)) { + report_openssl_error("Cannot initialize signing context"); + ret_value = FAIL; + goto done; + } + + /* Configure PSS padding if needed */ + if (H5PL_SIG_ALGO_IS_PSS(algorithm_id)) { + if (1 != EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, RSA_PKCS1_PSS_PADDING)) { + report_openssl_error("Cannot set PSS padding"); + ret_value = FAIL; + goto done; + } + + if (1 != EVP_PKEY_CTX_set_rsa_pss_saltlen(pkey_ctx, RSA_PSS_SALTLEN_DIGEST)) { + report_openssl_error("Cannot set PSS salt length"); + ret_value = FAIL; + goto done; + } + } + + /* Allocate buffer for reading file in chunks */ + if (NULL == (hash_buffer = (unsigned char *)malloc(HASH_CHUNK_SIZE))) { + fprintf(rawerrorstream, "Error: Cannot allocate hash buffer\n"); + ret_value = FAIL; + goto done; + } + + /* Read file in chunks and update hash */ + bytes_read = 0; + + if (opt_verbose) + /* EVP_MD_get0_name() requires OpenSSL 3.0+; fall back to OBJ_nid2sn() + * for OpenSSL 1.1.x compatibility. */ +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + fprintf(rawoutstream, "Computing %s hash...\n", EVP_MD_get0_name(hash_algorithm)); +#else + fprintf(rawoutstream, "Computing %s hash...\n", OBJ_nid2sn(EVP_MD_type(hash_algorithm))); +#endif + + while (bytes_read < file_size) { + size_t chunk_size = + (size_t)((file_size - bytes_read) > HASH_CHUNK_SIZE ? HASH_CHUNK_SIZE : (file_size - bytes_read)); + h5_posix_io_ret_t read_result = 0; + + /* Read chunk with EINTR retry */ + do { + read_result = HDread(fd, hash_buffer, chunk_size); + } while (-1 == read_result && EINTR == errno); + + if (read_result < 0) { + fprintf(rawerrorstream, "Error: Cannot read from plugin file '%s': %s\n", plugin_path, + strerror(errno)); + ret_value = FAIL; + goto done; + } + + if (read_result == 0) { + fprintf(rawerrorstream, "Error: Unexpected end of file in '%s'\n", plugin_path); + ret_value = FAIL; + goto done; + } + + /* Update hash with chunk */ + if (1 != EVP_DigestSignUpdate(mdctx, hash_buffer, (size_t)read_result)) { + report_openssl_error("Cannot update hash"); + ret_value = FAIL; + goto done; + } + + bytes_read += (hsize_t)read_result; + } + + if (opt_verbose) + fprintf(rawoutstream, "Hash computed successfully\n"); + + /* Get signature length */ + if (1 != EVP_DigestSignFinal(mdctx, NULL, &sig_len)) { + report_openssl_error("Cannot get signature length"); + ret_value = FAIL; + goto done; + } + + if (sig_len == 0 || sig_len > H5PL_MAX_SIGNATURE_SIZE) { + fprintf(rawerrorstream, "Error: Invalid signature length: %zu bytes (max %d)\n", sig_len, + H5PL_MAX_SIGNATURE_SIZE); + ret_value = FAIL; + goto done; + } + + /* Allocate signature buffer */ + if (NULL == (signature = (unsigned char *)malloc(sig_len))) { + fprintf(rawerrorstream, "Error: Cannot allocate signature buffer\n"); + ret_value = FAIL; + goto done; + } + + /* Compute signature */ + if (1 != EVP_DigestSignFinal(mdctx, signature, &sig_len)) { + report_openssl_error("Cannot compute signature"); + ret_value = FAIL; + goto done; + } + + if (opt_verbose) { + fprintf(rawoutstream, "Signature created successfully\n"); + fprintf(rawoutstream, "Signature length: %zu bytes\n", sig_len); + } + + /* Verify the signed file will not exceed the verifier's size limit */ + if ((hsize_t)(file_size + sig_len + H5PL_SIG_FOOTER_SIZE) > (hsize_t)H5PL_MAX_PLUGIN_SIZE) { + fprintf(rawerrorstream, "Error: Signed plugin would exceed maximum size (%llu bytes)\n", + (unsigned long long)H5PL_MAX_PLUGIN_SIZE); + ret_value = FAIL; + goto done; + } + + if (HDlseek(fd, (HDoff_t)file_size, SEEK_SET) < 0) { + fprintf(rawerrorstream, "Error: Cannot seek in plugin file '%s': %s\n", plugin_path, strerror(errno)); + ret_value = FAIL; + goto done; + } + + /* Append signature to file */ + if (write_with_retry(fd, signature, sig_len, "signature", plugin_path, file_size) < 0) { + ret_value = FAIL; + goto done; + } + + if (opt_verbose) + fprintf(rawoutstream, "Signature appended to plugin\n"); + + /* Prepare and write footer in little-endian format */ + { + uint8_t footer_buf[H5PL_SIG_FOOTER_SIZE]; + H5PL_sig_footer_t footer; + + footer.signature_length = (uint32_t)sig_len; + footer.algorithm_id = algorithm_id; + footer.format_version = H5PL_SIG_FORMAT_VERSION_CURRENT; + footer.reserved = 0; + footer.magic = H5PL_SIG_MAGIC; + H5PL_sig_encode_footer(footer_buf, sizeof(footer_buf), &footer); + + /* Write footer to file */ + if (write_with_retry(fd, footer_buf, sizeof(footer_buf), "footer", plugin_path, file_size) < 0) { + ret_value = FAIL; + goto done; + } + } + + /* Close file descriptor */ + HDclose(fd); + fd = -1; + + if (opt_verbose) + fprintf(rawoutstream, "Footer written successfully\n"); + + /* Success! */ + fprintf(rawoutstream, "\nPlugin signed successfully!\n"); + fprintf(rawoutstream, " File: %s\n", plugin_path); + fprintf(rawoutstream, " Original size: %llu bytes\n", (unsigned long long)file_size); + { + const char *algo_name = algo_display_name(algorithm_id); + if (algo_name) + fprintf(rawoutstream, " Hash algorithm: %s (0x%02X)\n", algo_name, algorithm_id); + else + fprintf(rawoutstream, " Hash algorithm: 0x%02X\n", algorithm_id); + } + fprintf(rawoutstream, " Signature size: %zu bytes\n", sig_len); + fprintf(rawoutstream, " Footer size: %d bytes\n", H5PL_SIG_FOOTER_SIZE); + fprintf(rawoutstream, " Final size: %llu bytes\n", + (unsigned long long)(file_size + sig_len + H5PL_SIG_FOOTER_SIZE)); + fprintf(rawoutstream, "\n"); + +done: + if (fd >= 0) + HDclose(fd); + if (hash_buffer) + free(hash_buffer); + if (signature) + free(signature); + if (mdctx) + EVP_MD_CTX_free(mdctx); + + /* Clear OpenSSL error queue */ + ERR_clear_error(); + + return ret_value; +} + +/*------------------------------------------------------------------------- + * Function: main + * + * Purpose: HDF5 plugin signing tool + * + * Return: Success: EXIT_SUCCESS + * Failure: EXIT_FAILURE + *------------------------------------------------------------------------- + */ +int +main(int argc, char *argv[]) +{ + EVP_PKEY *private_key = NULL; + const EVP_MD *hash_algorithm = NULL; + H5PL_sig_algo_t algorithm_id = (H5PL_sig_algo_t)0; + int ret_value = EXIT_SUCCESS; + + /* Initialize HDF5 tools infrastructure */ + h5tools_setprogname(PROGRAMNAME); + h5tools_setstatus(EXIT_SUCCESS); + + /* Initialize h5tools lib */ + h5tools_init(); + + /* Parse command line */ + if (parse_command_line(argc, (const char *const *)argv) < 0) { + ret_value = EXIT_FAILURE; + goto done; + } + + fprintf(rawoutstream, "HDF5 Plugin Signature Tool\n"); + fprintf(rawoutstream, "===========================\n\n"); + + /* Parse algorithm option or use default */ + if (opt_algorithm) { + if (parse_algorithm_name(opt_algorithm, &hash_algorithm, &algorithm_id) < 0) { + ret_value = EXIT_FAILURE; + goto done; + } + fprintf(rawoutstream, "Using hash algorithm: %s\n", opt_algorithm); + } + else { + /* Default: SHA-512 with PKCS1 */ + hash_algorithm = EVP_sha512(); + algorithm_id = H5PL_SIG_ALGO_SHA512; + fprintf(rawoutstream, "Using default hash algorithm: sha512\n"); + } + + /* Read private key */ + fprintf(rawoutstream, "Reading private key from '%s'...\n", privkey_file); + if (NULL == (private_key = read_private_key(privkey_file))) { + ret_value = EXIT_FAILURE; + goto done; + } + fprintf(rawoutstream, "Private key loaded successfully\n\n"); + + /* Sign the plugin */ + fprintf(rawoutstream, "Signing plugin '%s'...\n\n", plugin_file); + if (sign_plugin_file(plugin_file, private_key, hash_algorithm, algorithm_id) < 0) { + ret_value = EXIT_FAILURE; + goto done; + } + + fprintf(rawoutstream, "SECURITY REMINDERS:\n"); + fprintf(rawoutstream, " - Keep your private key secure (chmod 600 %s)\n", privkey_file); + fprintf(rawoutstream, " - Never share or commit your private key\n"); + fprintf(rawoutstream, " - Test the signed plugin before deployment\n"); + fprintf(rawoutstream, "\n"); + +done: + if (private_key) + EVP_PKEY_free(private_key); + + leave(ret_value); + + return ret_value; +} diff --git a/tools/test/CMakeLists.txt b/tools/test/CMakeLists.txt index 5d9077a7a75..14a7e250da9 100644 --- a/tools/test/CMakeLists.txt +++ b/tools/test/CMakeLists.txt @@ -1,6 +1,9 @@ cmake_minimum_required (VERSION 3.26) project (HDF5_TOOLS_TEST C) +# Include plugin signing helper function (used by tool test subdirectories) +include(${HDF5_SOURCE_DIR}/config/cmake/SignPlugin.cmake) + set (HDF5_TOOLS h5copy h5diff @@ -118,3 +121,9 @@ add_subdirectory (h5format_convert) #-- Add the perform tests add_subdirectory (perform) + +#-- Add the h5sign tests +# Only build h5sign tests when signed plugins are required (h5sign tool must be built) +if (HDF5_REQUIRE_SIGNED_PLUGINS) + add_subdirectory (h5sign) +endif () diff --git a/tools/test/h5copy/CMakeLists.txt b/tools/test/h5copy/CMakeLists.txt index 6a4ab1ccc54..8cf5121369f 100644 --- a/tools/test/h5copy/CMakeLists.txt +++ b/tools/test/h5copy/CMakeLists.txt @@ -47,6 +47,11 @@ if (BUILD_SHARED_LIBS) "$" "${CMAKE_BINARY_DIR}/plugins/$" ) + + #----------------------------------------------------------------------------- + # Sign the plugin if signature verification is enabled + #----------------------------------------------------------------------------- + sign_plugin_target(${H5COPY_TOOL_PLUGIN_LIB_TARGET} "${CMAKE_BINARY_DIR}/plugins") endif () # If tool and serial tests are enabled, include test configuration diff --git a/tools/test/h5copy/CMakeTests.cmake b/tools/test/h5copy/CMakeTests.cmake index 5958b4d837b..1f64494da65 100644 --- a/tools/test/h5copy/CMakeTests.cmake +++ b/tools/test/h5copy/CMakeTests.cmake @@ -392,6 +392,7 @@ macro (ADD_H5_UD_TEST testname resultcode infile sparam srcname dparam dstname c -D "TEST_ENV_VAR=HDF5_PLUGIN_PATH" -D "TEST_ENV_VALUE=${ud_search_path}${CMAKE_SEP}${vol_plugin_path}" -D "TEST_LIBRARY_DIRECTORY=${CMAKE_TEST_OUTPUT_DIRECTORY}" + $<$:-DTEST_KEYSTORE_DIR=${CMAKE_BINARY_DIR}/test_keystore> -P "${HDF_RESOURCES_DIR}/runTest.cmake" ) set_tests_properties (${vol_prefix}H5COPY_UD-${testname} PROPERTIES @@ -420,6 +421,7 @@ macro (ADD_H5_UD_TEST testname resultcode infile sparam srcname dparam dstname c -D "TEST_ENV_VAR=HDF5_PLUGIN_PATH" -D "TEST_ENV_VALUE=${CMAKE_BINARY_DIR}/plugins${CMAKE_SEP}${vol_plugin_path}" -D "TEST_LIBRARY_DIRECTORY=${CMAKE_TEST_OUTPUT_DIRECTORY}" + $<$:-DTEST_KEYSTORE_DIR=${CMAKE_BINARY_DIR}/test_keystore> -P "${HDF_RESOURCES_DIR}/runTest.cmake" ) set_tests_properties (${vol_prefix}H5COPY_UD-${testname}-DIFF PROPERTIES @@ -535,6 +537,7 @@ macro (ADD_H5_UD_ERR_TEST testname resultcode infile sparam srcname dparam dstna -D "TEST_ENV_VAR=HDF5_PLUGIN_PATH" -D "TEST_ENV_VALUE=${ud_search_path}${CMAKE_SEP}${vol_plugin_path}" -D "TEST_LIBRARY_DIRECTORY=${CMAKE_TEST_OUTPUT_DIRECTORY}" + $<$:-DTEST_KEYSTORE_DIR=${CMAKE_BINARY_DIR}/test_keystore> -P "${HDF_RESOURCES_DIR}/runTest.cmake" ) set_tests_properties (${vol_prefix}H5COPY_UD_ERR-${testname} PROPERTIES @@ -563,6 +566,7 @@ macro (ADD_H5_UD_ERR_TEST testname resultcode infile sparam srcname dparam dstna -D "TEST_ENV_VAR=HDF5_PLUGIN_PATH" -D "TEST_ENV_VALUE=${CMAKE_BINARY_DIR}/plugins${CMAKE_SEP}${vol_plugin_path}" -D "TEST_LIBRARY_DIRECTORY=${CMAKE_TEST_OUTPUT_DIRECTORY}" + $<$:-DTEST_KEYSTORE_DIR=${CMAKE_BINARY_DIR}/test_keystore> -P "${HDF_RESOURCES_DIR}/runTest.cmake" ) set_tests_properties (${vol_prefix}H5COPY_UD_ERR-${testname}-DIFF PROPERTIES diff --git a/tools/test/h5diff/CMakeLists.txt b/tools/test/h5diff/CMakeLists.txt index 37cd3672958..9095e2be299 100644 --- a/tools/test/h5diff/CMakeLists.txt +++ b/tools/test/h5diff/CMakeLists.txt @@ -40,6 +40,11 @@ if (BUILD_SHARED_LIBS) "$" "${CMAKE_BINARY_DIR}/plugins/$" ) + + #----------------------------------------------------------------------------- + # Sign the plugin if signature verification is enabled + #----------------------------------------------------------------------------- + sign_plugin_target(${H5DIFF_TOOL_PLUGIN_LIB_TARGET} "${CMAKE_BINARY_DIR}/plugins") endif () if (HDF5_TEST_TOOLS) diff --git a/tools/test/h5diff/CMakeTests.cmake b/tools/test/h5diff/CMakeTests.cmake index c90658006c4..9b2aafc21a0 100644 --- a/tools/test/h5diff/CMakeTests.cmake +++ b/tools/test/h5diff/CMakeTests.cmake @@ -713,6 +713,7 @@ macro (ADD_H5_UD_TEST testname resultcode resultfile) -D "TEST_ENV_VAR=HDF5_PLUGIN_PATH" -D "TEST_ENV_VALUE=${ud_search_path}" -D "TEST_LIBRARY_DIRECTORY=${CMAKE_TEST_OUTPUT_DIRECTORY}" + $<$:-DTEST_KEYSTORE_DIR=${CMAKE_BINARY_DIR}/test_keystore> -P "${HDF_RESOURCES_DIR}/runTest.cmake" ) if ("${vol_prefix}H5DIFF_UD-${testname}" MATCHES "${HDF5_DISABLE_TESTS_REGEX}") diff --git a/tools/test/h5dump/CMakeLists.txt b/tools/test/h5dump/CMakeLists.txt index 611816c0451..02add762f89 100644 --- a/tools/test/h5dump/CMakeLists.txt +++ b/tools/test/h5dump/CMakeLists.txt @@ -40,6 +40,11 @@ if (BUILD_SHARED_LIBS) "$" "${CMAKE_BINARY_DIR}/plugins/$" ) + + #----------------------------------------------------------------------------- + # Sign the plugin if signature verification is enabled + #----------------------------------------------------------------------------- + sign_plugin_target(${H5DUMP_TOOL_PLUGIN_LIB_TARGET} "${CMAKE_BINARY_DIR}/plugins") endif () if (HDF5_TEST_TOOLS AND HDF5_TEST_SERIAL) diff --git a/tools/test/h5dump/CMakeTests.cmake b/tools/test/h5dump/CMakeTests.cmake index ea1aa6657f8..2788008add0 100644 --- a/tools/test/h5dump/CMakeTests.cmake +++ b/tools/test/h5dump/CMakeTests.cmake @@ -911,6 +911,7 @@ macro (ADD_H5_UD_TEST testname resultcode resultfile) -D "TEST_ENV_VAR=HDF5_PLUGIN_PATH" -D "TEST_ENV_VALUE=${CMAKE_BINARY_DIR}/plugins" -D "TEST_LIBRARY_DIRECTORY=${CMAKE_TEST_OUTPUT_DIRECTORY}" + $<$:-DTEST_KEYSTORE_DIR=${CMAKE_BINARY_DIR}/test_keystore> -P "${HDF_RESOURCES_DIR}/runTest.cmake" ) set_tests_properties (H5DUMP_UD-${testname}-${resultfile} PROPERTIES diff --git a/tools/test/h5ls/CMakeLists.txt b/tools/test/h5ls/CMakeLists.txt index c4d33850211..0b890802e1b 100644 --- a/tools/test/h5ls/CMakeLists.txt +++ b/tools/test/h5ls/CMakeLists.txt @@ -41,6 +41,11 @@ if (BUILD_SHARED_LIBS) "$" "${CMAKE_BINARY_DIR}/plugins/$" ) + + #----------------------------------------------------------------------------- + # Sign the plugin if signature verification is enabled + #----------------------------------------------------------------------------- + sign_plugin_target(${H5LS_TOOL_PLUGIN_LIB_TARGET} "${CMAKE_BINARY_DIR}/plugins") endif () if (HDF5_TEST_TOOLS AND HDF5_TEST_SERIAL) diff --git a/tools/test/h5ls/CMakeTests.cmake b/tools/test/h5ls/CMakeTests.cmake index b2720577c9c..5f2b18ec4a4 100644 --- a/tools/test/h5ls/CMakeTests.cmake +++ b/tools/test/h5ls/CMakeTests.cmake @@ -349,6 +349,7 @@ macro (ADD_H5_UD_TEST testname resultcode resultfile) -D "TEST_ENV_VAR=HDF5_PLUGIN_PATH" -D "TEST_ENV_VALUE=${CMAKE_BINARY_DIR}/plugins" -D "TEST_LIBRARY_DIRECTORY=${CMAKE_TEST_OUTPUT_DIRECTORY}" + $<$:-DTEST_KEYSTORE_DIR=${CMAKE_BINARY_DIR}/test_keystore> -P "${HDF_RESOURCES_DIR}/runTest.cmake" ) set_tests_properties (H5LS_UD-${testname}-${resultfile} PROPERTIES diff --git a/tools/test/h5repack/CMakeLists.txt b/tools/test/h5repack/CMakeLists.txt index c16c8391727..32d3fb415e5 100644 --- a/tools/test/h5repack/CMakeLists.txt +++ b/tools/test/h5repack/CMakeLists.txt @@ -98,6 +98,12 @@ if (BUILD_SHARED_LIBS) "$" "${CMAKE_BINARY_DIR}/plugins/$" ) + + #----------------------------------------------------------------------------- + # Sign the plugins if signature verification is enabled + #----------------------------------------------------------------------------- + sign_plugin_target(${H5REPACK_TOOL_PLUGIN_LIB_TARGET} "${CMAKE_BINARY_DIR}/plugins") + sign_plugin_target(${H5REPACK_TOOL_PLUGIN_LIB_VTARGET} "${CMAKE_BINARY_DIR}/plugins") endif () if (HDF5_TEST_TOOLS AND HDF5_TEST_SERIAL) diff --git a/tools/test/h5repack/CMakeTests.cmake b/tools/test/h5repack/CMakeTests.cmake index 044ef36ae18..e22562f839b 100644 --- a/tools/test/h5repack/CMakeTests.cmake +++ b/tools/test/h5repack/CMakeTests.cmake @@ -1066,6 +1066,7 @@ macro (ADD_H5_UD_TEST testname resultcode resultfile) -D "TEST_ENV_VAR=HDF5_PLUGIN_PATH" -D "TEST_ENV_VALUE=${CMAKE_BINARY_DIR}/plugins" -D "TEST_LIBRARY_DIRECTORY=${CMAKE_TEST_OUTPUT_DIRECTORY}" + $<$:-DTEST_KEYSTORE_DIR=${CMAKE_BINARY_DIR}/test_keystore> -P "${HDF_RESOURCES_DIR}/runTest.cmake" ) set_tests_properties (H5REPACK_UD-${testname} PROPERTIES @@ -1087,6 +1088,7 @@ macro (ADD_H5_UD_TEST testname resultcode resultfile) -D "TEST_ENV_VAR=HDF5_PLUGIN_PATH" -D "TEST_ENV_VALUE=${CMAKE_BINARY_DIR}/plugins" -D "TEST_LIBRARY_DIRECTORY=${CMAKE_TEST_OUTPUT_DIRECTORY}" + $<$:-DTEST_KEYSTORE_DIR=${CMAKE_BINARY_DIR}/test_keystore> -P "${HDF_RESOURCES_DIR}/runTest.cmake" ) set_tests_properties (H5REPACK_UD-${testname}-h5dump PROPERTIES diff --git a/tools/test/h5sign/CMakeLists.txt b/tools/test/h5sign/CMakeLists.txt new file mode 100644 index 00000000000..88cb68f34b1 --- /dev/null +++ b/tools/test/h5sign/CMakeLists.txt @@ -0,0 +1,65 @@ +# CMake configuration for building and testing the h5sign tool +# Creates test executables and scripts for signing plugin files +# Supports both static and shared builds + +cmake_minimum_required (VERSION 3.26) +project (HDF5_TOOLS_TEST_H5SIGN C) + +# Create a dummy plugin generator for testing +add_executable (h5signgentest ${HDF5_TOOLS_TEST_H5SIGN_SOURCE_DIR}/h5signgentest.c) +target_include_directories (h5signgentest + PRIVATE + "${HDF5_TOOLS_ROOT_DIR}/lib" + "${HDF5_SRC_INCLUDE_DIRS}" + "${HDF5_SRC_BINARY_DIR}" + "${HDF5_SRC_DIR}" # For H5PLsig.h + $<$:${MPI_C_INCLUDE_DIRS}> +) + +if (BUILD_STATIC_LIBS) + TARGET_C_PROPERTIES (h5signgentest STATIC) + target_link_libraries (h5signgentest PRIVATE ${HDF5_TOOLS_LIB_TARGET} ${HDF5_LIB_TARGET}) +else () + TARGET_C_PROPERTIES (h5signgentest SHARED) + target_link_libraries (h5signgentest PRIVATE ${HDF5_TOOLS_LIBSH_TARGET} ${HDF5_LIBSH_TARGET}) +endif () +set_target_properties (h5signgentest PROPERTIES FOLDER generator/tools) + +# Create signature verification test executable +add_executable (h5signverifytest ${HDF5_TOOLS_TEST_H5SIGN_SOURCE_DIR}/h5signverifytest.c) +target_include_directories (h5signverifytest + PRIVATE + "${HDF5_TOOLS_ROOT_DIR}/lib" + "${HDF5_SRC_INCLUDE_DIRS}" + "${HDF5_SRC_BINARY_DIR}" + "${HDF5_SRC_DIR}" # For H5PLsig.h and H5PLpkg.h + $<$:${MPI_C_INCLUDE_DIRS}> +) + +if (BUILD_STATIC_LIBS) + TARGET_C_PROPERTIES (h5signverifytest STATIC) + target_link_libraries (h5signverifytest PRIVATE ${HDF5_TOOLS_LIB_TARGET} ${HDF5_LIB_TARGET}) +else () + TARGET_C_PROPERTIES (h5signverifytest SHARED) + target_link_libraries (h5signverifytest PRIVATE ${HDF5_TOOLS_LIBSH_TARGET} ${HDF5_LIBSH_TARGET}) +endif () +# OpenSSL needed for SHA-256 hash computation in revocation test +if (HDF5_REQUIRE_SIGNED_PLUGINS) + target_link_libraries (h5signverifytest PRIVATE OpenSSL::Crypto) +endif () +set_target_properties (h5signverifytest PROPERTIES FOLDER test/tools) + +#----------------------------------------------------------------------------- +# Add Target to clang-format +#----------------------------------------------------------------------------- +if (HDF5_ENABLE_FORMATTERS) + clang_format (HDF5_TOOLS_TEST_H5SIGN_gentest_FORMAT h5signgentest) + clang_format (HDF5_TOOLS_TEST_H5SIGN_verifytest_FORMAT h5signverifytest) +endif () + +#----------------------------------------------------------------------------- +# Add tests +#----------------------------------------------------------------------------- +if (HDF5_TEST_TOOLS AND HDF5_TEST_SERIAL) + include (CMakeTests.cmake) +endif () diff --git a/tools/test/h5sign/CMakeTests.cmake b/tools/test/h5sign/CMakeTests.cmake new file mode 100644 index 00000000000..393287173eb --- /dev/null +++ b/tools/test/h5sign/CMakeTests.cmake @@ -0,0 +1,352 @@ +# +# Copyright by The HDF Group. +# All rights reserved. +# +# This file is part of HDF5. The full HDF5 copyright notice, including +# terms governing use, modification, and redistribution, is contained in +# the COPYING file, which can be found at the root of the source code +# distribution tree, or in https://www.hdfgroup.org/licenses. +# If you do not have access to either file, you may request a copy from +# help@hdfgroup.org. +# + +############################################################################## +############################################################################## +### T E S T I N G ### +############################################################################## +############################################################################## + +# -------------------------------------------------------------------- +# Copy test files to build directory +# -------------------------------------------------------------------- +set (HDF5_REFERENCE_TEST_FILES) +set (HDF5_TOOLS_TEST_H5SIGN_FILES) + +# No reference files needed for basic signing tests + +# -------------------------------------------------------------------- +# Create testfiles directory +# -------------------------------------------------------------------- +file (MAKE_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles") + +# -------------------------------------------------------------------- +# Test Macro +# -------------------------------------------------------------------- +macro (ADD_H5SIGN_TEST testname resultcode) + add_test ( + NAME H5SIGN-${testname} + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $ ${ARGN} + ) + set_tests_properties (H5SIGN-${testname} PROPERTIES + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + if (${resultcode} STREQUAL "1") + set_tests_properties (H5SIGN-${testname} PROPERTIES WILL_FAIL "true") + endif () +endmacro () + +# -------------------------------------------------------------------- +# Generate test files +# -------------------------------------------------------------------- +add_test ( + NAME H5SIGN-gentest + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $ + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" +) +set_tests_properties (H5SIGN-gentest PROPERTIES + FIXTURES_SETUP H5SIGN_testfiles +) + +# -------------------------------------------------------------------- +# Generate test RSA key pair +# Note: This requires OpenSSL to be available during testing +# -------------------------------------------------------------------- +find_program(OPENSSL_EXECUTABLE openssl) +if (OPENSSL_EXECUTABLE) + # Generate private key + add_test ( + NAME H5SIGN-genkey-private + COMMAND ${OPENSSL_EXECUTABLE} genrsa -out test_private.pem 2048 + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-genkey-private PROPERTIES + FIXTURES_REQUIRED H5SIGN_testfiles + FIXTURES_SETUP H5SIGN_keys + ) + + # Generate public key + add_test ( + NAME H5SIGN-genkey-public + COMMAND ${OPENSSL_EXECUTABLE} rsa -in test_private.pem -pubout -out test_public.pem + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-genkey-public PROPERTIES + DEPENDS H5SIGN-genkey-private + FIXTURES_REQUIRED H5SIGN_testfiles + FIXTURES_SETUP H5SIGN_keys + ) + + # Test 1: Show help + add_test ( + NAME H5SIGN-h_help + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $ -h + ) + + # Test 2: Show version + add_test ( + NAME H5SIGN-V_version + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $ -V + ) + + # Test 3: Sign a small plugin + # Note: depends on verify-copy-small-for-signing to ensure the unsigned copy + # is preserved before this test signs plugin_small.so in-place. + add_test ( + NAME H5SIGN-sign_small + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $ -p plugin_small.so -k test_private.pem + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-sign_small PROPERTIES + DEPENDS H5SIGN-verify-copy-small-for-signing + FIXTURES_REQUIRED "H5SIGN_testfiles;H5SIGN_keys" + ) + + # Test 4: Sign a medium plugin with verbose output + add_test ( + NAME H5SIGN-sign_medium_verbose + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $ -p plugin_medium.so -k test_private.pem -v + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-sign_medium_verbose PROPERTIES + DEPENDS H5SIGN-verify-copy-unsigned + FIXTURES_REQUIRED "H5SIGN_testfiles;H5SIGN_keys" + ) + + # Test 5: Sign a large plugin + # Note: depends on verify-copy-large-for-signing to ensure the unsigned copy + # is preserved before this test signs plugin_large.so in-place. + add_test ( + NAME H5SIGN-sign_large + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $ -p plugin_large.so -k test_private.pem + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-sign_large PROPERTIES + DEPENDS H5SIGN-verify-copy-large-for-signing + FIXTURES_REQUIRED "H5SIGN_testfiles;H5SIGN_keys" + ) + + # Test 6: Re-sign an already-signed plugin using --force + add_test ( + NAME H5SIGN-resign_force + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $ -p plugin_small.so -k test_private.pem -f + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-resign_force PROPERTIES + DEPENDS H5SIGN-sign_small + ) + + # Test 7: Error test - already-signed plugin without --force + add_test ( + NAME H5SIGN-error_already_signed + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $ -p plugin_small.so -k test_private.pem + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-error_already_signed PROPERTIES + DEPENDS "H5SIGN-sign_small;H5SIGN-resign_force" + WILL_FAIL "true" + ) + + # Test 9: Error test - missing plugin file + add_test ( + NAME H5SIGN-error_no_plugin + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $ -k test_private.pem + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-error_no_plugin PROPERTIES + WILL_FAIL "true" + ) + + # Test 10: Error test - missing key file + add_test ( + NAME H5SIGN-error_no_key + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $ -p plugin_small.so + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-error_no_key PROPERTIES + WILL_FAIL "true" + ) + + # Test 11: Error test - nonexistent plugin file + add_test ( + NAME H5SIGN-error_bad_plugin + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $ -p nonexistent.so -k test_private.pem + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-error_bad_plugin PROPERTIES + FIXTURES_REQUIRED H5SIGN_keys + WILL_FAIL "true" + ) + + # Test 12: Error test - nonexistent key file + add_test ( + NAME H5SIGN-error_bad_key + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $ -p plugin_small.so -k nonexistent.pem + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-error_bad_key PROPERTIES + FIXTURES_REQUIRED H5SIGN_testfiles + WILL_FAIL "true" + ) + + # -------------------------------------------------------------------- + # Signature Verification Tests + # These tests verify that the signature verification and caching work + # -------------------------------------------------------------------- + + # ---- Keystore setup (fixture: H5SIGN_keystore) ---- + + # Create keystore directory for verification tests + add_test ( + NAME H5SIGN-verify-setup-keystore + COMMAND ${CMAKE_COMMAND} -E make_directory "${PROJECT_BINARY_DIR}/testfiles/test_keystore" + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-verify-setup-keystore PROPERTIES + FIXTURES_REQUIRED H5SIGN_keys + FIXTURES_SETUP H5SIGN_keystore + ) + + # On Windows, restrict test keystore ACLs so the permission security check passes + if (WIN32) + add_test ( + NAME H5SIGN-verify-secure-keystore + COMMAND powershell -NonInteractive -NoProfile + -Command "icacls '${PROJECT_BINARY_DIR}/testfiles/test_keystore' /inheritance:r '/grant' ($env:USERNAME + ':(OI)(CI)F') '/grant' 'Administrators:(OI)(CI)F'" + ) + set_tests_properties (H5SIGN-verify-secure-keystore PROPERTIES + DEPENDS H5SIGN-verify-setup-keystore + FIXTURES_SETUP H5SIGN_keystore + ) + endif () + + # Copy public key to keystore directory + add_test ( + NAME H5SIGN-verify-copy-pubkey + COMMAND ${CMAKE_COMMAND} -E copy test_public.pem test_keystore/test_public.pem + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-verify-copy-pubkey PROPERTIES + DEPENDS H5SIGN-verify-setup-keystore + FIXTURES_REQUIRED H5SIGN_keys + FIXTURES_SETUP H5SIGN_keystore + ) + + # ---- Signed plugin preparation (fixture: H5SIGN_signed_plugins) ---- + + # Create unsigned plugin for negative test + add_test ( + NAME H5SIGN-verify-copy-unsigned + COMMAND ${CMAKE_COMMAND} -E copy plugin_medium.so plugin_unsigned.so + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-verify-copy-unsigned PROPERTIES + FIXTURES_REQUIRED H5SIGN_testfiles + FIXTURES_SETUP H5SIGN_signed_plugins + ) + + # Copy plugin_small.so to a private file for the verification sign test, + # so it does not conflict with H5SIGN-sign_small which signs the same file. + add_test ( + NAME H5SIGN-verify-copy-small-for-signing + COMMAND ${CMAKE_COMMAND} -E copy plugin_small.so plugin_small_to_sign.so + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-verify-copy-small-for-signing PROPERTIES + FIXTURES_REQUIRED "H5SIGN_testfiles;H5SIGN_keystore" + FIXTURES_SETUP H5SIGN_signed_plugins + ) + + # Sign the private copy of the small plugin for verification tests + add_test ( + NAME H5SIGN-verify-sign-plugins + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $ -p plugin_small_to_sign.so -k test_private.pem + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-verify-sign-plugins PROPERTIES + DEPENDS H5SIGN-verify-copy-small-for-signing + FIXTURES_REQUIRED "H5SIGN_testfiles;H5SIGN_keys" + FIXTURES_SETUP H5SIGN_signed_plugins + ) + + # Rename signed plugin for verification test + add_test ( + NAME H5SIGN-verify-rename-signed + COMMAND ${CMAKE_COMMAND} -E copy plugin_small_to_sign.so plugin_signed.so + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-verify-rename-signed PROPERTIES + DEPENDS H5SIGN-verify-sign-plugins + FIXTURES_SETUP H5SIGN_signed_plugins + ) + + # Copy plugin_large.so to a private file for the cache sign test, + # so it does not conflict with H5SIGN-sign_large which signs the same file. + add_test ( + NAME H5SIGN-verify-copy-large-for-signing + COMMAND ${CMAKE_COMMAND} -E copy plugin_large.so plugin_large_to_sign.so + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-verify-copy-large-for-signing PROPERTIES + FIXTURES_REQUIRED "H5SIGN_testfiles;H5SIGN_keystore" + FIXTURES_SETUP H5SIGN_signed_plugins + ) + + # Sign the private copy of the large plugin for cache tests + add_test ( + NAME H5SIGN-verify-sign-cache-test + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $ -p plugin_large_to_sign.so -k test_private.pem + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-verify-sign-cache-test PROPERTIES + DEPENDS H5SIGN-verify-copy-large-for-signing + FIXTURES_REQUIRED "H5SIGN_testfiles;H5SIGN_keys" + FIXTURES_SETUP H5SIGN_signed_plugins + ) + + # Rename signed plugin for cache test + add_test ( + NAME H5SIGN-verify-rename-cache-test + COMMAND ${CMAKE_COMMAND} -E copy plugin_large_to_sign.so plugin_cache_test.so + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-verify-rename-cache-test PROPERTIES + DEPENDS H5SIGN-verify-sign-cache-test + FIXTURES_SETUP H5SIGN_signed_plugins + ) + + # Create tampered plugin (sign then modify) + add_test ( + NAME H5SIGN-verify-create-tampered + COMMAND ${CMAKE_COMMAND} + -DFILE=${PROJECT_BINARY_DIR}/testfiles/plugin_tampered.so + -DSOURCE=${PROJECT_BINARY_DIR}/testfiles/plugin_signed.so + -P ${HDF5_TOOLS_TEST_H5SIGN_SOURCE_DIR}/CreateTamperedPlugin.cmake + ) + set_tests_properties (H5SIGN-verify-create-tampered PROPERTIES + DEPENDS H5SIGN-verify-rename-signed + FIXTURES_SETUP H5SIGN_signed_plugins + ) + + # ---- Run verification tests ---- + add_test ( + NAME H5SIGN-verify-tests + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $ + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/testfiles" + ) + set_tests_properties (H5SIGN-verify-tests PROPERTIES + FIXTURES_REQUIRED "H5SIGN_testfiles;H5SIGN_keys;H5SIGN_keystore;H5SIGN_signed_plugins" + ) + +else () + message(WARNING "OpenSSL executable not found - h5sign tests will be skipped") +endif () diff --git a/tools/test/h5sign/CreateTamperedPlugin.cmake b/tools/test/h5sign/CreateTamperedPlugin.cmake new file mode 100644 index 00000000000..247098a95b1 --- /dev/null +++ b/tools/test/h5sign/CreateTamperedPlugin.cmake @@ -0,0 +1,49 @@ +# +# CMake script to create a tampered plugin for testing +# This script copies a signed plugin and modifies it to simulate tampering +# + +if (NOT DEFINED FILE OR NOT DEFINED SOURCE) + message(FATAL_ERROR "FILE and SOURCE must be defined") +endif () + +# Read the source file directly +file(READ ${SOURCE} content HEX) + +# Modify a byte in the middle of the file (before the signature) +# This simulates tampering with the plugin binary +string(LENGTH "${content}" content_length) +math(EXPR modify_pos "${content_length} / 2") +math(EXPR modify_pos "${modify_pos} - ${modify_pos} % 2") # Ensure even position + +# Extract parts +string(SUBSTRING "${content}" 0 ${modify_pos} before) +string(SUBSTRING "${content}" ${modify_pos} 2 byte_to_modify) +math(EXPR after_pos "${modify_pos} + 2") +string(SUBSTRING "${content}" ${after_pos} -1 after) + +# Flip the byte (XOR with FF) +if (byte_to_modify STREQUAL "00") + set(modified_byte "FF") +elseif (byte_to_modify STREQUAL "FF") + set(modified_byte "00") +else () + # Just flip the first hex digit + string(SUBSTRING "${byte_to_modify}" 0 1 first_digit) + string(SUBSTRING "${byte_to_modify}" 1 1 second_digit) + if (first_digit STREQUAL "F") + set(first_digit "0") + else () + set(first_digit "F") + endif () + set(modified_byte "${first_digit}${second_digit}") +endif () + +# Reassemble +set(tampered_content "${before}${modified_byte}${after}") + +# Write to destination +file(WRITE ${FILE} "${tampered_content}" HEX) + +message(STATUS "Created tampered plugin: ${FILE}") +message(STATUS " Modified byte at position ${modify_pos}: ${byte_to_modify} -> ${modified_byte}") diff --git a/tools/test/h5sign/h5signgentest.c b/tools/test/h5sign/h5signgentest.c new file mode 100644 index 00000000000..300ea556765 --- /dev/null +++ b/tools/test/h5sign/h5signgentest.c @@ -0,0 +1,80 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Copyright by The HDF Group. * + * All rights reserved. * + * * + * This file is part of HDF5. The full HDF5 copyright notice, including * + * terms governing use, modification, and redistribution, is contained in * + * the LICENSE file, which can be found at the root of the source code * + * distribution tree, or in https://www.hdfgroup.org/licenses. * + * If you do not have access to either file, you may request a copy from * + * help@hdfgroup.org. * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +/* + * Purpose: Generate test files for h5sign testing + * + * Creates dummy plugin binaries and test keys for signature testing. + */ + +#include "hdf5.h" +#include "H5private.h" + +/*------------------------------------------------------------------------- + * Function: create_dummy_plugin + * + * Purpose: Create a simple binary file that mimics a plugin + * + * Return: Success: 0 + * Failure: 1 + *------------------------------------------------------------------------- + */ +static int +create_dummy_plugin(const char *filename, size_t size) +{ + FILE *fp; + size_t i; + + if (NULL == (fp = fopen(filename, "wb"))) { + fprintf(stderr, "Error: Cannot create file '%s'\n", filename); + return 1; + } + + /* Write simple binary pattern */ + for (i = 0; i < size; i++) { + unsigned char byte = (unsigned char)(i % 256); + if (1 != fwrite(&byte, 1, 1, fp)) { + fprintf(stderr, "Error: Cannot write to file '%s'\n", filename); + fclose(fp); + return 1; + } + } + + fclose(fp); + return 0; +} + +/*------------------------------------------------------------------------- + * Function: main + * + * Purpose: Generate test files for h5sign + * + * Return: Success: EXIT_SUCCESS + * Failure: EXIT_FAILURE + *------------------------------------------------------------------------- + */ +int +main(void) +{ + /* Create dummy plugin files of various sizes */ + if (create_dummy_plugin("plugin_small.so", 1024) != 0) + return EXIT_FAILURE; + + if (create_dummy_plugin("plugin_medium.so", 64 * 1024) != 0) + return EXIT_FAILURE; + + if (create_dummy_plugin("plugin_large.so", 1024 * 1024) != 0) + return EXIT_FAILURE; + + printf("Test files created successfully\n"); + return EXIT_SUCCESS; +} diff --git a/tools/test/h5sign/h5signverifytest.c b/tools/test/h5sign/h5signverifytest.c new file mode 100644 index 00000000000..863a4e3caab --- /dev/null +++ b/tools/test/h5sign/h5signverifytest.c @@ -0,0 +1,388 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Copyright by The HDF Group. * + * All rights reserved. * + * * + * This file is part of HDF5. The full HDF5 copyright notice, including * + * terms governing use, modification, and redistribution, is contained in * + * the LICENSE file, which can be found at the root of the source code * + * distribution tree, or in https://www.hdfgroup.org/licenses. * + * If you do not have access to either file, you may request a copy from * + * help@hdfgroup.org. * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +/* + * Purpose: Test signature verification and caching + * + * Tests the H5PL__verify_signature_appended() function. + */ + +#include "hdf5.h" +#include "H5private.h" + +/* Declare as friend of H5PL package to access package-private functions */ +#define H5PL_FRIEND +#include "H5PLpkg.h" /* For H5PL__verify_signature_appended() */ +#include "H5PLsig.h" /* For signature structures */ +#include "H5MMprivate.h" + +#ifdef H5_REQUIRE_DIGITAL_SIGNATURE + +#include +#include + +#include /* For SHA-256 hash in revocation test */ +#include "H5encode.h" /* For UINT32DECODE in footer decode */ + +/* Test file names */ +#define TEST_PLUGIN_SIGNED "plugin_signed.so" +#define TEST_PLUGIN_UNSIGNED "plugin_unsigned.so" +#define TEST_PLUGIN_TAMPERED "plugin_tampered.so" +#define TEST_PUBLIC_KEY "test_public.pem" +#define TEST_KEYSTORE_DIR "test_keystore" + +/* Test counters */ +static int tests_passed = 0; +static int tests_failed = 0; + +/*------------------------------------------------------------------------- + * Function: test_verify_signed_plugin + * + * Purpose: Test that a properly signed plugin verifies successfully + * + * Return: 0 on success, 1 on failure + *------------------------------------------------------------------------- + */ +static int +test_verify_signed_plugin(void) +{ + herr_t ret; + + printf("TEST: Verify signed plugin... "); + + /* Verify the signed plugin */ + ret = H5PL__verify_signature_appended(TEST_PLUGIN_SIGNED); + + if (ret == SUCCEED) { + printf("PASSED\n"); + tests_passed++; + return 0; + } + else { + printf("FAILED\n"); + printf(" Expected: SUCCEED, Got: FAIL\n"); + tests_failed++; + return 1; + } +} + +/*------------------------------------------------------------------------- + * Function: test_verify_unsigned_plugin + * + * Purpose: Test that an unsigned plugin fails verification + * + * Return: 0 on success, 1 on failure + *------------------------------------------------------------------------- + */ +static int +test_verify_unsigned_plugin(void) +{ + herr_t ret; + + printf("TEST: Verify unsigned plugin (should fail)... "); + + /* Try to verify unsigned plugin - should fail */ + ret = H5PL__verify_signature_appended(TEST_PLUGIN_UNSIGNED); + + if (ret == FAIL) { + printf("PASSED\n"); + tests_passed++; + return 0; + } + else { + printf("FAILED\n"); + printf(" Expected: FAIL, Got: SUCCEED (unsigned plugin should not verify!)\n"); + tests_failed++; + return 1; + } +} + +/*------------------------------------------------------------------------- + * Function: test_verify_tampered_plugin + * + * Purpose: Test that a tampered plugin fails verification + * + * Return: 0 on success, 1 on failure + *------------------------------------------------------------------------- + */ +static int +test_verify_tampered_plugin(void) +{ + herr_t ret; + + printf("TEST: Verify tampered plugin (should fail)... "); + + /* Try to verify tampered plugin - should fail */ + ret = H5PL__verify_signature_appended(TEST_PLUGIN_TAMPERED); + + if (ret == FAIL) { + printf("PASSED\n"); + tests_passed++; + return 0; + } + else { + printf("FAILED\n"); + printf(" Expected: FAIL, Got: SUCCEED (tampered plugin should not verify!)\n"); + tests_failed++; + return 1; + } +} + +/*------------------------------------------------------------------------- + * Function: create_revocation_file + * + * Purpose: Read a signed plugin, compute the SHA-256 hash of its + * signature, and write the hex hash to + * /revoked_signatures.txt. + * + * Return: 0 on success, 1 on failure + *------------------------------------------------------------------------- + */ +static int +create_revocation_file(const char *signed_plugin, const char *keystore_dir) +{ + int fd = -1; + h5_stat_t st; + uint8_t footer_buf[H5PL_SIG_FOOTER_SIZE]; + H5PL_sig_footer_t footer; + unsigned char *signature = NULL; + size_t binary_size; + unsigned char hash[EVP_MAX_MD_SIZE]; + unsigned int hash_len = 0; + EVP_MD_CTX *mdctx = NULL; + FILE *fp = NULL; + char filepath[512]; + unsigned int i; + int ret = 1; /* assume failure */ + + fd = HDopen(signed_plugin, O_RDONLY, 0); + if (fd < 0) + goto cleanup; + if (HDfstat(fd, &st) < 0) + goto cleanup; + + /* Read footer from end of file */ + if (HDlseek(fd, (HDoff_t)(st.st_size - H5PL_SIG_FOOTER_SIZE), SEEK_SET) < 0) + goto cleanup; + if (HDread(fd, footer_buf, H5PL_SIG_FOOTER_SIZE) != (h5_posix_io_ret_t)H5PL_SIG_FOOTER_SIZE) + goto cleanup; + if (!H5PL_sig_decode_footer(footer_buf, sizeof(footer_buf), &footer)) + goto cleanup; + + /* Read signature bytes */ + signature = (unsigned char *)malloc(footer.signature_length); + if (!signature) + goto cleanup; + + binary_size = (size_t)st.st_size - footer.signature_length - H5PL_SIG_FOOTER_SIZE; + if (HDlseek(fd, (HDoff_t)binary_size, SEEK_SET) < 0) + goto cleanup; + if (HDread(fd, signature, footer.signature_length) != (h5_posix_io_ret_t)footer.signature_length) + goto cleanup; + + /* Compute SHA-256 hash of signature */ + mdctx = EVP_MD_CTX_new(); + if (!mdctx) + goto cleanup; + if (1 != EVP_DigestInit_ex(mdctx, EVP_sha256(), NULL)) + goto cleanup; + if (1 != EVP_DigestUpdate(mdctx, signature, footer.signature_length)) + goto cleanup; + if (1 != EVP_DigestFinal_ex(mdctx, hash, &hash_len)) + goto cleanup; + + /* Write hex hash to revoked_signatures.txt */ + snprintf(filepath, sizeof(filepath), "%s/revoked_signatures.txt", keystore_dir); + fp = fopen(filepath, "w"); + if (!fp) + goto cleanup; + + fprintf(fp, "# Revoked signature hash for testing\n"); + for (i = 0; i < hash_len; i++) + fprintf(fp, "%02x", hash[i]); + fprintf(fp, "\n"); + + ret = 0; /* success */ + +cleanup: + if (fp) + fclose(fp); + if (mdctx) + EVP_MD_CTX_free(mdctx); + free(signature); + if (fd >= 0) + HDclose(fd); + return ret; +} + +/*------------------------------------------------------------------------- + * Function: remove_revocation_file + * + * Purpose: Remove revoked_signatures.txt from the keystore directory + * + * Return: void + *------------------------------------------------------------------------- + */ +static void +remove_revocation_file(const char *keystore_dir) +{ + char filepath[512]; + snprintf(filepath, sizeof(filepath), "%s/revoked_signatures.txt", keystore_dir); + HDremove(filepath); +} + +/*------------------------------------------------------------------------- + * Function: test_verify_revoked_plugin + * + * Purpose: Test that a signed plugin with a revoked signature + * fails verification. + * + * This test must be run after H5close() / H5open() so that + * the keystore (including the revocation list) is reloaded. + * + * Return: 0 on success, 1 on failure + *------------------------------------------------------------------------- + */ +static int +test_verify_revoked_plugin(void) +{ + herr_t ret; + + printf("TEST: Verify revoked plugin (should fail)... "); + + ret = H5PL__verify_signature_appended(TEST_PLUGIN_SIGNED); + + if (ret == FAIL) { + printf("PASSED\n"); + tests_passed++; + return 0; + } + else { + printf("FAILED\n"); + printf(" Expected: FAIL, Got: SUCCEED (revoked plugin should not verify!)\n"); + tests_failed++; + return 1; + } +} + +/*------------------------------------------------------------------------- + * Function: main + * + * Purpose: Run all signature verification tests + * + * Return: EXIT_SUCCESS or EXIT_FAILURE + *------------------------------------------------------------------------- + */ +int +main(void) +{ + printf("\n"); + printf("========================================\n"); + printf("HDF5 Signature Verification Test Suite\n"); + printf("========================================\n"); + printf("\n"); + + /* Initialize HDF5 library before using any HDF5 functions */ + if (H5open() < 0) { + fprintf(stderr, "ERROR: Cannot initialize HDF5 library\n"); + return EXIT_FAILURE; + } + + /* Set up environment for keystore */ + if (HDsetenv("HDF5_PLUGIN_KEYSTORE", TEST_KEYSTORE_DIR, 1) != 0) { + fprintf(stderr, "ERROR: Cannot set HDF5_PLUGIN_KEYSTORE environment variable\n"); + H5close(); + return EXIT_FAILURE; + } + + /* Initialize the H5PL package through a public API call so that + * H5PL_term_package() runs cleanup during H5close(). Without this, + * the package-private test functions alone would not set H5PL_init_g, + * so H5close() would skip keystore cleanup and the revocation test + * would fail (stale keystore survives the H5close/H5open cycle). */ + { + unsigned mask; + if (H5PLget_loading_state(&mask) < 0) { + fprintf(stderr, "ERROR: Cannot initialize H5PL package\n"); + H5close(); + return EXIT_FAILURE; + } + } + + /* Run basic verification tests */ + test_verify_signed_plugin(); + test_verify_unsigned_plugin(); + test_verify_tampered_plugin(); + + /* --- Revocation test --- + * Close and re-open HDF5 so the keystore is re-initialized with the + * revocation list. The signed plugin's signature hash is written to + * revoked_signatures.txt before re-opening. */ + H5close(); + + if (create_revocation_file(TEST_PLUGIN_SIGNED, TEST_KEYSTORE_DIR) != 0) { + fprintf(stderr, "ERROR: Cannot create revocation file for testing\n"); + return EXIT_FAILURE; + } + + if (H5open() < 0) { + fprintf(stderr, "ERROR: Cannot re-initialize HDF5 library for revocation test\n"); + remove_revocation_file(TEST_KEYSTORE_DIR); + return EXIT_FAILURE; + } + + /* Re-set keystore env var (still valid, but ensures it's set after re-init) */ + HDsetenv("HDF5_PLUGIN_KEYSTORE", TEST_KEYSTORE_DIR, 1); + + test_verify_revoked_plugin(); + + /* Clean up revocation file so it doesn't affect other tests */ + H5close(); + remove_revocation_file(TEST_KEYSTORE_DIR); + + /* Re-open for final cleanup */ + H5open(); + + /* Print summary */ + printf("\n"); + printf("========================================\n"); + printf("Test Summary\n"); + printf("========================================\n"); + printf("Tests Passed: %d\n", tests_passed); + printf("Tests Failed: %d\n", tests_failed); + printf("Total Tests: %d\n", tests_passed + tests_failed); + printf("\n"); + + /* Clean up HDF5 library resources */ + H5close(); + + if (tests_failed == 0) { + printf("ALL TESTS PASSED!\n"); + return EXIT_SUCCESS; + } + else { + printf("SOME TESTS FAILED!\n"); + return EXIT_FAILURE; + } +} + +#else /* H5_REQUIRE_DIGITAL_SIGNATURE */ + +int +main(void) +{ + printf("Digital signature support not enabled (H5_REQUIRE_DIGITAL_SIGNATURE not defined)\n"); + printf("Skipping signature verification tests\n"); + return EXIT_SUCCESS; /* Not a failure - just not compiled with signature support */ +} + +#endif /* H5_REQUIRE_DIGITAL_SIGNATURE */