Firmar con el DNI electrónico desde una aplicación propia suele ser menos agradable de lo que parece. Entre la tarjeta y el programa hay capas (PC/SC, PKCS#11, OpenSC, drivers del sistema) y cada sistema operativo se comporta a su manera. rdnie, un proyecto de código abierto publicado en GitHub, intenta saltarse esas capas: es un driver escrito en Rust que habla directamente con el DNIe mediante APDU sobre PC/SC y que, según su documentación, se comporta igual en Linux, macOS y Windows.
De dónde sale: un problema con el DNIe en macOS
El origen es práctico. Su autor trabajaba en rFirma, una herramienta de firma electrónica, y al llevarla a macOS se encontró con que el DNIe no funcionaba bien. Según cuenta, el driver de tarjetas inteligentes que trae el sistema por defecto tiene un fallo: resetea la tarjeta a los pocos segundos (habla de unos 6) sin uso y la deja inutilizable. Con el driver CCID de Ludovic Rousseau el problema desaparece, pero pedir a los usuarios que cambien la configuración de su equipo no es una solución razonable.
La alternativa era usar el mismo driver que emplea AutoFirma, JMulticard, escrito en Java. Eso obligaba a transpilar otro módulo y a arrastrar más dependencias, justo lo contrario de lo que buscaba: que el código fuera nativo de principio a fin. De ahí salió la decisión de portar JMulticard a Rust.
Qué hace rdnie
El crate está pensado para el DNIe 3.0 y 4.0 y cubre lo imprescindible para firmar:
- Localiza el DNIe entre los lectores conectados a partir de su ATR.
- Abre el canal seguro CWA-14890.
- Lee los certificados y, a partir de ellos, el nombre, los apellidos y el número del titular.
- Consulta los intentos de PIN que quedan sin enviar el PIN.
- Firma un resumen SHA-256, SHA-384 o SHA-512 con PKCS#1 v1.5.
Quedan fuera, de forma explícita, el DNIe 1.0 y 2.0, NFC y PACE, el cambio de PIN, el descifrado y cualquier otra tarjeta. No necesita PKCS#11, OpenSC ni drivers del sistema.
Uso básico
Se añade como dependencia con cargo add rdnie. El flujo mínimo para firmar un resumen es este:
use rdnie::{DigestAlgorithm, PcscTransport, Pin, SIGNATURE_LABEL, Session};
use sha2::{Digest, Sha256};
fn main() -> Result<(), rdnie::Error> {
let mut session = Session::open(PcscTransport::connect_first_dnie()?)?;
let signing = session
.private_key(SIGNATURE_LABEL)?
.expect("a DNIe has a signature key");
let retries = session.pin_retries_left()?;
let pin = Pin::new(ask_holder_for_pin(retries));
let digest = Sha256::digest(b"document");
let signature = session.sign(&digest, DigestAlgorithm::Sha256, &signing, pin)?;
Ok(())
}
La firma no necesita leer ningún certificado: basta con la clave. Si la aplicación quiere el certificado X.509, session.certificate(label) lo lee la primera vez que se pide y lo conserva durante la sesión.
Cambios respecto a JMulticard
rdnie no es una copia literal. Su documentación incluye una tabla comparativa con las diferencias de comportamiento, y las más relevantes son estas:
Aspecto | JMulticard | rdnie |
Buscar el DNIe | Resetea cada lector hasta dar con uno que tenga un DNIe | Lee el ATR de cada lector sin resetear |
Abrir sesión | Resetea la tarjeta dos veces | No hay reset |
OpenSC o Firefox con la misma tarjeta | Pierden su estado | Siguen funcionando (comprobado con una tarjeta real) |
Lecturas al abrir | MF, CDF, todos los certificados y PrKDF | Solo el IDESP |
Firmar | Necesita el certificado para conocer la longitud del módulo | No lee ningún certificado |
Certificado corrupto | Rompe toda la carga | Solo falla la petición de ese certificado |
Canal perdido al firmar | Vuelve a firmar | Devuelve el error, sin reintentos |
El punto de los resets es el que más importa en la práctica. Si la librería no reinicia la tarjeta, no interrumpe a otros programas que estén usando el DNIe al mismo tiempo, algo que sí ocurre con JMulticard.
Rendimiento
Como solo lee lo que se le pide y nunca resetea la tarjeta, envía bastantes menos APDU. El repositorio publica una medición con rdnie 0.3.0, JMulticard 2.1 y OpenSC 0.27, sobre un único DNIe y lector, desde que se conecta hasta tener el certificado de firma:
Driver | Tiempo | APDU |
rdnie | 416 ms | 42 |
JMulticard | 1323 ms (3,2×) | 84 |
OpenSC | 1526 ms (3,7×) | 130 |
Conviene leerlo con cautela: es una medición con una sola tarjeta y un solo lector, y el método y sus limitaciones están descritos en docs/benchmark.md del repositorio. Si se leen todos los certificados, como hace JMulticard, rdnie tarda 830 ms.
El PIN: menos intentos desperdiciados
El DNIe bloquea el chip tras tres PIN incorrectos seguidos, así que cualquier intento malgastado cuenta. rdnie lo trata con bastante cuidado:
- Nunca se envía a ciegas. El PIN solo se manda si el programa consultó antes los intentos restantes en esa sesión; si no, devuelve
Error::PinRetriesNotRead. Antes de la verificación vuelve a leer los intentos y se detiene sin enviar nada si son 0 o si no coinciden con los esperados. - Un intento por llamada. Un PIN erróneo devuelve
Error::WrongPiny nada reintenta por su cuenta, ni siquiera tras un reset de la tarjeta. - Validación previa. Un PIN fuera del rango de 8 a 16 caracteres falla con
Error::InvalidPinFormatsin llegar a la tarjeta. Es decir, un PIN de 17 caracteres se rechaza por incumplir las reglas y no consume uno de los tres intentos. - Borrado de memoria. El tipo
Pinse pone a cero al destruirse, y ni el PIN ni la clave de sesión sobreviven a una operación.
Además, como se puede consultar cuántos intentos quedan antes de pedir el PIN, la aplicación puede avisar al usuario con exactitud. Con el enfoque anterior solo era posible advertir de que quedaba el último.
Plataformas y requisitos
Sistema | En ejecución | Para compilar |
Linux |
|
|
macOS | nada | nada |
Windows | nada | nada |
La versión mínima de Rust es la 1.87. La característica pcsc viene activada por defecto y aporta PcscTransport; con default-features = false el crate compila sin PC/SC y funciona sobre cualquier implementación del trait Transport, lo que facilita hacer pruebas sin hardware o usar otros lectores.
Licencia y estado del proyecto
rdnie se distribuye bajo EUPL-1.2, una licencia con copyleft, y contiene partes derivadas de JMulticard (el fichero NOTICE detalla cuáles). Es un proyecto joven, y conviene tenerlo presente: en el momento de escribir esto el repositorio tiene una estrella y la última versión citada es la 0.3.0. Antes de meterlo en un producto de firma en producción, lo sensato es revisar el código, probarlo con varias tarjetas y lectores y pasar por su política de seguridad (SECURITY.md) si se encuentra algún fallo.
Para quien desarrolla en Rust o necesita integrar el DNIe sin depender de Java ni de módulos del sistema, es una opción que merece la pena mirar: github.com/sgomez/rdnie.
Imagen: Pexels / REINER SCT
