Skip to content

El DNIe en macOS, con rdnie y los intentos restantes - #2086

Merged
sgomez merged 26 commits into
mainfrom
agent/developer/spec-2069
Oct 21, 2026
Merged

sgomez merged 26 commits into
mainfrom
agent/developer/spec-2069

Conversation

@sgomez

@sgomez sgomez commented Oct 21, 2026

Copy link
Copy Markdown
Owner

Delivers spec #2069.

Closes #2069
Closes #2070
Closes #2071
Closes #2072
Closes #2073
Closes #2074
Closes #2075
Closes #2076
Closes #2077

sgomez added 25 commits October 21, 2026 18:56
Implements #2070: Los intentos restantes exactos en el aviso del PIN
- PinWarning::AttemptsLeft(n): el diálogo y la terminal dicen cuántos quedan; con 1 o 2, resaltado
- Sin intentos no hay diálogo: prompted_until_accepted termina como PIN bloqueado
- TokenError lleva los intentos exactos (with_attempts_left) y attempts_left los da
feat(identity): Los intentos restantes exactos en el aviso del PIN
Implements #2071: El DNIe aparece en la lista en macOS, leído con rdnie
- DnieToken, adaptador del puerto Token sobre rdnie con un CardOpener inyectable:
  PcscCard en producción, rdnie-emulator en las pruebas.
- MacosToken reparte en tres: llavero, almacén del DNIe (dnie:pcsc, clase tarjeta) y RealToken.
- En macOS se vigilan los lectores; sin DNIe legible el almacén no enseña nada.
- El DNIe aún no firma: offers responde mecanismo no ofrecido hasta #2075.
feat(identity): el DNIe aparece en la lista en macOS, leído con rdnie
… la DGP

Implements #2072: La línea ilegible de macOS ya no remite a OpenSC ni a la DGP
- macOS enseña solo el mensaje genérico; Linux y Windows conservan su ayuda
feat(identity): la línea ilegible de macOS ya no remite a OpenSC ni a la DGP
…ódulo PKCS#11

Implements #2073: El DNIe visto también por un módulo PKCS#11 firma por rdnie
- Store::is_the_dnie sustituye a dnie::is_the_dnie
feat(identity): la fusión de copias prefiere la del DNIe a la de un módulo PKCS#11
Implements #2075: El DNIe firma en macOS con los intentos restantes
- DnieToken firma RSA PKCS#1 v1.5 con SHA-256/384/512 por rdnie; offers rechaza SHA-1 y PSS
- pin_warning da los intentos exactos y abre la tarjeta solo para leerlos
- Si los intentos cambian entre el aviso y el envío, el PIN no se envía
- Los errores de rdnie pasan a PIN incorrecto, bloqueado o tarjeta retirada
- El login compartido de un lote se anida y su primer fallo corta las firmas que quedan
Implements #2074: --debug-info y plantilla de issue del DNIe en macOS
- sección DNIe con ATR, versión, juego de claves y versión de rdnie, sin datos del titular
- la versión de rdnie sale de Cargo.lock en build.rs
La copia que la fusión prefiere desde #2073 firma por el adaptador de rdnie con sus intentos.
feat(identity): el DNIe firma en macOS con los intentos restantes
…11_MODULE

Implements #2077: El manual y la web: el DNIe en macOS y `RFIRMA_PKCS11_MODULE`
- la tabla comparativa dice que rFirma lee el DNIe también en macOS, en los cinco idiomas
docs(web): El manual y la web: el DNIe en macOS y RFIRMA_PKCS11_MODULE
… compartido

Implements #2076: Un lote con el DNIe pide el PIN una vez
- el PIN aceptado se guarda en memoria protegida solo dentro de un login compartido
- se borra al cortarse el lote o al cerrar el último nivel
…agent/developer/fix-pr-2083

# Conflicts:
#	rfirma-app/src-tauri/src/identity/adapters/macos_store/dnie.rs
#	rfirma-app/src-tauri/src/identity/adapters/macos_store/dnie/tests.rs
feat(identity): Un lote con el DNIe pide el PIN una vez
feat(identity): --debug-info y plantilla de issue del DNIe en macOS

