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 @@ -17,7 +17,6 @@ como fallaría Linux sin el servicio y dice en su nombre qué falta.
| Cerrojos (Firefox, carpeta de paso) | Los de Unix, sin cambios. |
| Llavero `oo7` (ADR-0034) | `PendingMacosKeychain`: responde que no hay llavero. |
| Manejadores de `afirma://` por GIO | `PendingMacosLaunchServices`: la lista no está disponible y elegir falla. |
| CA local en los almacenes NSS (ADR-0005) | `PendingMacosKeychainTrust`, sin perfiles: la CA consta como no instalada. |
| Diálogo GTK de fallo de arranque | El texto sale por la salida de error. |

La firma trifásica (ADR-0001) funciona de extremo a extremo con una clave en memoria: PAdES,
Expand Down Expand Up @@ -150,5 +149,5 @@ dependencia en todas las plataformas para algo que en Linux y Windows ya resuelv
el `Info.plist` embebido, y llamarla dos veces falla al enlazar.
- Los códigos con que macOS rechaza un certificado de sede no reconocido (ADR-0039) se añaden a
los de OpenSSL y Schannel, sin condicional de sistema.
- Sin llavero ni CA local, el navegador no confía en el canal local de un trámite de sede en
macOS sin instalar la CA a mano.
- La CA local (ADR-0005) se confía en el dominio de la persona usuaria, con el diálogo de
contraseña de macOS; Safari, Chrome y Firefox 120 o posterior la leen de ahí, sin perfiles NSS.
22 changes: 18 additions & 4 deletions rfirma-app/src-tauri/src/desktop/adapters/tauri.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ fn brand_of(
if crate::site::adapters::is_the_system_root_store(profile) {
return StoreBrand::Windows;
}
if crate::site::adapters::is_the_macos_user_trust(profile) {
return StoreBrand::Macos;
}
brand_of_class(StoreClass::of_nss_owner(owner))
}

Expand All @@ -148,12 +151,23 @@ fn measured_local_ca_certificate_signal(
site: &SiteRoot,
restart_firefox_notice: bool,
) -> crate::desktop::domain::status::SignalRow {
let detail = site
.measure_local_ca_trust()
.unwrap_or_default()
local_ca_signal_of(
site.measure_local_ca_trust().unwrap_or_default(),
|profile| site.trust.owner_of(profile),
restart_firefox_notice,
)
}

/// La señal del certificado de rFirma con lo medido en cada almacén, rotulado con su marca.
fn local_ca_signal_of(
readings: Vec<crate::site::ProfileTrust>,
owner_of: impl Fn(&std::path::Path) -> Option<crate::identity::domain::browsers::NssOwner>,
restart_firefox_notice: bool,
) -> crate::desktop::domain::status::SignalRow {
let detail = readings
.into_iter()
.map(|reading| StoreDetail {
brand: brand_of(&reading.profile, site.trust.owner_of(&reading.profile)),
brand: brand_of(&reading.profile, owner_of(&reading.profile)),
trusted: reading.trusted,
})
.collect();
Expand Down
13 changes: 13 additions & 0 deletions rfirma-app/src-tauri/src/desktop/adapters/tauri/tests.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use super::*;

mod macos_trust;

#[test]
fn a_trust_profile_takes_its_brand_from_its_discovered_owner_and_not_from_its_path() {
use crate::identity::domain::browsers::{browser_named, BrowserChannel, NssOwner};
Expand Down Expand Up @@ -32,6 +34,17 @@ fn a_trust_profile_takes_its_brand_from_its_discovered_owner_and_not_from_its_pa
);
}

#[test]
fn the_macos_user_trust_is_the_macos_brand() {
assert_eq!(
brand_of(
std::path::Path::new(crate::site::adapters::MACOS_USER_TRUST),
None
),
StoreBrand::Macos
);
}

