diff --git a/.vscode/settings.json b/.vscode/settings.json index 5bd0649e2..cf37d3c3b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "cSpell.words": [ "actix", "ascii", + "Byok", "canonicalize", "chacha", "ciphertext", diff --git a/crate/cli/src/actions/kms/actions.rs b/crate/cli/src/actions/kms/actions.rs index 6be6e5099..29b0cf975 100644 --- a/crate/cli/src/actions/kms/actions.rs +++ b/crate/cli/src/actions/kms/actions.rs @@ -11,9 +11,9 @@ use super::configurable_kem::ConfigurableKemCommands; use super::cover_crypt::CovercryptCommands; use crate::{ actions::kms::{ - access::AccessAction, attributes::AttributesCommands, azure::AzureCommands, - bench::BenchAction, certificates::CertificatesCommands, console::Stdout, - derive_key::DeriveKeyAction, elliptic_curves::EllipticCurveCommands, + access::AccessAction, attributes::AttributesCommands, aws::AwsCommands, + azure::AzureCommands, bench::BenchAction, certificates::CertificatesCommands, + console::Stdout, derive_key::DeriveKeyAction, elliptic_curves::EllipticCurveCommands, google::GoogleCommands, hash::HashAction, login::LoginAction, mac::MacCommands, opaque_object::OpaqueObjectCommands, rng::RngAction, rsa::RsaCommands, secret_data::SecretDataCommands, shared::LocateObjectsAction, symmetric::SymmetricCommands, @@ -30,6 +30,8 @@ pub enum KmsActions { Attributes(AttributesCommands), #[command(subcommand)] Azure(AzureCommands), + #[command(subcommand)] + Aws(AwsCommands), #[clap(hide = true)] Bench(BenchAction), #[cfg(feature = "non-fips")] @@ -81,6 +83,7 @@ impl KmsActions { match self { Self::AccessRights(action) => Box::pin(action.process(kms_rest_client)).await?, Self::Attributes(action) => Box::pin(action.process(kms_rest_client)).await?, + Self::Aws(action) => Box::pin(action.process(kms_rest_client)).await?, Self::Azure(action) => Box::pin(action.process(kms_rest_client)).await?, Self::Bench(action) => Box::pin(action.process(kms_rest_client)).await?, #[cfg(feature = "non-fips")] diff --git a/crate/cli/src/actions/kms/aws/README.ms b/crate/cli/src/actions/kms/aws/README.ms new file mode 100644 index 000000000..31a72c105 --- /dev/null +++ b/crate/cli/src/actions/kms/aws/README.ms @@ -0,0 +1,32 @@ +## creating keys on amazon kms + +https://docs.aws.amazon.com/kms/latest/developerguide/importing-keys-conceptual.html + +"importing keys link" + +https://docs.aws.amazon.com/kms/latest/developerguide/importing-keys.html + + +requirements for key material : +https://docs.aws.amazon.com/kms/latest/developerguide/importing-keys-conceptual.html#importing-keys-material-requirements + + + // TODO: export a file path, if specified + // test key : e8518bca-e1d0-4519-a915-d80da8e8f38a + + // aws kms get-parameters-for-import \ + // --key-id e8518bca-e1d0-4519-a915-d80da8e8f38a \ + // --wrapping-algorithm RSA_AES_KEY_WRAP_SHA_256 \ + // --wrapping-key-spec RSA_3072 + + // results : + // { + // "KeyId": "arn:aws:kms:eu-west-3:447182645454:key/e8518bca-e1d0-4519-a915-d80da8e8f38a", + // "ImportToken": "AQECAHjI9wyV8duc1PbnNnvRgoPixtls559v7PxIfCjrbMLOIwAACp0wggqZBgkqhkiG9w0BBwagggqKMIIKhgIBADCCCn8GCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQM2pSQz0twuQDrlkbDAgEQgIIKUJqgIjd/9DLvIMVgDWbHiAJSkLkE0b7DzodC87ItmcpzxYB8+Mh/Z9ZbtSM55CmqODnPXRxqZ53HotBl4FtbdbO/9/OQ/1mFiAIiS7JqVu1HQilQ+FgfKsl/O2QGF/i3Ql+bcXqLJ2A+j66nO1/iQYuwlljAVoA8ymlVczqMYWpZc0pKMOr2KFA+X9zM6jXfjq3aDRO895a2QO0WDTTPiK2SQPh5lzjimnE93SUfu4yBRgpnEFmGgJSJsMXY1FFjZVXpy4Zxyu4Fa7qiWCXpFgpfj/VE1rjZ+oWHA5YyxE8GdxhCrzVU4NW5W3E34aRF4X+77iegFl45wB8ukBkornW6wN2GoD7t+AmO5rr6TdGcJebyoSSgh3zszn9+LogHcY1y5Mxqac1zR9NvwEGCMcSbOYUqpUz3MbmngYNODxZNUZ5k4fM1NtUTDWm2VI80L9R0G6BOEiUZBFWQWS7fQyuqbXltR+J4eybb5RRytv8yHoCiT0uu0F/jhzRJN2du6fzdr4J98bh4z+WfHONK1s7sr2ExGVXVZ+t3BgGIhmEDGTHOEF6suugIHthxXrL8/NEjoMfYsS5XSD6OtRbpqe5zI8L+5iT1vIFFrOCAIRLkL+AQ3drzR9WQNTHwJrO6MH/25WzjXeCuIXLRuONG/+tBmg9Q6E0jUGWWEnHtROu11X3Rp4ZwFvXTOEQSYX4esPr6J1nAD9FMa/VXR0zlOAMf+DrAuiX8b9mWRqWAAAEwM2lfOU9vMiNHtJfQx68Srx5Kw+qieC5gtbKDJnKRQqsmf6+4KsI8PpCiMLhdyeN6atk02nC/LCjOpA5I3tE7WMHKoMWjSArMXblunYLKaN+ntjTKtW4xB67cALZt1VLqJ5108RQHVI04blWfYipnvAWPtO1ZjCHNvirIaNJrnwMDeJKb64XAECnJpk29yJ9yWCUvmZeefGrNabEa7TiPdSn6fsMjgtwPk91+yrSQSDL2J9eR8TPqMXVeEQojt6rVfgj6VU7fazV2mqkgEiPSkl7YkA6bQZ098rDYT5rF4G5oLu2I4+vebrMA/ZkaHl266sznt0w2UwgcV4ktEx9AYD1icpNb+E7xDA49epMAbMWh6jBRmOljcfi5IgF6Xs7a00ZqJKK2gzxYqmRcgNPI6eWXGJjGbIMUybxbgXYoJPS/EhnIuZlZlhi/aVZWueDqiXYHghSRPTka3ZjNl+LUcQJM5k7yrW/rRS8EDnLE4LFdpmeDxUA2t5GQggOpxbEklAf0gfPhpcmDDKv3FPzR1pAiMO1oMjsvDJzHlnSqSD6Uy+G0OgEINTYvsC/zVqDFPRejNs5o0MYG9NKWCPsJTDCXl2GjdgSROkkb0onZjqDjhlJBV16jkwGfCxe1ONLCEhYFJoTuL8suFEQdLQHvs3me8j8cTCyzTIB0kmYO5PnpnNEibM/bvrrGY4xQqUWws7kg4x8lJTCcHI38NIP9cniFr0vlwY0D9DFphoY7jkOROPyAVBgpVecb7KOIEeij88y8VljtJbfj4qF1TkgOoGN/hFpZKsVHPRUZ2+94zPsr4nGE4lFhFlgK95wyms68U8Uzh4ZlDUvd8GoUeDTkwc9RK+tK7+It++LI1hc8hcKkjdQWfLNkp723qfdJ6CCvX3vulVoSBnAAjA/F4UyRv2MsLGOb2znNVXsrchYTBQ8PRoErS9/9y5bw/+vbICEVPvHbFTYlhQkKCRtnL+pHp021fWllHCf2day/yb/bCrmn4row2HTP9RwC3KhW/fkf6hgJViKQNWtK9L3NtzWuDV8iqz5CQsh/OH8LJqQtDaQ2to9MrRBlgnYZRkaSF0+u9fFg/mspWAv3y4lClP9Rnj95HVIYDVFn8EEWIxiZpXpSSxrtPw6MOlrI6OCZtlS50EGuSkZ0xjXo5oX6rQgUz16f5GX0Nt+VSm4HcfkoGb1uC+jqVmD8mCGIwZ/H2Dn6t8tCL3IAlhxfWLpKws67zZpQON0cO0zfjldYHChiKrHjzUm/EWkRfc7z363AHjpO3975e8aeu/Onif277oT9CN3CBuSiL7Tge2Whd17IPdhLJcGCjjZtUx4auBOL4Eq2JaDWNjm2zgadqOOYbKtmBMqlDKK1O4B1K3wRGbjU7PEuD8OqZPCLwwuDV0Od5ynQjv/NMuBNhi/ydiKfyOx4bOBao0uVhkcVgAbnuBdLJ5OJ5QOFFxiaSCoSAsv1XhC3MVAg0Qxo+DNyK6OxF5vWOd6D6qzZdZH7S0eh0wHvNp7FwuTReIpqQPC8PdzTxH1JdkQx6uWF7OMGvB1Y1UxMxrAL9BZtTKh9ReItqs5yVnd4s4BNoLR2Rc+9qEiR8Dz58agjBQ/Z8XDxfJ7o+JZtdZ2qQVenWNY1NWzl1qIuSUYzD6Ozeuoae0xPBeawBNWVoUOOCPnHsExgB/tOsxSIeVZBhNqfi9ES+2Nj0T09IxUo2Su43gb7vTZ46Ig+NSGp7EKWZp8BIieQIt7Q4VKL3yMmCdsFBD36bVP+3Ci7H26lC/4QGnzSUYSsxF8/X8UWspHjEKMfv/QPFuHMC5n+qpgI8L1I6f3bjoJFsjL4y0fyJkY3y6UyVRCU6jPN0YUzRJ3mOUPpYLc9OQWQTdEOw7hVWXKfqU4w7iZ+99dqKhu3oUlb4693Bb+JcWprVSWkyHfQ5MwKx7gYjH4N8Zz23lMy8TvAAiXxfFHa8zLrhufNaPGGXIIS4Y7ajQUsq4zv/nveDSLHiBgOWzDLWjj/PBfLTBgjUD49SfF0eHYwQrgiVbW7TE2NtVL9ohZcFpsUcGsgqc5pTeoa/bUPNK+dSt1ka2a/puQ+HmBDjq+bbFSdwv+9YA83ayGX/acMCrgMFZM/x9a2Q/yWBfX1dTh/1iuibUIh5lbGEyIPc0lR0j/b/pEtFE+pbPRTD+p/1aLL/5vCzxz1Vq7JUknz57Ejlh3tNVdxzYiCIXWRLT+dqQv3On6JowMlvzY+A6jks2NAqXeJzwMlwz92/otBoN+o8UkuG5SBu4vN9Z+Qhs9mle5UQlG1meLZhDVo+ivAav86im037t/LvGxj5yb7BGhsN76kk8H/K2ERWe3hLG2O6QMOxFX2/pJGT7Moqv/GLFxA2cEkyAV5U/SvBcHMh01dXg9J1K2ijwJrNQp/b/5AAnxYXj/H3kEx/jcug4bfv9Eu7sVhVndCRXmC1DZKr31OqbMVcTFeFicpisUva1KL/xn8ducKW/3Y9/Ug4DUceGxTn3YTlGRPtXw9//0ICw4/CIHL3otj7PzY7BoQ+jD3a0eZti499JJLFHrFHsq4CaLcA35YETXIWZlN/JV2TcVKQXx8DmkehROKz34FI4d7KmX/+ZN7dZek2c0DzKSjXynCgLcgz5WHFD5tymLxUULi6FvLOt4TV8pbGGEfMqzMG0Et0uIMcxSZlmk73o7uaURmIa9kcCQLOh/L++qgU1uw7J12P521Q6XZuOvBV3vB8Vit6JCcINF4mV7Xh5K97pGvtakQ75B9y20QCmQcNpyXLQ==", + // "PublicKey": "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA020eGsIGUYdaNMP2Ty6b/YmpL+8eMopdaXg5I+x3TMzTT7QV7d3mfe5E/wgNRGDjYRdA74S8WwdwcSpfFpppVnDKX1dKIXS5woYa7hx/YAdY/3kv0VJKGKxt8Tf1Ajpekot0uVLbHOwR9DF8aBiDCtrZR7x94H1GPSXZ3VLZimBldcJ+Tszt4wITmwBjJxkPXJzkVa8zg2tnTKm/M5TwMDe5Q3DUOYKVgMKdcsJPX43q72radD1VdMazxzps+7wDT64AD5FZoXHcJH4ZytIMySbhlwsQntT5lNeyulg7kbFAFuSzGM7SipDwvVPzgxrDY1aw/VpJ9vuMqtO/V7TWwL8NSuoh/5bsVXCUFdSXHmz9oh/hj8zIPDLjHc/Z1guGanl+4/ZQbs4M4SgqN1KT/aayX+oDsvj6Q+GVzhCIpq7XQJekcOcRjCyYocCLfYOTv2N/vFMGxcKZZLM4LmCGMFsDEO1GaISPsv6mVUgewWMeBmzTBbyB6atA07Li9JMjAgMBAAE=", + // "ParametersValidTo": "2026-01-15T16:58:27.315000+01:00" + // } + + // aws kms import-key-material --key-id e8518bca-e1d0-4519-a915-d80da8e8f38a \ + // --encrypted-key-material fileb://EncryptedKeyMaterial.bin \ + // --import-token fileb://ImportToken.bin diff --git a/crate/cli/src/actions/kms/aws/byok/export_key_material.rs b/crate/cli/src/actions/kms/aws/byok/export_key_material.rs new file mode 100644 index 000000000..982f97899 --- /dev/null +++ b/crate/cli/src/actions/kms/aws/byok/export_key_material.rs @@ -0,0 +1,183 @@ +use std::{fs, path::PathBuf}; + +use base64::Engine; +use clap::Parser; +use cosmian_kmip::{ + kmip_0::kmip_types::{HashingAlgorithm, PaddingMethod}, + kmip_2_1::{ + kmip_data_structures::KeyValue, + kmip_types::{CryptographicAlgorithm, CryptographicParameters, Tag}, + }, +}; +use cosmian_kms_client::{ExportObjectParams, KmsClient, export_object}; +use cosmian_logger::warn; + +use crate::{ + actions::kms::{ + attributes::get_attributes, aws::byok::wrapping_algorithms::AwsKmsWrappingAlgorithm, + console, + }, + cli_bail, + error::{ + KmsCliError, + result::{KmsCliResult, KmsCliResultHelper}, + }, +}; + +/// Wrap a KMS key with an AWS Key Encryption Key (KEK). +#[derive(Parser)] +#[clap(verbatim_doc_comment)] +pub struct ExportByokAction { + /// The unique ID of the KMS private key that will be wrapped and then exported + #[clap(required = true)] + pub(crate) key_id: String, + + /// The AWS KEK ID in this KMS. + #[clap(required = true)] + pub(crate) kek_id: String, + + /// The file path containing the import token previously generated when importing the KEK. + /// This file isn't red and neither used by the KMS, it's simply for providing copy-paste ready output for + /// aws cli users upon a successful key material wrapping + #[clap(required = false)] + pub(crate) token_file_path: Option, + + /// If not specified, a base64 encoded blob containing the key material will be printed to stdout. Can be piped to desired file or command. + #[clap(required = false)] + pub(crate) output_file_path: Option, +} + +impl ExportByokAction { + #[allow(clippy::print_stdout, clippy::or_fun_call)] // the kms console wrapper forces a println but this function does not want a line return for proper display + pub async fn run(&self, kms_client: KmsClient) -> KmsCliResult { + // Recover the attributes of the KEK key + let (_kek_id, kek_attributes) = + get_attributes(&kms_client, &self.kek_id, &[Tag::Tag], &[]).await?; + let kek_tag_error = |msg: &str| -> String { + format!( + "The KEK is not an AWS Key Encryption Key: {msg}. Import it using the \ + `cosmian kms aws byok import` command." + ) + }; + + let tags: Vec = serde_json::from_value( + kek_attributes + .get("Tag") + .context(&kek_tag_error("no tags"))? + .clone(), + )?; + + if !tags.contains(&"aws".to_owned()) { + return Err(KmsCliError::InconsistentOperation(kek_tag_error( + "missing `aws` tag", + ))); + } + + let key_arn = tags.iter().find_map(|t| t.strip_prefix("key_arn:")); + + let wrapping_algorithm_str = tags + .iter() + .find(|t| t.starts_with("wrapping_algorithm:")) + .context(&kek_tag_error("wrapping algorithm not found"))? + .strip_prefix("wrapping_algorithm:") + .ok_or(KmsCliError::Default(kek_tag_error( + "invalid wrapping algorithm tag", + )))? + .parse::() + .context(&kek_tag_error("invalid wrapping algorithm tag"))?; + + let wrapping_cryptographic_parameters = Some(match wrapping_algorithm_str { + AwsKmsWrappingAlgorithm::RsaesOaepSha1 => CryptographicParameters { + cryptographic_algorithm: Some(CryptographicAlgorithm::RSA), + padding_method: Some(PaddingMethod::OAEP), + hashing_algorithm: Some(HashingAlgorithm::SHA1), + ..CryptographicParameters::default() + }, + AwsKmsWrappingAlgorithm::RsaesOaepSha256 => CryptographicParameters { + cryptographic_algorithm: Some(CryptographicAlgorithm::RSA), + padding_method: Some(PaddingMethod::OAEP), + hashing_algorithm: Some(HashingAlgorithm::SHA256), + ..CryptographicParameters::default() + }, + AwsKmsWrappingAlgorithm::RsaAesKeyWrapSha1 => CryptographicParameters { + cryptographic_algorithm: Some(CryptographicAlgorithm::RSA), + // Note: We use "None" padding to route toward RSA AES Key Wrap, this is not a mistake + // see: crate/crypto/src/crypto/wrap/unwrap_key.rs line 365 + padding_method: Some(PaddingMethod::None), + hashing_algorithm: Some(HashingAlgorithm::SHA1), + ..CryptographicParameters::default() + }, + AwsKmsWrappingAlgorithm::RsaAesKeyWrapSha256 => CryptographicParameters { + cryptographic_algorithm: Some(CryptographicAlgorithm::RSA), + padding_method: Some(PaddingMethod::None), + hashing_algorithm: Some(HashingAlgorithm::SHA256), + ..CryptographicParameters::default() + }, + // SM2PKE: SM2 public key encryption (China Regions only) + // Supported for: RSA private keys, ECC private keys, SM2 private keys + // TODO: gate this + AwsKmsWrappingAlgorithm::Sm2Pke => { + warn!( + "This encrypted key material can only be imported into AWS KMS in China Regions." + ); + CryptographicParameters { + cryptographic_algorithm: Some(CryptographicAlgorithm::SM2), + padding_method: None, // SM2 uses its own encryption scheme per GM/T 0003.4-2012 + ..CryptographicParameters::default() + } + } + }); + + // Export the key wrapped with the KEK + let export_params = ExportObjectParams { + unwrap: false, + wrapping_key_id: Some(&self.kek_id), + allow_revoked: false, + key_format_type: None, + encode_to_ttlv: false, + wrapping_cryptographic_parameters, + authenticated_encryption_additional_data: None, + }; + + let (_, object, _) = export_object(&kms_client, &self.key_id, export_params).await?; + + // Recover the wrapped bytes from the KeyBlock + let key_block = object.key_block()?; + let Some(KeyValue::ByteString(wrapped_key)) = &key_block.key_value else { + cli_bail!("The wrapped key should be a byte string"); + }; + let b64_key = base64::engine::general_purpose::STANDARD.encode(wrapped_key); + + if let Some(file_path) = &self.output_file_path { + fs::write(file_path, wrapped_key)?; + + // Print all formatting and instructions to stderr to not interfere with pipes + eprint!("The encrypted key material was successfully written to "); + print!("{}", file_path.display()); + eprintln!( + "{} for key {}.\n\n\ + To import into AWS KMS using the API, run:\n\ + aws kms import-key-material \\\n\ + --key-id {} \\\n\ + --encrypted-key-material fileb://{} \\\n\ + --import-token fileb://{} \\\n\ + --expiration-model KEY_MATERIAL_DOES_NOT_EXPIRE", + wrapped_key.len(), + self.key_id, + key_arn.unwrap_or(""), + file_path.display(), + self.token_file_path.as_ref().map_or_else( + || "".to_owned(), + |p| { p.display().to_string() } + ) + ); + } else { + // Same as above: descriptive info to stderr... + eprintln!("Wrapped key material (base64-encoded):"); + // And raw output goes to stdout (can be piped) + let stdout = console::Stdout::new(&b64_key); + stdout.write()?; + } + Ok(b64_key) + } +} diff --git a/crate/cli/src/actions/kms/aws/byok/import_kek.rs b/crate/cli/src/actions/kms/aws/byok/import_kek.rs new file mode 100644 index 000000000..176f00545 --- /dev/null +++ b/crate/cli/src/actions/kms/aws/byok/import_kek.rs @@ -0,0 +1,103 @@ +use std::path::PathBuf; + +use crate::{ + actions::kms::{ + aws::byok::wrapping_algorithms::AwsKmsWrappingAlgorithm, + shared::ImportSecretDataOrKeyAction, + }, + error::{KmsCliError, result::KmsCliResult}, +}; +use base64::{Engine, prelude::BASE64_STANDARD}; +use clap::{ArgGroup, Parser}; +use cosmian_kmip::kmip_2_1::kmip_types::UniqueIdentifier; +use cosmian_kms_client::{ + KmsClient, + reexport::cosmian_kms_client_utils::import_utils::{ImportKeyFormat, KeyUsage}, +}; + +/// Validate that the string is valid base64 and its decoded length is between 1 and 4096 bytes. +fn validate_kek_base64(s: &str) -> Result { + let decoded = BASE64_STANDARD + .decode(s) + .map_err(|e| format!("Invalid base64 encoding: {e}"))?; + + if decoded.is_empty() { + return Err("KEK decoded data is empty".to_owned()); + } + + if decoded.len() > 4096 { + return Err(format!( + "KEK decoded data exceeds maximum length of 4096 bytes (got {})", + decoded.len() + )); + } + Ok(s.to_owned()) +} + +/// Import an AWS Key Encryption Key (KEK) into the KMS. +#[derive(Parser)] +#[clap(verbatim_doc_comment)] +#[clap(group(ArgGroup::new("kek_input").required(true).args(["kek_base64", "kek_file"])))] // At least one of kek_file or kek_blob must be provided +pub struct ImportKekAction { + /// The RSA Key Encryption public key (the KEK) as a base64-encoded string + #[clap( + short = 'b', + long, + value_parser = clap::builder::ValueParser::new(validate_kek_base64), + group = "kek_input" + )] + pub(crate) kek_base64: Option, + + /// In case of KEK provided as a file blob. + #[clap(short = 'f', long, group = "kek_input")] + pub(crate) kek_file: Option, + + #[clap(short = 'w', long, required = true)] + pub(crate) wrapping_algorithm: AwsKmsWrappingAlgorithm, + + /// The Amazon Resource Name (key ARN) of the KMS key. It's recommended to provide it for an easier export later. + #[clap(short = 'a', long, required = false)] + pub(crate) key_arn: Option, + + /// The unique ID of the key in this KMS; a random UUID + /// is generated if not specified. + #[clap(short = 'i', long, required = false)] + pub(crate) key_id: Option, +} + +impl ImportKekAction { + pub async fn run(&self, kms_client: KmsClient) -> KmsCliResult { + // build tags + let mut tags = vec![ + "aws".to_owned(), + format!("wrapping_algorithm:{}", self.wrapping_algorithm), + ]; + if let Some(arn) = &self.key_arn { + tags.push(format!("key_arn:{arn}")); + } + + let import_action = ImportSecretDataOrKeyAction { + key_file: match (&self.kek_file, &self.kek_base64) { + (Some(file), _) => file.clone(), + (None, Some(base64_str)) => { + let temp_path = std::env::temp_dir().join(format!("{}", uuid::Uuid::new_v4())); + std::fs::write(&temp_path, BASE64_STANDARD.decode(base64_str)?)?; + temp_path + } + (None, None) => { + return Err(KmsCliError::Default( + "KEK file or base64 data must be provided".to_owned(), + )); + } + }, + key_id: self.key_id.clone(), + key_format: ImportKeyFormat::Pkcs8Pub, + tags, + key_usage: Some(vec![KeyUsage::WrapKey, KeyUsage::Encrypt]), + replace_existing: true, + ..Default::default() + }; + + import_action.run(kms_client).await + } +} diff --git a/crate/cli/src/actions/kms/aws/byok/mod.rs b/crate/cli/src/actions/kms/aws/byok/mod.rs new file mode 100644 index 000000000..7ef5a31b6 --- /dev/null +++ b/crate/cli/src/actions/kms/aws/byok/mod.rs @@ -0,0 +1,33 @@ +pub mod export_key_material; +pub mod import_kek; +pub mod wrapping_algorithms; + +use clap::Subcommand; +use cosmian_kms_client::KmsClient; + +use crate::{ + actions::kms::aws::byok::{export_key_material::ExportByokAction, import_kek::ImportKekAction}, + error::result::KmsCliResult, +}; + +/// AWS BYOK support. +/// See: +#[derive(Subcommand)] +pub enum ByokCommands { + Import(ImportKekAction), + Export(ExportByokAction), +} + +impl ByokCommands { + pub async fn process(&self, kms_rest_client: KmsClient) -> KmsCliResult<()> { + match self { + Self::Import(action) => { + action.run(kms_rest_client).await?; + } + Self::Export(action) => { + action.run(kms_rest_client).await?; + } + } + Ok(()) + } +} diff --git a/crate/cli/src/actions/kms/aws/byok/wrapping_algorithms.rs b/crate/cli/src/actions/kms/aws/byok/wrapping_algorithms.rs new file mode 100644 index 000000000..4b8646cdf --- /dev/null +++ b/crate/cli/src/actions/kms/aws/byok/wrapping_algorithms.rs @@ -0,0 +1,35 @@ +use clap::{Parser, ValueEnum}; +use strum::{Display, EnumString}; + +/// The algorithm used with the RSA public key to protect key material during import. +/// +/// For `RSA_AES` wrapping algorithms, you encrypt your key material with an AES key +/// that you generate, then encrypt your AES key with the RSA public key from AWS KMS. +/// For `RSA_AES` wrapping algorithms, you encrypt your key material directly with the +/// RSA public key from AWS KMS. +#[derive(Display, Parser, Debug, Clone, Copy, PartialEq, Eq, ValueEnum, EnumString)] +pub enum AwsKmsWrappingAlgorithm { + /// Supported for all types of key material, except RSA key material (private key). + /// Cannot be used with `RSA_2048` wrapping key spec to wrap `ECC_NIST_P521` key material. + #[clap(name = "RSAES_OAEP_SHA_1")] + RsaesOaepSha1, + + /// Supported for all types of key material, except RSA key material (private key). + /// Cannot be used with `RSA_2048` wrapping key spec to wrap `ECC_NIST_P521` key material. + #[clap(name = "RSAES_OAEP_SHA_256")] + RsaesOaepSha256, + + /// Supported for wrapping RSA and ECC key material. + /// Required for importing RSA private keys. + #[clap(name = "RSA_AES_KEY_WRAP_SHA_1")] + RsaAesKeyWrapSha1, + + /// Supported for wrapping RSA and ECC key material. + /// Required for importing RSA private keys. + #[clap(name = "RSA_AES_KEY_WRAP_SHA_256")] + RsaAesKeyWrapSha256, + + /// Chinese SM2 public key encryption algorithm. + #[clap(name = "SM2PKE")] + Sm2Pke, +} diff --git a/crate/cli/src/actions/kms/aws/mod.rs b/crate/cli/src/actions/kms/aws/mod.rs new file mode 100644 index 000000000..b3778d53c --- /dev/null +++ b/crate/cli/src/actions/kms/aws/mod.rs @@ -0,0 +1,22 @@ +pub mod byok; + +use clap::Parser; +use cosmian_kms_client::KmsClient; + +use crate::{actions::kms::aws::byok::ByokCommands, error::result::KmsCliResult}; + +/// Support for AWS specific interactions. +#[derive(Parser)] +pub enum AwsCommands { + #[command(subcommand)] + Byok(ByokCommands), +} + +impl AwsCommands { + pub async fn process(&self, kms_rest_client: KmsClient) -> KmsCliResult<()> { + match self { + Self::Byok(command) => command.process(kms_rest_client).await?, + } + Ok(()) + } +} diff --git a/crate/cli/src/actions/kms/azure/byok/export_byok.rs b/crate/cli/src/actions/kms/azure/byok/export_byok.rs index 4b53a1e24..3aba105ec 100644 --- a/crate/cli/src/actions/kms/azure/byok/export_byok.rs +++ b/crate/cli/src/actions/kms/azure/byok/export_byok.rs @@ -103,7 +103,7 @@ impl ExportByokAction { "alg": "dir", "enc": "CKM_RSA_AES_KEY_WRAP" }, - "ciphertext": base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(wrapped_key), + "ciphertext": base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(wrapped_key), // TODO: check the docs, why use URL_SAFE_NO_PAD here instead of standard one "generator": "Cosmian_KMS;v5" }); // write byok file diff --git a/crate/cli/src/actions/kms/console.rs b/crate/cli/src/actions/kms/console.rs index 856e8e460..2294754aa 100644 --- a/crate/cli/src/actions/kms/console.rs +++ b/crate/cli/src/actions/kms/console.rs @@ -145,7 +145,7 @@ impl Stdout { // Print the unique identifier if present if let Some(id) = &self.unique_identifier { - println!("\t Unique identifier: {id}"); + println!("\tUnique identifier: {id}"); } // Print the list of unique identifiers if present @@ -157,12 +157,12 @@ impl Stdout { // Print the public key unique identifier if present if let Some(id) = &self.public_key_unique_identifier { - println!("\t Public key unique identifier: {id}"); + println!("\tPublic key unique identifier: {id}"); } // Print the private key unique identifier if present if let Some(id) = &self.private_key_unique_identifier { - println!("\t Private key unique identifier: {id}"); + println!("\tPrivate key unique identifier: {id}"); } // Print the attribute if present: attribute is a single element @@ -201,12 +201,13 @@ impl Stdout { // Print the list of tags if present if let Some(t) = &self.tags { if !t.is_empty() { - println!("\n Tags:"); + println!("\tTags:"); for tag in t { - println!(" - {tag}"); + println!("\t\t- {tag}"); } } } + println!(); // consecutive calls feel cluttered and become hard to read } OutputFormat::Json => { // Serialize the output as JSON and print it diff --git a/crate/cli/src/actions/kms/mod.rs b/crate/cli/src/actions/kms/mod.rs index 76f803e1d..5de34e88c 100644 --- a/crate/cli/src/actions/kms/mod.rs +++ b/crate/cli/src/actions/kms/mod.rs @@ -1,6 +1,7 @@ pub mod access; pub mod actions; pub mod attributes; +pub mod aws; pub mod azure; pub mod bench; pub mod certificates; diff --git a/crate/cli/src/actions/kms/shared/import_key.rs b/crate/cli/src/actions/kms/shared/import_key.rs index 0cb35e79e..977ec5420 100644 --- a/crate/cli/src/actions/kms/shared/import_key.rs +++ b/crate/cli/src/actions/kms/shared/import_key.rs @@ -142,7 +142,7 @@ impl ImportSecretDataOrKeyAction { // print the response let stdout = format!( - "The {:?} in file {} was imported with id: {}", + "The {:?} in file {} was successfully imported with id: {}.", object_type, self.key_file.display(), unique_identifier, diff --git a/crate/cli/src/lib.rs b/crate/cli/src/lib.rs index a1b58c4f8..5888cbd9b 100644 --- a/crate/cli/src/lib.rs +++ b/crate/cli/src/lib.rs @@ -7,6 +7,7 @@ pub mod reexport { pub use cosmian_kms_crypto; } +// Clippy lints that are allowed in tests #[cfg(test)] #[allow( clippy::unwrap_used, @@ -19,6 +20,7 @@ pub mod reexport { clippy::large_stack_frames, clippy::ignore_without_reason, dead_code, - clippy::unwrap_in_result + clippy::unwrap_in_result, + clippy::as_conversions )] mod tests; diff --git a/crate/cli/src/tests/kms/aws/integration_tests.rs b/crate/cli/src/tests/kms/aws/integration_tests.rs new file mode 100644 index 000000000..62f199463 --- /dev/null +++ b/crate/cli/src/tests/kms/aws/integration_tests.rs @@ -0,0 +1,412 @@ +//! Since AWS KMS is a managed service where private keys never leave the HSM, +//! we simulate the `ImportKeyMaterial` step by unwrapping with OpenSSL. +//! +//! ## Test Matrix +//! +//! | Test Function | Wrapping Algorithm | Key Type | Key Source | KEK Import | Export Mode | +//! |----------------------------------------|---------------------------|--------------------|--------------------|------------|-------------| +//! | `aws_byok_with_rsa_aes_key_wrap_sha256`| `RSA_AES_KEY_WRAP_SHA_256`| ECC (private key) | KMS (generated) | Base64 | File (bin) | +//! | `aws_byok_with_rsaes_oaep_sha256` | `RSAES_OAEP_SHA_256` | AES-256 | Test file (imported) | Base64 | Base64 | +//! | `aws_byok_with_rsaes_oaep_sha1` | `RSAES_OAEP_SHA_1` | HMAC | KMS (generated) | File (DER) | Base64 | +//! | `aws_byok_with_rsa_aes_key_wrap_sha1` | `RSA_AES_KEY_WRA_SHA_1` | RSA (private key) | KMS (generated) | File (DER) | File (bin) | +//! +//! [AWS KMS Docs](https://docs.aws.amazon.com/kms/latest/developerguide/importing-keys-encrypt-key-material.html) + +use base64::Engine; +use cosmian_kms_client::reexport::cosmian_kms_client_utils::{ + create_utils::SymmetricAlgorithm, import_utils::ImportKeyFormat, +}; +use cosmian_kms_client::{ExportObjectParams, export_object}; +use cosmian_kms_crypto::reexport::cosmian_crypto_core::CsRng; +use cosmian_logger::log_init; +use sha2::digest::crypto_common::rand_core::{RngCore, SeedableRng}; +use test_kms_server::start_default_test_kms_server; +use uuid::Uuid; + +use crate::actions::kms::{ + aws::byok::{ + export_key_material::ExportByokAction, import_kek::ImportKekAction, + wrapping_algorithms::AwsKmsWrappingAlgorithm, + }, + elliptic_curves::keys::create_key_pair::CreateKeyPairAction as CreateEccKeyPairAction, + rsa::keys::create_key_pair::CreateKeyPairAction as CreateRsaKeyPairAction, + shared::ImportSecretDataOrKeyAction, + symmetric::keys::create_key::CreateKeyAction, +}; +use crate::error::result::KmsCliResult; +use crate::tests::kms::shared::openssl_utils::{ + generate_rsa_keypair, rsa_aes_key_wrap_sha1_unwrap, rsa_aes_key_wrap_sha256_unwrap, + rsaes_oaep_sha1_unwrap, rsaes_oaep_sha256_unwrap, +}; + +// Test constants from AWS KMS GetParametersForImport response +const TEST_KEY_ARN: &str = + "arn:aws:kms:eu-west-3:447182645454:key/e8518bca-e1d0-4519-a915-d80da8e8f38a"; + +const TEST_PUBLIC_KEY_BASE64: &str = "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEApujv1m1gfctmaIaWD4ns9b5MWrr2JwYJYo82Ri3AoQZkOq0BQKkBazO61Scn/+buRE57x5tYTfUTZdnwUe4OuGgTRmH/2SPbcILbpulLP31YnqEP5IxLnn7Z9NR6VODn0QiUyv/uaHE/uBD7mt1+KHKEOBn+rL53/ht3yrboGgqxKj84FITNPaiOZ7yTccB0yCqvlKWYpcrIPeTBdGlpXni10GyBxRqGfkmKuX9/rxwDlBbzdAXn9nHOmhhZlzBUHDzidXZvYrfWEqxfnYAuTbb0Dwj/7eTiFUKseV7NXU/KpAyIG3OghDjNF7PnKT7Zlf7CvSYE+9DOqadBzjQjbOu10lLdoo2nWfCtkvE5XrZkqJHHk+9DUBnkQX3I6MdCWlfTp8QWHiwbo8rFLC4ZSLCB/QqhTh8XnHwdVkmrDKhpYQH6m1pJcsG4sIICDwIkdMSkw/CHOk+bl76TIsVqCu/7QyvFLtsvIDG3Ia0qwshYpUuIoKxXfgwUuZiwSN2RAgMBAAE="; + +// Generate the key material locally, then import it to the kms using ImportSecretDataOrKeyAction +// The key material of this test will be a symmetric encryption key (32 bytes) +// Import kek as base64 string +// Export the key material wrapped with the kek as base64 string +#[tokio::test] +async fn aws_byok_with_rsaes_oaep_sha256() -> KmsCliResult<()> { + log_init(None); + let ctx = start_default_test_kms_server().await; + + // Test initialization steps : + // Generate a local RSA keypair for wrapping (simulating AWS KMS GetParametersForImport) + let (private_key, public_key) = generate_rsa_keypair().expect("Failed to generate RSA keypair"); + + let public_key_base64 = base64::engine::general_purpose::STANDARD.encode( + public_key + .public_key_to_der() + .expect("Failed to export public key to DER"), + ); + + let temp_dir = std::env::temp_dir(); + + // Generate a random symmetric key to be wrapped (simulating the key material to be imported) + let cosmian_key_id = "test-symmetric-key"; + let mut cosmian_key_bytes = [0_u8; 32]; + let mut rng = CsRng::from_entropy(); + rng.fill_bytes(&mut cosmian_key_bytes); + + let cosmian_key_file = temp_dir.join(format!("cosmian_key_test_{}.bin", uuid::Uuid::new_v4())); + std::fs::write(&cosmian_key_file, cosmian_key_bytes).expect("Failed to write public key file"); + + let import_key_action = ImportSecretDataOrKeyAction { + key_file: cosmian_key_file.clone(), + key_id: Some(cosmian_key_id.to_owned()), + key_format: ImportKeyFormat::Aes, // Indicates this is an AES symmetric key + ..Default::default() + }; + + import_key_action.run(ctx.get_owner_client()).await?; + + // We now have all necessary elements to start the test + // Step 1: Import the Kek + let import_action = ImportKekAction { + // TODO: check why the compiler complains abt an optional fields (the kek id) + kek_base64: Some(public_key_base64), + kek_file: None, + key_arn: Some(TEST_KEY_ARN.to_owned()), + wrapping_algorithm: AwsKmsWrappingAlgorithm::RsaesOaepSha256, + key_id: None, + }; + + let kek_id = import_action.run(ctx.get_owner_client()).await?; + + // Step 2: export the wrapped key + let export_action = ExportByokAction { + key_id: cosmian_key_id.to_string(), + kek_id: kek_id.to_string(), + token_file_path: None, + output_file_path: None, + }; + + let wrapped_key_b64 = export_action.run(ctx.get_owner_client()).await?; + + let wrapped_key_bytes = base64::engine::general_purpose::STANDARD + .decode(&wrapped_key_b64) + .expect("Failed to decode base64 wrapped key"); + + // Step 3: (simulating AWS KMS ImportKeyMaterial) Unwrap the key locally with the private key + let unwrapped_key_bytes = + rsaes_oaep_sha256_unwrap(&wrapped_key_bytes, &private_key).expect("Failed to unwrap key"); + + // Finally: Verify the unwrapped key matches the original key material + assert_eq!( + unwrapped_key_bytes, cosmian_key_bytes, + "Unwrapped key should match the original key material" + ); + + std::fs::remove_file(&cosmian_key_file)?; + Ok(()) +} + +// Generate the key material with the KMS, then export it using ExportObjectParams for later verification +// The key material of this test will be a HMAC keys +// Import kek as a file blob +// Export the key material wrapped with the kek as a file blob +#[tokio::test] +async fn aws_byok_with_rsaes_oaep_sha1() -> KmsCliResult<()> { + log_init(None); + + let ctx = start_default_test_kms_server().await; + // Test initialization steps : + // Generate a local RSA keypair for wrapping (simulating AWS KMS GetParametersForImport). + let (aws_private_key_mock, aws_public_key_mock) = + generate_rsa_keypair().expect("Failed to generate RSA keypair"); + + let temp_dir = std::env::temp_dir(); + + // Write the public key to a file (DER format) to import it later. + let kek_file_path = temp_dir.join(format!("kek_test_{}.der", uuid::Uuid::new_v4())); + std::fs::write( + &kek_file_path, + aws_public_key_mock + .public_key_to_der() + .expect("Failed to export public key to DER"), + ) + .expect("Failed to write KEK file"); + + let key_sizes = [224, 256, 384, 512]; + let mut rng = CsRng::from_entropy(); + let bits = key_sizes[(rng.next_u32() as usize) % key_sizes.len()]; + + // Generate a random symmetric key in the kms. + let cosmian_key_id = CreateKeyAction { + algorithm: SymmetricAlgorithm::Sha3, + number_of_bits: Some(bits), + ..Default::default() + } + .run(ctx.get_owner_client()) + .await?; + + let (_, cosmian_key_material, _attributes) = export_object( + &ctx.get_owner_client(), + &cosmian_key_id.to_string(), + ExportObjectParams::default(), + ) + .await?; + + // Keep this here for the final verification. + let cosmian_key_bytes = cosmian_key_material.key_block()?.key_bytes()?; + + // We now have all necessary elements to start the test + // Step 1: Import the KEK from file + let import_action = ImportKekAction { + kek_base64: None, + kek_file: Some(kek_file_path.clone()), + key_arn: Some(TEST_KEY_ARN.to_owned()), + wrapping_algorithm: AwsKmsWrappingAlgorithm::RsaesOaepSha1, + key_id: None, + }; + + let kek_id = import_action.run(ctx.get_owner_client()).await?; + + // Step 2: Export the wrapped key + let export_action = ExportByokAction { + key_id: cosmian_key_id.to_string(), + kek_id: kek_id.to_string(), + token_file_path: None, + output_file_path: None, + }; + + let wrapped_key_b64 = export_action.run(ctx.get_owner_client()).await?; + + let wrapped_key_bytes = base64::engine::general_purpose::STANDARD + .decode(&wrapped_key_b64) + .expect("Failed to decode base64 wrapped key"); + + // Verification step: (simulating AWS KMS ImportKeyMaterial) Unwrap the key locally with the private key + let unwrapped_key_bytes = rsaes_oaep_sha1_unwrap(&wrapped_key_bytes, &aws_private_key_mock) + .expect("Failed to unwrap key"); + + // Finally: Verify the unwrapped key matches the original key material + assert_eq!( + unwrapped_key_bytes, + cosmian_key_bytes.to_vec(), + "Unwrapped key should match the original key material" + ); + + // Cleanup temp files + std::fs::remove_file(&kek_file_path)?; + + Ok(()) +} + +// Generate the key material with the KMS, then export it using ExportObjectParams for later verification +// The key material of this test will be an RSA private key +// Import kek as a file blob +// Export the key material wrapped with the kek as a file blob +#[tokio::test] +async fn aws_byok_with_rsa_aes_key_wrap_sha1() -> KmsCliResult<()> { + log_init(None); + let ctx = start_default_test_kms_server().await; + let (aws_private_key_mock, aws_public_key_mock) = + generate_rsa_keypair().expect("Failed to generate RSA keypair"); + + let temp_dir = std::env::temp_dir(); + + // Write the public key to a file (DER format) to import it later + let kek_file_path = temp_dir.join(format!("kek_test_{}.der", Uuid::new_v4())); + std::fs::write( + &kek_file_path, + aws_public_key_mock + .public_key_to_der() + .expect("Failed to export public key to DER"), + ) + .expect("Failed to write KEK file"); + + // Generate an RSA keypair in the KMS (the key material to wrap will be the private key) + let key_sizes = [2048, 3072, 4096]; + let mut rng = CsRng::from_entropy(); + let bits = key_sizes[(rng.next_u32() as usize) % key_sizes.len()]; + + let create_keypair_action = CreateRsaKeyPairAction { + key_size: bits, + ..Default::default() + }; + + // we will discard the public key for the test - real world users will simply export it in plaintext + let (private_key_id, _public_key_id) = + create_keypair_action.run(ctx.get_owner_client()).await?; + + // Export the private key unwrapped and keep its plaintext bytes for later verification + let (_, cosmian_key_material, _) = export_object( + &ctx.get_owner_client(), + &private_key_id.to_string(), + ExportObjectParams::default(), + ) + .await?; + let cosmian_key_bytes = cosmian_key_material.key_block()?.key_bytes()?; + + // We now have all necessary elements to start the test + // Step 1: Import the KEK from file + let import_action = ImportKekAction { + kek_file: Some(kek_file_path.clone()), + kek_base64: None, + key_arn: Some(TEST_KEY_ARN.to_owned()), + wrapping_algorithm: AwsKmsWrappingAlgorithm::RsaAesKeyWrapSha1, + key_id: None, + }; + + let kek_id = import_action.run(ctx.get_owner_client()).await?; + let output_file_path = temp_dir.join(format!("wrapped_key_test_{private_key_id}.bin")); + + // Step 2: Export the wrapped key + let export_action = ExportByokAction { + key_id: private_key_id.to_string(), + kek_id: kek_id.to_string(), + token_file_path: None, + output_file_path: Some(output_file_path.clone()), + }; + + export_action.run(ctx.get_owner_client()).await?; + + // Verification step: Read the file and unwrap the key locally with the private key + let wrapped_key_bytes = std::fs::read(&output_file_path).expect("Failed to read KEK file"); + + let mut unwrapped_key_bytes = + rsa_aes_key_wrap_sha1_unwrap(&wrapped_key_bytes, &aws_private_key_mock) + .expect("Failed to unwrap key"); + + // IMPORTANT: Asymmetric key material must be BER-encoded or DER-encoded in Public-Key Cryptography Standards (PKCS) #8 format that complies with RFC 5208. + let pkey = openssl::pkey::PKey::private_key_from_pkcs8(&unwrapped_key_bytes) + .expect("Failed to parse PKCS#8 key"); + let rsa = pkey.rsa().expect("Key should be RSA"); + unwrapped_key_bytes = rsa + .private_key_to_der() + .expect("Failed to convert to PKCS#1"); + + // Finally: Verify the unwrapped key matches the original key material + assert_eq!( + unwrapped_key_bytes, + cosmian_key_bytes.to_vec(), + "Unwrapped key should match the original key material" + ); + + // Cleanup temp files + std::fs::remove_file(&kek_file_path)?; + std::fs::remove_file(&output_file_path)?; + + Ok(()) +} + +// Generate the key material with the KMS, then export it using ExportObjectParams for later verification +// Import kek as base64 string +// Export the key material wrapped with the kek as a file blob +// /!\ It's not possible to export cleartext ECC private keys from the KMS, so we skip the plaintext verification step +#[tokio::test] +async fn aws_byok_with_rsa_aes_key_wrap_sha256() -> KmsCliResult<()> { + log_init(None); + let ctx = start_default_test_kms_server().await; + let (aws_private_key_mock, aws_public_key_mock) = + generate_rsa_keypair().expect("Failed to generate RSA keypair"); + + let public_key_base64 = base64::engine::general_purpose::STANDARD.encode( + aws_public_key_mock + .public_key_to_der() + .expect("Failed to export public key to DER"), + ); + + let temp_dir = std::env::temp_dir(); + + // Generate an ECC keypair in the KMS (the key material to wrap will be the private key) + let create_keypair_action = CreateEccKeyPairAction { + sensitive: false, + ..Default::default() + }; + + let (private_key_id, _public_key_id) = + create_keypair_action.run(ctx.get_owner_client()).await?; + + let import_action = ImportKekAction { + kek_base64: Some(public_key_base64), + kek_file: None, + key_arn: Some(TEST_KEY_ARN.to_owned()), + wrapping_algorithm: AwsKmsWrappingAlgorithm::RsaAesKeyWrapSha256, + key_id: None, + }; + + let kek_id = import_action.run(ctx.get_owner_client()).await?; + + let output_file_path = temp_dir.join(format!("wrapped_key_test_{private_key_id}.bin")); + + let export_action = ExportByokAction { + key_id: private_key_id.to_string(), + kek_id: kek_id.to_string(), + token_file_path: None, + output_file_path: Some(output_file_path.clone()), + }; + + export_action.run(ctx.get_owner_client()).await?; + + // Verification step: Read the file and unwrap the key locally with the private key + let wrapped_key_bytes = + std::fs::read(&output_file_path).expect("Failed to read wrapped key file"); + + let unwrapped_key_bytes = + rsa_aes_key_wrap_sha256_unwrap(&wrapped_key_bytes, &aws_private_key_mock) + .expect("Failed to unwrap key"); + + // Parse the unwrapped key as PKCS#8 + let pkey = openssl::pkey::PKey::private_key_from_pkcs8(&unwrapped_key_bytes) + .expect("Failed to parse PKCS#8 key"); + + // Extract the ECC key (and check it's valid) + let _ec_key = pkey.ec_key().expect("Key should be ECC"); + + std::fs::remove_file(&output_file_path)?; + + Ok(()) +} + +// #[cfg(feature = "non-fips")] +// pub(crate) fn sm2pke_unwrap( +// ciphertext: &[u8], +// private_key: &PKey, +// ) -> Result, Box> { +// // Verify the key is an SM2 key + +// use openssl::pkey_ctx::PkeyCtx; +// if private_key.id() != openssl::pkey::Id::SM2 { +// return Err("Private key is not an SM2 key".into()); +// } + +// // Create decryption context +// let mut ctx = PkeyCtx::new(private_key)?; +// ctx.decrypt_init()?; + +// // Calculate buffer size for decryption +// let buffer_len = ctx.decrypt(ciphertext, None)?; +// let mut plaintext = vec![0_u8; buffer_len]; + +// // Perform decryption +// let plaintext_len = ctx.decrypt(ciphertext, Some(&mut plaintext))?; +// plaintext.truncate(plaintext_len); + +// Ok(plaintext) +// } diff --git a/crate/cli/src/tests/kms/aws/mod.rs b/crate/cli/src/tests/kms/aws/mod.rs new file mode 100644 index 000000000..f8ec3c087 --- /dev/null +++ b/crate/cli/src/tests/kms/aws/mod.rs @@ -0,0 +1,2 @@ +mod integration_tests; +mod unwrap_utils; diff --git a/crate/cli/src/tests/kms/aws/unwrap_utils.rs b/crate/cli/src/tests/kms/aws/unwrap_utils.rs new file mode 100644 index 000000000..3200320a4 --- /dev/null +++ b/crate/cli/src/tests/kms/aws/unwrap_utils.rs @@ -0,0 +1,66 @@ +// //! AWS KMS is a managed service that can't be run locally for tests. By its design, private key materials never leave the AWS HSM, which makes it even harder to make tests that do not involve +// //! actual calls to external infrastructure. Therefore, to verify the correct behavior of the AWS KMS BYOK import and export commands, we will unwrap using openssl. +// //! As long as we can trust AWS KMS to behave correctly, we can consider these functions viable to verify the unwrapping process. +// //! +// //! If ever E2E tests with AWS KMS are to be implemented, simply edit the calls to the functions below to calls to AWS KMS `import-key-material` command. +// use openssl::cipher::{Cipher, CipherRef}; +// use openssl::cipher_ctx::CipherCtx; +// use openssl::pkey::{PKey, Private, Public}; +// use openssl::rsa::Padding; +// use openssl::{encrypt::Decrypter, hash::MessageDigest}; + +// /// Generate SM2 keypair using OpenSSL +// /// This replaces the chinese AWS KMS keypair generation for testing purposes. +// #[cfg(feature = "non-fips")] +// pub(crate) fn generate_sm2_keypair() +// -> Result<(PKey, PKey), Box> { +// use openssl::ec::{EcGroup, EcKey}; +// use openssl::nid::Nid; + +// let group = EcGroup::from_curve_name(Nid::SM2)?; + +// // Generate EC key on SM2 curve +// let ec_key = EcKey::generate(&group)?; + +// // Convert to PKey +// let private_key = PKey::from_ec_key(ec_key.clone())?; + +// // Extract public key +// let public_ec_key = EcKey::from_public_key(&group, ec_key.public_key())?; +// let public_key = PKey::from_ec_key(public_ec_key)?; + +// Ok((private_key, public_key)) +// } + +// /// Unwrap (decrypt) the given ciphertext using SM2PKE (SM2 Public Key Encryption) +// /// SM2PKE is a Chinese national standard encryption algorithm used in AWS China regions. +// /// This replaces the AWS KMS Import key material step for testing purposes. +// /// +// /// Note: SM2 support requires OpenSSL 1.1.1+ compiled with SM2 support. +// /// This is typically available in non-FIPS mode only. +// #[cfg(feature = "non-fips")] +// pub(crate) fn sm2pke_unwrap( +// ciphertext: &[u8], +// private_key: &PKey, +// ) -> Result, Box> { +// // Verify the key is an SM2 key + +// use openssl::pkey_ctx::PkeyCtx; +// if private_key.id() != openssl::pkey::Id::SM2 { +// return Err("Private key is not an SM2 key".into()); +// } + +// // Create decryption context +// let mut ctx = PkeyCtx::new(private_key)?; +// ctx.decrypt_init()?; + +// // Calculate buffer size for decryption +// let buffer_len = ctx.decrypt(ciphertext, None)?; +// let mut plaintext = vec![0_u8; buffer_len]; + +// // Perform decryption +// let plaintext_len = ctx.decrypt(ciphertext, Some(&mut plaintext))?; +// plaintext.truncate(plaintext_len); + +// Ok(plaintext) +// } diff --git a/crate/cli/src/tests/kms/azure/mod.rs b/crate/cli/src/tests/kms/azure/mod.rs index bd1a5999b..da0c65b44 100644 --- a/crate/cli/src/tests/kms/azure/mod.rs +++ b/crate/cli/src/tests/kms/azure/mod.rs @@ -1,55 +1,16 @@ -use std::fs; - -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}, symmetric::keys::create_key::CreateKeyAction, }, error::{KmsCliError, result::KmsCliResult}, + tests::kms::shared::openssl_utils::{generate_rsa_keypair, rsa_aes_key_wrap_sha1_unwrap}, }; - -/// 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)) -} +use base64::Engine; +use cosmian_kms_client::{ExportObjectParams, export_object}; +use std::fs; +use tempfile::TempDir; +use test_kms_server::start_default_test_kms_server; #[tokio::test] async fn test_azure_byok_import_kek_then_export_byok() -> KmsCliResult<()> { @@ -61,7 +22,7 @@ async fn test_azure_byok_import_kek_then_export_byok() -> KmsCliResult<()> { let kek_pem_path = tmp_dir.path().join("kek_pub.pem"); // 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 (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}")))?; @@ -88,6 +49,10 @@ async fn test_azure_byok_import_kek_then_export_byok() -> KmsCliResult<()> { .run(kms_client.clone()) .await? .to_string(); + // for later verification + let (_, cosmian_key_material, _) = + export_object(&kms_client, &sym_key_id, ExportObjectParams::default()).await?; + let original_key_bytes = cosmian_key_material.key_block()?.key_bytes()?; let byok_file = tmp_dir.path().join("out.byok"); @@ -99,10 +64,33 @@ async fn test_azure_byok_import_kek_then_export_byok() -> KmsCliResult<()> { .run(kms_client) .await?; + // 4. Post-export verifications + // Assert byok file written - let contents = std::fs::read_to_string(&byok_file)?; - assert!(contents.contains("\"ciphertext\"")); - assert!(contents.contains("\"kid\"")); + let byok_contents = std::fs::read_to_string(&byok_file)?; + assert!(byok_contents.contains("\"ciphertext\"")); + assert!(byok_contents.contains("\"kid\"")); + + // Unwrap and verify the key matches original (via helper function) + let json: serde_json::Value = serde_json::from_str(&byok_contents)?; + let ciphertext_b64url = json["ciphertext"] + .as_str() + .ok_or("Missing 'ciphertext' field in BYOK JSON") + .unwrap(); + + // Decode BASE64URL first + let ciphertext = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(ciphertext_b64url) + .map_err(|e| KmsCliError::Default(format!("Failed to decode BASE64URL: {e}")))?; + + // now unwrap + let unwrapped_key_bytes = rsa_aes_key_wrap_sha1_unwrap(&ciphertext, &private_key).unwrap(); + + assert_eq!( + unwrapped_key_bytes, + original_key_bytes.to_vec(), + "Unwrapped key should match original" + ); Ok(()) } diff --git a/crate/cli/src/tests/kms/mod.rs b/crate/cli/src/tests/kms/mod.rs index 5ea705895..f93685815 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 aws; mod azure; mod certificates; #[cfg(feature = "non-fips")] diff --git a/crate/cli/src/tests/kms/shared/mod.rs b/crate/cli/src/tests/kms/shared/mod.rs index e3eb9d945..323d744fa 100644 --- a/crate/cli/src/tests/kms/shared/mod.rs +++ b/crate/cli/src/tests/kms/shared/mod.rs @@ -7,6 +7,7 @@ pub(super) mod import_export_encodings; pub(super) mod import_export_wrapping; #[cfg(feature = "non-fips")] pub(super) mod locate; +pub(super) mod openssl_utils; pub(super) mod revoke; #[cfg(feature = "non-fips")] pub(super) mod wrap_unwrap; diff --git a/crate/cli/src/tests/kms/shared/openssl_utils.rs b/crate/cli/src/tests/kms/shared/openssl_utils.rs new file mode 100644 index 000000000..af0677293 --- /dev/null +++ b/crate/cli/src/tests/kms/shared/openssl_utils.rs @@ -0,0 +1,186 @@ +//! These functions use OpenSSL to simulate some cloud provider HSM operations +//! for testing purposes. Using OpenSSL avoids using vendor-specific SDKs in tests +//! and keeps the tests independent from KMS key generation/export actions. +use cosmian_kms_crypto::reexport::cosmian_crypto_core::CsRng; +use openssl::cipher::{Cipher, CipherRef}; +use openssl::cipher_ctx::CipherCtx; +use openssl::encrypt::Decrypter; +use openssl::hash::MessageDigest; +use openssl::pkey::{PKey, Private, Public}; +use openssl::rsa::{Padding, Rsa}; +use sha2::digest::crypto_common::rand_core::{RngCore, SeedableRng}; + +use crate::error::{KmsCliError, result::KmsCliResult}; + +/// Generate RSA keypair using OpenSSL (random size from 2048, 3072, or 4096 bits). +pub(crate) fn generate_rsa_keypair() -> KmsCliResult<(PKey, PKey)> { + let key_sizes = [2048, 3072, 4096]; + let mut rng = CsRng::from_entropy(); + let bits = key_sizes[(rng.next_u32() as usize) % key_sizes.len()]; + + 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)) +} + +/// Unwrap (decrypt) the given ciphertext using `RSA_AES_KEY_WRAP_SHA_1` +/// This is a two-step unwrapping process: +/// 1. RSA-OAEP with SHA-1 unwraps the ephemeral AES key +/// 2. AES Key Wrap (RFC 5649) unwraps the actual key material +pub(crate) fn rsa_aes_key_wrap_sha1_unwrap( + ciphertext: &[u8], + private_key: &PKey, +) -> Result, Box> { + // The ciphertext structure is: [RSA-encrypted AES key | AES-wrapped key material] + // RSA-encrypted part size equals the RSA key size in bytes + let rsa_key_size = private_key.size(); + + if ciphertext.len() <= rsa_key_size { + return Err("Ciphertext too short for RSA_AES_KEY_WRAP".into()); + } + + // Split the ciphertext into RSA-encrypted AES key and AES-wrapped key material + let (encrypted_aes_key, wrapped_key_material) = ciphertext.split_at(rsa_key_size); + + // Step 1: Unwrap the ephemeral AES key using RSA-OAEP with SHA-1 + let aes_key = rsaes_oaep_sha1_unwrap(encrypted_aes_key, private_key)?; + + // Step 2: Unwrap the key material using AES Key Wrap (RFC 5649) + let unwrapped_key = aes_key_unwrap(wrapped_key_material, &aes_key)?; + Ok(unwrapped_key) +} + +/// Unwrap (decrypt) the given ciphertext using RSAES-OAEP with SHA-256 +/// This replaces the AWS KMS Import key material step for testing purposes. +/// The aws API equivalent command (on cli) is: +/// ```sh +/// aws kms import-key-material --key-id \ +/// --encrypted-key-material fileb:// \ +/// --import-token fileb:// \ +/// --expiration-model KEY_MATERIAL_DOES_NOT_EXPIRE +/// ``` +pub(crate) fn rsaes_oaep_sha256_unwrap( + ciphertext: &[u8], + private_key: &PKey, +) -> Result, Box> { + let mut decrypter = Decrypter::new(private_key)?; + + // Set OAEP padding with SHA-256 + decrypter.set_rsa_padding(Padding::PKCS1_OAEP)?; + decrypter.set_rsa_oaep_md(MessageDigest::sha256())?; + decrypter.set_rsa_mgf1_md(MessageDigest::sha256())?; + + // Calculate buffer size + let buffer_len = decrypter.decrypt_len(ciphertext)?; + let mut decrypted = vec![0_u8; buffer_len]; + + // Decrypt + let decrypted_len = decrypter.decrypt(ciphertext, &mut decrypted)?; + decrypted.truncate(decrypted_len); + + Ok(decrypted) +} + +/// Unwrap (decrypt) the given ciphertext using RSAES-OAEP with SHA-1 +/// This replaces the AWS KMS Import key material step for testing purposes. +pub(crate) fn rsaes_oaep_sha1_unwrap( + ciphertext: &[u8], + private_key: &PKey, +) -> Result, Box> { + let mut decrypter = Decrypter::new(private_key)?; + + // Set OAEP padding with SHA-1 + decrypter.set_rsa_padding(Padding::PKCS1_OAEP)?; + decrypter.set_rsa_oaep_md(MessageDigest::sha1())?; + decrypter.set_rsa_mgf1_md(MessageDigest::sha1())?; + + // Calculate buffer size + let buffer_len = decrypter.decrypt_len(ciphertext)?; + let mut decrypted = vec![0_u8; buffer_len]; + + // Decrypt + let decrypted_len = decrypter.decrypt(ciphertext, &mut decrypted)?; + decrypted.truncate(decrypted_len); + + Ok(decrypted) +} + +/// AES Key Unwrap with Padding (RFC 5649) using OpenSSL +fn aes_key_unwrap(ciphertext: &[u8], kek: &[u8]) -> Result, Box> { + const AES_WRAP_BLOCK_SIZE: usize = 8; + + // RFC 5649 requires ciphertext to be at least 16 bytes and a multiple of 8 bytes + if ciphertext.len() < 16 || !ciphertext.len().is_multiple_of(AES_WRAP_BLOCK_SIZE) { + return Err("Invalid ciphertext size for AES Key Unwrap".into()); + } + + // Select cipher based on KEK size + let cipher: &CipherRef = match kek.len() { + 16 => Cipher::aes_128_wrap_pad(), + 24 => Cipher::aes_192_wrap_pad(), + 32 => Cipher::aes_256_wrap_pad(), + _ => { + return Err(format!( + "Invalid KEK size: {} bytes. Expected 16, 24, or 32", + kek.len() + ) + .into()); + } + }; + let mut ctx = CipherCtx::new()?; + ctx.decrypt_init(Some(cipher), Some(kek), None)?; + + // Allocate output buffer with extra space (defensive maneuver - the final result will be truncated to the actual size) + let mut plaintext = vec![0_u8; ciphertext.len() + 16]; + + let mut written = ctx.cipher_update(ciphertext, Some(&mut plaintext))?; + written += ctx.cipher_final(&mut plaintext[written..])?; + + // Truncate to actual output size + plaintext.truncate(written); + + Ok(plaintext) +} + +/// Unwrap (decrypt) the given ciphertext using `RSA_AES_KEY_WRAP_SHA_256` +/// This is a two-step unwrapping process: +/// 1. RSA-OAEP with SHA-256 unwraps the ephemeral AES key +/// 2. AES Key Wrap (RFC 5649) unwraps the actual key material +pub(crate) fn rsa_aes_key_wrap_sha256_unwrap( + ciphertext: &[u8], + private_key: &PKey, +) -> Result, Box> { + // The ciphertext structure is: [RSA-encrypted AES key | AES-wrapped key material] + // RSA-encrypted part size equals the RSA key size in bytes + let rsa_key_size = private_key.size(); + + if ciphertext.len() <= rsa_key_size { + return Err("Ciphertext too short for RSA_AES_KEY_WRAP".into()); + } + + // Split the ciphertext into RSA-encrypted AES key and AES-wrapped key material + let (encrypted_aes_key, wrapped_key_material) = ciphertext.split_at(rsa_key_size); + + // Step 1: Unwrap the ephemeral AES key using RSA-OAEP with SHA-256 + let aes_key = rsaes_oaep_sha256_unwrap(encrypted_aes_key, private_key)?; + + // Step 2: Unwrap the key material using AES Key Wrap (RFC 5649) + let unwrapped_key = aes_key_unwrap(wrapped_key_material, &aes_key)?; + + Ok(unwrapped_key) +} diff --git a/crate/server/src/main.rs b/crate/server/src/main.rs index 8caddd145..07345b8b9 100644 --- a/crate/server/src/main.rs +++ b/crate/server/src/main.rs @@ -322,6 +322,8 @@ rolling_log_name = "kms_log" enable_metering = false environment = "development" ansi_colors = false + +[kmip.allowlists] "#; assert_eq!(toml_string.trim(), toml::to_string(&config).unwrap().trim()); diff --git a/documentation/docs/aws/byok.md b/documentation/docs/aws/byok.md new file mode 100644 index 000000000..ddb1e73df --- /dev/null +++ b/documentation/docs/aws/byok.md @@ -0,0 +1,239 @@ +# AWS KMS - Bring Your Own Key (BYOK) + +Cosmian KMS provides an `aws byok` command in its CLI (also available in the ui) to facilitate the import of an AWS wrapping key (KEK) in Cosmian KMS, and the export of the wrapped keys for direct import in AWS KMS. To use the AWS KMS terminology, the key that will be created in the [Cosmian KMS](https://cosmian.com/data-protection-suite/cosmian-kms/) will be called the _external key material_ as stated in the [AWS KMS docs](https://docs.aws.amazon.com/kms/latest/developerguide/importing-keys-conceptual.html). + +The **key material** refers to the actual cryptographic key bytes that form the basis of a KMS key. While AWS KMS keys include additional metadata, policies, and access controls, the BYOK process allows Cosmian KMS users to maintain full control over key generation while leveraging AWS KMS's infrastructure for other usages. + +## Table of Contents + +[TOC] + +## Overview + +Since AWS KMS is a managed service where private key material never leaves AWS HSMs. The key import process requires: + +1. Creating a KMS key with `EXTERNAL` origin (no key material) +2. Download the wrapping public key and import token from AWS +3. Wrap your key material using Cosmian KMS +4. Import the wrapped key material into AWS KMS + +Supported wrapping algorithms: + +| Wrapping Algorithm | Description | Supported Key Material Types | +|-------------------|-------------|------------------------------| +| **RSAES_OAEP_SHA_256**
**RSAES_OAEP_SHA_1** | The RSA encryption algorithm with Optimal Asymmetric Encryption Padding (OAEP) with SHA-256 or SHA-1 hash function. | • 256-bit AES Symmetric keys
• HMAC keys
• Asymmetric ECC private keys* | +| **RSA_AES_KEY_WRAP_SHA_256**
**RSA_AES_KEY_WRAP_SHA_1** | Hybrid wrapping (RSA + AES Key Wrap) with SHA-256 or SHA-1 hash function. | • Asymmetric RSA private keys
• Asymmetric ECC private keys | + +**Wrapping Key Specs :** + +- RSA_2048 (***Note**: cannot be used to wrap _ECC_NIST_P521_ keys with _RSAES_OAEP_SHA_*_ ) +- RSA_3072 +- RSA_4096 + +⚠️ **WARNING:** Invalid combinations of wrapping algorithms, key material types may lead to errors. Ensure that your selected key material type is supported by the chosen wrapping algorithm and that the wrapping key spec is compatible with both. + + +## Prerequisites + +- An active AWS account +- Either : AWS CLI installed and configured on your machine (**recommended**) or an access to AWS Management Console and open the AWS Key Management Service (AWS KMS) console at [https://console.aws.amazon.com/kms](https://console.aws.amazon.com/kms). +- A running [Cosmian KMS](https://docs.cosmian.com/key_management_system/quick_start/) instance. +- Either : [Cosmian KMS CLI](https://docs.cosmian.com/cosmian_cli/installation/) installed and configured on your machine or an access to the [Cosmian KMS UI](../ui.md) of your deployed KMS instance. +- Any tool to convert base64 values to their binary counterparts (e.g. [openssl](https://openssl.org/), python, etc). + +## Creating an AES key and importing it using the AWS CLI and the Cosmian CLI : + +### 1. Create a KMS key with `EXTERNAL` origin + +To use the AWS KMS API to create a symmetric encryption KMS key with no key material, send a CreateKey request with the Origin parameter set to EXTERNAL : + +```bash + aws kms create-key --origin EXTERNAL +``` + +If successful, the output should look like : + +```json +{ + "KeyMetadata": { + "AWSAccountId": "447182645454", + "KeyId": "350e35ef-ac51-4dbb-82a4-9bc50b0ea42b", + "Arn": "arn:aws:kms:eu-west-3:447182645454:key/350e35ef-ac51-4dbb-82a4-9bc50b0ea42b", + "CreationDate": "2026-02-18T15:05:59.358000+01:00", + "Enabled": false, + "Description": "", + "KeyUsage": "ENCRYPT_DECRYPT", + "KeyState": "PendingImport", + "Origin": "EXTERNAL", + "KeyManager": "CUSTOMER", + "CustomerMasterKeySpec": "SYMMETRIC_DEFAULT", + "KeySpec": "SYMMETRIC_DEFAULT", + "EncryptionAlgorithms": [ + "SYMMETRIC_DEFAULT" + ], + "MultiRegion": false + } +} +``` + +> **Copy the key ARN, you will need it in the next step.** In this example, the key ARN is `arn:aws:kms:eu-west-3:447182645454:key/350e35ef-ac51-4dbb-82a4-9bc50b0ea42b`. + +**Note**: If no key spec is specified, a symmetric key is created by default. To create a different key, pass the `key-spec` argument like below for an _ECC_NIST_P384_ key : + +```bash + aws kms create-key \ + --origin EXTERNAL \ + --key-spec ECC_NIST_P384 \ + --description "External NIST-P384 key for signing" +``` + +### 2. Create a symmetric key in Cosmian KMS + +```bash +./cosmian kms sym keys create symmetric_key_test1 +``` + +**Response:** +``` +The symmetric key was successfully generated. + Unique identifier: symmetric_key_test1 +``` + +### 3. Download wrapping public key and import token from AWS + +After you create a AWS KMS key with no key material, download a wrapping public key and an import token for that KMS key by using the AWS KMS console or the GetParametersForImport API. The wrapping public key and import token are an indivisible set that must be used together. + +A very detailed example on how to do this is [detailed on this page, please refer to it for more info](https://docs.aws.amazon.com/kms/latest/developerguide/importing-keys-get-public-key-and-token.html). + +The command you should use is the following : + +```bash +aws kms get-parameters-for-import \ + --key-id arn:aws:kms:eu-west-3:447182645454:key/350e35ef-ac51-4dbb-82a4-9bc50b0ea42b \ + --wrapping-algorithm RSAES_OAEP_SHA_256 \ + --wrapping-key-spec RSA_4096 +``` + +**Response:** +```json +{ + "KeyId": "arn:aws:kms:eu-west-3:447182645454:key/350e35ef-ac51-4dbb-82a4-9bc50b0ea42b", + "ImportToken": "", + "PublicKey": "", + "ParametersValidTo": "2026-02-19T15:07:13.227000+01:00" +} +``` + +As mentioned in the [AWS documentation for importing key material](https://docs.aws.amazon.com/kms/latest/developerguide/importing-keys-get-public-key-and-token.html#importing-keys-get-public-key-and-token-api), you will have to convert the base64 values to their binary counterparts before importing them. This can be done with the method of your choice, the most straightforward method (If you have [openssl](https://openssl.org/) on your cli), would be to copy the token and paste it with the following command to get the file `token.bin` : + +```bash +echo -n "" | openssl enc -d -base64 -A -out token.bin +``` + +In a similar manner, you can use the command if you want to keep your public key as a binary blob : + +```bash +echo -n "" | openssl enc -d -base64 -A -out kek.bin +``` + +### 4. Import the AWS KEK into Cosmian KMS + +```bash +./cosmian kms aws byok import \ + --kek-base64 "" \ + --wrapping-algorithm RSAES_OAEP_SHA_256 \ + --key-arn "" \ + --key-id aws_kek_1 +``` + +Feel free to change the key id to whatever you want, we will call the kek `aws_kek_1` in this example. + +#### Alternative : importing the kek as file + + +```bash +./cosmian kms aws byok import \ + --kek-file kek.bin \ + --wrapping-algorithm RSAES_OAEP_SHA_256 \ + --key-arn arn:aws:kms:eu-west-3:447182645454:key/350e35ef-ac51-4dbb-82a4-9bc50b0ea42b \ + --key-id aws_kek_1 +``` + +**Response:** +``` +The PublicKey in file /tmp/ca9f45ad-8596-45a6-bc57-5591e662cb61 was successfully imported with id: aws_kek_1. + Unique identifier: aws_kek_1 + Tags: + - aws + - wrapping_algorithm:RsaesOaepSha256 + - key_arn:arn:aws:kms:eu-west-3:447182645454:key/350e35ef-ac51-4dbb-82a4-9bc50b0ea42b +``` + + +### 5. Export the wrapped key material from Cosmian KMS + +```bash +./cosmian kms aws byok export \ + symmetric_key_test1 \ + kek_test1 \ + token.bin \ # optional, does not need the actual token, only uses the path to display the aws command + EncryptedKeyMaterial.bin # optional, but if not specified, the cli will return the base64 encoded encrypted key material +``` + +**Response:** +``` +The encrypted key material was successfully written to 512 for key symmetric_key_test1. + +To import into AWS KMS using the API, run: +aws kms import-key-material \ +--key-id arn:aws:kms:eu-west-3:447182645454:key/350e35ef-ac51-4dbb-82a4-9bc50b0ea42b \ +--encrypted-key-material fileb://EncryptedKeyMaterial.bin \ +--import-token fileb://token.bin \ +--expiration-model KEY_MATERIAL_DOES_NOT_EXPIRE +``` + +### 6. Import the wrapped key material into AWS KMS + +If you have filled all the for the fields on the previous step, you can directly use the command that the cosmian cli automatically generated for you : + +```bash +aws kms import-key-material \ + --key-id arn:aws:kms:eu-west-3:447182645454:key/350e35ef-ac51-4dbb-82a4-9bc50b0ea42b \ + --encrypted-key-material fileb://EncryptedKeyMaterial.bin \ + --import-token fileb://token.bin \ + --expiration-model KEY_MATERIAL_DOES_NOT_EXPIRE +``` + +**Response:** +```json +{ + "KeyId": "arn:aws:kms:eu-west-3:447182645454:key/350e35ef-ac51-4dbb-82a4-9bc50b0ea42b", + "KeyMaterialId": "60a5374a2457941eaf5c26b75fea0236bcdc5ddbe70c4aa15046e6fdda49e334" +} +``` + +Receiving this response means that the key material has been successfully imported into AWS KMS. + +## Creating an AES key and importing it using the AWS CLI and the Cosmian CLI : + +For this example, we will create an 2048 bits RSA key material, wrapped using a 4096 kek with RSAES_OAEP_SHA_256. + +First, Sign in to the AWS Management Console and open the AWS Key Management Service (AWS KMS) console at [https://console.aws.amazon.com/kms](https://console.aws.amazon.com/kms) and complete the neccessary steps to [create a KMS key with external key material](). Be mindful to provide the correct [key spec](https://docs.aws.amazon.com/kms/latest/developerguide/symm-asymm-choose-key-spec.html) - otherwise the console will expect a symmetric key by default. For this example, we will use `RSA_2048`. + +The next step is to [download the wrapping public key and import token](https://docs.aws.amazon.com/kms/latest/developerguide/importing-keys-get-public-key-and-token.html#importing-keys-get-public-key-and-token-console). **Be mindful that an RSA_AES_KEY_WRAP_SHA_* wrapping algorithm is required for wrapping RSA private key material (except in China Regions).** Chosing `RSAES_OAEP_SHA_256` will work for this example. + +Once this is done, create your key on the cosmian KMS like follow, we call it `rsa_key_material` : + +![Create an RSA key in Cosmian KMS](create_rsa.png) + +Then, navigate to the **AWS** - **Import Kek** section and fill with the adequate data. In this example, we paste the kek in the base64 format for convenience, and call it `aws_kek_2`. + +![Import the AWS KEK into Cosmian KMS](import_kek.png) + +Finally, export the wrapped key material from Cosmian KMS to import it into AWS KMS. + +![Export the wrapped key material from Cosmian KMS](export_key_material.png) + +We named the file to export `EncryptedKeyMaterial.bin`. You can import the wrapped key material using the AWS CLI or the AWS Management Console. + +An in depth explanation of the import process can be found in the [AWS documentation to Import key material (console)](https://docs.aws.amazon.com/kms/latest/developerguide/importing-keys-import-key-material.html#importing-keys-import-key-material-console). diff --git a/documentation/docs/aws/create_rsa.png b/documentation/docs/aws/create_rsa.png new file mode 100644 index 000000000..78df1ad9f Binary files /dev/null and b/documentation/docs/aws/create_rsa.png differ diff --git a/documentation/docs/aws/export_key_material.png b/documentation/docs/aws/export_key_material.png new file mode 100644 index 000000000..8e7f6a277 Binary files /dev/null and b/documentation/docs/aws/export_key_material.png differ diff --git a/documentation/docs/aws_fargate.md b/documentation/docs/aws/fargate.md similarity index 100% rename from documentation/docs/aws_fargate.md rename to documentation/docs/aws/fargate.md diff --git a/documentation/docs/aws/import_kek.png b/documentation/docs/aws/import_kek.png new file mode 100644 index 000000000..7d56ac012 Binary files /dev/null and b/documentation/docs/aws/import_kek.png differ diff --git a/documentation/docs/index.md b/documentation/docs/index.md index 170150ce7..b1144dd77 100644 --- a/documentation/docs/index.md +++ b/documentation/docs/index.md @@ -13,10 +13,10 @@ The **Cosmian KMS** is a high-performance, [**source available**](https://github ## Standards' compliance - [FIPS 140-3](./fips.md) mode -- KMIP support (versions 1.0-1.4, 2.0-2.1) in both binary and JSON formats - see [KMIP documentation](./kmip/index.md) -- [HSM support](./hsms/index.md) for Trustway Proteccio & Crypt2Pay, Utimaco general purpose, Nitrokey HSM 2, Smartcard HSMs, etc. with KMS keys wrapped by the HSM -- Developed in Rust, a memory safe language, with the source code available on [GitHub](https://github.com/Cosmian/kms) -- 100% developed in the European Union +- KMIP support (versions 1.0-1.4, 2.0-2.1) in both binary and JSON formats - see [KMIP documentation](./kmip/index.md). +- [HSM support](./hsms/index.md) for Trustway Proteccio & Crypt2Pay, Utimaco general purpose, Nitrokey HSM 2, Smartcard HSMs, etc. with KMS keys wrapped by the HSM. +- Developed in Rust, a memory safe language, with the source code available on [GitHub](https://github.com/Cosmian/kms). +- 100% developed in the European Union. ## Modern technology @@ -32,6 +32,7 @@ The **Cosmian KMS** is a high-performance, [**source available**](https://github - **Cloud integrations**: - [Azure BYOK](./azure/byok.md) - [GCP CSEK](./google_gcp/csek.md) and [Google CMEK](./google_gcp/cmek.md) + - [AWS BYOK](./aws/byok.md) and [AWS Fargate](./aws/fargate.md) - ... - **Workplace security**: - [Google Workspace Client Side Encryption (CSE)](./google_cse/index.md) @@ -78,6 +79,8 @@ token authentication. ![Cosmian KMS UI](./images/kms-ui.png) +The UI can be [fully customized](./ui_branding.md) to match your organization's branding. + ## Client CLI The [Cosmian CLI](../cosmian_cli/index.md) provides a powerful command-line interface for managing the server, handling keys, and performing encryption/decryption operations. It features integrated help and is available for multiple operating systems. diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 03c33d3a9..d9a9a33b4 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -74,7 +74,9 @@ nav: - Other HSMs: hsms/other_hsms.md - Integrations: - API Endpoints: api.md - - AWS ECS Fargate: aws_fargate.md + - AWS: + - ECS Fargate: aws/fargate.md + - BYOK (Bring Your Own Key): aws/byok.md - Azure: - BYOK (Bring Your Own Key): azure/byok.md - Google GCP: diff --git a/ui/eslint.config.js b/ui/eslint.config.js index 092408a9f..34a6099eb 100644 --- a/ui/eslint.config.js +++ b/ui/eslint.config.js @@ -4,7 +4,7 @@ import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' import tseslint from 'typescript-eslint' -export default tseslint.config( +export default tseslint.config( // TODO: must be updated - this signature is deprecated { ignores: ['dist'] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 1b7397f8b..5f2718fc5 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -49,6 +49,8 @@ import SymmetricDecryptForm from "./SymmetricDecrypt"; import SymmetricEncryptForm from "./SymmetricEncrypt"; import { AuthMethod, fetchAuthMethod, fetchIdToken, getNoTTLVRequest } from "./utils"; import init from "./wasm/pkg"; +import ImportAwsKekForm from "./AwsImportKek"; +import AwsExportKeyMaterialForm from "./AwsExportKeyMaterial"; type AppContentProps = { isDarkMode: boolean; @@ -203,6 +205,10 @@ const AppContent: React.FC = ({isDarkMode, setIsDarkMode}) => { }/> }/> + + } /> + } /> + }/> }/> diff --git a/ui/src/AwsExportKeyMaterial.tsx b/ui/src/AwsExportKeyMaterial.tsx new file mode 100644 index 000000000..6007e7c02 --- /dev/null +++ b/ui/src/AwsExportKeyMaterial.tsx @@ -0,0 +1,226 @@ +import { Button, Card, Form, Input, Space } from "antd"; +import React, { useEffect, useRef, useState } from "react"; +import { useAuth } from "./AuthContext"; +import { downloadFile, sendKmipRequest } from "./utils"; +import * as wasm from "./wasm/pkg/cosmian_kms_client_wasm"; +import ExternalLink from "./components/ExternalLink"; + +const getTags = (attributes: Map): string[] => { + const vendor_attributes: Array> | undefined = attributes.get("vendor_attributes"); + if (typeof vendor_attributes !== "undefined") { + const attrs_value_map: Map | undefined = (vendor_attributes as Array>) + .find((attribute: Map) => { + return attribute.get("AttributeName") === "tag"; + }) + ?.get("AttributeValue"); + if (typeof attrs_value_map === "undefined") { + return []; + } + const tags_string = (attrs_value_map as Map).get("_c"); + if (tags_string) { + try { + return JSON.parse(tags_string); + } catch (error) { + console.error("Error parsing tags JSON:", error); + return []; + } + } else { + return []; + } + } + return []; +}; + +interface AwsExportKeyMaterialFormData { + wrappedKeyId: string; + kekId: string; + tokenFile?: string; + byokFile?: string; +} + +const AwsExportKeyMaterialForm: React.FC = () => { + const [form] = Form.useForm(); + const [res, setRes] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + const { idToken, serverUrl } = useAuth(); + const responseRef = useRef(null); + + useEffect(() => { + if (res && responseRef.current) { + responseRef.current.scrollIntoView({ behavior: "smooth" }); + } + }, [res]); + + const onFinish = async (values: AwsExportKeyMaterialFormData) => { + setIsLoading(true); + setRes(undefined); + try { + // Step 1: Get KEK attributes to retrieve AWS tags + const getAttrsRequest = wasm.get_attributes_ttlv_request_with_options(values.kekId, true); + const attrsResultStr = await sendKmipRequest(getAttrsRequest, idToken, serverUrl); + + if (!attrsResultStr) { + setRes("Failed to retrieve KEK attributes"); + return; + } + + const allAttributes = [ + "activation_date", + "cryptographic_algorithm", + "cryptographic_length", + "key_usage", + "key_format_type", + "object_type", + "vendor_attributes", + "public_key_id", + "private_key_id", + ]; + const attributes = await wasm.parse_get_attributes_ttlv_response(attrsResultStr, allAttributes); + + const tags = getTags(attributes); + + if (!tags.includes("aws")) { + setRes("The KEK is not an AWS Key Encryption Key: missing 'aws' tag. Import it using the Import KEK command."); + return; + } + + const keyArnTag = tags.find((t: string) => t.startsWith("key_arn:")); + const keyArn = keyArnTag ? keyArnTag.substring(8) : undefined; + + const wrappingAlgTag = tags.find((t: string) => t.startsWith("wrapping_algorithm:")); + if (!wrappingAlgTag) { + setRes("The KEK is not an AWS Key Encryption Key: wrapping algorithm not found. Import it using the Import KEK command."); + return; + } + const wrappingAlgorithm = wrappingAlgTag.substring(19); + + // Step 2: Export the wrapped key using the KEK + const exportRequest = wasm.export_ttlv_request( + values.wrappedKeyId, // Key ID to wrap + true, // Unwrap flag + "raw", // Key format (raw bytes) + values.kekId, // Wrapping key ID + wrappingAlgorithm, // Wrapping algorithm + ); + + const exportResultStr = await sendKmipRequest(exportRequest, idToken, serverUrl); + + if (!exportResultStr) { + setRes("Failed to export wrapped key"); + return; + } + + const wrappedKeyData = await wasm.parse_export_ttlv_response(exportResultStr, "raw"); + + let wrappedKeyBytes: Uint8Array; + if (wrappedKeyData instanceof Uint8Array) { + wrappedKeyBytes = wrappedKeyData; + } else if (typeof wrappedKeyData === "string") { + const binaryString = atob(wrappedKeyData); + wrappedKeyBytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + wrappedKeyBytes[i] = binaryString.charCodeAt(i); + } + } else { + setRes("Unexpected wrapped key format"); + return; + } + + // Step 3: Generate output + if (values.byokFile) { + // Download as file + downloadFile(wrappedKeyBytes, values.byokFile, "application/octet-stream"); + + // Build AWS CLI command + const awsCommand = `aws kms import-key-material \\ + --key-id ${keyArn || ""} \\ + --encrypted-key-material fileb://${values.byokFile} \\ + --import-token fileb://${values.tokenFile || ""} \\ + --expiration-model KEY_MATERIAL_DOES_NOT_EXPIRE`; + + setRes( + `The encrypted key material (${wrappedKeyBytes.length} bytes) was successfully written to ${values.byokFile} for key ${values.wrappedKeyId}.\n\nTo import into AWS KMS using the CLI, you can run:\n\n${awsCommand}`, + ); + } else { + // Display as base64 + const b64Key = btoa(String.fromCharCode(...wrappedKeyBytes)); + setRes(`Wrapped key material (base64-encoded):\n\n${b64Key}`); + } + } catch (e) { + setRes(`Error exporting key material: ${e}`); + console.error("Error exporting key material:", e); + } finally { + setIsLoading(false); + } + }; + + return ( +
+

Export AWS Key Material

+
+

Wrap a Cosmian KMS key with an AWS KMS wrapping key and generate the key material to be imported into.

+

The KEK must be previously imported using the Import KEK command.

+

+ See:{" "} + + AWS KMS Import Key Material + +

+
+
+ + +

Key Identifiers

+ + + + + + +
+ +

Output Options (Optional)

+ + + + + + +
+ + + +
+
+ {res && ( +
+ +
{res}
+
+
+ )} +
+ ); +}; + +export default AwsExportKeyMaterialForm; diff --git a/ui/src/AwsImportKek.tsx b/ui/src/AwsImportKek.tsx new file mode 100644 index 000000000..a05cfc7d2 --- /dev/null +++ b/ui/src/AwsImportKek.tsx @@ -0,0 +1,239 @@ +import { UploadOutlined } from "@ant-design/icons"; +import { Button, Card, Form, Input, Select, Space, Upload, Tabs } from "antd"; +import React, { useEffect, useRef, useState } from "react"; +import { useAuth } from "./AuthContext"; +import { sendKmipRequest } from "./utils"; +import * as wasm from "./wasm/pkg"; +import ExternalLink from "./components/ExternalLink"; + +interface ImportAwsKekFormData { + kekFile?: Uint8Array; + kekBase64?: string; + keyArn?: string; + wrappingAlgorithm: WrappingAlgorithm; + keyId?: string; +} + +type KeyImportResponse = { + UniqueIdentifier: string; +}; + +// These values MUST match the WrappingAlgorithm enum's strum kebab-case serialization +// in crate/client_utils/src/export_utils.rs (used by wasm.export_ttlv_request). +export enum WrappingAlgorithm { + RsaOaepSha1 = "rsa-oaep-sha1", + RsaOaepSha256 = "rsa-oaep", + RsaAesKeyWrapSha1 = "rsa-aes-key-wrap-sha1", + RsaAesKeyWrapSha256 = "rsa-aes-key-wrap", +} + +const WRAPPING_ALGORITHMS = [ + { label: "RSAES_OAEP_SHA_1", value: WrappingAlgorithm.RsaOaepSha1 }, + { label: "RSAES_OAEP_SHA_256", value: WrappingAlgorithm.RsaOaepSha256 }, + { label: "RSA_AES_KEY_WRAP_SHA_1", value: WrappingAlgorithm.RsaAesKeyWrapSha1 }, + { label: "RSA_AES_KEY_WRAP_SHA_256", value: WrappingAlgorithm.RsaAesKeyWrapSha256 }, +]; + +const ImportAwsKekForm: React.FC = () => { + const [form] = Form.useForm(); + const [res, setRes] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + const { idToken, serverUrl } = useAuth(); + const responseRef = useRef(null); + const [inputType, setInputType] = useState<"file" | "base64">("file"); + + useEffect(() => { + if (res && responseRef.current) { + responseRef.current.scrollIntoView({ behavior: "smooth" }); + } + }, [res]); + + const onFinish = async (values: ImportAwsKekFormData) => { + setIsLoading(true); + setRes(undefined); + try { + const tags = ["aws", `wrapping_algorithm:${values.wrappingAlgorithm}`]; + // only include key_arn if provided: + if (values.keyArn) { + tags.push(`key_arn:${values.keyArn}`); + } + const keyUsage = ["WrapKey", "Encrypt"]; + + let kekData: Uint8Array | undefined = undefined; + let kekFormat: string | undefined = undefined; + + if (inputType === "file" && values.kekFile) { + kekData = values.kekFile; + kekFormat = "pkcs8-pub"; + } else if (inputType === "base64" && values.kekBase64) { + // Decode base64 to Uint8Array + const binary = atob(values.kekBase64.replace(/\s/g, "")); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + kekData = bytes; + kekFormat = "pkcs8-pub"; + } else { + setRes("Please provide the KEK as a file or base64 string."); + setIsLoading(false); + return; + } + + const request = wasm.import_ttlv_request( + values.keyId || null, // Custom key ID + kekData, // Key bytes + kekFormat, // Format type + null, // Public key ID + null, // Private key ID + null, // Certificate ID + false, // Unwrap flag + true, // Replace existing + tags, + keyUsage, // Key usage + null, // Wrapping key ID + ); + + const result_str = await sendKmipRequest(request, idToken, serverUrl); + if (result_str) { + const result: KeyImportResponse = await wasm.parse_import_ttlv_response(result_str); + setRes(`AWS KEK has been successfully imported - Key ID: ${result.UniqueIdentifier}`); + } + } catch (e) { + setRes(`Error importing AWS KEK: ${e}`); + console.error("Error importing AWS KEK:", e); + } finally { + setIsLoading(false); + } + }; + + return ( +
+

Import AWS Key Encryption Key (KEK)

+
+

Import an RSA Public Key Encryption Key (KEK) generated by AWS KMS.

+

+ The KEK should be provided as a raw binary file (recommended) or as a base64-encoded string. +

+ {/* prettier-ignore */} +

+ See AWS KMS developer documentation for {" "} + + downloading the wrapping public key and import token + . +

+
+
+ + + {/* prettier-ignore */} + setInputType(key as "file" | "base64")} + items={[ + { + key: "file", + label: "Upload KEK File (raw binary)", + children: ( + + { + const reader = new FileReader(); + reader.onload = (e) => { + const content = e.target?.result; + if (content instanceof ArrayBuffer) { + const bytes = new Uint8Array(content); + form.setFieldsValue({ kekFile: bytes }); + } + }; + reader.readAsArrayBuffer(file); + return false; + }} + maxCount={1} + > + + + + ), + }, + { + key: "base64", + label: "Paste Base64-encoded KEK", + children: ( + + Paste the base64-encoded KEK (as exported from{" "} + + AWS KMS API + ) + + } + > + + + ), + }, + ]} + /> + + +

