Source code for quantum_safe.exceptions

"""
quantum_safe.exceptions
~~~~~~~~~~~~~~~~~~~~~~~

All exceptions raised by this library live here. We keep a strict hierarchy
so callers can catch at whatever granularity they need:

    QuantumSafeError
    ├── CryptoError
    │   ├── DecapsulationError
    │   ├── VerificationError
    │   ├── KeyGenerationError
    │   └── InsecureOperationError
    ├── SerializationError
    │   ├── KeyParseError
    │   └── UnsupportedFormatError
    ├── BackendError
    │   ├── BackendNotAvailable
    │   └── BackendMismatch
    ├── MigrationError
    │   ├── ClassicalKeyDetected
    │   └── IncompatibleKeyVersion
    └── ConfigurationError
        └── UnsupportedAlgorithm

Design notes:
- Every exception carries a machine-readable `code` so callers can branch
  on error type without parsing message strings. Codes are stable across
  patch versions.
- We never put sensitive material (raw key bytes, shared secrets) in
  exception messages. If you see "*** REDACTED ***" in a message, something
  upstream accidentally tried to include key material.
"""

from __future__ import annotations

from typing import Any


[docs] class QuantumSafeError(Exception): """Base class for all quantum-safe library errors.""" code: str = "QS_ERROR" def __init__(self, message: str, **context: Any) -> None: # noqa: ANN401 super().__init__(message) self.message = message # Extra context (file paths, algorithm names, etc.) for structured # logging — never put secret material here. self.context: dict[str, Any] = context def __repr__(self) -> str: ctx = ", ".join(f"{k}={v!r}" for k, v in self.context.items()) return f"{type(self).__name__}({self.message!r}{', ' + ctx if ctx else ''})"
# --------------------------------------------------------------------------- # Crypto errors — something went wrong at the primitive level # ---------------------------------------------------------------------------
[docs] class CryptoError(QuantumSafeError): """Raised when a cryptographic operation fails.""" code = "QS_CRYPTO"
[docs] class DecapsulationError(CryptoError): """Decapsulation failed — usually means wrong secret key or tampered ciphertext. Do NOT log ciphertexts or key material when catching this; the failure itself is the only safe thing to propagate. """ code = "QS_DECAP_FAILED" def __init__(self, algo: str | None = None) -> None: super().__init__( "Decapsulation failed: ciphertext may be malformed or the wrong secret key was used", algo=algo, )
[docs] class VerificationError(CryptoError): """Signature verification failed.""" code = "QS_VERIFY_FAILED" def __init__(self, algo: str | None = None, context_mismatch: bool = False) -> None: if context_mismatch: msg = "Signature verification failed: signing context does not match" else: msg = "Signature verification failed: signature is invalid or message was tampered" super().__init__(msg, algo=algo, context_mismatch=context_mismatch)
[docs] class KeyGenerationError(CryptoError): """Key generation failed — usually an RNG or backend issue.""" code = "QS_KEYGEN_FAILED"
[docs] class InsecureOperationError(CryptoError): """Attempted operation is insecure and has been blocked. Examples: - Using an algorithm below the required security level - Disabling hybrid mode without an explicit override - Reusing a nonce in a scheme that prohibits it """ code = "QS_INSECURE_OP"
# --------------------------------------------------------------------------- # Serialization errors # ---------------------------------------------------------------------------
[docs] class SerializationError(QuantumSafeError): """Raised when key/ciphertext serialization or deserialization fails.""" code = "QS_SERIAL"
[docs] class KeyParseError(SerializationError): """Failed to parse a key from PEM, DER, CBOR, or JWK. `field` indicates which part of the structure was malformed, if known. """ code = "QS_KEY_PARSE" def __init__(self, fmt: str, reason: str, field: str | None = None) -> None: super().__init__( f"Failed to parse {fmt} key: {reason}", format=fmt, field=field, )
class UnsupportedFormatError(SerializationError): """The requested serialization format is not supported for this key type.""" code = "QS_FMT_UNSUPPORTED" def __init__(self, fmt: str, key_type: str) -> None: super().__init__( f"Format '{fmt}' is not supported for key type '{key_type}'", format=fmt, key_type=key_type, ) # --------------------------------------------------------------------------- # Backend errors # ---------------------------------------------------------------------------
[docs] class BackendError(QuantumSafeError): """Errors related to cryptographic backend selection or execution.""" code = "QS_BACKEND"
[docs] class BackendNotAvailable(BackendError): """A required backend is not installed or could not be loaded. The `install_hint` attribute contains a pip command the user can run to fix the problem. """ code = "QS_BACKEND_MISSING" # Maps backend names to install instructions _INSTALL_HINTS: dict[str, str] = { "liboqs": "pip install 'quantum-safe[liboqs]'", "rustcrypto": "pip install 'quantum-safe[rustcrypto]'", } def __init__(self, backend: str) -> None: hint = self._INSTALL_HINTS.get(backend, f"install the '{backend}' backend") super().__init__( f"Backend '{backend}' is not available. To install: {hint}", backend=backend, ) self.install_hint = hint
class BackendMismatch(BackendError): """A key was created with one backend but an operation requires another.""" code = "QS_BACKEND_MISMATCH" def __init__(self, key_backend: str, op_backend: str) -> None: super().__init__( f"Key was created with backend '{key_backend}' but this operation " f"requires '{op_backend}'", key_backend=key_backend, op_backend=op_backend, ) # --------------------------------------------------------------------------- # Migration errors # ---------------------------------------------------------------------------
[docs] class MigrationError(QuantumSafeError): """Errors raised during key migration or classical-crypto scanning.""" code = "QS_MIGRATE"
[docs] class ClassicalKeyDetected(MigrationError): """A pure classical (non-hybrid) key was used where PQC is required. This is raised in strict mode when a classical-only key is passed to an operation that requires at least hybrid security. """ code = "QS_CLASSICAL_KEY" def __init__(self, algo: str, min_required: str = "hybrid") -> None: super().__init__( f"Classical algorithm '{algo}' does not meet the minimum required " f"security level '{min_required}'. Use a hybrid or PQC-only key.", detected_algo=algo, required=min_required, )
[docs] class IncompatibleKeyVersion(MigrationError): """The key's qs-version field is not supported by this library version.""" code = "QS_KEY_VERSION" def __init__(self, key_version: int, supported_max: int) -> None: super().__init__( f"Key uses format version {key_version} but this library only " f"supports up to version {supported_max}. Upgrade quantum-safe.", key_version=key_version, supported_max=supported_max, )
# --------------------------------------------------------------------------- # Configuration errors # ---------------------------------------------------------------------------
[docs] class ConfigurationError(QuantumSafeError): """Raised when the library is misconfigured.""" code = "QS_CONFIG"
[docs] class UnsupportedAlgorithm(ConfigurationError): """The requested algorithm is not supported. `available` contains the list of valid algorithm names so callers can suggest alternatives without hard-coding the list. """ code = "QS_ALGO_UNSUPPORTED" def __init__(self, algo: str, available: list[str] | None = None) -> None: hint = "" if available: hint = f" Available: {', '.join(available)}" super().__init__( f"Algorithm '{algo}' is not supported.{hint}", algo=algo, available=available or [], )