#[test]
fn a_card_and_an_installed_p12_are_each_their_own_brand() {
assert_eq!(brand_of_class(StoreClass::Card), StoreBrand::Card);
Expand Down
136 changes: 136 additions & 0 deletions rfirma-app/src-tauri/src/desktop/adapters/tauri/tests/macos_trust.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
use std::path::{Path, PathBuf};
use std::sync::Mutex;

use super::super::local_ca_signal_of;
use crate::desktop::domain::status::{
ActionKind, SignalDetail, SignalRow, StoreBrand, StoreDetail, Verdict,
};
use crate::site::adapters::MACOS_USER_TRUST;
use crate::site::application::tests::InMemoryCaSlots;
use crate::site::application::trust::{measure_local_ca_trust, refresh_local_ca_trust};
use crate::site::domain::local_ca::{ChannelMark, LocalCa};
use crate::site::domain::trust::{Moment, TRUSTED_SSL_CA};
use crate::site::domain::trust_error::{Situation, TrustError};
use crate::site::ports::{LocalCaSlots, TrustStores};

/// La confianza de la persona usuaria de macOS, con su diálogo de contraseña aceptado o cancelado.
struct UserTrust {
password_given: bool,
trusted: Mutex<Vec<Vec<u8>>>,
}

impl UserTrust {
fn accepting() -> Self {
Self {
password_given: true,
trusted: Mutex::new(Vec::new()),
}
}

fn cancelling() -> Self {
Self {
password_given: false,
..Self::accepting()
}
}
}

impl TrustStores for UserTrust {
fn install(&self, _profile: &Path, der: &[u8], _nickname: &str) -> Result<(), TrustError> {
if !self.password_given {
return Err(TrustError::new(
Situation::TrustNotWritten,
"se ha cancelado el diálogo de contraseña de macOS",
));
}
crate::lock(&self.trusted).push(der.to_vec());
Ok(())
}

fn trust_of(&self, _profile: &Path, der: &[u8]) -> Result<Option<u32>, TrustError> {
Ok(crate::lock(&self.trusted)
.iter()
.any(|trusted| trusted == der)
.then_some(TRUSTED_SSL_CA))
}

fn withdraw(&self, _profile: &Path, der: &[u8]) -> Result<(), TrustError> {
crate::lock(&self.trusted).retain(|trusted| trusted != der);
Ok(())
}

fn local_cas(&self, _profile: &Path) -> Result<Vec<Vec<u8>>, TrustError> {
Ok(crate::lock(&self.trusted).clone())
}
}

fn the_macos_user_trust() -> [PathBuf; 1] {
[PathBuf::from(MACOS_USER_TRUST)]
}

fn installed_then_measured(slots: &InMemoryCaSlots, trust: &UserTrust) -> SignalRow {
let profiles = the_macos_user_trust();
let _ = refresh_local_ca_trust(
slots,
&profiles,
trust,
Moment::Startup,
ChannelMark::Native,
);
let readings =
measure_local_ca_trust(slots, &profiles, trust).expect("las ranuras en memoria se leen");
local_ca_signal_of(readings, |_| None, false)
}

fn the_macos_store(trusted: bool) -> Option<SignalDetail> {
Some(SignalDetail::Trust {
stores: vec![StoreDetail {
brand: StoreBrand::Macos,
trusted,
}],
})
}

#[test]
fn installing_with_the_password_given_shows_the_macos_store_trusting_the_local_ca() {
let signal = installed_then_measured(&InMemoryCaSlots::default(), &UserTrust::accepting());

assert_eq!(signal.detail, the_macos_store(true));
assert_eq!(signal.verdict, Verdict::Correct);
assert_eq!(signal.action, None);
}

#[test]
fn cancelling_the_password_dialog_leaves_the_local_ca_not_installed_and_offers_to_install_it() {
let signal = installed_then_measured(&InMemoryCaSlots::default(), &UserTrust::cancelling());

assert_eq!(signal.detail, the_macos_store(false));
assert_eq!(signal.verdict, Verdict::Incorrect);
assert_eq!(
signal.action.map(|action| action.kind),
Some(ActionKind::Repair)
);
}

#[test]
fn an_expired_local_ca_is_replaced_and_the_macos_store_trusts_the_new_one() {
let slots = InMemoryCaSlots::default();
let trust = UserTrust::accepting();
let expired = LocalCa::expired_for_test().expect("se fabrica una CA caducada");
slots
.write_serving(&expired)
.expect("las ranuras en memoria se escriben");

let signal = installed_then_measured(&slots, &trust);

let serving = slots
.serving()
.expect("se lee")
.expect("hay una CA vigente");
assert_eq!(signal.detail, the_macos_store(true));
assert_ne!(der_of(&serving), der_of(&expired));
}

fn der_of(ca: &LocalCa) -> Vec<u8> {
ca.certificate().to_der().expect("sale en DER")
}
2 changes: 1 addition & 1 deletion rfirma-app/src-tauri/src/desktop/domain/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ pub enum StoreBrand {
Installed,
/// Almacén raíz del usuario de Windows.
Windows,
/// Los certificados con clave del llavero de macOS.
/// El Almacén de macOS: los certificados con clave del llavero y la confianza de la persona usuaria.
Macos,
}

Expand Down
Loading
Loading