Source code for cadrumo.application.modelo.review_package_signing

"""Ed25519 signing and signature verification for review packages.

This module implements the signing slice deferred by
:mod:`~application.modelo.review_package`: that module's
:func:`~application.modelo.review_package.verify_review_package` is an INTEGRITY check
only (did every archived member arrive byte-for-byte as built); it makes no
claim about WHO built the package. This module adds the AUTHENTICITY layer on
top of that integrity check by signing the package's self-attesting
:attr:`~core.corpus_manifest.manifest.CorpusManifest.manifest_sha256` digest with a
per-profile Ed25519 keypair.

Signing the manifest digest (not the archive bytes or a re-derived hash) means
the signature transitively covers every archived member: the digest is a
SHA-256 over the canonical JSON of the manifest's per-file entries, and a
tampered member is already caught by
:func:`~core.corpus_manifest.manifest.verify_corpus_bundle` before signature
verification is even attempted (see :func:`verify_review_package_signature`).

Key custody (``sensitive-financial-data-secure-storage-only`` /
``no-legacy-compatibility``): the private key is generated once per profile
bucket and persisted ONLY as ciphertext by the required signing-keypair
capability. Its outer binding owns the secure-storage classification and
failure translation. The key is never logged, never written to a plaintext
file, and never leaves this module as raw bytes except transiently in process
memory to sign. The public key is, by construction, safe to export and hand
to a receiving accountant for signature verification -- it carries no secrecy
requirement.

Counter-signed accountant feedback-package round trips remain OUT OF SCOPE for
this module; it exposes only the primitive: mint/load a per-profile keypair,
sign a package's manifest digest, verify a signature against a public key.

See Also:
    :mod:`~application.modelo.review_package`
        Builds and integrity-verifies the review package this module signs.
    :mod:`~application.modelo.review_package_signing_ports`
        Application-owned keypair capability contract.
"""

from __future__ import annotations

from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING

from cryptography.hazmat.primitives.asymmetric.ed25519 import (
    Ed25519PrivateKey,
    Ed25519PublicKey,
)
from pydantic import BaseModel, Field

from ...core.corpus_manifest.errors import CorpusBundleError, CorpusManifestTamperError
from ...core.corpus_manifest.manifest import verify_corpus_bundle
from ...core.ed25519_signing import (
    digest_signature_is_valid,
    ed25519_private_key_from_hex,
    ed25519_public_key_from_hex,
    sign_digest_hex,
)
from ...core.errors.hierarchy import CadrumoError
from ...core.hex import HEX_PATTERN_64 as _HEX_PATTERN_64
from ...core.hex import HEX_PATTERN_128 as _HEX_PATTERN_128
from ...core.identity.bucket import BucketId
from ...core.identity.hex_ids import CalculationRevisionId
from ...core.models import STRICT_FROZEN_CONFIG as _STRICT_FROZEN
from ...core.time.clock import now as _utc_now
from ...core.time.utc import UtcInstant
from ...domain.calculations.registry.authority import bundled_indexed_authority
from .review_package import assert_review_package_verifies

if TYPE_CHECKING:
    from .review_package_signing_ports import ReviewPackageSigningKeypairCapability

#: Wire-format version of the signature envelope. Bumped when the envelope
#: schema changes shape (e.g. a future multi-signer / counter-sign extension).
_SIGNATURE_ENVELOPE_VERSION = 1


