tiny_ca.ca_factory package¶
- class tiny_ca.ca_factory.CAFileLoader(ca_cert_path, ca_key_path, ca_key_password=None, logger=None)[source]¶
Bases:
objectLoads a CA certificate and private key from PEM files on the local filesystem.
Responsibility: file reading and PEM deserialisation only. Does not generate certificates, manage sessions, or perform any cryptographic operations beyond deserialisation.
On construction the loader: 1. Validates that both paths point to existing, regular files with
permitted extensions (see
ALLOWED_CERT_EXTENSIONS).Deserialises the CA certificate and private key from PEM.
Extracts
CertificateInfofrom the CA certificate’s Subject.
After successful construction all three
ICALoaderproperties are available and will not change for the lifetime of the instance.- Parameters:
ca_cert_path (str | Path) – Path to the PEM-encoded CA certificate file.
ca_key_path (str | Path) – Path to the PEM-encoded CA private key file.
ca_key_password (str | bytes | None) – Optional password protecting the private key. A
strvalue is encoded tobytesusing UTF-8 before being passed to the cryptography library.Nonemeans the key is unencrypted.logger (Logger | None) – Logger instance for diagnostic messages. Falls back to
DEFAULT_LOGGERwhenNone.
- property base_info: CertificateInfo¶
Structured metadata extracted from the CA certificate Subject.
- Returns:
Contains organization, organizational_unit, country, state, and locality fields; any absent attribute is
None.- Return type:
- property ca_cert: Certificate¶
The deserialized CA certificate.
- Returns:
The CA certificate loaded from ca_cert_path.
- Return type:
x509.Certificate
- property ca_key: RSAPrivateKey¶
The deserialized CA private key.
- Returns:
The private key loaded from ca_key_path.
- Return type:
rsa.RSAPrivateKey
- class tiny_ca.ca_factory.CertLifetime[source]¶
Bases:
objectStateless helper that computes and inspects X.509 certificate validity windows.
All operations are pure functions (no side effects, no shared state) and are therefore safe to call from multiple threads simultaneously.
Use this class to: - Compute a
(not_before, not_after)pair for a new certificate. - Extract thenot_valid_after/not_valid_beforetimestamps from anexisting certificate as timezone-aware UTC
datetimeobjects.- static compute(valid_from=None, days_valid=365)[source]¶
Calculate the
(not_before, not_after)validity interval for a new certificate.If valid_from is
Nonethe current UTC time is used as the start of the interval. The end of the interval is valid_from plus days_valid calendar days.The result is validated to ensure the computed end date has not already passed (which would produce an immediately-invalid certificate).
- Parameters:
valid_from (datetime | None) – Start of the validity period as a timezone-aware
datetime. PassNoneto usedatetime.now(timezone.utc)automatically.days_valid (int) – Number of calendar days the certificate should remain valid. Default:
365(one year).
- Returns:
(not_before, not_after)both expressed in UTC withtzinfo=timezone.utc.- Return type:
tuple[datetime, datetime]
- Raises:
InvalidRangeTimeCertificate – If the computed not_after is earlier than the current UTC time, meaning the certificate would be expired immediately upon issuance.
Examples
>>> start, end = CertLifetime.compute(days_valid=90) >>> assert (end - start).days == 90
- async static compute_async(valid_from=None, days_valid=365)[source]¶
Async version of
compute().Configures the calculations in the thread pool so as not to block the event loop.
- Parameters:
valid_from (datetime | None)
hour. (The beginning of the window of action (UTC). None → exact UTC)
days_valid (int)
instructions (Calendar days are trivial. For)
- Return type:
- Returns:
tuple[datetime, datetime]
(not_before, not_after)in UTC.
- Raises:
The date of completion was calculated as already in the past. –
Examples
>>> start, end = await CertLifetime.compute_async(days_valid=90) >>> assert (end - start).days == 90
- static normalize_dt(dt)[source]¶
Ensure dt is a timezone-aware UTC
datetime.SQLAlchemy’s
DateTimecolumn stores naive datetimes (notzinfo). This helper centralises the normalisation so that lifecycle managers never duplicate theif dt.tzinfo is Noneguard inline.- Parameters:
dt (datetime) – Any
datetimeobject, aware or naive.- Returns:
The same instant expressed as a UTC-aware
datetime. If dt already carriestzinfo, it is returned unchanged. If dt is naive it is assumed to represent UTC andtzinfois attached via.replace(tzinfo=UTC).- Return type:
datetime
Examples
>>> naive = datetime(2025, 1, 1, 12, 0, 0) >>> CertLifetime.normalize_dt(naive).tzinfo is UTC True
- static valid_from(cert)[source]¶
Return the activation timestamp of cert as a timezone-aware UTC datetime.
Wraps
cert.not_valid_before_utcand ensures the returned value always carriestzinfo=timezone.utcfor safe comparison with other aware datetimes.- Parameters:
cert (x509.Certificate) – The certificate whose activation date should be read.
- Returns:
cert.not_valid_before_utcwithtzinfoexplicitly set totimezone.utc.- Return type:
datetime
- async static valid_from_async(cert)[source]¶
Async version
valid_from().- Parameters:
cert (x509.Certificate)
certificate (The)
read. (the date of the beginning of each one needs to be)
- Return type:
- Returns:
datetime
cert.not_valid_before_utcwithtzinfo=UTC.
- static valid_to(cert)[source]¶
Return the expiry timestamp of cert as a timezone-aware UTC datetime.
Wraps
cert.not_valid_after_utcand ensures the returned value always carriestzinfo=timezone.utcfor safe comparison with other aware datetimes.- Parameters:
cert (x509.Certificate) – The certificate whose expiry date should be read.
- Returns:
cert.not_valid_after_utcwithtzinfoexplicitly set totimezone.utc.- Return type:
datetime
- async static valid_to_async(cert)[source]¶
Async version
valid_to().- Parameters:
cert (x509.Certificate)
read. (The certificate and the date of completion must be)
- Return type:
- Returns:
datetime
cert.not_valid_after_utcwithtzinfo=UTC.
- class tiny_ca.ca_factory.CertificateFactory(ca_loader, logger=None)[source]¶
Bases:
objectCryptographic factory for X.509 certificates, CSRs, and CRLs.
CertificateFactoryis the single source of all certificate-generation logic in the library. It accepts anICALoaderat 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:
- param ca_loader:
Provider of the CA certificate, private key, and base Subject info. Must satisfy the
ICALoaderProtocol (seefile_loader.py).- type ca_loader:
ICALoader
- type logger:
- param logger:
Logger for operational messages. Falls back to
DEFAULT_LOGGERwhenNone.- type logger:
Logger | None
- raises TypeError:
If ca_loader does not implement the
ICALoaderProtocol.
- 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_validdays.- Parameters:
revoked_certs (Generator[CertificateRecord, None, None]) – Iterable of revoked certificate records as returned by
BaseDB.get_revoked_certificates. Each record must exposeserial_number(castable toint) andrevocation_date(adatetimeobject).days_valid (int) – Number of days until the CRL expires and must be regenerated. Typical values are
1(daily rotation) to7(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. -KeyUsagewithkey_cert_signandcrl_signset toTrue. -SubjectKeyIdentifierderived 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
2048for standard security or4096for 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.
Noneuses 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.Certificatethat 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
Nonethe 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
nowwhen valid_from is alsoNone).Nonepreserves the originalnot_valid_before/not_valid_afterwindow 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.
Noneproduces 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
.p12or.pfxfile, or send as an HTTP response withContent-Type: application/x-pkcs12.- Return type:
- 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 nginxssl_certificate, envoytls_certificates, and thefullchain.pemconvention 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
CertificateDetailsdataclass. The method never performs cryptographic verification — usevalidate_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, orNonewhen absent.issuer_cn— CN from the Issuer field, orNone.not_valid_before/not_valid_after— UTC datetimes.is_ca—TruewhenBasicConstraints.caisTrue.san_dns/san_ip— lists from the SAN extension.key_usage— list of enabledKeyUsagebit names.extended_key_usage— list of EKU OID dotted strings.fingerprint_sha256— colon-separated uppercase hex.subject_key_identifier— hex string orNone.public_key_size— RSA key bits orNone.
- Return type:
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.Namefrom 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 fromself._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.
Noneuses the current UTC time.email (str | None) – Optional email address added as an
emailAddressSubject attribute.is_server_cert (bool) – When
True, addsServerAuthto the Extended Key Usage extension and includes common_name as a DNS SAN (RFC 2818 compliance).is_client_cert (bool) – When
True, addsClientAuthto 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)andKeyUsage(key_cert_sign=True, crl_sign=True)so it can in turn sign leaf certificates. Thepath_lengthconstraint 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.
Noneuses the current UTC time.path_length (int | None) –
BasicConstraints.path_lengthvalue.0means this intermediate can only sign leaf certificates (cannot create further sub-CAs).Nonemeans 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
SerialWithEncodingso 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.
Noneuses 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 match —
cert.issuermust equal the CA’s Subject. 2. Validity window — current UTC time must be betweencert.not_valid_before_utcandcert.not_valid_after_utc.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
nextUpdatetimestamp has not yet passed — i.e. the CRLis 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 (
nextUpdateis in the past).
- Parameters:
ca_loader (ICALoader)
logger (Logger | None)
- class tiny_ca.ca_factory.ICALoader(*args, **kwargs)[source]¶
Bases:
ProtocolProtocol that defines the minimum contract for CA-material providers.
Any object that exposes the three properties below satisfies this Protocol and can be injected into
CertificateFactorywithout any inheritance. This makes it trivial to substitute the real filesystem loader with an in-memory stub, an HSM-backed loader, or a mock in unit tests.Properties¶
- ca_certx509.Certificate
The loaded CA certificate object.
- ca_keyrsa.RSAPrivateKey
The loaded CA private key used for signing.
- base_infoCertificateInfo
Structured metadata extracted from the CA certificate’s Subject field (organization, country, state, locality, organizational unit).
- __init__(*args, **kwargs)¶
- property base_info: CertificateInfo¶
- property ca_cert: Certificate¶
- property ca_key: RSAPrivateKey¶
- class tiny_ca.ca_factory.SerialWithEncoding[source]¶
Bases:
objectStateless serial-number generator that encodes a short name prefix and a UUID-derived random fragment into a single integer.
Serial number layout
[ 16-bit prefix ][ 80-bit encoded name ][ 64-bit random ]
Total width: 160 bits (well within Python’s arbitrary-precision int; X.509 allows up to 20 bytes / 160 bits per RFC 5280 §4.1.2.2).
prefix — 2-byte ASCII code from
_PrefixRegistry.- encoded name — up to 4 ASCII characters packed into 32 bits
(little-endian byte order, zero-padded).
- random — lower 64 bits of a fresh
uuid.uuid4()ensuring global uniqueness without shared state.
- random — lower 64 bits of a fresh
Because no mutable state is kept, this class is safe to use from multiple threads or processes simultaneously.
Class Attributes¶
- RANDOM_BITSint
Number of bits reserved for the random (UUID) portion. Default:
64.- NAME_BITSint
Number of bits reserved for the encoded name portion. Default:
32(4 bytes × 8 bits).- MAX_NAME_LENGTHint
Maximum number of ASCII characters that can be encoded. Default:
4.
- classmethod generate(name, serial_type)[source]¶
Generate a globally unique serial number for name and serial_type.
Only the first
MAX_NAME_LENGTHcharacters of name are encoded; uniqueness is guaranteed by the UUID random segment, not by the name.- Parameters:
- Returns:
Non-negative integer serial suitable for X.509 certificates.
- Return type:
- Raises:
KeyError – If serial_type has no registered prefix.
Examples
>>> serial = SerialWithEncoding.generate("nginx", CertType.SERVICE) >>> cert_type, name = SerialWithEncoding.parse(serial) >>> assert cert_type == CertType.SERVICE >>> assert name == "ngin" # only first 4 chars are stored
- classmethod parse(serial)[source]¶
Decode a serial number produced by
generate().- Parameters:
serial (int) – Integer serial number to decode.
- Returns:
(cert_type, name_prefix)where name_prefix is the up-to-4-char string recovered from the encoded-name segment. cert_type isNoneif the prefix is unrecognised.- Return type:
Examples
>>> serial = SerialWithEncoding.generate("ca-root", CertType.CA) >>> cert_type, name = SerialWithEncoding.parse(serial) >>> assert cert_type == CertType.CA >>> assert name == "ca-r"
Subpackages¶
Submodules¶
- tiny_ca.ca_factory.factory module
- SOLID notes
CertificateFactoryCertificateFactory.__init__()CertificateFactory.abuild_crl()CertificateFactory.build_crl()CertificateFactory.build_self_signed_ca()CertificateFactory.cosign_certificate()CertificateFactory.export_pkcs12()CertificateFactory.get_cert_chain()CertificateFactory.inspect_certificate()CertificateFactory.issue_certificate()CertificateFactory.issue_intermediate_ca()CertificateFactory.renew_certificate()CertificateFactory.validate_cert()CertificateFactory.verify_crl()