use std::path::PathBuf;
use base64::{Engine, engine::general_purpose};
use clap::Parser;
use cosmian_kms_client::{
ExportObjectParams, KmsClient,
cosmian_kmip::kmip_0::kmip_types::BlockCipherMode,
export_object,
kmip_2_1::{
extra::tagging::SYSTEM_TAG_PRIVATE_KEY,
kmip_attributes::Attributes,
kmip_objects::{Certificate, Object, ObjectType},
kmip_operations::{Certify, GetAttributes},
kmip_types::{
CertificateAttributes, CryptographicAlgorithm, CryptographicParameters, KeyFormatType,
Link, LinkType, LinkedObjectIdentifier, UniqueIdentifier,
},
requests::create_rsa_key_pair_request,
},
reexport::cosmian_kms_client_utils::import_utils::CertificateInputFormat,
};
use cosmian_logger::{debug, info, trace};
use serde::{Deserialize, Serialize};
use super::KEY_PAIRS_ENDPOINT;
use crate::{
actions::kms::{
certificates::import_certificate::ImportCertificateAction,
google::gmail_client::GmailClient,
},
error::KmsCliError,
};
const RSA_4096: usize = 4096;
#[derive(Parser, Clone, Debug)]
#[clap(verbatim_doc_comment)]
pub struct CreateKeyPairsAction {
#[clap(required = true)]
pub user_id: String,
#[clap(long, required = true)]
pub cse_key_id: String,
#[clap(long, short = 's', verbatim_doc_comment, required = true)]
pub subject_name: String,
#[clap(long, short = 'k')]
pub rsa_private_key_id: Option<String>,
#[clap(long, default_value = "false")]
pub sensitive: bool,
#[clap(long, short = 'w', verbatim_doc_comment)]
pub wrapping_key_id: Option<String>,
#[clap(
long,
short = 'i',
conflicts_with_all = ["using-existing-certificate-by-file", "using-existing-certificate-by-id"],
requires = "leaf_certificate_extensions",
group = "leaf-autogenerated"
)]
pub issuer_private_key_id: Option<String>,
#[clap(
long,
short = 'e',
conflicts_with_all = ["using-existing-certificate-by-file", "using-existing-certificate-by-id"],
requires = "issuer_private_key_id",
verbatim_doc_comment
)]
pub leaf_certificate_extensions: Option<PathBuf>,
#[clap(
long,
conflicts_with_all = ["using-existing-certificate-by-file", "leaf-autogenerated"],
group = "using-existing-certificate-by-id",
verbatim_doc_comment
)]
pub leaf_certificate_id: Option<String>,
#[clap(long,
conflicts_with_all = ["using-existing-certificate-by-id", "leaf-autogenerated"],
requires = "leaf_certificate_pkcs12_password",
group = "using-existing-certificate-by-file",
verbatim_doc_comment)]
pub leaf_certificate_pkcs12_file: Option<PathBuf>,
#[clap(
long,
conflicts_with_all = ["leaf_certificate_id", "leaf_certificate_extensions"],
verbatim_doc_comment
)]
pub leaf_certificate_pkcs12_password: Option<String>,
#[clap(long = "days", short = 'd', default_value = "365")]
pub number_of_days: usize,
#[clap(long, default_value = "false")]
pub dry_run: bool,
}
#[derive(Serialize, Deserialize)]
#[expect(non_snake_case)]
struct KeyPairInfo {
pkcs7: String,
privateKeyMetadata: Vec<PrivateKeyMetadata>,
}
#[derive(Serialize, Deserialize)]
#[expect(non_snake_case)]
struct PrivateKeyMetadata {
kaclsKeyMetadata: KaclsKeyMetadata,
}
#[derive(Serialize, Deserialize)]
#[expect(non_snake_case)]
struct KaclsKeyMetadata {
kaclsUri: String,
kaclsData: String,
}
impl CreateKeyPairsAction {
async fn post_keypair(
gmail_client: &GmailClient,
certificate_value: Vec<u8>,
wrapped_private_key: String,
kacls_url: String,
) -> Result<(), KmsCliError> {
let key_pair_info = KeyPairInfo {
pkcs7: pem::encode(&pem::Pem::new(String::from("PKCS7"), certificate_value)),
privateKeyMetadata: vec![PrivateKeyMetadata {
kaclsKeyMetadata: KaclsKeyMetadata {
kaclsUri: kacls_url,
kaclsData: wrapped_private_key,
},
}],
};
let response = gmail_client
.post(KEY_PAIRS_ENDPOINT, serde_json::to_string(&key_pair_info)?)
.await?;
GmailClient::handle_response(response).await
}
#[expect(clippy::print_stdout)]
pub async fn run(&self, kms_rest_client: KmsClient) -> Result<UniqueIdentifier, KmsCliError> {
let gmail_client = GmailClient::new(kms_rest_client.config.clone(), &self.user_id);
let email = &self.user_id;
let kacls_url = kms_rest_client.google_cse_status();
let (private_key_id, public_key_id) = if let Some(id) = &self.rsa_private_key_id {
let attributes_response = kms_rest_client
.get_attributes(GetAttributes {
unique_identifier: Some(UniqueIdentifier::TextString(id.clone())),
attribute_reference: None,
})
.await?;
if attributes_response.attributes.object_type == Some(ObjectType::PrivateKey) {
if let Some(linked_public_key_id) = attributes_response
.attributes
.get_link(LinkType::PublicKeyLink)
{
(id.clone(), linked_public_key_id.to_string())
} else {
return Err(KmsCliError::ServerError(
"Invalid private-key-id - no linked public key found".to_owned(),
));
}
} else {
return Err(KmsCliError::ServerError(
"Invalid private-key-id - must be of PrivateKey type".to_owned(),
));
}
} else {
let created_key_pair = kms_rest_client
.create_key_pair(create_rsa_key_pair_request(
kms_rest_client.config.vendor_id.as_str(),
None,
Vec::<String>::new(),
RSA_4096,
self.sensitive,
self.wrapping_key_id.as_ref(),
)?)
.await?;
(
created_key_pair.private_key_unique_identifier.to_string(),
created_key_pair.public_key_unique_identifier.to_string(),
)
};
println!(
"[{email}] - RSA keypair ID used: private_key {private_key_id} - public_key \
{public_key_id}"
);
let (_, wrapped_private_key, _attributes) = export_object(
&kms_rest_client,
&private_key_id,
ExportObjectParams {
wrapping_key_id: Some(&self.cse_key_id),
wrapping_cryptographic_parameters: Some(CryptographicParameters {
cryptographic_algorithm: Some(CryptographicAlgorithm::AES),
block_cipher_mode: Some(BlockCipherMode::GCM),
..CryptographicParameters::default()
}),
..ExportObjectParams::default()
},
)
.await?;
let wrapped_key_bytes = wrapped_private_key.key_block()?.wrapped_key_bytes()?;
trace!("Determine the certificate to use - either existing or newly created");
let certificate_unique_identifier = match (
&self.leaf_certificate_id,
&self.issuer_private_key_id,
&self.leaf_certificate_extensions,
&self.leaf_certificate_pkcs12_file,
&self.leaf_certificate_pkcs12_password,
) {
(None, Some(issuer_private_key_id), Some(leaf_certificate_extensions), None, None) => {
info!(
"[{email}] - Generating new leaf certificate with extensions file: {:?}",
self.leaf_certificate_extensions
);
let certificate_extensions_bytes =
tokio::fs::read(leaf_certificate_extensions).await?;
let mut attributes = Attributes {
object_type: Some(ObjectType::Certificate),
certificate_attributes: Some(CertificateAttributes::parse_subject_line(
&self.subject_name,
)?),
link: Some(vec![Link {
link_type: LinkType::PrivateKeyLink,
linked_object_identifier: LinkedObjectIdentifier::TextString(
issuer_private_key_id.to_owned(),
),
}]),
..Attributes::default()
};
attributes.set_x509_extension_file(
kms_rest_client.config.vendor_id.as_str(),
certificate_extensions_bytes,
);
attributes.set_requested_validity_days(
kms_rest_client.config.vendor_id.as_str(),
i32::try_from(self.number_of_days).map_err(|_e| {
KmsCliError::Conversion(
"number of days must be a positive integer".to_owned(),
)
})?,
);
debug!("Creating new leaf certificate with attributes: {attributes}");
let certify_request = Certify {
unique_identifier: Some(UniqueIdentifier::TextString(public_key_id)),
attributes: Some(attributes),
..Certify::default()
};
let certificate_unique_identifier = kms_rest_client
.certify(certify_request)
.await
.map_err(|e| {
KmsCliError::ServerError(format!("failed creating certificate: {e:?}"))
})?
.unique_identifier;
println!("[{email}] - certificate ID: {certificate_unique_identifier}");
certificate_unique_identifier
}
(None, None, None, Some(p12_file), Some(p12_password)) => {
info!(
"[{email}] - Import PKCS12 file before using it in Google key pair generation"
);
let import_action = ImportCertificateAction {
certificate_file: Some(p12_file.clone()),
input_format: CertificateInputFormat::Pkcs12,
pkcs12_password: Some(p12_password.clone()),
certificate_id: None,
replace_existing: true,
tags: vec!["google_cse_pkcs12_certificate_import".to_owned()],
..Default::default()
};
let private_unique_identifier =
Box::pin(import_action.run(kms_rest_client.clone()))
.await?
.ok_or_else(|| {
KmsCliError::ServerError(
"failed importing leaf certificate from PKCS12 file".to_owned(),
)
})?;
let certificate_unique_identifier =
private_unique_identifier.replace(SYSTEM_TAG_PRIVATE_KEY, "");
println!("[{email}] - certificate ID: {certificate_unique_identifier}");
UniqueIdentifier::TextString(certificate_unique_identifier)
}
(Some(leaf_cert_id), None, None, None, None) => {
println!("[{email}] - Using existing leaf certificate ID: {leaf_cert_id}");
UniqueIdentifier::TextString(leaf_cert_id.clone())
}
_ => {
return Err(KmsCliError::InvalidRequest(
"Incorrect parameters. Only exclusive options are possible: either \
--leaf_certificate_id argument OR --leaf-certificate-extensions AND \
--issuer_private_key_id OR --leaf-certificate-pkcs12-file AND \
--leaf-certificate-pkcs12-password must be provided"
.to_owned(),
));
}
};
info!("[{email}] - certificate ID used: {certificate_unique_identifier}");
let (_, pkcs7_object, _pkcs7_object_export_attributes) = export_object(
&kms_rest_client,
&certificate_unique_identifier.to_string(),
ExportObjectParams {
key_format_type: Some(KeyFormatType::PKCS7),
..ExportObjectParams::default()
},
)
.await?;
if let Object::Certificate(Certificate {
certificate_value, ..
}) = &pkcs7_object
{
trace!(
"pkcs7_object: {:?}",
general_purpose::STANDARD.encode(certificate_value)
);
trace!(
"wrapped_key_bytes: {:?}",
general_purpose::STANDARD.encode(wrapped_key_bytes.clone())
);
}
if self.dry_run {
println!("Dry run mode - key pair not pushed to Gmail API");
} else {
let email = &self.user_id;
println!("[{email}] - Pushing new keypair to Gmail API");
if let Object::Certificate(Certificate {
certificate_value, ..
}) = pkcs7_object
{
println!("Processing {email:?}.");
Self::post_keypair(
&gmail_client.await?,
certificate_value,
general_purpose::STANDARD.encode(wrapped_key_bytes),
kacls_url.await?.kacls_url,
)
.await?;
println!("Key pair inserted for {email:?}.");
} else {
return Err(KmsCliError::ServerError(format!(
"Error inserting key pair for {email:?} - exported object is not a Certificate"
)));
}
}
Ok(certificate_unique_identifier)
}
}