1use std::sync::Arc;
18
19use axum::http::request::Parts;
20use tracing::{debug, info, warn};
21
22use synta_certificate::{
23 SignatureVerifier, cert_byte_ranges, crl::CertificateList, default_signature_verifier,
24};
25
26use super::{AuthMethod, AuthResult};
27use crate::ocsp::{OcspClient, OcspStatus};
28use crate::state::AppState;
29
30#[derive(Clone, Debug)]
36pub struct PeerCertificate(pub Vec<u8>);
37
38pub async fn try_extract_mtls(parts: &Parts, _app: &Arc<AppState>) -> Option<AuthResult> {
55 let peer_cert = parts.extensions.get::<PeerCertificate>()?;
56
57 debug!("mTLS client certificate present, extracting identity");
58
59 let cert_der = &peer_cert.0;
62
63 let subject_dn = extract_subject_dn(cert_der);
64 let sans = extract_subject_alt_names(cert_der);
65 let ekus = extract_extended_key_usage(cert_der);
66
67 let identity = sans
70 .first()
71 .cloned()
72 .or_else(|| subject_dn.clone())
73 .unwrap_or_else(|| "unknown".to_string());
74
75 if let Err(e) = check_revocation(cert_der, _app).await {
76 warn!(error = %e, identity = %identity, "certificate revocation check failed");
77 return None;
78 }
79
80 Some(AuthResult {
81 identity,
82 method: AuthMethod::Mtls,
83 client_cert_der: Some(cert_der.to_vec()),
84 subject_dn,
85 subject_alt_names: sans,
86 extended_key_usage: ekus,
87 })
88}
89
90pub fn validate_pop_linking(auth: &AuthResult, csr_subject: &str) -> Result<(), String> {
103 let cert_subject = auth
106 .subject_dn
107 .as_deref()
108 .ok_or_else(|| "mTLS certificate has no subject DN for POP linking".to_string())?;
109
110 let cert_norm = cert_subject.trim().to_lowercase();
112 let csr_norm = csr_subject.trim().to_lowercase();
113
114 if cert_norm != csr_norm {
115 return Err(format!(
116 "POP linking failed: TLS cert subject {cert_subject:?} does not match \
117 CSR subject {csr_subject:?}"
118 ));
119 }
120
121 Ok(())
122}
123
124pub fn validate_pop_linking_simple(
129 client_cert_subject: Option<&str>,
130 csr_subject: &str,
131) -> Result<(), String> {
132 let cert_subject = client_cert_subject
133 .ok_or_else(|| "mTLS certificate has no subject DN for POP linking".to_string())?;
134
135 let cert_norm = cert_subject.trim().to_lowercase();
136 let csr_norm = csr_subject.trim().to_lowercase();
137
138 if cert_norm != csr_norm {
139 return Err(format!(
140 "POP linking failed: TLS cert subject {cert_subject:?} does not match \
141 CSR subject {csr_subject:?}"
142 ));
143 }
144
145 Ok(())
146}
147
148pub fn validate_cert_attributes(
153 auth: &AuthResult,
154 allowed_subject_patterns: &[String],
155 allowed_issuer_patterns: &[String],
156) -> Result<(), String> {
157 if allowed_subject_patterns.is_empty() && allowed_issuer_patterns.is_empty() {
159 return Ok(());
160 }
161
162 if !allowed_subject_patterns.is_empty() {
164 let subject = auth.subject_dn.as_deref().unwrap_or("");
165 let matches = allowed_subject_patterns
166 .iter()
167 .any(|pattern| subject.contains(pattern.as_str()));
168 if !matches {
169 return Err(format!(
170 "certificate subject {subject:?} does not match any allowed pattern"
171 ));
172 }
173 }
174
175 let _ = allowed_issuer_patterns;
178
179 Ok(())
180}
181
182fn extract_subject_dn(cert_der: &[u8]) -> Option<String> {
189 if cert_der.is_empty() {
190 return None;
191 }
192 let cert = synta_certificate::Certificate::from_der(cert_der).ok()?;
193 let dn = synta_certificate::format_dn(cert.tbs_certificate.subject.0);
194 if dn.is_empty() || dn == "<invalid>" {
195 None
196 } else {
197 Some(dn)
198 }
199}
200
201fn extract_subject_alt_names(cert_der: &[u8]) -> Vec<String> {
206 let cert = match synta_certificate::Certificate::from_der(cert_der) {
207 Ok(c) => c,
208 Err(_) => return Vec::new(),
209 };
210
211 let mut result = Vec::new();
212 for (tag, content) in cert.subject_alt_names() {
213 let name = match tag {
214 synta_certificate::general_name::DNS_NAME => {
215 std::str::from_utf8(&content)
217 .map(|s| s.to_string())
218 .unwrap_or_else(|_| "DNS:<invalid UTF-8>".to_string())
219 }
220 synta_certificate::general_name::RFC822_NAME => {
221 std::str::from_utf8(&content)
223 .map(|s| s.to_string())
224 .unwrap_or_else(|_| "email:<invalid UTF-8>".to_string())
225 }
226 synta_certificate::general_name::URI => {
227 std::str::from_utf8(&content)
229 .map(|s| s.to_string())
230 .unwrap_or_else(|_| "URI:<invalid UTF-8>".to_string())
231 }
232 synta_certificate::general_name::IP_ADDRESS if content.len() == 4 => {
233 format!(
235 "{}.{}.{}.{}",
236 content[0], content[1], content[2], content[3]
237 )
238 }
239 synta_certificate::general_name::IP_ADDRESS if content.len() == 16 => {
240 let addr = std::net::Ipv6Addr::new(
242 u16::from_be_bytes([content[0], content[1]]),
243 u16::from_be_bytes([content[2], content[3]]),
244 u16::from_be_bytes([content[4], content[5]]),
245 u16::from_be_bytes([content[6], content[7]]),
246 u16::from_be_bytes([content[8], content[9]]),
247 u16::from_be_bytes([content[10], content[11]]),
248 u16::from_be_bytes([content[12], content[13]]),
249 u16::from_be_bytes([content[14], content[15]]),
250 );
251 addr.to_string()
252 }
253 synta_certificate::general_name::DIRECTORY_NAME => {
254 format!("DirName:{}", synta_certificate::format_dn(&content))
256 }
257 _ => {
258 format!("GeneralName(tag={tag})")
260 }
261 };
262 result.push(name);
263 }
264 result
265}
266
267fn extract_extended_key_usage(cert_der: &[u8]) -> Vec<String> {
273 let cert = match synta_certificate::Certificate::from_der(cert_der) {
274 Ok(c) => c,
275 Err(_) => return Vec::new(),
276 };
277
278 let ext_raw = match cert.tbs_certificate.extensions.as_ref() {
280 Some(r) => r,
281 None => return Vec::new(),
282 };
283
284 let eku_bytes = match synta_certificate::find_extension_value(
286 ext_raw.as_bytes(),
287 synta_certificate::oids::EXTENDED_KEY_USAGE,
288 ) {
289 Some(bytes) => bytes,
290 None => return Vec::new(),
291 };
292
293 let mut decoder = synta::Decoder::new(eku_bytes, synta::Encoding::Der);
295 let oids: Vec<synta::ObjectIdentifier> = match decoder.decode() {
296 Ok(v) => v,
297 Err(_) => return Vec::new(),
298 };
299
300 oids.iter().map(|oid| oid.to_string()).collect()
301}
302
303pub(crate) async fn check_revocation(cert_der: &[u8], app: &Arc<AppState>) -> Result<(), String> {
311 let ocsp_config = &app.config.ocsp;
312
313 if !ocsp_config.enabled {
314 debug!("OCSP checking disabled, skipping revocation check");
315 return Ok(());
316 }
317
318 let ocsp_client = OcspClient::new(ocsp_config.clone());
319
320 let issuer_der = super::certificate::issuer_for(cert_der, app)?;
323
324 match ocsp_client
325 .check_certificate_status(cert_der, &issuer_der)
326 .await
327 {
328 Ok(OcspStatus::Good) => {
329 info!("OCSP: certificate status is good");
330 Ok(())
331 }
332 Ok(OcspStatus::Revoked {
333 reason,
334 revocation_time,
335 }) => {
336 warn!(
337 reason = %reason,
338 revocation_time = %revocation_time,
339 "OCSP: certificate has been revoked"
340 );
341 Err(format!(
342 "certificate revoked: reason={reason}, time={revocation_time}"
343 ))
344 }
345 Ok(OcspStatus::Unknown) => {
346 warn!("OCSP: certificate status unknown");
347 if ocsp_config.soft_fail {
348 Ok(())
349 } else {
350 Err("OCSP: certificate status unknown".to_string())
351 }
352 }
353 Err(e) => {
354 warn!(error = %e, "OCSP check failed, attempting CRL fallback");
355 if ocsp_config.soft_fail {
357 info!("OCSP soft-fail enabled, accepting certificate despite OCSP failure");
358 Ok(())
359 } else {
360 match check_crl_fallback(cert_der, app).await {
362 Ok(()) => {
363 info!("CRL fallback: certificate is not revoked");
364 Ok(())
365 }
366 Err(crl_err) => {
367 warn!(
368 ocsp_error = %e,
369 crl_error = %crl_err,
370 "both OCSP and CRL revocation checks failed"
371 );
372 Err(format!(
373 "OCSP check failed ({e}) and CRL fallback also failed ({crl_err})"
374 ))
375 }
376 }
377 }
378 }
379 }
380}
381
382async fn check_crl_fallback(cert_der: &[u8], app: &Arc<AppState>) -> Result<(), String> {
388 let cdp_urls = extract_cdp_http_urls(cert_der)?;
390 if cdp_urls.is_empty() {
391 return Err("certificate has no CRL Distribution Point HTTP URLs".to_string());
392 }
393
394 let client_cert = synta_certificate::Certificate::from_der(cert_der)
396 .map_err(|e| format!("failed to parse client certificate: {e}"))?;
397 let client_serial = client_cert.tbs_certificate.serial_number.clone();
398
399 let issuer_der = super::certificate::issuer_for(cert_der, app)?;
401 let issuer_ranges = cert_byte_ranges(&issuer_der)
402 .ok_or_else(|| "failed to extract byte ranges from issuer certificate".to_string())?;
403 let issuer_spki = &issuer_der[issuer_ranges.subject_public_key_info.clone()];
404
405 let mut last_err = String::new();
407 for url in &cdp_urls {
408 debug!(url = %url, "fetching CRL from distribution point");
409 match fetch_and_check_crl(
410 url,
411 &client_serial,
412 issuer_spki,
413 client_cert.tbs_certificate.issuer.as_bytes(),
414 )
415 .await
416 {
417 Ok(()) => return Ok(()),
418 Err(e) => {
419 if e.starts_with("certificate serial") {
420 return Err(e);
421 }
422 warn!(url = %url, error = %e, "CRL check failed for this distribution point");
423 last_err = e;
424 }
425 }
426 }
427
428 Err(format!(
429 "all CRL distribution points failed; last error: {last_err}"
430 ))
431}
432
433async fn fetch_and_check_crl(
436 url: &str,
437 client_serial: &synta::Integer,
438 issuer_spki_der: &[u8],
439 issuer_name_der: &[u8],
440) -> Result<(), String> {
441 let client = reqwest::Client::builder()
443 .timeout(std::time::Duration::from_secs(10))
444 .build()
445 .map_err(|e| format!("CRL client error: {e}"))?;
446 let mut response = client
447 .get(url)
448 .send()
449 .await
450 .map_err(|e| format!("HTTP fetch failed for {url}: {e}"))?;
451
452 if !response.status().is_success() {
453 return Err(format!(
454 "HTTP {} fetching CRL from {url}",
455 response.status()
456 ));
457 }
458
459 if let Some(len) = response.content_length()
461 && len > 10_000_000
462 {
463 return Err(format!("CRL from {url} too large ({len} bytes, max 10 MB)"));
464 }
465
466 let mut crl_der = Vec::new();
467 while let Some(chunk) = response.chunk().await.map_err(|e| e.to_string())? {
468 if chunk.len() > 10_000_000usize.saturating_sub(crl_der.len()) {
469 return Err("CRL body exceeds 10 MB".into());
470 }
471 crl_der.extend_from_slice(&chunk);
472 }
473
474 let crl =
476 CertificateList::from_der(&crl_der).map_err(|e| format!("failed to parse CRL DER: {e}"))?;
477
478 if crl
479 .tbs_cert_list
480 .issuer
481 .to_der()
482 .map_err(|e| e.to_string())?
483 != issuer_name_der
484 {
485 return Err("CRL issuer name does not match certificate issuer".into());
486 }
487 let now = chrono::Utc::now();
488 let this_update = super::certificate::time(&crl.tbs_cert_list.this_update)?;
489 let next_update = crl
490 .tbs_cert_list
491 .next_update
492 .as_ref()
493 .ok_or("CRL missing nextUpdate")?;
494 let next_update = super::certificate::time(next_update)?;
495 if this_update > now || next_update <= now || next_update <= this_update {
496 return Err("CRL is stale or future-dated".into());
497 }
498 if let Some(extensions) = &crl.tbs_cert_list.crl_extensions {
499 for ext in extensions {
500 let oid = ext.extn_id.to_string();
501 if oid == "2.5.29.27"
502 || oid == "2.5.29.28"
503 || ext.critical.as_ref().is_some_and(|v| v.0)
504 {
505 return Err("unsupported scoped, delta or critical CRL extension".into());
506 }
507 }
508 }
509
510 let crl_ranges = crl_byte_ranges(&crl_der)
514 .ok_or_else(|| "failed to extract byte ranges from CRL DER".to_string())?;
515
516 let tbs_bytes = &crl_der[crl_ranges.tbs];
517 let sig_alg_bytes = &crl_der[crl_ranges.sig_alg];
518 let sig_bits = &crl_der[crl_ranges.signature];
519
520 let verifier = default_signature_verifier();
521 verifier
522 .verify_certificate_signature(tbs_bytes, sig_alg_bytes, sig_bits, issuer_spki_der)
523 .map_err(|e| format!("CRL signature verification failed: {e}"))?;
524
525 info!("CRL signature verified successfully");
526
527 if let Some(ref revoked_certs) = crl.tbs_cert_list.revoked_certificates {
529 for entry in revoked_certs {
530 if entry.user_certificate == *client_serial {
531 return Err(format!(
532 "certificate serial {} is revoked (revocation date: {:?})",
533 format_serial_hex(client_serial),
534 entry.revocation_date,
535 ));
536 }
537 }
538 }
539
540 Ok(())
541}
542
543fn extract_cdp_http_urls(cert_der: &[u8]) -> Result<Vec<String>, String> {
549 let cert = synta_certificate::Certificate::from_der(cert_der)
550 .map_err(|e| format!("failed to parse certificate for CDP extraction: {e}"))?;
551
552 let ext_raw = cert
553 .tbs_certificate
554 .extensions
555 .as_ref()
556 .ok_or_else(|| "certificate has no extensions".to_string())?;
557
558 let cdp_bytes = synta_certificate::find_extension_value(
559 ext_raw.as_bytes(),
560 synta_certificate::oids::CRL_DISTRIBUTION_POINTS,
561 )
562 .ok_or_else(|| "CRL Distribution Points extension not found".to_string())?;
563
564 use synta::tag::TAG_SEQUENCE;
575 use synta::{Tag, TagClass};
576
577 let mut urls = Vec::new();
578 let seq_tag = Tag::universal_constructed(TAG_SEQUENCE);
579 let mut decoder = synta::Decoder::new(cdp_bytes, synta::Encoding::Der);
580 let mut outer = decoder
581 .enter_constructed(seq_tag)
582 .map_err(|e| format!("CDP outer SEQUENCE decode error: {e}"))?;
583
584 while !outer.is_empty() {
585 let mut dp = match outer.enter_constructed(seq_tag) {
587 Ok(d) => d,
588 Err(_) => break,
589 };
590 while !dp.is_empty() {
591 let dp_tag = match dp.read_tag() {
592 Ok(t) => t,
593 Err(_) => break,
594 };
595 let dp_len = match dp.read_length() {
596 Ok(l) => match l.definite() {
597 Ok(n) => n,
598 Err(_) => break,
599 },
600 Err(_) => break,
601 };
602 let dp_content = match dp.read_bytes(dp_len) {
603 Ok(c) => c,
604 Err(_) => break,
605 };
606
607 if dp_tag.class() == TagClass::ContextSpecific && dp_tag.number() == 0 {
609 let mut gn_dec = synta::Decoder::new(dp_content, synta::Encoding::Der);
611 while !gn_dec.is_empty() {
612 let gn_tag = match gn_dec.read_tag() {
613 Ok(t) => t,
614 Err(_) => break,
615 };
616 let gn_len = match gn_dec.read_length() {
617 Ok(l) => match l.definite() {
618 Ok(n) => n,
619 Err(_) => break,
620 },
621 Err(_) => break,
622 };
623 let gn_content = match gn_dec.read_bytes(gn_len) {
624 Ok(c) => c,
625 Err(_) => break,
626 };
627
628 if gn_tag.class() == TagClass::ContextSpecific
630 && gn_tag.number() == 6
631 && let Ok(uri) = std::str::from_utf8(gn_content)
632 && (uri.starts_with("http://") || uri.starts_with("https://"))
633 {
634 urls.push(uri.to_string());
635 }
636 }
637 }
638 }
640 }
641
642 Ok(urls)
643}
644
645struct CrlByteRanges {
647 tbs: std::ops::Range<usize>,
649 sig_alg: std::ops::Range<usize>,
651 signature: std::ops::Range<usize>,
654}
655
656fn crl_byte_ranges(crl_der: &[u8]) -> Option<CrlByteRanges> {
661 use synta::{Decoder, Encoding};
662
663 let mut d = Decoder::new(crl_der, Encoding::Der);
664
665 d.read_tag().ok()?;
667 d.read_length().ok()?.definite().ok()?;
668
669 let tbs_start = d.position();
671 d.read_tag().ok()?;
672 let tbs_content_len = d.read_length().ok()?.definite().ok()?;
673 let tbs_end = d.position() + tbs_content_len;
674 d.read_bytes(tbs_content_len).ok()?;
675
676 let sig_alg_start = d.position();
678 d.read_tag().ok()?;
679 let sig_alg_content_len = d.read_length().ok()?.definite().ok()?;
680 let sig_alg_end = d.position() + sig_alg_content_len;
681 d.read_bytes(sig_alg_content_len).ok()?;
682
683 d.read_tag().ok()?;
685 let sig_len = d.read_length().ok()?.definite().ok()?;
686 if sig_len == 0 {
687 return None;
688 }
689 let sig_start = d.position() + 1;
692 let sig_end = d.position() + sig_len;
693
694 Some(CrlByteRanges {
695 tbs: tbs_start..tbs_end,
696 sig_alg: sig_alg_start..sig_alg_end,
697 signature: sig_start..sig_end,
698 })
699}
700
701fn format_serial_hex(serial: &synta::Integer) -> String {
703 serial
704 .as_bytes()
705 .iter()
706 .map(|b| format!("{b:02x}"))
707 .collect::<Vec<_>>()
708 .join(":")
709}