tiny_ca.ca_factory.utils package

class tiny_ca.ca_factory.utils.CAFileLoader(ca_cert_path, ca_key_path, ca_key_password=None, logger=None)[source]

Bases: object

Loads 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).

  1. Deserialises the CA certificate and private key from PEM.

  2. Extracts CertificateInfo from the CA certificate’s Subject.

After successful construction all three ICALoader properties 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 str value is encoded to bytes using UTF-8 before being passed to the cryptography library. None means the key is unencrypted.

  • logger (Logger | None) – Logger instance for diagnostic messages. Falls back to DEFAULT_LOGGER when None.

__init__(ca_cert_path, ca_key_path, ca_key_password=None, logger=None)[source]
Parameters:
Return type:

None

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:

CertificateInfo

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.utils.CertLifetime[source]

Bases: object

Stateless 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 the not_valid_after / not_valid_before timestamps from an

existing certificate as timezone-aware UTC datetime objects.

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 None the 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. Pass None to use datetime.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 with tzinfo=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:

tuple[datetime, datetime]

Returns:

  • tuple[datetime, datetime]

  • (not_before, not_after) in UTC.

Raises:

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 DateTime column stores naive datetimes (no tzinfo). This helper centralises the normalisation so that lifecycle managers never duplicate the if dt.tzinfo is None guard inline.

Parameters:

dt (datetime) – Any datetime object, aware or naive.

Returns:

The same instant expressed as a UTC-aware datetime. If dt already carries tzinfo, it is returned unchanged. If dt is naive it is assumed to represent UTC and tzinfo is 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_utc and ensures the returned value always carries tzinfo=timezone.utc for safe comparison with other aware datetimes.

Parameters:

cert (x509.Certificate) – The certificate whose activation date should be read.

Returns:

cert.not_valid_before_utc with tzinfo explicitly set to timezone.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:

datetime

Returns:

  • datetime

  • cert.not_valid_before_utc with tzinfo=UTC.

static valid_to(cert)[source]

Return the expiry timestamp of cert as a timezone-aware UTC datetime.

Wraps cert.not_valid_after_utc and ensures the returned value always carries tzinfo=timezone.utc for safe comparison with other aware datetimes.

Parameters:

cert (x509.Certificate) – The certificate whose expiry date should be read.

Returns:

cert.not_valid_after_utc with tzinfo explicitly set to timezone.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:

datetime

Returns:

  • datetime

  • cert.not_valid_after_utc with tzinfo=UTC.

class tiny_ca.ca_factory.utils.ICALoader(*args, **kwargs)[source]

Bases: Protocol

Protocol 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 CertificateFactory without 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.utils.SerialWithEncoding[source]

Bases: object

Stateless 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.

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.

MAX_NAME_LENGTH: int = 10

Maximum number of characters accepted by _encode_name().

NAME_BITS: int = 80

Bit-width of the encoded-name segment (10 ASCII chars × 8 bits).

RANDOM_BITS: int = 64

Bit-width of the random (UUID) segment.

classmethod generate(name, serial_type)[source]

Generate a globally unique serial number for name and serial_type.

Only the first MAX_NAME_LENGTH characters of name are encoded; uniqueness is guaranteed by the UUID random segment, not by the name.

Parameters:
  • name (str) – Human-readable identifier. Only the first 4 ASCII characters are embedded; the remainder is ignored (not hashed or truncated with loss).

  • serial_type (CertType) – Certificate category; determines the 2-byte prefix.

Returns:

Non-negative integer serial suitable for X.509 certificates.

Return type:

int

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 is None if the prefix is unrecognised.

Return type:

tuple[CertType | None, str]

Examples

>>> serial = SerialWithEncoding.generate("ca-root", CertType.CA)
>>> cert_type, name = SerialWithEncoding.parse(serial)
>>> assert cert_type == CertType.CA
>>> assert name == "ca-r"

Submodules