Skip to content

Choosing what to use ​

The library offers several ways to do each job. This page says which to pick for a given situation and, just as important, when not to use something. It is the answer the maintainers would give you in person.

The decision in one table ​

You want to...UseDo not use
Encrypt data to someone's public keyEnvelope.seal with a hybrid key (HybridKEM, default X25519+ML-KEM-768)HybridKEM.encapsulate plus your own AES code
Encrypt under the CNSA 2.0 parameter setsEnvelope.seal with a pure ML-KEM-1024 key (envelope v2) via cnsa2.kem()A hybrid key (reported partial); ML-KEM-768
Encrypt data that other ecosystems (not Python) must decryptX-WingThe default hybrid (it is its own construction: only Python and this library read it)
Encrypt a large file or an uploadsealStream / openStreamEnvelope.seal on a multi-gigabyte buffer
Sign data that only quantum-safe libraries verifyHybridSign with a contextA signature without a context
Sign data that other ecosystems must verifySign('ML-DSA-65-v2') or HybridSign('Ed25519+ML-DSA-65-v2')The default format (a generic ML-DSA library cannot verify it)
Issue tokens your own services verifyJWTSigner / JWTVerifierHand-built tokens
Issue tokens that other JOSE libraries must verifyStandardJwt (RFC 9964)JWTSigner (only quantum-safe libraries verify it)
Move existing keys to post-quantum safelyUpgrader plus MigrationStateManagerSwapping every key atomically in one deploy
Find classical cryptography in a codebasequantum-safe-audit scan in CIGrep
Check a configuration against the CNSA 2.0 parameter setscnsa2.report and cnsa2.enforceA hybrid suite offered as a compliance claim

The same decision as code:

ts
import { HybridKEM, HybridSign, cnsa2, Envelope, utf8 } from 'quantum-safe-ts';

function encryptionKeys(cnsa2Profile: boolean) {
  // Pure ML-KEM-1024 (envelope v2) for the CNSA 2.0 profile; otherwise the hybrid default.
  return cnsa2Profile ? cnsa2.kem().generateKeyPair() : new HybridKEM().generateKeyPair();
}

function signer(everyVerifierIsPy031OrTs: boolean) {
  // -v2 when every verifier can read it; the default format when an older Python verifier is involved.
  return everyVerifierIsPy031OrTs ? new HybridSign('Ed25519+ML-DSA-65-v2') : new HybridSign();
}

for (const profile of [false, true]) {
  using keys = encryptionKeys(profile);
  const sealed = Envelope.seal(utf8('data'), keys.publicKey);
  if (sealed.version !== (profile ? 2 : 1)) throw new Error('the version is chosen by the key, not by you');
}
for (const modern of [false, true]) {
  const s = signer(modern);
  using kp = s.generateKeyPair();
  const sm = s.sign(utf8('data'), kp.secretKey, { context: utf8('example-v1') });
  s.verify(sm, kp.publicKey, { expectedContext: utf8('example-v1') });
}

Key exchange and encryption ​

Hybrid or pure? ​

Use hybrid (the default) for new deployments during the transition. A hybrid such as X25519+ML-KEM-768 stays secure if either of its two components holds, which is the position NIST, CISA, BSI and NCSC take for the transition period. The price is a few tens of microseconds and 34 extra bytes of ciphertext and of public key (X25519's 32 bytes plus a 2-byte length).

Use pure ML-KEM when a standard or a contract requires it. CNSA 2.0 is the main case: NSA accepts standalone ML-KEM-1024 and does not require hybrids (see CNSA 2.0).

Do not use pure ML-KEM merely to save bytes. If bandwidth is tight, measure first; the hybrid overhead is small next to a network round trip.

Which security level? ​

Parameter setNIST categoryPick it when
ML-KEM-5121Almost never. Only to interoperate with something that fixes it.
ML-KEM-7683The default. Right for most applications.
ML-KEM-10245CNSA 2.0, or long-lived secrets where you want margin.

Hybrid, X-Wing, or the Python-compatible hybrid? ​

X25519+ML-KEM-768 (default)X-Wing
Readable byquantum-safe-py and quantum-safe-tsOther X-Wing implementations (for example @noble/post-quantum) and quantum-safe-ts; not quantum-safe-py
Specified byquantum-safe-py's own HKDF combinerdraft-connolly-cfrg-xwing-kem (an individual Internet-Draft, not an RFC)
Reviewed against NIST SP 800-227's combiner guidanceNoMentioned in SP 800-227 as an example; that is not an endorsement

If Python is on the other end, take the default. If anyone else is, take X-Wing.

Envelope v1 or v2? ​

You do not choose a version: Envelope.seal picks it from the key. A hybrid key gives v1 (HKDF-SHA-256). A pure ML-KEM-1024 key gives v2 (HKDF-SHA-384, which is what CNSA 2.0 asks of key derivation). quantum-safe-py 0.3.2 or later reads and writes both.

Signatures ​

Hybrid or pure? ​

Use hybrid (HybridSign, Ed25519+ML-DSA-65) while classical signatures still carry trust in your ecosystem. Both signatures must verify.

Use pure ML-DSA (Sign) for CNSA 2.0 (ML-DSA-87) or when size matters more than belt-and-braces. Sizes are fixed by the algorithm:

ts
import { Sign, HybridSign, utf8 } from 'quantum-safe-ts';

const message = utf8('size check');
const sizes: Record<string, number> = {};
for (const name of ['ML-DSA-65', 'ML-DSA-65-v2'] as const) {
  const s = new Sign(name);
  using kp = s.generateKeyPair();
  sizes[name] = s.sign(message, kp.secretKey).signature.length;
}
const h = new HybridSign('Ed25519+ML-DSA-65-v2');
using hk = h.generateKeyPair();
sizes['Ed25519+ML-DSA-65-v2'] = h.sign(message, hk.secretKey).signature.length;
console.log(sizes);
// -v2: the ML-DSA-65 signature is 3309 bytes; the hybrid adds a 64-byte Ed25519 signature.
if (sizes['ML-DSA-65-v2'] !== 3309 || sizes['Ed25519+ML-DSA-65-v2'] !== 3309 + 64) throw new Error('unexpected sizes');
// The default format adds a 33-byte hedging prefix (1 length byte + 32 random bytes).
if (sizes['ML-DSA-65'] !== 3309 + 33) throw new Error('unexpected default-format size');

Default format or -v2? ​

Default (v1)-v2
Who can verifyquantum-safe-py (all versions) and quantum-safe-tsquantum-safe-py 0.3.2 or later and quantum-safe-ts. A standard FIPS 204 library can verify it only by rebuilding the wrapped message M2 and passing the context quantum-safe-sig-v2.
StructureHedging prefix and an unsigned length byte, wrapped in CBORNo prefix; one fixed-length blob
HedgingYour choice (hedged); the verifier must matchAlways hedged inside ML-DSA; nothing to match
What is signedContext yes; the algorithm noAlgorithm and context, for both halves
Available forEvery ML-DSA suite, the hybrids, SLH-DSAML-DSA-44/65/87 and the Ed25519 / P-256 hybrids (no SLH-DSA)

Recommendation. For new signatures where every verifier you control is quantum-safe-py 0.3.2+ or quantum-safe-ts, use -v2. Keep the default while an older Python verifier is in the loop. The default of easy.generateSigningKeys and HybridSign() stays Ed25519+ML-DSA-65 for now, so that existing Python deployments keep verifying; it is planned to flip to -v2 in the next minor release. Choosing -v2 explicitly today is safe and is the clean choice.

WARNING

Never use the same key material in both formats. The ML-DSA halves are separated by FIPS 204's context, but the classical halves are not. Generate separate keys for -v2.

Always give signatures a context ​

A context such as myapp-release-v1 ties a signature to one purpose. Sign with it and verify with the same value (expectedContext), and a signature made for one purpose cannot be replayed for another.

Tokens (JWT) ​

  • JWTSigner / JWTVerifier: tokens that only quantum-safe-py and quantum-safe-ts can verify. Supports hybrid keys. Pick it when every verifier is yours.
  • StandardJwt (RFC 9964): tokens any compliant JOSE library can verify; pure ML-DSA only. Pick it when other ecosystems, gateways or third parties verify your tokens.

Do not put a JWTSigner token in front of a generic JWT gateway: it will be rejected.

Migration ​

Upgrader produces a hybrid key that contains your classical key, and MigrationStateManager tracks each key through classical_only → hybrid_transition → pqc_preferred → pqc_only. An upgraded hybrid key is not backward compatible: classical-only software cannot parse it, so keep publishing the original classical key to such clients during the transition. Give the manager a store with compareAndSet whenever more than one process shares it (Migration).

When this library is not the right tool ​

  • You need stateful hash-based signing (LMS or XMSS, for firmware signing under CNSA 2.0). Only LMS verification is provided. Signing needs durable state and is deliberately left out.
  • You need to run on constrained or embedded devices. The WebAssembly is about 410 KiB gzipped. Use liboqs, mlkem-native or a vendor SDK there.
  • You only need a primitive and the platform already has it (recent Node.js and some browsers expose ML-KEM and ML-DSA in WebCrypto). A smaller dependency is better; see Which library should I use?.

Apache-2.0.