#![allow(
clippy::as_conversions,
clippy::indexing_slicing,
clippy::expect_used,
clippy::unwrap_used,
clippy::implicit_clone,
clippy::format_push_string,
clippy::branches_sharing_code,
clippy::only_used_in_recursion,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap
)]
use std::{
cell::RefCell,
collections::BTreeMap,
fs,
io::Write as _,
path::{Path, PathBuf},
time::{Duration, Instant},
};
use clap::{Parser, ValueEnum};
#[cfg(feature = "non-fips")]
use cosmian_kms_client::kmip_2_1::requests::create_pqc_key_pair_request;
#[cfg(feature = "non-fips")]
use cosmian_kms_client::reexport::cosmian_kms_client_utils::{
configurable_kem_utils::{KemAlgorithm, build_create_configurable_kem_keypair_request},
cover_crypt_utils::{
build_create_covercrypt_master_keypair_request, build_create_covercrypt_usk_request,
},
};
use cosmian_kms_client::{
KmsClient,
cosmian_kmip::kmip_0::{
kmip_messages::{RequestMessage, RequestMessageBatchItemVersioned, RequestMessageHeader},
kmip_types::{BlockCipherMode, HashingAlgorithm, PaddingMethod, ProtocolVersion},
},
kmip_2_1::{
extra::{
BulkData,
fips::{
FIPS_PRIVATE_ECC_MASK_SIGN_ECDH, FIPS_PRIVATE_RSA_MASK,
FIPS_PUBLIC_ECC_MASK_SIGN_ECDH, FIPS_PUBLIC_RSA_MASK,
},
},
kmip_messages::RequestMessageBatchItem,
kmip_operations::{CreateKeyPair, Decrypt, Encrypt, Operation, Sign, SignatureVerify},
kmip_types::{
CryptographicAlgorithm, CryptographicParameters, DigitalSignatureAlgorithm,
RecommendedCurve, UniqueIdentifier,
},
requests::{
create_ec_key_pair_request, create_rsa_key_pair_request, decrypt_request,
encrypt_request, symmetric_key_create_request,
},
},
};
use criterion::{BenchmarkId, Criterion, Throughput};
use serde::{Deserialize, Serialize};
use tokio::{runtime::Runtime, task::JoinSet};
use zeroize::Zeroizing;
use crate::error::{KmsCliError, result::KmsCliResult};
thread_local! {
static BENCH_KO: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
}
fn bench_ko(name: impl Into<String>) {
BENCH_KO.with(|r| r.borrow_mut().push(name.into()));
}
fn bench_ko_reset() {
BENCH_KO.with(|r| r.borrow_mut().clear());
}
#[derive(Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
pub enum BenchFormat {
#[default]
Text,
Json,
Markdown,
Compact,
Html,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
pub enum BenchSpeed {
#[default]
Normal,
Quick,
Sanity,
}
#[derive(Clone, Debug, Default, ValueEnum)]
pub enum BenchMode {
#[default]
All,
Encrypt,
KeyCreation,
SignVerify,
Batch,
}
#[derive(Parser, Debug)]
#[clap(verbatim_doc_comment)]
pub struct BenchAction {
#[clap(long = "mode", short = 'm', default_value = "all")]
mode: BenchMode,
#[clap(long = "format", short = 'f', default_value = "text")]
format: BenchFormat,
#[clap(long = "speed", short = 's', default_value = "normal")]
speed: BenchSpeed,
#[clap(long = "time", short = 't', default_value = "10")]
time: u64,
#[clap(long = "save-baseline")]
save_baseline: Option<String>,
#[clap(long = "load-baseline")]
load_baseline: Option<String>,
#[clap(long = "version-label")]
version_label: Option<String>,
#[clap(long = "load", default_value = "false")]
load: bool,
#[clap(long = "load-concurrency", default_value = "1,2,4,8,16,32")]
load_concurrency: String,
}
fn with_fips_rsa_masks(mut req: CreateKeyPair) -> CreateKeyPair {
if let Some(a) = req.common_attributes.as_mut() {
a.cryptographic_usage_mask = Some(FIPS_PRIVATE_RSA_MASK | FIPS_PUBLIC_RSA_MASK);
}
if let Some(a) = req.private_key_attributes.as_mut() {
a.cryptographic_usage_mask = Some(FIPS_PRIVATE_RSA_MASK);
}
if let Some(a) = req.public_key_attributes.as_mut() {
a.cryptographic_usage_mask = Some(FIPS_PUBLIC_RSA_MASK);
}
req
}
fn with_fips_ec_masks(mut req: CreateKeyPair) -> CreateKeyPair {
if let Some(a) = req.common_attributes.as_mut() {
a.cryptographic_usage_mask =
Some(FIPS_PRIVATE_ECC_MASK_SIGN_ECDH | FIPS_PUBLIC_ECC_MASK_SIGN_ECDH);
}
if let Some(a) = req.private_key_attributes.as_mut() {
a.cryptographic_usage_mask = Some(FIPS_PRIVATE_ECC_MASK_SIGN_ECDH);
}
if let Some(a) = req.public_key_attributes.as_mut() {
a.cryptographic_usage_mask = Some(FIPS_PUBLIC_ECC_MASK_SIGN_ECDH);
}
req
}
fn aes_gcm_params() -> CryptographicParameters {
CryptographicParameters {
cryptographic_algorithm: Some(CryptographicAlgorithm::AES),
block_cipher_mode: Some(BlockCipherMode::GCM),
..Default::default()
}
}
#[cfg(feature = "non-fips")]
fn chacha20_params() -> CryptographicParameters {
CryptographicParameters {
cryptographic_algorithm: Some(CryptographicAlgorithm::ChaCha20),
..Default::default()
}
}
fn rsa_oaep_params() -> CryptographicParameters {
CryptographicParameters {
cryptographic_algorithm: Some(CryptographicAlgorithm::RSA),
padding_method: Some(PaddingMethod::OAEP),
hashing_algorithm: Some(HashingAlgorithm::SHA256),
..Default::default()
}
}
fn rsa_kwp_params() -> CryptographicParameters {
CryptographicParameters {
cryptographic_algorithm: Some(CryptographicAlgorithm::RSA),
hashing_algorithm: Some(HashingAlgorithm::SHA256),
..Default::default()
}
}
#[cfg(feature = "non-fips")]
fn rsa_pkcs15_params() -> CryptographicParameters {
CryptographicParameters {
cryptographic_algorithm: Some(CryptographicAlgorithm::RSA),
padding_method: Some(PaddingMethod::PKCS1v15),
hashing_algorithm: Some(HashingAlgorithm::SHA256),
..Default::default()
}
}
fn aes_xts_params() -> CryptographicParameters {
CryptographicParameters {
cryptographic_algorithm: Some(CryptographicAlgorithm::AES),
block_cipher_mode: Some(BlockCipherMode::XTS),
..Default::default()
}
}
#[cfg(feature = "non-fips")]
fn aes_gcm_siv_params() -> CryptographicParameters {
CryptographicParameters {
cryptographic_algorithm: Some(CryptographicAlgorithm::AES),
block_cipher_mode: Some(BlockCipherMode::GCMSIV),
..Default::default()
}
}
#[cfg(feature = "non-fips")]
fn kem_params() -> CryptographicParameters {
CryptographicParameters {
cryptographic_algorithm: Some(CryptographicAlgorithm::ConfigurableKEM),
..Default::default()
}
}
fn create_sym_key(
rt: &Runtime,
client: &KmsClient,
bits: usize,
algo: CryptographicAlgorithm,
) -> UniqueIdentifier {
rt.block_on(async {
let req = symmetric_key_create_request(
&client.config.vendor_id,
None,
bits,
algo,
["bench"],
false,
None,
)
.expect("symmetric key request");
client
.create(req)
.await
.expect("create symmetric key")
.unique_identifier
})
}
#[cfg(feature = "non-fips")]
fn try_create_sym_key(
rt: &Runtime,
client: &KmsClient,
bits: usize,
algo: CryptographicAlgorithm,
) -> Option<UniqueIdentifier> {
rt.block_on(async {
let req = symmetric_key_create_request(
&client.config.vendor_id,
None,
bits,
algo,
["bench"],
false,
None,
)
.ok()?;
client.create(req).await.ok().map(|r| r.unique_identifier)
})
}
fn create_rsa_kp(
rt: &Runtime,
client: &KmsClient,
bits: usize,
) -> (UniqueIdentifier, UniqueIdentifier) {
rt.block_on(async {
let req = with_fips_rsa_masks(
create_rsa_key_pair_request(
&client.config.vendor_id,
None,
["bench"],
bits,
false,
None,
)
.expect("RSA key pair request"),
);
let resp = client
.create_key_pair(req)
.await
.expect("create RSA key pair");
(
resp.public_key_unique_identifier,
resp.private_key_unique_identifier,
)
})
}
fn try_create_ec_kp(
rt: &Runtime,
client: &KmsClient,
curve: RecommendedCurve,
) -> Option<(UniqueIdentifier, UniqueIdentifier)> {
rt.block_on(async {
let req = with_fips_ec_masks(
create_ec_key_pair_request(
&client.config.vendor_id,
None,
["bench"],
curve,
false,
None,
)
.ok()?,
);
let resp = client.create_key_pair(req).await.ok()?;
Some((
resp.public_key_unique_identifier,
resp.private_key_unique_identifier,
))
})
}
#[cfg(feature = "non-fips")]
fn try_create_ec_kp_no_fips(
rt: &Runtime,
client: &KmsClient,
curve: RecommendedCurve,
) -> Option<(UniqueIdentifier, UniqueIdentifier)> {
rt.block_on(async {
let req = create_ec_key_pair_request(
&client.config.vendor_id,
None,
["bench"],
curve,
false,
None,
)
.ok()?;
let resp = client.create_key_pair(req).await.ok()?;
Some((
resp.public_key_unique_identifier,
resp.private_key_unique_identifier,
))
})
}
#[cfg(feature = "non-fips")]
fn try_create_pqc_kp(
rt: &Runtime,
client: &KmsClient,
algorithm: CryptographicAlgorithm,
) -> Option<(UniqueIdentifier, UniqueIdentifier)> {
rt.block_on(async {
let req =
create_pqc_key_pair_request(&client.config.vendor_id, ["bench"], algorithm, false)
.ok()?;
let resp = client.create_key_pair(req).await.ok()?;
Some((
resp.public_key_unique_identifier,
resp.private_key_unique_identifier,
))
})
}
fn bench_encrypt(c: &mut Criterion, client: &KmsClient, rt: &Runtime) {
bench_encrypt_aes_gcm(c, client, rt);
bench_encrypt_aes_xts(c, client, rt);
#[cfg(feature = "non-fips")]
bench_encrypt_aes_gcm_siv(c, client, rt);
#[cfg(feature = "non-fips")]
bench_encrypt_chacha20(c, client, rt);
bench_rsa_encrypt_family(
c,
client,
rt,
"rsa-oaep",
&rsa_oaep_params(),
&[2048, 3072, 4096],
);
bench_rsa_encrypt_family(
c,
client,
rt,
"rsa-aes-kwp",
&rsa_kwp_params(),
&[2048, 3072, 4096],
);
#[cfg(feature = "non-fips")]
bench_rsa_encrypt_family(
c,
client,
rt,
"rsa-pkcs1v15",
&rsa_pkcs15_params(),
&[2048, 3072, 4096],
);
#[cfg(feature = "non-fips")]
bench_encrypt_ecies(c, client, rt);
#[cfg(feature = "non-fips")]
bench_encrypt_salsa(c, client, rt);
#[cfg(feature = "non-fips")]
bench_encrypt_covercrypt(c, client, rt);
#[cfg(feature = "non-fips")]
bench_kem(c, client, rt);
#[cfg(feature = "non-fips")]
bench_pqc_kem(c, client, rt);
}
fn bench_encrypt_aes_gcm(c: &mut Criterion, client: &KmsClient, rt: &Runtime) {
let mut group = c.benchmark_group("encrypt/aes-gcm");
let params = aes_gcm_params();
for bits in [128, 192, 256] {
let key_id = create_sym_key(rt, client, bits, CryptographicAlgorithm::AES);
let enc_req = Encrypt {
unique_identifier: Some(key_id.clone()),
cryptographic_parameters: Some(params.clone()),
data: Some(Zeroizing::new(vec![1_u8; 64])),
..Default::default()
};
let Ok(enc_resp) = rt.block_on(client.encrypt(enc_req.clone())) else {
eprintln!("[bench] AES-GCM-{bits} not supported by server, skipping");
bench_ko("encrypt/aes-gcm");
continue;
};
group.bench_function(BenchmarkId::new("encrypt", bits), |b| {
b.to_async(rt).iter(|| client.encrypt(enc_req.clone()));
});
let dec_req = Decrypt {
unique_identifier: Some(key_id),
cryptographic_parameters: Some(params.clone()),
data: Some(enc_resp.data.map_or_else(Vec::new, |z| z.to_vec())),
i_v_counter_nonce: enc_resp.i_v_counter_nonce,
authenticated_encryption_tag: enc_resp.authenticated_encryption_tag,
..Default::default()
};
group.bench_function(BenchmarkId::new("decrypt", bits), |b| {
b.to_async(rt).iter(|| client.decrypt(dec_req.clone()));
});
}
group.finish();
}
#[cfg(feature = "non-fips")]
fn bench_encrypt_chacha20(c: &mut Criterion, client: &KmsClient, rt: &Runtime) {
let Some(key_id) = try_create_sym_key(rt, client, 256, CryptographicAlgorithm::ChaCha20) else {
eprintln!("[bench] ChaCha20 not supported by server, skipping");
bench_ko("encrypt/chacha20-poly1305");
return;
};
let mut group = c.benchmark_group("encrypt/chacha20-poly1305");
let params = chacha20_params();
let enc_req = Encrypt {
unique_identifier: Some(key_id.clone()),
cryptographic_parameters: Some(params.clone()),
data: Some(Zeroizing::new(vec![1_u8; 64])),
..Default::default()
};
group.bench_function("encrypt/256", |b| {
b.to_async(rt).iter(|| client.encrypt(enc_req.clone()));
});
let enc_resp = rt
.block_on(client.encrypt(enc_req))
.expect("pre-encrypt for decrypt setup");
let dec_req = Decrypt {
unique_identifier: Some(key_id),
cryptographic_parameters: Some(params),
data: Some(enc_resp.data.map_or_else(Vec::new, |z| z.to_vec())),
i_v_counter_nonce: enc_resp.i_v_counter_nonce,
authenticated_encryption_tag: enc_resp.authenticated_encryption_tag,
..Default::default()
};
group.bench_function("decrypt/256", |b| {
b.to_async(rt).iter(|| client.decrypt(dec_req.clone()));
});
group.finish();
}
fn bench_rsa_encrypt_family(
c: &mut Criterion,
client: &KmsClient,
rt: &Runtime,
label: &str,
params: &CryptographicParameters,
key_sizes: &[usize],
) {
let mut group = c.benchmark_group(format!("encrypt/{label}"));
for &bits in key_sizes {
let (pub_id, priv_id) = create_rsa_kp(rt, client, bits);
let pub_str = pub_id.to_string();
let test_req = encrypt_request(
&pub_str,
None,
vec![0x42_u8; 32],
None,
None,
Some(params.clone()),
)
.expect("encrypt request");
if rt.block_on(client.encrypt(test_req)).is_err() {
eprintln!("[bench] {label}-{bits} not supported by server, skipping");
bench_ko(format!("encrypt/{label}"));
continue;
}
let enc_req = encrypt_request(
&pub_str,
None,
vec![0x42_u8; 32],
None,
None,
Some(params.clone()),
)
.expect("encrypt request");
group.bench_function(BenchmarkId::new("encrypt", bits), |b| {
b.to_async(rt).iter(|| client.encrypt(enc_req.clone()));
});
let enc_resp = rt
.block_on(client.encrypt(enc_req))
.expect("pre-encrypt for decrypt");
let ct = enc_resp.data.map_or_else(Vec::new, |z| z.to_vec());
let dec_req = Decrypt {
unique_identifier: Some(priv_id),
cryptographic_parameters: Some(params.clone()),
data: Some(ct),
..Default::default()
};
group.bench_function(BenchmarkId::new("decrypt", bits), |b| {
b.to_async(rt).iter(|| client.decrypt(dec_req.clone()));
});
}
group.finish();
}
fn bench_encrypt_aes_xts(c: &mut Criterion, client: &KmsClient, rt: &Runtime) {
let mut group = c.benchmark_group("encrypt/aes-xts");
let params = aes_xts_params();
for (label, bits) in [("128", 256), ("256", 512)] {
let key_id = create_sym_key(rt, client, bits, CryptographicAlgorithm::AES);
let enc_req = Encrypt {
unique_identifier: Some(key_id.clone()),
cryptographic_parameters: Some(params.clone()),
data: Some(Zeroizing::new(vec![1_u8; 64])),
i_v_counter_nonce: Some(vec![0_u8; 16]),
..Default::default()
};
if rt.block_on(client.encrypt(enc_req.clone())).is_err() {
eprintln!("[bench] AES-XTS-{label} not supported by server, skipping");
bench_ko("encrypt/aes-xts");
continue;
}
group.bench_function(BenchmarkId::new("encrypt", label), |b| {
b.to_async(rt).iter(|| client.encrypt(enc_req.clone()));
});
let enc_resp = rt
.block_on(client.encrypt(enc_req))
.expect("pre-encrypt for decrypt setup");
let dec_req = Decrypt {
unique_identifier: Some(key_id),
cryptographic_parameters: Some(params.clone()),
data: Some(enc_resp.data.map_or_else(Vec::new, |z| z.to_vec())),
i_v_counter_nonce: enc_resp.i_v_counter_nonce,
..Default::default()
};
group.bench_function(BenchmarkId::new("decrypt", label), |b| {
b.to_async(rt).iter(|| client.decrypt(dec_req.clone()));
});
}
group.finish();
}
#[cfg(feature = "non-fips")]
fn bench_encrypt_aes_gcm_siv(c: &mut Criterion, client: &KmsClient, rt: &Runtime) {
let mut group = c.benchmark_group("encrypt/aes-gcm-siv");
let params = aes_gcm_siv_params();
for bits in [128, 256] {
let key_id = create_sym_key(rt, client, bits, CryptographicAlgorithm::AES);
let enc_req = Encrypt {
unique_identifier: Some(key_id.clone()),
cryptographic_parameters: Some(params.clone()),
data: Some(Zeroizing::new(vec![1_u8; 64])),
..Default::default()
};
if rt.block_on(client.encrypt(enc_req.clone())).is_err() {
eprintln!("[bench] AES-GCM-SIV-{bits} not supported by server, skipping");
bench_ko("encrypt/aes-gcm-siv");
continue;
}
group.bench_function(BenchmarkId::new("encrypt", bits), |b| {
b.to_async(rt).iter(|| client.encrypt(enc_req.clone()));
});
let enc_resp = rt
.block_on(client.encrypt(enc_req))
.expect("pre-encrypt for decrypt setup");
let dec_req = Decrypt {
unique_identifier: Some(key_id),
cryptographic_parameters: Some(params.clone()),
data: Some(enc_resp.data.map_or_else(Vec::new, |z| z.to_vec())),
i_v_counter_nonce: enc_resp.i_v_counter_nonce,
authenticated_encryption_tag: enc_resp.authenticated_encryption_tag,
..Default::default()
};
group.bench_function(BenchmarkId::new("decrypt", bits), |b| {
b.to_async(rt).iter(|| client.decrypt(dec_req.clone()));
});
}
group.finish();
}
#[cfg(feature = "non-fips")]
fn bench_encrypt_ecies(c: &mut Criterion, client: &KmsClient, rt: &Runtime) {
let mut group = c.benchmark_group("encrypt/ecies");
for (label, curve) in [
("P-256", RecommendedCurve::P256),
("P-384", RecommendedCurve::P384),
("P-521", RecommendedCurve::P521),
] {
let Some((pub_id, priv_id)) = try_create_ec_kp_no_fips(rt, client, curve) else {
eprintln!("[bench] ECIES {label} not supported by server, skipping");
bench_ko("encrypt/ecies");
continue;
};
let pub_str = pub_id.to_string();
let enc_req = encrypt_request(&pub_str, None, vec![0x42_u8; 64], None, None, None)
.expect("encrypt request");
if rt.block_on(client.encrypt(enc_req.clone())).is_err() {
eprintln!("[bench] ECIES {label} encrypt failed, skipping");
bench_ko("encrypt/ecies");
continue;
}
group.bench_function(BenchmarkId::new("encrypt", label), |b| {
b.to_async(rt).iter(|| client.encrypt(enc_req.clone()));
});
let enc_resp = rt
.block_on(client.encrypt(enc_req))
.expect("pre-encrypt for decrypt");
let ct = enc_resp.data.map_or_else(Vec::new, |z| z.to_vec());
let dec_req = decrypt_request(&priv_id.to_string(), None, ct, None, None, None);
group.bench_function(BenchmarkId::new("decrypt", label), |b| {
b.to_async(rt).iter(|| client.decrypt(dec_req.clone()));
});
}
group.finish();
}
#[cfg(feature = "non-fips")]
fn bench_encrypt_salsa(c: &mut Criterion, client: &KmsClient, rt: &Runtime) {
let Some((pub_id, priv_id)) =
try_create_ec_kp_no_fips(rt, client, RecommendedCurve::CURVE25519)
else {
eprintln!("[bench] Salsa Sealed Box (X25519) not supported by server, skipping");
bench_ko("encrypt/salsa-sealed-box");
return;
};
let pub_str = pub_id.to_string();
let enc_req = encrypt_request(&pub_str, None, vec![0x42_u8; 64], None, None, None)
.expect("encrypt request");
if rt.block_on(client.encrypt(enc_req.clone())).is_err() {
eprintln!("[bench] Salsa Sealed Box encrypt failed, skipping");
bench_ko("encrypt/salsa-sealed-box");
return;
}
let mut group = c.benchmark_group("encrypt/salsa-sealed-box");
group.bench_function("encrypt", |b| {
b.to_async(rt).iter(|| client.encrypt(enc_req.clone()));
});
let enc_resp = rt
.block_on(client.encrypt(enc_req))
.expect("pre-encrypt for decrypt");
let ct = enc_resp.data.map_or_else(Vec::new, |z| z.to_vec());
let dec_req = decrypt_request(&priv_id.to_string(), None, ct, None, None, None);
group.bench_function("decrypt", |b| {
b.to_async(rt).iter(|| client.decrypt(dec_req.clone()));
});
group.finish();
}
#[cfg(feature = "non-fips")]
fn bench_encrypt_covercrypt(c: &mut Criterion, client: &KmsClient, rt: &Runtime) {
let access_structure =
r#"{"Department": ["RnD", "HR"], "Security Level::<": ["Protected", "Confidential"]}"#;
let encryption_policy = "Department::RnD && Security Level::Confidential";
let decryption_policy = "Department::RnD && Security Level::Confidential";
let vid = client.config.vendor_id.clone();
let result = rt.block_on(async {
let kp_req = build_create_covercrypt_master_keypair_request(
&vid,
access_structure,
["bench"],
false,
None,
)
.map_err(|e| format!("CC key pair request: {e}"))?;
let kp_resp = client
.create_key_pair(kp_req)
.await
.map_err(|e| format!("CC key pair creation: {e}"))?;
let usk_req = build_create_covercrypt_usk_request(
&vid,
decryption_policy,
&kp_resp.private_key_unique_identifier.to_string(),
Vec::<String>::new(),
false,
None,
)
.map_err(|e| format!("CC USK request: {e}"))?;
let usk_resp = client
.create(usk_req)
.await
.map_err(|e| format!("CC USK creation: {e}"))?;
Ok::<_, String>((
kp_resp.public_key_unique_identifier,
usk_resp.unique_identifier,
))
});
let (pub_id, usk_id) = match result {
Ok(ids) => ids,
Err(e) => {
eprintln!("[bench] Covercrypt not supported by server: {e}, skipping");
bench_ko("encrypt/covercrypt");
return;
}
};
let pub_str = pub_id.to_string();
let usk_str = usk_id.to_string();
let enc_req = encrypt_request(
&pub_str,
Some(encryption_policy.to_owned()),
vec![0x42_u8; 64],
None,
None,
Some(CryptographicParameters {
cryptographic_algorithm: Some(CryptographicAlgorithm::CoverCrypt),
..Default::default()
}),
)
.expect("CC encrypt request");
if rt.block_on(client.encrypt(enc_req.clone())).is_err() {
eprintln!("[bench] Covercrypt encrypt failed, skipping");
bench_ko("encrypt/covercrypt");
return;
}
let mut group = c.benchmark_group("encrypt/covercrypt");
group.bench_function("encrypt", |b| {
b.to_async(rt).iter(|| client.encrypt(enc_req.clone()));
});
let enc_resp = rt
.block_on(client.encrypt(enc_req))
.expect("pre-encrypt for decrypt");
let ct = enc_resp.data.map_or_else(Vec::new, |z| z.to_vec());
let dec_req = decrypt_request(
&usk_str,
None,
ct,
None,
None,
Some(CryptographicParameters {
cryptographic_algorithm: Some(CryptographicAlgorithm::CoverCrypt),
..Default::default()
}),
);
group.bench_function("decrypt", |b| {
b.to_async(rt).iter(|| client.decrypt(dec_req.clone()));
});
group.finish();
}
#[cfg(feature = "non-fips")]
fn bench_kem(c: &mut Criterion, client: &KmsClient, rt: &Runtime) {
let vid = client.config.vendor_id.clone();
let params = kem_params();
let algorithms = [
("ML-KEM-512", KemAlgorithm::MlKem512),
("ML-KEM-768", KemAlgorithm::MlKem768),
("ML-KEM-512/P-256", KemAlgorithm::MlKem512P256),
("ML-KEM-768/P-256", KemAlgorithm::MlKem768P256),
("ML-KEM-512/X25519", KemAlgorithm::MlKem512Curve25519),
("ML-KEM-768/X25519", KemAlgorithm::MlKem768Curve25519),
];
let mut group = c.benchmark_group("kem/configurable");
for (label, algo) in algorithms {
let result = rt.block_on(async {
let kp_req = build_create_configurable_kem_keypair_request(
&vid,
None,
["bench"],
algo,
false,
None,
)
.map_err(|e| format!("KEM key pair request ({label}): {e}"))?;
let kp_resp = client
.create_key_pair(kp_req)
.await
.map_err(|e| format!("KEM key pair creation ({label}): {e}"))?;
Ok::<_, String>((
kp_resp.public_key_unique_identifier,
kp_resp.private_key_unique_identifier,
))
});
let (pub_id, priv_id) = match result {
Ok(ids) => ids,
Err(e) => {
eprintln!("[bench] KEM {label} not supported: {e}, skipping");
bench_ko("kem/configurable");
continue;
}
};
let pub_str = pub_id.to_string();
let enc_req = encrypt_request(&pub_str, None, Vec::new(), None, None, Some(params.clone()))
.expect("KEM encapsulate request");
if rt.block_on(client.encrypt(enc_req.clone())).is_err() {
eprintln!("[bench] KEM {label} encapsulate failed, skipping");
bench_ko("kem/configurable");
continue;
}
group.bench_function(BenchmarkId::new("encapsulate", label), |b| {
b.to_async(rt).iter(|| client.encrypt(enc_req.clone()));
});
let enc_resp = rt
.block_on(client.encrypt(enc_req))
.expect("pre-encapsulate for decapsulate");
let ct = enc_resp.data.map_or_else(Vec::new, |z| z.to_vec());
let dec_req = decrypt_request(
&priv_id.to_string(),
None,
ct,
None,
None,
Some(params.clone()),
);
group.bench_function(BenchmarkId::new("decapsulate", label), |b| {
b.to_async(rt).iter(|| client.decrypt(dec_req.clone()));
});
}
group.finish();
}
#[cfg(feature = "non-fips")]
fn bench_pqc_kem(c: &mut Criterion, client: &KmsClient, rt: &Runtime) {
let algorithms: &[(&str, CryptographicAlgorithm)] = &[
("ML-KEM-512", CryptographicAlgorithm::MLKEM_512),
("ML-KEM-768", CryptographicAlgorithm::MLKEM_768),
("ML-KEM-1024", CryptographicAlgorithm::MLKEM_1024),
("X25519MLKEM768", CryptographicAlgorithm::X25519MLKEM768),
("X448MLKEM1024", CryptographicAlgorithm::X448MLKEM1024),
];
let mut group = c.benchmark_group("kem/pqc");
for &(label, algo) in algorithms {
let Some((pub_id, priv_id)) = try_create_pqc_kp(rt, client, algo) else {
eprintln!("[bench] PQC KEM {label} not supported by server, skipping");
bench_ko("kem/pqc");
continue;
};
let pub_str = pub_id.to_string();
let enc_req =
encrypt_request(&pub_str, None, Vec::new(), None, None, None).expect("KEM request");
let Ok(enc_resp) = rt.block_on(client.encrypt(enc_req.clone())) else {
eprintln!("[bench] PQC KEM {label} encapsulate failed, skipping");
bench_ko("kem/pqc");
continue;
};
group.bench_function(BenchmarkId::new("encapsulate", label), |b| {
b.to_async(rt).iter(|| client.encrypt(enc_req.clone()));
});
let ct = enc_resp
.i_v_counter_nonce
.unwrap_or_else(|| enc_resp.data.map_or_else(Vec::new, |z| z.to_vec()));
let dec_req = decrypt_request(&priv_id.to_string(), None, ct, None, None, None);
group.bench_function(BenchmarkId::new("decapsulate", label), |b| {
b.to_async(rt).iter(|| client.decrypt(dec_req.clone()));
});
}
group.finish();
}
fn bench_key_creation(c: &mut Criterion, client: &KmsClient, rt: &Runtime) {
let vid = client.config.vendor_id.clone();
{
let mut group = c.benchmark_group("key-creation/symmetric");
for (label, bits, algo) in [
("aes-128", 128, CryptographicAlgorithm::AES),
("aes-192", 192, CryptographicAlgorithm::AES),
("aes-256", 256, CryptographicAlgorithm::AES),
] {
let vid = vid.clone();
group.bench_function(label, |b| {
b.to_async(rt).iter(|| {
let vid = vid.clone();
async move {
let req = symmetric_key_create_request(
&vid,
None,
bits,
algo,
Vec::<String>::new(),
false,
None,
)
.unwrap();
client.create(req).await.unwrap();
}
});
});
}
#[cfg(feature = "non-fips")]
if try_create_sym_key(rt, client, 256, CryptographicAlgorithm::ChaCha20).is_some() {
group.bench_function("chacha20-256", |b| {
b.to_async(rt).iter(|| {
let vid = vid.clone();
async move {
let req = symmetric_key_create_request(
&vid,
None,
256,
CryptographicAlgorithm::ChaCha20,
Vec::<String>::new(),
false,
None,
)
.unwrap();
client.create(req).await.unwrap();
}
});
});
} else {
bench_ko("key-creation/symmetric");
}
group.finish();
}
{
let mut group = c.benchmark_group("key-creation/rsa");
for bits in [2048, 3072, 4096] {
let vid = vid.clone();
group.bench_function(format!("rsa-{bits}"), |b| {
b.to_async(rt).iter(|| {
let vid = vid.clone();
async move {
let req = with_fips_rsa_masks(
create_rsa_key_pair_request(
&vid,
None,
Vec::<String>::new(),
bits,
false,
None,
)
.unwrap(),
);
client.create_key_pair(req).await.unwrap();
}
});
});
}
group.finish();
}
{
let mut group = c.benchmark_group("key-creation/ec");
for (label, curve) in [
("p256", RecommendedCurve::P256),
("p384", RecommendedCurve::P384),
("p521", RecommendedCurve::P521),
] {
let vid = vid.clone();
group.bench_function(label, |b| {
b.to_async(rt).iter(|| {
let vid = vid.clone();
async move {
let req = with_fips_ec_masks(
create_ec_key_pair_request(
&vid,
None,
Vec::<String>::new(),
curve,
false,
None,
)
.unwrap(),
);
client.create_key_pair(req).await.unwrap();
}
});
});
}
#[cfg(feature = "non-fips")]
for (label, curve) in [
("ed25519", RecommendedCurve::CURVEED25519),
("ed448", RecommendedCurve::CURVEED448),
("secp256k1", RecommendedCurve::SECP256K1),
] {
if try_create_ec_kp(rt, client, curve).is_some() {
let vid = vid.clone();
group.bench_function(label, |b| {
b.to_async(rt).iter(|| {
let vid = vid.clone();
async move {
let req = with_fips_ec_masks(
create_ec_key_pair_request(
&vid,
None,
Vec::<String>::new(),
curve,
false,
None,
)
.unwrap(),
);
client.create_key_pair(req).await.unwrap();
}
});
});
} else {
bench_ko("key-creation/ec");
}
}
group.finish();
}
#[cfg(feature = "non-fips")]
{
let access_structure =
r#"{"Department": ["RnD", "HR"], "Security Level::<": ["Protected", "Confidential"]}"#;
let vid2 = vid.clone();
let result = rt.block_on(async {
let req = build_create_covercrypt_master_keypair_request(
&vid2,
access_structure,
["bench"],
false,
None,
)
.ok();
match req {
Some(r) => client.create_key_pair(r).await.ok(),
None => None,
}
});
if result.is_some() {
let mut group = c.benchmark_group("key-creation/covercrypt");
group.bench_function("master-keypair", |b| {
b.to_async(rt).iter(|| {
let vid2 = vid.clone();
async move {
let req = build_create_covercrypt_master_keypair_request(
&vid2,
access_structure,
Vec::<String>::new(),
false,
None,
)
.unwrap();
client.create_key_pair(req).await.unwrap();
}
});
});
group.finish();
} else {
bench_ko("key-creation/covercrypt");
}
}
#[cfg(feature = "non-fips")]
{
let kem_algos = [
("ML-KEM-512", KemAlgorithm::MlKem512),
("ML-KEM-768", KemAlgorithm::MlKem768),
("ML-KEM-512/P-256", KemAlgorithm::MlKem512P256),
("ML-KEM-768/P-256", KemAlgorithm::MlKem768P256),
("ML-KEM-512/X25519", KemAlgorithm::MlKem512Curve25519),
("ML-KEM-768/X25519", KemAlgorithm::MlKem768Curve25519),
];
let mut group = c.benchmark_group("key-creation/kem");
for (label, algo) in kem_algos {
let vid2 = vid.clone();
let result = rt.block_on(async {
let req = build_create_configurable_kem_keypair_request(
&vid2,
None,
["bench"],
algo,
false,
None,
)
.ok()?;
client.create_key_pair(req).await.ok()
});
if result.is_some() {
let vid2 = vid.clone();
group.bench_function(label, |b| {
b.to_async(rt).iter(|| {
let vid2 = vid2.clone();
async move {
let req = build_create_configurable_kem_keypair_request(
&vid2,
None,
Vec::<String>::new(),
algo,
false,
None,
)
.unwrap();
client.create_key_pair(req).await.unwrap();
}
});
});
} else {
bench_ko("key-creation/kem");
}
}
group.finish();
}
#[cfg(feature = "non-fips")]
{
let pqc_algos: &[(&str, CryptographicAlgorithm)] = &[
("ML-KEM-512", CryptographicAlgorithm::MLKEM_512),
("ML-KEM-768", CryptographicAlgorithm::MLKEM_768),
("ML-KEM-1024", CryptographicAlgorithm::MLKEM_1024),
("X25519MLKEM768", CryptographicAlgorithm::X25519MLKEM768),
("X448MLKEM1024", CryptographicAlgorithm::X448MLKEM1024),
("ML-DSA-44", CryptographicAlgorithm::MLDSA_44),
("ML-DSA-65", CryptographicAlgorithm::MLDSA_65),
("ML-DSA-87", CryptographicAlgorithm::MLDSA_87),
(
"SLH-DSA-SHA2-128s",
CryptographicAlgorithm::SLHDSA_SHA2_128s,
),
(
"SLH-DSA-SHA2-128f",
CryptographicAlgorithm::SLHDSA_SHA2_128f,
),
(
"SLH-DSA-SHA2-192s",
CryptographicAlgorithm::SLHDSA_SHA2_192s,
),
(
"SLH-DSA-SHA2-192f",
CryptographicAlgorithm::SLHDSA_SHA2_192f,
),
(
"SLH-DSA-SHA2-256s",
CryptographicAlgorithm::SLHDSA_SHA2_256s,
),
(
"SLH-DSA-SHA2-256f",
CryptographicAlgorithm::SLHDSA_SHA2_256f,
),
(
"SLH-DSA-SHAKE-128s",
CryptographicAlgorithm::SLHDSA_SHAKE_128s,
),
(
"SLH-DSA-SHAKE-128f",
CryptographicAlgorithm::SLHDSA_SHAKE_128f,
),
(
"SLH-DSA-SHAKE-192s",
CryptographicAlgorithm::SLHDSA_SHAKE_192s,
),
(
"SLH-DSA-SHAKE-192f",
CryptographicAlgorithm::SLHDSA_SHAKE_192f,
),
(
"SLH-DSA-SHAKE-256s",
CryptographicAlgorithm::SLHDSA_SHAKE_256s,
),
(
"SLH-DSA-SHAKE-256f",
CryptographicAlgorithm::SLHDSA_SHAKE_256f,
),
];
let mut group = c.benchmark_group("key-creation/pqc");
for &(label, algo) in pqc_algos {
let vid2 = vid.clone();
let result = rt.block_on(async {
let req = create_pqc_key_pair_request(&vid2, ["bench"], algo, false).ok()?;
client.create_key_pair(req).await.ok()
});
if result.is_some() {
let vid2 = vid.clone();
group.bench_function(label, |b| {
b.to_async(rt).iter(|| {
let vid2 = vid2.clone();
async move {
let req = create_pqc_key_pair_request(
&vid2,
Vec::<String>::new(),
algo,
false,
)
.unwrap();
client.create_key_pair(req).await.unwrap();
}
});
});
} else {
bench_ko("key-creation/pqc");
}
}
group.finish();
}
}
fn bench_sign_verify(c: &mut Criterion, client: &KmsClient, rt: &Runtime) {
for (label, curve, algo) in [
(
"ecdsa-p256",
RecommendedCurve::P256,
DigitalSignatureAlgorithm::ECDSAWithSHA256,
),
(
"ecdsa-p384",
RecommendedCurve::P384,
DigitalSignatureAlgorithm::ECDSAWithSHA384,
),
(
"ecdsa-p521",
RecommendedCurve::P521,
DigitalSignatureAlgorithm::ECDSAWithSHA512,
),
] {
bench_ec_sign(c, client, rt, label, curve, Some(algo));
}
#[cfg(feature = "non-fips")]
{
bench_ec_sign(
c,
client,
rt,
"ecdsa-secp256k1",
RecommendedCurve::SECP256K1,
Some(DigitalSignatureAlgorithm::ECDSAWithSHA256),
);
bench_ec_sign(
c,
client,
rt,
"eddsa-ed25519",
RecommendedCurve::CURVEED25519,
None,
);
bench_ec_sign(
c,
client,
rt,
"eddsa-ed448",
RecommendedCurve::CURVEED448,
None,
);
}
bench_rsa_pss_sign(c, client, rt);
#[cfg(feature = "non-fips")]
bench_pqc_sign(
c,
client,
rt,
"sign-verify/ml-dsa",
&[
("44", CryptographicAlgorithm::MLDSA_44),
("65", CryptographicAlgorithm::MLDSA_65),
("87", CryptographicAlgorithm::MLDSA_87),
],
);
#[cfg(feature = "non-fips")]
bench_pqc_sign(
c,
client,
rt,
"sign-verify/slh-dsa",
&[
("SHA2-128s", CryptographicAlgorithm::SLHDSA_SHA2_128s),
("SHA2-128f", CryptographicAlgorithm::SLHDSA_SHA2_128f),
("SHA2-192s", CryptographicAlgorithm::SLHDSA_SHA2_192s),
("SHA2-192f", CryptographicAlgorithm::SLHDSA_SHA2_192f),
("SHA2-256s", CryptographicAlgorithm::SLHDSA_SHA2_256s),
("SHA2-256f", CryptographicAlgorithm::SLHDSA_SHA2_256f),
("SHAKE-128s", CryptographicAlgorithm::SLHDSA_SHAKE_128s),
("SHAKE-128f", CryptographicAlgorithm::SLHDSA_SHAKE_128f),
("SHAKE-192s", CryptographicAlgorithm::SLHDSA_SHAKE_192s),
("SHAKE-192f", CryptographicAlgorithm::SLHDSA_SHAKE_192f),
("SHAKE-256s", CryptographicAlgorithm::SLHDSA_SHAKE_256s),
("SHAKE-256f", CryptographicAlgorithm::SLHDSA_SHAKE_256f),
],
);
}
fn bench_ec_sign(
c: &mut Criterion,
client: &KmsClient,
rt: &Runtime,
label: &str,
curve: RecommendedCurve,
sign_algo: Option<DigitalSignatureAlgorithm>,
) {
let Some((pub_id, priv_id)) = try_create_ec_kp(rt, client, curve) else {
eprintln!("[bench] {label} not supported by server, skipping");
bench_ko(format!("sign-verify/{label}"));
return;
};
let sign_params = sign_algo.map(|a| CryptographicParameters {
digital_signature_algorithm: Some(a),
..Default::default()
});
let message = Zeroizing::new(vec![0x42_u8; 32]);
let sign_req = Sign {
unique_identifier: Some(priv_id),
cryptographic_parameters: sign_params.clone(),
data: Some(message.clone()),
..Default::default()
};
let Ok(sign_resp) = rt.block_on(client.sign(sign_req.clone())) else {
eprintln!("[bench] {label} sign not supported by server, skipping");
bench_ko(format!("sign-verify/{label}"));
return;
};
let sample_sig = sign_resp.signature_data.unwrap_or_default();
let verify_req = SignatureVerify {
unique_identifier: Some(pub_id),
cryptographic_parameters: sign_params,
data: Some(message.to_vec()),
signature_data: Some(sample_sig),
..Default::default()
};
let mut group = c.benchmark_group(format!("sign-verify/{label}"));
group.bench_function("sign", |b| {
b.to_async(rt).iter(|| client.sign(sign_req.clone()));
});
group.bench_function("verify", |b| {
b.to_async(rt)
.iter(|| client.signature_verify(verify_req.clone()));
});
group.finish();
}
fn bench_rsa_pss_sign(c: &mut Criterion, client: &KmsClient, rt: &Runtime) {
let sign_params = Some(CryptographicParameters {
digital_signature_algorithm: Some(DigitalSignatureAlgorithm::RSASSAPSS),
..Default::default()
});
let message = Zeroizing::new(vec![0x42_u8; 32]);
let mut group = c.benchmark_group("sign-verify/rsa-pss");
for bits in [2048, 3072, 4096] {
let (pub_id, priv_id) = create_rsa_kp(rt, client, bits);
let sign_req = Sign {
unique_identifier: Some(priv_id),
cryptographic_parameters: sign_params.clone(),
data: Some(message.clone()),
..Default::default()
};
let Ok(sign_resp) = rt.block_on(client.sign(sign_req.clone())) else {
eprintln!("[bench] rsa-pss-{bits} sign not supported by server, skipping");
bench_ko("sign-verify/rsa-pss");
continue;
};
let sample_sig = sign_resp.signature_data.unwrap_or_default();
let verify_req = SignatureVerify {
unique_identifier: Some(pub_id),
cryptographic_parameters: sign_params.clone(),
data: Some(message.to_vec()),
signature_data: Some(sample_sig),
..Default::default()
};
group.bench_function(BenchmarkId::new("sign", bits), |b| {
b.to_async(rt).iter(|| client.sign(sign_req.clone()));
});
group.bench_function(BenchmarkId::new("verify", bits), |b| {
b.to_async(rt)
.iter(|| client.signature_verify(verify_req.clone()));
});
}
group.finish();
}
#[cfg(feature = "non-fips")]
fn bench_pqc_sign(
c: &mut Criterion,
client: &KmsClient,
rt: &Runtime,
group_name: &str,
algorithms: &[(&str, CryptographicAlgorithm)],
) {
let message = Zeroizing::new(vec![0x42_u8; 32]);
let mut group = c.benchmark_group(group_name);
for &(label, algo) in algorithms {
let Some((pub_id, priv_id)) = try_create_pqc_kp(rt, client, algo) else {
eprintln!("[bench] {label} not supported by server, skipping");
bench_ko(group_name);
continue;
};
let sign_req = Sign {
unique_identifier: Some(priv_id),
cryptographic_parameters: None,
data: Some(message.clone()),
..Default::default()
};
let Ok(sign_resp) = rt.block_on(client.sign(sign_req.clone())) else {
eprintln!("[bench] {label} sign failed, skipping");
bench_ko(group_name);
continue;
};
let sample_sig = sign_resp.signature_data.unwrap_or_default();
let verify_req = SignatureVerify {
unique_identifier: Some(pub_id),
cryptographic_parameters: None,
data: Some(message.to_vec()),
signature_data: Some(sample_sig),
..Default::default()
};
group.bench_function(BenchmarkId::new("sign", label), |b| {
b.to_async(rt).iter(|| client.sign(sign_req.clone()));
});
group.bench_function(BenchmarkId::new("verify", label), |b| {
b.to_async(rt)
.iter(|| client.signature_verify(verify_req.clone()));
});
}
group.finish();
}
fn bench_batch(c: &mut Criterion, client: &KmsClient, rt: &Runtime, sanity: bool) {
bench_batch_aes_bulk(c, client, rt, sanity);
bench_batch_rsa_message(c, client, rt, "batch/rsa-oaep", &rsa_oaep_params(), sanity);
bench_batch_rsa_message(
c,
client,
rt,
"batch/rsa-aes-kwp",
&rsa_kwp_params(),
sanity,
);
#[cfg(feature = "non-fips")]
bench_batch_rsa_message(
c,
client,
rt,
"batch/rsa-pkcs1v15",
&rsa_pkcs15_params(),
sanity,
);
}
fn bench_batch_aes_bulk(c: &mut Criterion, client: &KmsClient, rt: &Runtime, sanity: bool) {
let mut group = c.benchmark_group("batch/aes-gcm");
let params = aes_gcm_params();
let batch_sizes: &[usize] = if sanity {
&[1]
} else {
&[1, 10, 50, 100, 500, 1000]
};
for bits in [128, 256] {
let key_id = create_sym_key(rt, client, bits, CryptographicAlgorithm::AES);
let key_str = key_id.to_string();
for n in batch_sizes.iter().copied() {
let parameter_name = if n == 1 {
format!("{n} request")
} else {
format!("{n} requests")
};
let data = if n == 1 {
Zeroizing::new(vec![1_u8; 64])
} else {
BulkData::new(vec![Zeroizing::new(vec![1_u8; 64]); n])
.serialize()
.expect("BulkData serialize")
};
let req = encrypt_request(
&key_str,
None,
data.to_vec(),
None,
None,
Some(params.clone()),
)
.expect("encrypt request");
let pre_resp = rt
.block_on(client.encrypt(req.clone()))
.expect("pre-encrypt bulk request");
let ciphertext = pre_resp.data.map_or_else(Vec::new, |z| z.to_vec());
let dec_req =
decrypt_request(&key_str, None, ciphertext, None, None, Some(params.clone()));
group.throughput(Throughput::Elements(n as u64));
group.bench_with_input(
BenchmarkId::new(format!("{bits}-bit key encrypt"), ¶meter_name),
&n,
|b, _| {
b.to_async(rt).iter(|| client.encrypt(req.clone()));
},
);
group.bench_with_input(
BenchmarkId::new(format!("{bits}-bit key decrypt"), ¶meter_name),
&n,
|b, _| {
b.to_async(rt).iter(|| client.decrypt(dec_req.clone()));
},
);
}
}
group.finish();
}
fn bench_batch_rsa_message(
c: &mut Criterion,
client: &KmsClient,
rt: &Runtime,
group_name: &str,
params: &CryptographicParameters,
sanity: bool,
) {
let mut group = c.benchmark_group(group_name);
for bits in [2048, 3072, 4096] {
let (pub_id, priv_id) = create_rsa_kp(rt, client, bits);
let pub_str = pub_id.to_string();
let priv_str = priv_id.to_string();
let test_req = encrypt_request(
&pub_str,
None,
vec![0_u8; 32],
None,
None,
Some(params.clone()),
)
.expect("test encrypt request");
if rt.block_on(client.encrypt(test_req)).is_err() {
eprintln!("[bench] {group_name}-{bits} not supported by server, skipping");
bench_ko(group_name);
continue;
}
let pre_req = encrypt_request(
&pub_str,
None,
vec![0_u8; 32],
None,
None,
Some(params.clone()),
)
.expect("pre-encrypt request");
let pre_resp = rt.block_on(client.encrypt(pre_req)).expect("pre-encrypt");
let ciphertext = pre_resp.data.map_or_else(Vec::new, |z| z.to_vec());
let rsa_batch_sizes: &[usize] = if sanity { &[1] } else { &[1, 10, 50, 100] };
for n in rsa_batch_sizes.iter().copied() {
let parameter_name = if n == 1 {
format!("{n} request")
} else {
format!("{n} requests")
};
let enc_item = encrypt_request(
&pub_str,
None,
vec![0_u8; 32],
None,
None,
Some(params.clone()),
)
.expect("encrypt request");
let enc_msg = RequestMessage {
request_header: RequestMessageHeader {
protocol_version: ProtocolVersion {
protocol_version_major: 2,
protocol_version_minor: 1,
},
batch_count: i32::try_from(n).expect("batch_count fits i32"),
..Default::default()
},
batch_item: (0..n)
.map(|_| {
RequestMessageBatchItemVersioned::V21(RequestMessageBatchItem::new(
Operation::Encrypt(Box::new(enc_item.clone())),
))
})
.collect(),
};
group.throughput(Throughput::Elements(n as u64));
group.bench_with_input(
BenchmarkId::new(format!("{bits}-bit key encrypt"), ¶meter_name),
&n,
|b, _| {
b.to_async(rt).iter(|| client.message(enc_msg.clone()));
},
);
let dec_item = decrypt_request(
&priv_str,
None,
ciphertext.clone(),
None,
None,
Some(params.clone()),
);
let dec_msg = RequestMessage {
request_header: RequestMessageHeader {
protocol_version: ProtocolVersion {
protocol_version_major: 2,
protocol_version_minor: 1,
},
batch_count: i32::try_from(n).expect("batch_count fits i32"),
..Default::default()
},
batch_item: (0..n)
.map(|_| {
RequestMessageBatchItemVersioned::V21(RequestMessageBatchItem::new(
Operation::Decrypt(Box::new(dec_item.clone())),
))
})
.collect(),
};
group.bench_with_input(
BenchmarkId::new(format!("{bits}-bit key decrypt"), ¶meter_name),
&n,
|b, _| {
b.to_async(rt).iter(|| client.message(dec_msg.clone()));
},
);
}
}
group.finish();
}
#[derive(Deserialize)]
struct CriterionEstimates {
mean: CriterionEstimate,
median: CriterionEstimate,
#[allow(dead_code)]
std_dev: CriterionEstimate,
median_abs_dev: Option<CriterionEstimate>,
}
#[derive(Deserialize)]
struct CriterionEstimate {
point_estimate: f64,
#[allow(dead_code)]
standard_error: f64,
confidence_interval: CriterionCI,
}
#[derive(Deserialize)]
struct CriterionCI {
#[allow(dead_code)]
confidence_level: f64,
lower_bound: f64,
upper_bound: f64,
}
#[derive(Serialize)]
struct CtEst {
estimate: f64,
lower_bound: f64,
upper_bound: f64,
unit: &'static str, }
#[derive(Serialize)]
struct CtBenchmarkComplete {
reason: &'static str,
id: String,
report_directory: &'static str, iteration_count: Vec<u64>, measured_values: Vec<f64>, unit: &'static str, throughput: Vec<serde_json::Value>, typical: CtEst,
mean: CtEst,
median: CtEst,
median_abs_dev: CtEst,
slope: Option<CtEst>,
change: Option<serde_json::Value>, }
fn count_baseline_files(home: &Path, baseline: &str) -> usize {
let mut count = 0;
if let Ok(walker) = fs::read_dir(home) {
count_baseline_recursive(walker, baseline, &mut count);
}
count
}
fn count_baseline_recursive(dir: fs::ReadDir, baseline: &str, count: &mut usize) {
for entry in dir.flatten() {
let path = entry.path();
if path.is_dir() {
if path.file_name().and_then(|n| n.to_str()) == Some(baseline) {
let estimates = path.join("estimates.json");
if estimates.exists() {
*count += 1;
}
} else if let Ok(sub) = fs::read_dir(&path) {
count_baseline_recursive(sub, baseline, count);
}
}
}
}
fn criterion_home() -> PathBuf {
std::env::var("CRITERION_HOME")
.map(PathBuf::from)
.or_else(|_| std::env::var("CARGO_TARGET_DIR").map(|p| PathBuf::from(p).join("criterion")))
.unwrap_or_else(|_| PathBuf::from("target/criterion"))
}
fn collect_json_output(version_label: Option<&str>) -> KmsCliResult<()> {
let home = criterion_home();
let mut entries: Vec<CtBenchmarkComplete> = Vec::new();
if home.exists() {
collect_estimates_recursive(&home, &home, version_label, &mut entries)?;
}
entries.sort_by(|a, b| a.id.cmp(&b.id));
for entry in &entries {
let line = serde_json::to_string(entry)
.map_err(|e| KmsCliError::Default(format!("JSON serialization: {e}")))?;
#[allow(clippy::print_stdout)]
{
println!("{line}");
}
}
let json_path = home.join("benchmarks.json");
let compat = serde_json::json!({ "benchmarks": entries });
let content = serde_json::to_string_pretty(&compat)
.map_err(|e| KmsCliError::Default(format!("JSON serialization: {e}")))?;
fs::write(&json_path, &content)
.map_err(|e| KmsCliError::Default(format!("Write {}: {e}", json_path.display())))?;
eprintln!("[bench] JSON results written to {}", json_path.display());
Ok(())
}
const fn make_ct_est(est: &CriterionEstimate) -> CtEst {
CtEst {
estimate: est.point_estimate,
lower_bound: est.confidence_interval.lower_bound,
upper_bound: est.confidence_interval.upper_bound,
unit: "ns",
}
}
fn apply_version_label(raw_id: &str, label: &str) -> String {
if let Some((section, rest)) = raw_id.split_once('/') {
let row = rest.replace('/', " - ");
format!("{section}/{label}/{row}")
} else {
format!("{raw_id}/{label}")
}
}
fn collect_estimates_recursive(
base: &Path,
dir: &Path,
version_label: Option<&str>,
results: &mut Vec<CtBenchmarkComplete>,
) -> KmsCliResult<()> {
let estimates_path = dir.join("new").join("estimates.json");
if estimates_path.exists() {
let content = fs::read_to_string(&estimates_path)
.map_err(|e| KmsCliError::Default(format!("Read {}: {e}", estimates_path.display())))?;
if let Ok(estimates) = serde_json::from_str::<CriterionEstimates>(&content) {
let raw_id = dir
.strip_prefix(base)
.unwrap_or(dir)
.to_string_lossy()
.to_string();
let id = version_label.map_or_else(
|| raw_id.clone(),
|label| apply_version_label(&raw_id, label),
);
let mean = make_ct_est(&estimates.mean);
let median = make_ct_est(&estimates.median);
let median_abs_dev = estimates.median_abs_dev.as_ref().map_or(
CtEst {
estimate: 0.0,
lower_bound: 0.0,
upper_bound: 0.0,
unit: "ns",
},
make_ct_est,
);
results.push(CtBenchmarkComplete {
reason: "benchmark-complete",
id,
report_directory: "",
iteration_count: vec![],
measured_values: vec![],
unit: "ns",
throughput: vec![],
typical: make_ct_est(&estimates.mean),
mean,
median,
median_abs_dev,
slope: None,
change: None,
});
}
}
if let Ok(dir_entries) = fs::read_dir(dir) {
for entry in dir_entries.flatten() {
if entry.file_type().is_ok_and(|t| t.is_dir())
&& entry.file_name() != "new"
&& entry.file_name() != "base"
&& entry.file_name() != "change"
{
collect_estimates_recursive(base, &entry.path(), version_label, results)?;
}
}
}
Ok(())
}
#[derive(Deserialize)]
struct BenchmarkMeta {
group_id: String,
function_id: Option<String>,
value_str: Option<String>,
#[allow(dead_code)]
full_id: String,
#[allow(dead_code)]
directory_name: String,
}
#[derive(Deserialize)]
struct SlopeEstimates {
slope: Option<SlopeEstimate>,
mean: CriterionEstimate,
}
#[derive(Deserialize)]
struct SlopeEstimate {
point_estimate: f64,
}
struct BenchPoint {
function_id: String,
value_str: String,
time_ns: f64,
}
fn format_time(ns: f64) -> String {
if ns < 1_000.0 {
format!("{ns:.2} ns")
} else if ns < 1_000_000.0 {
format!("{:.2} µs", ns / 1_000.0)
} else if ns < 1_000_000_000.0 {
format!("{:.2} ms", ns / 1_000_000.0)
} else {
format!("{:.2} s", ns / 1_000_000_000.0)
}
}
fn group_description(group_id: &str) -> Option<&'static str> {
match group_id {
"encrypt/aes-gcm" => {
Some("AES-GCM encrypt and decrypt (128/192/256-bit keys, 64-byte plaintext).")
}
"encrypt/aes-xts" => {
Some("AES-XTS encrypt and decrypt (128/256-bit AES, 64-byte plaintext).")
}
"encrypt/aes-gcm-siv" => {
Some("AES-GCM-SIV encrypt and decrypt (128/256-bit keys, 64-byte plaintext). Non-FIPS.")
}
"encrypt/chacha20-poly1305" => Some(
"ChaCha20-Poly1305 encrypt and decrypt (256-bit key, 64-byte plaintext). Non-FIPS.",
),
"encrypt/rsa-oaep" => {
Some("RSA-OAEP encrypt and decrypt (2048/3072/4096-bit keys, 32-byte plaintext).")
}
"encrypt/rsa-pkcs1v15" => Some(
"RSA PKCS#1 v1.5 encrypt and decrypt (2048/3072/4096-bit keys, 32-byte plaintext).",
),
"encrypt/ecies" => {
Some("ECIES encrypt and decrypt on NIST curves (P-256/P-384/P-521). Non-FIPS.")
}
"encrypt/salsa-sealed-box" => {
Some("Salsa Sealed Box (X25519) encrypt and decrypt. Non-FIPS.")
}
"encrypt/covercrypt" => Some("Covercrypt attribute-based encrypt and decrypt. Non-FIPS."),
"kem/configurable" => Some(
"Configurable KEM encapsulate and decapsulate (ML-KEM, hybrid variants). Non-FIPS.",
),
"kem/pqc" => Some(
"Standard PQC KEM encapsulate and decapsulate (ML-KEM, X25519MLKEM768, X448MLKEM1024). Non-FIPS.",
),
"key-creation/symmetric" => Some("AES (and ChaCha20 in non-FIPS) symmetric key creation."),
"key-creation/rsa" => Some("RSA key pair generation (2048/3072/4096-bit)."),
"key-creation/ec" => Some("Elliptic curve key pair generation (NIST and non-FIPS curves)."),
"key-creation/covercrypt" => Some("Covercrypt master key pair generation. Non-FIPS."),
"key-creation/kem" => {
Some("Configurable KEM key pair generation (ML-KEM, hybrid variants). Non-FIPS.")
}
"key-creation/pqc" => {
Some("PQC key pair generation (ML-KEM, ML-DSA, SLH-DSA, hybrid KEM). Non-FIPS.")
}
"sign-verify/ecdsa-p256" | "sign-verify/ecdsa-p384" | "sign-verify/ecdsa-p521" => {
Some("ECDSA sign and verify on NIST curves.")
}
"sign-verify/ecdsa-secp256k1" | "sign-verify/eddsa-ed25519" | "sign-verify/eddsa-ed448" => {
Some("Non-FIPS EC signature operations (secp256k1, Ed25519, Ed448).")
}
"sign-verify/rsa-pss" => Some("RSA-PSS sign and verify (SHA-256, 2048/3072/4096-bit)."),
"sign-verify/ml-dsa" => Some("ML-DSA sign and verify (ML-DSA-44/65/87). Non-FIPS."),
"sign-verify/slh-dsa" => Some(
"SLH-DSA (stateless hash-based) sign and verify (SHA2/SHAKE, 128/192/256). Non-FIPS.",
),
"batch/aes-gcm" => {
Some("AES-GCM batch — encrypt/decrypt N items in a single BulkData call.")
}
"batch/rsa-pkcs1v15" => {
Some("RSA PKCS#1 v1.5 batch — encrypt/decrypt N items in a single KMIP message.")
}
"batch/rsa-oaep" => {
Some("RSA-OAEP batch — encrypt/decrypt N items in a single KMIP message.")
}
"batch/rsa-aes-kwp" => {
Some("RSA AES Key Wrap batch — encrypt/decrypt N items in a single KMIP message.")
}
_ => None,
}
}
fn collect_bench_points(home: &Path) -> KmsCliResult<BTreeMap<String, Vec<BenchPoint>>> {
let mut groups: BTreeMap<String, Vec<BenchPoint>> = BTreeMap::new();
collect_bench_points_recursive(home, home, &mut groups)?;
Ok(groups)
}
fn collect_bench_points_recursive(
home: &Path,
dir: &Path,
groups: &mut BTreeMap<String, Vec<BenchPoint>>,
) -> KmsCliResult<()> {
let bm_path = dir.join("new").join("benchmark.json");
let est_path = dir.join("new").join("estimates.json");
if bm_path.exists() && est_path.exists() {
let bm_content = fs::read_to_string(&bm_path)
.map_err(|e| KmsCliError::Default(format!("Read {}: {e}", bm_path.display())))?;
let est_content = fs::read_to_string(&est_path)
.map_err(|e| KmsCliError::Default(format!("Read {}: {e}", est_path.display())))?;
if let (Ok(meta), Ok(est)) = (
serde_json::from_str::<BenchmarkMeta>(&bm_content),
serde_json::from_str::<SlopeEstimates>(&est_content),
) {
let time_ns = est
.slope
.map_or(est.mean.point_estimate, |s| s.point_estimate);
let function_id = meta.function_id.unwrap_or_default();
let value_str = meta.value_str.unwrap_or_default();
groups.entry(meta.group_id).or_default().push(BenchPoint {
function_id,
value_str,
time_ns,
});
}
}
if let Ok(entries) = fs::read_dir(dir) {
let mut sorted: Vec<_> = entries.flatten().collect();
sorted.sort_by_key(std::fs::DirEntry::file_name);
for entry in sorted {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if entry.file_type().is_ok_and(|t| t.is_dir())
&& name_str != "new"
&& name_str != "base"
&& name_str != "change"
&& name_str != "report"
{
collect_bench_points_recursive(home, &entry.path(), groups)?;
}
}
}
Ok(())
}
fn render_group_table(group_id: &str, points: &[BenchPoint]) -> String {
let mut out = String::new();
out.push_str(&format!("### {group_id}\n\n"));
if let Some(desc) = group_description(group_id) {
out.push_str(&format!("{desc}\n\n"));
}
let mut func_ids: Vec<String> = Vec::new();
let mut param_strs: Vec<String> = Vec::new();
for p in points {
if !func_ids.contains(&p.function_id) {
func_ids.push(p.function_id.clone());
}
if !p.value_str.is_empty() && !param_strs.contains(&p.value_str) {
param_strs.push(p.value_str.clone());
}
}
param_strs.sort_by(|a, b| {
let num_a = a
.split_whitespace()
.next()
.and_then(|s| s.parse::<u64>().ok());
let num_b = b
.split_whitespace()
.next()
.and_then(|s| s.parse::<u64>().ok());
match (num_a, num_b) {
(Some(na), Some(nb)) => na.cmp(&nb),
_ => a.cmp(b),
}
});
if param_strs.is_empty() {
out.push_str("| |");
for fid in &func_ids {
out.push_str(&format!(" `{fid}` |"));
}
out.push('\n');
out.push_str("| :--- |");
for _ in &func_ids {
out.push_str(" :--- |");
}
out.push('\n');
out.push_str("| |");
for fid in &func_ids {
let val = points
.iter()
.find(|p| p.function_id == *fid)
.map_or_else(|| "N/A".to_owned(), |p| format_time(p.time_ns));
out.push_str(&format!(" `{val}` |"));
}
out.push('\n');
} else {
out.push_str("| |");
for fid in &func_ids {
out.push_str(&format!(" `{fid}` |"));
}
out.push('\n');
out.push_str("| :--- |");
for _ in &func_ids {
out.push_str(" :--- |");
}
out.push('\n');
for param in ¶m_strs {
out.push_str(&format!("| **`{param}`** |"));
for fid in &func_ids {
let val = points
.iter()
.find(|p| p.function_id == *fid && p.value_str == *param)
.map_or_else(|| "N/A".to_owned(), |p| format_time(p.time_ns));
out.push_str(&format!(" `{val}` |"));
}
out.push('\n');
}
}
out
}
fn generate_markdown_output() -> KmsCliResult<()> {
let home = criterion_home();
if !home.exists() {
eprintln!("[bench] No criterion data found, skipping markdown generation");
return Ok(());
}
let groups = collect_bench_points(&home)?;
if groups.is_empty() {
eprintln!("[bench] No benchmark results found, skipping markdown generation");
return Ok(());
}
let mut md = String::from("## Benchmark Results\n\n");
for (group_id, points) in &groups {
md.push_str(&render_group_table(group_id, points));
md.push('\n');
}
let md_path = home.join("benchmarks.md");
fs::write(&md_path, &md)
.map_err(|e| KmsCliError::Default(format!("Write {}: {e}", md_path.display())))?;
eprintln!("[bench] Markdown report written to {}", md_path.display());
Ok(())
}
fn group_updated_since(dir: &Path, since: std::time::SystemTime) -> bool {
let Ok(entries) = fs::read_dir(dir) else {
return false;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if group_updated_since(&path, since) {
return true;
}
} else if path.file_name().is_some_and(|n| n == "estimates.json")
&& path
.metadata()
.and_then(|m| m.modified())
.is_ok_and(|t| t >= since)
{
return true;
}
}
false
}
fn generate_compact_output(run_start: std::time::SystemTime) -> KmsCliResult<()> {
let home = criterion_home();
let mut results: BTreeMap<String, bool> = BTreeMap::new();
if home.exists() {
let points = collect_bench_points(&home)?;
for group_id in points.keys() {
let dir_name = group_id.replace('/', "_");
let group_dir = home.join(&dir_name);
if group_updated_since(&group_dir, run_start) {
results.insert(group_id.clone(), true);
}
}
}
BENCH_KO.with(|r| {
for name in r.borrow().iter() {
results.entry(name.clone()).or_insert(false);
}
});
if results.is_empty() {
eprintln!("[bench] compact: no results collected");
return Ok(());
}
let width = results.keys().map(String::len).max().unwrap_or(40) + 2;
for (name, ok) in &results {
let status = if *ok { "OK" } else { "KO" };
#[allow(clippy::print_stdout)]
{
println!("{name:<width$} {status}");
}
}
let total = results.len();
let ok_cnt = results.values().filter(|&&v| v).count();
eprintln!(
"[bench] compact summary: {ok_cnt}/{total} OK, {} KO",
total - ok_cnt
);
Ok(())
}
#[derive(Debug, Serialize)]
pub struct LoadResult {
pub operation: String,
pub concurrency: usize,
pub throughput_rps: f64,
pub p50_ms: f64,
pub p95_ms: f64,
pub p99_ms: f64,
pub samples: usize,
}
#[derive(Clone)]
enum PreparedLoadOp {
AesGcmEncrypt { req: Encrypt },
AesSymCreate { vid: String },
EcdsaP256Sign { req: Sign },
AesGcmBatch10 { req: Encrypt },
}
impl PreparedLoadOp {
const fn name(&self) -> &'static str {
match self {
Self::AesGcmEncrypt { .. } => "encrypt/aes-gcm",
Self::AesSymCreate { .. } => "key-creation/aes-sym",
Self::EcdsaP256Sign { .. } => "sign-verify/ecdsa-p256",
Self::AesGcmBatch10 { .. } => "batch/aes-gcm-10",
}
}
async fn execute(&self, client: &KmsClient) -> bool {
match self {
Self::AesSymCreate { vid } => {
let Ok(req) = symmetric_key_create_request(
vid,
None,
256,
CryptographicAlgorithm::AES,
["load"],
false,
None,
) else {
return false;
};
client.create(req).await.is_ok()
}
Self::EcdsaP256Sign { req } => client.sign(req.clone()).await.is_ok(),
Self::AesGcmEncrypt { req } | Self::AesGcmBatch10 { req } => {
client.encrypt(req.clone()).await.is_ok()
}
}
}
}
fn load_percentile(sorted: &[f64], p: f64) -> f64 {
if sorted.is_empty() {
return 0.0;
}
#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
let idx = ((sorted.len() as f64 - 1.0) * p).round() as usize;
sorted[idx.min(sorted.len() - 1)]
}
fn parse_concurrency_levels(s: &str) -> KmsCliResult<Vec<usize>> {
s.split(',')
.map(str::trim)
.filter(|t| !t.is_empty())
.map(|t| {
t.parse::<usize>()
.map_err(|e| KmsCliError::Default(format!("Invalid concurrency level '{t}': {e}")))
})
.collect()
}
fn prepare_load_ops(rt: &Runtime, client: &KmsClient, mode: &BenchMode) -> Vec<PreparedLoadOp> {
let mut ops = Vec::new();
let needs_encrypt = matches!(mode, BenchMode::Encrypt | BenchMode::All);
let needs_key_create = matches!(mode, BenchMode::KeyCreation | BenchMode::All);
let needs_sign = matches!(mode, BenchMode::SignVerify | BenchMode::All);
let needs_batch = matches!(mode, BenchMode::Batch | BenchMode::All);
if needs_encrypt {
let key_id = create_sym_key(rt, client, 128, CryptographicAlgorithm::AES);
ops.push(PreparedLoadOp::AesGcmEncrypt {
req: Encrypt {
unique_identifier: Some(key_id),
cryptographic_parameters: Some(aes_gcm_params()),
data: Some(Zeroizing::new(vec![1_u8; 64])),
..Default::default()
},
});
}
if needs_key_create {
ops.push(PreparedLoadOp::AesSymCreate {
vid: client.config.vendor_id.clone(),
});
}
if needs_sign {
if let Some((_, priv_id)) = try_create_ec_kp(rt, client, RecommendedCurve::P256) {
ops.push(PreparedLoadOp::EcdsaP256Sign {
req: Sign {
unique_identifier: Some(priv_id),
cryptographic_parameters: Some(CryptographicParameters {
digital_signature_algorithm: Some(
DigitalSignatureAlgorithm::ECDSAWithSHA256,
),
..Default::default()
}),
data: Some(Zeroizing::new(vec![0x42_u8; 32])),
..Default::default()
},
});
} else {
eprintln!("[load] ECDSA P-256 key creation failed, skipping sign load test");
}
}
if needs_batch {
let key_id = create_sym_key(rt, client, 128, CryptographicAlgorithm::AES);
let key_str = key_id.to_string();
let data = BulkData::new(vec![Zeroizing::new(vec![1_u8; 64]); 10])
.serialize()
.expect("BulkData serialize");
let req = encrypt_request(
&key_str,
None,
data.to_vec(),
None,
None,
Some(aes_gcm_params()),
)
.expect("batch encrypt request");
ops.push(PreparedLoadOp::AesGcmBatch10 { req });
}
ops
}
async fn run_load_level(
client: KmsClient,
op: PreparedLoadOp,
concurrency: usize,
duration: Duration,
) -> LoadResult {
let wall_start = Instant::now();
let mut set: JoinSet<Vec<f64>> = JoinSet::new();
for _ in 0..concurrency {
let client = client.clone();
let op = op.clone();
set.spawn(async move {
let mut timings = Vec::new();
let task_start = Instant::now();
while task_start.elapsed() < duration {
let t0 = Instant::now();
let _ = op.execute(&client).await;
timings.push(t0.elapsed().as_secs_f64() * 1_000.0); }
timings
});
}
let mut all_timings: Vec<f64> = Vec::new();
while let Some(Ok(timings)) = set.join_next().await {
all_timings.extend(timings);
}
let elapsed = wall_start.elapsed().as_secs_f64();
let samples = all_timings.len();
let throughput_rps = if elapsed > 0.0 {
#[allow(clippy::cast_precision_loss)]
let s = samples as f64;
s / elapsed
} else {
0.0
};
all_timings.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
LoadResult {
operation: op.name().to_owned(),
concurrency,
throughput_rps,
p50_ms: load_percentile(&all_timings, 0.50),
p95_ms: load_percentile(&all_timings, 0.95),
p99_ms: load_percentile(&all_timings, 0.99),
samples,
}
}
fn bench_load(
rt: &Runtime,
client: &KmsClient,
mode: &BenchMode,
concurrency_levels: &[usize],
duration: Duration,
) -> Vec<LoadResult> {
let ops = prepare_load_ops(rt, client, mode);
if ops.is_empty() {
eprintln!("[load] No operations prepared for mode {mode:?}");
return Vec::new();
}
let mut results = Vec::new();
for op in ops {
eprintln!(
"[load] '{}' — sweep {:?} × {:.0}s",
op.name(),
concurrency_levels,
duration.as_secs_f64()
);
for &concurrency in concurrency_levels {
eprint!("[load] concurrency={concurrency:>3} … ");
let result = rt.block_on(run_load_level(
client.clone(),
op.clone(),
concurrency,
duration,
));
eprintln!(
"rps={:>8.1} p50={:>6.1}ms p95={:>6.1}ms p99={:>6.1}ms n={}",
result.throughput_rps, result.p50_ms, result.p95_ms, result.p99_ms, result.samples
);
results.push(result);
}
}
results
}
#[allow(clippy::print_stdout)]
fn print_load_results(results: &[LoadResult]) {
if results.is_empty() {
return;
}
let mut seen: Vec<&str> = Vec::new();
for r in results {
if !seen.contains(&r.operation.as_str()) {
seen.push(&r.operation);
}
}
for op in seen {
let op_rows: Vec<&LoadResult> = results.iter().filter(|r| r.operation == op).collect();
println!("\n── {op} ──");
println!(
"{:<14} {:>12} {:>10} {:>10} {:>10} {:>8}",
"Concurrency", "Throughput", "p50", "p95", "p99", "Samples"
);
println!("{}", "─".repeat(70));
for r in op_rows {
println!(
"{:<14} {:>10.1}/s {:>8.1}ms {:>8.1}ms {:>8.1}ms {:>8}",
r.concurrency, r.throughput_rps, r.p50_ms, r.p95_ms, r.p99_ms, r.samples
);
}
}
}
fn generate_markdown_load_output(results: &[LoadResult]) -> KmsCliResult<()> {
if results.is_empty() {
return Ok(());
}
let home = criterion_home();
fs::create_dir_all(&home)
.map_err(|e| KmsCliError::Default(format!("Create criterion dir: {e}")))?;
let mut seen: Vec<&str> = Vec::new();
for r in results {
if !seen.contains(&r.operation.as_str()) {
seen.push(&r.operation);
}
}
let mut md = String::new();
for op in seen {
let op_rows: Vec<&LoadResult> = results.iter().filter(|r| r.operation == op).collect();
md.push_str(&format!("### {op}\n\n"));
md.push_str(
"| Concurrency | Throughput (req/s) | p50 (ms) | p95 (ms) | p99 (ms) | Samples |\n",
);
md.push_str(
"|-------------|-------------------|----------|----------|----------|---------|\n",
);
for r in op_rows {
md.push_str(&format!(
"| {} | {:.1} | {:.1} | {:.1} | {:.1} | {} |\n",
r.concurrency, r.throughput_rps, r.p50_ms, r.p95_ms, r.p99_ms, r.samples
));
}
md.push('\n');
}
let md_path = home.join("benchmarks_load_tests.md");
fs::write(&md_path, &md)
.map_err(|e| KmsCliError::Default(format!("Write {}: {e}", md_path.display())))?;
eprintln!("[load] Markdown report → {}", md_path.display());
Ok(())
}
fn generate_html_output(results: &[LoadResult]) -> KmsCliResult<()> {
let home = criterion_home();
let report_dir = home.join("load-report");
fs::create_dir_all(&report_dir)
.map_err(|e| KmsCliError::Default(format!("Create report dir: {e}")))?;
let mut ops: Vec<String> = Vec::new();
for r in results {
if !ops.contains(&r.operation) {
ops.push(r.operation.clone());
}
}
let gnuplot_ok = std::process::Command::new("gnuplot")
.arg("--version")
.output()
.is_ok();
if !gnuplot_ok {
eprintln!("[load] gnuplot not found — SVG charts will be omitted from HTML report");
}
let mut sections = String::new();
for op in &ops {
let safe = op.replace('/', "_").replace(' ', "-");
let op_rows: Vec<&LoadResult> = results.iter().filter(|r| &r.operation == op).collect();
let dat_name = format!("{safe}.dat");
let dat_path = report_dir.join(&dat_name);
let mut dat =
String::from("# concurrency throughput_rps p50_ms p95_ms p99_ms samples\n");
for r in &op_rows {
dat.push_str(&format!(
"{:<14} {:<15.2} {:<8.2} {:<8.2} {:<8.2} {}\n",
r.concurrency, r.throughput_rps, r.p50_ms, r.p95_ms, r.p99_ms, r.samples
));
}
fs::write(&dat_path, &dat)
.map_err(|e| KmsCliError::Default(format!("Write {}: {e}", dat_path.display())))?;
let tp_svg = format!("{safe}-throughput.svg");
let lat_svg = format!("{safe}-latency.svg");
if gnuplot_ok {
let run_gnuplot = |script: &str, label: &str| {
let script_name = format!("{safe}-{label}.gnuplot");
let script_path = report_dir.join(&script_name);
if fs::write(&script_path, script).is_ok() {
let status = std::process::Command::new("gnuplot")
.arg(&script_name)
.current_dir(&report_dir)
.status();
if status.map_or(true, |s| !s.success()) {
eprintln!("[load] gnuplot {label} chart failed for {op}");
}
}
};
run_gnuplot(
&format!(
"set terminal svg size 800,400 enhanced font 'Helvetica,12'\n\
set output '{tp_svg}'\n\
set title 'Throughput — {op}'\n\
set xlabel 'Concurrency'\n\
set ylabel 'req/s'\n\
set grid\nset key top left\n\
plot '{dat_name}' using 1:2 with linespoints lw 2 pt 7 title 'throughput'\n"
),
"throughput",
);
run_gnuplot(
&format!(
"set terminal svg size 800,400 enhanced font 'Helvetica,12'\n\
set output '{lat_svg}'\n\
set title 'Latency — {op}'\n\
set xlabel 'Concurrency'\n\
set ylabel 'Latency (ms)'\n\
set grid\nset key top left\n\
plot '{dat_name}' using 1:3 with linespoints lw 2 pt 7 title 'p50', \\\n\
'{dat_name}' using 1:4 with linespoints lw 2 pt 5 title 'p95', \\\n\
'{dat_name}' using 1:5 with linespoints lw 2 pt 9 title 'p99'\n"
),
"latency",
);
}
sections.push_str(&format!("<section>\n<h2>{op}</h2>\n"));
if gnuplot_ok {
sections.push_str(&format!(
"<div class=\"charts\">\
<img src=\"{tp_svg}\" alt=\"Throughput\">\
<img src=\"{lat_svg}\" alt=\"Latency\">\
</div>\n"
));
}
sections.push_str(
"<table>\n<tr><th>Concurrency</th><th>Throughput (req/s)</th>\
<th>p50 (ms)</th><th>p95 (ms)</th><th>p99 (ms)</th><th>Samples</th></tr>\n",
);
for r in &op_rows {
sections.push_str(&format!(
"<tr><td>{}</td><td>{:.1}</td><td>{:.1}</td>\
<td>{:.1}</td><td>{:.1}</td><td>{}</td></tr>\n",
r.concurrency, r.throughput_rps, r.p50_ms, r.p95_ms, r.p99_ms, r.samples
));
}
sections.push_str("</table>\n</section>\n");
}
let gnuplot_notice = if gnuplot_ok {
String::new()
} else {
String::from(
"<p class=\"warn\">⚠ <code>gnuplot</code> not found — charts were not generated. \
Install gnuplot and re-run <code>ckms bench --load --format html</code>.</p>\n",
)
};
let html = format!(
"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n\
<title>KMS Load Test Report</title>\n<style>\n\
body{{font-family:sans-serif;max-width:1400px;margin:0 auto;padding:1em 2em}}\n\
h1{{border-bottom:2px solid #333;padding-bottom:.3em}}\n\
h2{{border-bottom:1px solid #ccc;margin-top:2em;color:#222}}\n\
table{{border-collapse:collapse;margin:1em 0}}\n\
th,td{{border:1px solid #bbb;padding:.35em .75em}}\n\
th{{background:#f4f4f4;text-align:center;font-weight:600}}\n\
td{{text-align:right}}\n\
td:first-child{{text-align:center}}\n\
.charts{{display:flex;gap:1em;flex-wrap:wrap;margin:.5em 0}}\n\
.charts img{{max-width:49%;min-width:280px;border:1px solid #ddd}}\n\
pre{{background:#f8f8f8;padding:1em;overflow-x:auto;font-size:.85em}}\n\
.warn{{background:#fff3cd;border:1px solid #ffc107;padding:.5em 1em;border-radius:4px}}\n\
section{{margin-bottom:2em}}\n\
</style>\n</head>\n<body>\n\
<h1>KMS Load Test Report</h1>\n\
{gnuplot_notice}\
{sections}\
</body>\n</html>\n"
);
let html_path = report_dir.join("index.html");
fs::write(&html_path, &html)
.map_err(|e| KmsCliError::Default(format!("Write {}: {e}", html_path.display())))?;
eprintln!("[load] HTML report → {}", html_path.display());
Ok(())
}
impl BenchAction {
pub async fn process(&self, kms_rest_client: KmsClient) -> KmsCliResult<()> {
let config = kms_rest_client.config.clone();
let mode = self.mode.clone();
let format = self.format.clone();
let speed = self.speed.clone();
let time = self.time;
let save_baseline = self.save_baseline.clone();
let load_baseline = self.load_baseline.clone();
let version_label = self.version_label.clone();
let load = self.load;
let load_concurrency = self.load_concurrency.clone();
drop(kms_rest_client);
tokio::task::spawn_blocking(move || -> KmsCliResult<()> {
let rt = tokio::runtime::Runtime::new()
.map_err(|e| KmsCliError::Default(format!("Runtime creation failed: {e}")))?;
let client = KmsClient::new_with_config(config)
.map_err(|e| KmsCliError::Default(e.to_string()))?;
let version = rt
.block_on(client.version())
.map_err(|e| KmsCliError::Default(format!("Server unreachable: {e}")))?;
eprintln!("[bench] Connected to KMS server version {version}");
bench_ko_reset();
let effective_format = if speed == BenchSpeed::Sanity && format == BenchFormat::Text {
BenchFormat::Compact
} else {
format.clone()
};
if load {
let concurrency_levels = parse_concurrency_levels(&load_concurrency)?;
let duration = Duration::from_secs(time.max(1));
let results = bench_load(&rt, &client, &mode, &concurrency_levels, duration);
print_load_results(&results);
if effective_format == BenchFormat::Html {
generate_html_output(&results)?;
}
if effective_format == BenchFormat::Markdown {
generate_markdown_load_output(&results)?;
}
return Ok(());
}
let run_start = std::time::SystemTime::now();
let mut c = match speed {
BenchSpeed::Sanity =>
{
Criterion::default()
.sample_size(10)
.measurement_time(Duration::from_millis(1))
.warm_up_time(Duration::from_millis(1))
}
BenchSpeed::Quick => Criterion::default()
.sample_size(10)
.measurement_time(Duration::from_secs(1))
.warm_up_time(Duration::from_millis(500)),
BenchSpeed::Normal => Criterion::default()
.sample_size(100)
.measurement_time(Duration::from_secs(time))
.warm_up_time(Duration::from_secs(3)),
};
if let Some(ref name) = save_baseline {
eprintln!("[bench] Saving baseline '{name}'");
c = c.save_baseline(name.clone());
} else if let Some(ref name) = load_baseline {
eprintln!("[bench] Comparing against baseline '{name}'");
c = c.retain_baseline(name.clone(), false);
}
std::io::stdout().flush().ok();
std::io::stderr().flush().ok();
let (saved_stdout_fd, saved_stderr_fd): (Option<i32>, Option<i32>) =
match effective_format {
BenchFormat::Json => {
#[allow(unsafe_code)]
let s = unsafe { libc::dup(1) };
#[allow(unsafe_code)]
unsafe {
libc::dup2(2, 1)
};
(if s < 0 { None } else { Some(s) }, None)
}
BenchFormat::Compact => {
#[allow(unsafe_code)]
let devnull = unsafe { libc::open(c"/dev/null".as_ptr(), libc::O_WRONLY) };
if devnull >= 0 {
#[allow(unsafe_code)]
let se = unsafe { libc::dup(2) };
#[allow(unsafe_code)]
unsafe {
libc::dup2(devnull, 2)
}; #[allow(unsafe_code)]
let so = unsafe { libc::dup(1) };
#[allow(unsafe_code)]
unsafe {
libc::dup2(2, 1); libc::close(devnull);
}
(
if so < 0 { None } else { Some(so) },
if se < 0 { None } else { Some(se) },
)
} else {
(None, None)
}
}
_ => (None, None),
};
let is_sanity = speed == BenchSpeed::Sanity;
match mode {
BenchMode::All => {
bench_encrypt(&mut c, &client, &rt);
bench_key_creation(&mut c, &client, &rt);
bench_sign_verify(&mut c, &client, &rt);
bench_batch(&mut c, &client, &rt, is_sanity);
}
BenchMode::Encrypt => {
bench_encrypt(&mut c, &client, &rt);
}
BenchMode::KeyCreation => {
bench_key_creation(&mut c, &client, &rt);
}
BenchMode::SignVerify => {
bench_sign_verify(&mut c, &client, &rt);
}
BenchMode::Batch => bench_batch(&mut c, &client, &rt, is_sanity),
}
drop(c);
if let Some(saved) = saved_stderr_fd {
#[allow(unsafe_code)]
unsafe {
libc::dup2(saved, 2);
libc::close(saved);
}
}
if let Some(saved) = saved_stdout_fd {
#[allow(unsafe_code)]
unsafe {
libc::dup2(saved, 1);
libc::close(saved);
}
}
if let Some(ref name) = save_baseline {
let home = criterion_home();
let count = count_baseline_files(&home, name);
eprintln!(
"[bench] Baseline '{name}' saved: {count} estimates written under {}",
home.display()
);
}
match effective_format {
BenchFormat::Json => collect_json_output(version_label.as_deref())?,
BenchFormat::Markdown => generate_markdown_output()?,
BenchFormat::Compact => generate_compact_output(run_start)?,
BenchFormat::Text | BenchFormat::Html => {}
}
Ok(())
})
.await
.map_err(|e| KmsCliError::Default(format!("Benchmark task panicked: {e}")))?
}
}