Ctrl K
ComunidadGuías…

aegis/engines/sovereign.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Copyright (c) 2026 Juan Luna. All rights reserved.
# SPDX-License-Identifier: LicenseRef-Aegis-Commercial-1.0
# Licensed under the Aegis Commercial License 1.0; see LICENSE and NOTICE.

"""Sovereign vault — post-quantum signing and hardware-backed key custody.

Wraps :mod:`aegis.core.pqc_signer`, :mod:`aegis.core.hsm` and
:mod:`aegis.core.pqc_tls` behind one surface, and reports honestly which of
them the running environment actually has:

    vault = SovereignVault()
    print(vault.capabilities())        # what is present, not what is claimed
    signature = vault.sign(b"payload")

Availability is a runtime fact, not a build claim
-------------------------------------------------

Three capabilities here are independently optional. ML-DSA needs the compiled
``aegis_rust`` extension; the hybrid KEM needs ``kyber-py``; PKCS#11 needs a
token and a driver library. :meth:`capabilities` probes each and returns what
is really there, so a caller can refuse rather than discover it mid-request.

Boundaries carried over unchanged
---------------------------------

- ML-DSA-65 and ML-KEM-1024 name **algorithms**, not validated modules. Nothing
  here is FIPS 140 validated, and algorithm choice is not an approved
  operational mode (``CLM-013``-class boundary).
- **No constant-time claim is made or approved.** The retained timing
  experiment passed non-detection for `sign` and failed for `verify`; see
  ``docs/security/PQC_CONSTANT_TIME.md``. Do not describe this surface as
  timing-invariant.
- The hybrid KEM helper is a **key-agreement exchange, not an authenticated
  transport**. It hashes no protocol transcript and authenticates no peer, so
  it is not TLS and must not be presented as one.
- PKCS#11 custody is only as good as the token, its driver, its PIN handling
  and the operator's key ceremony — none of which this module can observe.
- ``sign`` refuses to fabricate a signature when no real backend is present.
  There is no simulated mode.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

from aegis.core import pqc_tls
from aegis.core.hsm import HSMSigningBackend, HSMUnavailableError
from aegis.core.pqc_signer import PQCSigner, PQCUnavailableError
from aegis.core.pqc_signer import backend_available as pqc_backend_available
from aegis.engines import require_module
from aegis.licensing.validator import LicenseEntitlement

_MODULE_NAME = "sovereign"


@dataclass(frozen=True, slots=True)
class VaultCapabilities:
    """What this process can actually do right now."""

    mldsa_signing: bool
    hybrid_kem: bool
    pkcs11_hsm: bool

    def as_dict(self) -> dict[str, bool]:
        return {
            "mldsa_signing": self.mldsa_signing,
            "hybrid_kem": self.hybrid_kem,
            "pkcs11_hsm": self.pkcs11_hsm,
        }


@dataclass(frozen=True, slots=True)
class SignatureResult:
    """A signature together with what produced it.

    ``scheme`` is one of ``ml-dsa-65``, ``pkcs11-rsa-pss-sha256`` or
    ``pkcs11-ecdsa-sha256``. It travels with the signature because a verifier
    that assumes the wrong one cannot tell a misconfiguration from a forgery.
    """

    signature: bytes
    scheme: str
    public_key: bytes


class SovereignVault:
    """Post-quantum signing and hardware key custody as a library."""

    def __init__(
        self,
        *,
        require_real_pqc: bool = True,
        hsm_backend: HSMSigningBackend | None = None,
        entitlement: LicenseEntitlement | None = None,
    ) -> None:
        self._entitlement = require_module(_MODULE_NAME, entitlement=entitlement)
        self._hsm = hsm_backend
        self._require_real_pqc = require_real_pqc
        self._signer: PQCSigner | None = None

    @property
    def entitlement(self) -> LicenseEntitlement | None:
        return self._entitlement

    def capabilities(self) -> VaultCapabilities:
        """Probe each optional backend. Cheap, and safe to call repeatedly."""

        hsm_ready = False
        if self._hsm is not None:
            try:
                hsm_ready = bool(self._hsm.available)
            except Exception:  # pragma: no cover - driver-specific failure
                hsm_ready = False
        return VaultCapabilities(
            # The module-level probe, not ``PQCSigner.is_available``: that is a
            # property, so reading it off the class yields the property object
            # rather than a bool — always truthy, and a capabilities report that
            # is always true is worse than none.
            mldsa_signing=pqc_backend_available(),
            hybrid_kem=pqc_tls.backend_available(),
            pkcs11_hsm=hsm_ready,
        )

    # ── signing ─────────────────────────────────────────────────────────

    def _pqc_signer(self) -> PQCSigner:
        if self._signer is None:
            self._signer = PQCSigner(require_real=self._require_real_pqc)
        return self._signer

    def sign_detached(self, message: bytes) -> SignatureResult:
        """Sign, and say which scheme did it.

        The HSM takes precedence because a hardware-held key is the stronger
        custody story, and the point of configuring one is that it is used.

        The scheme is returned rather than implied because the two backends do
        not agree: PKCS#11 signs with ``pkcs11-rsa-pss-sha256`` or
        ``pkcs11-ecdsa-sha256``, while the software path signs with ML-DSA-65.
        Returning bare bytes from either would leave a verifier guessing which
        algorithm and which public key to use, and guessing wrong reads as a
        forged signature rather than a configuration error.

        Raises when neither backend is usable; it never returns a placeholder.
        """

        if self._hsm is not None:
            try:
                signature, public_key_hex, scheme = self._hsm.sign(message)
                return SignatureResult(
                    signature=signature,
                    scheme=scheme,
                    public_key=bytes.fromhex(public_key_hex),
                )
            except HSMUnavailableError:
                if self._require_real_pqc:
                    raise
        signer = self._pqc_signer()
        return SignatureResult(
            signature=signer.sign(message),
            scheme="ml-dsa-65",
            public_key=signer.public_key,
        )

    def sign(self, message: bytes) -> bytes:
        """Signature bytes only, for a caller that already knows the scheme.

        Prefer :meth:`sign_detached` unless the scheme is fixed by
        configuration you control.
        """

        return self.sign_detached(message).signature

    @staticmethod
    def verify(message: bytes, signature: bytes, public_key: bytes) -> bool:
        """Verify an ML-DSA-65 signature. No constant-time claim is made."""

        return PQCSigner.verify(message, signature, public_key)

    def public_key(self) -> bytes:
        key: bytes = self._pqc_signer().public_key
        return key

    # ── hybrid key agreement ────────────────────────────────────────────

    def new_hybrid_exchange(self) -> Any:
        """Return a hybrid X25519 + ML-KEM-1024 exchange, or refuse.

        This is a key-agreement helper. It is not an authenticated transport
        and must not be described as TLS.
        """

        if not pqc_tls.backend_available():
            raise PQCUnavailableError(
                "hybrid ML-KEM exchange needs the optional 'kyber-py' package; "
                "install the pqc extra or kyber-py directly"
            )
        return pqc_tls.HybridPQCExchange()


__all__ = ["SignatureResult", "SovereignVault", "VaultCapabilities"]