Sep 08 2026

Resucitar un HSM de un fabricante desaparecido: manejar un SPYRUS LYNKS Series II desde Linux, byte a byte

Tag: advanced,internals/hackingadmin @ 6:47 pm

Un módulo de seguridad hardware FIPS 140-2 de 2005. Sin driver. Sin middleware. Sin PIN. Un fabricante que ya no existe. Esta es la historia completa de cómo conseguí que generase una clave y firmase un mensaje dentro del chip desde una máquina Linux x86-64 moderna, contada al nivel al que realmente trabajé: descriptores USB, cabeceras ELF, desensamblado, bloques de comando big-endian, bytes DER y aritmética modular. Al final hay un pequeño proyecto de código abierto, hs4l, para que nadie tenga que repetir el viaje.

Dispositivo: SPYRUS LYNKS Series II, USB 08df:0a00, referencia 3003-F0 (2005), sistema operativo SPYCOS
Validación: FIPS 140-2 nivel 2, certificado NIST CMVP #679
Host: Fedora x86-64, kernel 7.1, OpenSSL 3.5, qemu-user ARM de 32 bits

Contenido

  1. El objeto sobre la mesa
  2. Todas las puertas cerradas
  3. El movimiento lateral: ¿quién más habla con este chip?
  4. Leer el firmware
  5. Código ARM, anfitrión x86-64, USB real
  6. Primer contacto, en el cable
  7. Apropiarse de la tarjeta: reinicialización
  8. El muro: 0x0a Execution Failure
  9. La causa raíz real, demostrada de tres maneras
  10. La criptografía, byte a byte
  11. Extra: una compilación nativa x86-64, con su propio bug
  12. Cómo lo probé
  13. Por qué construí hs4l y qué contiene
  14. Descargas y firmas

1. El objeto sobre la mesa

«RA», referencia 3003-F0. Dentro hay un módulo criptográfico del linaje Fortezza que ejecuta SPYCOS, el SPYRUS Cryptographic Operating System, validado a FIPS 140-2 nivel 2. Dos roles lo custodian: un Site Security Officer (el oficial criptográfico) y un User. Cada uno tiene una frase PIN de al menos doce caracteres de un alfabeto de 93 símbolos. Diez inicios de sesión SSO fallidos consecutivos lo ponen a cero.

No quería lo que hubiera dentro. Quería hacerlo mío: borrarlo, fijar mis propios PIN, meterle una clave y hacer que firmase. Es una función documentada del módulo. Resultó ser la parte fácil. La parte difícil era que en 2026 no hay software en la Tierra que hable con él, o eso parecía.

Hay que leer ese descriptor como lo lee el kernel. USB 1.1, full speed, una interfaz, clase 0. No es CCID (clase 0x0b), no es HID, no es almacenamiento masivo. Dos endpoints bulk, uno en cada dirección, paquetes de 64 bytes. Sin cadena de número de serie. Es una tubería cruda del fabricante: el dispositivo hará exactamente lo que le diga un programa que conozca su protocolo privado, y nada más.

Ningún driver del kernel reclama la interfaz 0. Nada en la tabla de alias de módulos conoce la cadena «spyrus». Conviene recordar esa ausencia de driver: parece una mala noticia y en realidad es el único hecho que hizo posible todo lo demás.

2. Todas las puertas cerradas

Puerta 1: el fabricante

SPYRUS Inc. fue absorbida por otra empresa el 15 de septiembre de 2021. El portal de desarrolladores ha desaparecido a nivel de DNS:

Lo que responde hoy en el dominio antiguo es una página aparcada detrás de un CDN de hosting compartido. La empresa sucesora tiene un formulario de contacto. Lo rellené. Nada.

Puerta 2: el middleware de Windows

El token se gestionaba con una aplicación de Windows llamada En-Sign, con un SDK SPEX+ que proporcionaba un CSP y un módulo PKCS#11. Todo se distribuía detrás de un muro de registro. La Wayback Machine nunca rastreó más allá de ese muro. archive.org/details/Spyrus contiene un GIF con el logotipo. El único artefacto que sobrevivió en la web pública es SCARDLYNKSUSBW.sys con su .inf y su .cat, firmado WHQL en 2005, todavía en el Microsoft Update Catalog y en sitios agregadores de drivers como un zip de 20 KB. Lo desmonté. Es un shim de lector: hace que Windows presente el token como un lector de tarjetas inteligentes para que otro software pueda manejarlo. Por sí solo no hace nada. Una máquina virtual con Windows 7 y paso USB de 08df:0a00 llegó exactamente hasta «lector presente, ningún software instalado que entienda la tarjeta».

Puerta 3: Linux

El mundo PC/SC no puede verlo, porque no es un lector CCID:

Nunca ha existido un driver Linux para el Series II. Nunca ha existido un driver macOS. No hay ningún módulo PKCS#11 para él fuera del SDK perdido.

Puerta 4: el PIN

Doce caracteres como mínimo de 93 símbolos: 9312 ≈ 4,2 × 1023. Diez intentos SSO fallidos ponen la tarjeta a cero. Incluso con un protocolo en la mano, la fuerza bruta no es un plan; es una forma de convertir el token en un pisapapeles de diez en diez intentos. La frase «Zeroize Default PIN» de fábrica que permite recuperar una tarjeta puesta a cero está en una guía de administrador propietaria que no pude encontrar.

Puerta 5: sondeo a ciegas

Con libusb se puede reclamar una interfaz sin driver y escribir bytes arbitrarios en EP1. Lo intenté. La palabra de estado con la que se responde a cada comando empieza por un byte que parecía 0x80, y más allá de eso no tenía ni formato de trama, ni lista de opcodes, ni regla de checkword. Cada intento devolvía una respuesta corta que no sabía interpretar. Hacer fuzzing a ciegas a un módulo FIPS con contador de intentos fallidos no es una afición que recomiende.

Puerta 6: la gente

Pregunté en dos foros. La respuesta más útil fue comprensiva y correcta: «el software es vapor».

Semanas después, el balance: un dispositivo que Linux puede enumerar pero con el que no puede hablar, un fabricante que no existe, un middleware que nunca se archivó, un espacio de PIN mayor que el número de estrellas del universo observable, y un contador de intentos fallidos. Todas las puertas que se me habían ocurrido estaban cerradas.

3. El movimiento lateral: ¿quién más habla con este chip?

La pregunta que había estado haciendo era «¿dónde está el software del fabricante?». La pregunta correcta era otra: ¿quién más, en cualquier sitio, en cualquier sistema operativo, distribuye código que hable SPYCOS?

Un token DSA validado FIPS de mediados de los 2000 existe por una razón: firmar flujos de datos cuya autenticidad tiene peso legal. El único ámbito que se me ocurría donde eso es obligatorio y público es la vigilancia del tratado de prohibición de ensayos nucleares. Las estaciones sísmicas que alimentan el Sistema Internacional de Vigilancia firman sus datos en el formato CD1.1, con claves hardware, y los digitalizadores que lo hacen los construyen un puñado de fabricantes de sismógrafos. Uno de ellos documenta, en un manual de usuario del firmware de su digitalizador Platinum, una herramienta de línea de comandos llamada spyrus_util que inicializa una tarjeta «Spyrus Lynks» y firma con ella. Sus digitalizadores ejecutan Linux embebido sobre ARM.

Si esa herramienta existía como binario, el protocolo vendría con ella. Y existía, en un espejo rsync público y anónimo que sirve sistemas de ficheros raíz completos del firmware del digitalizador:

«ctbto-prerelease» es la Organización del Tratado de Prohibición Completa de los Ensayos Nucleares. La corazonada era correcta. Se descarga el sistema de ficheros raíz ARM, o solo las piezas que necesita la herramienta:

Obsérvense las dos bibliotecas que viven en lib/ en lugar de en usr/lib/. Que faltasen me costó una hora más adelante; el cargador dinámico solo las reporta como ausentes después de haber encontrado todo lo demás.

4. Leer el firmware

Antes de ejecutar nada: ¿qué es este binario, para qué CPU, y cómo llega al hardware?

ARM de 32 bits little-endian, EABI versión 5, compilado para el juego de instrucciones ARMv4T, de modo que corre en un núcleo de la clase ARM9TDMI. La ruta del módulo del kernel en el listado, 2.6.36-cm-x270, dice que el digitalizador es un módulo CompuLab CM-X270, un Intel PXA270 XScale. El binario se construyó con GCC 4.4.4 contra glibc 2.12.2 y no está stripped, con información de depuración DWARF. Ese último detalle vale cien horas de conjeturas:

