Some checks failed
CI/CD Pipeline / Code Format (push) Successful in 3s
CI/CD Pipeline / Clippy Lints (push) Successful in 42s
CI/CD Pipeline / All Unit Tests (push) Successful in 1m10s
CI/CD Pipeline / Security Audit (push) Successful in 5s
CI/CD Pipeline / Enrollment Tests (push) Successful in 1m12s
CI/CD Pipeline / Verify Enrollment CLI Flag (push) Successful in 57s
CI/CD Pipeline / Build Debian Package (Ubuntu 22.04) (push) Failing after 37s
CI/CD Pipeline / Build Debian Package (push) Failing after 4s
CI/CD Pipeline / Build Arch Package (push) Successful in 2m24s
CI/CD Pipeline / Build RPM Package (push) Successful in 2m15s
CI/CD Pipeline / Build Alpine Package (push) Successful in 3m16s
* test: add CRL unit tests and CrlAwareVerifier construction tests (PR 6 of 6) * fix(ci): rename fmt job to match required status check context --------- Co-authored-by: git-echo <git-echo@moon-dragon.us>
661 lines
24 KiB
Rust
661 lines
24 KiB
Rust
//! mTLS Authentication Module
|
|
//!
|
|
//! Provides mutual TLS authentication middleware for Actix-web.
|
|
//! Non-mTLS connections are silently dropped (no response).
|
|
//! Supports CRL-aware client certificate verification when CRL is available.
|
|
|
|
use actix_web::{
|
|
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
|
|
Error, HttpMessage,
|
|
};
|
|
#[allow(unused_imports)]
|
|
use chrono::{DateTime, Duration, Utc};
|
|
use futures_util::future::LocalBoxFuture;
|
|
use rustls::{
|
|
client::danger::HandshakeSignatureValid,
|
|
crypto::aws_lc_rs,
|
|
pki_types::{CertificateDer, UnixTime},
|
|
server::{
|
|
danger::{ClientCertVerified, ClientCertVerifier},
|
|
ServerConfig, WebPkiClientVerifier,
|
|
},
|
|
version::TLS13,
|
|
DigitallySignedStruct, DistinguishedName, Error as RustlsError, RootCertStore, SignatureScheme,
|
|
};
|
|
use rustls_pemfile::{certs, private_key};
|
|
use std::{fs::File, io::BufReader, sync::Arc};
|
|
use tracing::{debug, error, info, warn};
|
|
|
|
use super::crl::{cert_serial_hex, SharedCrlState};
|
|
|
|
/// Check for duplicate critical headers (VULN-006)
|
|
/// Returns true if duplicate headers are detected
|
|
fn has_duplicate_critical_headers(req: &ServiceRequest) -> bool {
|
|
let critical_headers = ["content-type", "authorization", "host"];
|
|
|
|
for header_name in critical_headers.iter() {
|
|
// Count occurrences of this header
|
|
let mut count = 0;
|
|
for (name, _) in req.headers().iter() {
|
|
if name.as_str().eq_ignore_ascii_case(header_name) {
|
|
count += 1;
|
|
if count > 1 {
|
|
warn!(
|
|
peer_addr = ?req.peer_addr(),
|
|
header = header_name,
|
|
"Duplicate critical header detected - rejecting request"
|
|
);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// CRL-aware client certificate verifier.
|
|
///
|
|
/// Wraps WebPkiClientVerifier for chain validation, then checks the
|
|
/// end-entity certificate serial against the in-memory CRL index.
|
|
/// If CRL is unavailable (Missing/Degraded), falls back to WebPKI-only.
|
|
#[derive(Debug)]
|
|
struct CrlAwareVerifier {
|
|
inner: Arc<dyn ClientCertVerifier>,
|
|
crl_state: SharedCrlState,
|
|
}
|
|
|
|
impl CrlAwareVerifier {
|
|
fn new(inner: Arc<dyn ClientCertVerifier>, crl_state: SharedCrlState) -> Self {
|
|
Self { inner, crl_state }
|
|
}
|
|
}
|
|
|
|
impl ClientCertVerifier for CrlAwareVerifier {
|
|
fn offer_client_auth(&self) -> bool {
|
|
self.inner.offer_client_auth()
|
|
}
|
|
|
|
fn client_auth_mandatory(&self) -> bool {
|
|
self.inner.client_auth_mandatory()
|
|
}
|
|
|
|
fn root_hint_subjects(&self) -> &[DistinguishedName] {
|
|
self.inner.root_hint_subjects()
|
|
}
|
|
|
|
fn verify_client_cert(
|
|
&self,
|
|
end_entity: &CertificateDer<'_>,
|
|
intermediates: &[CertificateDer<'_>],
|
|
now: UnixTime,
|
|
) -> Result<ClientCertVerified, RustlsError> {
|
|
// 1. Delegate chain validation to WebPKI
|
|
self.inner
|
|
.verify_client_cert(end_entity, intermediates, now)?;
|
|
|
|
// 2. Check CRL revocation status
|
|
let crl = self.crl_state.load();
|
|
match crl.status {
|
|
super::crl::CrlStatus::Valid | super::crl::CrlStatus::Expired => {
|
|
// CRL is available -- check serial
|
|
if let Some(serial_hex) = cert_serial_hex(end_entity.as_ref()) {
|
|
if crl.is_revoked(&serial_hex) {
|
|
warn!(
|
|
serial = %serial_hex,
|
|
"Client certificate is revoked per CRL -- rejecting connection"
|
|
);
|
|
return Err(RustlsError::InvalidCertificate(
|
|
rustls::CertificateError::Revoked,
|
|
));
|
|
}
|
|
}
|
|
Ok(ClientCertVerified::assertion())
|
|
}
|
|
super::crl::CrlStatus::Missing | super::crl::CrlStatus::Degraded => {
|
|
// No CRL available -- fall back to WebPKI-only (already passed above)
|
|
warn!(
|
|
status = %crl.status,
|
|
"CRL not available -- allowing connection with WebPKI-only verification"
|
|
);
|
|
Ok(ClientCertVerified::assertion())
|
|
}
|
|
super::crl::CrlStatus::Invalid => {
|
|
// Invalid CRL signature -- fail-closed
|
|
error!(
|
|
"CRL signature is invalid -- refusing all client certificates (fail-closed)"
|
|
);
|
|
Err(RustlsError::InvalidCertificate(
|
|
rustls::CertificateError::Revoked,
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn verify_tls12_signature(
|
|
&self,
|
|
message: &[u8],
|
|
cert: &CertificateDer<'_>,
|
|
dss: &DigitallySignedStruct,
|
|
) -> Result<HandshakeSignatureValid, RustlsError> {
|
|
self.inner.verify_tls12_signature(message, cert, dss)
|
|
}
|
|
|
|
fn verify_tls13_signature(
|
|
&self,
|
|
message: &[u8],
|
|
cert: &CertificateDer<'_>,
|
|
dss: &DigitallySignedStruct,
|
|
) -> Result<HandshakeSignatureValid, RustlsError> {
|
|
self.inner.verify_tls13_signature(message, cert, dss)
|
|
}
|
|
|
|
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
|
self.inner.supported_verify_schemes()
|
|
}
|
|
}
|
|
|
|
/// mTLS Configuration
|
|
#[derive(Debug, Clone)]
|
|
pub struct MtlsConfig {
|
|
pub ca_cert_path: String,
|
|
pub server_cert_path: String,
|
|
pub server_key_path: String,
|
|
pub min_tls_version: String,
|
|
}
|
|
|
|
/// mTLS Middleware for Actix-web
|
|
pub struct MtlsMiddleware {
|
|
config: Arc<MtlsConfig>,
|
|
cert_store: Arc<RootCertStore>,
|
|
}
|
|
|
|
impl MtlsMiddleware {
|
|
/// Create a new mTLS middleware
|
|
pub fn new(config: MtlsConfig) -> Result<Self, MtlsError> {
|
|
let cert_store = load_ca_certs(&config.ca_cert_path)?;
|
|
|
|
Ok(Self {
|
|
config: Arc::new(config),
|
|
cert_store: Arc::new(cert_store),
|
|
})
|
|
}
|
|
|
|
/// Build rustls server configuration with client certificate verification.
|
|
///
|
|
/// When `crl_state` is provided and the CRL is available, wraps the
|
|
/// WebPkiClientVerifier with CrlAwareVerifier for revocation checking.
|
|
/// When CRL is missing/degraded, falls back to WebPKI-only verification.
|
|
pub fn build_rustls_config(
|
|
&self,
|
|
crl_state: Option<SharedCrlState>,
|
|
) -> Result<Arc<ServerConfig>, MtlsError> {
|
|
let webpki_verifier = WebPkiClientVerifier::builder(self.cert_store.clone())
|
|
.build()
|
|
.map_err(|e| MtlsError::ClientVerifierError(e.to_string()))?;
|
|
|
|
let client_verifier: Arc<dyn ClientCertVerifier> = match crl_state {
|
|
Some(state) => {
|
|
info!("CRL-aware client verification enabled");
|
|
Arc::new(CrlAwareVerifier::new(webpki_verifier, state))
|
|
}
|
|
None => {
|
|
info!("No CRL state provided -- using WebPKI-only client verification");
|
|
webpki_verifier
|
|
}
|
|
};
|
|
|
|
let server_cert = load_certs(&self.config.server_cert_path)?;
|
|
let server_key = load_private_key(&self.config.server_key_path)?;
|
|
|
|
let config = ServerConfig::builder_with_provider(Arc::new(aws_lc_rs::default_provider()))
|
|
.with_protocol_versions(&[&TLS13])
|
|
.map_err(|e| {
|
|
MtlsError::ServerConfigError(format!("Failed to set TLS 1.3 only: {}", e))
|
|
})?
|
|
.with_client_cert_verifier(client_verifier)
|
|
.with_single_cert(server_cert, server_key)
|
|
.map_err(|e| MtlsError::ServerConfigError(e.to_string()))?;
|
|
|
|
Ok(Arc::new(config))
|
|
}
|
|
}
|
|
|
|
/// Load CA certificates from PEM file
|
|
fn load_ca_certs(path: &str) -> Result<RootCertStore, MtlsError> {
|
|
let mut cert_store = RootCertStore::empty();
|
|
|
|
let cert_file = File::open(path)
|
|
.map_err(|e| MtlsError::IoError(format!("Failed to open CA cert {}: {}", path, e)))?;
|
|
let mut reader = BufReader::new(cert_file);
|
|
|
|
let certs = certs(&mut reader)
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.map_err(|e| MtlsError::ParseError(format!("Failed to parse CA certs: {}", e)))?;
|
|
|
|
for cert in certs {
|
|
cert_store
|
|
.add(cert)
|
|
.map_err(|e| MtlsError::StoreError(format!("Failed to add CA cert to store: {}", e)))?;
|
|
}
|
|
|
|
info!("Loaded CA certificates from {}", path);
|
|
Ok(cert_store)
|
|
}
|
|
|
|
/// Load server certificates from PEM file
|
|
fn load_certs(path: &str) -> Result<Vec<rustls::pki_types::CertificateDer<'static>>, MtlsError> {
|
|
let cert_file = File::open(path)
|
|
.map_err(|e| MtlsError::IoError(format!("Failed to open cert {}: {}", path, e)))?;
|
|
let mut reader = BufReader::new(cert_file);
|
|
|
|
let certs = certs(&mut reader)
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.map_err(|e| MtlsError::ParseError(format!("Failed to parse server certs: {}", e)))?;
|
|
|
|
Ok(certs)
|
|
}
|
|
|
|
/// Load private key from PEM file
|
|
fn load_private_key(path: &str) -> Result<rustls::pki_types::PrivateKeyDer<'static>, MtlsError> {
|
|
let key_file = File::open(path)
|
|
.map_err(|e| MtlsError::IoError(format!("Failed to open key {}: {}", path, e)))?;
|
|
let mut reader = BufReader::new(key_file);
|
|
|
|
let key = private_key(&mut reader)
|
|
.map_err(|e| MtlsError::ParseError(format!("Failed to parse private key: {}", e)))?
|
|
.ok_or_else(|| MtlsError::ParseError("No private key found in file".to_string()))?;
|
|
|
|
Ok(key)
|
|
}
|
|
|
|
/// mTLS Error types
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum MtlsError {
|
|
#[error("IO error: {0}")]
|
|
IoError(String),
|
|
#[error("Parse error: {0}")]
|
|
ParseError(String),
|
|
#[error("Certificate store error: {0}")]
|
|
StoreError(String),
|
|
#[error("Client verifier error: {0}")]
|
|
ClientVerifierError(String),
|
|
#[error("Server config error: {0}")]
|
|
ServerConfigError(String),
|
|
#[error("Certificate validation error: {0}")]
|
|
ValidationError(String),
|
|
}
|
|
|
|
impl<S, B> Transform<S, ServiceRequest> for MtlsMiddleware
|
|
where
|
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
|
S::Future: 'static,
|
|
B: 'static,
|
|
{
|
|
type Response = ServiceResponse<B>;
|
|
type Error = Error;
|
|
type InitError = ();
|
|
type Transform = MtlsMiddlewareService<S>;
|
|
type Future = futures_util::future::Ready<Result<Self::Transform, Self::InitError>>;
|
|
|
|
fn new_transform(&self, service: S) -> Self::Future {
|
|
futures_util::future::ok(MtlsMiddlewareService {
|
|
service,
|
|
config: self.config.clone(),
|
|
cert_store: self.cert_store.clone(),
|
|
})
|
|
}
|
|
}
|
|
|
|
pub struct MtlsMiddlewareService<S> {
|
|
service: S,
|
|
#[allow(dead_code)]
|
|
config: Arc<MtlsConfig>,
|
|
cert_store: Arc<RootCertStore>,
|
|
}
|
|
|
|
impl<S, B> Service<ServiceRequest> for MtlsMiddlewareService<S>
|
|
where
|
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
|
S::Future: 'static,
|
|
B: 'static,
|
|
{
|
|
type Response = ServiceResponse<B>;
|
|
type Error = Error;
|
|
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
|
|
|
forward_ready!(service);
|
|
|
|
fn call(&self, req: ServiceRequest) -> Self::Future {
|
|
let cert_store = self.cert_store.clone();
|
|
let peer_addr = req.peer_addr();
|
|
|
|
// VULN-006: Check for duplicate critical headers before processing
|
|
if has_duplicate_critical_headers(&req) {
|
|
warn!(
|
|
peer_addr = ?peer_addr,
|
|
"Duplicate critical headers detected - rejecting request (VULN-006)"
|
|
);
|
|
return Box::pin(async move {
|
|
Err(actix_web::error::ErrorBadRequest(
|
|
"Duplicate critical headers not allowed",
|
|
))
|
|
});
|
|
}
|
|
|
|
// Check for client certificate in request extensions
|
|
// In a proper mTLS setup with Actix-web + rustls, the certificate
|
|
// would be extracted from the TLS connection before reaching this middleware
|
|
let has_client_cert = req.extensions().get::<ClientCertInfo>().is_some();
|
|
|
|
if !has_client_cert {
|
|
// No client certificate provided - silent drop
|
|
warn!(
|
|
peer_addr = ?peer_addr,
|
|
"No client certificate provided - dropping connection (mTLS required)"
|
|
);
|
|
// Return error immediately without calling service
|
|
return Box::pin(async move {
|
|
Err(actix_web::error::ErrorBadRequest(
|
|
"Client certificate required",
|
|
))
|
|
});
|
|
}
|
|
|
|
// Certificate present - validate it
|
|
let cert_info = req.extensions().get::<ClientCertInfo>().cloned();
|
|
|
|
if let Some(info) = cert_info {
|
|
// Validate certificate against CA store
|
|
match validate_client_certificate(&info, &cert_store) {
|
|
Ok(_) => {
|
|
info!(
|
|
subject = %info.subject,
|
|
issuer = %info.issuer,
|
|
peer_addr = ?peer_addr,
|
|
"mTLS client certificate validated successfully"
|
|
);
|
|
}
|
|
Err(e) => {
|
|
warn!(
|
|
error = %e,
|
|
peer_addr = ?peer_addr,
|
|
"mTLS client certificate validation failed - dropping connection"
|
|
);
|
|
return Box::pin(async move {
|
|
Err(actix_web::error::ErrorBadRequest(
|
|
"Certificate validation failed",
|
|
))
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
warn!(
|
|
peer_addr = ?peer_addr,
|
|
"No client certificate provided - dropping connection (mTLS required)"
|
|
);
|
|
return Box::pin(async move {
|
|
Err(actix_web::error::ErrorBadRequest(
|
|
"Client certificate required",
|
|
))
|
|
});
|
|
}
|
|
|
|
debug!("mTLS authentication passed for request");
|
|
|
|
// All checks passed - call the service
|
|
let fut = self.service.call(req);
|
|
Box::pin(fut)
|
|
}
|
|
}
|
|
|
|
/// Certificate information extracted from client certificate
|
|
#[derive(Debug, Clone)]
|
|
pub struct ClientCertInfo {
|
|
pub subject: String,
|
|
pub issuer: String,
|
|
pub serial: String,
|
|
pub not_before: DateTime<Utc>,
|
|
pub not_after: DateTime<Utc>,
|
|
}
|
|
|
|
/// Validate client certificate against CA store
|
|
fn validate_client_certificate(
|
|
cert_info: &ClientCertInfo,
|
|
_cert_store: &RootCertStore,
|
|
) -> Result<(), MtlsError> {
|
|
// Check certificate validity period
|
|
let now = Utc::now();
|
|
|
|
if now < cert_info.not_before {
|
|
return Err(MtlsError::ValidationError(
|
|
"Certificate is not yet valid".to_string(),
|
|
));
|
|
}
|
|
|
|
if now > cert_info.not_after {
|
|
return Err(MtlsError::ValidationError(
|
|
"Certificate has expired".to_string(),
|
|
));
|
|
}
|
|
|
|
// In production, would verify certificate chain against CA store
|
|
// For now, we trust certificates that were extracted from the TLS connection
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_mtls_config_creation() {
|
|
let config = MtlsConfig {
|
|
ca_cert_path: "/etc/linux_patch_api/certs/ca.pem".to_string(),
|
|
server_cert_path: "/etc/linux_patch_api/certs/server.pem".to_string(),
|
|
server_key_path: "/etc/linux_patch_api/certs/server.key".to_string(),
|
|
min_tls_version: "1.3".to_string(),
|
|
};
|
|
|
|
assert_eq!(config.ca_cert_path, "/etc/linux_patch_api/certs/ca.pem");
|
|
assert_eq!(config.min_tls_version, "1.3");
|
|
}
|
|
|
|
#[test]
|
|
fn test_client_cert_info() {
|
|
let info = ClientCertInfo {
|
|
subject: "CN=test-client".to_string(),
|
|
issuer: "CN=Test CA".to_string(),
|
|
serial: "12345".to_string(),
|
|
not_before: Utc::now() - Duration::days(1),
|
|
not_after: Utc::now() + Duration::days(365),
|
|
};
|
|
|
|
assert!(info.subject.contains("CN="));
|
|
assert!(info.issuer.contains("CN="));
|
|
|
|
// Test validation with valid cert
|
|
let cert_store = RootCertStore::empty();
|
|
assert!(validate_client_certificate(&info, &cert_store).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_client_cert_expired() {
|
|
let info = ClientCertInfo {
|
|
subject: "CN=expired-client".to_string(),
|
|
issuer: "CN=Test CA".to_string(),
|
|
serial: "12345".to_string(),
|
|
not_before: Utc::now() - Duration::days(365),
|
|
not_after: Utc::now() - Duration::days(1),
|
|
};
|
|
|
|
let cert_store = RootCertStore::empty();
|
|
let result = validate_client_certificate(&info, &cert_store);
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().to_string().contains("expired"));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// CrlAwareVerifier unit tests
|
|
// -----------------------------------------------------------------------
|
|
|
|
/// Test that CrlAwareVerifier can be constructed with a WebPKI verifier
|
|
/// and a SharedCrlState. This verifies the wiring is correct.
|
|
#[test]
|
|
fn crl_aware_verifier_construction() {
|
|
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
|
use super::super::crl::{new_shared_state, CrlState, CrlStatus};
|
|
use std::collections::HashSet;
|
|
|
|
// Build a simple CA cert + key for the root store.
|
|
let ca_key = rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap();
|
|
let mut ca_params = rcgen::CertificateParams::default();
|
|
ca_params.not_before = time::OffsetDateTime::now_utc();
|
|
ca_params.not_after = time::OffsetDateTime::now_utc() + time::Duration::days(365);
|
|
ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
|
|
ca_params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign];
|
|
let mut dn = rcgen::DistinguishedName::new();
|
|
dn.push(rcgen::DnType::CommonName, "Test CA for Verifier");
|
|
ca_params.distinguished_name = dn;
|
|
let ca_cert = ca_params.self_signed(&ca_key).unwrap();
|
|
|
|
// Build root cert store with the CA.
|
|
let mut root_store = RootCertStore::empty();
|
|
root_store.add(ca_cert.der().to_owned()).unwrap();
|
|
|
|
// Build WebPKI verifier — build() returns Arc<WebPkiClientVerifier>
|
|
// which coerces to Arc<dyn ClientCertVerifier>.
|
|
let webpki_verifier: Arc<dyn ClientCertVerifier> =
|
|
WebPkiClientVerifier::builder(root_store.into())
|
|
.build()
|
|
.unwrap();
|
|
|
|
// Build CRL state in Valid status.
|
|
let crl_state = new_shared_state();
|
|
let valid_state = CrlState {
|
|
status: CrlStatus::Valid,
|
|
revoked_serials: HashSet::new(),
|
|
crl_mtime: None,
|
|
loaded_at: std::time::SystemTime::now(),
|
|
};
|
|
crl_state.store(Arc::new(valid_state));
|
|
|
|
// Construct CrlAwareVerifier — should succeed.
|
|
let _verifier = CrlAwareVerifier::new(webpki_verifier, crl_state);
|
|
// If we reach here without panic, construction succeeded.
|
|
}
|
|
|
|
/// Test that CrlAwareVerifier with Missing CRL state can be constructed.
|
|
/// Missing CRL means the verifier falls back to WebPKI-only.
|
|
#[test]
|
|
fn crl_aware_verifier_with_missing_crl() {
|
|
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
|
use super::super::crl::new_shared_state;
|
|
|
|
let ca_key = rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap();
|
|
let mut ca_params = rcgen::CertificateParams::default();
|
|
ca_params.not_before = time::OffsetDateTime::now_utc();
|
|
ca_params.not_after = time::OffsetDateTime::now_utc() + time::Duration::days(365);
|
|
ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
|
|
ca_params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign];
|
|
let mut dn = rcgen::DistinguishedName::new();
|
|
dn.push(rcgen::DnType::CommonName, "Test CA for Verifier");
|
|
ca_params.distinguished_name = dn;
|
|
let ca_cert = ca_params.self_signed(&ca_key).unwrap();
|
|
|
|
let mut root_store = RootCertStore::empty();
|
|
root_store.add(ca_cert.der().to_owned()).unwrap();
|
|
|
|
let webpki_verifier: Arc<dyn ClientCertVerifier> =
|
|
WebPkiClientVerifier::builder(root_store.into())
|
|
.build()
|
|
.unwrap();
|
|
|
|
// Default state is Missing.
|
|
let crl_state = new_shared_state();
|
|
let _verifier = CrlAwareVerifier::new(webpki_verifier, crl_state);
|
|
}
|
|
|
|
/// Test that CrlAwareVerifier with Invalid CRL state can be constructed.
|
|
/// Invalid CRL means the verifier should reject ALL client certificates.
|
|
#[test]
|
|
fn crl_aware_verifier_with_invalid_crl() {
|
|
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
|
use super::super::crl::{new_shared_state, CrlState, CrlStatus};
|
|
use std::collections::HashSet;
|
|
|
|
let ca_key = rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap();
|
|
let mut ca_params = rcgen::CertificateParams::default();
|
|
ca_params.not_before = time::OffsetDateTime::now_utc();
|
|
ca_params.not_after = time::OffsetDateTime::now_utc() + time::Duration::days(365);
|
|
ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
|
|
ca_params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign];
|
|
let mut dn = rcgen::DistinguishedName::new();
|
|
dn.push(rcgen::DnType::CommonName, "Test CA for Verifier");
|
|
ca_params.distinguished_name = dn;
|
|
let ca_cert = ca_params.self_signed(&ca_key).unwrap();
|
|
|
|
let mut root_store = RootCertStore::empty();
|
|
root_store.add(ca_cert.der().to_owned()).unwrap();
|
|
|
|
let webpki_verifier: Arc<dyn ClientCertVerifier> =
|
|
WebPkiClientVerifier::builder(root_store.into())
|
|
.build()
|
|
.unwrap();
|
|
|
|
let crl_state = new_shared_state();
|
|
let invalid_state = CrlState {
|
|
status: CrlStatus::Invalid,
|
|
revoked_serials: HashSet::new(),
|
|
crl_mtime: None,
|
|
loaded_at: std::time::SystemTime::now(),
|
|
};
|
|
crl_state.store(Arc::new(invalid_state));
|
|
|
|
let _verifier = CrlAwareVerifier::new(webpki_verifier, crl_state);
|
|
}
|
|
|
|
/// Test that CrlAwareVerifier with a revoked serial in Valid CRL state
|
|
/// can be constructed. The actual verification logic is tested through
|
|
/// integration tests since it requires a full TLS handshake.
|
|
#[test]
|
|
fn crl_aware_verifier_with_revoked_serial() {
|
|
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
|
use super::super::crl::{new_shared_state, CrlState, CrlStatus};
|
|
use std::collections::HashSet;
|
|
|
|
let ca_key = rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap();
|
|
let mut ca_params = rcgen::CertificateParams::default();
|
|
ca_params.not_before = time::OffsetDateTime::now_utc();
|
|
ca_params.not_after = time::OffsetDateTime::now_utc() + time::Duration::days(365);
|
|
ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
|
|
ca_params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign];
|
|
let mut dn = rcgen::DistinguishedName::new();
|
|
dn.push(rcgen::DnType::CommonName, "Test CA for Verifier");
|
|
ca_params.distinguished_name = dn;
|
|
let ca_cert = ca_params.self_signed(&ca_key).unwrap();
|
|
|
|
let mut root_store = RootCertStore::empty();
|
|
root_store.add(ca_cert.der().to_owned()).unwrap();
|
|
|
|
let webpki_verifier: Arc<dyn ClientCertVerifier> =
|
|
WebPkiClientVerifier::builder(root_store.into())
|
|
.build()
|
|
.unwrap();
|
|
|
|
let crl_state = new_shared_state();
|
|
let mut revoked = HashSet::new();
|
|
revoked.insert("deadbeef".to_string());
|
|
let valid_with_revoked = CrlState {
|
|
status: CrlStatus::Valid,
|
|
revoked_serials: revoked,
|
|
crl_mtime: None,
|
|
loaded_at: std::time::SystemTime::now(),
|
|
};
|
|
crl_state.store(Arc::new(valid_with_revoked));
|
|
|
|
let _verifier = CrlAwareVerifier::new(webpki_verifier, crl_state);
|
|
// Construction succeeded — the verifier is ready to reject revoked certs.
|
|
}
|
|
}
|