Core concepts

Why hybrid cryptography

ML-KEM and ML-DSA were standardized by NIST in 2024 (FIPS 203/204). They are believed to be secure against both classical and quantum computers. However, novel cryptographic standards have historically taken years to accumulate confidence — lattice-based schemes are no exception.

Every major security authority (NIST, CISA, BSI, NCSC) therefore recommends a hybrid approach during the transition period:

  • Combine a well-understood classical algorithm (X25519, Ed25519) with a PQC algorithm (ML-KEM-768, ML-DSA-65).

  • An attacker would need to break both simultaneously.

  • If ML-KEM is later found to have a weakness, X25519 still protects you.

  • If a quantum computer arrives, ML-KEM still protects you.

quantum-safe makes hybrid mode the default. Pure PQC or pure classical is available but requires an explicit opt-in.

Typed outputs

Raw bytes are never returned from key operations. Every output is a distinct Python type:

Type

Purpose

KeyPair

Holds .public and .secret — pass to all operations

PublicKey

Public key with algorithm metadata — safe to distribute

SecretKey

Secret key — zeroized on deletion

HybridCipherText

KEM output — pass to decapsulate()

SharedSecret

KEM result — call .derive_key() rather than using bytes directly

SignedMessage

Self-describing signed message — carries algorithm, context, timestamp

This prevents the entire class of bug where a SharedSecret is passed where a CipherText is expected.

Key metadata

Every key knows its algorithm, version, and migration state:

pub = kp.public
print(pub.algorithm)         # "X25519+ML-KEM-768"
print(pub.migration_state)   # MigrationState.HYBRID_TRANSITION
print(pub.fingerprint())     # "3a7f1c2e..." (SHA-256 hex, first 16 chars)
print(pub.qs_version)        # 1

The MigrationState enum tracks where a key sits in the classical → hybrid → PQC-only migration path:

  • CLASSICAL_ONLY — original classical key, no PQC component

  • HYBRID_TRANSITION — classical + PQC combined key (recommended)

  • PQC_ONLY — pure PQC (future state, after classical deprecation)

Hedged signing

HybridSign and Sign default to hedged mode: a 32-byte random prefix is prepended to the message before signing. This prevents fault-injection attacks that have been demonstrated on lattice signatures in lab conditions.

Hedged mode means two signings of the same message produce different signatures — this is intentional and does not affect verification:

sm1 = signer.sign(b"same", kp.secret)
sm2 = signer.sign(b"same", kp.secret)
assert sm1.signature != sm2.signature  # different random prefix
signer.verify(sm1, kp.public)          # both are valid
signer.verify(sm2, kp.public)

Disable with hedged=False only when deterministic signatures are required (e.g., reproducible test vectors).

Serialization format

Keys use a PEM envelope with custom headers that carry metadata:

-----BEGIN QUANTUM SAFE PUBLIC KEY-----
qs-version: 1
qs-algo: X25519+ML-KEM-768
qs-migration: HYBRID_TRANSITION

<base64-encoded payload>
-----END QUANTUM SAFE PUBLIC KEY-----

The payload is a CBOR-encoded struct:

{
  "algo":  "X25519+ML-KEM-768",
  "pub":   <2-byte-length-prefix + classical bytes + PQC bytes>,
  "v":     1,
}

Payloads larger than 10 MB are rejected by the serialization layer before parsing, guarding against memory-exhaustion via deeply nested or padded structures. Payloads with v < 1 are also rejected to prevent version-rollback attacks on stored key material.

Classical and PQC material is packed as: <2-byte big-endian length of classical bytes> + <classical bytes> + <PQC bytes>

This format is byte-for-byte identical between Python, TypeScript, and Rust, enabling cross-language interoperability.

Backend architecture

The library dispatches to a cryptographic backend for all PQC operations:

  • liboqs — reference implementation of ML-KEM, ML-DSA, SLH-DSA. Ships a pre-built binary for common platforms.

  • rustcrypto — stub awaiting a PyO3 crate (is_available() returns False until published).

Auto-selection prefers rustcrypto then falls back to liboqs. Force a specific backend with get_kem_backend("liboqs").

For classical operations (X25519, Ed25519, AES-GCM, HKDF, X.509) the library always uses the cryptography package directly.

HKDF combiner

The hybrid shared secret uses an HKDF-SHA256 combiner following draft-ietf-tls-hybrid-design:

combined = HKDF(
    salt     = classical_shared_secret,
    input    = pqc_shared_secret,
    info     = b"quantum-safe-hybrid-kem-v1",
    length   = 32,
)

This ensures the combined secret is at least as strong as the stronger of the two components.