mirror of
https://github.com/HDFGroup/hdf5.git
synced 2026-09-25 04:09:44 +03:00
✨[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:
co-authored by
Glenn Song
github-actions
parent
6e6273e573
commit
86bdc78365
@@ -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"
|
||||
@@ -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
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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+
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
@@ -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
@@ -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 */
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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}")
|
||||
|
||||
@@ -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 */
|
||||
@@ -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 ()
|
||||
|
||||
@@ -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 ()
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 ()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ()
|
||||
@@ -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}")
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
Reference in New Issue
Block a user