Post-Quantum Signing
Single-party post-quantum digital signatures via FIPS 204 (ML-DSA) and FIPS 205 (SLH-DSA). Six parameter sets covering NIST security levels 1-5.
Post-Quantum Signing
Standards: ML-DSA per NIST FIPS 204 (2024, based on CRYSTALS-Dilithium); SLH-DSA per NIST FIPS 205 (2024, based on SPHINCS+). See Cryptographic foundations — Post-quantum signatures for the full standard references and the original academic papers.
PostQuantum exposes FIPS-standardised post-quantum signature algorithms for applications that need forward secrecy against future quantum adversaries. Six parameter sets across two families, all available through one uniform API.
When to use post-quantum signatures
- Long-term archival signatures — a signature minted today that must remain verifiable in 20+ years, when large-scale quantum computers may be able to break classical EC signatures.
- Compliance with post-quantum mandates — government and regulated-industry guidance (NIST, CNSA 2.0, BSI) is increasingly mandating post-quantum readiness for new systems.
- Defence-in-depth alongside classical signatures — sign critical artefacts with both an EC signature and a post-quantum signature; either one verifying is sufficient. Protects against either family being broken without breaking your verification pipeline.
Available algorithms
The SDK exposes six FIPS-standardised algorithms across two families:
| Algorithm | Family | NIST level | Signature size |
|---|---|---|---|
PqAlgorithm.MlDsa44 | ML-DSA (lattice-based, FIPS 204) | Level 2 | 2,420 bytes |
PqAlgorithm.MlDsa65 | ML-DSA (lattice-based, FIPS 204) | Level 3 | 3,309 bytes |
PqAlgorithm.MlDsa87 | ML-DSA (lattice-based, FIPS 204) | Level 5 | 4,627 bytes |
PqAlgorithm.SlhDsaSha2128f | SLH-DSA (hash-based, FIPS 205) | Level 1 | 17,088 bytes |
PqAlgorithm.SlhDsaSha2192f | SLH-DSA (hash-based, FIPS 205) | Level 3 | 35,664 bytes |
PqAlgorithm.SlhDsaSha2256f | SLH-DSA (hash-based, FIPS 205) | Level 5 | 49,856 bytes |
The two families have different operational trade-offs:
| Property | ML-DSA | SLH-DSA |
|---|---|---|
| Cryptographic assumption | Lattice problems (LWE / Module-LWE) | Cryptographic hash functions (SHA-2) |
| Signature size | 2.4 - 4.6 KB | 17 - 50 KB |
| Public key size | 1.3 - 2.6 KB | 32 - 64 bytes |
| Sign speed | Microseconds | Tens of milliseconds |
| Verify speed | Microseconds | Microseconds |
| Conservatism | Newer; relies on hardness assumptions specific to lattices | Older / more conservative; relies only on hash-function security |
Pick ML-DSA-65 as your default. It's the FIPS-recommended general-purpose choice and balances signature size, speed, and security. Use SLH-DSA only when your threat model specifically requires the more conservative hash-based foundation, and you can absorb the much larger signature size.
API
import { PostQuantum, PqAlgorithm } from '@zafeguard/mpc-sdk';
const pq = new PostQuantum(PqAlgorithm.MlDsa65);
// Generate a fresh keypair.
const { publicKey, secretKey } = pq.keygen();
// Sign a message under the secret key.
const message = Buffer.from('payload-to-sign', 'utf8');
const signature = pq.sign(message, secretKey);
// Verify the signature under the public key.
const ok = pq.verify(signature, message, publicKey);
// ok === truenew PostQuantum(algorithm) → PostQuantum
Constructs a handle for one specific algorithm. Pick algorithm from the PqAlgorithm enum.
The handle is cheap to create — internally it's just a tag selecting which underlying algorithm to dispatch to. You can create one handle per algorithm or one per signing operation; the choice is purely stylistic.
keygen() → PqKeyPair
Generates a fresh keypair using the OS RNG. Returns { publicKey: Buffer, secretKey: Buffer }.
The key sizes are determined by the algorithm — see the table above. ML-DSA secret keys are ~2.6 KB to ~4.9 KB; SLH-DSA secret keys are 64 to 128 bytes.
Keygen is fast (microseconds) for both families. The expensive operation is signing — see the per-family speed comparison.
sign(message, secretKey) → Buffer
Returns a detached signature buffer. message is the data being signed (NOT a hash — the algorithm internally hashes the input).
ML-DSA signatures are randomised — two calls with the same (message, secretKey) produce different valid signatures. SLH-DSA signatures are deterministic for the same (message, secretKey).
verify(signature, message, publicKey) → boolean
Returns true on a valid signature, false on a tampered signature, a wrong public key, or a malformed signature buffer. Doesn't throw — invalid inputs yield false rather than an exception.
End-to-end example
import { PostQuantum, PqAlgorithm } from '@zafeguard/mpc-sdk';
// 1. Pick an algorithm. ML-DSA-65 is the FIPS-recommended default.
const pq = new PostQuantum(PqAlgorithm.MlDsa65);
// 2. Mint a keypair. Distribute publicKey freely; secretKey stays private.
const { publicKey, secretKey } = pq.keygen();
console.log('Public key size:', publicKey.length, 'bytes'); // 1952
console.log('Secret key size:', secretKey.length, 'bytes'); // 4032
// 3. Sign a document.
const document = Buffer.from('Long-term archival record');
const signature = pq.sign(document, secretKey);
console.log('Signature size:', signature.length, 'bytes'); // 3309
// 4. Verify (anyone with the public key can do this).
const verifier = new PostQuantum(PqAlgorithm.MlDsa65);
const valid = verifier.verify(signature, document, publicKey);
// valid === true
// 5. Tampering rejection.
const tampered = Buffer.from('Long-term archival record!');
const tamperedValid = verifier.verify(signature, tampered, publicKey);
// tamperedValid === falseHybrid signing — classical + post-quantum
The conservative approach during the transition period is to sign every artefact with BOTH a classical EC signature and a post-quantum signature. The artefact verifies if either signature checks out, so a future break in either family doesn't invalidate your records.
Scheme.signWithPrivateKey(privateKey, messageHash) takes a pre-computed digest — the caller hashes the message first (typically via ec.hash(document) for the curve's standard hash, or Utils.sha256 / Utils.keccak256 for cross-runtime).
PostQuantum.sign(message, secretKey) takes the raw message — the algorithm hashes internally as part of the signature construction (deterministic ML-DSA / SLH-DSA hashing is part of the spec).
In the hybrid example below, the classical side calls ec.hash(document) first; the post-quantum side passes document directly. Don't pre-hash the input to PostQuantum.sign — you'd be signing a hash-of-a-hash, which still verifies but produces signatures that won't validate against a verifier written to the FIPS spec.
import { Scheme, Curve, PostQuantum, PqAlgorithm } from '@zafeguard/mpc-sdk';
const document = Buffer.from('Critical artefact');
// Classical signature — caller hashes; signWithPrivateKey takes the digest.
const ec = new Scheme(Curve.Secp256k1);
const ecKeyPair = ec.generateKeypair();
const ecSig = ec.signWithPrivateKey(ecKeyPair.privateKey, ec.hash(document));
// Post-quantum signature — pass the raw document; ML-DSA hashes internally.
const pq = new PostQuantum(PqAlgorithm.MlDsa65);
const pqKeyPair = pq.keygen();
const pqSig = pq.sign(document, pqKeyPair.secretKey);
// Store BOTH signatures alongside the document. Verification
// accepts the document if either signature checks out.
const envelope = {
document,
ecSignature: { algorithm: 'secp256k1-ecdsa', signature: ecSig.signature, publicKey: ecKeyPair.publicKey },
pqSignature: { algorithm: 'ml-dsa-65', signature: pqSig, publicKey: pqKeyPair.publicKey },
};This is the standard recommendation during the post-quantum transition (CNSA 2.0, NIST PQ migration guidance) — don't replace classical signatures, augment them.
Single-party only
This surface is single-party: one process holds the secret key, that process signs. There's no threshold or distributed-keygen variant — threshold post-quantum signing is an open research area with no widely-deployed protocol as of the current FIPS standardisation.
If your topology needs MPC-distributed key generation today and you also need post-quantum coverage, the practical pattern is:
- Use the threshold-MPC SDK (
EmbeddedAgent,ClusterAgent) for your primary signing key on a classical curve. - Use
PostQuantumfor an additional single-party signing key held in your most-secure environment (HSM, secure enclave, isolated VM). - Sign critical artefacts with both. The threshold property protects against single-party compromise of the classical key; the post-quantum property protects against future cryptanalysis of the classical curve.
Security properties
- FIPS-standardised algorithms: every parameter set is from the published NIST PQ standards. No experimental or unstandardised variants.
- Tampering rejection: bit-flipping a message or signature causes verification to return
false. No silent accept-malformed path. - Wrong-key rejection: verifying a signature under a different public key returns
false. - Replay-safe verification: the algorithms bind to the exact message bytes; "the same signature verifies for a different message" is cryptographically impossible.
- Size-pinning: the SDK rejects malformed key / signature buffers whose lengths don't match the algorithm's spec.
Limitations
- No threshold or MPC variant of post-quantum signing is exposed — that's an open standardisation problem.
- Signatures are much larger than classical EC signatures (especially SLH-DSA, at 17 KB to 50 KB). Plan for this in storage / bandwidth budgets.
- Signing is slower than classical EC, particularly for SLH-DSA (tens of milliseconds per sign vs microseconds for EC).
- The SDK doesn't currently provide a key-export / sealed-storage path for PQ keypairs analogous to
EmbeddedAgent.export(). Store thesecretKeybuffer yourself with whatever sealing your application uses.
RSA
Threshold RSA-OAEP — trusted-dealer mode for simplicity, zero-trust mode for combiner-blind decryption.
Derivation paths
How hierarchical key derivation works in threshold MPC — when to use a derivation path, the standard path layouts (BIP-32, BIP-44, BIP-84), and the precise rules for signing and verifying against a derived sub-key.