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 @@ -12,7 +12,7 @@ como fallaría Linux sin el servicio y dice en su nombre qué falta.
| --- | --- |
| OpenSSL del sistema | `openssl` con `vendored`, como en Windows: el binario no depende del de Homebrew y compila en cruzado. |
| Canal TLS local con `native-tls` | rustls, como en Windows (ADR-0036). |
| Token PKCS#11 y sus almacenes | `MacosToken`: el llavero por Security.framework para el Almacén de macOS, que todavía no firma, y el `RealToken` de Linux para los módulos, que solo encuentra si `RFIRMA_PKCS11_MODULE` lo nombra. |
| Token PKCS#11 y sus almacenes | `MacosToken`: el llavero por Security.framework para el Almacén de macOS, que firma el resumen con la clave del llavero, y el `RealToken` de Linux para los módulos, que solo encuentra si `RFIRMA_PKCS11_MODULE` lo nombra. |
| `mlock` del secreto | `mlock`; `MADV_DONTDUMP` es solo de Linux. |
| Cerrojos (Firefox, carpeta de paso) | Los de Unix, sin cambios. |
| Llavero `oo7` (ADR-0034) | `PendingMacosKeychain`: responde que no hay llavero. |
Expand Down
13 changes: 3 additions & 10 deletions rfirma-app/src-tauri/src/identity/adapters/macos_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::path::Path;
use super::pkcs11::RealToken;
use crate::identity::domain::algorithm::SignatureAlgorithm;
use crate::identity::domain::certificate::{CertificateRef, TokenCertificate};
use crate::identity::domain::error::{Situation, TokenError};
use crate::identity::domain::error::TokenError;
use crate::identity::domain::protected_secret::ProtectedSecret;
use crate::identity::domain::secret::{PinWarning, StoreSecret};
use crate::identity::domain::store::{Store, StoreClass, MACOS_STORE_PREFIX};
Expand Down Expand Up @@ -44,13 +44,6 @@ fn stores_besides(pkcs11: Vec<Store>, module_named: bool) -> Vec<Store> {
std::iter::once(user_store()).chain(pkcs11).collect()
}

fn not_signing_yet() -> TokenError {
TokenError::new(
Situation::MechanismNotOffered,
"el Almacén de macOS todavía no firma",
)
}

/// El adaptador de [`Token`] en macOS: el llavero para el Almacén de macOS y [`RealToken`] para los demás.
#[derive(Clone, Copy, Debug, Default)]
pub struct MacosToken;
Expand Down Expand Up @@ -101,7 +94,7 @@ impl Token for MacosToken {
algorithm: SignatureAlgorithm,
) -> Result<(), TokenError> {
if is_a_keychain(&reference.store()) {
return Err(not_signing_yet());
return keychain::offers(reference, algorithm);
}
RealToken.offers(reference, algorithm)
}
Expand All @@ -125,7 +118,7 @@ impl Token for MacosToken {
data: &[u8],
) -> Result<Vec<u8>, TokenError> {
if is_a_keychain(&reference.store()) {
return Err(not_signing_yet());
return keychain::sign(reference, algorithm, data);
}
RealToken.sign_with_secret(reference, secret, algorithm, data)
}
Expand Down
121 changes: 119 additions & 2 deletions rfirma-app/src-tauri/src/identity/adapters/macos_store/keychain.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
//! El llavero de macOS leído con Security.framework: las identidades con su certificado y el token de su clave, sin abrirla (ADR-0001).
//! El llavero de macOS por Security.framework: las identidades se listan sin abrir su clave, y el resumen lo firma el sistema (ADR-0001).

use core_foundation::base::{CFType, TCFType};
use core_foundation::error::CFError;
use core_foundation::string::CFString;
use openssl::hash::{hash, MessageDigest};
use security_framework::certificate::SecCertificate;
use security_framework::identity::SecIdentity;
use security_framework::item::{ItemClass, ItemSearchOptions, Limit, Reference, SearchResult};
use security_framework::key::SecKey;
use security_framework::key::{Algorithm, SecKey};
use security_framework::os::macos::keychain::SecKeychain;
use security_framework_sys::item::kSecAttrTokenID;

use super::SEARCH_LIST;
use crate::identity::domain::algorithm::SignatureAlgorithm;
use crate::identity::domain::certificate::{CertificateRef, TokenCertificate};
use crate::identity::domain::ecdsa;
use crate::identity::domain::error::{Situation, TokenError};
use crate::identity::domain::store::{Store, MACOS_STORE_PREFIX};

Expand Down Expand Up @@ -56,6 +59,120 @@ pub fn every_certificate(store: &Store) -> Result<Vec<TokenCertificate>, TokenEr
.collect())
}

/// Comprueba que la clave del certificado es de la clase que exige el algoritmo.
pub fn offers(reference: &CertificateRef, algorithm: SignatureAlgorithm) -> Result<(), TokenError> {
let (certificate, _) = identity_of(reference)?;
if certificate.key_kind() == Some(algorithm.key_kind()) {
return Ok(());
}
Err(TokenError::new(
Situation::MechanismNotOffered,
format!(
"la clave de {} no sirve para {}",
reference.label(),
algorithm.name()
),
))
}