Ahora las dependencias, que dicen cómo llega un programa al hardware:

Esta es la observación decisiva de todo el proyecto. libspyrus habla con el token a través de libusb-1.0: enumerar dispositivos, abrir, reclamar la interfaz 0, transferencias bulk. Ni módulo del kernel, ni /dev/spyrus0 (eso solo existe para la variante PCMCIA a través de spyrus_cs.ko). La cadena es spyrus_util → libspyrus.so.3 → libusb-1.0 → ioctls USBDEVFS → EP1/EP2, enteramente en espacio de usuario. Y una interfaz sin driver en mi anfitrión es exactamente lo que libusb quiere: nada que desvincular, nada contra lo que pelear. El kernel del sismógrafo y mi kernel presentan la misma interfaz usbfs. El token no puede distinguir entre un PXA270 en una cámara acorazada y un portátil en Helsinki.

Las cadenas de la biblioteca me dieron el vocabulario de errores que necesitaría más tarde:

Y los propios ficheros de integración del firmware explican cómo usa la herramienta el digitalizador, que es la documentación más barata posible:

Conviene tener presente esa línea openssl dsaparam -out ... 1024. Es la semilla del único bug real de esta historia, y el OpenSSL del firmware es 1.0.2j:

Por último, el mismo paquete distribuye cabeceras GPL-2 en el módulo de bibliotecas cruzadas del espejo. Son lo más parecido a una especificación del protocolo que existe en público. Los opcodes son doce bits más un indicador de conjunto de comandos de ocho bits:

Obsérvese cómo funciona una comprobación de PIN: el anfitrión envía la frase y un desafío de 20 bytes, y la tarjeta responde con un par (r, s). El inicio de sesión es una firma DSA sobre un desafío. Por eso nada relativo al PIN puede cortocircuitarse desde fuera.

5. Código ARM, anfitrión x86-64, USB real

El binario es ARMv4T. Mi máquina es x86-64. qemu-arm-static es un emulador en modo usuario: traduce las instrucciones del invitado y pasa las llamadas al sistema del invitado al kernel anfitrión. Un ioctl(USBDEVFS_BULK) emitido por la libusb ARM aterriza en mi nodo de dispositivo real. Sin máquina virtual, sin controlador USB virtual, sin demonio. Tres detalles de entorno hacen que funcione:

La herramienta necesita escribir en tres sitios del anfitrión: el nodo de dispositivo USB, un fichero de bloqueo y un directorio de configuración. La biblioteca toma un bloqueo de uso exclusivo (la cabecera lo llama «the interprocess right to use the card») y lee su configuración, así que antes de nada:

Una versión permanente de esto (un grupo spyrus, una regla udev con TAG+="uaccess", una entrada tmpfiles.d porque /var/lock es un tmpfs) es lo que hace el script de configuración del proyecto; aquí muestro las acciones en crudo.

6. Primer contacto, en el cable

Primero solo lectura. --state pide la palabra de estado y nada más. Dos banderas -D hacen que la biblioteca vuelque en hexadecimal cada transacción:

La tarjeta respondió. Una palabra de estado de 9 bytes con el bit alto del byte 0 activado, y dos registros que la biblioteca llama SR y PRR. SR = 25 significaba «inicializada, en uso, nada cargado». --status es el primer bloque de comando real, y aquí está todo el formato de trama del protocolo en un solo viaje de ida y vuelta:

Todo es una secuencia de palabras big-endian de 32 bits, sobre un ARM little-endian, sobre un anfitrión x86 little-endian; la biblioteca intercambia los bytes a la salida. La cabecera son seis palabras:

palabra Get_Status enviado significado (leído directamente del cable)
W0 00 00 00 26 opcode; la respuesta pone el byte alto a 0x90 y devuelve el resto tal cual
W1 00 00 00 00 reservado
W2 00 00 00 00 desplazamiento de la carga útil de entrada (0 cuando no hay; 0x18 en caso contrario)
W3 00 00 00 18 longitud total del bloque (24 = solo cabecera)
W4 00 00 00 00 código de resultado, rellenado por la tarjeta
W5 00 00 00 00 reservado

Tras la cabecera, la respuesta lleva struct spyrus_status: longitud 0x34, después el número de serie de 8 bytes 01 00 00 00 f0 00 18 4f (la etiqueta del plástico termina en 00184F), estado 7, modos, personalidad 1, 20 ranuras de clave con sus banderas, 50 ranuras de certificado. El comando de lista de personalidades muestra las etiquetas de las ranuras como 20 cadenas fijas de 32 bytes:

