From 1bd243e0b07481682ee5f4eb8c04130ba82d948a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20G=C3=B3mez=20Bachiller?= Date: Mon, 19 Oct 2026 16:22:58 +0200 Subject: [PATCH] =?UTF-8?q?feat(sede):=20APPLE=20y=20-store=20apple=20acot?= =?UTF-8?q?an=20al=20Almac=C3=A9n=20de=20macOS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements #2006: APPLE y -store apple acotan al Almacén de macOS - Solo en macOS; en Linux y Windows siguen respondiendo SAF_08 y almacén desconocido --- .../command_line/tests/platform.rs | 48 +++++++++++++++---- .../src/desktop/application/store_scope.rs | 4 ++ .../src/desktop/domain/store_scope.rs | 4 ++ .../src/desktop/domain/store_scope/tests.rs | 22 +++++++++ .../src-tauri/src/site/adapters/codec.rs | 30 ++++++++++-- .../src/site/adapters/codec/tests.rs | 26 ++++++++++ .../src-tauri/src/site/adapters/codec_v3.rs | 7 ++- .../src/site/application/filtering.rs | 4 ++ .../src/site/application/filtering/tests.rs | 31 ++++++++++++ .../src/site/domain/protocol/key_store.rs | 26 +++++++++- .../site/domain/protocol/key_store/tests.rs | 42 ++++++++++++++-- .../src-tauri/src/site/domain/protocol/mod.rs | 2 +- 12 files changed, 227 insertions(+), 19 deletions(-) diff --git a/rfirma-app/src-tauri/src/desktop/application/command_line/tests/platform.rs b/rfirma-app/src-tauri/src/desktop/application/command_line/tests/platform.rs index 28a2ebc8a..19108d874 100644 --- a/rfirma-app/src-tauri/src/desktop/application/command_line/tests/platform.rs +++ b/rfirma-app/src-tauri/src/desktop/application/command_line/tests/platform.rs @@ -15,15 +15,19 @@ impl CardAndWindows { impl CertificateStores for CardAndWindows { fn certificates(&self) -> Result, TokenError> { self.opened.set(true); - Ok([("cng:CurrentUser", "WIN"), ("/modulo.so", "TARJETA")] - .into_iter() - .map(|(module, label)| { - TokenCertificate::new( - CertificateRef::new(Store::module(module), "token", label, None), - Vec::new(), - ) - }) - .collect()) + Ok([ + ("cng:CurrentUser", "WIN"), + ("/modulo.so", "TARJETA"), + ("keychain:login", "MAC"), + ] + .into_iter() + .map(|(module, label)| { + TokenCertificate::new( + CertificateRef::new(Store::module(module), "token", label, None), + Vec::new(), + ) + }) + .collect()) } fn discovered_module(&self, library: &str) -> Option { @@ -178,3 +182,29 @@ fn the_password_is_refused_in_any_position_with_a_message_naming_password_fd() { assert!(!said(&outcome).contains("1234"), "nunca repite el secreto"); } } + +#[test] +fn on_macos_store_apple_narrows_to_the_macos_store() { + let outcome = listed_on( + Platform::MacOs, + &["listaliases", "-store", "apple"], + &CardAndWindows::new(), + ); + + assert_eq!(outcome.exit_code, SUCCEEDED); + assert_eq!(outcome.stdout, b"MAC\n"); +} + +#[test] +fn on_linux_store_apple_is_refused_without_opening_any_store() { + let stores = CardAndWindows::new(); + + let outcome = listed_on( + Platform::Linux, + &["listaliases", "-store", "apple"], + &stores, + ); + + assert_eq!(outcome.exit_code, REFUSED); + assert!(!stores.opened.get()); +} diff --git a/rfirma-app/src-tauri/src/desktop/application/store_scope.rs b/rfirma-app/src-tauri/src/desktop/application/store_scope.rs index 15a51fb53..17071e9c9 100644 --- a/rfirma-app/src-tauri/src/desktop/application/store_scope.rs +++ b/rfirma-app/src-tauri/src/desktop/application/store_scope.rs @@ -28,6 +28,10 @@ pub fn within_the_scope( .into_iter() .filter(|certificate| certificate.reference().store().class() == StoreClass::Windows) .collect(), + (StoreScope::Macos, _) => certificates + .into_iter() + .filter(|certificate| certificate.reference().store().class() == StoreClass::Macos) + .collect(), (_, Some(module)) => certificates .into_iter() .filter(|certificate| certificate.reference().module() == module) diff --git a/rfirma-app/src-tauri/src/desktop/domain/store_scope.rs b/rfirma-app/src-tauri/src/desktop/domain/store_scope.rs index df69d2318..5ef96ae3a 100644 --- a/rfirma-app/src-tauri/src/desktop/domain/store_scope.rs +++ b/rfirma-app/src-tauri/src/desktop/domain/store_scope.rs @@ -10,6 +10,7 @@ const AUTO: &str = "auto"; const MOZILLA: &str = "mozilla"; const PKCS11: &str = "pkcs11"; const WINDOWS: &str = "windows"; +const APPLE: &str = "apple"; const NOT_SUPPORTED: [&str; 5] = ["pkcs12", "dni", "dnie", WINDOWS, "mac"]; const NOT_SUPPORTED_ON_WINDOWS: [&str; 4] = ["pkcs12", "dni", "dnie", "mac"]; @@ -23,6 +24,8 @@ pub enum StoreScope { Nss, /// Solo en el almacén de Windows. Windows, + /// Solo en el Almacén de macOS. + Macos, /// Solo en este módulo PKCS#11, si es uno ya descubierto. Module(String), } @@ -82,6 +85,7 @@ fn scope_of(value: &str, platform: Platform) -> Result let lowered = name.trim().to_ascii_lowercase(); let on_windows = platform == Platform::Windows; match (lowered.as_str(), library) { + (APPLE, None) if platform == Platform::MacOs => Ok(StoreScope::Macos), (AUTO | WINDOWS, None) if on_windows => Ok(StoreScope::Windows), (AUTO | MOZILLA, None) if !on_windows => Ok(StoreScope::Nss), (PKCS11, Some(library)) if !library.is_empty() => { diff --git a/rfirma-app/src-tauri/src/desktop/domain/store_scope/tests.rs b/rfirma-app/src-tauri/src/desktop/domain/store_scope/tests.rs index 5c7f88c79..fd0585698 100644 --- a/rfirma-app/src-tauri/src/desktop/domain/store_scope/tests.rs +++ b/rfirma-app/src-tauri/src/desktop/domain/store_scope/tests.rs @@ -118,3 +118,25 @@ fn store_without_a_value_is_refused() { Err(StoreRefusal::Missing) ); } + +#[test] +fn on_macos_apple_narrows_to_the_macos_store_in_any_case() { + for name in ["apple", "Apple", "APPLE"] { + assert_eq!( + scope_of_words_on(Platform::MacOs, &["listaliases", "-store", name]), + Ok(StoreScope::Macos), + "{name}" + ); + } +} + +#[test] +fn outside_macos_apple_is_still_an_unknown_store() { + for platform in [Platform::Linux, Platform::Windows] { + assert_eq!( + scope_of_words_on(platform, &["listaliases", "-store", "apple"]), + Err(StoreRefusal::Unknown("apple".to_owned())), + "{platform:?}" + ); + } +} diff --git a/rfirma-app/src-tauri/src/site/adapters/codec.rs b/rfirma-app/src-tauri/src/site/adapters/codec.rs index 0f2528418..dfa3cccf0 100644 --- a/rfirma-app/src-tauri/src/site/adapters/codec.rs +++ b/rfirma-app/src-tauri/src/site/adapters/codec.rs @@ -3,10 +3,11 @@ use base64::engine::general_purpose::STANDARD; use base64::Engine as _; +use crate::desktop::domain::platform::Platform; use crate::site::domain::batch::parse_local_batch; use crate::site::domain::protocol::{ - read_operation, AfirmaUrl, BatchRequest, Refusal, SiteOperation, WireAnswer, - THIRD_PROTOCOL_VERSION, + read_operation, refuse_the_macos_store, AfirmaUrl, BatchRequest, Refusal, SiteOperation, + WireAnswer, THIRD_PROTOCOL_VERSION, }; use crate::site::adapters::data_download::HttpDataSource; @@ -24,7 +25,10 @@ pub struct V4Codec; impl ProtocolCodec for V4Codec { fn decode(&self, message: &AfirmaUrl) -> SiteRequest { - request_of(read_operation(message, &HttpDataSource)) + request_of(on_this_host( + message, + read_operation(message, &HttpDataSource), + )) } fn encode(&self, outcome: &SiteOutcome) -> String { @@ -72,6 +76,26 @@ impl ProtocolCodec for V4Codec { } /// La petición que el trámite atiende a partir de la operación leída. +/// La lectura de la operación, con `APPLE` rechazado donde no hay Almacén de macOS. +pub(super) fn on_this_host( + url: &AfirmaUrl, + read: Result, +) -> Result { + on_host(url, read, Platform::CURRENT == Platform::MacOs) +} + +fn on_host( + url: &AfirmaUrl, + read: Result, + on_macos: bool, +) -> Result { + let operation = read?; + if !on_macos && operation.chooses_a_certificate() { + refuse_the_macos_store(url)?; + } + Ok(operation) +} + pub(super) fn request_of(read: Result) -> SiteRequest { match read { Ok(SiteOperation::SelectCertificate(request)) => SiteRequest::SelectCertificate(request), diff --git a/rfirma-app/src-tauri/src/site/adapters/codec/tests.rs b/rfirma-app/src-tauri/src/site/adapters/codec/tests.rs index 798822b32..67488d5b9 100644 --- a/rfirma-app/src-tauri/src/site/adapters/codec/tests.rs +++ b/rfirma-app/src-tauri/src/site/adapters/codec/tests.rs @@ -207,3 +207,29 @@ fn the_cancellation_and_the_refusals_go_out_as_the_catalogue_writes_them() { "el detalle no sale" ); } + +fn apple_selection() -> AfirmaUrl { + an_operation(&format!( + "afirma://selectcert?op=selectcert&idsession={CREDENTIAL}&keystore=APPLE" + )) +} + +#[test] +fn off_macos_a_site_that_names_apple_is_answered_with_saf_08() { + let url = apple_selection(); + + let refusal = on_host(&url, read_operation(&url, &HttpDataSource), false) + .expect_err("no hay Almacen de macOS"); + + assert!(refusal.code().as_str() == "SAF_08"); +} + +#[test] +fn on_macos_a_site_that_names_apple_is_attended_over_the_macos_store() { + let url = apple_selection(); + + let operation = on_host(&url, read_operation(&url, &HttpDataSource), true) + .expect("el Almacen de macOS lo abre rFirma"); + + assert!(matches!(operation, SiteOperation::SelectCertificate(_))); +} diff --git a/rfirma-app/src-tauri/src/site/adapters/codec_v3.rs b/rfirma-app/src-tauri/src/site/adapters/codec_v3.rs index 93e47729c..9e4b87d6b 100644 --- a/rfirma-app/src-tauri/src/site/adapters/codec_v3.rs +++ b/rfirma-app/src-tauri/src/site/adapters/codec_v3.rs @@ -5,7 +5,7 @@ use crate::site::domain::protocol::{read_operation_within_the_protocol, AfirmaUr use crate::site::adapters::data_download::HttpDataSource; use crate::site::application::errand::{ProtocolCodec, SiteOutcome, SiteRequest}; -use super::codec::{request_of, V4Codec}; +use super::codec::{on_this_host, request_of, V4Codec}; /// Códec de la versión 3 del protocolo de comunicación con la sede: el catálogo y la forma /// de respuesta de la versión 4, pero cada operación se somete a la versión que declara. @@ -14,7 +14,10 @@ pub struct V3Codec; impl ProtocolCodec for V3Codec { fn decode(&self, message: &AfirmaUrl) -> SiteRequest { - request_of(read_operation_within_the_protocol(message, &HttpDataSource)) + request_of(on_this_host( + message, + read_operation_within_the_protocol(message, &HttpDataSource), + )) } fn encode(&self, outcome: &SiteOutcome) -> String { diff --git a/rfirma-app/src-tauri/src/site/application/filtering.rs b/rfirma-app/src-tauri/src/site/application/filtering.rs index 8f44c78e1..1b4b3968d 100644 --- a/rfirma-app/src-tauri/src/site/application/filtering.rs +++ b/rfirma-app/src-tauri/src/site/application/filtering.rs @@ -96,6 +96,10 @@ fn within_the_scope( .into_iter() .filter(|certificate| certificate.reference().store().class() != StoreClass::Card) .collect()), + StoreScope::Macos => Ok(certificates + .into_iter() + .filter(|certificate| certificate.reference().store().class() == StoreClass::Macos) + .collect()), StoreScope::Module(named) => { let module = directory .discovered_module(named) diff --git a/rfirma-app/src-tauri/src/site/application/filtering/tests.rs b/rfirma-app/src-tauri/src/site/application/filtering/tests.rs index c48660b82..3d132eccb 100644 --- a/rfirma-app/src-tauri/src/site/application/filtering/tests.rs +++ b/rfirma-app/src-tauri/src/site/application/filtering/tests.rs @@ -82,6 +82,13 @@ fn an_nss_certificate(label: &str, der: &[u8]) -> TokenCertificate { ) } +fn a_macos_certificate(label: &str, der: &[u8]) -> TokenCertificate { + TokenCertificate::new( + CertificateRef::new("keychain:login", "llavero", label, vec![0x04]), + der.to_vec(), + ) +} + #[test] fn the_expression_and_the_listing_reach_the_engine_untouched() { let engine = AnEngine::answering(&[0]); @@ -420,3 +427,27 @@ fn a_certificate_inside_the_module_the_site_names_is_usable() { assert_eq!(chosen.reference().label(), "TARJETA"); } + +#[test] +fn a_site_that_names_apple_only_sees_the_certificates_of_the_macos_store() { + let engine = AnEngine::answering(&[0]); + let certificates = vec![ + a_certificate("TOKEN", &[0x01]), + an_nss_certificate("NSS", &[0x03]), + a_card_certificate("TARJETA", &[0x02]), + a_macos_certificate("LLAVERO", &[0x04]), + ]; + let listed = ListedCertificates::new(); + + let kept = keep_what_the_site_accepts( + &engine, + &SiteFilter::default().within(StoreScope::Macos), + certificates.clone(), + &a_directory(&certificates, &listed), + ) + .expect("el Almacen de macOS lo abre rFirma"); + + assert_eq!(kept.len(), 1); + assert_eq!(kept[0].reference().label(), "LLAVERO"); + assert_eq!(engine.asked.borrow()[0].1, "BA=="); +} diff --git a/rfirma-app/src-tauri/src/site/domain/protocol/key_store.rs b/rfirma-app/src-tauri/src/site/domain/protocol/key_store.rs index 3bea947b9..521074110 100644 --- a/rfirma-app/src-tauri/src/site/domain/protocol/key_store.rs +++ b/rfirma-app/src-tauri/src/site/domain/protocol/key_store.rs @@ -14,6 +14,9 @@ const THE_NSS_STORES: [KeyStoreOfTheOriginal; 2] = [ ("Mozilla / Firefox (unificado)", "MOZ_UNI"), ]; +/// Los dos nombres del Almacén de macOS, que solo en macOS rFirma abre. +const THE_MACOS_STORE: [KeyStoreOfTheOriginal; 1] = [("Llavero de Mac", "APPLE")]; + /// Los dos nombres del almacén PKCS#11, el único que admite biblioteca, y solo como acotación. const THE_PKCS11_STORE: [KeyStoreOfTheOriginal; 1] = [("PKCS#11", "PKCS11")]; @@ -98,6 +101,8 @@ pub enum StoreScope { Everywhere, /// Solo los de la familia NSS. Nss, + /// Solo el Almacén de macOS. + Macos, /// Solo el módulo PKCS#11 de esta biblioteca, tal y como vino. Module(String), } @@ -110,6 +115,7 @@ pub fn scope_named_by(url: &AfirmaUrl) -> StoreScope { match named.library { Some(library) if named_among(&named.name, &THE_PKCS11_STORE) => StoreScope::Module(library), None if named_among(&named.name, &THE_NSS_STORES) => StoreScope::Nss, + None if named_among(&named.name, &THE_MACOS_STORE) => StoreScope::Macos, _ => StoreScope::Everywhere, } } @@ -146,10 +152,28 @@ fn refused(blame: Parameter, detail: String) -> Refusal { .because(RefusalSituation::UnsupportedKeyStore) } +/// El `SAF_08` de `APPLE` fuera de macOS, donde no hay Almacén de macOS que abrir. +pub fn refuse_the_macos_store(url: &AfirmaUrl) -> Result<(), Refusal> { + match key_store_named_by(url) { + Some(named) if named.library().is_none() && named_among(named.name(), &THE_MACOS_STORE) => { + Err(refused( + named.declared_in(), + format!( + "rFirma no abre el almacen '{}' que nombra la sede", + named.name() + ), + )) + } + _ => Ok(()), + } +} + /// Un nombre del original que no es el de la familia NSS; uno que el original tampoco reconoce /// no lo es, porque allí acaba en el almacén por omisión del sistema. fn is_a_store_rfirma_does_not_open(name: &str) -> bool { - named_among(name, &THE_STORES_OF_THE_ORIGINAL) && !named_among(name, &THE_NSS_STORES) + named_among(name, &THE_STORES_OF_THE_ORIGINAL) + && !named_among(name, &THE_NSS_STORES) + && !named_among(name, &THE_MACOS_STORE) } /// El original mira antes el nombre visible, y ya recortado (ADR-0022). diff --git a/rfirma-app/src-tauri/src/site/domain/protocol/key_store/tests.rs b/rfirma-app/src-tauri/src/site/domain/protocol/key_store/tests.rs index 4a88984be..19ecce660 100644 --- a/rfirma-app/src-tauri/src/site/domain/protocol/key_store/tests.rs +++ b/rfirma-app/src-tauri/src/site/domain/protocol/key_store/tests.rs @@ -1,5 +1,6 @@ use super::{ - key_store_named_by, refuse_a_key_store_rfirma_does_not_open, scope_named_by, StoreScope, + key_store_named_by, refuse_a_key_store_rfirma_does_not_open, refuse_the_macos_store, + scope_named_by, StoreScope, }; use crate::site::domain::protocol::codes::{Parameter, SafCode}; use crate::site::domain::protocol::refusal::RefusalSituation; @@ -101,7 +102,6 @@ fn a_pkcs12_named_by_the_site_is_refused_with_saf_08() { fn the_stores_of_the_original_that_rfirma_does_not_open_are_refused_one_by_one() { for store in [ "WINDOWS", - "APPLE", "PKCS12", "JAVA", "PKCS11", @@ -146,7 +146,6 @@ fn the_visible_name_of_a_store_names_it_as_the_constant_does() { for store in [ "PKCS#12 / PFX", "pkcs#12 / pfx", - "Llavero de Mac", "DNIe y tarjetas FNMT-TIF", "Tipo desconocido", ] { @@ -228,3 +227,40 @@ fn the_legacy_nss_scopes_the_listing_over_a_module_in_base64() { fn a_store_the_original_does_not_recognise_scopes_nothing() { assert_eq!(scope_named_by(&ksb64("INVENTADO")), StoreScope::Everywhere); } + +#[test] +fn apple_scopes_the_listing_to_the_macos_store_in_any_spelling() { + for named in ["APPLE", "apple", "Llavero de Mac"] { + assert_eq!(scope_named_by(&ksb64(named)), StoreScope::Macos, "{named}"); + } +} + +#[test] +fn apple_is_not_refused_as_a_store_rfirma_does_not_open() { + assert!(refuse_a_key_store_rfirma_does_not_open(&ksb64("APPLE")).is_ok()); +} + +#[test] +fn apple_with_a_library_is_still_refused() { + let refusal = refuse_a_key_store_rfirma_does_not_open(&ksb64("APPLE:/usr/lib/x.dylib")) + .expect_err("la biblioteca no es de un almacen del sistema"); + + assert_eq!(refusal.code(), SafCode::CannotAccessKeystore); +} + +#[test] +fn off_macos_apple_is_refused_with_saf_08_naming_its_parameter() { + for named in ["APPLE", "Llavero de Mac"] { + let refusal = refuse_the_macos_store(&ksb64(named)).expect_err("no hay Almacen de macOS"); + + assert_eq!(refusal.code(), SafCode::CannotAccessKeystore, "{named}"); + } +} + +#[test] +fn off_macos_the_other_stores_are_left_to_their_own_refusal() { + for named in ["NSS", "PKCS12", "INVENTADO"] { + assert!(refuse_the_macos_store(&ksb64(named)).is_ok(), "{named}"); + } + assert!(refuse_the_macos_store(&a_selection("")).is_ok()); +} diff --git a/rfirma-app/src-tauri/src/site/domain/protocol/mod.rs b/rfirma-app/src-tauri/src/site/domain/protocol/mod.rs index 92ef838d6..45944384a 100644 --- a/rfirma-app/src-tauri/src/site/domain/protocol/mod.rs +++ b/rfirma-app/src-tauri/src/site/domain/protocol/mod.rs @@ -67,7 +67,7 @@ pub use framing::{ credential_matches, http_response, read_request, request_credential, split_response, FragmentBuffer, FramedRequest, RefusedOrder, MORE_DATA_NEED, RESPONSE_MAX_SIZE, }; -pub use key_store::{refuse_a_key_store_rfirma_does_not_open, StoreScope}; +pub use key_store::{refuse_a_key_store_rfirma_does_not_open, refuse_the_macos_store, StoreScope}; pub use launch::{ asks_for_active_wait, drawn_ports, location_for_a_refusal, warns_of_an_old_web_client, ChannelCredential, LaunchRequest, NegotiatedCredential, RelayChannelInfo, RelayRequest,