Source code for quantum_safe.backends.base
"""
quantum_safe.backends.base
~~~~~~~~~~~~~~~~~~~~~~~~~~
Abstract base class for cryptographic backends.
Every concrete backend (liboqs, rustcrypto, noble) must implement this
interface. The KEM and signature classes depend only on this interface —
they never import from a specific backend directly. Backend selection
happens at construction time (or via environment detection).
Adding a new backend:
1. Create a module in quantum_safe/backends/
2. Subclass AbstractBackend
3. Register it in quantum_safe/backends/__init__.py
4. It'll appear in KEM(backend="yourname") and in benchmarks automatically
Thread safety: backends are expected to be stateless (no mutable shared
state). Each operation gets fresh inputs and returns fresh outputs. If a
backend needs internal state (e.g. an OpenSSL context), it should create
and destroy it within each method call, not share it.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import ClassVar
@dataclass(frozen=True)
class AlgorithmInfo:
"""Metadata about a specific algorithm on a specific backend.
Used for capability discovery and benchmark reporting.
"""
name: str # e.g. "ML-KEM-768"
nist_level: int # NIST security level 1-5
public_key_size: int # bytes
secret_key_size: int # bytes
ciphertext_size: int # bytes (for KEMs); signature_size for signatures
is_kem: bool
is_signature: bool
is_nist_standard: bool # True if standardized in FIPS 203/204/205
notes: str = ""
[docs]
class AbstractKEMBackend(ABC):
"""Interface for KEM (Key Encapsulation Mechanism) backends.
Implementations wrap a specific cryptographic library (liboqs, RustCrypto,
etc.) and expose a uniform interface for keygen, encap, and decap.
All methods are synchronous. If you need async, call from a thread pool.
"""
# Subclasses must set this to identify themselves in logs and benchmarks
name: ClassVar[str] = "abstract"
[docs]
@abstractmethod
def supported_algorithms(self) -> list[AlgorithmInfo]:
"""Return metadata for all KEM algorithms this backend supports."""
...
[docs]
@abstractmethod
def keygen(self, algorithm: str) -> tuple[bytes, bytes]:
"""Generate a key pair.
Args:
algorithm: Algorithm name, e.g. "ML-KEM-768"
Returns:
(public_key_bytes, secret_key_bytes)
Raises:
UnsupportedAlgorithm: if the algorithm is not supported
KeyGenerationError: if keygen fails (RNG failure, etc.)
"""
...
[docs]
@abstractmethod
def encapsulate(self, algorithm: str, public_key: bytes) -> tuple[bytes, bytes]:
"""Encapsulate: generate a ciphertext and shared secret.
Args:
algorithm: Algorithm name
public_key: Recipient's public key bytes
Returns:
(ciphertext_bytes, shared_secret_bytes)
Raises:
UnsupportedAlgorithm: if the algorithm is not supported
CryptoError: if encapsulation fails
"""
...
[docs]
@abstractmethod
def decapsulate(self, algorithm: str, secret_key: bytes, ciphertext: bytes) -> bytes:
"""Decapsulate: recover the shared secret from a ciphertext.
Args:
algorithm: Algorithm name
secret_key: Recipient's secret key bytes
ciphertext: Ciphertext from encapsulate()
Returns:
shared_secret_bytes (32 bytes for all standardized algorithms)
Raises:
DecapsulationError: if decapsulation fails — NEVER reveal why
UnsupportedAlgorithm: if the algorithm is not supported
"""
...
[docs]
def is_available(self) -> bool:
"""Return True if this backend is installed and functional.
Default implementation tries to generate a key with the first
supported algorithm. Override if there's a cheaper availability check.
"""
try:
algos = self.supported_algorithms()
if not algos:
return False
kem_algos = [a for a in algos if a.is_kem]
if not kem_algos:
return False
self.keygen(kem_algos[0].name)
return True
except Exception: # noqa: BLE001
return False
[docs]
class AbstractSignatureBackend(ABC):
"""Interface for digital signature backends."""
name: ClassVar[str] = "abstract"
[docs]
@abstractmethod
def supported_algorithms(self) -> list[AlgorithmInfo]:
"""Return metadata for all signature algorithms this backend supports."""
...
[docs]
@abstractmethod
def keygen(self, algorithm: str) -> tuple[bytes, bytes]:
"""Generate a signing key pair.
Returns:
(public_key_bytes, secret_key_bytes)
"""
...
[docs]
@abstractmethod
def sign(
self,
algorithm: str,
secret_key: bytes,
message: bytes,
context: bytes = b"",
) -> bytes:
"""Sign a message.
Args:
algorithm: Algorithm name, e.g. "ML-DSA-65"
secret_key: Signer's secret key bytes
message: Message to sign (arbitrary length)
context: Domain-separation context (up to 255 bytes, per FIPS 204)
Returns:
signature_bytes
Raises:
UnsupportedAlgorithm: if the algorithm is not supported
CryptoError: if signing fails
"""
...
[docs]
@abstractmethod
def verify(
self,
algorithm: str,
public_key: bytes,
message: bytes,
signature: bytes,
context: bytes = b"",
) -> bool:
"""Verify a signature.
Args:
algorithm: Algorithm name
public_key: Signer's public key bytes
message: The signed message
signature: Signature bytes from sign()
context: Must match the context used during signing
Returns:
True if valid, False if invalid.
Note: This returns bool rather than raising on invalid signatures.
The caller (Sign.verify()) is responsible for raising VerificationError.
Backends should NOT raise on invalid signatures — return False.
"""
...
[docs]
def is_available(self) -> bool:
"""Return True if this backend is installed and functional."""
try:
algos = self.supported_algorithms()
sig_algos = [a for a in algos if a.is_signature]
if not sig_algos:
return False
pk, sk = self.keygen(sig_algos[0].name)
sig = self.sign(sig_algos[0].name, sk, b"test")
return self.verify(sig_algos[0].name, pk, b"test", sig)
except Exception: # noqa: BLE001
return False