cadrumo.adapters.outbound.aeat.auth.certificate module

PKCS#12 client-certificate records and checks for AEAT Sede Electrónica.

This module is the public surface for loading and evaluating certificates used for authentication against the Spanish tax authority’s Sede Electrónica.

adapters.outbound.aeat.auth.authenticator.AeatAuthenticator consumes this surface by loading a CertificateBundle into a LoadedCertificate, recording CertificateHealth, and deriving the taxpayer NIF/NIE through extract_nif_from_subject().

Design constraints:

  • All boundary records are pydantic v2 BaseModel with model_config set to the shared strict, frozen project config.

  • Cert passphrases are pydantic.SecretStr. The secret value is materialised only at the PKCS#12 or Playwright context boundary and is never logged, persisted, or serialised by model_dump.

  • Parsed private-key material and the raw PKCS#12 bytes live in pydantic.PrivateAttr fields on LoadedCertificate, so they can never be leaked via model_dump or repr.

  • All errors inherit from core.errors.hierarchy.CadrumoError via CertificateError.

exception CertificateError(message=None, *, context=None, translated_message=None)[source]

Bases: AuthError

Base class for every certificate-auth domain error.

Subclasses remain catchable through the shared AuthError branch while preserving certificate-specific causes for loading, password, health, and subject-identity failures.

Parameters:
  • message (str | None)

  • context (Mapping[str, object] | None)

  • translated_message (str | None)

Return type:

None

code
exception CertificateLoadError(message=None, *, context=None, translated_message=None)[source]

Bases: CertificateError

Raised when load_certificate() cannot parse PKCS#12 bytes.

Parameters:
  • message (str | None)

  • context (Mapping[str, object] | None)

  • translated_message (str | None)

Return type:

None

code
exception CertificatePasswordError(message=None, *, context=None, translated_message=None)[source]

Bases: CertificateError

Raised when CertificateBundle.password is empty or wrong.

Parameters:
  • message (str | None)

  • context (Mapping[str, object] | None)

  • translated_message (str | None)

Return type:

None

code
exception CertificateExpiredError(message=None, *, context=None, translated_message=None)[source]

Bases: CertificateError

Raised when load_certificate() sees an elapsed not_after.

Parameters:
  • message (str | None)

  • context (Mapping[str, object] | None)

  • translated_message (str | None)

Return type:

None

code
exception CertificatePreExpiryError(message=None, *, context=None, translated_message=None)[source]

Bases: CertificateError

Raised when a certificate is within the pre-expiry danger window.

Distinct from CertificateExpiredError (which fires after not_after has elapsed): this error is raised proactively by the workflow gate and CLI surfaces when a loaded certificate’s days_until_expiry has fallen below the configured critical threshold, before the bundle becomes technically unusable. Callers may suppress it via an explicit override flag on the narrow programmatic surfaces that still support certificate probes.

Parameters:
  • message (str | None)

  • context (Mapping[str, object] | None)

  • translated_message (str | None)

Return type:

None

code
exception CertificateNifParseError(message=None, *, context=None, translated_message=None)[source]

Bases: CertificateError

Raised when no NIF / NIE can be parsed from a certificate subject.

The project’s authenticator derives the taxpayer NIF from the FNMT certificate subject (canonical source: the serialNumber RDN, OID 2.5.4.5). Certificates that carry no such attribute, that use a CIF (legal-entity) shape, or whose CN/serialNumber lacks a registry-recognised persona-física identifier produce this error. Callers MUST propagate it rather than guess the identifier from other fields.

Parameters:
  • message (str | None)

  • context (Mapping[str, object] | None)

  • translated_message (str | None)

Return type:

None

code
class CertificateHealthSeverity(*values)[source]

Bases: StrEnum

Closed catalogue of certificate health verdicts.

Mapping from days_until_expiry to severity is driven by the warn_threshold_days / critical_threshold_days fields on the CertificateHealth record and the sourced values in core.config.Settings.

