feat(engine): load EC private keys from azihsm:// URIs#582
Conversation
Add Engine::set_load_privkey::<H>() and the LoadPrivKeyHandler trait, mirroring the existing set_destroy plumbing. The C trampoline returns *mut EVP_PKEY, so it wraps its body in catch_panic with a NULL fallback and uses a new result_to_ptr helper (the pointer-returning analogue of result_to_int) that records the error on the OpenSSL ERR queue before returning NULL. Signed-off-by: Christian Walter <christian.walter@9elements.com>
There was a problem hiding this comment.
Pull request overview
Adds OpenSSL 1.1.x engine support for loading private keys from azihsm:// URIs by wiring a new ENGINE_load_private_key hook and implementing EC key resolution through the HSM, with tests covering mock and (ignored) hardware flows.
Changes:
- Added a
LoadPrivKeyHandlertrait + OpenSSL trampoline (ENGINE_set_load_privkey_function) in the engine-core toolkit. - Implemented
azihsm://…;type=…URI parsing and EC key loading that rebuilds anEVP_PKEYfrom DER and attaches the live HSM key viaEC_KEYex_data for cleanup on free. - Extended engine context helpers/tests to support the full generate → export masked blob → load via engine → verify → free cycle.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| plugins/ossl_engine/azihsm_ossl_engine/src/uri.rs | New azihsm:// key URI parser + unit tests. |
| plugins/ossl_engine/azihsm_ossl_engine/src/lib.rs | Registers the new engine load_privkey hook and wires key loading modules. |
| plugins/ossl_engine/azihsm_ossl_engine/src/keyload.rs | Implements EC ENGINE_load_private_key resolution from masked key blobs and ex_data-based lifetime management. |
| plugins/ossl_engine/azihsm_ossl_engine/src/context.rs | Adds idempotent open semantics + with_session helper and end-to-end mock/hardware tests for key loading. |
| plugins/ossl_engine/azihsm_ossl_engine/Cargo.toml | Adjusts Linux OpenSSL dependency placement for the new key loader. |
| plugins/ossl_engine/azihsm_ossl_engine_core/src/error.rs | Adds result_to_ptr helper for pointer-returning OpenSSL callbacks. |
| plugins/ossl_engine/azihsm_ossl_engine_core/src/engine.rs | Adds set_load_privkey plumbing and C trampoline for OpenSSL’s private-key loader callback. |
Implement ENGINE_load_private_key for EC keys. A key id is an azihsm://<masked-key-file>;type=<ec|rsa|rsa-pss> URI (parsed in uri.rs, mirroring the provider's STORE). Loading opens the HSM on first use, unmasks the key pair, rebuilds the public EVP_PKEY from the SDK's DER via the openssl crate, and attaches the live HSM private key to the EC_KEY ex_data so it survives for later operations and is deleted from the HSM when the EVP_PKEY is freed. EngineData gains with_session() to run an operation against the open session. RSA/RSA-PSS URIs parse but return a clear not-supported error for now; RSA loading needs an HSM import path and lands with its own tests. A mock round-trip test generates an EC key, exports its masked blob, loads it back through the engine, and verifies the public key matches. Signed-off-by: Christian Walter <christian.walter@9elements.com>
Add load_ec_key_from_env_smoke to the hw_tests module: open the HSM from the ambient AZIHSM_* environment, generate a persistent EC P-384 key on the device, export its masked blob, load it back through the engine's load_private_key path, and verify the returned EVP_PKEY's public key matches. Dropping the key exercises the ex_data free callback (HSM delete). This is the same round trip tests::load_ec_key_round_trips_through_engine covers against the mock, but against a real device. Like open_from_env_smoke it is compiled only in non-mock builds and #[ignore]d, so it never runs in the mock CI cell; invoke it explicitly on a provisioned host. Signed-off-by: Christian Walter <christian.walter@9elements.com>
f73b850 to
63cca2c
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
plugins/ossl_engine/azihsm_ossl_engine/Cargo.toml:37
- These OpenSSL dependencies are gated only on
target_os = "linux", but all code that uses them is further gated on the crate featureengine. As written, building this crate on Linux without--features enginewill still pull in/compileopenssl/openssl-sys, which can be an unnecessary build-time/system-deps burden. Consider gating these dependencies oncfg(all(target_os = "linux", feature = "engine"))to match the code.
[target.'cfg(target_os = "linux")'.dependencies]
foreign-types.workspace = true
openssl.workspace = true
result_to_ptr returned Ok(ptr) verbatim, so an Ok(NULL) would reach a pointer-returning OpenSSL callback as a bare NULL with nothing on the ERR queue, contradicting the helper's documented contract. Treat Ok(NULL) like Err: push a reason onto the ERR queue, log, and return NULL. Signed-off-by: Christian Walter <christian.walter@9elements.com>
- Own loaded HSM keys in EngineData and delete them from the engine's destroy handler instead of registering an EC_KEY ex_data free callback. A libcrypto-held free callback holds a function pointer into this .so and can fire after the .so is unloaded (the hazard exdata.rs already avoids for ENGINE ex_data). EngineData now owns each loaded key and drops it — deleting it from the HSM — while the .so is still loaded; the ex_data slot keeps only a non-owning pointer, with no free callback. - Read the masked-key blob with O_NOFOLLOW|O_NONBLOCK|O_CLOEXEC and a regular-file check, matching the hardening used elsewhere for key-material files, instead of a bare std::fs::read that follows symlinks. - Reject unsupported key types before any HSM open or file I/O, so they fail with a clear error instead of a misleading environment/filesystem one. - Gate the openssl/foreign-types dependencies behind the engine feature so the crate's manifest declares them only where they are used. Signed-off-by: Christian Walter <christian.walter@9elements.com>
…read - Parse the azihsm:// URI attribute name and value case-insensitively, matching the provider's STORE (which compares with strcasecmp), so e.g. TYPE=EC parses the same as type=ec. - Cap read_masked_key at 64 KiB (a metadata pre-check plus a bounded read), mirroring read_regular_hardened in azihsm_ossl_engine_resiliency, so a caller-supplied path to a very large or unbounded file cannot drive an unbounded allocation. Signed-off-by: Christian Walter <christian.walter@9elements.com>
The round-trip tests wrote the masked-key blob with std::fs::write, which honors the caller's umask and can clobber or follow a symlink at the path. Add a write_key_material helper (create-new, mode 0600, O_NOFOLLOW|O_CLOEXEC) and use it in both the mock and hardware round-trip tests. Signed-off-by: Christian Walter <christian.walter@9elements.com>
…ilure - Make EngineData::with_session and retain_loaded_key pub(crate): they are internal to the key loader (with_session hands out a raw HsmSession, and retain_loaded_key is an implementation detail), not public engine API. - Add EngineData::release_loaded_key to drop a retained key by pointer, and call it from attach_ec when EC_KEY_set_ex_data fails, so a failed load no longer leaves the key in the HSM until engine teardown. Signed-off-by: Christian Walter <christian.walter@9elements.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
plugins/ossl_engine/azihsm_ossl_engine/src/context.rs:64
- Loaded HSM private keys are retained in EngineData and only dropped (deleted from the HSM) when the engine is destroyed, not when the returned EVP_PKEY is freed. This can cause unbounded growth in
loaded_keys/HSM objects in long-lived processes that load many keys, and it also differs from the PR description which states deletion happens on EVP_PKEY free. Consider tying key lifetime to EVP_PKEY lifetime (e.g., via an ex_data free callback combined with an engine reference that prevents.sounload while keys exist), or otherwise provide an explicit release path / caching strategy.
/// HSM private keys loaded via the engine's `load_private_key` path. Owned
/// here — never via an `EC_KEY` ex_data free callback, which would leave a
/// libcrypto-held function pointer into this `.so` (see the module docs of
/// [`azihsm_ossl_engine_core::exdata`]). Each key's `Drop` deletes it from
/// the HSM; that runs when this `EngineData` is dropped by the destroy
/// handler, while the `.so` is loaded. Declared before `hsm` so it drops
/// first, while the session is still open.
//
// `Box` gives each key a stable heap address: keyload stashes a raw pointer
// to it in EC_KEY ex_data, which must stay valid across `Vec` growth — so
// the `vec_box` suggestion to drop the `Box` does not apply.
#[allow(clippy::vec_box)]
loaded_keys: Mutex<Vec<Box<HsmEccPrivateKey>>>,
plugins/ossl_engine/azihsm_ossl_engine/src/keyload.rs:16
- The module docs describe that the live HSM private-key wrapper is deleted when the engine is destroyed (to avoid libcrypto calling back into this
.soafter unload). The PR description, however, says the key is deleted when the EVP_PKEY is freed. Please align the behavior and documentation: either implement EVP_PKEY-tied cleanup safely (preventing.sounload while callbacks may run) or update the PR description/expectations accordingly.
//! The returned key carries the real public key (rebuilt from the SDK's DER via
//! the safe `openssl` crate). The live HSM private-key wrapper is owned by
//! [`EngineData`], which deletes it from the HSM when the engine is destroyed;
//! a non-owning pointer to it is stashed in the underlying `EC_KEY` ex_data for
//! later sign/derive callbacks. Deletion runs from the engine's destroy handler
//! (while this `.so` is loaded), never from a libcrypto ex_data free callback —
//! such a callback holds a function pointer into this `.so` and can be invoked
//! after the `.so` is unloaded (see [`azihsm_ossl_engine_core::exdata`]). The
//! sign/derive methods themselves are not wired yet (a later change), so a
//! loaded key is currently usable for public-key operations (e.g. `-pubout`).
Summary
Adds OpenSSL 1.1.x engine support for loading EC private keys from the HSM via
azihsm://key URIs (ENGINE_load_private_key).load_private_keyhook in the engine-core toolkit (Engine::set_load_privkey+LoadPrivKeyHandler), mirroring the existingset_destroyplumbing. The C trampoline returns*mut EVP_PKEY, so it wraps its body incatch_panicwith a NULL fallback via a newresult_to_ptrhelper (the pointer-returning analogue ofresult_to_int).azihsm://<masked-key-file>;type=<ec|rsa|rsa-pss>URI (parsed inuri.rs, mirroring the provider's STORE). Loading opens the HSM on first use, unmasks the key pair, rebuilds the publicEVP_PKEYfrom the SDK's DER, and attaches the live HSM private key to theEC_KEYex_data so it survives later operations and is deleted from the HSM when theEVP_PKEYis freed.Testing
Full engine suite passes on OpenSSL 1.1.x (Ubuntu 20.04 / 1.1.1f): build, clippy
-D warnings(engine +azihsm_crypto), unit tests, doctests, and theopenssl engine -tload check. A mock round-trip test (load_ec_key_round_trips_through_engine) exercises the full generate → export masked blob → load via the engine → verify public key → free (HSM delete) cycle.Hardware tests
Two
#[ignore]d smoke tests live incontext.rshw_tests(compiled only in non-mock builds, so they never run in the mock CI cell). They validate a real device end to end — e.g. the TPM OBK/POTA sources the mock can't exercise:open_from_env_smoke— HSM open only (partition open → init → session).load_ec_key_from_env_smoke— the full key-loading round trip against a real device.Run them on a provisioned HSM host after configuring the
AZIHSM_*environment: