1pub mod audit;
13pub mod cas;
14pub mod certs;
15pub mod health;
16pub mod otp;
17
18use std::sync::Arc;
19
20use axum::Router;
21use axum::extract::{FromRef, FromRequestParts};
22use axum::http::StatusCode;
23use axum::http::request::Parts;
24use axum::response::{IntoResponse, Response};
25use axum::routing::{delete, get, post};
26
27use subtle::ConstantTimeEq;
28
29use crate::state::AppState;
30
31pub fn admin_router() -> Router<Arc<AppState>> {
56 Router::new()
57 .route("/health", get(health::get_health))
59 .route("/health/db", get(health::get_health_db))
60 .route("/health/hsm", get(health::get_health_hsm))
61 .route("/health/ca", get(health::get_health_ca))
62 .route("/cas", get(cas::list_cas))
64 .route("/cas/{id}", get(cas::get_ca))
65 .route("/cas/{id}/health", get(cas::get_ca_health))
66 .route("/otp/generate", post(otp::generate_otp))
68 .route("/otp", get(otp::list_otps))
69 .route("/otp/{id}", delete(otp::revoke_otp))
70 .route("/certs", get(certs::list_certs))
72 .route("/certs/{serial}", get(certs::get_cert))
73 .route("/certs/{serial}/revoke", post(certs::revoke_cert))
74 .route("/audit", get(audit::list_audit_events))
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum AdminRole {
87 Operator,
89 Auditor,
91}
92
93#[derive(Debug, Clone)]
101pub struct AdminAuth {
102 pub identity: String,
104 pub role: AdminRole,
106}
107
108impl AdminAuth {
109 pub fn require_operator(&self) -> Option<Response> {
121 if self.role == AdminRole::Operator {
122 None
123 } else {
124 tracing::warn!(
125 identity = %self.identity,
126 "admin operation denied: operator role required"
127 );
128 Some(
129 (
130 StatusCode::FORBIDDEN,
131 "operator role required for this operation",
132 )
133 .into_response(),
134 )
135 }
136 }
137}
138
139impl<S> FromRequestParts<S> for AdminAuth
140where
141 S: Send + Sync,
142 Arc<AppState>: FromRef<S>,
143{
144 type Rejection = Response;
145
146 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Response> {
147 let _app = Arc::<AppState>::from_ref(state);
148 let Some(admin_cfg) = _app.config.admin.as_ref().filter(|c| c.enabled) else {
149 return Err(StatusCode::NOT_FOUND.into_response());
150 };
151
152 if admin_cfg.auth_method == crate::config::AdminAuthMethod::Bearer
154 && let Some(auth_header) = parts
155 .headers
156 .get(axum::http::header::AUTHORIZATION)
157 .and_then(|v| v.to_str().ok())
158 && let Some(token) = auth_header.strip_prefix("Bearer ")
159 && !token.is_empty()
160 {
161 let token_bytes = token.as_bytes();
169 let mut matched: Option<AdminRole> = None;
170 if let Some(ref operator_token) = _app.secrets.admin_bearer_token
171 && token_bytes.ct_eq(operator_token.as_bytes()).into()
172 {
173 matched = Some(AdminRole::Operator);
174 }
175 if let Some(ref auditor_token) = _app.secrets.auditor_bearer_token
176 && token_bytes.ct_eq(auditor_token.as_bytes()).into()
177 {
178 matched.get_or_insert(AdminRole::Auditor);
180 }
181 if let Some(role) = matched {
182 let identity = match role {
183 AdminRole::Operator => "admin",
184 AdminRole::Auditor => "auditor",
185 };
186 return Ok(AdminAuth {
187 identity: identity.to_string(),
188 role,
189 });
190 }
191 }
193
194 if admin_cfg.auth_method == crate::config::AdminAuthMethod::Mtls
196 && let Some(cert) = parts.extensions.get::<crate::auth::mtls::PeerCertificate>()
197 && !cert.0.is_empty()
198 {
199 if let Some(ref admin_cfg) = _app.config.admin {
202 match validate_admin_cert(&cert.0, admin_cfg) {
203 Ok((identity, role)) => {
204 crate::auth::mtls::check_revocation(&cert.0, &_app)
205 .await
206 .map_err(|e| crate::error::KipukaError::Auth(e).into_response())?;
207 return Ok(AdminAuth { identity, role });
208 }
209 Err(reason) => {
210 tracing::warn!(
211 reason = %reason,
212 "admin mTLS certificate validation failed"
213 );
214 return Err((
215 StatusCode::FORBIDDEN,
216 format!("admin mTLS validation failed: {reason}"),
217 )
218 .into_response());
219 }
220 }
221 }
222 }
224
225 Err((
226 StatusCode::UNAUTHORIZED,
227 "admin authentication required: Bearer token or mTLS certificate",
228 )
229 .into_response())
230 }
231}
232
233static ADMIN_TRUST_ANCHORS: std::sync::OnceLock<Vec<Vec<u8>>> = std::sync::OnceLock::new();
238
239fn validate_admin_cert(
254 client_cert_der: &[u8],
255 admin_cfg: &crate::config::AdminConfig,
256) -> Result<(String, AdminRole), String> {
257 use std::io::BufReader;
258 use synta_certificate::SignatureVerifier;
259
260 let client_cert = synta_certificate::Certificate::from_der(client_cert_der)
262 .map_err(|e| format!("failed to parse admin client certificate: {e}"))?;
263 let client_dn = synta_certificate::format_dn(client_cert.tbs_certificate.subject.0);
264
265 if client_dn.is_empty() || client_dn == "<invalid>" {
266 return Err("admin client certificate has no valid subject DN".to_string());
267 }
268
269 let ca_file = admin_cfg
271 .admin_ca_file
272 .as_deref()
273 .ok_or_else(|| "admin_ca_file not configured for mTLS validation".to_string())?;
274
275 let trust_certs_der = match ADMIN_TRUST_ANCHORS.get() {
280 Some(cached) => cached,
281 None => {
282 let pem_data = std::fs::read(ca_file)
283 .map_err(|e| format!("failed to read admin CA file '{ca_file}': {e}"))?;
284 let mut reader = BufReader::new(&pem_data[..]);
285 let certs: Vec<Vec<u8>> = rustls_pemfile::certs(&mut reader)
286 .filter_map(|r| r.ok())
287 .map(|c| c.to_vec())
288 .collect();
289 if certs.is_empty() {
290 return Err(format!(
291 "no CA certificates found in admin CA file '{ca_file}'"
292 ));
293 }
294 let _ = ADMIN_TRUST_ANCHORS.set(certs);
297 ADMIN_TRUST_ANCHORS
298 .get()
299 .expect("admin trust anchors were just set")
300 }
301 };
302
303 let client_ranges = synta_certificate::cert_byte_ranges(client_cert_der)
310 .ok_or_else(|| "failed to extract byte ranges from admin client certificate".to_string())?;
311
312 let tbs_bytes = &client_cert_der[client_ranges.tbs.clone()];
313 let sig_alg_bytes = &client_cert_der[client_ranges.signature_algorithm.clone()];
314 let sig_bytes = client_cert.signature_value.as_bytes();
315
316 let verifier = synta_certificate::default_signature_verifier();
317
318 let mut verified = false;
319 for anchor_der in trust_certs_der {
320 let anchor_ranges = match synta_certificate::cert_byte_ranges(anchor_der) {
321 Some(r) => r,
322 None => continue,
323 };
324 let anchor_spki = &anchor_der[anchor_ranges.subject_public_key_info.clone()];
325
326 if verifier
327 .verify_certificate_signature(tbs_bytes, sig_alg_bytes, sig_bytes, anchor_spki)
328 .is_ok()
329 {
330 verified = true;
331 break;
332 }
333 }
334
335 if !verified {
336 return Err(format!(
337 "admin client certificate (subject: {client_dn}) does not chain \
338 to any trust anchor in '{ca_file}'"
339 ));
340 }
341
342 tracing::info!(
343 subject = %client_dn,
344 "admin mTLS certificate verified against admin truststore"
345 );
346
347 {
349 let validity = &client_cert.tbs_certificate.validity;
350 let now = chrono::Utc::now();
351
352 let time_to_chrono =
354 |t: &synta_certificate::Time| -> Result<chrono::DateTime<chrono::Utc>, String> {
355 let (year, month, day, hour, minute, second) = match t {
356 synta_certificate::Time::UtcTime(ut) => {
357 (ut.year, ut.month, ut.day, ut.hour, ut.minute, ut.second)
358 }
359 synta_certificate::Time::GeneralTime(gt) => {
360 (gt.year, gt.month, gt.day, gt.hour, gt.minute, gt.second)
361 }
362 };
363 let naive = chrono::NaiveDate::from_ymd_opt(year.into(), month.into(), day.into())
364 .and_then(|d| d.and_hms_opt(hour.into(), minute.into(), second.into()))
365 .ok_or_else(|| {
366 format!("invalid date components: {year}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}")
367 })?;
368 Ok(naive.and_utc())
369 };
370
371 let nb = time_to_chrono(&validity.not_before)?;
372 let na = time_to_chrono(&validity.not_after)?;
373
374 if now < nb {
375 return Err(format!(
376 "admin client certificate is not yet valid (notBefore: {nb})"
377 ));
378 }
379 if now > na {
380 return Err(format!(
381 "admin client certificate has expired (notAfter: {na})"
382 ));
383 }
384
385 tracing::debug!(
386 subject = %client_dn,
387 not_before = %nb,
388 not_after = %na,
389 "admin certificate validity check passed"
390 );
391 }
392
393 let role = resolve_admin_role(
395 &client_dn,
396 &admin_cfg.allowed_operators,
397 &admin_cfg.allowed_auditors,
398 )
399 .ok_or_else(|| {
400 format!(
401 "admin client DN '{client_dn}' does not match any allowed operator or auditor pattern"
402 )
403 })?;
404
405 tracing::debug!(
406 subject = %client_dn,
407 ?role,
408 "admin identity matched allow-list"
409 );
410
411 Ok((client_dn, role))
412}
413
414fn resolve_admin_role(dn: &str, operators: &[String], auditors: &[String]) -> Option<AdminRole> {
426 let dn_lower = dn.to_lowercase();
427 if operators.iter().any(|p| p.to_lowercase() == dn_lower) {
428 Some(AdminRole::Operator)
429 } else if auditors.iter().any(|p| p.to_lowercase() == dn_lower) {
430 Some(AdminRole::Auditor)
431 } else if operators.is_empty() && auditors.is_empty() {
432 Some(AdminRole::Operator)
434 } else {
435 None
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 fn auth(role: AdminRole) -> AdminAuth {
444 AdminAuth {
445 identity: "test".to_string(),
446 role,
447 }
448 }
449
450 #[test]
451 fn require_operator_allows_operator() {
452 assert!(auth(AdminRole::Operator).require_operator().is_none());
453 }
454
455 #[test]
456 fn require_operator_denies_auditor() {
457 assert!(auth(AdminRole::Auditor).require_operator().is_some());
459 }
460
461 #[test]
462 fn role_operator_precedence_over_auditor() {
463 let ops = vec!["CN=admin".to_string()];
464 let auds = vec!["CN=admin".to_string()];
465 assert_eq!(
466 resolve_admin_role("CN=admin", &ops, &auds),
467 Some(AdminRole::Operator)
468 );
469 }
470
471 #[test]
472 fn role_auditor_match() {
473 let ops = vec!["CN=boss".to_string()];
474 let auds = vec!["CN=watcher".to_string()];
475 assert_eq!(
476 resolve_admin_role("CN=watcher", &ops, &auds),
477 Some(AdminRole::Auditor)
478 );
479 }
480
481 #[test]
482 fn role_match_is_case_insensitive() {
483 let ops = vec!["CN=Admin".to_string()];
484 assert_eq!(
485 resolve_admin_role("cn=admin", &ops, &[]),
486 Some(AdminRole::Operator)
487 );
488 }
489
490 #[test]
491 fn role_backward_compat_empty_lists_is_operator() {
492 assert_eq!(
493 resolve_admin_role("CN=anyone", &[], &[]),
494 Some(AdminRole::Operator)
495 );
496 }
497
498 #[test]
499 fn role_unlisted_dn_is_denied_when_lists_present() {
500 let ops = vec!["CN=admin".to_string()];
501 assert_eq!(resolve_admin_role("CN=stranger", &ops, &[]), None);
502 }
503}