certificate
Compute TLS certificate lifecycle: CSR generation and Hub-issued cert renewal.
Once a compute has heartbeated with a private IP address, the hub registers a
"compute" record and returns a computeId/domain pair (see
BitfountHub.do_pod_heartbeat). This module builds a CSR for that domain,
submits it to BitfountHub.request_compute_certificate, polls
BitfountHub.get_compute_certificate until issued, persists the result, and later
detects when the issued certificate is two-thirds of the way through its
validity period so a renewal can be requested.
If the hub hands back a new computeId/domain pair; a persisted
certificate for the old domain is treated as absent (rather than just not-yet-
due-for-renewal) so a certificate for the new domain is requested straight
away instead of waiting out the old one's remaining validity. A certificate
issued to a key this pod no longer holds is treated the same way: it cannot be
served, and its validity window says nothing about that.
Module
Functions
build_csr_pem
def build_csr_pem(private_key: RSAPrivateKey, domain: str) ‑> str:Builds a PEM-encoded certificate signing request for domain.
The hub requires the CSR's public key to be RSA, its subject common name
to equal domain exactly, and exactly one subject alternative name (a
DNS name equal to domain).
Arguments
private_key: The RSA private key to sign the CSR with. Its public key is embedded in the CSR.domain: The domain the certificate is being requested for (thedomainreturned alongsidecomputeIdfrom the heartbeat).
Returns The PEM-encoded CSR.
certificate_domain
def certificate_domain(certificate_pem: str) ‑> str | None:Returns the leaf certificate's subject common name, or None if absent.
build_csr_pem sets the CSR's common name to the domain it was issued
for, and the hub is required to preserve it, so this is what identifies
which domain a persisted certificate belongs to.
Arguments
certificate_pem: A PEM-encoded certificate (the leaf certificate is sufficient; any following CA bundle entries are ignored).
certificate_fingerprint
def certificate_fingerprint(certificate_pem: str) ‑> str:Returns the leaf certificate's SHA-256 fingerprint, as hex.
This identifies a certificate independently of the PEM text carrying it.
The same certificate reaches this module as two different strings - the
persisted fullchain, which is the leaf followed by the CA bundle, and the
hub's leaf-only certificatePem - so comparing the text answers "is this
the same certificate?" with "no" for a certificate that is in fact the
same one.
Arguments
certificate_pem: A PEM-encoded certificate (the leaf certificate is sufficient; any following CA bundle entries are ignored).
certificate_matches_private_key
def certificate_matches_private_key( certificate_pem: str, private_key: RSAPrivateKey,) ‑> bool:Returns whether the leaf certificate was issued to private_key.
A certificate is only servable alongside the key it was issued to.
OpenSSL rejects the pair when the TLS context is built, with
[X509: KEY_VALUES_MISMATCH] raised out of the serving library rather
than out of anything holding the certificate - so nothing on the path
that persists or selects a certificate notices, and the failure only
reaches the logs as a server that would not start.
The public halves are compared as DER rather than as key objects, which holds regardless of how the two were loaded.
Arguments
certificate_pem: A PEM-encoded certificate (the leaf certificate is sufficient; any following CA bundle entries are ignored).private_key: The key the certificate should carry the public half of.
Returns
True if the certificate's public key is private_key's public half.
certificate_validity
def certificate_validity( certificate_pem: str,) ‑> tuple[datetime.datetime, datetime.datetime]:Returns the (not-valid-before, not-valid-after) UTC datetimes.
Arguments
certificate_pem: A PEM-encoded certificate (the leaf certificate is sufficient; any following CA bundle entries are ignored).
renewal_due_at
def renewal_due_at( not_before: datetime, not_after: datetime, fraction: float = 0.6666666666666666,) ‑> datetime.datetime:Returns the point at which a certificate should be renewed.
Defaults to two-thirds of the way through the certificate's validity period. This is the same heuristic used by ACME clients.
Classes
ComputeCertificateManager
class ComputeCertificateManager( hub: BitfountHub | None, key_directory: Path, private_key: RSAPrivateKey,):Manages the lifecycle of a compute's hub-issued TLS certificate.
Persists the issued certificate (leaf + CA bundle) alongside the pod's
existing RSA identity key, under key_directory / "compute.pem".
Arguments
hub: TheBitfountHubclient to request/poll certificates through. None makes the manager read-only: it can say which certificate is on disk and servable, but cannot request or renew one. That is all the patient-API child needs; theComputedoes the rest.key_directory: The pod's key directory (the same directorypod_rsa.pemlives in).private_key: The pod's existing RSA identity key. Reused as the CSR key rather than generating a separate one.
Variables
fullchain_path : pathlib.Path- Path to the persisted leaf-certificate + CA-bundle PEM file.
-
tls_keyfile_path : pathlib.Path- Path to a PEM/PKCS8 copy of the private key, for TLS.Written lazily, on first access.
ComputeCertificateManageris constructed for every pod regardless of whether TLS is ever actually used, and callers (e.g. tests) may pass a mockedprivate_key.
Methods
current_fullchain_path
def current_fullchain_path(self) ‑> pathlib.Path | None:Returns fullchain_path if a servable certificate is persisted.
A certificate that cannot be served is reported as absent rather than handed out: one issued to a key this pod no longer holds, or one that will not parse. OpenSSL rejects such a pair only when the TLS context is built, which takes down the server that was given it - where reporting it as absent leaves the caller on its no-certificate path, which is a state it already handles.
A key mismatch is warned about once per certificate rather than on every read: this is read on every certificate-lifecycle tick, and a mismatch lasts until a replacement is issued.
poll
def poll(self, compute_id: str | None, domain: str | None) ‑> None:Runs one idempotent certificate-lifecycle tick.
Safe to call repeatedly (e.g. from a background thread). Never
raises: hub/network failures are logged and left for the next tick,
matching how Pod._pod_heartbeat handles the same failure modes.
Arguments
compute_id: The compute worker ID from the most recent heartbeat response, or None if not yet known.domain: The domain from the most recent heartbeat response, or None if not yet known.