feat(crl): add CRL consumption and custom verifier for mTLS revocation enforcement
Implements agent-side CRL consumption for mTLS certificate revocation checking, as specified in issue #20. Changes: - NEW: src/auth/crl.rs - CRL loading, parsing, signature verification, in-memory revoked serial index (HashSet), 24h background refresh task - MODIFY: src/auth/mtls.rs - CrlAwareVerifier wrapping WebPkiClientVerifier with post-chain CRL serial lookup; fails closed on invalid signature, degrades gracefully when CRL is missing - MODIFY: src/auth/mod.rs - Register crl module, re-export CrlState/CrlStatus - MODIFY: src/config/loader.rs - Add crl_path field to TlsConfig - MODIFY: src/main.rs - Load CRL on startup, spawn refresh task, wire SharedCrlState into server and health endpoint - MODIFY: src/api/handlers/system.rs - Add crl_status and crl_age_seconds to health check response - MODIFY: Cargo.toml - Add arc-swap, base64 deps; enable x509-parser verify feature for CRL signature verification Design decisions: - ArcSwap for lock-free atomic CRL state swaps on the hot path - O(1) serial lookup via HashSet<String> of hex-encoded serials - Stale CRL = continue serving + warn + health reports degraded - Invalid CRL signature = refuse to start (fail-closed) - Missing CRL = fall back to WebPKI-only (backward compatible) Companion to PR #26 in linux-patch-manager (manager-side CRL generation) Refs: #20
This commit is contained in:
@ -12,6 +12,7 @@ use tracing::{error, info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::packages::ApiResponse;
|
||||
use crate::auth::crl::{CrlStatus, SharedCrlState};
|
||||
use crate::jobs::manager::{JobManager, JobOperation, JobStatus};
|
||||
use crate::packages::PackageManagerBackend;
|
||||
|
||||
@ -47,6 +48,8 @@ pub struct HealthData {
|
||||
pub version: String,
|
||||
pub last_cache_update: Option<String>, // RFC3339 timestamp
|
||||
pub cache_status: String, // "fresh", "stale", "unknown", "failed"
|
||||
pub crl_status: Option<String>, // "valid", "expired", "missing", "invalid", "degraded"
|
||||
pub crl_age_seconds: Option<u64>, // age of on-disk CRL file
|
||||
}
|
||||
|
||||
/// Service status response data
|
||||
@ -113,6 +116,7 @@ pub async fn get_system_info(
|
||||
pub async fn health_check(
|
||||
backend: web::Data<Box<dyn PackageManagerBackend>>,
|
||||
cache_state: web::Data<crate::packages::cache::PackageCacheState>,
|
||||
crl_state: web::Data<SharedCrlState>,
|
||||
_req: HttpRequest,
|
||||
) -> impl Responder {
|
||||
let _request_id = Uuid::new_v4().to_string();
|
||||
@ -134,7 +138,7 @@ pub async fn health_check(
|
||||
|
||||
// Check cache status and refresh if stale
|
||||
let cache_status_val = cache_state.status();
|
||||
let (status, cache_status_str, last_cache_update) = if cache_state.is_stale() {
|
||||
let (mut status, cache_status_str, last_cache_update) = if cache_state.is_stale() {
|
||||
match backend.refresh_package_cache(&cache_state) {
|
||||
Ok(_) => {
|
||||
let updated = cache_state.status();
|
||||
@ -161,12 +165,31 @@ pub async fn health_check(
|
||||
)
|
||||
};
|
||||
|
||||
// CRL status from shared state
|
||||
let crl = crl_state.load();
|
||||
let crl_status_str = match crl.status {
|
||||
CrlStatus::Valid
|
||||
| CrlStatus::Expired
|
||||
| CrlStatus::Missing
|
||||
| CrlStatus::Invalid
|
||||
| CrlStatus::Degraded => {
|
||||
// Downgrade overall health if CRL is invalid
|
||||
if crl.status == CrlStatus::Invalid {
|
||||
status = "degraded".to_string();
|
||||
}
|
||||
crl.status.to_string()
|
||||
}
|
||||
};
|
||||
let crl_age = crl.crl_age_seconds();
|
||||
|
||||
let response = ApiResponse::success(HealthData {
|
||||
status,
|
||||
uptime_seconds,
|
||||
version,
|
||||
last_cache_update,
|
||||
cache_status: cache_status_str,
|
||||
crl_status: Some(crl_status_str),
|
||||
crl_age_seconds: crl_age,
|
||||
});
|
||||
|
||||
HttpResponse::Ok().json(response)
|
||||
@ -386,6 +409,8 @@ mod tests {
|
||||
version: "0.1.0".to_string(),
|
||||
last_cache_update: Some("2026-05-27T14:00:00+00:00".to_string()),
|
||||
cache_status: "fresh".to_string(),
|
||||
crl_status: Some("valid".to_string()),
|
||||
crl_age_seconds: Some(3600),
|
||||
};
|
||||
let json = serde_json::to_string(&health).unwrap();
|
||||
assert!(json.contains("healthy"));
|
||||
|
||||
Reference in New Issue
Block a user