cosmian_kms_cli 5.20.0

Command Line Interface used to manage the KMS server If any assistance is needed, please either visit the Cosmian technical documentation at https://docs.cosmian.com or contact the Cosmian support team on Discord https://discord.com/invite/7kPMNtHpnz
Documentation
use std::path::PathBuf;

use base64::Engine;
use cosmian_kmip::{
    kmip_0::kmip_types::BlockCipherMode,
    kmip_2_1::{
        kmip_objects::Object,
        kmip_types::{CryptographicAlgorithm, CryptographicParameters},
    },
};
use cosmian_kms_client::{ExportObjectParams, export_object};
use cosmian_logger::{info, log_init};
use openssl::x509::X509;
use test_kms_server::{
    reexport::cosmian_kms_server::routes::google_cse::operations::PrivateKeySignRequest,
    start_default_test_kms_server,
};

use crate::{
    actions::kms::{
        attributes::GetAttributesAction, google::key_pairs::create::CreateKeyPairsAction,
        symmetric::keys::create_key::CreateKeyAction,
    },
    error::result::KmsCliResult,
    tests::kms::certificates::certify::import_root_and_intermediate,
};

#[tokio::test]
async fn create_google_key_pair() -> KmsCliResult<()> {
    log_init(None);
    let ctx = start_default_test_kms_server().await;

    // Create the Google CSE key
    let cse_key_id = CreateKeyAction::default()
        .run(ctx.get_owner_client())
        .await?;

    // import signers
    let (_root_id, _intermediate_id, issuer_private_key_id) =
        Box::pin(import_root_and_intermediate(ctx)).await.unwrap();

    // Create key pair without certificate extensions (must fail)
    let action = CreateKeyPairsAction {
        user_id: "john.doe@acme.com".to_owned(),
        cse_key_id: cse_key_id.to_string(),
        issuer_private_key_id: None,
        subject_name: "CN=John Doe,OU=Org Unit,O=Org Name,L=City,ST=State,C=US".to_owned(),
        rsa_private_key_id: None,
        sensitive: false,
        wrapping_key_id: None,
        leaf_certificate_extensions: None,
        leaf_certificate_id: None,
        leaf_certificate_pkcs12_file: None,
        leaf_certificate_pkcs12_password: None,
        number_of_days: 365,
        dry_run: true,
    };
    action.run(ctx.get_owner_client()).await.unwrap_err();

    // Create key pair with certificate extensions (must succeed)
    let action = CreateKeyPairsAction {
        issuer_private_key_id: Some(issuer_private_key_id.clone()),
        leaf_certificate_extensions: Some(PathBuf::from(
            "../../test_data/certificates/openssl/ext_leaf.cnf",
        )),
        ..action
    };
    let certificate_1 = action.run(ctx.get_owner_client()).await.unwrap();

    // Create key pair with certificate id (must succeed)
    let action = CreateKeyPairsAction {
        issuer_private_key_id: None,
        leaf_certificate_extensions: None,
        leaf_certificate_id: Some(certificate_1.to_string()),
        ..action
    };
    let _certificate_2 = action.run(ctx.get_owner_client()).await.unwrap();

    // Create key pair using a certificate file (must succeed)
    let action = CreateKeyPairsAction {
        user_id: "john.barry@acme.com".to_owned(),
        leaf_certificate_id: None,
        issuer_private_key_id: None,
        leaf_certificate_extensions: None,
        leaf_certificate_pkcs12_file: Some(PathBuf::from(
            "../../test_data/certificates/csr/leaf.p12",
        )),
        leaf_certificate_pkcs12_password: Some("secret".to_owned()),
        ..action
    };
    let _certificate_3 = action.run(ctx.get_owner_client()).await.unwrap();

    Ok(())
}