@sgomez sgomez left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NEEDS_FIXES: un [blocking] y tres [fix].

Revisión de alcance completo (no había revisiones previas) de 68b3d765, contra la spec #2069 y sus sub-issues #2070 a #2077.

Checks: el CI seguía en marcha en 68b3d765 cuando revisé (Imagen nativa, Plataformas / macOS, Plataformas / Windows, Cadena TypeScript, Cadena Java, Cadena Rust, Landing, y Analyze en sus cinco variantes). No he corrido la suite en local; la puerta de checks los espera.

Combinaciones que comparten mecanismo (prompted_until_accepted, el login compartido y el estado Login de DnieToken). Las comprobé con pruebas desechables contra rdnie-emulator, ya borradas y sin commit:

  • Lote o firma contra el servidor trifásico, con un PIN incorrecto: falla, ver el [blocking].
  • Ciclo del panel, con su in_one_login en cada intento, y un PIN incorrecto: funciona. El corte se borra al cerrar cada ciclo y el reintento llega a la tarjeta con los intentos releídos.
  • Cero intentos en el bucle compartido (panel, lote, terminal): funciona. No hay diálogo y termina en pinLocked.

[blocking]

  1. dnie.rs:296: en un lote, o en la firma contra el servidor trifásico, un PIN incorrecto no se vuelve a pedir. accepts_the_secret no verifica el PIN, así que el error salta en la primera firma, fuera del bucle del diálogo (historia 10, ADR-0047 regla 5).

[fix]

  1. secret.rs:112: el resaltado con uno o dos intentos solo existe en GTK, y el DNIe por rdnie solo existe en macOS, cuyo diálogo de AppKit no resalta nada (historia 8).
  2. dnie.rs:59: Login.pin es una copia muerta del PIN, y sus dos pruebas miran estado interno.
  3. dnie.rs:165: --debug-info dice «no hay DNIe» cuando hay un DNIe que rdnie no abre, que es justo el caso que hay que reportar.

[note]

  • ChosenCopy::among antepone la copia recordada a la del DNIe. Si alguien ya firmaba con el DNIe por RFIRMA_PKCS11_MODULE y tiene recordada esa copia, seguirá firmando por PKCS#11, sin los intentos restantes, hasta que elija otra cosa.
  • seen_dnie reinicia la tarjeta para leer el ATR. Un --debug-info lanzado mientras la app firma hace que esa firma acabe como tarjeta retirada.

Lo demás encaja con la spec: ID-557 (dependencias y versiones fijas), ID-558 (offers rechaza SHA-1 y PSS antes del PIN), ID-560 (MacosToken reparte en tres), ID-561 (TD-150), ID-563 (traducción de errores, con faults), ID-565 y #2072 (vigilancia y línea del lector, sin la ayuda de macOS), ID-566, ID-567 (campos sin datos del titular), ID-568 (plantilla), ID-569 (manual y comparativa en los cinco idiomas) y TD-152 (los condicionales siguen en identity/adapters/mod.rs).

if !has_a_length_the_dnie_admits(secret) {
return Err(token_error(&rdnie::Error::InvalidPinFormat));
}
let left = self.attempts_left()?;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blocking] En un lote, local o remoto, y en la firma contra el servidor trifásico de una sede, un PIN mal tecleado no se vuelve a pedir: el trámite entero termina con incorrectPin.

Los tres caminos pasan por batch_signed_with_one_secret → secret_for_the_batch, cuyo bucle solo reintenta lo que rechaza accepts_the_secret. Con PKCS#11 ese paso hace el C_Login, y un PIN incorrecto vuelve al diálogo con el aviso releído. Aquí accepts_the_secret solo mira la longitud y los intentos, sin VERIFY, así que acepta cualquier PIN de 8 a 16 caracteres. El fallo llega en la primera sign_with_secret, fuera del bucle, y corta el lote. Incumple la historia 10 de #2069 («me vuelva a pedir el PIN con los intentos que quedan ahora») y la regla 5 del ADR-0047 en esos caminos: la sede se queda sin firma por una errata.

