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:

AlgorithmFamilyNIST levelSignature size
PqAlgorithm.MlDsa44ML-DSA (lattice-based, FIPS 204)Level 22,420 bytes
PqAlgorithm.MlDsa65ML-DSA (lattice-based, FIPS 204)Level 33,309 bytes
PqAlgorithm.MlDsa87ML-DSA (lattice-based, FIPS 204)Level 54,627 bytes
PqAlgorithm.SlhDsaSha2128fSLH-DSA (hash-based, FIPS 205)Level 117,088 bytes
PqAlgorithm.SlhDsaSha2192fSLH-DSA (hash-based, FIPS 205)Level 335,664 bytes
PqAlgorithm.SlhDsaSha2256fSLH-DSA (hash-based, FIPS 205)Level 549,856 bytes

The two families have different operational trade-offs:

PropertyML-DSASLH-DSA
Cryptographic assumptionLattice problems (LWE / Module-LWE)Cryptographic hash functions (SHA-2)
Signature size2.4 - 4.6 KB17 - 50 KB
Public key size1.3 - 2.6 KB32 - 64 bytes
Sign speedMicrosecondsTens of milliseconds
Verify speedMicrosecondsMicroseconds
ConservatismNewer; relies on hardness assumptions specific to latticesOlder / 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 === true

new 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 === false

Hybrid 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 PostQuantum for 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 the secretKey buffer yourself with whatever sealing your application uses.

On this page