/// Resume `data` y firma el resumen con la clave del certificado; macOS pide permiso o el PIN si hace falta.
pub fn sign(
reference: &CertificateRef,
algorithm: SignatureAlgorithm,
data: &[u8],
) -> Result<Vec<u8>, TokenError> {
let (_, identity) = identity_of(reference)?;
let key = identity.private_key().map_err(|error| failure(&error))?;
let digest = ecdsa::digest(algorithm, data)?;
key.create_signature(over_a_digest(algorithm), &digest)
.map_err(|error| refused(&error))
}

/// La situación del catálogo que corresponde a un error de Security.framework, CryptoTokenKit o PC/SC.
pub fn situation_of(domain: &str, code: i64) -> Situation {
match (domain, code as i32) {
(OS_STATUS_DOMAIN, -128) | (TOKEN_DOMAIN, -4) => Situation::PinEntryCancelled,
(OS_STATUS_DOMAIN, -25293 | WRONG_CHV) | (TOKEN_DOMAIN, -5) => Situation::IncorrectPin,
(OS_STATUS_DOMAIN, CHV_BLOCKED) => Situation::PinLocked,
(OS_STATUS_DOMAIN, NO_SMARTCARD | REMOVED_CARD) | (TOKEN_DOMAIN, -7) => {
Situation::TokenAbsent
}
(OS_STATUS_DOMAIN, ITEM_NOT_FOUND) | (TOKEN_DOMAIN, -6) => Situation::CertificateNotFound,
_ => Situation::Unknown,
}
}

const OS_STATUS_DOMAIN: &str = "NSOSStatusErrorDomain";
const TOKEN_DOMAIN: &str = "CryptoTokenKit";
const WRONG_CHV: i32 = 0x8010_006B_u32 as i32;
const CHV_BLOCKED: i32 = 0x8010_006C_u32 as i32;
const NO_SMARTCARD: i32 = 0x8010_000C_u32 as i32;
const REMOVED_CARD: i32 = 0x8010_0069_u32 as i32;

fn refused(error: &CFError) -> TokenError {
let domain = error.domain().to_string();
let situation = situation_of(&domain, error.code() as i64);
let detail = if situation == Situation::PinEntryCancelled {
format!(
"has cancelado la petición de macOS ({domain} {})",
error.code()
)
} else {
format!(
"macOS no ha firmado ({domain} {}): {}",
error.code(),
error.description()
)
};
TokenError::new(situation, detail)
}

/// El algoritmo de Security.framework que firma un resumen ya calculado.
fn over_a_digest(algorithm: SignatureAlgorithm) -> Algorithm {
match algorithm {
SignatureAlgorithm::Sha1Rsa => Algorithm::RSASignatureDigestPKCS1v15SHA1,
SignatureAlgorithm::Sha256Rsa => Algorithm::RSASignatureDigestPKCS1v15SHA256,
SignatureAlgorithm::Sha384Rsa => Algorithm::RSASignatureDigestPKCS1v15SHA384,
SignatureAlgorithm::Sha512Rsa => Algorithm::RSASignatureDigestPKCS1v15SHA512,
SignatureAlgorithm::Sha256RsaPss => Algorithm::RSASignatureDigestPSSSHA256,
SignatureAlgorithm::Sha384RsaPss => Algorithm::RSASignatureDigestPSSSHA384,
SignatureAlgorithm::Sha512RsaPss => Algorithm::RSASignatureDigestPSSSHA512,
SignatureAlgorithm::Sha1Ecdsa => Algorithm::ECDSASignatureDigestX962SHA1,
SignatureAlgorithm::Sha256Ecdsa => Algorithm::ECDSASignatureDigestX962SHA256,
SignatureAlgorithm::Sha384Ecdsa => Algorithm::ECDSASignatureDigestX962SHA384,
SignatureAlgorithm::Sha512Ecdsa => Algorithm::ECDSASignatureDigestX962SHA512,
}
}

/// La identidad del llavero cuyo certificado tiene la huella de `reference`.
fn identity_of(reference: &CertificateRef) -> Result<(TokenCertificate, SecIdentity), TokenError> {
let not_there = || {
TokenError::new(
Situation::CertificateNotFound,
format!("el llavero ya no tiene {}", reference.label()),
)
};
let thumbprint = reference.cka_id().ok_or_else(not_there)?;
let store = reference.store();
let found = {
let _no_prompt =
SecKeychain::disable_user_interaction().map_err(|error| failure(&error))?;
found(ItemClass::identity(), &scope_of(&store)?)?
};
found
.into_iter()
.find_map(|reference| match reference {
Reference::Identity(identity) => {
let der = identity.certificate().ok()?.to_der();
let certificate = certificate_in(&store, &der, None);
(certificate.reference().cka_id() == Some(thumbprint))
.then_some((certificate, identity))
}
_ => None,
})
.ok_or_else(not_there)
}

/// Los llaveros donde se busca: ninguno en concreto es la lista de búsqueda de la persona usuaria.
fn scope_of(store: &Store) -> Result<Option<Vec<SecKeychain>>, TokenError> {
let named = store
Expand Down
Loading
Loading