Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,19 @@ impl CardAndWindows {
impl CertificateStores for CardAndWindows {
fn certificates(&self) -> Result<Vec<TokenCertificate>, 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<PathBuf> {
Expand Down Expand Up @@ -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());
}
4 changes: 4 additions & 0 deletions rfirma-app/src-tauri/src/desktop/application/store_scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions rfirma-app/src-tauri/src/desktop/domain/store_scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"];

Expand All @@ -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),
}
Expand Down Expand Up @@ -82,6 +85,7 @@ fn scope_of(value: &str, platform: Platform) -> Result<StoreScope, StoreRefusal>
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() => {
Expand Down
22 changes: 22 additions & 0 deletions rfirma-app/src-tauri/src/desktop/domain/store_scope/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:?}"
);
}
}
30 changes: 27 additions & 3 deletions rfirma-app/src-tauri/src/site/adapters/codec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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<SiteOperation, Refusal>,
) -> Result<SiteOperation, Refusal> {
on_host(url, read, Platform::CURRENT == Platform::MacOs)
}

fn on_host(
url: &AfirmaUrl,
read: Result<SiteOperation, Refusal>,
on_macos: bool,
) -> Result<SiteOperation, Refusal> {
let operation = read?;
if !on_macos && operation.chooses_a_certificate() {
refuse_the_macos_store(url)?;
}
Ok(operation)
}

pub(super) fn request_of(read: Result<SiteOperation, Refusal>) -> SiteRequest {
match read {
Ok(SiteOperation::SelectCertificate(request)) => SiteRequest::SelectCertificate(request),
Expand Down
26 changes: 26 additions & 0 deletions rfirma-app/src-tauri/src/site/adapters/codec/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(_)));
}
7 changes: 5 additions & 2 deletions rfirma-app/src-tauri/src/site/adapters/codec_v3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions rfirma-app/src-tauri/src/site/application/filtering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
31 changes: 31 additions & 0 deletions rfirma-app/src-tauri/src/site/application/filtering/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down Expand Up @@ -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==");
}
26 changes: 25 additions & 1 deletion rfirma-app/src-tauri/src/site/domain/protocol/key_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")];

Expand Down Expand Up @@ -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),
}
Expand All @@ -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,
}
}
Expand Down Expand Up @@ -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).
Expand Down
42 changes: 39 additions & 3 deletions rfirma-app/src-tauri/src/site/domain/protocol/key_store/tests.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
] {
Expand Down Expand Up @@ -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());
}
Loading
Loading