use std::path::PathBuf;
use cosmian_kms_client::{
cosmian_kmip::{
kmip_2_1::{kmip_objects::Object, kmip_types::LinkType},
ttlv::{TTLV, from_ttlv},
},
kmip_2_1::{
kmip_attributes::Attributes, kmip_objects::Certificate, kmip_types::ValidityIndicator,
},
read_from_json_file, read_object_from_json_ttlv_file,
reexport::cosmian_kms_client_utils::{
export_utils::CertificateExportFormat, import_utils::CertificateInputFormat,
},
};
use cosmian_logger::{debug, info, log_init};
use openssl::{nid::Nid, x509::X509};
use tempfile::TempDir;
use test_kms_server::{TestsContext, start_default_test_kms_server};
use uuid::Uuid;
use x509_parser::{der_parser::oid, prelude::*};
use crate::{
actions::kms::{
certificates::{
Algorithm, certify::CertifyAction, export_certificate::ExportCertificateAction,
import_certificate::ImportCertificateAction,
validate_certificate::ValidateCertificatesAction,
},
rsa::keys::create_key_pair::CreateKeyPairAction,
shared::ExportSecretDataOrKeyAction,
},
error::result::KmsCliResult,
};
pub(crate) async fn import_root_and_intermediate(
ctx: &TestsContext,
) -> KmsCliResult<(String, String, String)> {
let root_ca_id = Box::pin(
ImportCertificateAction {
certificate_file: Some(PathBuf::from("../../test_data/certificates/csr/ca.crt")),
input_format: CertificateInputFormat::Pem,
certificate_id: Some(Uuid::new_v4().to_string()),
replace_existing: true,
..Default::default()
}
.run(ctx.get_owner_client()),
)
.await?
.unwrap();
let intermediate_ca_id = Box::pin(
ImportCertificateAction {
certificate_file: Some(PathBuf::from(
"../../test_data/certificates/csr/intermediate.crt",
)),
input_format: CertificateInputFormat::Pem,
certificate_id: Some(Uuid::new_v4().to_string()),
replace_existing: true,
tags: vec!["root_ca".to_owned()],
..Default::default()
}
.run(ctx.get_owner_client()),
)
.await?
.unwrap();
let intermediate_ca_private_key_id = Box::pin(
ImportCertificateAction {
certificate_file: Some(PathBuf::from(
"../../test_data/certificates/csr/intermediate.p12",
)),
input_format: CertificateInputFormat::Pkcs12,
pkcs12_password: Some("secret".to_owned()),
certificate_id: Some(Uuid::new_v4().to_string()),
replace_existing: true,
tags: vec!["intermediate_ca".to_owned()],
..Default::default()
}
.run(ctx.get_owner_client()),
)
.await?
.unwrap();
Ok((
root_ca_id,
intermediate_ca_id,
intermediate_ca_private_key_id,
))
}
async fn fetch_certificate(
ctx: &TestsContext,
certificate_id: &str,
) -> (Object, Attributes, Vec<u8>) {
let tmp_dir = TempDir::new().unwrap();
let tmp_path = tmp_dir.path();
let exported_cert_file = tmp_path.join("new_cert.pem");
debug!("exporting certificate: new_cert: {:?}", exported_cert_file);
ExportCertificateAction {
certificate_file: exported_cert_file.clone(),
certificate_id: Some(certificate_id.to_owned()),
output_format: CertificateExportFormat::JsonTtlv,
allow_revoked: true,
..Default::default()
}
.run(ctx.get_owner_client())
.await
.unwrap();
let cert = read_object_from_json_ttlv_file(&exported_cert_file).unwrap();
let cert_x509_der = match &cert {
Object::Certificate(Certificate {
certificate_value, ..
}) => {
certificate_value
}
_ => panic!("wrong object type"),
}
.clone();
let cert_x509 = X509::from_der(&cert_x509_der).unwrap();
assert_eq!(
"Test Leaf",
cert_x509
.subject_name()
.entries_by_nid(Nid::COMMONNAME)
.next()
.unwrap()
.data()
.as_utf8()
.unwrap()
.to_string()
);
let ttlv: TTLV = read_from_json_file(&tmp_path.join("new_cert.attributes.json")).unwrap();
let cert_attributes: Attributes = from_ttlv(ttlv).unwrap();
(cert, cert_attributes, cert_x509_der)
}
async fn check_certificate_chain(
ctx: &TestsContext,
issuer_private_key_id: &str,
certificate_id: &str,
) -> (Object, Attributes, Vec<u8>) {
let tmp_dir = TempDir::new().unwrap();
let tmp_path = tmp_dir.path();
let (cert, cert_attributes, cert_x509_der) = fetch_certificate(ctx, certificate_id).await;
let certificate_link = cert_attributes.get_link(LinkType::CertificateLink).unwrap();
let signer_cert_file = tmp_path.join("signer_cert.pem");
ExportCertificateAction {
certificate_file: PathBuf::from(signer_cert_file.to_str().unwrap()),
certificate_id: Some(certificate_link.to_string()),
output_format: CertificateExportFormat::JsonTtlv,
allow_revoked: true,
..Default::default()
}
.run(ctx.get_owner_client())
.await
.unwrap();
let ttlv: TTLV = read_from_json_file(&tmp_path.join("signer_cert.attributes.json")).unwrap();
let signer_attributes: Attributes = from_ttlv(ttlv).unwrap();
let private_key_link = signer_attributes
.get_link(LinkType::PrivateKeyLink)
.unwrap();
assert_eq!(private_key_link.to_string(), issuer_private_key_id);
(cert, cert_attributes, cert_x509_der)
}
fn check_certificate_added_extensions(cert_x509_der: &[u8]) {
let (_, cert_x509) = X509Certificate::from_der(cert_x509_der).unwrap();
let exts_with_x509_parser = cert_x509.extensions();
for ext in exts_with_x509_parser {
info!("\next: {ext:?}");
info!("value is: {:?}", String::from_utf8(ext.value.to_vec()));
}
let bc = exts_with_x509_parser
.iter()
.find(|x| x.oid == oid!(2.5.29.19))
.unwrap();
assert!(!bc.critical);
assert_eq!(
bc.parsed_extension(),
&ParsedExtension::BasicConstraints(BasicConstraints {
ca: false,
path_len_constraint: Some(0)
})
);
let ku: &X509Extension<'_> = exts_with_x509_parser
.iter()
.find(|x| x.oid == oid!(2.5.29.15))
.unwrap();
assert!(!ku.critical);
assert_eq!(
ku.parsed_extension(),
&ParsedExtension::KeyUsage(KeyUsage { flags: 33 })
);
let eku: &X509Extension<'_> = exts_with_x509_parser
.iter()
.find(|x| x.oid == oid!(2.5.29.37))
.unwrap();
assert!(!eku.critical);
assert_eq!(
eku.parsed_extension(),
&ParsedExtension::ExtendedKeyUsage(ExtendedKeyUsage {
any: false,
server_auth: false,
client_auth: false,
code_signing: false,
email_protection: true,
time_stamping: false,
ocsp_signing: false,
other: vec![]
})
);
let crl_dp: &X509Extension<'_> = exts_with_x509_parser
.iter()
.find(|x| x.oid == oid!(2.5.29.31))
.unwrap();
assert!(!crl_dp.critical);
assert_eq!(
crl_dp.parsed_extension(),
&ParsedExtension::CRLDistributionPoints(CRLDistributionPoints {
points: vec![CRLDistributionPoint {
distribution_point: Some(DistributionPointName::FullName(vec![GeneralName::URI(
"https://package.cosmian.com/kms/crl_tests/intermediate.crl.pem"
)])),
reasons: None,
crl_issuer: None
}]
})
);
}
async fn check_certificate_and_public_key_linked(
ctx: &TestsContext,
certificate_id: &str,
certificate_attributes: &Attributes,
) -> (String, Attributes) {
let public_key_link = certificate_attributes
.get_link(LinkType::PublicKeyLink)
.unwrap();
let tmp_dir = TempDir::new().unwrap();
let tmp_path = tmp_dir.path();
let tmp_exported_pubkey = tmp_path.join("exported_pubkey.json");
ExportSecretDataOrKeyAction {
key_file: tmp_exported_pubkey.clone(),
key_id: Some(public_key_link.to_string()),
..Default::default()
}
.run(ctx.get_owner_client())
.await
.unwrap();
let public_key = read_object_from_json_ttlv_file(&tmp_exported_pubkey).unwrap();
let public_key_attributes = public_key.attributes().unwrap();
let certificate_link = public_key_attributes
.get_link(LinkType::CertificateLink)
.unwrap();
assert_eq!(certificate_link.to_string(), certificate_id);
(public_key_link.to_string(), public_key_attributes.clone())
}
async fn check_public_and_private_key_linked(
ctx: &TestsContext,
public_key_id: &str,
public_key_attributes: &Attributes,
) -> String {
let private_key_link = public_key_attributes
.get_link(LinkType::PrivateKeyLink)
.unwrap();
let tmp_dir = TempDir::new().unwrap();
let tmp_path = tmp_dir.path();
let tmp_exported_privkey = tmp_path.join("exported_privkey.json");
ExportSecretDataOrKeyAction {
key_file: tmp_exported_privkey.clone(),
key_id: Some(private_key_link.to_string()),
..Default::default()
}
.run(ctx.get_owner_client())
.await
.unwrap();
let private_key = read_object_from_json_ttlv_file(&tmp_exported_privkey).unwrap();
let public_key_link = private_key
.attributes()
.unwrap()
.get_link(LinkType::PublicKeyLink)
.unwrap();
assert_eq!(public_key_link.to_string(), public_key_id);
private_key_link.to_string()
}
#[tokio::test]
async fn test_certify_a_csr_without_extensions() -> KmsCliResult<()> {
log_init(None);
let ctx = start_default_test_kms_server().await;
let (root_id, intermediate_id, issuer_private_key_id) =
Box::pin(import_root_and_intermediate(ctx)).await?;
let certificate_id = CertifyAction {
certificate_signing_request: Some(PathBuf::from(
"../../test_data/certificates/csr/leaf.csr",
)),
issuer_private_key_id: Some(issuer_private_key_id.clone()),
tags: vec!["certify_a_csr_we_test".to_owned()],
..Default::default()
}
.run(ctx.get_owner_client())
.await?
.to_string();
check_certificate_chain(ctx, &issuer_private_key_id, &certificate_id).await;
let validation = ValidateCertificatesAction {
certificate_id: vec![root_id, intermediate_id, certificate_id],
validity_time: None,
}
.run(ctx.get_owner_client())
.await?;
assert_eq!(validation, ValidityIndicator::Valid);
Ok(())
}
#[tokio::test]
async fn test_certify_a_csr_with_extensions() -> KmsCliResult<()> {
log_init(None);
let ctx = start_default_test_kms_server().await;
let (root_id, intermediate_id, issuer_private_key_id) =
import_root_and_intermediate(ctx).await?;
let certificate_id = CertifyAction {
certificate_signing_request: Some(PathBuf::from(
"../../test_data/certificates/csr/leaf.csr",
)),
issuer_private_key_id: Some(issuer_private_key_id.clone()),
tags: vec!["certify_a_csr_test".to_owned()],
certificate_extensions: Some(PathBuf::from(
"../../test_data/certificates/openssl/ext.cnf",
)),
..Default::default()
}
.run(ctx.get_owner_client())
.await?
.to_string();
let (_, _, cert_x509_der) =
check_certificate_chain(ctx, &issuer_private_key_id, &certificate_id).await;
check_certificate_added_extensions(&cert_x509_der);
let validation = ValidateCertificatesAction {
certificate_id: vec![root_id, intermediate_id, certificate_id],
validity_time: None,
}
.run(ctx.get_owner_client())
.await?;
assert_eq!(validation, ValidityIndicator::Valid);
Ok(())
}
#[tokio::test]
async fn test_certify_a_public_key_test_without_extensions() -> KmsCliResult<()> {
log_init(None);
let ctx = start_default_test_kms_server().await;
let (root_id, intermediate_id, issuer_private_key_id) =
import_root_and_intermediate(ctx).await?;
let (_private_key_id, public_key_id) = CreateKeyPairAction::default()
.run(ctx.get_owner_client())
.await?;
let certificate_id = CertifyAction {
public_key_id_to_certify: Some(public_key_id.to_string()),
issuer_private_key_id: Some(issuer_private_key_id.clone()),
subject_name: Some("C = FR, ST = IdF, L = Paris, O = AcmeTest, CN = Test Leaf".to_owned()),
..Default::default()
}
.run(ctx.get_owner_client())
.await?
.to_string();
let (_, attributes, _) =
check_certificate_chain(ctx, &issuer_private_key_id, &certificate_id).await;
check_certificate_and_public_key_linked(ctx, &certificate_id, &attributes).await;
let validation = ValidateCertificatesAction {
certificate_id: vec![root_id, intermediate_id, certificate_id],
validity_time: None,
}
.run(ctx.get_owner_client())
.await?;
assert_eq!(validation, ValidityIndicator::Valid);
Ok(())
}
#[tokio::test]
async fn test_certify_a_public_key_test_with_extensions() -> KmsCliResult<()> {
log_init(None);
let ctx = start_default_test_kms_server().await;
let (root_id, intermediate_id, issuer_private_key_id) =
import_root_and_intermediate(ctx).await?;
let (_private_key_id, public_key_id) = CreateKeyPairAction::default()
.run(ctx.get_owner_client())
.await?;
let certificate_id = CertifyAction {
public_key_id_to_certify: Some(public_key_id.to_string()),
issuer_private_key_id: Some(issuer_private_key_id.clone()),
subject_name: Some("C = FR, ST = IdF, L = Paris, O = AcmeTest, CN = Test Leaf".to_owned()),
certificate_extensions: Some(PathBuf::from(
"../../test_data/certificates/openssl/ext.cnf",
)),
..Default::default()
}
.run(ctx.get_owner_client())
.await?
.to_string();
let (_, attributes, cert_x509_der) =
check_certificate_chain(ctx, &issuer_private_key_id, &certificate_id).await;
check_certificate_added_extensions(&cert_x509_der);
check_certificate_and_public_key_linked(ctx, &certificate_id, &attributes).await;
let validation = ValidateCertificatesAction {
certificate_id: vec![root_id, intermediate_id, certificate_id],
validity_time: None,
}
.run(ctx.get_owner_client())
.await?;
assert_eq!(validation, ValidityIndicator::Valid);
Ok(())
}
#[tokio::test]
async fn test_certify_renew_a_certificate() -> KmsCliResult<()> {
log_init(None);
let ctx = start_default_test_kms_server().await;
let (root_id, intermediate_id, issuer_private_key_id) =
Box::pin(import_root_and_intermediate(ctx)).await?;
let certificate_id = CertifyAction {
certificate_signing_request: Some(PathBuf::from(
"../../test_data/certificates/csr/leaf.csr",
)),
issuer_private_key_id: Some(issuer_private_key_id.clone()),
tags: vec!["certify_a_csr_test".to_owned()],
..Default::default()
}
.run(ctx.get_owner_client())
.await?
.to_string();
let (_, _, der) = check_certificate_chain(ctx, &issuer_private_key_id, &certificate_id).await;
let x509 = X509::from_der(&der).unwrap();
let num_days = x509.not_before().diff(x509.not_after()).unwrap().days;
assert_eq!(num_days, 365);
let renewed_certificate_id = CertifyAction {
certificate_id_to_re_certify: Some(certificate_id.clone()),
issuer_private_key_id: Some(issuer_private_key_id.clone()),
tags: vec!["renew_a_certificate_test".to_owned()],
number_of_days: 42,
..Default::default()
}
.run(ctx.get_owner_client())
.await?
.to_string();
assert_eq!(renewed_certificate_id, certificate_id);
let (_, _, der) = check_certificate_chain(ctx, &issuer_private_key_id, &certificate_id).await;
let x509 = X509::from_der(&der).unwrap();
let num_days = x509.not_before().diff(x509.not_after()).unwrap().days;
assert_eq!(num_days, 42);
let validation = ValidateCertificatesAction {
certificate_id: vec![root_id, intermediate_id, certificate_id],
validity_time: None,
}
.run(ctx.get_owner_client())
.await?;
assert_eq!(validation, ValidityIndicator::Valid);
Ok(())
}
#[tokio::test]
async fn test_certify_issue_with_subject_name() -> KmsCliResult<()> {
log_init(None);
let ctx = start_default_test_kms_server().await;
let (root_id, intermediate_id, issuer_private_key_id) =
Box::pin(import_root_and_intermediate(ctx)).await?;
let certificate_id = CertifyAction {
generate_key_pair: true,
algorithm: Algorithm::NistP256,
subject_name: Some("C = FR, ST = IdF, L = Paris, O = AcmeTest, CN = Test Leaf".to_owned()),
issuer_private_key_id: Some(issuer_private_key_id.clone()),
tags: vec!["certify_a_csr_test".to_owned()],
..Default::default()
}
.run(ctx.get_owner_client())
.await?
.to_string();
let (_, attributes, _) =
check_certificate_chain(ctx, &issuer_private_key_id, &certificate_id).await;
info!("{attributes}");
let (public_key_id, public_key_attributes) =
check_certificate_and_public_key_linked(ctx, &certificate_id, &attributes).await;
check_public_and_private_key_linked(ctx, &public_key_id, &public_key_attributes).await;
let validation = ValidateCertificatesAction {
certificate_id: vec![root_id, intermediate_id, certificate_id],
validity_time: None,
}
.run(ctx.get_owner_client())
.await?;
assert_eq!(validation, ValidityIndicator::Valid);
Ok(())
}
#[tokio::test]
async fn test_certify_a_public_key_test_self_signed() -> KmsCliResult<()> {
log_init(None);
let ctx = start_default_test_kms_server().await;
let (_private_key_id, public_key_id) = CreateKeyPairAction::default()
.run(ctx.get_owner_client())
.await?;
let certificate_id = CertifyAction {
public_key_id_to_certify: Some(public_key_id.to_string()),
subject_name: Some("C = FR, ST = IdF, L = Paris, O = AcmeTest, CN = Test Leaf".to_owned()),
..Default::default()
}
.run(ctx.get_owner_client())
.await?
.to_string();
let (_, attributes, _) = fetch_certificate(ctx, &certificate_id).await;
let certificate_link = attributes.get_link(LinkType::CertificateLink).unwrap();
assert_eq!(certificate_link.to_string(), certificate_id);
let validation = ValidateCertificatesAction {
certificate_id: vec![certificate_id.clone()],
validity_time: None,
}
.run(ctx.get_owner_client())
.await?;
assert_eq!(validation, ValidityIndicator::Valid);
Ok(())
}
pub(crate) async fn create_self_signed_cert(ctx: &TestsContext) -> KmsCliResult<String> {
let (_private_key_id, public_key_id) = CreateKeyPairAction::default()
.run(ctx.get_owner_client())
.await?;
let certificate_id = CertifyAction {
public_key_id_to_certify: Some(public_key_id.to_string()),
subject_name: Some("C = FR, ST = IdF, L = Paris, O = AcmeTest, CN = Test Leaf".to_owned()),
..Default::default()
}
.run(ctx.get_owner_client())
.await?
.to_string();
Ok(certificate_id)
}
#[tokio::test]
async fn test_certify_issue_with_subject_name_self_signed_without_extensions() -> KmsCliResult<()> {
log_init(None);
let ctx = start_default_test_kms_server().await;
let certificate_id = create_self_signed_cert(ctx).await?;
let (_, attributes, _) = fetch_certificate(ctx, &certificate_id).await;
let certificate_link = attributes.get_link(LinkType::CertificateLink).unwrap();
assert_eq!(certificate_link.to_string(), certificate_id);
let validation = ValidateCertificatesAction {
certificate_id: vec![certificate_id.clone()],
validity_time: None,
}
.run(ctx.get_owner_client())
.await?;
assert_eq!(validation, ValidityIndicator::Valid);
Ok(())
}
#[tokio::test]
async fn test_certify_issue_with_subject_name_self_signed_with_extensions() -> KmsCliResult<()> {
log_init(None);
let ctx = start_default_test_kms_server().await;
let certificate_id = CertifyAction {
generate_key_pair: true,
algorithm: Algorithm::NistP256,
subject_name: Some("C = FR, ST = IdF, L = Paris, O = AcmeTest, CN = Test Leaf".to_owned()),
tags: vec!["certify_self_signed".to_owned()],
certificate_extensions: Some(PathBuf::from(
"../../test_data/certificates/chain/root/ca/ext_v3_ca_root.cnf",
)),
..Default::default()
}
.run(ctx.get_owner_client())
.await?
.to_string();
let (_, attributes, _) = fetch_certificate(ctx, &certificate_id).await;
let certificate_link = attributes.get_link(LinkType::CertificateLink).unwrap();
assert_eq!(certificate_link.to_string(), certificate_id);
let validation = ValidateCertificatesAction {
certificate_id: vec![certificate_id.clone()],
validity_time: None,
}
.run(ctx.get_owner_client())
.await?;
assert_eq!(validation, ValidityIndicator::Valid);
Ok(())
}
#[tokio::test]
async fn test_certify_twice() -> KmsCliResult<()> {
log_init(None);
let ctx = start_default_test_kms_server().await;
let certificate_id = CertifyAction {
generate_key_pair: true,
algorithm: Algorithm::NistP256,
subject_name: Some("C = FR, ST = IdF, L = Paris, O = AcmeTest, CN = Test Leaf".to_owned()),
..Default::default()
}
.run(ctx.get_owner_client())
.await?
.to_string();
let (_, attributes, _) = fetch_certificate(ctx, &certificate_id).await;
let private_key_id = attributes.get_link(LinkType::PrivateKeyLink).unwrap();
let public_key_id = attributes.get_link(LinkType::PublicKeyLink).unwrap();
let certificate_id2 = CertifyAction {
certificate_id: Some(certificate_id.clone()),
generate_key_pair: true,
algorithm: Algorithm::NistP256,
subject_name: Some("C = FR, ST = IdF, L = Paris, O = AcmeTest, CN = Test Leaf".to_owned()),
..Default::default()
}
.run(ctx.get_owner_client())
.await?
.to_string();
let (_, attributes2, _) = fetch_certificate(ctx, &certificate_id2).await;
let private_key_id2 = attributes2.get_link(LinkType::PrivateKeyLink).unwrap();
let public_key_id2 = attributes2.get_link(LinkType::PublicKeyLink).unwrap();
assert_eq!(certificate_id, certificate_id2);
assert_ne!(private_key_id, private_key_id2);
assert_ne!(public_key_id, public_key_id2);
Ok(())
}