Lo he reproducido con una prueba desechable (ya borrada) contra el emulador, en 3.0 y 4.0: batch_signed_with_one_secret(&dnie, …) con el tecleo [a_wrong_pin(), the_pin()] devuelve Err(Failure { situation: "incorrectPin", attempts_left: Some(2) }) tras un solo diálogo y un PIN enviado. Con PKCS#11 serían dos diálogos y la firma.

rdnie 0.1.1 no tiene un VERIFY sin firma (Session solo expone pin_retries y sign). Hay dos salidas: añadir a rdnie una verificación del PIN y usarla aquí, o que el lote vuelva al diálogo cuando la primera firma falla con IncorrectPin antes de haber firmado nada. Falta una prueba por batch_signed_with_one_secret con el emulador: un PIN incorrecto y luego el correcto deben dar dos diálogos, el segundo con AttemptsLeft(2), y el lote firmado. Hoy a_batch_stops_at_the_first_failure_without_spending_more_attempts fija lo contrario y habrá que ajustarla.

}

/// Si el aviso se resalta: el último intento, o quedan uno o dos.
pub fn stands_out(self) -> bool {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[fix] Solo el diálogo GTK usa stands_out, y AttemptsLeft solo sale de rdnie, que solo existe en macOS. El diálogo de macOS (gtk_prompter/appkit.rs, texts_of) mete el aviso en el informativeText sin resaltarlo. Así, con uno o dos intentos el aviso no destaca en el único diálogo que llega a enseñarlo: la historia 8 de #2069 y el criterio «con 1 o 2, resaltado» de #2070 no le llegan a nadie. (No lo marco como blocking porque ID-562 dice «como hoy el último intento», y en macOS el último intento tampoco se resalta hoy.) Haz que el diálogo de AppKit resalte el aviso cuando request.pin_warning.stands_out(), por ejemplo con una etiqueta en negrita junto al campo seguro, y pruébalo en las pruebas de texts_of, sin AppKit.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corregido en f67052e. texts_of saca el aviso a AlertTexts.highlighted cuando stands_out(), y la alerta de AppKit lo muestra en negrita encima del campo seguro, dentro de una vista accesoria. Si el aviso no destaca, sigue en el informativeText. Esto también cubre el último intento (FinalTry), como ya hace GTK. Pruebas en texts_of sin AppKit: con 1 y 2 intentos el aviso destaca, con 3 no; además he ajustado la del último intento. He añadido las features NSFont, NSLayoutConstraint y objc2-core-foundation de objc2-app-kit, sin cambios en Cargo.lock.

shown_attempts: Option<u8>,
depth: usize,
cut: Option<TokenError>,
pin: Option<ProtectedSecret>,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[fix] Login.pin se escribe en keep_while_shared y solo lo lee holds_a_pin, que es #[cfg(test)]. Cada firma del lote usa el secreto que le pasa batch_signed_with_one_secret, y ese ya es el «secreto aceptado en memoria protegida» de ID-564. Esto es una segunda copia del PIN en memoria que no sirve para nada. Además, las dos pruebas que la miran (the_accepted_pin_is_kept_while_the_shared_login_lasts_and_wiped_when_it_closes y a_pin_accepted_outside_a_shared_login_is_not_kept) comprueban estado interno, en contra de TD-145. Quita el campo pin, keep_while_shared, holds_a_pin y esas dos pruebas.


/// El DNIe que abre `opener`; `None` si no hay ninguno legible.
pub fn seen_dnie<O: CardOpener>(opener: &O) -> Option<SeenDnie> {
let session = opener.open().ok()?;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[fix] seen_dnie devuelve None con cualquier error de open(), y el informe lo escribe como «tarjeta: no hay DNIe». Pero esta sección existe para un caso concreto (la historia 24, y la nota de #2069 sobre el DNIe 4.0 y el chip 19WL34 sin probar): un DNIe que rdnie no consigue abrir. Justo ahí el informe dice que no hay DNIe. Escribe «no hay DNIe» solo con NoReader, NoCard y NotDnie. Para cualquier otro error, escribe el error de rdnie, después de comprobar que su texto no lleva datos del titular. Añade una prueba en stores_tests.rs y otra con el emulador fallando en Step::Opening.

El lote comprueba el PIN del DNIe antes de firmar, el diálogo de AppKit resalta el aviso con uno o dos intentos, DnieToken no guarda una segunda copia del PIN y --debug-info da el error de rdnie de un DNIe que no abre.

@sgomez sgomez left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLEAN: ningún [blocking] ni [fix]; dos [note].

Revisión incremental desde 68b3d765 (la revisión anterior) hasta f67052e7, contra la spec #2069 y sus sub-issues #2070 a #2077.

Checks: el CI seguía en marcha en f67052e7 cuando revisé (Imagen nativa, Plataformas / macOS, Plataformas / Windows, Cadena TypeScript, Cadena Rust y Analyze (rust)). No he corrido la suite; la puerta de checks los espera. Sí he corrido en macOS las pruebas del adaptador del DNIe (macos_store::dnie) con las desechables de abajo: todas en verde.

Hallazgos anteriores, comprobados en el código:

  1. [blocking] PIN incorrecto en lote o trifásico: corregido. accepts_the_secret comprueba ahora el PIN con una firma desechada, dentro del bucle del diálogo. a_batch_asks_again_after_a_wrong_pin_with_the_attempts_left_and_signs_with_the_right_one pasa por batch_signed_with_one_secret y habría fallado con el código anterior.
  2. [fix] Resaltado en AppKit: corregido. texts_of saca a highlighted el aviso que stands_out(), la alerta lo pone en negrita sobre el campo y hay pruebas con 1, 2 y 3 intentos y con el último intento.
  3. [fix] Login.pin: corregido. Se han quitado el campo, keep_while_shared, holds_a_pin y sus dos pruebas.
  4. [fix] --debug-info con un DNIe que rdnie no abre: corregido. DnieInTheReader::Unopened lleva el error de rdnie, cuyos textos (rdnie::Error) no tienen datos del titular. Hay una prueba en stores_tests.rs y otra con el emulador en Step::Opening. LegacyDnie y BurnedDnie también acaban en Unopened, que es lo que interesa.

Combinaciones sobre el mecanismo que ha cambiado (accepts_the_secret y shown_attempts.take(), que solo usa el lote). Las comprobé con pruebas desechables contra rdnie-emulator, en 3.0 y 4.0, ya borradas y sin commit:

  • Lote con un intento y un PIN incorrecto: termina en pinLocked tras un solo diálogo y un PIN enviado. Funciona.
  • Lote con dos PIN incorrectos y luego el bueno: tres diálogos (3, 2 y 1, con reintento) y el lote firmado. Funciona.
  • Lote con cero intentos: sin diálogo y sin PIN enviado, pinLocked. Funciona.
  • Tarjeta retirada al firmar: la comprobación termina como tokenAbsent y no vuelve a pedir el PIN. Funciona.
  • Ciclo del panel después de un lote con un fallo: el aviso se relee (3 tras el PIN bueno) y firma. No queda ningún shown_attempts viejo. Funciona.

[note]

  • dnie.rs:88: cada lote o firma trifásica con el DNIe hace ahora una firma de más con la clave de firma, sobre una constante y con su propio canal seguro. Es el precio de no tener un VERIFY suelto en rdnie 0.1.1. Cuando rdnie lo exponga, conviene cambiar a él. El comentario cita el ADR-0047, pero el ADR no recoge esta decisión.
  • Siguen en pie las dos notas anteriores (la copia recordada por delante del DNIe y el reinicio de seen_dnie en plena firma). No las ha tocado nada de este pase.

@sgomez
sgomez marked this pull request as ready for review October 21, 2026 19:19
@sgomez
sgomez merged commit fbf00e5 into main Oct 21, 2026
15 checks passed
@sgomez
sgomez deleted the agent/developer/spec-2069 branch October 21, 2026 19:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment