Firmar desde tu sistema con el token USB: PKCS#11, el PIN y por qué no hay .pfx que exportar
El certificado llegó en un token USB y el equipo de desarrollo tiene una tarea sencilla: que el sistema firme los PDF de los contratos. Alguien abre el tutorial de siempre, que empieza con «exporta tu certificado a .pfx», y el programa del fabricante no ofrece esa opción. La pregunta que sigue es «¿cómo saco la clave privada?», y es la equivocada. Si el token marca esa clave privada como no extraíble, no sale, y lo que tu sistema puede hacer es pedirle que firme.
Este artículo explica el mecanismo con una sesión que ejecutamos nosotros y con lo que dice la norma peruana sobre el control de la clave privada. Qué formato elegir (PAdES, XAdES) está en la guía de integración; aquí vemos solo el tramo entre tu código y el dispositivo.
Qué es PKCS#11 y qué atributos retienen la clave privada
PKCS#11, que su texto llama Cryptoki, es la interfaz estándar para hablar con un dispositivo criptográfico. La mantiene OASIS. Citamos la versión 3.2, OASIS Standard del 3 de junio de 2026. En su sección 1.1 un token puede ser una tarjeta inteligente, un disco inteligente u otra tecnología, «including software-only», y una «Cryptoki library» es «a library that implements the functions specified in this standard». Esa biblioteca es el módulo del fabricante: un .so en Linux, una DLL en Windows. Tu aplicación no habla con el USB; carga el módulo y le llama funciones en C como C_Login o C_Sign.
La sección 4.10 describe los objetos de clave privada. CKA_SENSITIVE es «CK_TRUE if key is sensitive» y CKA_EXTRACTABLE es «CK_TRUE if key is extractable and can be wrapped». Y el párrafo que los ata: «If the CKA_SENSITIVE attribute is CK_TRUE, or if the CKA_EXTRACTABLE attribute is CK_FALSE, then certain attributes of the private key cannot be revealed in plaintext outside the token». Para una clave RSA (sección 6.1.3, tabla 38), esos atributos son el exponente privado y los primos, entre otros. Basta una de las dos condiciones.
La extracción es otra cosa. El C_WrapKey (sección 5.18.3) exige que la clave privada a sacar tenga CKA_EXTRACTABLE verdadero, y entonces sale envuelta, es decir cifrada con otra, no en claro. No es un .pfx que abre cualquiera con una contraseña; si el token no lo permite, no hay valor que entregar.
La sesión con pkcs11-tool
OpenSC incluye pkcs11-tool para probar un módulo sin escribir código. Su manual (0.27.1) define --module como la opción para cargar «a PKCS#11 module (or library)»; el binario del paquete de Fedora que usamos muestra /usr/lib64/opensc-pkcs11.so como valor por defecto.
Una advertencia: no tenemos a mano un token de una entidad peruana. Lo siguiente se ejecutó el 2 de octubre de 2026 contra SoftHSM 2.7.0, un token por software que implementa la misma interfaz, con un par RSA de 2048 bits y un certificado autofirmado de prueba. Sirve para ver las llamadas y las respuestas; el nombre del módulo, las ranuras y los intentos de PIN de un dispositivo físico cambian. Con tu token, sustituye la ruta por la que entregue el fabricante.
$ pkcs11-tool --module ./libsofthsm2.so -L (salida abreviada)
Slot 0 (0x25242c80): SoftHSM slot ID 0x25242c80
token label : TOKEN-PRUEBA
token flags : login required, rng, token initialized, PIN initialized
pin min/max : 4/255
$ pkcs11-tool --module ./libsofthsm2.so -M | grep -E "^ (RSA-PKCS,|SHA256-RSA-PKCS,)"
RSA-PKCS, keySize={512,16384}, encrypt, decrypt, sign, verify, wrap, unwrap
SHA256-RSA-PKCS, keySize={512,16384}, sign, verify
SHA256-RSA-PKCS es el mecanismo de la sección 6.1.14: firma RSA PKCS#1 v1.5 con SHA-256 como resumen. RSA-PKCS (sección 6.1.6) «does not compute a message digest or a DigestInfo encoding»: con él, el resumen lo preparas tú. El módulo devuelve la firma de los bytes que recibe; armar el PAdES o el XAdES alrededor es trabajo de tu código o de la biblioteca que uses.
El PIN y C_Login
Sin PIN, el token enseña el certificado y la clave pública, y nada más:
$ pkcs11-tool --module ./libsofthsm2.so -O (sin los bytes del módulo)
Certificate Object; type = X.509 cert
label: cert-firma
Public Key Object; RSA 2048 bits
label: firma-contratos
Usage: encrypt, verify, verifyRecover, wrap
No es un fallo de la herramienta. La sección 4.4 dice que cuando CKA_PRIVATE es verdadero, «a user may not access the object until the user has been authenticated to the token», y la búsqueda de objetos (sección 5.7.7) solo encuentra lo que la sesión puede ver. Con el PIN aparece la clave privada:
$ pkcs11-tool --module ./libsofthsm2.so --login --pin env:TOKEN_PIN -O --type privkey
Private Key Object; RSA 2048 bits
label: firma-contratos
Usage: decrypt, sign, signRecover, unwrap
Access: sensitive
El PIN entra por C_Login (sección 5.6.8), que recibe el tipo de usuario, el PIN y su longitud. pkcs11-tool usa CKU_USER salvo que pidas otro con --login-type. CKU_SO es el oficial de seguridad, el que inicializa el token y repone el PIN; no firma. Un PIN erróneo devuelve CKR_PIN_INCORRECT, y la lista de resultados de la función incluye CKR_PIN_LOCKED. Lo reprodujimos: tres PIN falsos seguidos cambian la bandera del token a user PIN count low. Cuántos intentos admite un dispositivo físico lo decide el fabricante, y no comprobamos ninguno.
La misma sección trae un caso para quien programa para usuarios finales. Si el token declara CKF_PROTECTED_AUTHENTICATION_PATH, el PIN se teclea en el propio dispositivo o en el lector, y se llama a C_Login con el puntero del PIN nulo. Tu código tiene que mirar esa bandera antes de abrir su propia ventana de PIN.
El manual de pkcs11-tool advierte de que otros usuarios del sistema pueden leer la línea de órdenes, o encontrar el PIN en un script, y ofrece env:VARIABLE. Por eso lo usamos arriba, aunque el PIN sigue en el entorno de quien lanzó el proceso. Ahora la firma y su verificación independiente con OpenSSL 3.5.8:
$ pkcs11-tool --module ./libsofthsm2.so --login --pin env:TOKEN_PIN \
--sign --id a1b2 -m SHA256-RSA-PKCS -i doc.txt -o doc.sig
Using signature algorithm SHA256-RSA-PKCS
$ openssl dgst -sha256 -verify pub.pem -signature doc.sig doc.txt
Verified OK
El token recibió los bytes del documento y devolvió solo la firma: 256 bytes para una clave RSA de 2048 bits. La lista de resultados de C_Sign (sección 5.13.2) incluye CKR_USER_NOT_LOGGED_IN para quien lo intente sin sesión.
Intentar sacar la clave privada: tres casos
Con un cliente de PyKCS11 conectado a SoftHSM pedimos con C_GetAttributeValue el exponente privado de tres objetos.
| Objeto | SENSITIVE | EXTRACTABLE | LOCAL | NEVER_EXTRACTABLE | Exponente privado |
|---|---|---|---|---|---|
| Importado desde un PEM | sí | no | no | no | no se devuelve |
| Generado dentro del token | sí | no | sí | sí | no se devuelve |
Generado con --extractable | sí | sí | sí | no | no se devuelve |
La sección 5.7.5 explica la última columna: si el atributo no se puede revelar por ser sensible o no extraíble, la longitud vuelve como CK_UNAVAILABLE_INFORMATION y la llamada responde CKR_ATTRIBUTE_SENSITIVE.
Lo que debería importarte al comprar son las dos primeras filas. La primera se creó fuera, en un archivo, y se importó: CKA_LOCAL y CKA_NEVER_EXTRACTABLE son falsos, así que el token no puede asegurar que esa clave privada no existió antes en un disco. La segunda nació dentro, y su historial sí se puede demostrar. La tabla 29 de la sección 4.10 define CKA_NEVER_EXTRACTABLE como «CK_TRUE if key has never had the CKA_EXTRACTABLE attribute set to CK_TRUE». Con tu token, pide pkcs11-tool --login -O --type privkey y lee la línea Access: de tu clave privada: si dice never extractable, la clave privada no se copió; si no lo dice, pregunta al emisor cómo se generó.
Lo que pide el reglamento
Leímos el D.S. 052-2008-PCM, aprobado el 18 de julio de 2008, y la segunda disposición complementaria modificatoria del D.S. 029-2021-PCM (El Peruano, 19 de febrero de 2021), que reescribió los artículos 6, 8, 10, 15, 16, 29, 33, 35, 36, 45, 47 y 48, entre otros. Del articulado, el D.S. 070-2011-PCM solo toca el 16. Tres textos importan:
- Artículo 7, literal d), no modificado: una característica mínima de la firma digital es que «su generación está bajo el control exclusivo del suscriptor».
- Artículo 10, literal c), texto de 2021: el suscriptor debe «mantener el control y la reserva de la clave privada bajo su responsabilidad», sin perjuicio de la responsabilidad del prestador de firma remota que la genere. El literal b) le permite generarla «por sí mismo» o autorizar su generación a distancia.
- Artículo 6, texto de 2021: la firma se crea por medios, «incluso a distancia», que garantizan que el firmante la mantiene bajo su control «con un elevado grado de confianza».
Ninguno nombra PKCS#11, tokens ni atributos de la clave privada, ni exige un dispositivo físico. Nuestra lectura, que no es una interpretación de INDECOPI: un CKA_NEVER_EXTRACTABLE verdadero es la evidencia técnica más directa de que el «control exclusivo» no se perdió por copia, y un .pfx que circuló por correo la debilita aunque nadie lo haya usado mal.
Cuando esto va a un servidor
El PIN es lo primero que se rompe. Un proceso desatendido que firma a cualquier hora lo necesita en una variable de entorno, un archivo o un gestor de secretos, y quien administra el servidor pasa a poder firmar. Es lo que el literal d) del artículo 7 busca evitar, según nuestra lectura. Además, el token tiene que seguir enchufado a esa máquina.
Eso ya está tratado y no lo repetimos:
- Token frente a firma remota, con costos y fricciones: Token USB o firma remota.
- La clave privada en un HSM y quién activa cada firma: Firma remota y HSM y tu propio HSM de nube.
- La llamada de tu sistema al prestador para firmar un hash: API de firma remota.
- El token en el equipo del usuario y no en tu servidor: firmar desde una aplicación web.
Para un sistema que firma solo, el certificado que corresponde es otro, el de agente automatizado. Las opciones están en certificados.
Lo que no afirmamos
No afirmamos qué modelo de token ni qué módulo entrega cada entidad peruana: no lo verificamos en fuente primaria. Tampoco cuántos intentos de PIN admite un dispositivo físico, porque la salida de arriba es de SoftHSM. No afirmamos que el D.S. 052-2008-PCM exija un dispositivo criptográfico, porque no lo dice, ni que INDECOPI use CKA_NEVER_EXTRACTABLE como criterio. Y un token por software no tiene la resistencia física de uno de hardware: los ataques al dispositivo quedan fuera de lo que mostramos.
Fuentes
- PKCS #11 Specification Version 3.2, OASIS Standard, 3 de junio de 2026. Secciones 1.1, 4.4, 4.10, 5.6.8, 5.7.5, 5.7.7, 5.13.2, 5.18.3, 6.1.3, 6.1.6 y 6.1.14.
- pkcs11-tool, manual de OpenSC 0.27.1.
- D.S. 052-2008-PCM, artículo 7 d).
- D.S. 029-2021-PCM, artículos 6, 8 y 10 del D.S. 052-2008-PCM en su texto vigente.
- D.S. 070-2011-PCM, El Peruano, 27 de julio de 2011.
- Prueba propia del 2 de octubre de 2026: SoftHSM 2.7.0, OpenSC 0.27.1, OpenSSL 3.5.8 y PyKCS11.
Última revisión: 2 de octubre de 2026.