diff --git a/rfirma-app/src-tauri/src/desktop/adapters/card_drivers.rs b/rfirma-app/src-tauri/src/desktop/adapters/card_drivers.rs new file mode 100644 index 00000000..0400c503 --- /dev/null +++ b/rfirma-app/src-tauri/src/desktop/adapters/card_drivers.rs @@ -0,0 +1,30 @@ +//! Los controladores de tarjeta (`.bundle`) que macOS trae para el servicio de tarjetas inteligentes. + +use std::path::Path; + +const SYSTEM_DRIVERS: &str = "/usr/libexec/SmartCardServices/drivers"; + +/// Los nombres de los controladores del sistema, ordenados; vacío si no hay ninguno o no se leen. +pub fn system_card_drivers() -> Vec { + card_drivers_in(Path::new(SYSTEM_DRIVERS)) +} + +fn card_drivers_in(directory: &Path) -> Vec { + let Ok(entries) = std::fs::read_dir(directory) else { + return Vec::new(); + }; + let mut names: Vec = entries + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| { + path.extension() + .is_some_and(|extension| extension == "bundle") + }) + .filter_map(|path| Some(path.file_stem()?.to_string_lossy().into_owned())) + .collect(); + names.sort(); + names +} + +#[cfg(test)] +mod tests; diff --git a/rfirma-app/src-tauri/src/desktop/adapters/card_drivers/tests.rs b/rfirma-app/src-tauri/src/desktop/adapters/card_drivers/tests.rs new file mode 100644 index 00000000..a9a8cdd6 --- /dev/null +++ b/rfirma-app/src-tauri/src/desktop/adapters/card_drivers/tests.rs @@ -0,0 +1,19 @@ +use super::*; + +#[test] +fn lists_the_bundles_sorted_by_name_and_ignores_other_files() { + let directory = tempfile::tempdir().unwrap(); + for name in ["ifd-gempc.bundle", "ifd-ccid.bundle", "readme.txt"] { + std::fs::create_dir(directory.path().join(name)).unwrap(); + } + + assert_eq!( + card_drivers_in(directory.path()), + vec!["ifd-ccid", "ifd-gempc"] + ); +} + +#[test] +fn a_missing_directory_has_no_drivers() { + assert!(card_drivers_in(Path::new("/nonexistent/drivers")).is_empty()); +} diff --git a/rfirma-app/src-tauri/src/desktop/adapters/debug_info.rs b/rfirma-app/src-tauri/src/desktop/adapters/debug_info.rs index fd92190b..59254c2c 100644 --- a/rfirma-app/src-tauri/src/desktop/adapters/debug_info.rs +++ b/rfirma-app/src-tauri/src/desktop/adapters/debug_info.rs @@ -11,9 +11,9 @@ use crate::desktop::adapters::installation::{this_glibc, this_installation, this use crate::desktop::adapters::paths::{Paths, Platform}; use crate::desktop::adapters::registry::this_desktop; use crate::desktop::application::debug_report::{ - DebugReport, DiscardReason, LinuxEnvironment, ModuleStatus, NativeLibrary, NativeLibraryStatus, - NssProfile, NssProfileState, NssStores, OwnerFolder, PcscStatus, Pkcs11Module, Pkcs11Modules, - ProtocolHandlerStatus, ReportOwner, WindowsStores, + DebugReport, DiscardReason, LinuxEnvironment, MacosStores, ModuleStatus, NativeLibrary, + NativeLibraryStatus, NssProfile, NssProfileState, NssStores, OwnerFolder, PcscStatus, + Pkcs11Module, Pkcs11Modules, ProtocolHandlerStatus, ReportOwner, WindowsStores, }; use crate::desktop::domain::channel::Channel; use crate::identity::adapters as identity_adapters; @@ -57,6 +57,7 @@ pub fn this_process_report() -> DebugReport { pkcs11_modules: pkcs11_modules(), nss_stores: nss_stores(), windows_stores: windows_stores(channel), + macos_stores: macos_stores(channel), } } @@ -274,9 +275,20 @@ fn native_library() -> NativeLibrary { } fn windows_stores(channel: Channel) -> Option { - if channel != Channel::Windows { - return None; - } + (channel == Channel::Windows).then(|| WindowsStores { + local_channel: user_trust_state(), + minidrivers: crate::desktop::adapters::minidrivers::registered_minidrivers(), + }) +} + +fn macos_stores(channel: Channel) -> Option { + (channel == Channel::MacOs).then(|| MacosStores { + local_channel: user_trust_state(), + card_drivers: crate::desktop::adapters::card_drivers::system_card_drivers(), + }) +} + +fn user_trust_state() -> NssProfileState { let local_ca = Paths::from_environment() .ok() .map(|paths| LocalCaStore::of(&paths)); @@ -287,17 +299,13 @@ fn windows_stores(channel: Channel) -> Option { .copied() .unwrap_or(false) }); - let local_channel = if trusted { + if trusted { NssProfileState::TrustsLocalChannel { until: local_ca.as_ref().and_then(local_ca_expiry), } } else { NssProfileState::DoesNotTrustLocalChannel - }; - Some(WindowsStores { - local_channel, - minidrivers: crate::desktop::adapters::minidrivers::registered_minidrivers(), - }) + } } fn nss_stores() -> Option { diff --git a/rfirma-app/src-tauri/src/desktop/adapters/mod.rs b/rfirma-app/src-tauri/src/desktop/adapters/mod.rs index d569c29a..30a6f70c 100644 --- a/rfirma-app/src-tauri/src/desktop/adapters/mod.rs +++ b/rfirma-app/src-tauri/src/desktop/adapters/mod.rs @@ -1,5 +1,6 @@ //! Adaptadores de `desktop`: todo lo que toca el mundo, incluidas las órdenes y las vistas de Tauri. +pub mod card_drivers; pub mod channel; pub mod choice; pub mod command_line_ports; diff --git a/rfirma-app/src-tauri/src/desktop/application/debug_report.rs b/rfirma-app/src-tauri/src/desktop/application/debug_report.rs index 7881e20f..bd7b5a8f 100644 --- a/rfirma-app/src-tauri/src/desktop/application/debug_report.rs +++ b/rfirma-app/src-tauri/src/desktop/application/debug_report.rs @@ -84,6 +84,15 @@ pub struct WindowsStores { pub minidrivers: Vec, } +/// Lo que solo existe en macOS: la confianza del usuario y los controladores de tarjeta del sistema. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MacosStores { + /// Si la confianza del usuario en el Almacén de macOS acepta el canal local. + pub local_channel: NssProfileState, + /// Controladores de tarjeta que macOS tiene instalados. + pub card_drivers: Vec, +} + /// Dónde y con qué instalador está el flatpak. #[derive(Clone, Debug, PartialEq, Eq)] pub struct FlatpakInstallation { @@ -263,6 +272,8 @@ pub struct DebugReport { pub nss_stores: Option, /// Almacén de Windows y minidrivers; `None` fuera de Windows. pub windows_stores: Option, + /// Almacén de macOS y controladores de tarjeta; `None` fuera de macOS. + pub macos_stores: Option, } /// Una cabecera y una sección por bloque, con el patrón `clave: estado · detalle` y las rutas anonimizadas. @@ -293,6 +304,7 @@ pub fn debug_report_text(report: &DebugReport, owner: &ReportOwner) -> String { .map(|stores| section("Almacenes NSS", &stores::nss_lines(stores, owner))), ) .chain(stores::windows_sections(report.windows_stores.as_ref())) + .chain(stores::macos_sections(report.macos_stores.as_ref())) .flatten() .collect::>() .join("\n\n") diff --git a/rfirma-app/src-tauri/src/desktop/application/debug_report/stores.rs b/rfirma-app/src-tauri/src/desktop/application/debug_report/stores.rs index e2c0875f..dea33878 100644 --- a/rfirma-app/src-tauri/src/desktop/application/debug_report/stores.rs +++ b/rfirma-app/src-tauri/src/desktop/application/debug_report/stores.rs @@ -1,6 +1,6 @@ -//! Las líneas de los almacenes del informe: los perfiles NSS, y el almacén de Windows con sus minidrivers. +//! Las líneas de los almacenes del informe: los perfiles NSS, y el almacén de Windows con sus minidrivers y el de macOS con sus controladores. -use super::{section, NssProfileState, NssStores, ReportOwner, WindowsStores}; +use super::{section, MacosStores, NssProfileState, NssStores, ReportOwner, WindowsStores}; pub(super) fn windows_sections(stores: Option<&WindowsStores>) -> [Option; 2] { let Some(stores) = stores else { @@ -22,6 +22,29 @@ pub(super) fn windows_sections(stores: Option<&WindowsStores>) -> [Option) -> [Option; 2] { + let Some(stores) = stores else { + return [None, None]; + }; + let trust = format!( + "Confianza del usuario: {}", + profile_state(&stores.local_channel) + ); + let drivers = if stores.card_drivers.is_empty() { + vec!["tarjetas: ninguno instalado".to_owned()] + } else { + stores + .card_drivers + .iter() + .map(|name| format!("{name}: instalado")) + .collect() + }; + [ + section("Almacén de macOS", &[trust]), + section("Controladores de tarjeta", &drivers), + ] +} + pub(super) fn nss_lines(stores: &NssStores, owner: &ReportOwner) -> Vec { let rfirma_store = if stores.rfirma_store_installed { "instalado" diff --git a/rfirma-app/src-tauri/src/desktop/application/debug_report/stores_tests.rs b/rfirma-app/src-tauri/src/desktop/application/debug_report/stores_tests.rs index 1154e615..eb7a72f3 100644 --- a/rfirma-app/src-tauri/src/desktop/application/debug_report/stores_tests.rs +++ b/rfirma-app/src-tauri/src/desktop/application/debug_report/stores_tests.rs @@ -144,6 +144,59 @@ fn windows_without_registered_minidrivers_says_none() { ); } +fn report_with_macos(trust: NssProfileState, drivers: Vec<&str>) -> String { + let mut report = a_flatpak_report(); + report.pcsc = None; + report.macos_stores = Some(MacosStores { + local_channel: trust, + card_drivers: drivers.into_iter().map(str::to_owned).collect(), + }); + debug_report_text(&report, &ana()) +} + +#[test] +fn macos_replaces_the_nss_stores_with_the_macos_store_and_card_drivers() { + let text = report_with_macos( + NssProfileState::TrustsLocalChannel { + until: Some("2027-03-14".to_owned()), + }, + vec!["ifd-ccid", "ifd-gempc"], + ); + + assert!( + text.ends_with( + "Almacén de macOS\n \ + Confianza del usuario: confía en el canal local · hasta 2027-03-14\n\ + \n\ + Controladores de tarjeta\n \ + ifd-ccid: instalado\n \ + ifd-gempc: instalado" + ), + "{text}" + ); + assert!(!text.contains("Almacenes NSS"), "{text}"); +} + +#[test] +fn macos_without_trust_in_the_local_channel_says_so() { + let text = report_with_macos(NssProfileState::DoesNotTrustLocalChannel, vec!["ifd-ccid"]); + + assert!( + text.contains("Almacén de macOS\n Confianza del usuario: no confía en el canal local\n"), + "{text}" + ); +} + +#[test] +fn macos_without_card_drivers_says_none() { + let text = report_with_macos(NssProfileState::DoesNotTrustLocalChannel, Vec::new()); + + assert!( + text.ends_with("Controladores de tarjeta\n tarjetas: ninguno instalado"), + "{text}" + ); +} + #[test] fn a_flatpak_store_says_its_browser_and_channel_and_its_full_path() { let section = report_with_nss( diff --git a/rfirma-app/src-tauri/src/desktop/application/debug_report/tests.rs b/rfirma-app/src-tauri/src/desktop/application/debug_report/tests.rs index bfbffd0b..3ceb31aa 100644 --- a/rfirma-app/src-tauri/src/desktop/application/debug_report/tests.rs +++ b/rfirma-app/src-tauri/src/desktop/application/debug_report/tests.rs @@ -43,6 +43,7 @@ pub(super) fn a_flatpak_report() -> DebugReport { pkcs11_modules: None, nss_stores: None, windows_stores: None, + macos_stores: None, } }