(Ese listado es de hoy, después de cargar una clave; en el primer contacto todas las ranuras decían Empty.) Una ranura de certificado vacía responde a un Get_Certificate con texto literal:

Una ranura de certificado ocupa 0x800 bytes y contiene texto PEM, con finales de línea CRLF, rellenado con ceros. Esto importará en la sección 11.

7. Apropiarse de la tarjeta: reinicialización

Nunca supe el PIN SSO anterior y nunca lo necesité. La inicialización es una función documentada de SPYCOS que destruye todas las claves y certificados y fija PIN nuevos. El propio script del fabricante hace exactamente esto en un digitalizador nuevo. Dos banderas importan: --loose selecciona el modo que no exige un certificado en cada ranura, y la confirmación se lee del terminal de control, no de stdin, así que canalizar yes por una tubería solo funciona cuando no hay tty en absoluto.

SR pasó de 25 a 26. La tarjeta era mía, con PIN que elegí yo (se cambian después con --change --old-pin --pin; la biblioteca impone un mínimo de 4 caracteres, la regla propia de la tarjeta es 12). Nada de esto elude el modelo de seguridad del módulo: es el camino que sigue un administrador legítimo el primer día, destruye lo que hubiera, y no toca el motor de PIN.

8. El muro: 0x0a Execution Failure

Entonces intenté generar una clave como lo haría en cualquier máquina moderna: crear parámetros DSA con OpenSSL y entregárselos a la tarjeta.

Decodifiquemos la petición. W0 = 0x85 Generate_X. W3 = 0x144 = 324 bytes en total. Después la carga útil de struct spyrus_generate_x_in: longitud 0x12c = 300, índice 1, tipo 0x0a = DSA, y tres bloques cada uno introducido por una palabra de longitud en bits: P 0x400 = 1024 bits, Q 0xa0 = 160 bits, G 0x400. La tarjeta devolvió el bloque entero y puso W4 = 0x0000000a. El viaje de ida y vuelta se completó. La trama se aceptó. La tarjeta entendió la petición, lo intentó y falló.

Diez es un número concreto. Leamos la tabla de errores del binario; la función es un switch sobre el código, y el compilador emitió una tabla de saltos, que se ve en ambas arquitecturas:

Recorrer la tabla da la correspondencia, y 0x0a cae en una cadena muy concreta:

código cadena código cadena
0x00 Passed 0x07 Invalid Data Size
0x01 Spyrus command failed 0x08 Invalid Header
0x02 Checkword Failure 0x09 Invalid State
0x03 Invalid Type Value 0x0a Execution Failure
0x04 Invalid Mode Value 0x0b No Key Loaded
0x05 Invalid Key Index 0x15 NO PQG Loaded
0x06 Invalid Cert Index    

No es Invalid Header, así que la trama era correcta. No es Invalid State, así que la tarjeta estaba lista. No es NO PQG Loaded, así que los parámetros estaban presentes. No es Invalid Data Size. La tarjeta ejecutó la operación y abortó dentro de ella. Eso reduce la búsqueda al contenido de los parámetros.

Dos pistas falsas, eliminadas en el cable y no por intuición:

  • «Necesita iniciar sesión primero.» Añadir --pin mandó la herramienta por una ruta de validación de ranura que falla en una tarjeta vacía («Zero length certificate»), y la traza no mostró ningún intercambio Check_Pin_Phrase en ningún caso. El script del fabricante no pasa ningún PIN al keygen. Descartada.
  • «Estado de sesión tras el init.» Reinicialicé y generé seguido, en el orden exacto del script. El mismo 0x0a. Descartada.

La única variable que quedaba eran los propios parámetros, y lo primero que hizo que la tarjeta superase el muro fue dejar de suministrarlos:

Sin --dsaparam la biblioteca llama a DSA_generate_parameters del OpenSSL 1.0.2 incluido (de ahí la línea de progreso con puntos y el informe de counter/h de la búsqueda de primos FIPS 186-2), envía el resultado, y la tarjeta acepta. Así que la tarjeta estaba bien, el transporte estaba bien, y el problema eran mis parámetros. Tenía un HSM que funcionaba y una teoría equivocada: supuse que el silicio de 2005 quería la semilla y el contador de generación FIPS 186-2 para validar la procedencia, cosa que el OpenSSL moderno no emite. Esa teoría era plausible, coherente con las pruebas que tenía, y errónea. La sección 9 es la corrección.

9. La causa raíz real, demostrada de tres maneras

Miremos otra vez la petición rechazada, en el bloque Q. La palabra de longitud dice 0x000000a0, 160 bits, y los bytes que siguen son 74 ae 65 dd e8 35 23 83 80 dc 2a f0 34 28 23 c3 f7 2a b9. Ahora miremos lo que OpenSSL cree que contiene el mismo fichero:

Q es FF6B3DECA626D5EF52 74AE65DD...F72AB9: 28 bytes, 224 bits. El cable solo llevó sus últimos 20 bytes. OpenSSL 3 cambió el valor por defecto: para parámetros DSA de 1024 bits ahora genera una q de 224 bits, siguiendo la tabla de tamaños de FIPS 186-3/186-4 en lugar del emparejamiento original de 186-2 de L = 1024 con N = 160.

Prueba 1, la biblioteca. Esta es la rutina de keygen en la compilación x86-64 de la misma biblioteca (código fuente idéntico; más fácil de leer que ARM). Calcula los tamaños de P y G con BN_num_bits, pero para Q escribe una constante y copia 20 bytes fijos:

La biblioteca se escribió cuando q solo podía tener 160 bits. Dada una q de 224 bits, BN_bn2bin_fixed(q, buf, 20) emite los 20 bytes de orden inferior y descarta los 8 superiores sin decir una palabra. La tarjeta nunca vio la q real.

Prueba 2, las matemáticas. Los parámetros DSA solo son válidos si q divide a p − 1 y g tiene orden q. Comprobemos la q real y la q’ truncada contra los P y G que realmente se enviaron:

La tarjeta recibió una terna (p, q’, g) en la que q’ no divide a p − 1 y g no tiene orden q’. Un módulo FIPS está obligado a validar los parámetros de dominio antes de usarlos. Lo hizo, falló, y el único código que tiene para «lo intenté y no funcionó» es 0x0a. Cada byte de ese comportamiento es correcto.

Prueba 3, el experimento. Si el tamaño es toda la historia, entonces unos parámetros de OpenSSL 3 con una q de 160 bits deberían aceptarse sin ninguna semilla ni contador. Se generan explícitamente y se cargan en una ranura vacía:

Aceptado en menos de dos segundos, sin búsqueda de primos en el chip, sin semilla, sin contador. La tarjeta devolvió struct spyrus_generate_x_out: longitud 0x88, longitud pública 0x80, y el valor público y de 128 bytes. La biblioteca envolvió entonces y junto con los parámetros en un SubjectPublicKeyInfo y emitió inmediatamente un Load_Certificate (0x2f) para guardar ese PEM en la ranura de certificado 2 bajo la etiqueta provisional TEMPXXXX. Por eso una ranura recién generada siempre muestra un «certificado»: es la clave pública en PEM, aparcada donde más adelante irá un certificado real.

Causa raíz, definitiva. El LYNKS Series II y su biblioteca implementan únicamente FIPS 186-2: L = 1024, N = 160. OpenSSL 3 usa por defecto N = 224 para L = 1024. La biblioteca trunca q a 160 bits en el cable en silencio, los parámetros resultantes son matemáticamente inválidos, y la tarjeta los rechaza con Execution Failure. Solución A: dejar que la tarjeta genere sus propios parámetros. Solución B: generarlos en el anfitrión con -pkeyopt qbits:160. Ni la autenticación, ni el transporte, ni la procedencia estuvieron nunca implicados. El firmware del sismógrafo nunca se topó con esto porque su OpenSSL 1.0.2 siempre producía una q de 160 bits.

Aquí corrijo mi propia explicación pública anterior. La versión anterior era coherente con lo que había observado; no era coherente con el desensamblado, que debería haber leído primero. La lección se generaliza: cuando se dispone de un binario sin stripped, la discusión termina en la instrucción que construye el paquete.

10. La criptografía, byte a byte

La clave pública

Lo que sale de la tarjeta es un SubjectPublicKeyInfo X.509 estándar. Su DER empieza con el identificador de objeto id-dsa, y por eso todas estas claves comienzan con la misma secuencia base64 GByqGSM44BAE:

p tiene 1024 bits con el bit alto activado, así que DER antepone un 0x00 para mantener el INTEGER positivo: 129 bytes. g resulta empezar por debajo de 0x80, así que 128 bytes. Esa regla del relleno de signo vuelve a aparecer en la firma.

La firma

Firmemos un mensaje de 99 bytes. Obsérvese el comando de firma: el anfitrión calcula el hash con SHA-1 (o se lo pide a la tarjeta, opcode 0x2a) y envía el resumen de 20 bytes, no el mensaje. La tarjeta responde con r y s en dos campos de 40 bytes, de los que se usan 20:

La biblioteca codifica entonces en DER SEQUENCE { INTEGER r, INTEGER s }. Cada firma usa una k aleatoria nueva, así que el mismo mensaje se firma de forma distinta cada vez; esta es la que conservo en el repositorio, anotada:

La firma de la ranura 2, hecha con los parámetros de 160 bits generados en el anfitrión, resulta tener ambos enteros por debajo de 0x80 y ocupa 46 bytes. Si alguna vez se ven firmas DSA de 46, 47 o 48 bytes de la misma clave, es la regla del relleno de signo, no corrupción.

Verificar sin nada más que aritmética

OpenSSL 3 no verificará esto. Rechaza SHA-1 con DSA en la capa de políticas, antes de cualquier matemática:

Eso es una política de obsolescencia, no un veredicto. Así que se verifica a mano. Con la clave pública (p, q, g, y), el resumen z y la firma (r, s):

 



La clave privada x nunca salió del módulo (hay un Generate_X y un Load_X en la lista de opcodes, y ninguna ruta en esta biblioteca para extraerla). La posesión se demuestra de la única manera posible: la firma hecha en el chip se verifica contra la clave pública exportada con pura aritmética modular, y un solo byte cambiado en el mensaje la rompe.

11. Extra: una compilación nativa x86-64, con su propio bug

El mismo espejo tiene un árbol CMG-NAM64: el appliance de red x86-64 del fabricante. Incluye spyrus_util compilado para mi propia arquitectura, así que ni siquiera hace falta qemu:

 

Invocar directamente el cargador dinámico del propio árbol, con --library-path, es la forma de ejecutar binarios de una glibc ajena sin tocar el anfitrión: el ld.so de la era 2010, la libc, libssl 1.0.0 y libspyrus vienen todos de la imagen del appliance. Es la versión 2.1.0 frente a la 2.1.5 en ARM, y tiene un bug propio: --getkey falla en una ranura escrita por la compilación 2.1.5, porque 2.1.5 guarda el PEM con finales de línea CRLF y el desempaquetador de PEM de 2.1.0 no los tolera («Unable to unpack PEM»). El volcado de la ranura de certificado sigue siendo legible, así que la clave se puede reconstruir a partir del hexadecimal en crudo: se toma el volcado de --get --index N, se decodifican los bytes, se descartan los \r y el relleno de ceros, y se obtiene el mismo PEM que imprime la compilación ARM. También fue útil para el desensamblado: el listado x86-64 de la sección 9 es de esta compilación, con símbolos idénticos a los de la ARM.

12. Cómo lo probé

comprobación cómo resultado
transporte --state, --status, --list, --time bajo qemu y en nativo; comparar el número de serie con la etiqueta número de serie ...f0:00:18:4f por ambas vías; el RTC responde 2026090811463500
borrar y apropiarse --init --loose con PIN nuevos; SR antes/después 25 → 26, después 27 una vez cargada una clave
parámetros en el chip --keygen --index 1 sin --dsaparam Passed; counter = 147, h = 2; clave en la ranura 1
parámetros rechazados parámetros de 1024 bits por defecto de OpenSSL 3 (q = 224) W4 = 0x0a, reproducible siempre, en 1,4 s
parámetros aceptados parámetros de OpenSSL 3 con qbits:160 en la ranura 2 Passed en 1,9 s; P, Q, G de la ranura 2 idénticos al fichero del anfitrión
firma, ranura 1 firmar, analizar el DER, verificar a mano y con pyca/cryptography v == r; mensaje manipulado rechazado
firma, ranura 2 lo mismo con la clave de parámetros del anfitrión válida; DER de 46 bytes (sin rellenos de signo)
frescura firmar el mismo mensaje dos veces (r, s) distintos cada vez; ambas verifican
reproducción desde cero directorio vacío, solo el script de descarga, sin ficheros en caché 20 ficheros, sumas de comprobación correctas, --list funciona sin privilegios tras la regla udev
compilación nativa los mismos comandos a través del ld.so propio del appliance funciona; bug CRLF de --getkey documentado con solución

