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
BaseModelwithmodel_configset 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 bymodel_dump.Parsed private-key material and the raw PKCS#12 bytes live in
pydantic.PrivateAttrfields onLoadedCertificate, so they can never be leaked viamodel_dumporrepr.All errors inherit from
core.errors.hierarchy.CadrumoErrorviaCertificateError.
See also
adapters.outbound.aeat.auth.providers.CertificateContextProvisioner
for wiring LoadedCertificate into browser contexts.
- exception CertificateError(message=None, *, context=None, translated_message=None)[source]¶
Bases:
AuthErrorBase class for every certificate-auth domain error.
Subclasses remain catchable through the shared
AuthErrorbranch while preserving certificate-specific causes for loading, password, health, and subject-identity failures.- Parameters:
- Return type:
None
- code¶
- exception CertificateLoadError(message=None, *, context=None, translated_message=None)[source]¶
Bases:
CertificateErrorRaised when
load_certificate()cannot parse PKCS#12 bytes.- Parameters:
- Return type:
None
- code¶
- exception CertificatePasswordError(message=None, *, context=None, translated_message=None)[source]¶
Bases:
CertificateErrorRaised when
CertificateBundle.passwordis empty or wrong.- Parameters:
- Return type:
None
- code¶
- exception CertificateExpiredError(message=None, *, context=None, translated_message=None)[source]¶
Bases:
CertificateErrorRaised when
load_certificate()sees an elapsednot_after.- Parameters:
- Return type:
None
- code¶
- exception CertificatePreExpiryError(message=None, *, context=None, translated_message=None)[source]¶
Bases:
CertificateErrorRaised when a certificate is within the pre-expiry danger window.
Distinct from
CertificateExpiredError(which fires afternot_afterhas elapsed): this error is raised proactively by the workflow gate and CLI surfaces when a loaded certificate’sdays_until_expiryhas 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:
- Return type:
None
- code¶
- exception CertificateNifParseError(message=None, *, context=None, translated_message=None)[source]¶
Bases:
CertificateErrorRaised 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
serialNumberRDN, 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:
- Return type:
None
- code¶
- class CertificateHealthSeverity(*values)[source]¶
Bases:
StrEnumClosed catalogue of certificate health verdicts.
Mapping from
days_until_expiryto severity is driven by thewarn_threshold_days/critical_threshold_daysfields on theCertificateHealthrecord and the sourced values incore.config.Settings.- Variables:
OK – Certificate has more than
warn_threshold_daysremaining.WARN – Within the warning window but outside the critical one.
CRITICAL – Inside the critical window but not yet expired.
EXPIRED –
not_afterhas already elapsed.
- OK¶
- WARN¶
- CRITICAL¶
- EXPIRED¶
- class CertificateBundle(**data)[source]¶
Bases:
BaseModelOperator-supplied pointer at a PKCS#12 bundle on disk.
load_certificate()turns this pointer into aLoadedCertificate.The PKCS#12 passphrase is carried directly as a
pydantic.SecretStrso callers no longer have to round-trip the secret throughos.environ. The secret is materialised only at the exact PKCS#12-decode boundary and is never logged, persisted, or serialised bymodel_dump.- Variables:
path – Filesystem path to the
.p12/.pfxbundle.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:
BaseModelA parsed, validated, in-memory PKCS#12 certificate.
adapters.outbound.aeat.auth.authenticator.AeatAuthenticatoruses this record for NIF/NIE extraction,CertificateHealthevaluation, 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.PrivateAttrfields and is therefore invisible tomodel_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.
- class CertificateHealth(**data)[source]¶
Bases:
BaseModelStructured health verdict for a PKCS#12 certificate bundle.
Computed from a loaded certificate’s
not_afteragainst a referenceevaluated_attimestamp and a pair of warning / critical thresholds sourced fromcore.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 existingLoadedCertificate;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_atandnot_after. Negative when the certificate is expired.severity –
CertificateHealthSeveritybucket.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 sameLoadedCertificatecontract.The passphrase is unwrapped from
bundle.passwordat the PKCS#12-decode boundary only. An emptySecretStrraisesCertificatePasswordErrorbefore any file I/O. On a successful load the returnedLoadedCertificatecarries the raw PKCS#12 bytes and a parsed private-key handle inPrivateAttrfields so the backends can consume them without a second on-disk round-trip.- Parameters:
bundle (
CertificateBundle) – Operator-suppliedCertificateBundle.- Return type:
- Returns:
A frozen
LoadedCertificate. Its public fields are safe to log; secret material is never serialised.- Raises:
CertificatePasswordError – Empty passphrase or wrong passphrase.
CertificateLoadError – PKCS#12 bytes cannot be parsed.
CertificateExpiredError – Certificate’s validity has elapsed.
- evaluate_loaded_certificate_health(cert, *, warn_days, critical_days, now=None)[source]¶
Compute a
CertificateHealthfrom 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.AeatAuthenticatoror operator probes, can reuse the parsed record rather than re-reading the bundle from disk.- Parameters:
cert (
LoadedCertificate) – A previously-loadedLoadedCertificate.warn_days (
int) – Warning threshold in days. Must be >critical_days.critical_days (
int) – Critical threshold in days. Must be positive.now (
datetime|None) – Optional timezone-aware reference timestamp. Defaults todatetime.now()in UTC.
- Return type:
- Returns:
A frozen
CertificateHealthrecord.- Raises:
AuthValidationError – If
critical_days <= 0orwarn_days <= critical_days.
- health(path, *, password, warn_days, critical_days, friendly_name=None, now=None)[source]¶
Load
pathand return itsCertificateHealth.Unlike
load_certificate(), this function never raises on an expired certificate — it returns aCertificateHealthrecord with severityCertificateHealthSeverity.EXPIREDinstead. Genuine load failures (empty passphrase, corrupt bytes, I/O) still raise the matchingCertificateErrorsubclass, because those are not pre-expiry conditions.- Parameters:
path (
Path) – Filesystem path to the PKCS#12 bundle.password (
SecretStr) – PKCS#12 passphrase as aSecretStr.warn_days (
int) – Warning threshold in days (seeevaluate_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:
- Returns:
A frozen
CertificateHealthrecord.- 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:
CertificateHealthProbePortTranslate certificate adapter health records for the application probe.
- evaluate(request)[source]¶
Return application-owned health facts, translating load failures.
- Return type:
- 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
serialNumberRDN (OID2.5.4.5), optionally prefixed withIDCES-. Some older bundles repeat it in the common name with the formatNAME 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:
- 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.