Loading certificates¶
Client certificates arrive in whatever shape your PKI team, cloud provider, or corporate CA happened to produce. This page covers every file-based form httpx-pki accepts. The two OS certificate stores have their own pages (Windows, macOS), as does configuration from the environment.
Important
Whatever the shape, mTLS needs a certificate and the private key that matches
it — the certificate states who you are, the key proves you are entitled to
it, and the handshake uses both. Some of the formats below carry only
certificates: a .crt or .cer is a single certificate, and a PKCS#7 .p7b
cannot hold a key at all. If yours has no key in it, see
Do you have both halves?.
The extension does not matter¶
Certificate files come with a lot of names — .p12, .pfx, .pem, .crt,
.cer, .key, .tls, .ukey — but an extension is just a label somebody
chose. What matters is the encoding of the bytes, and httpx-pki reads that
from the content:
PKIClient("whatever-they-sent-me.tls") # works if the bytes are PKCS#12 or PEM
So you can generally point PKIClient at the file you were handed without
first working out what it is. Use the explicit constructors below when you
would rather force one interpretation than rely on detection.
Input |
Constructor |
|---|---|
PKCS#12 — |
|
PEM bundle — key + cert(s) in one file |
|
Separate cert + key — PEM or DER |
|
PKCS#7 — |
|
Every source below accepts a str path, a pathlib.Path, or raw
bytes.
PKCS#12 bundles¶
The usual enterprise hand-off: private key, leaf certificate, and chain in one password-protected blob.
from pathlib import Path
from httpx_pki import PKIClient
PKIClient("client.p12", password="secret")
PKIClient(Path("client.pfx"), password="secret")
PKIClient(p12_bytes, password=b"secret") # the password may be bytes too
PKIClient("client.p12") # no password on the bundle
PKIClient.from_pkcs12("client.p12", "secret") # explicit; password is positional
Any chain certificates inside the bundle are presented to the server
automatically. A bundle exported without them — a common shape, since
Windows only includes the chain when “include all certificates in the
certification path” is ticked — can be completed with chain=:
PKIClient("client.p12", password="secret", chain="intermediate.crt")
PEM bundles¶
A single file holding the private key and its certificate — and possibly intermediates — as consecutive PEM blocks.
PKIClient("client.pem") # auto-detected
PKIClient.from_pem("client.pem") # explicit
PKIClient.from_pem(pem_bytes, password="pw") # if the key block is encrypted
Block order does not matter, and the key may be any of the usual encodings:
PKCS#8 —
-----BEGIN PRIVATE KEY-----PKCS#1 —
-----BEGIN RSA PRIVATE KEY-----Encrypted PKCS#8 —
-----BEGIN ENCRYPTED PRIVATE KEY-----, withpassword=EC keys, alongside RSA
A separate certificate and key¶
Two files, the shape most non-Windows tooling produces:
client = PKIClient.from_key_pair(
certificate="client.crt",
private_key="client.key",
password="secret", # only if the key is encrypted
chain="intermediate.crt", # optional intermediates to present
)
Both files may be PEM or DER — again detected from the bytes, so a DER
certificate with a .crt name and a DER key with a .key name work as-is.
When the certificate file is itself a bundle¶
If certificate holds several certificates — a leaf plus intermediates — the
leaf is identified by matching it against the private key, in any block
order. The remaining certificates become the chain automatically:
# fullchain.pem contains the CA first, then the leaf. Still correct.
PKIClient.from_key_pair("fullchain.pem", "client.key")
Intermediates¶
chain takes a single source, or a list:
chain="intermediates.pem" # one file, may concatenate several
chain=["intermediate.crt", "root.crt"] # a list of sources
chain=b"-----BEGIN CERTIFICATE-----\n..." # raw bytes
It is accepted by every constructor — PKIClient(...), from_pkcs12,
from_pem, from_key_pair — and by build_ssl_context, so a source of any
kind that arrives without its intermediates can be completed the same way.
auto_reload watches the chain files alongside the certificate, and reload()
re-reads them.
Tip
Certificates passed as chain= that are not actually between your certificate
and its issuer show up in the explain()
report as chain.stray — sent for nothing, and a strict server may reject the
chain. A chain= that connects to nothing is the one that warns at
construction (chain.disconnected), because that handshake will fail.
Dropping what does not belong¶
When the extra certificates are in a source you can edit, remove them. When
they are baked into a .p12 your PKI team exported — or a file cert-manager
rewrites on every rotation — there is nothing to edit, and prune_chain=True
drops everything that is not on the path from your certificate upward:
PKIClient("corp.p12", password="secret", prune_chain=True)
It is safe by construction: a certificate nothing reaches contributes nothing
to path building, which is why httpx-pki has always excluded other identities’
certificates from a multi-identity bundle’s chain. It is opt-in because
silently changing what goes on the wire is worse than reporting it, and it
survives reload() and pickling.
Note
Pruning removes what is unnecessary; it cannot supply what is missing. If
nothing in the chain is on the path, the certificates are left alone —
dropping them all would leave a bare certificate that looks like the ordinary
“the root is not included” shape, silencing the
chain.disconnected finding while the handshake
still fails for exactly that reason. Supply the right intermediates with
chain= instead.
PKCS#7 bundles¶
.p7b / .p7c files hold certificates but no private key, which is the
format Windows CAs commonly export chains in. They work anywhere a certificate
source is accepted — DER or PEM — and pair with a separate key:
# The .p7b holds the leaf and its intermediates; the key comes separately
PKIClient.from_key_pair("issued.p7b", "client.key")
# Or as the chain alongside a normal leaf certificate
PKIClient.from_key_pair("client.crt", "client.key", chain="chain.p7b")
A certs-only PKCS#7 is also valid as a verify= CA bundle — see
Server trust (verify).
When a source holds several identities¶
A PKCS#12 or PEM bundle can hold more than one key-and-certificate pair — a dual key pair from AD key archival, or a renewed certificate kept alongside the one it replaces. httpx-pki will not guess which one you meant:
AmbiguousCertificateError: this PKCS#12 data holds 2 identities:
[0] dual (dual) key_usage=digital_signature expires=...
[1] dual (dual) key_usage=key_encipherment expires=...
from_pkcs12 and from_pem both take identity=, key_usage=, and
extended_key_usage= to resolve it. That is its own topic:
Choosing the right certificate.
When the key and certificate do not match¶
Pairing the wrong two files is a common and confusing mistake, so httpx-pki checks at load time rather than letting it surface as an opaque handshake failure:
CertificateLoadError: private key does not match certificate
(their public keys differ)
An encrypted key with the wrong password — or none — fails the same way:
CertificateLoadError: could not parse private key (wrong password?)
Next steps¶
Choosing the right certificate — picking one identity out of several
Server trust (verify) — how the server gets verified
Expiry and rotation — reloading these sources as they rotate