Tools: Your Digital Signing Cryptography Has an Expiration Date: What NIST Published and How to Migrate Your HSM - 2025 Update

Tools: Your Digital Signing Cryptography Has an Expiration Date: What NIST Published and How to Migrate Your HSM - 2025 Update

Why this is urgent even though quantum computers don't really exist yet

What NIST published in August 2024

What changes in your HSM

The hybrid approach: how to migrate without breaking anything

What changes in the code

The working demo: SoftwareHSM with ML-DSA in TypeScript

What to do right now

The real deadline If you have a system today that digitally signs anything — a document, a JWT, a certificate, a binary — and that system uses RSA or ECDSA, NIST is telling you that system has an expiration date. This isn't FUD. It's not "sometime in the future." The final standards have been published since August 2024. Serious organizations have already started migrating. And the clock is ticking because there are attacks happening right now — the results just won't show up until someone has the quantum computer to cash them in. Let me walk you through what NIST published, what exactly breaks, how it hits HSMs, and what you can actually do starting today. The attack is called harvest now, decrypt later. Nation-state actors are capturing encrypted TLS traffic and digitally signed data today, storing it, and waiting until they have access to a quantum computer powerful enough to break it. For symmetric crypto (AES, HMAC), the impact is manageable — double your key size and you're mostly fine. For asymmetric cryptography — RSA, ECDSA, EdDSA — Shor's algorithm breaks it completely. There's no key-size patch that saves you. If you're signing documents that need to stay valid in 10 or 15 years, the problem is already yours. Three final standards and one draft. Three are relevant for digital signatures: FIPS 204 — ML-DSA (Module-Lattice-Based Digital Signature Algorithm)

The direct successor to ECDSA. Based on CRYSTALS-Dilithium, which survived several years of cryptanalysis during the NIST competition. This is what you'll actually use in practice. Three security levels: For reference: an ECDSA P-256 signature is 64 bytes. An ML-DSA-44 signature is 2,420 bytes — almost 38x larger. This matters a lot when you're signing at volume or when certificate size has hard constraints. FIPS 205 — SLH-DSA (Stateless Hash-Based Digital Signature)Based on SPHINCS+. Its security rests entirely on hash functions — if SHA-3 holds, SLH-DSA holds. It's the conservative backup for scenarios where you want the fewest possible mathematical assumptions. Bigger signatures, slower operations, but the security argument is the strongest of the three. FIPS 206 (draft) — FN-DSA (FFT-based Digital Signature)

Based on FALCON. More compact signatures than ML-DSA, but constant-time implementation is notoriously tricky — especially on hardware with floating-point operations. For now, unless you have a very specific size requirement, ML-DSA is the practical choice. Here's the concrete problem for most production digital signature implementations. An RSA-2048 private key is 256 bytes. An ML-DSA-44 private key has two representations: the compact seed (32 bytes) and the expanded key for operations (2,528 bytes for ML-DSA-44, 4,032 bytes for ML-DSA-65). HSMs are designed to store and operate on RSA and ECDSA keys — internal memory constraints and transfer buffers don't necessarily handle these dimensions without changes. PKCS#11 needs updating. The PKCS#11 standard that virtually every HSM uses to expose its cryptographic operations had no OIDs or key types for ML-DSA. Vendor firmware updates include PKCS#11 extensions to support the new algorithms. But that means your middleware — the library that talks to the HSM — also needs to be updated. Not all HSMs have the same migration path. Utimaco launched Quantum Protect, an application package that activates in-field on their Se-Series line without replacing the hardware. It ships via an updated PKCS#11 and supports ML-KEM, ML-DSA, plus stateful hash-based signatures (LMS, XMSS). That's genuinely good news — if you have modern Utimaco hardware, you probably don't need to replace it. Thales has their HSEs (High Speed Encryptors) built on reprogrammable FPGAs, which gives them similar flexibility. Their Luna Network HSM line is also getting PQC support. The YubiHSM 2, which a lot of people use for development or smaller deployments, has memory constraints that limit scalability with expanded ML-DSA keys. There, the path may be replacement. FIPS 140-3 validation is a separate concern. An HSM that supports ML-DSA via firmware update doesn't automatically have FIPS 140-3 validation for those algorithms. Validation requires an accredited lab process that can take 12-18 months. If your compliance requires validated FIPS 140-3 for the module, check your specific vendor's validation status for PQC — don't assume the firmware update covers it. The practical recommendation from both NIST and the vendors is to migrate in hybrid mode: sign with the classical algorithm and the PQC algorithm simultaneously during the transition period. A verifier that doesn't support PQC keeps verifying with ECDSA. A verifier that does support PQC can verify with ML-DSA. Once every participant in the system has migrated, you drop ECDSA. For X.509 certificates, IETF is working on the hybrid certificate draft (draft-ietf-lamps-pq-composite-sigs). Experimental implementations already exist in some stacks. For JWTs and other token formats, the working group is still finalizing the algorithms. The ML-DSA algorithm identifier in JWA will be ML-DSA-44, ML-DSA-65, and ML-DSA-87 (or with an id- prefix depending on the final RFC). If you're using OpenSSL or a similar library to verify signatures today, the code-level change is smaller than it looks — if the library already supports the new algorithms. OpenSSL 3.x has experimental ML-DSA support via the Open Quantum Safe provider. In practice: In Node.js, node-forge still doesn't support ML-DSA natively. For production today, the path is: The key point: if your architecture already routes all crypto through the HSM (which is how it should be), the application code change is minimal. The hard part is updating the HSM, the middleware, and the certificates — not rewriting business logic. To go along with this post I built a project that implements everything I described above: a software HSM that never exposes private keys, with real support for ML-DSA and SLH-DSA using @noble/post-quantum — the pure TypeScript implementation of the new NIST standards. The repo: JuanTorchia/pq-signing-demo 1. SoftwareHSM with 6 algorithms All private key logic is encapsulated. The outside world only receives KeyPair (with the public key) and SignatureResult (just the signature). Same security model as a hardware HSM, without the hardware. When your vendor's firmware arrives, you swap the implementation. The rest of the code doesn't change — that's crypto-agility in practice. 3. JWT with ML-DSA-65 The demo implements the IETF draft format draft-ietf-cose-dilithium, with alg: "ML-DSA-65" in the header. The resulting token is ~4,600 chars vs ~250 for a typical ES256 JWT. Practical implication: for long-lived tokens or document signing, it's fine. For millions of short-lived access tokens per day, you'll want to wait for ML-DSA-44 or FN-DSA. 4. The benchmark shows the real trade-offs SLH-DSA has big signatures AND is slow — it's there as a maximum-security backup, not a practical ECDSA replacement. ML-DSA-65 at 4.6ms per signing operation is completely viable in production. 1. Inventory your cryptographic surface. Map where you sign digitally today: TLS certificates, document signing, JWTs, code signing, intermediate CA certificates. For each one, note: what algorithm it uses, what HSM backs it, and how long that signature needs to remain valid. Documents signed with ECDSA that need to be valid in 2035 go to the top of the list. 2. Talk to your HSM vendor. Specific questions to ask: 3. Start experimenting with the OQS stack. The Open Quantum Safe project (liboqs + oqs-provider for OpenSSL) lets you experiment with ML-DSA today, without real hardware. It's the environment to understand how sizes change, how timing changes, and what the impact is on your certificate infrastructure. 4. Check signature size assumptions in your protocols. If you have a protocol that assumes 64-byte ECDSA signatures and you're moving to 3,309-byte ML-DSA-65, there are implications for MTU, buffers, and payload size validation. Better to discover that in staging. 5. For new systems: build crypto-agility in from the start. Crypto-agility means the signing algorithm is configurable, not hardcoded. The right abstraction: When migration time comes, you swap the implementation, not the architecture. The NSA requires all national security systems (NSS) to complete migration to PQC before 2030. NIST IR 8547 establishes that quantum-vulnerable algorithms (RSA, ECDSA, ECDH, DH) will be deprecated and removed from NIST standards before 2035. The European Commission expects all member states to have a complete migration plan implemented by end of 2026. This isn't science fiction. It's a compliance calendar that's already running. The problem isn't that quantum computers exist today. The problem is that public key infrastructure takes years to migrate, certificates have long lives, and documents signed today need to be verifiable in 2035. That time is already being consumed. The OpenSSL and PKCS#11 code examples assume OpenSSL 3.x with oqs-provider and PKCS#11 v3.x with vendor ML-DSA support. For production digital signing, check your HSM's support status before committing to an implementation. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