#[tokio::test]
async fn create_google_key_pair_and_sign_with_private_key() -> KmsCliResult<()> {
    log_init(None);
    let ctx = start_default_test_kms_server().await;

    // Create the Google CSE key if does not exist
    let resp = GetAttributesAction {
        id: Some("google_cse".to_owned()),
        ..Default::default()
    }
    .run(ctx.get_owner_client())
    .await;

    if resp.is_err() {
        let _cse_key_id = CreateKeyAction {
            key_id: Some("google_cse".to_owned()),
            ..Default::default()
        }
        .run(ctx.get_owner_client())
        .await?;
    }

    // import signers
    let (_root_id, _intermediate_id, issuer_private_key_id) =
        Box::pin(import_root_and_intermediate(ctx)).await.unwrap();

    // Create key pair without certificate extensions (must fail)
    let action = CreateKeyPairsAction {
        user_id: "marta.doe@acme.com".to_owned(),
        cse_key_id: "google_cse".to_string(),
        issuer_private_key_id: Some(issuer_private_key_id.to_string()),
        subject_name: "CN=Marta Doe,OU=Org Unit,O=Org Name,L=City,ST=State,C=US".to_owned(),
        rsa_private_key_id: None,
        sensitive: false,
        wrapping_key_id: None,
        leaf_certificate_extensions: Some(PathBuf::from(
            "../../test_data/certificates/openssl/ext_leaf.cnf",
        )),
        leaf_certificate_id: None,
        leaf_certificate_pkcs12_file: None,
        leaf_certificate_pkcs12_password: None,
        number_of_days: 10,
        dry_run: true,
    };
    let cert_id = action.run(ctx.get_owner_client()).await?;
    info!("Created certificate ID: {cert_id}");

    // ========================================================================
    // Verify that the certificate expiration date matches the requested number_of_days
    // ========================================================================
    let owner_client = ctx.get_owner_client();
    let (_, cert_object, _) = export_object(
        &owner_client,
        &cert_id.to_string(),
        ExportObjectParams {
            key_format_type: None,
            ..ExportObjectParams::default()
        },
    )
    .await?;

    // Extract the certificate value from the object and parse it with OpenSSL
    if let Object::Certificate(certificate) = cert_object {
        let cert_x509 = X509::from_der(&certificate.certificate_value)
            .expect("Failed to parse certificate from DER");

        // Get not_before and not_after dates from the X509 certificate
        let not_before = cert_x509.not_before();
        let not_after = cert_x509.not_after();

        // Calculate the actual validity period in days
        let diff = not_before
            .diff(not_after)
            .expect("Failed to calculate date difference");
        let actual_days = diff.days.abs();

        info!(
            "Certificate validity: {} days (requested: {} days)",
            actual_days, action.number_of_days
        );
        info!("Not Before: {}", not_before);
        info!("Not After: {}", not_after);

        // Verify that the actual certificate validity period exactly matches the requested number_of_days
        assert_eq!(
            actual_days,
            i32::try_from(action.number_of_days).unwrap(),
            "Certificate validity ({} days) does not match requested validity ({} days)",
            actual_days,
            action.number_of_days
        );
    } else {
        panic!("Expected Certificate object, got something else");
    }

    // ========================================================================
    // Double check on RSA private key: export and verify signature capability
    // ========================================================================
    // Resolve the private key id from the certificate via GetAttributes
    let owner_client = ctx.get_owner_client();
    let attrs = owner_client
        .get_attributes(cosmian_kmip::kmip_2_1::kmip_operations::GetAttributes {
            unique_identifier: Some(
                cosmian_kmip::kmip_2_1::kmip_types::UniqueIdentifier::TextString(
                    cert_id.to_string(),
                ),
            ),
            attribute_reference: None,
        })
        .await?
        .attributes;
    let private_key_id = attrs
        .get_link(cosmian_kmip::kmip_2_1::kmip_types::LinkType::PrivateKeyLink)
        .expect("Certificate should be linked to a private key")
        .to_string();
    info!("Resolved private key ID: {private_key_id}");

    let (_, wrapped_private_key, _attributes) = export_object(
        &ctx.get_owner_client(),
        &private_key_id,
        ExportObjectParams {
            wrapping_key_id: Some("google_cse"),
            wrapping_cryptographic_parameters: Some(CryptographicParameters {
                cryptographic_algorithm: Some(CryptographicAlgorithm::AES),
                block_cipher_mode: Some(BlockCipherMode::GCM),
                ..CryptographicParameters::default()
            }),
            // When wrapping, the server requires the default key format (unspecified)
            key_format_type: None,
            ..ExportObjectParams::default()
        },
    )
    .await?;

    let pkcs1_b64 = base64::engine::general_purpose::STANDARD.encode(
        wrapped_private_key
            .key_block()
            .unwrap()
            .wrapped_key_bytes()
            .unwrap(),
    );
    info!(
        "Exported private key in PKCS#8 DER (base64): {}",
        &pkcs1_b64
    );

    // Provide a valid SHA-256 digest (base64, padded). Here we use SHA-256("")
    // which equals 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
    let digest_b64 = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=".to_owned();
    // In tests, token validation is disabled (see server test utils), so any non-empty strings will do
    let req = PrivateKeySignRequest {
        authentication: "test".to_string(),
        authorization: "test".to_string(),
        algorithm: "SHA256withRSA".to_string(),
        digest: digest_b64,
        rsa_pss_salt_length: None,
        reason: "CLI test".to_string(),
        wrapped_private_key: pkcs1_b64,
    };

    // Use raw post_no_ttlv to send JSON to /google_cse/privatekeysign and expect an error
    owner_client
        .post_no_ttlv::<_, serde_json::Value>("/google_cse/privatekeysign", Some(&req))
        .await?;

    Ok(())
}