Wrapping Algorithm

+ + + +
+ +

KMS Key ID

+ + + +
+ + + +
+
+ {res && ( +
+ {res} +
+ )} +
+ ); +}; + +export default ImportAwsKekForm; diff --git a/ui/src/AzureExportByok.tsx b/ui/src/AzureExportByok.tsx index 31b710e59..3f3a6e345 100644 --- a/ui/src/AzureExportByok.tsx +++ b/ui/src/AzureExportByok.tsx @@ -1,22 +1,24 @@ -import {Button, Card, Form, Input, Space} from "antd"; -import React, {useEffect, useRef, useState} from "react"; -import {useAuth} from "./AuthContext"; -import {downloadFile, sendKmipRequest} from "./utils"; +import { Button, Card, Form, Input, Space } from "antd"; +import React, { useEffect, useRef, useState } from "react"; +import { useAuth } from "./AuthContext"; +import { downloadFile, sendKmipRequest } from "./utils"; import { export_ttlv_request, get_attributes_ttlv_request_with_options, parse_export_ttlv_response, - parse_get_attributes_ttlv_response + parse_get_attributes_ttlv_response, } from "./wasm/pkg"; const getTags = (attributes: Map): string[] => { const vendor_attributes: Array> | undefined = attributes.get("vendor_attributes"); if (typeof vendor_attributes !== "undefined") { - const attrs_value_map: Map | undefined = (vendor_attributes as Array>).find((attribute: Map) => { - return attribute.get("AttributeName") === "tag"; - })?.get("AttributeValue"); + const attrs_value_map: Map | undefined = (vendor_attributes as Array>) + .find((attribute: Map) => { + return attribute.get("AttributeName") === "tag"; + }) + ?.get("AttributeValue"); if (typeof attrs_value_map === "undefined") { - return [] + return []; } const tags_string = (attrs_value_map as Map).get("_c"); if (tags_string) { @@ -31,9 +33,8 @@ const getTags = (attributes: Map): string[] => { } } - - return [] -} + return []; +}; interface ExportAzureBYOKFormData { wrappedKeyId: string; @@ -45,12 +46,12 @@ const ExportAzureBYOKForm: React.FC = () => { const [form] = Form.useForm(); const [res, setRes] = useState(undefined); const [isLoading, setIsLoading] = useState(false); - const {idToken, serverUrl} = useAuth(); + const { idToken, serverUrl } = useAuth(); const responseRef = useRef(null); useEffect(() => { if (res && responseRef.current) { - responseRef.current.scrollIntoView({behavior: "smooth"}); + responseRef.current.scrollIntoView({ behavior: "smooth" }); } }, [res]); @@ -85,17 +86,13 @@ const ExportAzureBYOKForm: React.FC = () => { const tags = getTags(attributes); if (!tags.includes("azure")) { - setRes( - "The KEK is not an Azure Key Encryption Key: missing 'azure' tag. Import it using the Import KEK command." - ); + setRes("The KEK is not an Azure Key Encryption Key: missing 'azure' tag. Import it using the Import KEK command."); return; } const kidTag = tags.find((t: string) => t.startsWith("kid:")); if (!kidTag) { - setRes( - "The KEK is not an Azure Key Encryption Key: Azure kid not found. Import it using the Import KEK command." - ); + setRes("The KEK is not an Azure Key Encryption Key: Azure kid not found. Import it using the Import KEK command."); return; } @@ -110,7 +107,7 @@ const ExportAzureBYOKForm: React.FC = () => { true, // unwrap - export the key in wrapped form "raw", // key_format - raw bytes values.kekId, // wrap_key_id - the KEK to wrap with - "rsa-aes-key-wrap-sha1" // wrapping_algorithm + "rsa-aes-key-wrap-sha1", // wrapping_algorithm ); const exportResultStr = await sendKmipRequest(exportRequest, idToken, serverUrl); @@ -178,8 +175,7 @@ const ExportAzureBYOKForm: React.FC = () => {

Export Azure BYOK File

-

Wrap a KMS key with an Azure Key Encryption Key (KEK) and generate a .byok file for Azure Key Vault - import.

+

Wrap a KMS key with an Azure Key Encryption Key (KEK) and generate a .byok file for Azure Key Vault import.

The KEK must be previously imported using the Import KEK command.

See:{" "} @@ -195,25 +191,25 @@ const ExportAzureBYOKForm: React.FC = () => {

- +

Key Identifiers (required)

- + - +
@@ -224,13 +220,12 @@ const ExportAzureBYOKForm: React.FC = () => { label="BYOK Filename" help="The filename for the exported .byok file. If not specified, it will be named .byok" > - + - @@ -246,5 +241,4 @@ const ExportAzureBYOKForm: React.FC = () => { ); }; - export default ExportAzureBYOKForm; diff --git a/ui/src/AzureImportKek.tsx b/ui/src/AzureImportKek.tsx index 82d1f48fd..3fe63d835 100644 --- a/ui/src/AzureImportKek.tsx +++ b/ui/src/AzureImportKek.tsx @@ -1,9 +1,10 @@ -import {UploadOutlined} from "@ant-design/icons"; -import {Button, Card, Form, Input, Space, Upload} from "antd"; -import React, {useEffect, useRef, useState} from "react"; -import {useAuth} from "./AuthContext"; -import {sendKmipRequest} from "./utils"; -import {import_ttlv_request, parse_import_ttlv_response} from "./wasm/pkg"; +import { UploadOutlined } from "@ant-design/icons"; +import { Button, Card, Form, Input, Space, Upload } from "antd"; +import React, { useEffect, useRef, useState } from "react"; +import { useAuth } from "./AuthContext"; +import { sendKmipRequest } from "./utils"; +import { import_ttlv_request, parse_import_ttlv_response } from "./wasm/pkg"; +import ExternalLink from "./components/ExternalLink"; interface ImportAzureKekFormData { kekFile: Uint8Array; @@ -19,12 +20,12 @@ const ImportAzureKekForm: React.FC = () => { const [form] = Form.useForm(); const [res, setRes] = useState(undefined); const [isLoading, setIsLoading] = useState(false); - const {idToken, serverUrl} = useAuth(); + const { idToken, serverUrl } = useAuth(); const responseRef = useRef(null); useEffect(() => { if (res && responseRef.current) { - responseRef.current.scrollIntoView({behavior: "smooth"}); + responseRef.current.scrollIntoView({ behavior: "smooth" }); } }, [res]); @@ -72,29 +73,20 @@ const ImportAzureKekForm: React.FC = () => {

The KEK should be exported from Azure in PKCS#8 PEM format.

See:{" "} - + Azure BYOK Specification - Generate KEK - +

- - + +

KEK File (required)

{ // For PEM files, we need to convert to bytes const encoder = new TextEncoder(); const bytes = encoder.encode(content); - form.setFieldsValue({kekFile: bytes}); + form.setFieldsValue({ kekFile: bytes }); } else if (content instanceof ArrayBuffer) { const bytes = new Uint8Array(content); - form.setFieldsValue({kekFile: bytes}); + form.setFieldsValue({ kekFile: bytes }); } }; reader.readAsText(file); @@ -117,7 +109,7 @@ const ImportAzureKekForm: React.FC = () => { }} maxCount={1} > - +
@@ -127,16 +119,16 @@ const ImportAzureKekForm: React.FC = () => { The Azure Key ID should be in the format: -
+
https://mypremiumkeyvault.vault.azure.net/keys/KEK-BYOK/664f5aa2797a4075b8e36ca4500636d8 } > - +
@@ -147,13 +139,12 @@ const ImportAzureKekForm: React.FC = () => { label="Key ID in KMS" help="The unique ID for this key in the KMS. A random UUID will be generated if not specified." > - + - diff --git a/ui/src/CseInfo.tsx b/ui/src/CseInfo.tsx index 1755e92a6..f1dfc51d8 100644 --- a/ui/src/CseInfo.tsx +++ b/ui/src/CseInfo.tsx @@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useState } from "react"; import { useAuth } from "./AuthContext"; import { getNoTTLVRequest, sendKmipRequest } from "./utils"; import { export_ttlv_request } from "./wasm/pkg/cosmian_kms_client_wasm"; +import ExternalLink from "./components/ExternalLink"; interface CseStatus { server_type: string; @@ -95,14 +96,9 @@ const CseInfo: React.FC = () => {

KACLS URL:{" "} - + {cseStatus.kacls_url} - +

diff --git a/ui/src/Sidebar.tsx b/ui/src/Sidebar.tsx index 7138305c1..ecef4a1ce 100644 --- a/ui/src/Sidebar.tsx +++ b/ui/src/Sidebar.tsx @@ -55,10 +55,10 @@ const Sidebar: React.FC = () => { // Check if item is an Import item const isImportItem = item.key && (item.key.includes("/import") || item.key.includes("/import-") || item.label === "Import"); - // Handle disabled state based on access rights - if (isCreateItem || isImportItem) { - newItem.disabled = !hasCreateAccess; - } + // // Handle disabled state based on access rights + // if (isCreateItem || isImportItem) { + // newItem.disabled = !hasCreateAccess; + // } // Process children recursively if they exist if (newItem.children) { @@ -99,7 +99,9 @@ const Sidebar: React.FC = () => { .findIndex((key: string) => levelKeys[key] === levelKeys[currentOpenKey]); setStateOpenKeys( - openKeys.filter((_, index: number) => index !== repeatIndex).filter((key: string) => levelKeys[key] <= levelKeys[currentOpenKey]) + openKeys + .filter((_, index: number) => index !== repeatIndex) + .filter((key: string) => levelKeys[key] <= levelKeys[currentOpenKey]), ); } else { // close diff --git a/ui/src/components/ExternalLink.tsx b/ui/src/components/ExternalLink.tsx new file mode 100644 index 000000000..21ae306a0 --- /dev/null +++ b/ui/src/components/ExternalLink.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +interface ExternalLinkProps { + href: string; + children: React.ReactNode; + className?: string; +} + +const ExternalLink: React.FC = ({ href, children, className = "text-blue-600 hover:underline" }) => { + return ( + + {children} + + ); +}; + +export default ExternalLink; diff --git a/ui/src/menuItems.tsx b/ui/src/menuItems.tsx index edf92679c..6190ed628 100644 --- a/ui/src/menuItems.tsx +++ b/ui/src/menuItems.tsx @@ -157,7 +157,16 @@ const baseMenu: MenuItem[] = [ collapsedlabel: "Azure", children: [ {key: "azure/import-kek", label: "Import KEK"}, - {key: "azure/export-byok", label: "Export BYOK"}, + {key: "azure/export-key-material", label: "Export BYOK"}, + ], + }, + { + key: "aws", + label: "AWS", + collapsedlabel: "AWS", + children: [ + { key: "aws/import-kek", label: "Import KEK" }, + { key: "aws/export-key-material", label: "Export key material" }, ], }, {