11//! TLS certificate generation and management for HTTPS support.
2- //!
3- //! This module handles:
4- //! - Detecting device IP addresses for certificate SANs
5- //! - Generating self-signed certificates using rcgen
6- //! - Loading existing certificates or generating new ones
7- //! - Validating certificate expiration and integrity
8- //! - Boot loop prevention for certificate regeneration
92
103use std:: net:: IpAddr ;
114use std:: path:: { Path , PathBuf } ;
@@ -45,40 +38,29 @@ pub fn get_device_default_ip(device: &Device) -> IpAddr {
4538 }
4639}
4740
48- /// Detect IP addresses for certificate SANs .
41+ /// Build Subject Alternative Names for the certificate .
4942///
50- /// Combines device-specific default IP with any detected network interface IPs.
51- /// The device default IP is always included first to ensure it's in the certificate
52- /// even when there's no SIM card or network connection.
53- pub fn detect_device_ips ( device : & Device ) -> Vec < IpAddr > {
54- let mut ips = Vec :: new ( ) ;
55-
56- // Always include the device-specific default IP first
57- let device_ip = get_device_default_ip ( device) ;
58- ips. push ( device_ip) ;
59- info ! ( "Using device default IP: {}" , device_ip) ;
60-
61- // Try to detect additional IPs from network interfaces
62- match if_addrs:: get_if_addrs ( ) {
63- Ok ( interfaces) => {
64- let detected: Vec < IpAddr > = interfaces
65- . into_iter ( )
66- . filter ( |iface| !iface. is_loopback ( ) )
67- . map ( |iface| iface. ip ( ) )
68- . filter ( |ip| ip. is_ipv4 ( ) && !ips. contains ( ip) )
69- . collect ( ) ;
70-
71- if !detected. is_empty ( ) {
72- info ! ( "Also detected network IPs: {:?}" , detected) ;
73- ips. extend ( detected) ;
74- }
43+ /// Uses device-specific default gateway IP plus localhost, with optional custom hosts.
44+ pub fn get_certificate_sans ( device : & Device , custom_hosts : & [ String ] ) -> Vec < SanType > {
45+ let mut sans = vec ! [
46+ SanType :: IpAddress ( get_device_default_ip( device) ) ,
47+ SanType :: IpAddress ( "127.0.0.1" . parse( ) . unwrap( ) ) ,
48+ ] ;
49+
50+ // Add user-provided custom hosts if any
51+ for host in custom_hosts {
52+ let host = host. trim ( ) ;
53+ if host. is_empty ( ) {
54+ continue ;
7555 }
76- Err ( e) => {
77- warn ! ( "Failed to detect network interfaces: {}" , e) ;
56+ if let Some ( san) = parse_san_entry ( host) {
57+ if !sans. contains ( & san) {
58+ sans. push ( san) ;
59+ }
7860 }
7961 }
8062
81- ips
63+ sans
8264}
8365
8466/// Parse a host string into a SanType (either IP or DNS name).
@@ -94,21 +76,11 @@ fn parse_san_entry(host: &str) -> Option<SanType> {
9476 }
9577}
9678
97- /// Generate a self-signed certificate with the given IP addresses as SANs.
79+ /// Generate a self-signed certificate with the given SANs.
9880///
9981/// Returns (certificate_pem, private_key_pem) as strings.
100- #[ allow( dead_code) ] // Used by tests and kept as simpler public API
101- pub fn generate_self_signed_cert ( ips : & [ IpAddr ] ) -> Result < ( String , String ) , RayhunterError > {
102- generate_self_signed_cert_with_hosts ( ips, & [ ] )
103- }
104-
105- /// Generate a self-signed certificate with IPs and custom hosts as SANs.
106- ///
107- /// Custom hosts can be IP addresses or DNS names (hostnames).
108- /// Returns (certificate_pem, private_key_pem) as strings.
109- pub fn generate_self_signed_cert_with_hosts (
110- ips : & [ IpAddr ] ,
111- custom_hosts : & [ String ] ,
82+ fn generate_self_signed_cert_with_sans (
83+ sans : Vec < SanType > ,
11284) -> Result < ( String , String ) , RayhunterError > {
11385 let mut params = CertificateParams :: default ( ) ;
11486
@@ -120,30 +92,6 @@ pub fn generate_self_signed_cert_with_hosts(
12092 . distinguished_name
12193 . push ( DnType :: OrganizationName , "Electronic Frontier Foundation" ) ;
12294
123- // Add Subject Alternative Names (SANs)
124- let mut sans: Vec < SanType > = ips. iter ( ) . map ( |ip| SanType :: IpAddress ( * ip) ) . collect ( ) ;
125-
126- // Add custom hosts (can be IPs or DNS names)
127- for host in custom_hosts {
128- let host = host. trim ( ) ;
129- if host. is_empty ( ) {
130- continue ;
131- }
132- if let Some ( san) = parse_san_entry ( host) {
133- // Avoid duplicates
134- if !sans. contains ( & san) {
135- info ! ( "Adding custom SAN: {}" , host) ;
136- sans. push ( san) ;
137- }
138- }
139- }
140-
141- // Always include localhost
142- let localhost_san = SanType :: IpAddress ( "127.0.0.1" . parse ( ) . unwrap ( ) ) ;
143- if !sans. contains ( & localhost_san) {
144- sans. push ( localhost_san) ;
145- }
146-
14795 let san_count = sans. len ( ) ;
14896 params. subject_alt_names = sans;
14997
@@ -262,7 +210,10 @@ async fn validate_certificate(cert_path: &Path) -> Result<bool, RayhunterError>
262210 // Full expiration checking would require x509-parser crate
263211 // Instead, we'll rely on rustls failing to load expired certs
264212
265- info ! ( "Certificate validation passed (parseable PEM with {} cert(s))" , certs. len( ) ) ;
213+ info ! (
214+ "Certificate validation passed (parseable PEM with {} cert(s))" ,
215+ certs. len( )
216+ ) ;
266217 Ok ( true )
267218}
268219
@@ -272,13 +223,15 @@ async fn set_key_permissions(key_path: &Path) -> Result<(), RayhunterError> {
272223 use std:: os:: unix:: fs:: PermissionsExt ;
273224
274225 let permissions = std:: fs:: Permissions :: from_mode ( 0o600 ) ;
275- fs:: set_permissions ( key_path, permissions) . await . map_err ( |e| {
276- RayhunterError :: TlsError ( format ! (
277- "Failed to set permissions on {}: {}" ,
278- key_path. display( ) ,
279- e
280- ) )
281- } ) ?;
226+ fs:: set_permissions ( key_path, permissions)
227+ . await
228+ . map_err ( |e| {
229+ RayhunterError :: TlsError ( format ! (
230+ "Failed to set permissions on {}: {}" ,
231+ key_path. display( ) ,
232+ e
233+ ) )
234+ } ) ?;
282235
283236 info ! ( "Set private key permissions to 0600" ) ;
284237 Ok ( ( ) )
@@ -310,15 +263,13 @@ pub async fn load_or_generate_certs(
310263 let key_path = tls_dir. join ( "key.pem" ) ;
311264
312265 // Ensure TLS directory exists
313- fs:: create_dir_all ( & tls_dir)
314- . await
315- . map_err ( |e| {
316- RayhunterError :: TlsError ( format ! (
317- "Failed to create TLS directory {}: {}" ,
318- tls_dir. display( ) ,
319- e
320- ) )
321- } ) ?;
266+ fs:: create_dir_all ( & tls_dir) . await . map_err ( |e| {
267+ RayhunterError :: TlsError ( format ! (
268+ "Failed to create TLS directory {}: {}" ,
269+ tls_dir. display( ) ,
270+ e
271+ ) )
272+ } ) ?;
322273
323274 // Check boot loop prevention - if we've tried too many times, fail immediately
324275 let regen_attempts = read_regen_attempts ( & tls_dir) . await ;
@@ -351,7 +302,10 @@ pub async fn load_or_generate_certs(
351302 true
352303 }
353304 Err ( e) => {
354- warn ! ( "Failed to validate existing certificate: {}, will regenerate" , e) ;
305+ warn ! (
306+ "Failed to validate existing certificate: {}, will regenerate" ,
307+ e
308+ ) ;
355309 true
356310 }
357311 }
@@ -374,31 +328,27 @@ pub async fn load_or_generate_certs(
374328 let _ = fs:: remove_file ( & cert_path) . await ;
375329 let _ = fs:: remove_file ( & key_path) . await ;
376330
377- // Detect IPs (using device-specific defaults) and generate cert with custom hosts
378- let ips = detect_device_ips ( device) ;
379- let ( cert_pem, key_pem) = generate_self_signed_cert_with_hosts ( & ips , custom_hosts ) ?;
331+ // Build SANs and generate certificate
332+ let sans = get_certificate_sans ( device, custom_hosts ) ;
333+ let ( cert_pem, key_pem) = generate_self_signed_cert_with_sans ( sans ) ?;
380334
381335 // Write certificate
382- fs:: write ( & cert_path, & cert_pem)
383- . await
384- . map_err ( |e| {
385- RayhunterError :: TlsError ( format ! (
386- "Failed to write certificate to {}: {}" ,
387- cert_path. display( ) ,
388- e
389- ) )
390- } ) ?;
336+ fs:: write ( & cert_path, & cert_pem) . await . map_err ( |e| {
337+ RayhunterError :: TlsError ( format ! (
338+ "Failed to write certificate to {}: {}" ,
339+ cert_path. display( ) ,
340+ e
341+ ) )
342+ } ) ?;
391343
392344 // Write private key
393- fs:: write ( & key_path, & key_pem)
394- . await
395- . map_err ( |e| {
396- RayhunterError :: TlsError ( format ! (
397- "Failed to write private key to {}: {}" ,
398- key_path. display( ) ,
399- e
400- ) )
401- } ) ?;
345+ fs:: write ( & key_path, & key_pem) . await . map_err ( |e| {
346+ RayhunterError :: TlsError ( format ! (
347+ "Failed to write private key to {}: {}" ,
348+ key_path. display( ) ,
349+ e
350+ ) )
351+ } ) ?;
402352
403353 // Set restrictive permissions on private key
404354 set_key_permissions ( & key_path) . await ?;
@@ -441,29 +391,31 @@ mod tests {
441391 }
442392
443393 #[ test]
444- fn test_detect_device_ips_orbic ( ) {
445- let ips = detect_device_ips ( & Device :: Orbic ) ;
446- // Should always include the device default IP first
447- assert ! ( !ips. is_empty( ) ) ;
448- assert_eq ! ( ips[ 0 ] , "192.168.1.1" . parse:: <IpAddr >( ) . unwrap( ) ) ;
449- // All should be valid IPv4 addresses
450- for ip in & ips {
451- assert ! ( ip. is_ipv4( ) ) ;
452- }
394+ fn test_get_certificate_sans_basic ( ) {
395+ let sans = get_certificate_sans ( & Device :: Orbic , & [ ] ) ;
396+ // Should include device default IP and localhost
397+ assert ! ( sans. len( ) >= 2 ) ;
453398 }
454399
455400 #[ test]
456- fn test_detect_device_ips_tplink ( ) {
457- let ips = detect_device_ips ( & Device :: Tplink ) ;
458- assert ! ( !ips. is_empty( ) ) ;
459- assert_eq ! ( ips[ 0 ] , "192.168.0.1" . parse:: <IpAddr >( ) . unwrap( ) ) ;
401+ fn test_get_certificate_sans_with_custom_hosts ( ) {
402+ let custom_hosts = vec ! [
403+ "rayhunter.local" . to_string( ) ,
404+ "10.0.0.5" . to_string( ) , // IP as string
405+ ] ;
406+ let sans = get_certificate_sans ( & Device :: Orbic , & custom_hosts) ;
407+ // Should include device IP, localhost, and custom hosts
408+ assert ! ( sans. len( ) >= 4 ) ;
460409 }
461410
462411 #[ test]
463412 fn test_generate_self_signed_cert ( ) {
464- let ips: Vec < IpAddr > = vec ! [ "192.168.1.1" . parse( ) . unwrap( ) , "10.0.0.1" . parse( ) . unwrap( ) ] ;
413+ let sans = vec ! [
414+ SanType :: IpAddress ( "192.168.1.1" . parse( ) . unwrap( ) ) ,
415+ SanType :: IpAddress ( "10.0.0.1" . parse( ) . unwrap( ) ) ,
416+ ] ;
465417
466- let result = generate_self_signed_cert ( & ips ) ;
418+ let result = generate_self_signed_cert_with_sans ( sans ) ;
467419 assert ! ( result. is_ok( ) ) ;
468420
469421 let ( cert_pem, key_pem) = result. unwrap ( ) ;
@@ -475,13 +427,6 @@ mod tests {
475427 assert ! ( key_pem. ends_with( "-----END PRIVATE KEY-----\n " ) ) ;
476428 }
477429
478- #[ test]
479- fn test_generate_self_signed_cert_empty_ips ( ) {
480- // Should still work with empty IPs (will add localhost)
481- let result = generate_self_signed_cert ( & [ ] ) ;
482- assert ! ( result. is_ok( ) ) ;
483- }
484-
485430 #[ test]
486431 fn test_get_tls_dir ( ) {
487432 let tls_dir = get_tls_dir ( "/data/rayhunter/qmdl" ) ;
@@ -492,14 +437,14 @@ mod tests {
492437 }
493438
494439 #[ test]
495- fn test_generate_self_signed_cert_with_custom_hosts ( ) {
496- let ips : Vec < IpAddr > = vec ! [ "192.168.1.1" . parse ( ) . unwrap ( ) ] ;
497- let custom_hosts = vec ! [
498- "rayhunter.local" . to_string ( ) ,
499- "10.0.0.5" . to_string ( ) , // IP as string
440+ fn test_generate_self_signed_cert_with_custom_sans ( ) {
441+ let sans = vec ! [
442+ SanType :: IpAddress ( "192.168.1.1" . parse ( ) . unwrap ( ) ) ,
443+ SanType :: DnsName ( Ia5String :: try_from ( "rayhunter.local" ) . unwrap ( ) ) ,
444+ SanType :: IpAddress ( "10.0.0.5" . parse ( ) . unwrap ( ) ) ,
500445 ] ;
501446
502- let result = generate_self_signed_cert_with_hosts ( & ips , & custom_hosts ) ;
447+ let result = generate_self_signed_cert_with_sans ( sans ) ;
503448 assert ! ( result. is_ok( ) ) ;
504449
505450 let ( cert_pem, _) = result. unwrap ( ) ;
@@ -566,8 +511,8 @@ mod tests {
566511 std:: fs:: create_dir_all ( & tls_path) . unwrap ( ) ;
567512
568513 // Create valid cert files (validation now checks PEM format)
569- let ips : Vec < IpAddr > = vec ! [ "192.168.1.1" . parse( ) . unwrap( ) ] ;
570- let ( cert_pem, key_pem) = generate_self_signed_cert ( & ips ) . unwrap ( ) ;
514+ let sans = vec ! [ SanType :: IpAddress ( "192.168.1.1" . parse( ) . unwrap( ) ) ] ;
515+ let ( cert_pem, key_pem) = generate_self_signed_cert_with_sans ( sans ) . unwrap ( ) ;
571516
572517 let cert_path = tls_path. join ( "cert.pem" ) ;
573518 let key_path = tls_path. join ( "key.pem" ) ;
0 commit comments