@@ -32,6 +32,10 @@ pub fn install_ca(path: &Path) -> Result<(), InstallError> {
3232 other => return Err ( InstallError :: Unsupported ( other. to_string ( ) ) ) ,
3333 } ;
3434
35+ // Best-effort: also try to install into Firefox NSS stores if certutil
36+ // is available. Firefox maintains its own trust store separate from the OS.
37+ install_firefox_nss ( & path_s) ;
38+
3539 if ok {
3640 Ok ( ( ) )
3741 } else {
@@ -264,3 +268,144 @@ fn install_windows(cert_path: &str) -> bool {
264268 tracing:: error!( "Windows install failed — run as administrator or install manually." ) ;
265269 false
266270}
271+
272+ // ---------- Firefox (NSS) ----------
273+
274+ /// Best-effort install of the CA into all discovered Firefox profiles.
275+ /// Silently no-ops if `certutil` (from libnss3-tools) is not available.
276+ /// Firefox must be closed during install for changes to take effect.
277+ fn install_firefox_nss ( cert_path : & str ) {
278+ // Check if certutil exists at all.
279+ if Command :: new ( "certutil" )
280+ . arg ( "--help" )
281+ . output ( )
282+ . ok ( )
283+ . map ( |o| {
284+ // macOS has a different certutil (built-in) that doesn't support -d.
285+ // Look for NSS-specific flags in the help output.
286+ String :: from_utf8_lossy ( & o. stderr ) . contains ( "-d" )
287+ || String :: from_utf8_lossy ( & o. stdout ) . contains ( "-d" )
288+ } )
289+ . unwrap_or ( false )
290+ == false
291+ {
292+ tracing:: debug!(
293+ "NSS certutil not found — Firefox users must import ca.crt manually \
294+ via Settings -> Privacy & Security -> Certificates."
295+ ) ;
296+ return ;
297+ }
298+
299+ let profiles = firefox_profile_dirs ( ) ;
300+ if profiles. is_empty ( ) {
301+ tracing:: debug!( "no Firefox profiles found" ) ;
302+ return ;
303+ }
304+
305+ let mut ok = 0 ;
306+ for p in & profiles {
307+ if install_nss_in_profile ( p, cert_path) {
308+ ok += 1 ;
309+ }
310+ }
311+ if ok > 0 {
312+ tracing:: info!( "CA installed in {} Firefox profile(s)." , ok) ;
313+ } else {
314+ tracing:: debug!(
315+ "No Firefox profiles updated. If Firefox wasn't running, try installing manually."
316+ ) ;
317+ }
318+ }
319+
320+ fn install_nss_in_profile ( profile : & Path , cert_path : & str ) -> bool {
321+ let prefix = if profile. join ( "cert9.db" ) . exists ( ) {
322+ "sql:"
323+ } else if profile. join ( "cert8.db" ) . exists ( ) {
324+ ""
325+ } else {
326+ return false ;
327+ } ;
328+ let dir_arg = format ! ( "{}{}" , prefix, profile. display( ) ) ;
329+
330+ // Delete any stale entry first (ignore errors).
331+ let _ = Command :: new ( "certutil" )
332+ . args ( [ "-D" , "-n" , CERT_NAME , "-d" , & dir_arg] )
333+ . output ( ) ;
334+
335+ let res = Command :: new ( "certutil" )
336+ . args ( [
337+ "-A" ,
338+ "-n" ,
339+ CERT_NAME ,
340+ "-t" ,
341+ "C,," ,
342+ "-d" ,
343+ & dir_arg,
344+ "-i" ,
345+ cert_path,
346+ ] )
347+ . output ( ) ;
348+ match res {
349+ Ok ( o) if o. status . success ( ) => {
350+ tracing:: debug!( "NSS install ok: {}" , profile. display( ) ) ;
351+ true
352+ }
353+ Ok ( o) => {
354+ tracing:: debug!(
355+ "NSS install failed for {}: {}" ,
356+ profile. display( ) ,
357+ String :: from_utf8_lossy( & o. stderr) . trim( )
358+ ) ;
359+ false
360+ }
361+ Err ( e) => {
362+ tracing:: debug!( "NSS certutil exec failed for {}: {}" , profile. display( ) , e) ;
363+ false
364+ }
365+ }
366+ }
367+
368+ fn firefox_profile_dirs ( ) -> Vec < std:: path:: PathBuf > {
369+ use std:: path:: PathBuf ;
370+ let mut roots: Vec < PathBuf > = Vec :: new ( ) ;
371+ let home = std:: env:: var ( "HOME" ) . unwrap_or_default ( ) ;
372+ match std:: env:: consts:: OS {
373+ "macos" => {
374+ roots. push ( PathBuf :: from ( format ! (
375+ "{}/Library/Application Support/Firefox/Profiles" ,
376+ home
377+ ) ) ) ;
378+ }
379+ "linux" => {
380+ roots. push ( PathBuf :: from ( format ! ( "{}/.mozilla/firefox" , home) ) ) ;
381+ roots. push ( PathBuf :: from ( format ! (
382+ "{}/snap/firefox/common/.mozilla/firefox" ,
383+ home
384+ ) ) ) ;
385+ }
386+ "windows" => {
387+ if let Ok ( appdata) = std:: env:: var ( "APPDATA" ) {
388+ roots. push ( PathBuf :: from ( format ! ( "{}\\ Mozilla\\ Firefox\\ Profiles" , appdata) ) ) ;
389+ }
390+ }
391+ _ => { }
392+ }
393+
394+ let mut out: Vec < PathBuf > = Vec :: new ( ) ;
395+ for root in & roots {
396+ let Ok ( entries) = std:: fs:: read_dir ( root) else {
397+ continue ;
398+ } ;
399+ for ent in entries. flatten ( ) {
400+ let p = ent. path ( ) ;
401+ if !p. is_dir ( ) {
402+ continue ;
403+ }
404+ // A profile has cert9.db or cert8.db.
405+ if p. join ( "cert9.db" ) . exists ( ) || p. join ( "cert8.db" ) . exists ( ) {
406+ out. push ( p) ;
407+ }
408+ }
409+ }
410+ out
411+ }
0 commit comments