✨[Feature] Digital Signature Verification for HDF5 Plugins (#6198)

feat: add optional digital signature verification for HDF5 filter plugins

Introduce an opt-in plugin signing and verification system that allows
HDF5 deployments to require cryptographically signed filter plugins before
loading them. Disabled by default (HDF5_REQUIRE_SIGNED_PLUGINS=OFF).

New tool: h5sign
- Signs plugin shared libraries by appending an RSA signature and a
  14-byte footer (algo_id | sig_len | 8-byte magic | format_ver) to the
  binary without modifying the original content.
- Supports SHA-512 (default), SHA-256, SHA-384, and their PSS variants
  (-a/--algorithm flag).
- Detects already-signed plugins; --force strips the old signature and
  re-signs.
- Security hardened: keeps the file descriptor open through hashing and
  appending (no TOCTOU window), enforces a 2048-bit minimum RSA key size,
  rolls back partial writes on failure, and rejects paths that are not
  regular files.

Verification (H5PLsig.c)
- At plugin load time, reads the footer, validates the magic and format
  version, then checks the RSA signature against all public keys found in
  the KeyStore directory.
- File is hashed once; per-key verification operates on the pre-computed
  digest (no redundant I/O for multi-key keystores).
- Plugins whose signature hash appears in revoked_signatures.txt are
  rejected regardless of key validity.
- Runtime debug output via HDF5_DEBUG=pl.

KeyStore management
- Trusted public keys are PEM files in a directory specified by
  HDF5_PLUGIN_KEYSTORE_DIR (build time) or HDF5_PLUGIN_KEYSTORE (env var).
- HDF5_LOCK_PLUGIN_KEYSTORE cmake option disables the env-var override for
  security-hardened deployments.

Test infrastructure
- h5signverifytest: positive, negative, tamper, re-sign, and revocation
  test cases.
- CTest fixture-based dependency graph (FIXTURES_SETUP/FIXTURES_REQUIRED)
  replaces fragile DEPENDS chains so tests remain correct under -R filtering.
- Dedicated signed-plugins.yml CI workflow; full test suite scoped to
  H5SIGN and H5PLUGIN-signature tests to avoid unrelated flaky failures.
- Cross-platform: Linux, macOS, and Windows (MSVC-compatible, BIO-based
  OpenSSL I/O, HDsleep/HDsetenv portability wrappers).

Documentation: docs/PLUGIN_SIGNATURE_README.md covers usage, footer
format, revocation file format, FAQ, and troubleshooting.

Co-authored-by: Glenn Song <gsong@hdfgroup.org>
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Scot Breitenfeld
2026-05-06 09:35:19 -05:00
committed by GitHub
co-authored by Glenn Song github-actions
parent 6e6273e573
commit 86bdc78365
37 changed files with 5419 additions and 3 deletions
+139
View File
@@ -0,0 +1,139 @@
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 \
-R "H5SIGN|H5PLUGIN-signature"
+82
View File
@@ -766,6 +766,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
#-----------------------------------------------------------------------------
+52
View File
@@ -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> <plugin_dir>)
``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 $<TARGET_FILE:h5sign>
ARGS -p "${PLUGIN_DIR}/$<TARGET_FILE_NAME:${TARGET}>"
-k "${CMAKE_BINARY_DIR}/private.pem"
COMMENT "Signing test plugin ${TARGET} for signature verification"
)
endif()
endfunction()
+6 -1
View File
@@ -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)
+1
View File
@@ -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}
+287
View File
@@ -0,0 +1,287 @@
# 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 14-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.
Note: the revocation entry is **not** the raw signature itself hex-encoded.
It is the SHA-256 digest of the raw signature bytes, represented as 64 hex
characters (32 bytes). This provides a fixed-length identifier regardless of
RSA key size. To compute the hash for a signed plugin:
```bash
# Extract the raw signature, then hash it
h5sign -p my_plugin.so -v # displays signature details
# Use OpenSSL to compute SHA-256 of the raw signature bytes
```
```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 (14 bytes): |
| - Algorithm ID (1B) |
| - Signature length (4B) |
| - Magic (8B, non-ASCII) |
| - Format version (1B) |
+-----------------------------+
```
### 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 14 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**: <https://www.openssl.org/docs/>
- **HDF5 Plugin Documentation**: <https://portal.hdfgroup.org/display/support/Registered+Filter+Plugins>
---
**Document Version**: 1.2
**Last Updated**: 2026-03-20
**HDF5 Version**: 2.2.0+
+8
View File
@@ -97,6 +97,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=<path>`) 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 `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.
@@ -119,6 +123,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 14-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
+2
View File
@@ -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
)
+1
View File
@@ -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";
+12
View File
@@ -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.
*/
+21
View File
@@ -108,6 +108,21 @@ 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
#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 +171,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 */
+1295
View File
@@ -0,0 +1,1295 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* 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 <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/bio.h>
#include <openssl/err.h>
/* For directory operations */
#ifndef H5_HAVE_WIN32_API
#include <dirent.h>
#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 <keystore_dir>/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
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,
"no valid public keys found for plugin signature verification\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");
/* 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 */
+175
View File
@@ -0,0 +1,175 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* 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
*/
/* 8-byte magic to identify HDF5 signed plugins.
* Modelled on the HDF5 file signature ("\211HDF\r\n\032\n") but distinct.
* Contains non-ASCII bytes to detect transport corruption and reduce
* the chance of a false positive in arbitrary binary data. */
#define H5PL_SIG_MAGIC_LEN 8
static const uint8_t H5PL_SIG_MAGIC[H5PL_SIG_MAGIC_LEN] = {0x89, 'H', 'P', 'S', '\r', '\n', 0x1A, '\n'};
/* 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 (14 bytes) */
#define H5PL_SIG_FOOTER_SIZE 14
/* 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 (14 bytes, little-endian where applicable):
* [algo_id: 1][sig_len: 4][magic: 8][format_ver: 1]
* byte 0 bytes 1-4 bytes 5-12 byte 13
*
* Magic (8 bytes) and version (1 byte) are placed at the end so they
* reside at a fixed offset from EOF regardless of any future footer
* growth. This lets any library version locate the magic, check the
* version, and give a meaningful error (e.g. "unsupported version")
* rather than "not signed".
*
* During decoding, magic is still verified *first* — before any other
* field is interpreted — to avoid parsing untrusted data from an
* unsigned file.
*
* 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 signature_length; /* Length of RSA signature in bytes */
H5PL_sig_algo_t algorithm_id; /* Hash algorithm identifier */
uint8_t format_version; /* Footer format version */
} 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.
* buf_size must be >= H5PL_SIG_FOOTER_SIZE (14).
*-------------------------------------------------------------------------
*/
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 */
*p++ = (uint8_t)footer->algorithm_id; /* byte 0 */
UINT32ENCODE(p, footer->signature_length); /* bytes 1-4 */
memcpy(p, H5PL_SIG_MAGIC, H5PL_SIG_MAGIC_LEN); /* bytes 5-12 */
p += H5PL_SIG_MAGIC_LEN;
*p++ = footer->format_version; /* byte 13 */
} /* 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.
* buf_size must be >= H5PL_SIG_FOOTER_SIZE (14).
*
* On-disk order is [algo_id][sig_len][magic][version], but
* magic is decoded and verified first (at offset 5) to avoid
* interpreting untrusted fields from an unsigned file.
*-------------------------------------------------------------------------
*/
static inline bool
H5PL_sig_decode_footer(const uint8_t *buf, size_t buf_size, H5PL_sig_footer_t *footer)
{
const uint8_t *p;
if (buf_size < H5PL_SIG_FOOTER_SIZE)
return false;
/* Decode and verify magic first (at offset 5) */
if (memcmp(buf + 5, H5PL_SIG_MAGIC, H5PL_SIG_MAGIC_LEN) != 0)
return false;
/* Magic valid — now decode remaining fields from the beginning */
p = buf;
footer->algorithm_id = (H5PL_sig_algo_t)*p++; /* byte 0 */
UINT32DECODE(p, footer->signature_length); /* bytes 1-4 */
/* skip magic (already verified above) */
p += H5PL_SIG_MAGIC_LEN; /* bytes 5-12 */
footer->format_version = *p++; /* byte 13 */
/* 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 */
+1
View File
@@ -993,6 +993,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 */
+3
View File
@@ -667,4 +667,7 @@
/* Define to `long' if <sys/types.h> 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
+100
View File
@@ -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)
"$<TARGET_FILE:${HDF5_TEST_PLUGIN_TARGET}>"
"${CMAKE_BINARY_DIR}/filter_plugin_dir1/$<TARGET_FILE_NAME:${HDF5_TEST_PLUGIN_TARGET}>"
)
#-----------------------------------------------------------------------------
# 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)
"$<TARGET_FILE:${HDF5_TEST_PLUGIN_TARGET}>"
"${CMAKE_BINARY_DIR}/filter_plugin_dir2/$<TARGET_FILE_NAME:${HDF5_TEST_PLUGIN_TARGET}>"
)
#-----------------------------------------------------------------------------
# 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)
"$<TARGET_FILE:${HDF5_VFD_PLUGIN_LIB_TARGET}>"
"${CMAKE_BINARY_DIR}/null_vfd_plugin_dir/$<TARGET_FILE_NAME:${HDF5_VFD_PLUGIN_LIB_TARGET}>"
)
#-----------------------------------------------------------------------------
# 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)
"$<TARGET_FILE:${HDF5_VOL_PLUGIN_LIB_TARGET}>"
"${CMAKE_BINARY_DIR}/null_vol_plugin_dir/$<TARGET_FILE_NAME:${HDF5_VOL_PLUGIN_LIB_TARGET}>"
)
#-----------------------------------------------------------------------------
# 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 ()
@@ -822,6 +903,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};$<$<BOOL:${HDF5_ENABLE_PARALLEL}>:${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 ()
##############################################################################
+24 -2
View File
@@ -1002,13 +1002,31 @@ if (BUILD_SHARED_LIBS)
endif ()
add_test (NAME H5PLUGIN-filter_plugin COMMAND $<TARGET_FILE:filter_plugin>)
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 $<TARGET_FILE:test_plugin_signature>)
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)
@@ -1102,8 +1120,12 @@ if (BUILD_SHARED_LIBS)
endif ()
add_test (NAME H5PLUGIN-vol_plugin COMMAND $<TARGET_FILE:vol_plugin>)
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}")
+1293
View File
@@ -0,0 +1,1293 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* 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 <sys/stat.h>
#include <fcntl.h>
/* 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;
/* 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.signature_length = 256;
footer.algorithm_id = H5PL_SIG_ALGO_SHA256;
footer.format_version = 1;
H5PL_sig_encode_footer(footer_bytes, sizeof(footer_bytes), &footer);
/* Corrupt the magic bytes (at offset 5) */
footer_bytes[5] = 0xDE;
footer_bytes[6] = 0xAD;
footer_bytes[7] = 0xBE;
footer_bytes[8] = 0xEF;
}
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;
}
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 */
+8
View File
@@ -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 ()
+82
View File
@@ -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
$<$<BOOL:${HDF5_ENABLE_PARALLEL}>:${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 ()
+849
View File
@@ -0,0 +1,849 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* 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 (14 bytes, little-endian where applicable):
* - Algorithm ID (1 byte)
* - Signature length (4 bytes)
* - Magic number (8 bytes, non-ASCII)
* - Format version (1 byte)
*
* 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 <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/bio.h>
#include <openssl/err.h>
/* On Windows, OpenSSL requires applink to bridge different CRT versions */
#ifdef _MSC_VER
#include <openssl/applink.c>
#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'},
{"version", 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 <plugin> -k <private-key.pem> [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 <file> Plugin binary to sign (.so, .dll, .dylib)\n");
fprintf(rawoutstream, " -k, --key <file> RSA private key in PEM format\n");
fprintf(rawoutstream, "\n");
fprintf(rawoutstream, "OTHER OPTIONS\n");
fprintf(rawoutstream, " -a, --algorithm <alg> 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, --version Print version number and exit\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;
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;
}
+9
View File
@@ -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 ()
+5
View File
@@ -47,6 +47,11 @@ if (BUILD_SHARED_LIBS)
"$<TARGET_FILE:${H5COPY_TOOL_PLUGIN_LIB_TARGET}>"
"${CMAKE_BINARY_DIR}/plugins/$<TARGET_FILE_NAME:${H5COPY_TOOL_PLUGIN_LIB_TARGET}>"
)
#-----------------------------------------------------------------------------
# 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
+4
View File
@@ -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}"
$<$<BOOL:${HDF5_REQUIRE_SIGNED_PLUGINS}>:-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}"
$<$<BOOL:${HDF5_REQUIRE_SIGNED_PLUGINS}>:-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}"
$<$<BOOL:${HDF5_REQUIRE_SIGNED_PLUGINS}>:-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}"
$<$<BOOL:${HDF5_REQUIRE_SIGNED_PLUGINS}>:-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
+5
View File
@@ -40,6 +40,11 @@ if (BUILD_SHARED_LIBS)
"$<TARGET_FILE:${H5DIFF_TOOL_PLUGIN_LIB_TARGET}>"
"${CMAKE_BINARY_DIR}/plugins/$<TARGET_FILE_NAME:${H5DIFF_TOOL_PLUGIN_LIB_TARGET}>"
)
#-----------------------------------------------------------------------------
# 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)
+1
View File
@@ -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}"
$<$<BOOL:${HDF5_REQUIRE_SIGNED_PLUGINS}>:-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}")
+5
View File
@@ -40,6 +40,11 @@ if (BUILD_SHARED_LIBS)
"$<TARGET_FILE:${H5DUMP_TOOL_PLUGIN_LIB_TARGET}>"
"${CMAKE_BINARY_DIR}/plugins/$<TARGET_FILE_NAME:${H5DUMP_TOOL_PLUGIN_LIB_TARGET}>"
)
#-----------------------------------------------------------------------------
# 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)
+1
View File
@@ -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}"
$<$<BOOL:${HDF5_REQUIRE_SIGNED_PLUGINS}>:-DTEST_KEYSTORE_DIR=${CMAKE_BINARY_DIR}/test_keystore>
-P "${HDF_RESOURCES_DIR}/runTest.cmake"
)
set_tests_properties (H5DUMP_UD-${testname}-${resultfile} PROPERTIES
+5
View File
@@ -41,6 +41,11 @@ if (BUILD_SHARED_LIBS)
"$<TARGET_FILE:${H5LS_TOOL_PLUGIN_LIB_TARGET}>"
"${CMAKE_BINARY_DIR}/plugins/$<TARGET_FILE_NAME:${H5LS_TOOL_PLUGIN_LIB_TARGET}>"
)
#-----------------------------------------------------------------------------
# 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)
+1
View File
@@ -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}"
$<$<BOOL:${HDF5_REQUIRE_SIGNED_PLUGINS}>:-DTEST_KEYSTORE_DIR=${CMAKE_BINARY_DIR}/test_keystore>
-P "${HDF_RESOURCES_DIR}/runTest.cmake"
)
set_tests_properties (H5LS_UD-${testname}-${resultfile} PROPERTIES
+6
View File
@@ -98,6 +98,12 @@ if (BUILD_SHARED_LIBS)
"$<TARGET_FILE:${H5REPACK_TOOL_PLUGIN_LIB_VTARGET}>"
"${CMAKE_BINARY_DIR}/plugins/$<TARGET_FILE_NAME:${H5REPACK_TOOL_PLUGIN_LIB_VTARGET}>"
)
#-----------------------------------------------------------------------------
# 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)
+2
View File
@@ -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}"
$<$<BOOL:${HDF5_REQUIRE_SIGNED_PLUGINS}>:-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}"
$<$<BOOL:${HDF5_REQUIRE_SIGNED_PLUGINS}>:-DTEST_KEYSTORE_DIR=${CMAKE_BINARY_DIR}/test_keystore>
-P "${HDF_RESOURCES_DIR}/runTest.cmake"
)
set_tests_properties (H5REPACK_UD-${testname}-h5dump PROPERTIES
+65
View File
@@ -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
$<$<BOOL:${HDF5_ENABLE_PARALLEL}>:${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
$<$<BOOL:${HDF5_ENABLE_PARALLEL}>:${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 ()
+352
View File
@@ -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} $<TARGET_FILE:h5sign> ${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} $<TARGET_FILE:h5signgentest>
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} $<TARGET_FILE:h5sign> -h
)
# Test 2: Show version
add_test (
NAME H5SIGN-V_version
COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $<TARGET_FILE:h5sign> -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} $<TARGET_FILE:h5sign> -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} $<TARGET_FILE:h5sign> -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} $<TARGET_FILE:h5sign> -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} $<TARGET_FILE:h5sign> -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} $<TARGET_FILE:h5sign> -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} $<TARGET_FILE:h5sign> -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} $<TARGET_FILE:h5sign> -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} $<TARGET_FILE:h5sign> -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} $<TARGET_FILE:h5sign> -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} $<TARGET_FILE:h5sign> -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} $<TARGET_FILE:h5sign> -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} $<TARGET_FILE:h5signverifytest>
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 ()
@@ -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}")
+80
View File
@@ -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;
}
+388
View File
@@ -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 <sys/stat.h>
#include <time.h>
#include <openssl/evp.h> /* 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
* <keystore_dir>/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 */