"""
quantum_safe.kem.algorithms
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Algorithm registry and security policy for KEM operations.
This module is the single source of truth for:
- Which algorithms are supported
- What their security levels are
- Which combinations are valid for hybrid mode
- What the minimum acceptable security level is
If you want to allow or disallow an algorithm library-wide, this is where
to do it — not scattered across multiple backend files.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import IntEnum
class NISTLevel(IntEnum):
"""NIST security categories from FIPS 203/204/205.
Each level is defined as "at least as hard to break as":
L1 → AES-128 key search
L2 → SHA-256 / SHA3-256 collision search
L3 → AES-192 key search
L4 → SHA-384 / SHA3-384 collision search
L5 → AES-256 key search
"""
L1 = 1
L2 = 2
L3 = 3
L4 = 4
L5 = 5
@dataclass(frozen=True)
class KEMAlgorithmSpec:
"""Everything the library needs to know about a KEM algorithm."""
name: str
nist_level: NISTLevel
public_key_bytes: int
secret_key_bytes: int
ciphertext_bytes: int
shared_secret_bytes: int
is_nist_standard: bool # True = FIPS 203
suitable_for_hybrid: bool # True = approved classical companion
notes: str = ""
@dataclass(frozen=True)
class ClassicalKEMSpec:
"""Spec for a classical KEM used as the hybrid companion.
We only support X25519 and P-256 as classical companions. RSA-OAEP is
deliberately excluded — it's larger and slower and provides no meaningful
additional security in the hybrid context.
"""
name: str
public_key_bytes: int # ephemeral public key sent in ciphertext
shared_secret_bytes: int # X25519 and ECDH both produce 32 bytes
# ---------------------------------------------------------------------------
# PQC KEM algorithm registry
# ---------------------------------------------------------------------------
KEM_ALGORITHMS: dict[str, KEMAlgorithmSpec] = {
"ML-KEM-512": KEMAlgorithmSpec(
name="ML-KEM-512",
nist_level=NISTLevel.L1,
public_key_bytes=800,
secret_key_bytes=1632,
ciphertext_bytes=768,
shared_secret_bytes=32,
is_nist_standard=True,
suitable_for_hybrid=True,
notes=(
"Smallest ML-KEM variant. Security level 1 is generally considered "
"adequate for most applications today, but ML-KEM-768 is preferred "
"for new deployments since the size difference is small."
),
),
"ML-KEM-768": KEMAlgorithmSpec(
name="ML-KEM-768",
nist_level=NISTLevel.L3,
public_key_bytes=1184,
secret_key_bytes=2400,
ciphertext_bytes=1088,
shared_secret_bytes=32,
is_nist_standard=True,
suitable_for_hybrid=True,
notes="Recommended default. Security level 3 matches X25519.",
),
"ML-KEM-1024": KEMAlgorithmSpec(
name="ML-KEM-1024",
nist_level=NISTLevel.L5,
public_key_bytes=1568,
secret_key_bytes=3168,
ciphertext_bytes=1568,
shared_secret_bytes=32,
is_nist_standard=True,
suitable_for_hybrid=True,
notes="Maximum security. Use when long-term key confidentiality is critical.",
),
# Research / non-standard algorithms — not recommended for production
"BIKE-L1": KEMAlgorithmSpec(
name="BIKE-L1",
nist_level=NISTLevel.L1,
public_key_bytes=1541,
secret_key_bytes=3114,
ciphertext_bytes=1573,
shared_secret_bytes=32,
is_nist_standard=False,
suitable_for_hybrid=False,
notes="NIST Round 4 candidate. Not standardized.",
),
"HQC-128": KEMAlgorithmSpec(
name="HQC-128",
nist_level=NISTLevel.L1,
public_key_bytes=2249,
secret_key_bytes=2289,
ciphertext_bytes=4433,
shared_secret_bytes=64,
is_nist_standard=False,
suitable_for_hybrid=False,
notes="NIST Round 4 candidate. Not standardized.",
),
}
# Classical companion specs
CLASSICAL_KEM_ALGORITHMS: dict[str, ClassicalKEMSpec] = {
"X25519": ClassicalKEMSpec(
name="X25519",
public_key_bytes=32,
shared_secret_bytes=32,
),
"P-256": ClassicalKEMSpec(
name="P-256",
public_key_bytes=65, # uncompressed point
shared_secret_bytes=32,
),
}
# Valid hybrid combinations: classical_name -> [pqc_name, ...]
# The IETF hybrid-design draft specifies exactly these combinations.
HYBRID_COMBINATIONS: dict[str, list[str]] = {
"X25519": ["ML-KEM-768", "ML-KEM-512", "ML-KEM-1024"],
"P-256": ["ML-KEM-512", "ML-KEM-768"],
}
# The default hybrid: X25519 + ML-KEM-768, as recommended by
# NIST, CISA, BSI, and NCSC transition guidance documents.
DEFAULT_HYBRID_CLASSICAL = "X25519"
DEFAULT_HYBRID_PQC = "ML-KEM-768"
# Minimum NIST level we'll use in non-strict mode.
# Algorithms below this need explicit opt-in via allow_low_security=True.
MINIMUM_NIST_LEVEL = NISTLevel.L1
[docs]
def canonical_hybrid_name(classical: str, pqc: str) -> str:
"""Return the canonical algorithm string for a hybrid combination.
Example: canonical_hybrid_name("X25519", "ML-KEM-768") -> "X25519+ML-KEM-768"
"""
return f"{classical}+{pqc}"
[docs]
def parse_hybrid_name(name: str) -> tuple[str, str]:
"""Parse a hybrid algorithm name into (classical, pqc) components.
Raises ValueError if the name doesn't look like a hybrid name.
"""
if "+" not in name:
raise ValueError(f"'{name}' is not a hybrid algorithm name (expected 'Classical+PQC')")
parts = name.split("+", 1)
return parts[0], parts[1]
[docs]
def validate_hybrid_combination(classical: str, pqc: str) -> None:
"""Raise ValueError if the combination is not approved.
We don't block unapproved combinations entirely (a researcher might have
a legitimate reason), but we raise by default so callers have to be
explicit about using a non-standard combination.
"""
if classical not in CLASSICAL_KEM_ALGORITHMS:
raise ValueError(
f"Classical KEM '{classical}' is not supported. "
f"Valid options: {list(CLASSICAL_KEM_ALGORITHMS)}"
)
if pqc not in KEM_ALGORITHMS:
raise ValueError(
f"PQC KEM '{pqc}' is not in the algorithm registry. "
f"Valid options: {list(KEM_ALGORITHMS)}"
)
approved_pqc = HYBRID_COMBINATIONS.get(classical, [])
if pqc not in approved_pqc:
raise ValueError(
f"Combination '{classical}+{pqc}' is not an approved hybrid combination. "
f"Approved PQC algorithms for {classical}: {approved_pqc}. "
f"Pass validate=False to override."
)
[docs]
def get_algorithm_spec(name: str) -> KEMAlgorithmSpec:
"""Return the spec for a PQC algorithm, raising UnsupportedAlgorithm if unknown."""
spec = KEM_ALGORITHMS.get(name)
if spec is None:
from quantum_safe.exceptions import UnsupportedAlgorithm
raise UnsupportedAlgorithm(name, available=list(KEM_ALGORITHMS))
return spec