[docs] class ReviewPackageSigningError(CadrumoError): """Base error for review-package signing/verification failures."""
[docs] class ReviewPackageSigningKeypair(BaseModel): """A profile's Ed25519 signing keypair, private key included. This model is the PLAINTEXT in-memory shape used only transiently around generation, persistence, and signing; :meth:`private_key` / :meth:`public_key` reconstruct live ``cryptography`` key objects from the stored raw hex bytes. Nothing about this model changes the secure-storage contract: the caller (:func:`ensure_review_package_signing_keypair`) is responsible for obtaining it through the required signing-keypair capability. """ model_config = _STRICT_FROZEN bucket_id: BucketId private_key_hex: str = Field(pattern=_HEX_PATTERN_64) public_key_hex: str = Field(pattern=_HEX_PATTERN_64) created_at: UtcInstant
[docs] def private_key(self) -> Ed25519PrivateKey: """Reconstruct the live :class:`Ed25519PrivateKey` from stored raw bytes.""" return ed25519_private_key_from_hex(self.private_key_hex)
[docs] def public_key(self) -> Ed25519PublicKey: """Reconstruct the live :class:`Ed25519PublicKey` from stored raw bytes.""" return ed25519_public_key_from_hex(self.public_key_hex)
[docs] class ReviewPackageSigningPublicKey(BaseModel): """The exportable, non-secret half of a profile's signing keypair. Safe to hand to a receiving accountant so they can verify a package's signature independently. Carries no secrecy requirement -- unlike :class:`ReviewPackageSigningKeypair`, this model is fine to write to a plaintext file, print, or transmit. """ model_config = _STRICT_FROZEN bucket_id: BucketId public_key_hex: str = Field(pattern=_HEX_PATTERN_64) created_at: UtcInstant
[docs] class SignedReviewPackage(BaseModel): """Signature envelope binding a review package's manifest digest to a signer. ``manifest_sha256`` is the review package's own self-attesting :attr:`~core.corpus_manifest.manifest.CorpusManifest.manifest_sha256` (recovered via :func:`~application.modelo.review_package.verify_review_package`), NOT a re-derived hash of the archive bytes: signing the manifest digest transitively covers every archived member because the manifest digest already covers every per-file checksum record. """ model_config = _STRICT_FROZEN envelope_version: int = Field(default=_SIGNATURE_ENVELOPE_VERSION, ge=1) bucket_id: BucketId calculation_revision_id: CalculationRevisionId manifest_sha256: str = Field(pattern=_HEX_PATTERN_64) signature_hex: str = Field(pattern=_HEX_PATTERN_128) public_key_hex: str = Field(pattern=_HEX_PATTERN_64) signed_at: UtcInstant
[docs] def ensure_review_package_signing_keypair( *, bucket_id: str, signing_keypair: ReviewPackageSigningKeypairCapability, generated_at: datetime | None = None, ) -> ReviewPackageSigningKeypair: """Return the profile's Ed25519 signing keypair through its capability. Args: bucket_id: The active profile bucket id this keypair is scoped to. signing_keypair: The required application-facing keypair capability. generated_at: Optional override for the keypair's ``created_at`` timestamp (tests only); defaults to the current UTC time. """ return signing_keypair.ensure_keypair(bucket_id=bucket_id, generated_at=generated_at)
[docs] def review_package_signing_public_key( keypair: ReviewPackageSigningKeypair, ) -> ReviewPackageSigningPublicKey: """Project the exportable public half out of a full keypair. The projection never touches ``private_key_hex``; the returned model is safe to hand to a receiving accountant. """ return ReviewPackageSigningPublicKey( bucket_id=keypair.bucket_id, public_key_hex=keypair.public_key_hex, created_at=keypair.created_at, )
[docs] def sign_review_package( package_path: Path, *, keypair: ReviewPackageSigningKeypair, signed_at: datetime | None = None, ) -> SignedReviewPackage: """Verify ``package_path``'s checksum manifest, then sign its digest. Delegates the integrity check entirely to :func:`~application.modelo.review_package.assert_review_package_verifies` (no hashing logic is re-derived here): a package that is not checksum-clean raises before any signature is produced, so a signature can never be minted over a package this module itself cannot vouch is intact. Args: package_path: Path to the review-package ZIP to sign. keypair: The signer's :class:`ReviewPackageSigningKeypair` (see :func:`ensure_review_package_signing_keypair`). signed_at: Optional override for the envelope's ``signed_at`` timestamp (tests only); defaults to the current UTC time. Raises: FileNotFoundError: If ``package_path`` does not exist. ReviewPackageIntegrityError: If the package fails checksum-manifest verification (propagated from :func:`~application.modelo.review_package.assert_review_package_verifies`). """ with bundled_indexed_authority().operation() as operation: manifest = assert_review_package_verifies(package_path, operation=operation) manifest_sha256 = _package_manifest_sha256(package_path) signature_hex = sign_digest_hex( private_key_hex=keypair.private_key_hex, digest_hex=manifest_sha256, ) return SignedReviewPackage( bucket_id=manifest.bucket_id, calculation_revision_id=manifest.calculation_revision_id, manifest_sha256=manifest_sha256, signature_hex=signature_hex, public_key_hex=keypair.public_key_hex, signed_at=signed_at or _utc_now(), )
[docs] def verify_review_package_signature( package_path: Path, signed_package: SignedReviewPackage, *, public_key_hex: str, ) -> bool: """Verify ``signed_package``'s signature against ``public_key_hex``. Re-runs the checksum-manifest integrity check (a fresh :func:`~core.corpus_manifest.manifest.verify_corpus_bundle` call, not a trust of ``signed_package.manifest_sha256``) first. This matters because ``manifest_sha256`` is a digest over the embedded manifest's OWN recorded per-file hashes -- it does NOT change if an archived member's bytes are swapped without touching the manifest metadata; only the ``mismatched`` check (re-hashing every archived file against its manifest record) catches that tamper. So a package whose CURRENT bytes are not checksum-clean, or whose current manifest digest no longer matches the signed digest, fails here even before the Ed25519 check runs. Args: package_path: Path to the review-package ZIP to verify. signed_package: The :class:`SignedReviewPackage` envelope produced by :func:`sign_review_package`. public_key_hex: The signer's raw public key, as 64 lowercase hex characters (see :attr:`~ReviewPackageSigningPublicKey.public_key_hex`). Passed explicitly (never read off ``signed_package``) so a verifier must supply the key it actually trusts, rather than trusting whatever key the envelope claims. Returns: ``True`` iff the package is currently checksum-clean, its manifest digest matches the signed digest, AND the Ed25519 signature verifies against ``public_key_hex``. Returns ``False`` (never raises) on any mismatch or invalid-signature outcome -- signature verification is an authenticity *check*, not an assertion; callers that want a raising assertion should call :func:`sign_review_package`'s sibling :func:`~application.modelo.review_package.assert_review_package_verifies` first and test this function's boolean themselves. """ try: result = verify_corpus_bundle(package_path) except (CorpusBundleError, CorpusManifestTamperError): return False if not result.is_clean: return False if result.manifest.manifest_sha256 != signed_package.manifest_sha256: return False return digest_signature_is_valid( public_key_hex=public_key_hex, digest_hex=signed_package.manifest_sha256, signature_hex=signed_package.signature_hex, )
def _package_manifest_sha256(package_path: Path) -> str: """Return the package's current self-attesting corpus manifest digest. Re-runs :func:`~core.corpus_manifest.manifest.verify_corpus_bundle` directly (the same checksum layer :func:`~application.modelo.review_package.verify_review_package` delegates to) so the digest reflects the archive's CURRENT bytes, never a cached value, and is available even when the caller only needs the digest rather than the full review-specific verification result. """ return verify_corpus_bundle(package_path).manifest.manifest_sha256 __all__ = [ "ReviewPackageSigningError", "ReviewPackageSigningKeypair", "ReviewPackageSigningPublicKey", "SignedReviewPackage", "ensure_review_package_signing_keypair", "review_package_signing_public_key", "sign_review_package", "verify_review_package_signature", ]