| use serde::{Deserialize, Serialize}; |
|
|
| #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
| pub enum Severity { |
| Critical, |
| Warning, |
| Info, |
| } |
|
|
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct ParamDiagnostic { |
| pub severity: Severity, |
| pub message: String, |
| pub suggested_fix: Option<String>, |
| } |
|
|
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| #[serde(rename_all = "camelCase")] |
| pub struct ValidationResult { |
| pub n: usize, |
| pub q: u64, |
| pub beta_or_sigma: f64, |
| pub is_cbd: bool, |
|
|
| pub noise_budget_max: f64, |
| pub noise_budget_rms: f64, |
| pub decryption_budget: f64, |
| pub failure_prob: f64, |
| pub expected_failures_in_500: f64, |
|
|
| pub passes: bool, |
| pub diagnostics: Vec<ParamDiagnostic>, |
|
|
| pub suggested_beta: f64, |
| pub suggested_q: u64, |
| } |
|
|
| pub struct ParamValidator; |
|
|
| impl ParamValidator { |
| pub fn validate_lwe(n: usize, q: u64, beta: f64) -> ValidationResult { |
| Self::validate_inner(n, q, beta, false) |
| } |
|
|
| pub fn validate_kyber(n: usize, q: u64, eta: usize) -> ValidationResult { |
| let mut r = Self::validate_inner(n, q, eta as f64, true); |
| r.beta_or_sigma = eta as f64; |
| r |
| } |
|
|
| pub fn validate_gaussian(n: usize, q: u64, sigma: f64) -> ValidationResult { |
| Self::validate_inner(n, q, sigma, false) |
| } |
|
|
| fn validate_inner(n: usize, q: u64, noise_param: f64, is_cbd: bool) -> ValidationResult { |
| let q_f = q as f64; |
| let n_f = n as f64; |
|
|
| let std_per_coeff = if is_cbd { |
| (noise_param / 2.0).sqrt() |
| } else { |
| noise_param |
| }; |
|
|
| let beta_max = if is_cbd { noise_param } else { 3.0 * std_per_coeff }; |
|
|
| let noise_budget_rms = std_per_coeff * (3.0 * n_f).sqrt() * beta_max; |
| let noise_budget_max = beta_max * beta_max * n_f * 3.0; |
| let decryption_budget = q_f / 4.0; |
|
|
| let total_std = std_per_coeff * beta_max * (3.0 * n_f).sqrt(); |
| let z = decryption_budget / total_std.max(1e-10); |
| let failure_prob = 2.0 * gaussian_q_function(z); |
|
|
| let expected_failures_in_500 = failure_prob * 500.0; |
| let passes = noise_budget_rms < decryption_budget * 0.8; |
|
|
| let mut diagnostics = Vec::new(); |
|
|
| if noise_budget_max >= decryption_budget && expected_failures_in_500 > 1e-3 { |
| diagnostics.push(ParamDiagnostic { |
| severity: Severity::Critical, |
| message: format!( |
| "WORST-CASE noise ({:.1}) EXCEEDS expected limit and probability of failure is high. Decryption WILL fail for some inputs.", |
| noise_budget_max |
| ), |
| suggested_fix: Some(format!( |
| "Either: (a) reduce β to ≤{:.1}, or (b) increase q to ≥{}", |
| (decryption_budget / (n_f * 3.0)).sqrt(), |
| (noise_budget_max * 4.0).ceil() as u64 |
| )), |
| }); |
| } else if noise_budget_max >= decryption_budget { |
| diagnostics.push(ParamDiagnostic { |
| severity: Severity::Info, |
| message: format!( |
| "WORST-CASE noise bound ({:.1}) exceeds budget ({:.1}), but failure probability is low. Typical for practical RLWE.", |
| noise_budget_max, decryption_budget |
| ), |
| suggested_fix: None, |
| }); |
| } else if noise_budget_max > decryption_budget * 0.85 { |
| diagnostics.push(ParamDiagnostic { |
| severity: Severity::Warning, |
| message: format!( |
| "WORST-CASE noise ({:.1}) is dangerously close to budget ({:.1}). Rare failures may occur.", |
| noise_budget_max, decryption_budget |
| ), |
| suggested_fix: Some("Increase q or decrease noise to increase margin".to_string()), |
| }); |
| } else if noise_budget_rms >= decryption_budget * 0.5 { |
| diagnostics.push(ParamDiagnostic { |
| severity: Severity::Warning, |
| message: format!( |
| "RMS noise ({:.1}) is {:.0}% of budget ({:.1}). Failure rate ≈{:.2}%. Marginal parameters.", |
| noise_budget_rms, |
| 100.0 * noise_budget_rms / decryption_budget, |
| decryption_budget, |
| failure_prob * 100.0 |
| ), |
| suggested_fix: Some("Increase q by 2x or decrease β by √2".to_string()), |
| }); |
| } else { |
| diagnostics.push(ParamDiagnostic { |
| severity: Severity::Info, |
| message: format!( |
| "Noise budget OK: RMS noise {:.1} is {:.0}% of q/4={:.1}.", |
| noise_budget_rms, |
| 100.0 * noise_budget_rms / decryption_budget, |
| decryption_budget |
| ), |
| suggested_fix: None, |
| }); |
| } |
|
|
| let estimated_security = estimate_security_bits(n, q, std_per_coeff); |
| if estimated_security < 80.0 { |
| diagnostics.push(ParamDiagnostic { |
| severity: Severity::Critical, |
| message: format!( |
| "Parameters provide only ~{:.0} bits of security (need ≥128).", |
| estimated_security |
| ), |
| suggested_fix: Some("Use n≥256 and q≥3329 for any security claim".to_string()), |
| }); |
| } |
|
|
| let suggested_beta = (decryption_budget / (n_f * 3.0)).sqrt() * 0.5; |
| let suggested_q = (noise_budget_max * 8.0).ceil() as u64; |
|
|
| ValidationResult { |
| n, |
| q, |
| beta_or_sigma: noise_param, |
| is_cbd, |
| noise_budget_max, |
| noise_budget_rms, |
| decryption_budget, |
| failure_prob, |
| expected_failures_in_500, |
| passes, |
| diagnostics, |
| suggested_beta, |
| suggested_q, |
| } |
| } |
| } |
|
|
| fn gaussian_q_function(z: f64) -> f64 { |
| if z > 10.0 { return 0.0; } |
| if z < 0.0 { return 1.0; } |
| let t = 1.0 / (1.0 + 0.2316419 * z); |
| let poly = t * (0.319381530 |
| + t * (-0.356563782 |
| + t * (1.781477937 |
| + t * (-1.821255978 |
| + t * 1.330274429)))); |
| let phi = (-z * z / 2.0).exp() / (2.0 * std::f64::consts::PI).sqrt(); |
| phi * poly |
| } |
|
|
| fn estimate_security_bits(n: usize, q: u64, sigma: f64) -> f64 { |
| let n_f = n as f64; |
| let q_f = q as f64; |
| let alpha = sigma / q_f; |
| if alpha <= 0.0 { return 0.0; } |
| 0.265 * n_f * (-alpha.log2()) |
| } |
|
|