Variables:
  • OK – Certificate has more than warn_threshold_days remaining.

  • WARN – Within the warning window but outside the critical one.

  • CRITICAL – Inside the critical window but not yet expired.

  • EXPIRED – not_after has already elapsed.

OK
WARN
CRITICAL
EXPIRED
class CertificateBundle(**data)[source]

Bases: BaseModel

Operator-supplied pointer at a PKCS#12 bundle on disk.

load_certificate() turns this pointer into a LoadedCertificate.

The PKCS#12 passphrase is carried directly as a pydantic.SecretStr so callers no longer have to round-trip the secret through os.environ. The secret is materialised only at the exact PKCS#12-decode boundary and is never logged, persisted, or serialised by model_dump.

Variables:
  • path – Filesystem path to the .p12 / .pfx bundle.

  • password – PKCS#12 passphrase as a SecretStr.

  • friendly_name – Optional human-readable label for logs.

Parameters:

data (Any)

path: Path
password: SecretStr
friendly_name: str | None
class LoadedCertificate(**data)[source]

Bases: BaseModel

A parsed, validated, in-memory PKCS#12 certificate.

adapters.outbound.aeat.auth.authenticator.AeatAuthenticator uses this record for NIF/NIE extraction, CertificateHealth evaluation, and browser-context provisioning.

