use std::time::{Duration, SystemTime};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Security {
tls_version: Option<String>,
cipher_suite: Option<String>,
alpn: Vec<String>,
certificate: Option<CertificateInfo>,
}
impl Security {
pub fn new() -> Self {
Self {
tls_version: None,
cipher_suite: None,
alpn: Vec::new(),
certificate: None,
}
}
pub fn tls_version(&self) -> Option<&str> {
self.tls_version.as_deref()
}
pub fn cipher_suite(&self) -> Option<&str> {
self.cipher_suite.as_deref()
}
pub fn alpn(&self) -> &[String] {
&self.alpn
}
pub fn certificate(&self) -> Option<&CertificateInfo> {
self.certificate.as_ref()
}
pub fn with_tls_version(mut self, version: impl Into<String>) -> Self {
self.tls_version = Some(version.into());
self
}
pub fn with_cipher_suite(mut self, cipher: impl Into<String>) -> Self {
self.cipher_suite = Some(cipher.into());
self
}
pub fn add_alpn(mut self, protocol: impl Into<String>) -> Self {
let proto_str = protocol.into();
if !self.alpn.contains(&proto_str) {
self.alpn.push(proto_str);
}
self
}
pub fn with_certificate(mut self, cert: CertificateInfo) -> Self {
self.certificate = Some(cert);
self
}
pub fn merge(&mut self, other: Security) {
if self.tls_version.is_none() {
self.tls_version = other.tls_version;
}
if self.cipher_suite.is_none() {
self.cipher_suite = other.cipher_suite;
}
if self.certificate.is_none() {
self.certificate = other.certificate;
}
for protocol in other.alpn {
if !self.alpn.contains(&protocol) {
self.alpn.push(protocol);
}
}
}
pub fn is_cert_valid(&self) -> bool {
self.is_cert_valid_at(SystemTime::now())
}
pub fn is_cert_valid_at(&self, target_time: SystemTime) -> bool {
self.certificate
.as_ref()
.is_some_and(|c| target_time >= c.validity_start() && target_time <= c.validity_end())
}
pub fn is_cert_expiring(&self, threshold: Duration) -> bool {
let now = SystemTime::now();
self.certificate.as_ref().is_some_and(|c| {
if now < c.validity_start() || now > c.validity_end() {
return false;
}
c.validity_end() < now + threshold
})
}
}
impl Default for Security {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CertificateInfo {
common_name: String,
sans: Vec<String>,
issuer: String,
validity_start: SystemTime,
validity_end: SystemTime,
pubkey_type: String,
pubkey_bits: u32,
fingerprint_sha256: String,
}
impl CertificateInfo {
#[allow(clippy::too_many_arguments)]
pub fn new(
common_name: impl Into<String>,
sans: Vec<String>,
issuer: impl Into<String>,
validity_start: SystemTime,
validity_end: SystemTime,
pubkey_type: impl Into<String>,
pubkey_bits: u32,
fingerprint_sha256: impl Into<String>,
) -> Self {
Self {
common_name: common_name.into(),
sans,
issuer: issuer.into(),
validity_start,
validity_end,
pubkey_type: pubkey_type.into(),
pubkey_bits,
fingerprint_sha256: fingerprint_sha256.into(),
}
}
pub fn common_name(&self) -> &str {
&self.common_name
}
pub fn sans(&self) -> &[String] {
&self.sans
}
pub fn issuer(&self) -> &str {
&self.issuer
}
pub fn validity_start(&self) -> SystemTime {
self.validity_start
}
pub fn validity_end(&self) -> SystemTime {
self.validity_end
}
pub fn pubkey_type(&self) -> &str {
&self.pubkey_type
}
pub fn pubkey_bits(&self) -> u32 {
self.pubkey_bits
}
pub fn fingerprint_sha256(&self) -> &str {
&self.fingerprint_sha256
}
}
#[cfg(test)]
mod tests {
use super::*;
fn mock_cert(start_offset: i64, end_offset: i64) -> CertificateInfo {
let now = SystemTime::now();
let start = if start_offset < 0 {
now - Duration::from_secs(start_offset.unsigned_abs())
} else {
now + Duration::from_secs(start_offset as u64)
};
let end = if end_offset < 0 {
now - Duration::from_secs(end_offset.unsigned_abs())
} else {
now + Duration::from_secs(end_offset as u64)
};
CertificateInfo::new(
"test.local",
vec!["*.test.local".into()],
"Local CA",
start,
end,
"RSA",
2048,
"deadbeef",
)
}
#[test]
fn security_builder_pattern() {
let sec = Security::new()
.with_tls_version("TLSv1.3")
.with_cipher_suite("TLS_AES_256_GCM_SHA384")
.add_alpn("h2")
.add_alpn("http/1.1");
assert_eq!(sec.tls_version(), Some("TLSv1.3"));
assert_eq!(sec.cipher_suite(), Some("TLS_AES_256_GCM_SHA384"));
assert_eq!(sec.alpn().len(), 2);
}
#[test]
fn security_merge_logic() {
let mut s1 = Security::new()
.with_tls_version("TLSv1.2")
.add_alpn("http/1.1");
let s2 = Security::new()
.with_cipher_suite("AES128-GCM")
.add_alpn("h2")
.add_alpn("http/1.1");
s1.merge(s2);
assert_eq!(s1.tls_version(), Some("TLSv1.2"));
assert_eq!(s1.cipher_suite(), Some("AES128-GCM"));
assert_eq!(s1.alpn().len(), 2);
assert!(s1.alpn().contains(&"h2".to_string()));
}
#[test]
fn certificate_validity_lifecycle() {
let valid_cert = mock_cert(-864000, 864000);
let sec_valid = Security::new().with_certificate(valid_cert);
assert!(sec_valid.is_cert_valid());
assert!(!sec_valid.is_cert_expiring(Duration::from_secs(86400 * 5)));
assert!(sec_valid.is_cert_expiring(Duration::from_secs(86400 * 15)));
let expired_cert = mock_cert(-864000, -432000);
let sec_expired = Security::new().with_certificate(expired_cert);
assert!(!sec_expired.is_cert_valid());
assert!(!sec_expired.is_cert_expiring(Duration::from_secs(86400 * 30)));
let future_cert = mock_cert(86400, 864000);
let sec_future = Security::new().with_certificate(future_cert);
assert!(!sec_future.is_cert_valid());
}
}