Skip to content

Commit 3863968

Browse files
committed
verify CSR hostname during setup
1 parent 5d24cb5 commit 3863968

2 files changed

Lines changed: 95 additions & 0 deletions

File tree

crates/defguard_certs/src/lib.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pub enum CertificateError {
2626
ParsingError(String),
2727
#[error(transparent)]
2828
IoError(#[from] std::io::Error),
29+
#[error("CSR hostname mismatch: {0}")]
30+
HostnameMismatch(String),
2931
}
3032

3133
pub struct CertificateAuthority<'a> {
@@ -296,6 +298,41 @@ impl Csr<'_> {
296298
Ok(params)
297299
}
298300

301+
/// Verify that the CSR's SAN list contains exactly `expected_hostname` and
302+
/// nothing else. The hostname may be a DNS name or an IP address literal.
303+
///
304+
/// This is used during component setup to ensure the component has not
305+
/// substituted a different hostname in the CSR it returns to Core.
306+
pub fn verify_hostname(&self, expected_hostname: &str) -> Result<(), CertificateError> {
307+
let params = self.params()?;
308+
let sans = &params.params.subject_alt_names;
309+
310+
if sans.is_empty() {
311+
return Err(CertificateError::HostnameMismatch(format!(
312+
"CSR contains no SANs; expected {expected_hostname:?}"
313+
)));
314+
}
315+
316+
let expected_ip: Option<std::net::IpAddr> = expected_hostname.parse().ok();
317+
318+
for san in sans {
319+
let matches = match san {
320+
rcgen::SanType::IpAddress(ip) => expected_ip.is_some_and(|e| &e == ip),
321+
rcgen::SanType::DnsName(name) => {
322+
expected_ip.is_none() && name.as_str() == expected_hostname
323+
}
324+
_ => false,
325+
};
326+
if !matches {
327+
return Err(CertificateError::HostnameMismatch(format!(
328+
"CSR SAN does not match expected hostname {expected_hostname:?}"
329+
)));
330+
}
331+
}
332+
333+
Ok(())
334+
}
335+
299336
#[must_use]
300337
pub fn to_der(&self) -> &[u8] {
301338
self.csr.as_ref()
@@ -522,4 +559,52 @@ mod tests {
522559
let parsed = parse_pem_certificate(&pem).unwrap();
523560
assert_eq!(parsed, ca.cert_der);
524561
}
562+
563+
#[test]
564+
fn test_csr_verify_hostname_dns_ok() {
565+
let key = generate_key_pair().unwrap();
566+
let csr = Csr::new(&key, &["proxy.example.com".to_string()], vec![]).unwrap();
567+
assert!(
568+
csr.verify_hostname("proxy.example.com").is_ok(),
569+
"matching DNS SAN should pass"
570+
);
571+
}
572+
573+
#[test]
574+
fn test_csr_verify_hostname_ip_ok() {
575+
let key = generate_key_pair().unwrap();
576+
let csr = Csr::new(&key, &["10.0.0.1".to_string()], vec![]).unwrap();
577+
assert!(
578+
csr.verify_hostname("10.0.0.1").is_ok(),
579+
"matching IP SAN should pass"
580+
);
581+
}
582+
583+
#[test]
584+
fn test_csr_verify_hostname_mismatch() {
585+
let key = generate_key_pair().unwrap();
586+
let csr = Csr::new(&key, &["evil.attacker.com".to_string()], vec![]).unwrap();
587+
assert!(
588+
csr.verify_hostname("proxy.example.com").is_err(),
589+
"mismatched DNS SAN should fail"
590+
);
591+
}
592+
593+
#[test]
594+
fn test_csr_verify_hostname_extra_san_rejected() {
595+
let key = generate_key_pair().unwrap();
596+
let csr = Csr::new(
597+
&key,
598+
&[
599+
"proxy.example.com".to_string(),
600+
"evil.extra.com".to_string(),
601+
],
602+
vec![],
603+
)
604+
.unwrap();
605+
assert!(
606+
csr.verify_hostname("proxy.example.com").is_err(),
607+
"CSR with extra SANs beyond the expected hostname should fail"
608+
);
609+
}
525610
}

crates/defguard_core/src/handlers/component_setup.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,11 @@ pub async fn setup_proxy_tls_stream(
525525
}
526526
};
527527

528+
if let Err(e) = csr.verify_hostname(hostname) {
529+
yield Ok(flow.error(&format!("CSR hostname validation failed: {e}")));
530+
return;
531+
}
532+
528533
debug!("Received certificate signing request from Edge for hostname: {hostname}");
529534

530535
// Step 5: Sign certificate
@@ -983,6 +988,11 @@ pub async fn setup_gateway_tls_stream(
983988
}
984989
};
985990

991+
if let Err(e) = csr.verify_hostname(hostname) {
992+
yield Ok(flow.error(&format!("CSR hostname validation failed: {e}")));
993+
return;
994+
}
995+
986996
debug!("Received certificate signing request from Gateway for hostname: {hostname}");
987997

988998
// Step 5: Sign certificate

0 commit comments

Comments
 (0)