aeat.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 certificate-based authentication
against the Spanish tax authority’s Sede Electrónica. Callers import
exclusively from adapters.outbound.aeat.auth; the backend implementations live in
the private adapters.outbound.aeat.auth._certificate_backends package.
adapters.outbound.aeat.auth.AeatAuthenticator consumes this
surface by loading a CertificateBundle into a LoadedCertificate,
recording CertificateHealth, deriving the taxpayer NIF/NIE through
extract_nif_from_subject(), and storing HandshakeResult evidence
in certificate-backed sessions.
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 exact TLS-handshake 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.AeatErrorviaCertificateError.
See also
adapters.outbound.aeat.auth.CertificateContextProvisioner
for wiring LoadedCertificate into browser contexts, and
adapters.outbound.aeat.auth.describe_certificate_provider()
for the provider summary built from CertificateHealth.
- exception CertificateError(message=None, *, context=None, suggestion=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, handshake, and subject-identity failures.- Parameters:
- Return type:
None
- code: ClassVar[ErrorCode]¶
- exception CertificateLoadError(message=None, *, context=None, suggestion=None, translated_message=None)[source]¶
Bases:
CertificateErrorRaised when
load_certificate()cannot parse PKCS#12 bytes.- Parameters:
- Return type:
None
- code: ClassVar[ErrorCode]¶
- exception CertificatePasswordError(message=None, *, context=None, suggestion=None, translated_message=None)[source]¶
Bases:
CertificateErrorRaised when
CertificateBundle.passwordis empty or wrong.- Parameters:
- Return type:
None
- code: ClassVar[ErrorCode]¶
- exception CertificateExpiredError(message=None, *, context=None, suggestion=None, translated_message=None)[source]¶
Bases:
CertificateErrorRaised when
load_certificate()sees an elapsednot_after.- Parameters:
- Return type:
None
- code: ClassVar[ErrorCode]¶
- exception CertificatePreExpiryError(message=None, *, context=None, suggestion=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: ClassVar[ErrorCode]¶
- exception CertificateHandshakeError(message=None, *, context=None, suggestion=None, translated_message=None)[source]¶
Bases:
CertificateErrorRaised when handshake input is structurally invalid.
TLS failures encountered during
verify_handshake()are returned asHandshakeResult(success=False, ...)rather than raised; this exception is reserved for cases where the caller passed nonsense (e.g. an empty URL).- Parameters:
- Return type:
None
- code: ClassVar[ErrorCode]¶
- exception CertificateNifParseError(message=None, *, context=None, suggestion=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 recognisable DNI ([0-9]{7,8}[A-Z]) or NIE ([XYZ][0-9]{7}[A-Z]) identifier produce this error. Callers MUST propagate it rather than guess the identifier from other fields.- Parameters:
- Return type:
None
- code: ClassVar[ErrorCode]¶
- 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 selectedCertificateBackenddetermines which private backend later consumes the loaded certificate.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.
backend – Which backend should consume this bundle.
- Parameters:
path (Path)
password (SecretStr)
friendly_name (str | None)
backend (CertificateBackend)
- path: Path¶
- password: SecretStr¶
- friendly_name: str | None¶
- backend: CertificateBackend¶
- class LoadedCertificate(**data)[source]¶
Bases:
BaseModelA parsed, validated, in-memory PKCS#12 certificate.
adapters.outbound.aeat.auth.AeatAuthenticatoruses this record for NIF/NIE extraction,CertificateHealthevaluation,HandshakeResultcreation, 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.
backend – Backend this cert should be handed to.
- Parameters:
- subject: str¶
- issuer: str¶
- not_before: datetime¶
- not_after: datetime¶
- serial_number: str¶
- sha256_thumbprint: str¶
- source_path: Path¶
- friendly_name: str | None¶
- backend: CertificateBackend¶
- 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:
- 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¶
- class HandshakeResult(**data)[source]¶
Bases:
BaseModelStructured outcome of a
verify_handshake()attempt.Successful certificate sessions persist this result inside
adapters.outbound.aeat.auth._authenticator_persistence.PersistedSessionMetadataso resumed sessions can preserve the original mTLS probe evidence.- Variables:
success – Whether the TLS handshake completed successfully.
status_code – HTTP status returned by the verify URL (0 if the handshake failed before any HTTP response was observed).
server_cert_chain – Tuple of subject DNs from the server-presented chain, outermost leaf first. Empty on failure.
elapsed_ms – Wall-clock elapsed time in milliseconds.
attempted_at – Timezone-aware UTC timestamp of the attempt.
error_message – Human-readable failure reason when
success=False.
- Parameters:
- success: bool¶
- status_code: int¶
- server_cert_chain: tuple[str, ...]¶
- elapsed_ms: int¶
- attempted_at: datetime¶
- error_message: str | None¶
- 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.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.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, backend=CertificateBackend.PLAYWRIGHT_CONTEXT, 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.backend (
CertificateBackend) – Backend the bundle belongs to (defaultPLAYWRIGHT_CONTEXT).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.
- 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, 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.
- preload_into_browser_context(cert, context)[source]¶
Validate that
contextwas constructed withcert.Per Playwright’s API, per-context client certificates must be supplied at
playwright.async_api.Browser.new_context()time; there is no post-hoc injection hook. This function therefore validates the contract rather than mutatingcontext. It is the integration hook used byadapters.outbound.aeat.auth.CertificateContextProvisionerafter it passes aCertificateBundle-derived certificate through tonew_context.- Parameters:
cert (
LoadedCertificate) – TheLoadedCertificateto verify againstcontext.context (
object) – A PlaywrightBrowserContextduck-typed via_BrowserContextLike.
- Return type:
- verify_handshake(cert, url)[source]¶
Perform an opt-in TLS handshake smoke test.
adapters.outbound.aeat.auth.AeatAuthenticatorcalls this before constructing a certificate-backedAeatSession. Backend implementations own the actual transport behavior.Dispatches to the backend selected by
cert.backend. TLS failures are returned asHandshakeResultwithsuccess=Falseso callers can record them in health-check reports without catching exceptions. Only structurally invalid input raisesCertificateHandshakeError.- Parameters:
cert (
LoadedCertificate) – The loaded certificate to present.url (
str) – Fully-qualified target URL (must include scheme + host).
- Return type:
- Returns:
A frozen
HandshakeResult.- Raises:
CertificateHandshakeError – When
urlis empty or malformed.