@@ -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
3133pub 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}
0 commit comments