use std::path::{
Path,
PathBuf,
};
use std::{
fs,
io,
};
use crate::report::SelfCertReport;
#[cfg(any(
feature = "mlkem",
feature = "mldsa",
feature = "lattice-zkp-hardened",
feature = "hqc-hardened"
))]
use crate::report::{
Channel,
EvaluationReport,
};
#[derive(Clone, Copy, Debug)]
pub struct BatteryConfig {
pub samples_per_class: usize,
pub abs_t_threshold: f64,
pub hqc_all_parameter_sets: bool,
}
impl Default for BatteryConfig {
fn default() -> Self {
Self {
samples_per_class: 10_000,
abs_t_threshold: crate::evaluation::DEFAULT_TVLA_ABS_T,
hqc_all_parameter_sets: true,
}
}
}
impl BatteryConfig {
#[must_use]
pub fn smoke() -> Self {
Self {
samples_per_class: 4,
abs_t_threshold: crate::evaluation::DEFAULT_TVLA_ABS_T,
hqc_all_parameter_sets: false,
}
}
}
#[cfg(any(
feature = "mlkem",
feature = "mldsa",
feature = "lattice-zkp-hardened",
feature = "hqc-hardened"
))]
#[must_use]
pub fn run_timing_battery(config: BatteryConfig) -> SelfCertReport {
let mut report = SelfCertReport::new();
#[cfg(feature = "mlkem")]
{
let (fixed, random) =
crate::evaluation::mlkem_decaps_tvla_timings(config.samples_per_class);
report.push(EvaluationReport::new(
"lib-q-ml-kem:decapsulate",
Channel::WallClockTiming,
config.samples_per_class,
crate::welch_t_statistic(&fixed, &random),
config.abs_t_threshold,
"fixed dk/ct vs rotated dk/ct; MlKem768 hardened decapsulation",
));
}
#[cfg(feature = "mldsa")]
{
let (fixed, random) = crate::evaluation::mldsa_sign_tvla_timings(config.samples_per_class);
report.push(EvaluationReport::new(
"lib-q-ml-dsa:sign",
Channel::WallClockTiming,
config.samples_per_class,
crate::welch_t_statistic(&fixed, &random),
config.abs_t_threshold,
"fixed signing key vs rotated signing key; ML-DSA-44 hardened signing",
));
}
#[cfg(feature = "lattice-zkp-hardened")]
{
let (fixed, random) =
crate::evaluation::lattice_zkp_prove_opening_tvla_timings(config.samples_per_class);
report.push(EvaluationReport::new(
"lib-q-lattice-zkp:prove_opening",
Channel::WallClockTiming,
config.samples_per_class,
crate::welch_t_statistic(&fixed, &random),
config.abs_t_threshold,
"fixed token opening vs rotated token header; hardened fixed-iteration prover",
));
}
#[cfg(feature = "hqc-hardened")]
{
use lib_q_hqc::{
Hqc1Params,
Hqc3Params,
Hqc5Params,
};
macro_rules! push_hqc_target {
($id:literal, $params:ty, $timings:ident, $notes:literal) => {{
let (fixed, random) =
crate::evaluation::$timings::<$params>(config.samples_per_class);
report.push(EvaluationReport::new(
$id,
Channel::WallClockTiming,
config.samples_per_class,
crate::welch_t_statistic(&fixed, &random),
config.abs_t_threshold,
$notes,
));
}};
}
push_hqc_target!(
"lib-q-hqc:hqc128_keygen",
Hqc1Params,
hqc_keygen_tvla_timings,
"fixed 48-byte KEM seed vs rotated seed; HQC-128 keygen (hardened)"
);
push_hqc_target!(
"lib-q-hqc:hqc128_encapsulate",
Hqc1Params,
hqc_encapsulate_tvla_timings,
"fixed pk/SHAKE PRNG vs rotated pk/PRNG; HQC-128 encapsulation (hardened)"
);
push_hqc_target!(
"lib-q-hqc:hqc128_decapsulate",
Hqc1Params,
hqc_decapsulate_tvla_timings,
"fixed sk/ct vs rotated sk/ct; HQC-128 decapsulation (hardened)"
);
if config.hqc_all_parameter_sets {
push_hqc_target!(
"lib-q-hqc:hqc192_keygen",
Hqc3Params,
hqc_keygen_tvla_timings,
"fixed 48-byte KEM seed vs rotated seed; HQC-192 keygen (hardened)"
);
push_hqc_target!(
"lib-q-hqc:hqc256_keygen",
Hqc5Params,
hqc_keygen_tvla_timings,
"fixed 48-byte KEM seed vs rotated seed; HQC-256 keygen (hardened)"
);
push_hqc_target!(
"lib-q-hqc:hqc192_encapsulate",
Hqc3Params,
hqc_encapsulate_tvla_timings,
"fixed pk/SHAKE PRNG vs rotated pk/PRNG; HQC-192 encapsulation (hardened)"
);
push_hqc_target!(
"lib-q-hqc:hqc256_encapsulate",
Hqc5Params,
hqc_encapsulate_tvla_timings,
"fixed pk/SHAKE PRNG vs rotated pk/PRNG; HQC-256 encapsulation (hardened)"
);
push_hqc_target!(
"lib-q-hqc:hqc192_decapsulate",
Hqc3Params,
hqc_decapsulate_tvla_timings,
"fixed sk/ct vs rotated sk/ct; HQC-192 decapsulation (hardened)"
);
push_hqc_target!(
"lib-q-hqc:hqc256_decapsulate",
Hqc5Params,
hqc_decapsulate_tvla_timings,
"fixed sk/ct vs rotated sk/ct; HQC-256 decapsulation (hardened)"
);
}
}
report
}
#[cfg(not(any(
feature = "mlkem",
feature = "mldsa",
feature = "lattice-zkp-hardened",
feature = "hqc-hardened"
)))]
#[must_use]
pub fn run_timing_battery(_config: BatteryConfig) -> SelfCertReport {
SelfCertReport::new()
}
pub fn write_evidence_package(
dir: &Path,
report: &SelfCertReport,
) -> io::Result<(PathBuf, PathBuf)> {
fs::create_dir_all(dir)?;
let json_path = dir.join("report.json");
let md_path = dir.join("report.md");
fs::write(&json_path, report.to_json())?;
fs::write(&md_path, report.to_markdown())?;
Ok((json_path, md_path))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::report::Channel;
#[test]
fn battery_runs_for_active_features() {
let report = run_timing_battery(BatteryConfig::smoke());
#[cfg(any(
feature = "mlkem",
feature = "mldsa",
feature = "lattice-zkp-hardened",
feature = "hqc-hardened"
))]
assert!(
!report.reports.is_empty(),
"expected at least one hardened target in the battery"
);
for entry in &report.reports {
assert_eq!(entry.channel, Channel::WallClockTiming);
assert!((entry.abs_t_threshold - 4.5).abs() < f64::EPSILON);
}
let _ = report.to_json();
let _ = report.to_markdown();
}
#[test]
fn evidence_package_round_trips_to_disk() {
let report = run_timing_battery(BatteryConfig::smoke());
let mut dir = std::env::temp_dir();
dir.push(format!("libq-sca-self-cert-test-{}", std::process::id()));
let (json_path, md_path) =
write_evidence_package(&dir, &report).expect("write evidence package");
let json = fs::read_to_string(&json_path).expect("read json");
let md = fs::read_to_string(&md_path).expect("read md");
assert!(json.contains("\"schema\":\"libq.sca.self-cert.v1\""));
assert!(md.contains("self-certification report"));
let _ = fs::remove_dir_all(&dir);
}
}