Files
hdf5/config/cmake/runTest.cmake
86bdc78365 ✨[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>
2026-05-06 09:35:19 -05:00

104 lines
3.7 KiB
CMake

#
# 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.
#
# runTest.cmake executes a command and captures the output in a file. File is then compared
# against a reference file. Exit status of command can also be compared.
cmake_policy(SET CMP0007 NEW)
cmake_policy(SET CMP0053 NEW)
# arguments checking
if (NOT TEST_PROGRAM) #the program to be run
message (FATAL_ERROR "Require TEST_PROGRAM to be defined")
endif ()
if (NOT TEST_FOLDER) # this is the folder where the test program is run
message (FATAL_ERROR "Require TEST_FOLDER to be defined")
endif ()
if (NOT TEST_OUTPUT) # the output file to capture the test program output
message (FATAL_ERROR "Require TEST_OUTPUT to be defined")
endif ()
if (NOT DEFINED TEST_EXPECT)
message (VERBOSE "Optional TEST_EXPECT is not defined")
endif ()
message (STATUS "ARGS: ${TEST_EMULATOR}/'${TEST_JAVA}' ${TEST_PROGRAM} ${TEST_ARGS}")
include (${CMAKE_CURRENT_LIST_DIR}/runExecute.cmake)
EXECUTE_TEST (TEST_FOLDER ${TEST_FOLDER}
TEST_JAVA ${TEST_JAVA}
TEST_PROGRAM ${TEST_PROGRAM}
TEST_ARGS ${TEST_ARGS}
TEST_EMULATOR ${TEST_EMULATOR}
TEST_OUTPUT ${TEST_OUTPUT}
TEST_EXPECT ${TEST_EXPECT}
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}
)
FILTER_TEST (TEST_OUTPUT ${TEST_OUTPUT}
TEST_FOLDER ${TEST_FOLDER}
TEST_NO_DISPLAY ${TEST_NO_DISPLAY}
TEST_REGEX ${TEST_REGEX}
TEST_ERRREF ${TEST_ERRREF}
TEST_REFERENCE ${TEST_REFERENCE}
TEST_MATCH ${TEST_MATCH}
TEST_MASK_ERROR ${TEST_MASK_ERROR}
TEST_FILTER ${TEST_FILTER}
TEST_FILTER_REPLACE ${TEST_FILTER_REPLACE}
TEST_REF_FILTER ${TEST_REF_FILTER}
)
COMPARE_TEST (TEST_OUTPUT ${TEST_OUTPUT}
TEST_FOLDER ${TEST_FOLDER}
TEST_GREP_EXPECT ${TEST_GREP_EXPECT}
TEST_REFERENCE ${TEST_REFERENCE}
TEST_ERRREF ${TEST_ERRREF}
TEST_SKIP_COMPARE ${TEST_SKIP_COMPARE}
TEST_SORT_COMPARE ${TEST_SORT_COMPARE}
TEST_GREP_COMPARE ${TEST_GREP_COMPARE}
TEST_GREP_FILTER ${TEST_GREP_FILTER}
)
# dump the output unless nodisplay option is set
if (TEST_SKIP_COMPARE AND NOT TEST_NO_DISPLAY AND EXISTS "${TEST_FOLDER}/${TEST_OUTPUT}")
file (READ ${TEST_FOLDER}/${TEST_OUTPUT} TEST_STREAM)
execute_process (
COMMAND ${CMAKE_COMMAND} -E echo ${TEST_STREAM}
RESULT_VARIABLE TEST_RESULT
)
endif ()
# Check if the output files should not be removed
if (NOT DEFINED ENV{HDF5_NOCLEANUP})
if (EXISTS "${TEST_FOLDER}/${TEST_OUTPUT}" AND NOT TEST_SAVE)
file (REMOVE ${TEST_FOLDER}/${TEST_OUTPUT})
endif ()
if (EXISTS "${TEST_FOLDER}/${TEST_OUTPUT}.err")
file (REMOVE ${TEST_FOLDER}/${TEST_OUTPUT}.err)
endif ()
if (TEST_DELETE_LIST)
foreach (dfile in ${TEST_DELETE_LIST})
file (REMOVE ${dfile})
endforeach ()
endif ()
endif ()
# everything went fine...
message (STATUS "${TEST_PROGRAM} Passed")