Cómo queda el anfitrión después, para que nada sorprenda: un grupo spyrus, una regla udev para 08df:0a00, /etc/spyrus/spyrus.local, y /var/lock/spyrus.lck recreado en el arranque por una entrada tmpfiles. Sin módulo del kernel, sin demonio, sin cambios en el OpenSSL del sistema.

13. Por qué construí hs4l y qué contiene

Todo lo anterior es reproducible con una shell, rsync, qemu y OpenSSL. También es lo bastante engorroso como para que no quiera que nadie lo rehaga a partir de una entrada de blog. Así que empaqueté el conocimiento como hs4l (HSM-SPYRUS-4-Linux), BSD-3-Clause, y lo mantengo:

https://github.com/borjatarraso/hs4l

  • Envoltorios para la compilación ARM bajo qemu y para la compilación nativa x86-64, que localizan el token en sysfs y solo escalan privilegios cuando el nodo, el bloqueo o la configuración no son escribibles.
  • Un script de descarga con sumas de comprobación. Los binarios del fabricante no están en el repositorio. Son firmware con derechos de autor, y la forma correcta de obtenerlos es desde el espejo público, en el momento de la instalación, verificados contra las sumas SHA-256 de los ficheros exactos que probé (20 para el entorno de ejecución ARM, 158 para el corpus completo incluyendo la compilación x86-64 y las cabeceras GPL).
  • Las partes GPL del paquete del fabricante, cabeceras y scripts, con sus avisos intactos, porque son lo que hace legible el formato del cable.
  • Documentación: las notas del protocolo, el recorrido de reinicialización, la lista de resolución de problemas (cada entrada rastreada hasta una causa raíz en el cable), una tabla de dispositivos soportados y los diagramas.
  • Ejemplos que se pueden verificar ahora mismo: la clave pública, el mensaje y la firma de esta entrada; los parámetros rechazados de 224 bits; los parámetros aceptados de 160 bits con la clave y la firma que la tarjeta hizo a partir de ellos; y un verificador de 40 líneas que hace la aritmética DSA sin la capa de políticas de OpenSSL.
  • Un script de configuración de udev/permisos, un Makefile con un objetivo check, y una plantilla de incidencia para informar sobre otras unidades SPYRUS (se espera que las variantes USB del Series II funcionen; la ruta de la tarjeta PCMCIA a través de spyrus_cs.ko no está probada; Rosetta y los productos posteriores usan una pila distinta).

Las versiones van de v0.1 → v0.8 y el registro de cambios cuenta la historia con honestidad, incluida la corrección de la sección 9. Si alguien tiene uno de estos tokens, o un dispositivo SPYRUS distinto, que abra una incidencia con la salida de su lsusb -v y de --status -D -D y le ayudaré.

A más largo plazo, la solución correcta es una implementación libre de libspyrus. La biblioteca solo necesita diecisiete pequeños símbolos de utilidad de la biblioteca auxiliar del fabricante y cuatro stubs de control de alimentación, todos trivialmente reemplazables; los binarios sin stripped y las cabeceras GPL hacen realista una reimplementación de sala limpia. Eso eliminaría la última pieza no libre del camino.

14. Descargas y firmas

Cada versión es una etiqueta git anotada. También publico aquí los tarballs de código fuente, firmados con mi clave OpenPGP, para que se pueda verificar lo que se ejecuta sin confiar en una plataforma de alojamiento:

Lo que deliberadamente no distribuyo es el entorno de ejecución del fabricante en sí. El script de descarga obtiene los ficheros exactos del espejo público y se niega a continuar si una sola suma de comprobación difiere del conjunto que probé. Eso mantiene el proyecto dentro de la legalidad, protege frente a un binario manipulado, y deja al fabricante, que resolvió discretamente este problema en 2010 para los sismólogos del mundo, el control de su propio código.

Un fabricante muerto no es un dispositivo muerto. El protocolo estuvo vivo todo el tiempo, en un binario ARM dentro de un sismógrafo, esperando a que alguien hiciera la pregunta correcta.