tiny_ca.ca_factory.factory module

factory.py

X.509 certificate factory: issues end-entity certificates, builds self-signed CA certificates, generates CRLs, and validates certificate chains.

This module contains a single public class, CertificateFactory, whose only responsibility is cryptographic construction. It never touches the filesystem, the database, or any business-logic layer.

SOLID notes

SRPCertificateFactory handles cryptographic generation only.

File persistence → BaseStorage. DB registration → BaseDB.

OCPNew X.509 extension types are added in _build_extensions without

modifying any other method.

LSP : Any ICALoader implementation can be swapped in transparently. ISP : SerialWithEncoding and CertLifetime are separate utility modules;

only the symbols needed here are imported.

DIPCA material is provided through the ICALoader Protocol injected at

construction time; the factory never instantiates a loader itself.

class tiny_ca.ca_factory.factory.CertificateFactory(ca_loader, logger=None)[source]

Bases: object

Cryptographic factory for X.509 certificates, CSRs, and CRLs.

CertificateFactory is the single source of all certificate-generation logic in the library. It accepts an ICALoader at construction time and uses the CA certificate and key it provides to sign all issued artefacts.

Responsibilities

  • Generate self-signed root CA certificates (build_self_signed_ca).

  • Issue end-entity certificates signed by the loaded CA (issue_certificate).

  • Build and sign Certificate Revocation Lists (build_crl).

  • Validate an existing certificate against the loaded CA (validate_cert).

Out of scope

  • Writing any files to disk.

  • Recording certificates in a database.

  • Business-level rules (duplicate CN detection, rotation policies, etc.).

type ca_loader:

ICALoader

param ca_loader:

Provider of the CA certificate, private key, and base Subject info. Must satisfy the ICALoader Protocol (see file_loader.py).

type ca_loader:

ICALoader

type logger:

Logger | None

param logger:

Logger for operational messages. Falls back to DEFAULT_LOGGER when None.

type logger:

Logger | None

raises TypeError:

If ca_loader does not implement the ICALoader Protocol.

__init__(ca_loader, logger=None)[source]
Parameters:
Return type:

None

async abuild_crl(revoked_certs, days_valid=1)[source]
Return type:

CertificateRevocationList

Parameters:
build_crl(revoked_certs, days_valid=1)[source]

Build and sign a Certificate Revocation List from the provided records.

Iterates over revoked_certs, adds each entry to the CRL builder, then signs the list with the CA private key. The resulting CRL is valid from the current UTC time until now + days_valid days.

Parameters:
  • revoked_certs (Generator[CertificateRecord, None, None]) – Iterable of revoked certificate records as returned by BaseDB.get_revoked_certificates. Each record must expose serial_number (castable to int) and revocation_date (a datetime object).

  • days_valid (int) – Number of days until the CRL expires and must be regenerated. Typical values are 1 (daily rotation) to 7 (weekly). Default: 1.

Returns:

The signed CRL object. The caller is responsible for persisting it to storage via BaseStorage.

Return type:

x509.CertificateRevocationList

static build_self_signed_ca(common_name='Internal CA', organization='My Company', country='UA', key_size=2048, days_valid=3650, valid_from=None, logger=None)[source]

Generate a self-signed root CA certificate and its private key.

This is a @staticmethod — it requires no loaded CA because the resulting certificate is its own issuer. It is typically called once during bootstrap to establish the trust anchor for the PKI.

The generated certificate includes: - BasicConstraints(ca=True) — marks it as a CA certificate. - KeyUsage with key_cert_sign and crl_sign set to True. - SubjectKeyIdentifier derived from the public key.

Parameters:
  • common_name (str) – Common Name (CN) for the CA Subject / Issuer fields. Default: "Internal CA".

  • organization (str) – Organization (O) field. Default: "My Company".

  • country (str) – Two-letter ISO 3166-1 alpha-2 country code (C field). Default: "UA".

  • key_size (int) – RSA key length in bits. Use 2048 for standard security or 4096 for long-lived roots. Default: 2048.

  • days_valid (int) – Validity period in calendar days. Default: 3650 (≈10 years).

  • valid_from (datetime.datetime | None) – Start of the validity period. None uses the current UTC time.

  • logger (Logger | None) – Optional logger. Falls back to DEFAULT_LOGGER.

Returns:

(certificate, private_key) — both must be persisted by the caller.

Return type:

tuple[x509.Certificate, rsa.RSAPrivateKey]

Raises:

InvalidRangeTimeCertificate – If the computed expiry date is already in the past.

cosign_certificate(cert, days_valid=None, valid_from=None)[source]

Re-sign an existing certificate with this CA’s key and certificate.

Creates a new x509.Certificate that preserves the original Subject, public key, and all v3 extensions, but replaces:

  • Issuer — set to this CA’s Subject.

  • AuthorityKeyIdentifier — updated to reflect this CA’s SKI.

  • Serial number — a fresh serial is generated so the co-signed certificate is distinguishable from the original in CRLs and logs.

  • Validity window — optionally overridden via days_valid and valid_from; when both are None the original window is preserved exactly.

The certificate is signed with SHA-256 using self._ca.ca_key.

Note

This operation does not verify that the original certificate was valid or trusted before co-signing. Call validate_cert() first if pre-validation is required.

Parameters:
  • cert (x509.Certificate) – The source certificate whose Subject, public key, and extensions are copied into the co-signed output.

  • days_valid (int | None) – Override the validity duration in calendar days, counted from valid_from (or now when valid_from is also None). None preserves the original not_valid_before / not_valid_after window unchanged.

  • valid_from (datetime.datetime | None) – Override the start of the validity window. Ignored when days_valid is None. None + days_valid set → uses the current UTC time as the start.

Returns:

A new certificate object identical in content to cert except for the issuer, AKI, serial number, and (optionally) validity window. Must be persisted by the caller.

Return type:

x509.Certificate

Raises:

InvalidRangeTimeCertificate – If days_valid is provided and the computed expiry is already in the past.

Examples

>>> cosigned = factory.cosign_certificate(third_party_cert, days_valid=365)
>>> assert cosigned.issuer == factory._ca.ca_cert.subject
>>> assert cosigned.subject == third_party_cert.subject
export_pkcs12(cert, private_key, password=None, name=None)[source]

Pack cert and private_key into a PKCS#12 (PFX) bundle.

PKCS#12 is the standard container format accepted by Windows certificate stores, macOS Keychain, Java keystores, and most browser import dialogs. The CA certificate is automatically included as the issuer in the chain.

Parameters:
  • cert (x509.Certificate) – The leaf certificate to export.

  • private_key (rsa.RSAPrivateKey) – The private key corresponding to cert’s public key.

  • password (bytes | None) – Optional password to encrypt the PKCS#12 file. None produces an unencrypted bundle (not recommended for production).

  • name (str | None) – Friendly name (alias) embedded in the PKCS#12 bag. Defaults to the certificate’s Common Name when None.

Returns:

Raw DER-encoded PKCS#12 bytes. Write to a .p12 or .pfx file, or send as an HTTP response with Content-Type: application/x-pkcs12.

Return type:

bytes

get_cert_chain(cert)[source]

Return the full certificate chain from cert up to the CA root.

For a single-level PKI (leaf → root CA) this returns [cert, ca_cert]. The list is ordered leaf-first, root-last — the same order expected by nginx ssl_certificate, envoy tls_certificates, and the fullchain.pem convention used by Let’s Encrypt.

Parameters:

cert (x509.Certificate) – The leaf (or intermediate) certificate to start the chain from.

Returns:

[cert, self._ca.ca_cert] — leaf first, CA root last.

Return type:

list[x509.Certificate]

static inspect_certificate(cert)[source]

Extract and return a structured, human-readable summary of cert.

Parses every commonly-used X.509 v3 extension and Subject attribute into plain Python values wrapped in a CertificateDetails dataclass. The method never performs cryptographic verification — use validate_cert() for that. It is therefore safe to call on certificates from any issuer.

Parameters:

cert (x509.Certificate) – The certificate to inspect. May have been issued by this CA or by a completely different PKI.

Returns:

A frozen dataclass with the following fields populated:

  • serial_number — raw integer serial.

  • common_name / organization / country — first matching Subject attribute, or None when absent.

  • issuer_cn — CN from the Issuer field, or None.

  • not_valid_before / not_valid_after — UTC datetimes.

  • is_caTrue when BasicConstraints.ca is True.

  • san_dns / san_ip — lists from the SAN extension.

  • key_usage — list of enabled KeyUsage bit names.

  • extended_key_usage — list of EKU OID dotted strings.

  • fingerprint_sha256 — colon-separated uppercase hex.

  • subject_key_identifier — hex string or None.

  • public_key_size — RSA key bits or None.

Return type:

CertificateDetails

Examples

>>> details = CertificateFactory.inspect_certificate(cert)
>>> print(details.common_name)
'nginx.internal'
>>> print(details.is_ca)
False
>>> print(details.fingerprint_sha256[:8])
'AB:CD:EF'
issue_certificate(common_name, serial_type=CertType.SERVICE, key_size=2048, days_valid=365, valid_from=None, email=None, is_server_cert=False, is_client_cert=False, san_dns=None, san_ip=None)[source]

Issue a signed end-entity certificate for the given subject parameters.

Workflow: 1. Generate a fresh RSA key pair. 2. Build the Subject x509.Name from CA base info + common_name / email. 3. Create a CSR signed with the new private key. 4. Assemble X.509 extensions (KeyUsage, EKU, SAN, SKI, AKI). 5. Sign the certificate with the CA key from self._ca.

The Subject inherits country and organization from the CA’s own certificate so that all issued certificates share a consistent issuer hierarchy.

Parameters:
  • common_name (str) – Common Name (CN) for the new certificate’s Subject.

  • serial_type (CertType) – Certificate category used when encoding the serial number. Default: CertType.SERVICE.

  • key_size (int) – RSA key length in bits. Default: 2048.

  • days_valid (int) – Validity period in calendar days. Default: 365.

  • valid_from (datetime.datetime | None) – Start of the validity period. None uses the current UTC time.

  • email (str | None) – Optional email address added as an emailAddress Subject attribute.

  • is_server_cert (bool) – When True, adds ServerAuth to the Extended Key Usage extension and includes common_name as a DNS SAN (RFC 2818 compliance).

  • is_client_cert (bool) – When True, adds ClientAuth to the Extended Key Usage extension.

  • san_dns (list[str] | None) – Additional DNS names for the Subject Alternative Name extension.

  • san_ip (list[str] | None) – IP addresses (as strings) for the Subject Alternative Name extension.

Returns:

(certificate, private_key, csr) — the certificate and key must be persisted by the caller; the CSR is returned for audit purposes.

Return type:

tuple[x509.Certificate, rsa.RSAPrivateKey, x509.CertificateSigningRequest]

Raises:

InvalidRangeTimeCertificate – If the computed expiry date is already in the past.

issue_intermediate_ca(common_name, key_size=4096, days_valid=1825, valid_from=None, path_length=0, organization=None, country=None)[source]

Issue a subordinate (intermediate) CA certificate signed by this CA.

The resulting certificate has BasicConstraints(ca=True) and KeyUsage(key_cert_sign=True, crl_sign=True) so it can in turn sign leaf certificates. The path_length constraint limits how deep the sub-hierarchy can go.

Parameters:
  • common_name (str) – CN for the intermediate CA Subject.

  • key_size (int) – RSA key size for the intermediate CA key. Defaults to 4096 (recommended for long-lived CA keys).

  • days_valid (int) – Validity in calendar days. Defaults to 1825 (5 years).

  • valid_from (datetime.datetime | None) – Start of the validity window. None uses the current UTC time.

  • path_length (int | None) – BasicConstraints.path_length value. 0 means this intermediate can only sign leaf certificates (cannot create further sub-CAs). None means unlimited sub-levels.

  • organization (str | None) – O field for the intermediate CA Subject. Falls back to the parent CA’s organization when None.

  • country (str | None) – C field. Falls back to the parent CA’s country when None.

Returns:

(intermediate_ca_cert, intermediate_ca_key).

Return type:

tuple[x509.Certificate, rsa.RSAPrivateKey]

Raises:

InvalidRangeTimeCertificate – If the computed expiry is already in the past.

renew_certificate(cert, days_valid=365, valid_from=None)[source]

Issue a renewal of cert with a fresh validity window but the same Subject, public key, and extensions.

Unlike rotate_certificate() (which generates a new key pair), renewal re-uses the existing public key. This is appropriate when the private key has not been compromised and the owner simply needs to extend the validity period.

The renewed certificate receives a new serial number generated by SerialWithEncoding so it is distinguishable from the original in CRLs and audit logs.

Parameters:
  • cert (x509.Certificate) – The certificate to renew. Its Subject, public key, and all v3 extensions (except AKI, which is updated to point to the current CA) are copied verbatim into the renewal.

  • days_valid (int) – Number of days the renewed certificate should be valid. Default: 365.

  • valid_from (datetime.datetime | None) – Start of the new validity window. None uses the current UTC time.

Returns:

A freshly signed certificate with the same identity but a new validity window and serial number.

Return type:

x509.Certificate

Raises:

InvalidRangeTimeCertificate – If the computed expiry is already in the past.

validate_cert(cert)[source]

Verify that cert was issued by this CA, is within its validity window, and carries a cryptographically correct signature.

Three checks are performed in order: 1. Issuer matchcert.issuer must equal the CA’s Subject. 2. Validity window — current UTC time must be between

cert.not_valid_before_utc and cert.not_valid_after_utc.

  1. Signature — the CA public key is used to verify the certificate signature using PKCS#1 v1.5 with the algorithm declared in the cert.

Parameters:

cert (x509.Certificate) – The certificate to validate.

Returns:

Returns silently when all checks pass.

Return type:

None

Raises:

ValidationCertError – If any of the three checks fails. The message describes which check failed and includes the relevant values (timestamps, issuer).

verify_crl(crl)[source]

Verify the signature and validity window of crl.

Checks that: 1. The CRL was signed by this CA’s private key (issuer match + signature). 2. The CRL’s nextUpdate timestamp has not yet passed — i.e. the CRL

is still within its declared validity window.

Parameters:

crl (x509.CertificateRevocationList) – The CRL object to verify.

Returns:

Returns silently when all checks pass.

Return type:

None

Raises:

ValidationCertError – If the CRL issuer does not match this CA, the signature is invalid, or the CRL has expired (nextUpdate is in the past).

Parameters:
  • ca_loader (ICALoader)

  • logger (Logger | None)