Skip to content

Commit 9eb009a

Browse files
committed
fix: wrap_and_cache must not attempt to wrap an HSM-resident key
1 parent ca7eaef commit 9eb009a

8 files changed

Lines changed: 164 additions & 23 deletions

File tree

CHANGELOG/docs_key-autorotation-spec.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@
8686
- Add 3 negative ReCertify test vectors (missing UID, non-existent, not a certificate) ([#968](https://github.com/Cosmian/kms/pull/968))
8787
- Add 2 offset state verification vectors (rekey + rekey-keypair: Offset=0 → Active, Offset=86400 → PreActive) ([#968](https://github.com/Cosmian/kms/pull/968))
8888
- Add 2 negative state restriction vectors: `rekey_preactive_fails`, `rekey_keypair_preactive_fails` ([#968](https://github.com/Cosmian/kms/pull/968))
89-
- Fix test vectors `rekey_keypair_preactive_fails` and `rsa_sign_verify_roundtrip`: `CryptographicUsageMask` must be in `PrivateKeyAttributes`/`PublicKeyAttributes`, not `CommonAttributes` — FIPS mode reads the mask only from per-key sections ([#968](https://github.com/Cosmian/kms/pull/968))
89+
- Fix `wrap_and_cache`: skip server-wide KEK wrapping for HSM-resident keys (UID has `hsm::` prefix) — they are hardware-protected and the wrapping step caused a self-wrap error when the key being created IS the configured KEK ([#968](https://github.com/Cosmian/kms/pull/968))
90+
- Add vector `test_data/vectors/hsm/kek_bootstrap_self_create` + `server_type = "hsm_kek_uncreated"` server type to reproduce and prevent regressions of the HSM self-wrap bug ([#968](https://github.com/Cosmian/kms/pull/968))
9091
- Add 7 keyset resolution test vectors: `keyset_encrypt_latest`, `keyset_encrypt_bare_name`, `keyset_encrypt_latest_after_rotation`, `keyset_decrypt_try_each`, `keyset_decrypt_double_rotation`, `keyset_decrypt_at_latest`, `keyset_rotate_name_at_rejected` ([#968](https://github.com/Cosmian/kms/pull/968))
9192
- Add HSM keyset support: `find_due_for_rotation` on HsmStore reads `CKA_START_DATE`/`CKA_END_DATE`; keyset metadata stored in `CKA_LABEL` (`name::gen::key_id[::latest]`); `SetAttribute rotate_name` writes `CKA_LABEL` via `C_SetAttributeValue`; `SetAttribute rotate_interval` writes `CKA_START_DATE`/`CKA_END_DATE`; `Re-Key` on HSM UIDs generates a new HSM key and updates both CKA_LABELs; `walk_keyset_chain` resolves HSM keysets by `CKA_LABEL` without `ReplacedObjectLink` ([#968](https://github.com/Cosmian/kms/pull/968))
9293
- Add 3 HSM keyset test vectors: `resident_keyset_set_rotate_name`, `resident_keyset_rekey_and_decrypt`, `resident_keyset_double_rotation` + 1 negative: `hsm_rotate_offset_rejected` ([#968](https://github.com/Cosmian/kms/pull/968))

crate/server/src/core/wrapping/wrap.rs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,28 @@ pub(crate) async fn wrap_and_cache(
5959
// or in the HSM.
6060
// Either the user has provided a wrapping key ID or a key wrapping key is
6161
// provided in the parameters.
62-
let Some(wrapping_key_id) = object
62+
let uid_str = unique_identifier.to_string();
63+
let explicit_wrapping_key_id = object
6364
.attributes_mut()
6465
.ok()
65-
.and_then(|attrs| attrs.remove_wrapping_key_id(kms.vendor_id()))
66-
.or_else(|| kms.params.key_wrapping_key.clone())
67-
else {
68-
// no wrapping key provided
69-
return Ok(());
66+
.and_then(|attrs| attrs.remove_wrapping_key_id(kms.vendor_id()));
67+
let wrapping_key_id = if let Some(id) = explicit_wrapping_key_id {
68+
id
69+
} else {
70+
let Some(kek) = kms.params.key_wrapping_key.clone() else {
71+
// no wrapping key provided
72+
return Ok(());
73+
};
74+
// HSM-resident keys are hardware-protected: skip the server-wide KEK wrapping
75+
// to avoid creating a circular dependency where the KEK would wrap itself.
76+
if has_prefix(&uid_str).is_some() {
77+
return Ok(());
78+
}
79+
kek
7080
};
7181

7282
// A key cannot be its own wrapping key.
73-
if wrapping_key_id == unique_identifier.to_string() {
83+
if wrapping_key_id == uid_str {
7484
return Err(KmsError::InvalidRequest(format!(
7585
"Key '{wrapping_key_id}' cannot be used as its own wrapping key: \
7686
the wrapping key ID must differ from the key ID being created"

crate/test_kms_server/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ replays the steps sequentially.
228228
| HSM / KEK Create | `hsm/kek_ec_p256_create_sign` | CreateKeyPair (EC P-256, KEK-wrapped), Sign, Destroy ×2 | 3 |
229229
| HSM / KEK Create | `hsm/kek_ed25519_create_sign` | CreateKeyPair (Ed25519, KEK-wrapped), Sign, Destroy ×2 | 3 |
230230
| HSM / KEK ReKey | `hsm/kek_rekey_wrapped` | Create (AES-256, KEK-wrapped), Encrypt, ReKey (unwrap from KEK, new material, re-wrap), Encrypt new, GetAttributes | 9 |
231+
| HSM / KEK Bootstrap | `hsm/kek_bootstrap_self_create` | **Regression**: Create KEK itself (self-wrap guard bypass), Create DEK, Encrypt, Decrypt, Revoke, Destroy DEK | 6 |
231232
| HSM / Resident Keyset | `hsm/resident_keyset_set_rotate_name` | Create (AES-256, HSM), SetAttribute rotate_name (writes CKA_LABEL), Encrypt by keyset name, Decrypt by UID | 4 |
232233
| HSM / Resident Keyset | `hsm/resident_keyset_rekey_and_decrypt` | Create, SetAttribute rotate_name, Encrypt (gen-0), ReKey, Decrypt by keyset name (chain: gen-1→gen-0) | 7 |
233234
| HSM / Resident Keyset | `hsm/resident_keyset_double_rotation` | Create, SetAttribute rotate_name, Encrypt (gen-0), ReKey ×2, Decrypt by keyset name (chain: gen-2→gen-1→gen-0) | 9 |

crate/test_kms_server/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub use test_server::{
1313
start_default_test_kms_server_with_privileged_users,
1414
start_default_test_kms_server_with_softhsm2_and_kek,
1515
start_default_test_kms_server_with_softhsm2_and_kek_for_vectors,
16+
start_default_test_kms_server_with_softhsm2_kek_uncreated_for_vectors,
1617
start_default_test_kms_server_with_three_softhsm2,
1718
start_default_test_kms_server_with_utimaco_and_kek,
1819
start_default_test_kms_server_with_utimaco_hsm, start_test_kms_server_with_config,
@@ -24,6 +25,8 @@ mod test_server;
2425

2526
mod test_jwt;
2627

28+
pub mod test_env;
29+
2730
pub mod vector_runner;
2831

2932
use std::sync::Once;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//! Safe in-process environment-variable overrides for test vectors.
2+
//!
3+
//! `std::env::set_var` is `unsafe` in Rust 1.87+ and is forbidden by this
4+
//! crate's `deny(unsafe_code)` policy. This module provides a thread-safe
5+
//! alternative that the vector runner consults **before** falling back to the
6+
//! real process environment.
7+
//!
8+
//! Usage (server init):
9+
//! ```rust,ignore
10+
//! crate::test_env::set("HSM_BOOTSTRAP_KEK_ID", &kek_id);
11+
//! ```
12+
//!
13+
//! Usage (vector runner placeholder resolution):
14+
//! ```rust,ignore
15+
//! let value = crate::test_env::get("HSM_BOOTSTRAP_KEK_ID")
16+
//! .or_else(|| std::env::var("HSM_BOOTSTRAP_KEK_ID").ok());
17+
//! ```
18+
19+
use std::{
20+
collections::HashMap,
21+
sync::{OnceLock, PoisonError, RwLock},
22+
};
23+
24+
static OVERRIDES: OnceLock<RwLock<HashMap<String, String>>> = OnceLock::new();
25+
26+
fn map() -> &'static RwLock<HashMap<String, String>> {
27+
OVERRIDES.get_or_init(|| RwLock::new(HashMap::new()))
28+
}
29+
30+
/// Store an in-process env override under `key`.
31+
///
32+
/// # Panics
33+
/// Panics if the internal `RwLock` has been poisoned (should never happen in
34+
/// a normal test run).
35+
pub fn set(key: &str, value: &str) {
36+
map()
37+
.write()
38+
.unwrap_or_else(PoisonError::into_inner)
39+
.insert(key.to_owned(), value.to_owned());
40+
}
41+
42+
/// Look up an in-process env override. Returns `None` if not set.
43+
#[must_use]
44+
pub fn get(key: &str) -> Option<String> {
45+
map()
46+
.read()
47+
.unwrap_or_else(PoisonError::into_inner)
48+
.get(key)
49+
.cloned()
50+
}

crate/test_kms_server/src/test_server.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,52 @@ pub async fn start_default_test_kms_server_with_softhsm2_and_kek_for_vectors()
589589
start_server_from_config(config, &config_path).await
590590
}
591591

592+
/// Start a `SoftHSM2` + KEK test server where the KEK has **not** been pre-created.
593+
///
594+
/// This server type is used to reproduce the self-wrap regression (PR #968):
595+
/// `wrap_and_cache` must not attempt to wrap an HSM-resident key with the
596+
/// server-wide KEK, even when the key being created IS the KEK itself.
597+
///
598+
/// Concretely, `key_encryption_key` is set to `"hsm::{slot}::kek_bootstrap"`
599+
/// before the server starts. The first vector step creates that exact HSM key,
600+
/// which would have triggered the self-wrap error prior to the fix.
601+
///
602+
/// # Errors
603+
/// Returns an error if the server fails to start.
604+
///
605+
/// # Panics
606+
/// Panics if `HSM_SLOT_ID` is not set or is not a valid `usize`.
607+
pub async fn start_default_test_kms_server_with_softhsm2_kek_uncreated_for_vectors()
608+
-> Result<TestsContext, KmsClientError> {
609+
let slot = get_softhsm2_slot_id();
610+
let workspace_dir = std::env::temp_dir().join(format!(
611+
"kms_test_softhsm2_kek_bootstrap_{}_{}_{}",
612+
std::process::id(),
613+
std::time::SystemTime::now()
614+
.duration_since(std::time::UNIX_EPOCH)
615+
.unwrap_or_default()
616+
.as_nanos(),
617+
TEST_DIR_COUNTER.fetch_add(1, Ordering::Relaxed)
618+
));
619+
let kek_id = format!("hsm::{slot}::kek_bootstrap_{}", std::process::id());
620+
// Export for {{$HSM_BOOTSTRAP_KEK_ID}} substitution in vector steps.
621+
// Called once inside OnceCell initialisation before any vector steps run.
622+
crate::test_env::set("HSM_BOOTSTRAP_KEK_ID", &kek_id);
623+
624+
let config_path = hsm_config_path("hsm_softhsm2_kek.toml");
625+
let mut config = load_test_config_from_toml(&config_path)?;
626+
config.hsm.hsm_slot = vec![slot];
627+
config.db.sqlite_path = workspace_dir.join("sqlite-data");
628+
config.workspace.root_data_path = workspace_dir.join("workspace");
629+
config.workspace.tmp_path = workspace_dir.join("tmp");
630+
config.key_encryption_key = Some(kek_id);
631+
config.default_unwrap_type = Some(vec!["SecretData".to_owned(), "SymmetricKey".to_owned()]);
632+
// Disable Google CSE: starting with an empty workspace means no Google CSE
633+
// RSA keypair exists yet, and this test does not need that feature.
634+
config.google_cse_config.google_cse_enable = false;
635+
start_server_from_config(config, &config_path).await
636+
}
637+
592638
/// Start a test KMS server with three `SoftHSM2` instances:
593639
///
594640
/// - Slot 1 (`HSM_SLOT_ID_1`): legacy single-HSM config (`hsm:` top-level fields).

crate/test_kms_server/src/vector_runner.rs

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ static ONCE_VECTOR_CERT_AUTH: OnceCell<TestsContext> = OnceCell::const_new();
3030
static ONCE_VECTOR_AUTH_HTTPS: OnceCell<TestsContext> = OnceCell::const_new();
3131
/// Singleton server for vector tests requiring `SoftHSM2` + KEK.
3232
static ONCE_VECTOR_HSM_KEK: OnceCell<TestsContext> = OnceCell::const_new();
33+
/// Singleton server for vector tests where the HSM KEK is configured but **not yet created**.
34+
/// Used to verify that `wrap_and_cache` does not attempt to self-wrap when the first
35+
/// operation creates the KEK itself (regression for PR #968 self-wrap bug).
36+
static ONCE_VECTOR_HSM_KEK_UNCREATED: OnceCell<TestsContext> = OnceCell::const_new();
3337

3438
/// A test vector manifest loaded from a TOML file.
3539
///
@@ -438,12 +442,14 @@ fn load_request_json(
438442
))
439443
})?;
440444
let var_name = &rest[..end];
441-
let var_value = std::env::var(var_name).map_err(|_e| {
442-
KmsClientError::UnexpectedError(format!(
443-
"Environment variable '{var_name}' referenced in {} is not set",
444-
path.display()
445-
))
446-
})?;
445+
let var_value = crate::test_env::get(var_name)
446+
.or_else(|| std::env::var(var_name).ok())
447+
.ok_or_else(|| {
448+
KmsClientError::UnexpectedError(format!(
449+
"Environment variable '{var_name}' referenced in {} is not set",
450+
path.display()
451+
))
452+
})?;
447453
content = format!(
448454
"{}{var_value}{}",
449455
&content[..start],
@@ -479,13 +485,15 @@ fn resolve_assertion_value(
479485
let rest = &result[start + 3..];
480486
if let Some(end) = rest.find("}}") {
481487
let var_name = &rest[..end];
482-
let var_value = std::env::var(var_name).map_err(|_err| {
483-
KmsClientError::UnexpectedError(format!(
484-
"resolve_assertion_value: environment variable '{var_name}' \
485-
referenced in assertion template '{template}' is not set — \
486-
refusing to silently use an empty string"
487-
))
488-
})?;
488+
let var_value = crate::test_env::get(var_name)
489+
.or_else(|| std::env::var(var_name).ok())
490+
.ok_or_else(|| {
491+
KmsClientError::UnexpectedError(format!(
492+
"resolve_assertion_value: environment variable '{var_name}' \
493+
referenced in assertion template '{template}' is not set — \
494+
refusing to silently use an empty string"
495+
))
496+
})?;
489497
result = format!("{}{}{}", &result[..start], var_value, &rest[end + 2..]);
490498
} else {
491499
break;
@@ -789,7 +797,7 @@ pub async fn run_test_vector(vector_dir: &str) -> Result<(), KmsClientError> {
789797

790798
// Check required environment variables; skip gracefully if any is missing
791799
for env_var in &manifest.requires_env {
792-
if std::env::var(env_var).is_err() {
800+
if crate::test_env::get(env_var).is_none() && std::env::var(env_var).is_err() {
793801
eprintln!(
794802
"SKIP vector '{}': required env var '{env_var}' is not set",
795803
manifest.name
@@ -814,6 +822,19 @@ pub async fn run_test_vector(vector_dir: &str) -> Result<(), KmsClientError> {
814822
);
815823
return execute_steps(context, &manifest, &vector_path).await;
816824
}
825+
"hsm_kek_uncreated" => {
826+
let context = ONCE_VECTOR_HSM_KEK_UNCREATED
827+
.get_or_try_init(|| async {
828+
crate::start_default_test_kms_server_with_softhsm2_kek_uncreated_for_vectors()
829+
.await
830+
})
831+
.await?;
832+
eprintln!(
833+
"▶ Running vector '{}' on server_type 'hsm_kek_uncreated'",
834+
manifest.name
835+
);
836+
return execute_steps(context, &manifest, &vector_path).await;
837+
}
817838
other => {
818839
return Err(KmsClientError::UnexpectedError(format!(
819840
"Unknown server_type '{other}' in manifest for vector '{}'",
@@ -3786,6 +3807,15 @@ ObjectType = "SymmetricKey"
37863807
run_test_vector("test_data/vectors/hsm/kek_rekey_wrapped").await
37873808
}
37883809

3810+
/// Regression test for the HSM self-wrap bug (PR #968):
3811+
/// `wrap_and_cache` must not attempt to wrap an HSM-resident key with the
3812+
/// server-wide KEK when the key being created IS the configured KEK UID.
3813+
#[tokio::test]
3814+
async fn test_vec_hsm_kek_bootstrap_self_create() -> Result<(), KmsClientError> {
3815+
crate::init_test_logging();
3816+
run_test_vector("test_data/vectors/hsm/kek_bootstrap_self_create").await
3817+
}
3818+
37893819
// ── HSM Resident: Keyset (rotate_name / CKA_LABEL) ───────────────────
37903820

37913821
#[tokio::test]

0 commit comments

Comments
 (0)