SPYRUS LYNKS (Fortezza) USB HSM - anyone have the En-Sign / SPEX PKCS#11 or legacy driver? #3802
|
I have a working SPYRUS LYNKS Series II USB HSM (08df:0a00, the older Fortezza-derived token) and I'm trying to use it on Linux/Windows. What I've found: the only public driver is the WHQL smart-card shim (SCARDLYNKSUSBW.sys), which registers it as a reader but can't do crypto - SCardConnect fails SCARD_E_PROTO_MISMATCH because the card presents a proprietary protocol (ATR declares T=14). I've reverse-engineered the transport (bulk EP1/EP2) and recovered the command structure from spex32.dll, but the bulk channel stays gated (every write → 0xff); the channel-enable + framing live only in SPYRUS's legacy SPEX kernel driver / En-Sign stack, which isn't public. pcsc-lite/ccid don't list PID 0x0a00, and OpenSC has no LYNKS driver. Does anyone happen to have the LYNKS-capable SPYRUS middleware - the En-Sign / En-Sign NG SDK (PKCS11sc.dll + a Linux libpkcs11*.so) or the legacy SPEX driver/CSP? SPYRUS is defunct (now Route1); I'm also reaching out to the original developers. Any copy, pointer, or even confirmation it's gone would be hugely appreciated. Happy to share my RE notes. Thanks! |
Replies: 1 comment
Resurrecting a SPYRUS LYNKS Series II HSM on Linux, from scratchNo driver, no middleware, no PIN — using Güralp's If you have one of these orphaned SPYRUS tokens in a drawer, here is a complete, reproducible way to wipe it and use it again — entirely on Linux, with no Windows and no vendor software. Everything below is raw commands — no wrapper, no repo to clone. The only third-party pieces are Güralp's own ARM binaries, which you pull yourself from their public rsync mirror (I do not redistribute them). TL;DRA 2005 SPYRUS LYNKS Series II (USB Reinitialise the card (documented from-scratch path, destroys all keys, sets new PINs — no old secret needed), generate a DSA-1024 keypair on-chip, sign, and verify with the public key. Done. The one hard bug — keygen always returning The device
Goal: I did not care about whatever was on it. I wanted to own it and use it from scratch. The three walls
The USB picture (why nothing binds it)That The pivot: seismographsGüralp Systems build earthquake digitisers that sign their data streams (CD1.1 / IMS2.0) with a hardware key, and the hardware they picked is the LYNKS Series II. Their embedded-Linux "Platinum" firmware ships:
The firmware is a browsable rootfs on a public rsync mirror (release ≥ 15781): The key detail is in the binary's dependencies: It links The rig: ARM binary, x86-64 host, real USB
sudo env QEMU_LD_PREFIX=$SYS LD_LIBRARY_PATH=$SYS/lib:$SYS/usr/lib \
qemu-arm-static $SYS/usr/sbin/spyrus_util <flags>One-time (and again every time it re-enumerates), let sudo chmod 666 /dev/bus/usb/001/<dev>There is no daemon — the firmware init script says so. One process owns the pipe for the length of one command. First contact (read-only)SR is the status register; it reads 25 on an initialised-but-empty card and steps to 26 after a fresh The wipe (documented from-scratch path)Notes:
The bug that ate a day: keygen →
|
| word | sent | response | meaning |
|---|---|---|---|
| W0 opcode | 0000 0085 |
9000 0085 |
0x85 GenerateDSAKeyPair |
| W4 result | 0000 0000 |
0000 000a |
result code 0x0a |
| W7 index | 0000 0001 |
0000 0001 |
slot 1 (valid 1..9; slot 9 = EC) |
| W9.. P/Q/G | 0000 0400.. |
echoed | 1024-bit P, 160-bit Q, 1024-bit G |
The result-code strings live in spyrus_strerror. Indexed from zero:
| code | meaning | code | meaning |
|---|---|---|---|
| 0 | Spyrus command failed | 6 | Invalid Cert Index |
| 1 | Passed | 7 | Invalid Data Size |
| 2 | Checkword Failure | 8 | Invalid Header |
| 3 | Invalid Type | 9 | Invalid State |
| 4 | Invalid Mode | 10 | Execution Failure (= 0x0a) |
| 5 | Invalid Key Index | 11 | No Key Loaded |
| 21 | NO PQG Loaded |
So 0x0a = Execution Failure: a generic "the card tried and could not", not a structural rejection (not Invalid Header, not Invalid State, not NO PQG). The command was fine; the operation itself aborted.
False leads I ruled out on the wire:
- "It needs a login." Passing
--pinonly sent the tool down a validation path that trips on an empty card; no login exchange appeared either way. Güralp's owngenerate_keypairpasses no PIN. No change. - "Post-init session lapsed." Re-init + keygen back-to-back, Güralp's exact order. Identical failure. So not auth, not session state.
Root cause: the DSA domain parameters. I had been feeding the card P, Q, G generated by the host's OpenSSL 3. This 2005 chip validates DSA parameters the FIPS 186-2 way — it wants the generation seed and counter so it can recompute and check them — and modern OpenSSL emits bare parameters with no such provenance. The card accepted the structure, tried to validate, and bailed.
Fix: stop supplying parameters. Drop --dsaparam and let the card generate its own (it runs the FIPS 186-2 routine on-chip, ~1 minute, and prints its counter and h):
# spyrus_util --keygen --index 1 (no --dsaparam, no --pin)
......+......++++++++++++++++++++++* on-chip prime search
counter = 147, h = 2
Sending Generate DSA key pair command block ...
Received response: 90 00 00 85 ... result Passed
-----BEGIN PUBLIC KEY-----
MIIBtjCCASsGByqGSM44BAEwggEeAoGB...
-----END PUBLIC KEY-----
Proof: sign on-chip, verify off-chip
The private key is sealed in the module and can never be read out. So prove possession indirectly: sign on the card, verify with the public key alone.
Message signed (99 bytes):
LYNKS Series II HSM 2026-09-08T12:35:52Z reborn on Linux via
spyrus_util+qemu-arm. Signed on-chip.
SHA-1(msg) = a91e306721cbd648888e2c2d5a86d1dc49f5a899
The signature is textbook DSS — an ASN.1 SEQUENCE of two 160-bit integers r and s. sig.bin is 47 bytes:
30 2d 02 14 60 27 82 67 82 79 bf 57 49 4d eb 62
87 18 4a b3 29 46 46 e3 02 15 00 87 82 50 af 73
60 b3 38 16 b2 f2 ba e3 b0 d5 fb ed 9c 3b b7
30 SEQUENCE
2d length 45
02 14 INTEGER, 20 bytes
r = 602782678279BF57494DEB6287184AB3294646E3
02 15 INTEGER, 21 bytes <- 21, not 20, because...
00 ...the next byte 0x87 has its high bit set, and DER integers
are signed, so a 0x00 sign-pad is prepended to keep s positive
s = 878250AF7360B33816B2F2BAE3B0D5FBED9C3BB7
Nice DER gotcha: r starts with 0x60 (< 0x80) so no pad, 20 bytes; s starts with 0x87 (≥ 0x80) so it gets the 0x00 pad, 21 bytes. Both are 160-bit values.
DSA verification, for reference, with public (p, q, g, y) and z = SHA-1(msg):
w = s^-1 mod q
u1 = (z * w) mod q
u2 = (r * w) mod q
v = ((g^u1 * y^u2) mod p) mod q
accept iff v == r
Result:
- Genuine message → Verified OK
- Tampered message → rejected (bound to the exact bytes)
Important verify caveat: OpenSSL 3 refuses SHA-1 + DSA at policy level (do_sigver_init: invalid digest). That is a deprecation policy, not a signature mismatch — it errors before doing any math, on good and bad input alike, so it cannot be your judge. Use a legacy-tolerant verifier. I used Python's cryptography (pyca):
from cryptography.hazmat.primitives import hashes, serialization
pub = serialization.load_pem_public_key(open("pub.pem", "rb").read())
pub.verify(open("sig.bin", "rb").read(), open("msg", "rb").read(),
hashes.SHA1()) # raises InvalidSignature if tamperedThe full recipe (copy/paste), no wrapper
Raw commands only. $SYS is a directory holding the Güralp Platinum ARM rootfs subset (spyrus_util, libspyrus.so.3, libusb, /lib, /usr/lib).
0. Stage the ARM rootfs subset from Güralp's public rsync mirror
SYS=$HOME/platinum-sysroot; mkdir -p $SYS
rsync -av --prune-empty-dirs \
--include='usr/' --include='usr/sbin/' --include='usr/sbin/spyrus_*' \
--include='usr/lib/' --include='usr/lib/libspyrus.so.*' \
--include='usr/lib/libgslutil.so.*' --include='usr/lib/libiso8601.so.*' \
--include='usr/lib/libioline-*.so.*' --include='usr/lib/libusb-*.so*' \
--include='usr/lib/libssl.so.1.0.0' \
--include='usr/lib/libcrypto.so.1.0.0' \
--include='lib/' --include='lib/ld-*' --include='lib/lib*.so*' \
--exclude='*' \
rsync://rsync.guralp.com/platinum-stable/CMG-DCM-mk4-eabi/ $SYS/
# any Platinum release >= 15781 works; if a NEEDED lib is missing,
# 'readelf -d $SYS/usr/sbin/spyrus_util' tells you which
sha256sum $SYS/usr/sbin/spyrus_util $SYS/usr/lib/libspyrus.so.3.1
# d78d56d4cf05faf2be39677adfb0e6411843b71815a8877b2a6bb4a06fac39ef spyrus_util
# c77447cf1cccdf03c2f849e93d7a89cf0f259d28c2defff8a685e32311b9c2b1 libspyrus.so.3.11. User-mode ARM emulation on the x86-64 host
sudo dnf install qemu-user-static # Debian/Ubuntu: apt install qemu-user-static2. Let libusb open the token (redo after every re-enumeration)
lsusb -d 08df:0a00 # Bus 001 Device 0DD: ...
sudo chmod 666 /dev/bus/usb/001/0DD3. Run the ARM binary against the real USB node — the whole trick. qemu-user forwards syscalls to the host kernel, so libusb's ioctls on /dev/bus/usb reach the device unchanged. Every call below is the same invocation with different spyrus_util flags.
4. Read-only hello
sudo env QEMU_LD_PREFIX=$SYS LD_LIBRARY_PATH=$SYS/lib:$SYS/usr/lib \
qemu-arm-static $SYS/usr/sbin/spyrus_util --state5. Wipe & own (the yes is read from the tty, not stdin)
sudo env QEMU_LD_PREFIX=$SYS LD_LIBRARY_PATH=$SYS/lib:$SYS/usr/lib \
qemu-arm-static $SYS/usr/sbin/spyrus_util \
--init --loose --sso-pin 1234 --user-pin 12346. Keygen on-chip (no --dsaparam, no --pin)
sudo env QEMU_LD_PREFIX=$SYS LD_LIBRARY_PATH=$SYS/lib:$SYS/usr/lib \
qemu-arm-static $SYS/usr/sbin/spyrus_util --keygen --index 17. Export the public key (SubjectPublicKeyInfo, PEM)
sudo env QEMU_LD_PREFIX=$SYS LD_LIBRARY_PATH=$SYS/lib:$SYS/usr/lib \
qemu-arm-static $SYS/usr/sbin/spyrus_util --getkey --index 1 > pub.pem8. Sign on-chip (ASN.1 DER SEQUENCE{r,s}, SHA-1 DSA)
printf 'hello from the LYNKS' > msg
sudo env QEMU_LD_PREFIX=$SYS LD_LIBRARY_PATH=$SYS/lib:$SYS/usr/lib \
qemu-arm-static $SYS/usr/sbin/spyrus_util \
--sign msg --index 1 --binary > sig.bin9. Verify off-card with a legacy-tolerant library (OpenSSL 3 refuses SHA-1 DSA by policy)
python3 - <<'EOF'
from cryptography.hazmat.primitives import hashes, serialization
pub = serialization.load_pem_public_key(open("pub.pem", "rb").read())
pub.verify(open("sig.bin", "rb").read(), open("msg", "rb").read(),
hashes.SHA1())
print("VALID")
EOFGotchas, condensed
- Never pass
--dsaparamfrom modern OpenSSL →0x0a Execution Failure. Let the card self-generate. - No
--pinon keygen; it triggers cert/slot validation that fails on an empty loose card. keygen auto-loads aTEMPXXXXplaceholder cert. --looseis init-only; standard mode needs a cert in every slot.- The
--init"yes" reads from the tty, not stdin. - OpenSSL 3 will not verify SHA-1 DSA (policy, not a real failure).
libusbneeds RW on the device node; re-chmod after re-enumeration.- Slot indices are 1..9; slot 9 is EC.
Legality / ethics
This is recovery of a device I physically own and am entitled to use. I reinitialised it from scratch through a documented SPYCOS function; I did not extract any key material and did not bypass any authentication. The vendor firmware components remain the property of their owners and were used only to operate my own hardware.
If you have one of these tokens, the whole Linux toolchain is now public at https://github.com/borjatarraso/hs4l — clone it, run scripts/fetch-vendor.sh (or scripts/fetch-corpus.sh for a native x86-64 build that needs no qemu) and bin/spy.sh --status; it is verified on the LYNKS Series II USB interface (USB ID 08df:0a00), should work on other LYNKS Series II units, ships the GPL headers with the full card command set so the PCMCIA/CardBus card variant is reachable too (untested), and does not cover other SPYRUS products such as Rosetta or Hydra — reports from anyone with a different unit are welcome as GitHub issues.
Hope this saves someone else several weeks it cost me.
Questions are welcomed.
Borja Tarraso
Resurrecting a SPYRUS LYNKS Series II HSM on Linux, from scratch
No driver, no middleware, no PIN — using Güralp's
spyrus_utilunderqemu-arm+libusb.If you have one of these orphaned SPYRUS tokens in a drawer, here is a complete, reproducible way to wipe it and use it again — entirely on Linux, with no Windows and no vendor software.
Everything below is raw commands — no wrapper, no repo to clone. The only third-party pieces are Güralp's own ARM binaries, which you pull yourself from their public rsync mirror (I do not redistribute them).
TL;DR
A 2005 SPYRUS LYNKS Series II (USB
08df:0a00, FIPS 140-2 L2, NIST CMVP #679, SPYCOS OS) has no Linux driver and no surviving middleware. But Gü…