rdnie: un driver en Rust para el DNIe que funciona igual en Linux, macOS y Windows

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::WrongPin y 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::InvalidPinFormat sin 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 Pin se 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

pcscd

libpcsclite-dev y pkg-config

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

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP