use crate::core::TdsResult;
use crate::error::Error;
use native_tls::Certificate;
use std::fs;
use std::path::Path;
use tracing::{debug, info};
pub fn load_certificate_from_file(path: &Path) -> TdsResult<Vec<u8>> {
debug!("Loading certificate from file: {path:?}");
if !path.exists() {
return Err(Error::CertificateNotFound {
path: path.to_path_buf(),
});
}
let cert_data = fs::read(path).map_err(|e| Error::CertificateFileIoError {
path: path.to_path_buf(),
error: e.to_string(),
})?;
let certificate = Certificate::from_pem(&cert_data)
.or_else(|_| {
debug!("Not PEM format, trying DER");
Certificate::from_der(&cert_data)
})
.map_err(|_| Error::InvalidCertificateFormat {
path: path.to_path_buf(),
})?;
let der_data = certificate
.to_der()
.map_err(|_| Error::InvalidCertificateFormat {
path: path.to_path_buf(),
})?;
info!(
"Successfully loaded certificate from: {path:?} ({} bytes)",
der_data.len()
);
Ok(der_data)
}
pub fn is_certificate_expired(der_data: &[u8]) -> TdsResult<bool> {
use x509_parser::prelude::*;
let (_, cert) = X509Certificate::from_der(der_data).map_err(|e| {
Error::ProtocolError(format!(
"Failed to parse certificate for expiry check: {}",
e
))
})?;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| Error::ImplementationError(format!("System time error: {}", e)))?;
let not_after = cert.validity().not_after.timestamp();
Ok(now.as_secs() as i64 > not_after)
}
pub fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut result = 0u8;
for (byte_a, byte_b) in a.iter().zip(b.iter()) {
result |= byte_a ^ byte_b;
}
result == 0
}
pub fn validate_server_certificate(user_cert_path: &Path, server_cert_der: &[u8]) -> TdsResult<()> {
info!("Validating server certificate against: {user_cert_path:?}");
let user_cert_der = load_certificate_from_file(user_cert_path)?;
if is_certificate_expired(server_cert_der)? {
return Err(Error::CertificateExpired);
}
if !constant_time_compare(&user_cert_der, server_cert_der) {
debug!(
"Certificate mismatch: user cert size={}, server cert size={}",
user_cert_der.len(),
server_cert_der.len()
);
return Err(Error::CertificateMismatch);
}
info!("Server certificate validation successful");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_constant_time_compare_equal() {
let a = vec![1, 2, 3, 4, 5];
let b = vec![1, 2, 3, 4, 5];
assert!(constant_time_compare(&a, &b));
}
#[test]
fn test_constant_time_compare_different() {
let a = vec![1, 2, 3, 4, 5];
let b = vec![1, 2, 3, 4, 6];
assert!(!constant_time_compare(&a, &b));
}
#[test]
fn test_constant_time_compare_different_sizes() {
let a = vec![1, 2, 3];
let b = vec![1, 2, 3, 4];
assert!(!constant_time_compare(&a, &b));
}
#[test]
fn test_load_certificate_file_not_found() {
let p = Path::new("/nonexistent/path/cert.cer");
let result = load_certificate_from_file(p);
assert!(result.is_err());
match result {
Err(Error::CertificateNotFound { path }) => {
assert_eq!(path, p);
}
_ => panic!("Expected CertificateNotFound error"),
}
}
#[test]
fn test_load_certificate_from_pem() {
let cert_path = Path::new("tests/test_certificates/valid_cert.pem");
let result = load_certificate_from_file(cert_path);
match result {
Ok(der_bytes) => {
assert!(!der_bytes.is_empty(), "DER bytes should not be empty");
assert_eq!(
der_bytes[0], 0x30,
"DER certificate should start with SEQUENCE tag"
);
}
Err(e) => panic!("Failed to load PEM certificate: {:?}", e),
}
}
#[test]
fn test_load_certificate_from_der() {
let cert_path = Path::new("tests/test_certificates/valid_cert.der");
let result = load_certificate_from_file(cert_path);
match result {
Ok(der_bytes) => {
assert!(!der_bytes.is_empty(), "DER bytes should not be empty");
assert_eq!(
der_bytes[0], 0x30,
"DER certificate should start with SEQUENCE tag"
);
}
Err(e) => panic!("Failed to load DER certificate: {:?}", e),
}
}
#[test]
fn test_load_certificate_invalid_format() {
let cert_path = Path::new("tests/test_certificates/invalid_format.txt");
let result = load_certificate_from_file(cert_path);
assert!(result.is_err(), "Should fail to load invalid certificate");
match result {
Err(Error::InvalidCertificateFormat { path, .. }) => {
assert_eq!(path, cert_path);
}
Err(e) => panic!("Expected InvalidCertificateFormat error, got: {:?}", e),
Ok(_) => panic!("Should not succeed loading invalid certificate"),
}
}
#[test]
fn test_pem_and_der_certificates_produce_same_der() {
let pem_path = Path::new("tests/test_certificates/valid_cert.pem");
let der_path = Path::new("tests/test_certificates/valid_cert.der");
let pem_result = load_certificate_from_file(pem_path);
let der_result = load_certificate_from_file(der_path);
assert!(
pem_result.is_ok(),
"PEM certificate should load successfully"
);
assert!(
der_result.is_ok(),
"DER certificate should load successfully"
);
let pem_der = pem_result.unwrap();
let der_der = der_result.unwrap();
assert_eq!(
pem_der, der_der,
"PEM and DER files should produce identical DER encodings"
);
}
#[test]
fn test_is_certificate_expired_valid() {
let cert_path = Path::new("tests/test_certificates/valid_cert.pem");
let der_bytes =
load_certificate_from_file(cert_path).expect("Failed to load test certificate");
let result = is_certificate_expired(&der_bytes);
assert!(result.is_ok(), "Certificate expiry check should succeed");
assert!(!result.unwrap(), "Test certificate should not be expired");
}
#[test]
fn test_constant_time_compare_all_zeros() {
let a = vec![0u8; 100];
let b = vec![0u8; 100];
assert!(
constant_time_compare(&a, &b),
"All zeros should compare equal"
);
}
#[test]
fn test_constant_time_compare_single_bit_difference() {
let mut a = vec![0u8; 32];
let mut b = vec![0u8; 32];
b[16] = 0x01;
assert!(
!constant_time_compare(&a, &b),
"Single bit difference should be detected"
);
a = vec![0u8; 32];
b = vec![0u8; 32];
b[0] = 0x80; assert!(
!constant_time_compare(&a, &b),
"Difference at start should be detected"
);
a = vec![0u8; 32];
b = vec![0u8; 32];
b[31] = 0x01; assert!(
!constant_time_compare(&a, &b),
"Difference at end should be detected"
);
}
#[test]
fn test_constant_time_compare_empty_slices() {
let a: Vec<u8> = vec![];
let b: Vec<u8> = vec![];
assert!(
constant_time_compare(&a, &b),
"Empty slices should compare equal"
);
}
}