Public fields are safe to log and serialise. Secret material (raw PKCS#12 bytes, parsed private key, passphrase) lives in pydantic.PrivateAttr fields and is therefore invisible to model_dump, model_dump_json, and the overridden __repr__().

Variables:
  • subject – X.509 subject distinguished name.

  • issuer – X.509 issuer distinguished name.

  • not_before – Validity start (timezone-aware UTC).

  • not_after – Validity end (timezone-aware UTC).

  • serial_number – Hex-encoded serial number.

  • sha256_thumbprint – Hex-encoded SHA-256 fingerprint of the DER encoding.

  • source_path – Path the bundle was loaded from.

  • friendly_name – Optional label propagated from the bundle.

Parameters:

data (Any)

subject: str
issuer: str
not_before: datetime
not_after: datetime
serial_number: str
sha256_thumbprint: str
source_path: Path
friendly_name: str | None
client_certificate_material()[source]

Return the PKCS#12 bytes and passphrase for a client-TLS handshake.

The bundle and its passphrase are held as private attributes so they never reach a model dump, log line, or serialized payload. This is the one authorized read: the browser context binds them to the AEAT origin for mutual TLS and nothing else. Callers must not store or forward the returned material.

Return type:

tuple[bytes, str]

is_expired(now=None)[source]

Return True if the certificate’s validity has elapsed.

Parameters:

now (datetime | None) – Timezone-aware reference time. Defaults to datetime.now() in UTC.

Return type:

bool

Returns:

True when the certificate has expired relative to now.

class CertificateHealth(**data)[source]

Bases: BaseModel

Structured health verdict for a PKCS#12 certificate bundle.

Computed from a loaded certificate’s not_after against a reference evaluated_at timestamp and a pair of warning / critical thresholds sourced from core.config.Settings. The record never carries any secret material; it is safe to log, persist, or surface to the CLI. evaluate_loaded_certificate_health() computes this from an existing LoadedCertificate; health() computes it from a bundle path while preserving the expired-certificate reporting path.

Variables:
  • subject – RFC-4514 subject DN.

  • issuer – RFC-4514 issuer DN.

  • serial_number – Hex-encoded serial number.

  • not_before – Timezone-aware validity start.

  • not_after – Timezone-aware validity end.

  • days_until_expiry – Whole days between evaluated_at and not_after. Negative when the certificate is expired.

  • severity – CertificateHealthSeverity bucket.

  • warn_threshold_days – The WARN cut-off that produced this verdict.

  • critical_threshold_days – The CRITICAL cut-off that produced this verdict.

  • evaluated_at – Timezone-aware reference timestamp.

Parameters:

data (Any)

subject: str
issuer: str
serial_number: str
not_before: datetime
not_after: datetime
days_until_expiry: int
severity: CertificateHealthSeverity
warn_threshold_days: int
critical_threshold_days: int
evaluated_at: datetime
load_certificate(bundle)[source]

Load and validate a PKCS#12 bundle from disk.

This is the canonical decode path for certificate auth. It feeds adapters.outbound.aeat.auth.authenticator.AeatAuthenticator, operator probes, and backend provisioning surfaces with the same LoadedCertificate contract.

The passphrase is unwrapped from bundle.password at the PKCS#12-decode boundary only. An empty SecretStr raises CertificatePasswordError before any file I/O. On a successful load the returned LoadedCertificate carries the raw PKCS#12 bytes and a parsed private-key handle in PrivateAttr fields so the backends can consume them without a second on-disk round-trip.

Parameters:

bundle (CertificateBundle) – Operator-supplied CertificateBundle.

Return type:

LoadedCertificate

Returns:

A frozen LoadedCertificate. Its public fields are safe to log; secret material is never serialised.

Raises:
evaluate_loaded_certificate_health(cert, *, warn_days, critical_days, now=None)[source]

Compute a CertificateHealth from an already-loaded cert.

The helper exists so callers that have already paid the PKCS#12 decode cost, such as adapters.outbound.aeat.auth.authenticator.AeatAuthenticator or operator probes, can reuse the parsed record rather than re-reading the bundle from disk.

Parameters:
Return type:

CertificateHealth

Returns:

A frozen CertificateHealth record.

Raises:

AuthValidationError – If critical_days <= 0 or warn_days <= critical_days.

health(path, *, password, warn_days, critical_days, friendly_name=None, now=None)[source]

Load path and return its CertificateHealth.

Unlike load_certificate(), this function never raises on an expired certificate — it returns a CertificateHealth record with severity CertificateHealthSeverity.EXPIRED instead. Genuine load failures (empty passphrase, corrupt bytes, I/O) still raise the matching CertificateError subclass, because those are not pre-expiry conditions.

Parameters:
  • path (Path) – Filesystem path to the PKCS#12 bundle.

  • password (SecretStr) – PKCS#12 passphrase as a SecretStr.

  • warn_days (int) – Warning threshold in days (see evaluate_loaded_certificate_health()).

  • critical_days (int) – Critical threshold in days.

  • friendly_name (str | None) – Optional label propagated to the bundle.

  • now (datetime | None) – Optional reference time, for deterministic tests.

Return type:

CertificateHealth

Returns:

A frozen CertificateHealth record.

Raises:
  • CertificateExpiredError – When the certificate has expired and the raw bytes cannot be re-decoded for the health report.

  • CertificateLoadError – When the PKCS#12 bytes cannot be re-decoded for an expired-cert health report.

class CertificateHealthProbeAdapter(*args, **kwargs)[source]

Bases: CertificateHealthProbePort

Translate certificate adapter health records for the application probe.

evaluate(request)[source]

Return application-owned health facts, translating load failures.

Return type:

CertificateHealthObservation | CertificateHealthFailure

Parameters:

request (CertificateHealthProbeRequest)

extract_nif_from_subject(cert)[source]

Return the FNMT taxpayer identifier encoded in cert’s subject.

FNMT persona física certificates carry the subject’s NIF or NIE in the serialNumber RDN (OID 2.5.4.5), optionally prefixed with IDCES-. Some older bundles repeat it in the common name with the format NAME SURNAME - NNNNNNNNL.

Uses cryptography.x509.Name.from_rfc4514_string() to parse the subject so that RFC 4514 escape sequences (\\,, \\+, etc.) and multi-valued RDNs are handled correctly.

Parameters:

cert (LoadedCertificate) – The loaded PKCS#12 certificate.

Return type:

str

Returns:

The uppercase normalised NIF/NIE (e.g. "12345678Z" or "X1234567L").

Raises:

CertificateNifParseError – When the subject contains no recognisable DNI / NIE identifier — including one whose checksum letter fails — or when the value present is a CIF (legal-entity). Certificate auth here accepts individual taxpayer certificates and rejects organization certificates rather than guessing an identity.