Document signature = ECDSA(hash) || ML-DSA(hash) Document signature = ECDSA(hash) || ML-DSA(hash) Document signature = ECDSA(hash) || ML-DSA(hash) # Install the OQS provider for OpenSSL 3 # (available at oqs-provider: https://github.com/open-quantum-safe/oqs-provider) # Generate an ML-DSA-65 key pair openssl genpkey -algorithm mldsa65 -out private.pem # Generate the self-signed certificate openssl req -new -x509 -key private.pem -out cert.pem -days 365 \ -subj "/CN=test/O=JuanchiDev" # Inspect the certificate — the public key will be 1952 bytes openssl x509 -in cert.pem -text -noout | grep "Public Key" # Public Key Size: 1952 bytes (ML-DSA-65) # Install the OQS provider for OpenSSL 3 # (available at oqs-provider: https://github.com/open-quantum-safe/oqs-provider) # Generate an ML-DSA-65 key pair openssl genpkey -algorithm mldsa65 -out private.pem # Generate the self-signed certificate openssl req -new -x509 -key private.pem -out cert.pem -days 365 \ -subj "/CN=test/O=JuanchiDev" # Inspect the certificate — the public key will be 1952 bytes openssl x509 -in cert.pem -text -noout | grep "Public Key" # Public Key Size: 1952 bytes (ML-DSA-65) # Install the OQS provider for OpenSSL 3 # (available at oqs-provider: https://github.com/open-quantum-safe/oqs-provider) # Generate an ML-DSA-65 key pair openssl genpkey -algorithm mldsa65 -out private.pem # Generate the self-signed certificate openssl req -new -x509 -key private.pem -out cert.pem -days 365 \ -subj "/CN=test/O=JuanchiDev" # Inspect the certificate — the public key will be 1952 bytes openssl x509 -in cert.pem -text -noout | grep "Public Key" # Public Key Size: 1952 bytes (ML-DSA-65) // Conceptual example — signing via PKCS#11 to an HSM with ML-DSA import { PKCS11 } from "pkcs11js" const pkcs11 = new PKCS11() pkcs11.load("/usr/lib/softhsm/libsofthsm2.so") // or your HSM driver // The HSM exposes ML-DSA as CKM_ML_DSA (new mechanism in PKCS#11 v3.x) const mechanism = { mechanism: pkcs11.CKM_ML_DSA } // The signing API is identical to ECDSA // The difference is in the mechanism and key handles const signature = pkcs11.C_Sign(session, data, mechanism) // The signature will be 3309 bytes for ML-DSA-65 // vs 64 bytes for ECDSA P-256 console.log(`Signature size: ${signature.length} bytes`) // Conceptual example — signing via PKCS#11 to an HSM with ML-DSA import { PKCS11 } from "pkcs11js" const pkcs11 = new PKCS11() pkcs11.load("/usr/lib/softhsm/libsofthsm2.so") // or your HSM driver // The HSM exposes ML-DSA as CKM_ML_DSA (new mechanism in PKCS#11 v3.x) const mechanism = { mechanism: pkcs11.CKM_ML_DSA } // The signing API is identical to ECDSA // The difference is in the mechanism and key handles const signature = pkcs11.C_Sign(session, data, mechanism) // The signature will be 3309 bytes for ML-DSA-65 // vs 64 bytes for ECDSA P-256 console.log(`Signature size: ${signature.length} bytes`) // Conceptual example — signing via PKCS#11 to an HSM with ML-DSA import { PKCS11 } from "pkcs11js" const pkcs11 = new PKCS11() pkcs11.load("/usr/lib/softhsm/libsofthsm2.so") // or your HSM driver // The HSM exposes ML-DSA as CKM_ML_DSA (new mechanism in PKCS#11 v3.x) const mechanism = { mechanism: pkcs11.CKM_ML_DSA } // The signing API is identical to ECDSA // The difference is in the mechanism and key handles const signature = pkcs11.C_Sign(session, data, mechanism) // The signature will be 3309 bytes for ML-DSA-65 // vs 64 bytes for ECDSA P-256 console.log(`Signature size: ${signature.length} bytes`) git clone https://github.com/JuanTorchia/pq-signing-demo npm install npm run demo # full interactive demo npm run benchmark # size and timing comparison git clone https://github.com/JuanTorchia/pq-signing-demo npm install npm run demo # full interactive demo npm run benchmark # size and timing comparison git clone https://github.com/JuanTorchia/pq-signing-demo npm install npm run demo # full interactive demo npm run benchmark # size and timing comparison interface HSMProvider { generateKeyPair(algorithm: Algorithm, label: string): Promise<KeyPair> sign(keyId: string, data: Uint8Array): Promise<SignatureResult> verify(keyId: string, data: Uint8Array, signature: Uint8Array): Promise<VerifyResult> } interface HSMProvider { generateKeyPair(algorithm: Algorithm, label: string): Promise<KeyPair> sign(keyId: string, data: Uint8Array): Promise<SignatureResult> verify(keyId: string, data: Uint8Array, signature: Uint8Array): Promise<VerifyResult> } interface HSMProvider { generateKeyPair(algorithm: Algorithm, label: string): Promise<KeyPair> sign(keyId: string, data: Uint8Array): Promise<SignatureResult> verify(keyId: string, data: Uint8Array, signature: Uint8Array): Promise<VerifyResult> } const doc = "Services Agreement — April 2026" const ecdsaDoc = await signDocument(doc, "ecdsa-p256") // 64 bytes const mldsaDoc = await signDocument(doc, "ml-dsa-65") // 3309 bytes const valid = await verifyDocument(mldsaDoc) // ✅ const doc = "Services Agreement — April 2026" const ecdsaDoc = await signDocument(doc, "ecdsa-p256") // 64 bytes const mldsaDoc = await signDocument(doc, "ml-dsa-65") // 3309 bytes const valid = await verifyDocument(mldsaDoc) // ✅ const doc = "Services Agreement — April 2026" const ecdsaDoc = await signDocument(doc, "ecdsa-p256") // 64 bytes const mldsaDoc = await signDocument(doc, "ml-dsa-65") // 3309 bytes const valid = await verifyDocument(mldsaDoc) // ✅ ── Signature Size ────────────────────────────────── ecdsa-p256 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 64 bytes (1x) ml-dsa-44 █████████░░░░░░░░░░░░░░░░░░░░░ 2420 bytes (38x) ml-dsa-65 █████████████░░░░░░░░░░░░░░░░░ 3309 bytes (52x) slh-dsa-sha2-128s ██████████████████████████████ 7856 bytes (123x) ── Signing Time (average over 20 iterations) ─────── ecdsa-p256 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.99 ms ml-dsa-65 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 4.64 ms slh-dsa-sha2-128s ██████████████████████████████ 3806.09 ms ── Signature Size ────────────────────────────────── ecdsa-p256 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 64 bytes (1x) ml-dsa-44 █████████░░░░░░░░░░░░░░░░░░░░░ 2420 bytes (38x) ml-dsa-65 █████████████░░░░░░░░░░░░░░░░░ 3309 bytes (52x) slh-dsa-sha2-128s ██████████████████████████████ 7856 bytes (123x) ── Signing Time (average over 20 iterations) ─────── ecdsa-p256 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.99 ms ml-dsa-65 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 4.64 ms slh-dsa-sha2-128s ██████████████████████████████ 3806.09 ms ── Signature Size ────────────────────────────────── ecdsa-p256 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 64 bytes (1x) ml-dsa-44 █████████░░░░░░░░░░░░░░░░░░░░░ 2420 bytes (38x) ml-dsa-65 █████████████░░░░░░░░░░░░░░░░░ 3309 bytes (52x) slh-dsa-sha2-128s ██████████████████████████████ 7856 bytes (123x) ── Signing Time (average over 20 iterations) ─────── ecdsa-p256 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.99 ms ml-dsa-65 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 4.64 ms slh-dsa-sha2-128s ██████████████████████████████ 3806.09 ms docker run -it openquantumsafe/oqs-ossl3-img # Already has OpenSSL 3 + oqs-provider installed openssl list -signature-algorithms | grep mldsa docker run -it openquantumsafe/oqs-ossl3-img # Already has OpenSSL 3 + oqs-provider installed openssl list -signature-algorithms | grep mldsa docker run -it openquantumsafe/oqs-ossl3-img # Already has OpenSSL 3 + oqs-provider installed openssl list -signature-algorithms | grep mldsa interface SigningProvider { algorithm: "ecdsa-p256" | "ml-dsa-65" | "hybrid-ecdsa-mldsa65" sign(data: Buffer): Promise<Buffer> verify(data: Buffer, signature: Buffer, publicKey: Buffer): Promise<boolean> publicKey(): Promise<Buffer> } interface SigningProvider { algorithm: "ecdsa-p256" | "ml-dsa-65" | "hybrid-ecdsa-mldsa65" sign(data: Buffer): Promise<Buffer> verify(data: Buffer, signature: Buffer, publicKey: Buffer): Promise<boolean> publicKey(): Promise<Buffer> } interface SigningProvider { algorithm: "ecdsa-p256" | "ml-dsa-65" | "hybrid-ecdsa-mldsa65" sign(data: Buffer): Promise<Buffer> verify(data: Buffer, signature: Buffer, publicKey: Buffer): Promise<boolean> publicKey(): Promise<Buffer> } - The HSM signs via PKCS#11 with ML-DSA (once your vendor ships the firmware) - Your application uses the PKCS#11 binding (pkcs11js in Node.js or equivalent) without needing the runtime's crypto library to support ML-DSA directly - Verification can be done with liboqs via native binding - Does your HSM model have planned ML-DSA support via firmware? - What's the timeline? - Does the update maintain FIPS 140-3 validation for ML-DSA? - How do existing keys migrate?