From 15fa7ad3dd39ad626e6cd6c5a1933a847a50b611 Mon Sep 17 00:00:00 2001 From: Manuthor Date: Thu, 29 Jan 2026 09:29:59 +0100 Subject: [PATCH 1/2] fix(ui): Azure BYOK export --- Cargo.lock | 1 + crate/cli/Cargo.toml | 1 + crate/cli/src/actions/kms/azure/byok/mod.rs | 7 +- crate/cli/src/actions/kms/azure/mod.rs | 2 +- crate/cli/src/tests/kms/azure/mod.rs | 145 ++++++++++++++++++++ crate/cli/src/tests/kms/mod.rs | 1 + crate/wasm/src/wasm.rs | 26 ++++ ui/src/AzureExportByok.tsx | 4 +- 8 files changed, 180 insertions(+), 7 deletions(-) create mode 100644 crate/cli/src/tests/kms/azure/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 3b3016787d..8a22810e40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1168,6 +1168,7 @@ dependencies = [ "cosmian_config_utils", "cosmian_kmip", "cosmian_kms_client", + "cosmian_kms_client_utils", "cosmian_kms_crypto", "cosmian_logger", "der", diff --git a/crate/cli/Cargo.toml b/crate/cli/Cargo.toml index ec62e9dca9..307bb80fc4 100644 --- a/crate/cli/Cargo.toml +++ b/crate/cli/Cargo.toml @@ -68,6 +68,7 @@ sha2 = { workspace = true } [dev-dependencies] assert_cmd = "2.0" +cosmian_kms_client_utils = { path = "../client_utils", version = "5.15.0" } openssl = { workspace = true } serial_test = "3.0" tempfile = "3.19" diff --git a/crate/cli/src/actions/kms/azure/byok/mod.rs b/crate/cli/src/actions/kms/azure/byok/mod.rs index f2b3e2d1b1..f69eb55308 100644 --- a/crate/cli/src/actions/kms/azure/byok/mod.rs +++ b/crate/cli/src/actions/kms/azure/byok/mod.rs @@ -3,11 +3,10 @@ mod import_kek; use clap::Subcommand; use cosmian_kms_client::KmsClient; +pub(crate) use export_byok::ExportByokAction; +pub(crate) use import_kek::ImportKekAction; -use crate::{ - actions::kms::azure::byok::{export_byok::ExportByokAction, import_kek::ImportKekAction}, - error::result::KmsCliResult, -}; +use crate::error::result::KmsCliResult; /// Azure BYOK support. /// See: diff --git a/crate/cli/src/actions/kms/azure/mod.rs b/crate/cli/src/actions/kms/azure/mod.rs index 7540955454..34688602e2 100644 --- a/crate/cli/src/actions/kms/azure/mod.rs +++ b/crate/cli/src/actions/kms/azure/mod.rs @@ -1,4 +1,4 @@ -mod byok; +pub(crate) mod byok; use clap::Parser; use cosmian_kms_client::KmsClient; diff --git a/crate/cli/src/tests/kms/azure/mod.rs b/crate/cli/src/tests/kms/azure/mod.rs new file mode 100644 index 0000000000..cbf878fb8a --- /dev/null +++ b/crate/cli/src/tests/kms/azure/mod.rs @@ -0,0 +1,145 @@ +use std::fs; + +use cosmian_kmip::kmip_2_1::{kmip_objects::ObjectType, kmip_types::KeyFormatType}; +use tempfile::TempDir; +use test_kms_server::start_default_test_kms_server; + +use crate::{ + actions::kms::{ + azure::byok::{ExportByokAction, ImportKekAction}, + rsa::keys::create_key_pair::CreateKeyPairAction, + symmetric::keys::create_key::CreateKeyAction, + }, + error::{KmsCliError, result::KmsCliResult}, +}; + +#[tokio::test] +async fn test_azure_byok_import_kek_then_export_byok() -> KmsCliResult<()> { + // 1. Instantiate a default KMS server + let ctx = start_default_test_kms_server().await; + let kms_client = ctx.get_owner_client(); + + // 2. Generate an RSA key pair, export the public key as PEM, then import it as Azure KEK + let (_private_key_id, public_key_id) = CreateKeyPairAction { + key_size: 2048, + tags: vec!["test".to_owned()], + ..CreateKeyPairAction::default() + } + .run(kms_client.clone()) + .await?; + + let tmp_dir = TempDir::new()?; + let kek_pem_path = tmp_dir.path().join("kek_pub.pem"); + + // Export the public key material as PKCS#8 DER and convert to PEM. + let (_id, kek_pub_object, _attributes) = cosmian_kms_client::export_object( + &kms_client, + &public_key_id.to_string(), + cosmian_kms_client::ExportObjectParams { + unwrap: true, + wrapping_key_id: None, + allow_revoked: false, + key_format_type: Some(cosmian_kmip::kmip_2_1::kmip_types::KeyFormatType::PKCS8), + encode_to_ttlv: false, + wrapping_cryptographic_parameters: None, + authenticated_encryption_additional_data: None, + }, + ) + .await + .map_err(|e| KmsCliError::Default(format!("Failed to export RSA public key: {e}")))?; + + let key_block = kek_pub_object + .key_block() + .map_err(|e| KmsCliError::Default(format!("Invalid exported key block: {e}")))?; + let der: Vec = match key_block.key_value.as_ref() { + Some(cosmian_kmip::kmip_2_1::kmip_data_structures::KeyValue::ByteString(v)) => v.to_vec(), + Some(cosmian_kmip::kmip_2_1::kmip_data_structures::KeyValue::Structure { + key_material, + .. + }) => match key_material { + cosmian_kmip::kmip_2_1::kmip_data_structures::KeyMaterial::ByteString(v) => v.to_vec(), + x => { + return Err(KmsCliError::Default(format!( + "Unsupported exported public key material: {x:?}" + ))); + } + }, + None => { + return Err(KmsCliError::Default( + "Exported public key has no key value".into(), + )); + } + }; + + let pem = cosmian_kms_client_utils::export_utils::der_to_pem( + der.as_slice(), + KeyFormatType::PKCS8, + ObjectType::PublicKey, + ) + .map_err(|e| KmsCliError::Default(format!("DER to PEM conversion failed: {e}")))?; + + fs::write(&kek_pem_path, pem.as_slice())?; + + let kid = "https://unit.test/keys/KEK/00000000000000000000000000000000".to_owned(); + ImportKekAction { + kek_file: kek_pem_path, + kid: kid.clone(), + key_id: None, + } + .run(kms_client.clone()) + .await?; + + // The import action writes to stdout and does not return the imported id; locate it via tag. + // Tag is `kid:`. + let locate_request = cosmian_kms_client_utils::locate_utils::build_locate_request( + Some(vec![format!("kid:{kid}")]), + None, + None, + None, + None, + None, + None, + None, + ) + .map_err(|e| KmsCliError::Default(format!("Failed to build Locate request: {e}")))?; + + let locate_response = kms_client + .locate(locate_request) + .await + .map_err(|e| KmsCliError::Default(format!("Failed to locate imported KEK: {e}")))?; + + let imported_kek_id = locate_response + .unique_identifier + .unwrap_or_default() + .into_iter() + .next() + .ok_or_else(|| KmsCliError::Default("Failed to locate imported Azure KEK".to_owned()))? + .to_string(); + + // 3. Generate a symmetric key and run ExportByokAction using it as wrapped_key_id + let sym_key_id = CreateKeyAction { + number_of_bits: Some(256), + tags: vec!["test".to_owned()], + ..CreateKeyAction::default() + } + .run(kms_client.clone()) + .await? + .to_string(); + + let byok_file = tmp_dir.path().join("out.byok"); + + ExportByokAction { + wrapped_key_id: sym_key_id, + kek_id: imported_kek_id, + byok_file: Some(byok_file.clone()), + } + .run(kms_client) + .await?; + + // Assert byok file written + let contents = std::fs::read_to_string(&byok_file)?; + assert!(contents.contains("\"ciphertext\"")); + assert!(contents.contains("\"kid\"")); + + Ok(()) +} diff --git a/crate/cli/src/tests/kms/mod.rs b/crate/cli/src/tests/kms/mod.rs index 0a4f8a71b5..2f02a3755d 100644 --- a/crate/cli/src/tests/kms/mod.rs +++ b/crate/cli/src/tests/kms/mod.rs @@ -1,6 +1,7 @@ mod access; mod attributes; mod auth_tests; +mod azure; mod certificates; #[cfg(feature = "non-fips")] mod cover_crypt; diff --git a/crate/wasm/src/wasm.rs b/crate/wasm/src/wasm.rs index 4faccca834..dfff748302 100644 --- a/crate/wasm/src/wasm.rs +++ b/crate/wasm/src/wasm.rs @@ -1670,6 +1670,32 @@ pub fn get_attributes_ttlv_request(unique_identifier: String) -> Result Result { + let unique_identifier = UniqueIdentifier::TextString(unique_identifier); + + let attribute_reference = if force_tags { + Some(vec![AttributeReference::Standard(Tag::Tag)]) + } else { + None + }; + + let request = GetAttributes { + unique_identifier: Some(unique_identifier), + attribute_reference, + }; + + let objects = to_ttlv(&request).map_err(|e| JsValue::from(e.to_string()))?; + serde_wasm_bindgen::to_value(&objects).map_err(|e| JsValue::from(e.to_string())) +} + #[allow(clippy::needless_pass_by_value)] #[wasm_bindgen] pub fn parse_get_attributes_ttlv_response( diff --git a/ui/src/AzureExportByok.tsx b/ui/src/AzureExportByok.tsx index 6082f06d8e..31b710e594 100644 --- a/ui/src/AzureExportByok.tsx +++ b/ui/src/AzureExportByok.tsx @@ -4,7 +4,7 @@ import {useAuth} from "./AuthContext"; import {downloadFile, sendKmipRequest} from "./utils"; import { export_ttlv_request, - get_attributes_ttlv_request, + get_attributes_ttlv_request_with_options, parse_export_ttlv_response, parse_get_attributes_ttlv_response } from "./wasm/pkg"; @@ -59,7 +59,7 @@ const ExportAzureBYOKForm: React.FC = () => { setRes(undefined); try { // Step 1: Get the KEK attributes to retrieve the Azure kid - const getAttrsRequest = get_attributes_ttlv_request(values.kekId); + const getAttrsRequest = get_attributes_ttlv_request_with_options(values.kekId, true); const attrsResultStr = await sendKmipRequest(getAttrsRequest, idToken, serverUrl); if (!attrsResultStr) { From 75a0449a29c26b592245b5cf46ecdd5254e7a3a4 Mon Sep 17 00:00:00 2001 From: Manuthor Date: Thu, 29 Jan 2026 13:12:41 +0100 Subject: [PATCH 2/2] fix: generate_rsa_keypair in Azure tests --- Cargo.lock | 1 - crate/cli/Cargo.toml | 1 - .../src/actions/kms/azure/byok/import_kek.rs | 5 +- crate/cli/src/actions/kms/azure/byok/mod.rs | 5 +- crate/cli/src/tests/kms/azure/mod.rs | 133 +++++++----------- 5 files changed, 55 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8a22810e40..3b3016787d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1168,7 +1168,6 @@ dependencies = [ "cosmian_config_utils", "cosmian_kmip", "cosmian_kms_client", - "cosmian_kms_client_utils", "cosmian_kms_crypto", "cosmian_logger", "der", diff --git a/crate/cli/Cargo.toml b/crate/cli/Cargo.toml index 307bb80fc4..ec62e9dca9 100644 --- a/crate/cli/Cargo.toml +++ b/crate/cli/Cargo.toml @@ -68,7 +68,6 @@ sha2 = { workspace = true } [dev-dependencies] assert_cmd = "2.0" -cosmian_kms_client_utils = { path = "../client_utils", version = "5.15.0" } openssl = { workspace = true } serial_test = "3.0" tempfile = "3.19" diff --git a/crate/cli/src/actions/kms/azure/byok/import_kek.rs b/crate/cli/src/actions/kms/azure/byok/import_kek.rs index 80fd870c19..a6f2c6b0ea 100644 --- a/crate/cli/src/actions/kms/azure/byok/import_kek.rs +++ b/crate/cli/src/actions/kms/azure/byok/import_kek.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use clap::Parser; +use cosmian_kmip::kmip_2_1::kmip_types::UniqueIdentifier; use cosmian_kms_client::{ KmsClient, reexport::cosmian_kms_client_utils::import_utils::{ImportKeyFormat, KeyUsage}, @@ -30,7 +31,7 @@ pub struct ImportKekAction { } impl ImportKekAction { - pub async fn run(&self, kms_client: KmsClient) -> KmsCliResult<()> { + pub async fn run(&self, kms_client: KmsClient) -> KmsCliResult { let import_action = ImportSecretDataOrKeyAction { key_file: self.kek_file.clone(), key_id: self.key_id.clone(), @@ -45,6 +46,6 @@ impl ImportKekAction { wrapping_key_id: None, }; - import_action.run(kms_client).await.map(|_| ()) + import_action.run(kms_client).await } } diff --git a/crate/cli/src/actions/kms/azure/byok/mod.rs b/crate/cli/src/actions/kms/azure/byok/mod.rs index f69eb55308..d1fe0c3348 100644 --- a/crate/cli/src/actions/kms/azure/byok/mod.rs +++ b/crate/cli/src/actions/kms/azure/byok/mod.rs @@ -19,7 +19,10 @@ pub enum ByokCommands { impl ByokCommands { pub async fn process(&self, kms_rest_client: KmsClient) -> KmsCliResult<()> { match self { - Self::Import(action) => action.run(kms_rest_client).await, + Self::Import(action) => { + action.run(kms_rest_client).await?; + Ok(()) + } Self::Export(action) => action.run(kms_rest_client).await, } } diff --git a/crate/cli/src/tests/kms/azure/mod.rs b/crate/cli/src/tests/kms/azure/mod.rs index cbf878fb8a..bd1a5999bf 100644 --- a/crate/cli/src/tests/kms/azure/mod.rs +++ b/crate/cli/src/tests/kms/azure/mod.rs @@ -1,87 +1,74 @@ use std::fs; -use cosmian_kmip::kmip_2_1::{kmip_objects::ObjectType, kmip_types::KeyFormatType}; +use openssl::{ + pkey::{PKey, Private, Public}, + rsa::Rsa, +}; use tempfile::TempDir; use test_kms_server::start_default_test_kms_server; use crate::{ actions::kms::{ azure::byok::{ExportByokAction, ImportKekAction}, - rsa::keys::create_key_pair::CreateKeyPairAction, symmetric::keys::create_key::CreateKeyAction, }, error::{KmsCliError, result::KmsCliResult}, }; +/// Generate RSA keypair using OpenSSL (random size from 2048, 3072, or 4096 bits). +/// +/// This mirrors AWS KMS "get-parameters-for-import" wrapping key specs and keeps +/// the test independent from KMS RSA key generation/export actions. +fn generate_rsa_keypair() -> KmsCliResult<(PKey, PKey)> { + let key_sizes = [2048_u32, 3072_u32, 4096_u32]; + // Avoid introducing new RNG deps in the CLI crate's dev-deps. + let bits = key_sizes[std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| { + let len_u32 = u32::try_from(key_sizes.len()).unwrap_or(1); + let idx_u32 = d.subsec_nanos() % len_u32; + usize::try_from(idx_u32).unwrap_or(0) + }) + .unwrap_or(0)]; + + let rsa = Rsa::generate(bits) + .map_err(|e| KmsCliError::Default(format!("Failed to generate RSA key: {e}")))?; + let private_key = PKey::from_rsa(rsa.clone()) + .map_err(|e| KmsCliError::Default(format!("Failed to build private key: {e}")))?; + let public_key = PKey::from_rsa( + Rsa::from_public_components( + rsa.n() + .to_owned() + .map_err(|e| KmsCliError::Default(format!("Failed to clone modulus: {e}")))?, + rsa.e() + .to_owned() + .map_err(|e| KmsCliError::Default(format!("Failed to clone exponent: {e}")))?, + ) + .map_err(|e| KmsCliError::Default(format!("Failed to build public RSA key: {e}")))?, + ) + .map_err(|e| KmsCliError::Default(format!("Failed to build public key: {e}")))?; + + Ok((private_key, public_key)) +} + #[tokio::test] async fn test_azure_byok_import_kek_then_export_byok() -> KmsCliResult<()> { // 1. Instantiate a default KMS server let ctx = start_default_test_kms_server().await; let kms_client = ctx.get_owner_client(); - // 2. Generate an RSA key pair, export the public key as PEM, then import it as Azure KEK - let (_private_key_id, public_key_id) = CreateKeyPairAction { - key_size: 2048, - tags: vec!["test".to_owned()], - ..CreateKeyPairAction::default() - } - .run(kms_client.clone()) - .await?; - let tmp_dir = TempDir::new()?; let kek_pem_path = tmp_dir.path().join("kek_pub.pem"); - // Export the public key material as PKCS#8 DER and convert to PEM. - let (_id, kek_pub_object, _attributes) = cosmian_kms_client::export_object( - &kms_client, - &public_key_id.to_string(), - cosmian_kms_client::ExportObjectParams { - unwrap: true, - wrapping_key_id: None, - allow_revoked: false, - key_format_type: Some(cosmian_kmip::kmip_2_1::kmip_types::KeyFormatType::PKCS8), - encode_to_ttlv: false, - wrapping_cryptographic_parameters: None, - authenticated_encryption_additional_data: None, - }, - ) - .await - .map_err(|e| KmsCliError::Default(format!("Failed to export RSA public key: {e}")))?; - - let key_block = kek_pub_object - .key_block() - .map_err(|e| KmsCliError::Default(format!("Invalid exported key block: {e}")))?; - let der: Vec = match key_block.key_value.as_ref() { - Some(cosmian_kmip::kmip_2_1::kmip_data_structures::KeyValue::ByteString(v)) => v.to_vec(), - Some(cosmian_kmip::kmip_2_1::kmip_data_structures::KeyValue::Structure { - key_material, - .. - }) => match key_material { - cosmian_kmip::kmip_2_1::kmip_data_structures::KeyMaterial::ByteString(v) => v.to_vec(), - x => { - return Err(KmsCliError::Default(format!( - "Unsupported exported public key material: {x:?}" - ))); - } - }, - None => { - return Err(KmsCliError::Default( - "Exported public key has no key value".into(), - )); - } - }; - - let pem = cosmian_kms_client_utils::export_utils::der_to_pem( - der.as_slice(), - KeyFormatType::PKCS8, - ObjectType::PublicKey, - ) - .map_err(|e| KmsCliError::Default(format!("DER to PEM conversion failed: {e}")))?; - - fs::write(&kek_pem_path, pem.as_slice())?; + // 2. Generate an RSA key pair locally, write the public key in PKCS#8 PEM, then import it as Azure KEK + let (_private_key, public_key) = generate_rsa_keypair()?; + let public_key_pem = public_key + .public_key_to_pem() + .map_err(|e| KmsCliError::Default(format!("Failed to serialize public key PEM: {e}")))?; + fs::write(&kek_pem_path, &public_key_pem)?; let kid = "https://unit.test/keys/KEK/00000000000000000000000000000000".to_owned(); - ImportKekAction { + let imported_kek_id = ImportKekAction { kek_file: kek_pem_path, kid: kid.clone(), key_id: None, @@ -91,30 +78,6 @@ async fn test_azure_byok_import_kek_then_export_byok() -> KmsCliResult<()> { // The import action writes to stdout and does not return the imported id; locate it via tag. // Tag is `kid:`. - let locate_request = cosmian_kms_client_utils::locate_utils::build_locate_request( - Some(vec![format!("kid:{kid}")]), - None, - None, - None, - None, - None, - None, - None, - ) - .map_err(|e| KmsCliError::Default(format!("Failed to build Locate request: {e}")))?; - - let locate_response = kms_client - .locate(locate_request) - .await - .map_err(|e| KmsCliError::Default(format!("Failed to locate imported KEK: {e}")))?; - - let imported_kek_id = locate_response - .unique_identifier - .unwrap_or_default() - .into_iter() - .next() - .ok_or_else(|| KmsCliError::Default("Failed to locate imported Azure KEK".to_owned()))? - .to_string(); // 3. Generate a symmetric key and run ExportByokAction using it as wrapped_key_id let sym_key_id = CreateKeyAction { @@ -130,7 +93,7 @@ async fn test_azure_byok_import_kek_then_export_byok() -> KmsCliResult<()> { ExportByokAction { wrapped_key_id: sym_key_id, - kek_id: imported_kek_id, + kek_id: imported_kek_id.to_string(), byok_file: Some(byok_file.clone()), } .run(kms_client)