mirror of
https://github.com/HDFGroup/hdf5.git
synced 2026-09-25 04:09:44 +03:00
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>
1294 lines
41 KiB
C
1294 lines
41 KiB
C
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
* 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 */
|