Skip to content

feat(engine): load EC private keys from azihsm:// URIs#582

Open
walterchris wants to merge 8 commits into
mainfrom
engine/key-loading
Open

feat(engine): load EC private keys from azihsm:// URIs#582
walterchris wants to merge 8 commits into
mainfrom
engine/key-loading

Conversation

@walterchris

Copy link
Copy Markdown
Collaborator

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_key hook in the engine-core toolkit (Engine::set_load_privkey + LoadPrivKeyHandler), 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 via a new result_to_ptr helper (the pointer-returning analogue of result_to_int).
  • EC key loading: 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, and attaches the live HSM private key to the EC_KEY ex_data so it survives later operations and is deleted from the HSM when the EVP_PKEY is freed.
  • 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).

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 the openssl engine -t load 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 in context.rs hw_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:

export AZIHSM_CREDENTIALS_ID=<32 hex>  AZIHSM_CREDENTIALS_PIN=<32 hex>
export AZIHSM_RESILIENCY_ENABLED=1                     # storage dir + MOBK/POTA persistence
export AZIHSM_OBK_SOURCE=tpm  AZIHSM_POTA_SOURCE=tpm   # source selection
# Storage dir must exist, be mode 0700, and be owned by you (override with
# AZIHSM_RESILIENCY_STORAGE_DIR); default is /var/lib/azihsm/resiliency:
sudo install -d -m 700 -o "$USER" /var/lib/azihsm/resiliency
umask 0077

cargo test -p azihsm_ossl_engine --features engine \
  open_from_env_smoke load_ec_key_from_env_smoke -- --ignored --nocapture

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>
Copilot AI review requested due to automatic review settings July 18, 2026 19:45
@walterchris
walterchris requested a review from rajesh-gali July 18, 2026 19:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 LoadPrivKeyHandler trait + OpenSSL trampoline (ENGINE_set_load_privkey_function) in the engine-core toolkit.
  • Implemented azihsm://…;type=… URI parsing and EC key loading that rebuilds an EVP_PKEY from DER and attaches the live HSM key via EC_KEY ex_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.

Comment thread plugins/ossl_engine/azihsm_ossl_engine/src/keyload.rs Outdated
Comment thread plugins/ossl_engine/azihsm_ossl_engine_core/src/error.rs
Comment thread plugins/ossl_engine/azihsm_ossl_engine/Cargo.toml Outdated
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>
Copilot AI review requested due to automatic review settings July 18, 2026 20:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 feature engine. As written, building this crate on Linux without --features engine will still pull in/compile openssl/openssl-sys, which can be an unnecessary build-time/system-deps burden. Consider gating these dependencies on cfg(all(target_os = "linux", feature = "engine")) to match the code.
[target.'cfg(target_os = "linux")'.dependencies]
foreign-types.workspace = true
openssl.workspace = true

Comment thread plugins/ossl_engine/azihsm_ossl_engine/src/keyload.rs
Comment thread plugins/ossl_engine/azihsm_ossl_engine/src/keyload.rs
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>
Copilot AI review requested due to automatic review settings July 18, 2026 20:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Comment thread plugins/ossl_engine/azihsm_ossl_engine/src/keyload.rs
Comment thread plugins/ossl_engine/azihsm_ossl_engine/src/uri.rs
Comment thread plugins/ossl_engine/azihsm_ossl_engine/src/uri.rs
Comment thread plugins/ossl_engine/azihsm_ossl_engine/src/context.rs
Comment thread plugins/ossl_engine/azihsm_ossl_engine/src/context.rs
Comment thread plugins/ossl_engine/azihsm_ossl_engine/src/context.rs
…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>
Copilot AI review requested due to automatic review settings July 19, 2026 10:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Comment thread plugins/ossl_engine/azihsm_ossl_engine/src/context.rs
Comment thread plugins/ossl_engine/azihsm_ossl_engine/src/context.rs Outdated
Comment thread plugins/ossl_engine/azihsm_ossl_engine/src/keyload.rs
…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>
Copilot AI review requested due to automatic review settings July 19, 2026 12:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 .so unload 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 .so after 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 .so unload 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`).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants