#![deny(unsafe_code)]
#![deny(missing_docs)]
use crate::tls::{ClientVerificationMode, TlsConfig, TlsMode};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecurityProperty {
Confidentiality,
ServerAuthentication,
ClientAuthentication,
ForwardSecrecy,
Integrity,
QuantumResistance,
}
#[derive(Debug, Clone)]
pub struct PropertyCheck {
pub property: SecurityProperty,
pub satisfied: bool,
pub reason: String,
}
pub struct SecurityProperties {
required: Vec<SecurityProperty>,
}
impl SecurityProperties {
#[must_use]
pub fn new() -> Self {
Self {
required: vec![
SecurityProperty::Confidentiality,
SecurityProperty::ServerAuthentication,
SecurityProperty::ForwardSecrecy,
SecurityProperty::Integrity,
],
}
}
#[must_use]
pub fn full() -> Self {
Self {
required: vec![
SecurityProperty::Confidentiality,
SecurityProperty::ServerAuthentication,
SecurityProperty::ClientAuthentication,
SecurityProperty::ForwardSecrecy,
SecurityProperty::Integrity,
SecurityProperty::QuantumResistance,
],
}
}
#[must_use]
pub fn require(mut self, property: SecurityProperty) -> Self {
if !self.required.contains(&property) {
self.required.push(property);
}
self
}
#[must_use]
pub fn verify(&self, config: &TlsConfig) -> Vec<PropertyCheck> {
self.required.iter().map(|prop| self.check_property(*prop, config)).collect()
}
#[must_use]
pub fn all_satisfied(&self, config: &TlsConfig) -> bool {
self.verify(config).iter().all(|c| c.satisfied)
}
#[allow(clippy::unused_self)] fn check_property(&self, property: SecurityProperty, config: &TlsConfig) -> PropertyCheck {
match property {
SecurityProperty::Confidentiality => PropertyCheck {
property,
satisfied: true, reason: "TLS 1.3 mandates authenticated encryption (AES-GCM/ChaCha20-Poly1305)"
.to_string(),
},
SecurityProperty::ServerAuthentication => PropertyCheck {
property,
satisfied: true, reason: "rustls verifies server certificate chain against system root store"
.to_string(),
},
SecurityProperty::ClientAuthentication => {
let satisfied = config.client_verification != ClientVerificationMode::None;
PropertyCheck {
property,
satisfied,
reason: if satisfied {
format!("Client verification mode: {:?}", config.client_verification)
} else {
"Client authentication not configured (mTLS disabled)".to_string()
},
}
}
SecurityProperty::ForwardSecrecy => PropertyCheck {
property,
satisfied: true, reason: "TLS 1.3 uses ephemeral key exchange (X25519/ML-KEM) for all sessions"
.to_string(),
},
SecurityProperty::Integrity => PropertyCheck {
property,
satisfied: true, reason: "AEAD cipher suites provide authenticated encryption with integrity"
.to_string(),
},
SecurityProperty::QuantumResistance => {
let satisfied = config.mode != TlsMode::Classic;
PropertyCheck {
property,
satisfied,
reason: if satisfied {
format!("{:?} mode uses ML-KEM for quantum resistance", config.mode)
} else {
"Classic mode uses only classical key exchange (X25519)".to_string()
},
}
}
}
}
}
impl Default for SecurityProperties {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_standard_properties_satisfied_by_default_succeeds() {
let checker = SecurityProperties::new();
let config = TlsConfig::new();
assert!(checker.all_satisfied(&config));
}
#[test]
fn test_full_properties_fail_without_mtls_fails() {
let checker = SecurityProperties::full();
let config = TlsConfig::new(); assert!(!checker.all_satisfied(&config));
let checks = checker.verify(&config);
let client_auth =
checks.iter().find(|c| c.property == SecurityProperty::ClientAuthentication);
assert!(client_auth.is_some());
assert!(!client_auth.expect("checked above").satisfied);
}
#[test]
fn test_quantum_resistance_fails_classic_mode_fails() {
let checker = SecurityProperties::new().require(SecurityProperty::QuantumResistance);
let config = TlsConfig { mode: TlsMode::Classic, ..TlsConfig::default() };
assert!(!checker.all_satisfied(&config));
}
#[test]
fn test_quantum_resistance_passes_hybrid_mode_succeeds() {
let checker = SecurityProperties::new().require(SecurityProperty::QuantumResistance);
let config = TlsConfig::new(); assert!(checker.all_satisfied(&config));
}
#[test]
fn test_verify_returns_all_checks_succeeds() {
let checker = SecurityProperties::new();
let config = TlsConfig::new();
let checks = checker.verify(&config);
assert_eq!(checks.len(), 4);
}
#[test]
fn test_require_adds_property_succeeds() {
let checker = SecurityProperties::new().require(SecurityProperty::QuantumResistance);
let config = TlsConfig::new();
let checks = checker.verify(&config);
assert_eq!(checks.len(), 5);
}
#[test]
fn test_require_deduplicates_succeeds() {
let checker = SecurityProperties::new().require(SecurityProperty::Confidentiality); let config = TlsConfig::new();
let checks = checker.verify(&config);
assert_eq!(checks.len(), 4); }
#[test]
fn test_property_check_debug_succeeds() {
let check = PropertyCheck {
property: SecurityProperty::Confidentiality,
satisfied: true,
reason: "test".to_string(),
};
let debug = format!("{:?}", check);
assert!(debug.contains("Confidentiality"));
}
#[test]
fn test_security_property_eq_succeeds() {
assert_eq!(SecurityProperty::Confidentiality, SecurityProperty::Confidentiality);
assert_ne!(SecurityProperty::Confidentiality, SecurityProperty::Integrity);
}
}