httpx-pki
PKCS#12 client-certificate (mTLS) sessions for httpx2 and httpx.
httpx-pki gives you an httpx.Client (and httpx.AsyncClient) subclass with a
client certificate already mounted, so mutual-TLS endpoints "just work":
from httpx_pki import PKIClient
with PKIClient("client.p12", password="secret") as client:
resp = client.get("https://mtls.example.com/")
print(resp.status_code)
Purpose
httpx deprecated its cert= argument in 0.28 — a design httpx2 keeps — in
favor of building an ssl.SSLContext yourself, which stdlib ssl can't do
from PKCS#12 or in-memory bytes. httpx-pki is that missing piece.
Install
pip install httpx-pki
Requires Python 3.10+. httpx2 comes with it, along with cryptography,
truststore, and certifi.
Prefer the original httpx? It stays fully supported — install with --no-deps
so httpx2 isn't pulled in. See
Install and
Backends.
Whatever you were handed, there's a one-liner for it
Certificate files come with all sorts of extensions — .p12, .pfx, .pem,
.crt, .tls — but an extension is just a name. httpx-pki detects the
encoding from the bytes, so you can point it at whatever your PKI team sent
you:
from httpx_pki import PKIClient
# PKCS#12 bundle — key + cert + chain in one blob
PKIClient("client.p12", password="secret")
# PEM bundle — key + cert(s) in one file, any block order
PKIClient("client.pem")
# Raw bytes you already have in hand
PKIClient(p12_bytes, password=b"secret")
# Separate certificate and key, PEM or DER
PKIClient.from_key_pair("client.crt", "client.key")
# ...with intermediates, as PEM or PKCS#7
PKIClient.from_key_pair("client.crt", "client.key", chain="chain.p7b")
# The Windows certificate store (Windows only)
PKIClient.from_windows_cert_store(name="Acme Corp")
# The macOS keychain (macOS only)
PKIClient.from_macos_keychain(name="Acme Corp")
# Configured entirely by environment variables
PKIClient.from_env()
Handed a whole folder?
A CA rarely sends one file. inventory reads the folder, says what each file
actually is, pairs the keys with their certificates, and prints the call each
pairing amounts to:
$ python -m httpx_pki inventory ./corp-export
INVENTORY corp-export — 7 files, 2 identities
IDENTITY svc-client RSA-2048 expires 2027-01-15
bundle corp.p12 (password #1)
chain corp-issuing-ca.crt
→ PKIClient("corp.p12", password=..., chain="corp-issuing-ca.crt")
IDENTITY svc-client RSA-2048 expires 2027-01-15
certificate svc-client.pem
private key svc-client.key (encrypted — opened with password #2)
chain corp-issuing-ca.crt
same certificate as corp.p12
→ from_key_pair(certificate="svc-client.pem", private_key="svc-client.key", password=..., chain="corp-issuing-ca.crt")
LOCKED old-2025.pem — 1 certificate, plus 1 encrypted private key none of the given passwords open
NOTES cert-details.txt — human-readable dump; fingerprint matches corp.p12 (not loadable)
svc-client.csr — certificate request for the key of corp.p12 (issuance artifact, not loadable)
Nothing is skipped: a file no password opens is reported as locked, not dropped. It classifies and pairs — it never builds a session for you, because a folder like that usually holds more than one answer.
→ Taking inventory of a folder
Async
from httpx_pki import AsyncPKIClient
async with AsyncPKIClient("client.p12", password="secret") as client:
resp = await client.get("https://mtls.example.com/")
One file, several certificates
A PKCS#12 or PEM bundle can hold more than one identity — a dual key pair from
AD key archival, or a renewed certificate kept beside the one it replaces.
cryptography can't express that: it returns the first key and leaves the other
identity's certificate looking like a chain certificate. httpx-pki reads the
structure itself, so you can inspect and select:
from httpx_pki import PKIClient, list_identities, for_mtls
list_identities("corp.p12", password="secret") # see what's in there
# Usually all you need: the identity that is valid now and can do client auth
PKIClient("corp.p12", password="secret", identity=for_mtls)
# Or pick one yourself
PKIClient("corp.p12", password="secret", key_usage="digital_signature")
PKIClient("corp.p12", password="secret", identity="Signature")
Loading a multi-identity bundle without a selector raises rather than guessing.
→ Choosing the right certificate
Server trust
Your client certificate and the server's are independent. verify=True (the
default) uses the OS trust store, so corporate CAs distributed by group
policy or MDM work out of the box:
PKIClient("client.p12", password="secret", verify="/etc/ssl/internal-ca.pem")
PKIClient("client.p12", password="secret", verify="certifi")
Naming a bundle replaces the default trust. To keep it and add your own — the usual shape for a service that talks to internal and public endpoints both — pass a list:
PKIClient("client.p12", password="secret",
verify=["system", "/etc/pki/internal-root.pem"])
Expiry and rotation
Certificates keep getting shorter-lived. Warn early, reload automatically, or fail loudly:
from datetime import timedelta
PKIClient(
"/etc/certs/client.pem",
auto_reload=True, # pick up cert-manager rotations
strict_validity=True, # fail clearly, not at handshake
warn_if_expires_within=timedelta(days=7),
)
Inspecting what's mounted
client.cn # 'corp-user'
client.not_valid_after # datetime (UTC)
client.is_expired # bool
client.cert_info() # CertInfo: subject, issuer, fingerprints, usages, SANs
Why won't the handshake work?
explain() takes the same arguments as the constructors and reports what it
would do instead of doing it — what the source holds, what it would present,
what it would trust, and what would stop it working:
$ python -m httpx_pki explain corp.p12 --verify internal-ca.pem
corp.p12 — PKCS#12, 1 identity, 1 chain certificate
PRESENTS svc-client
valid 2026-01-15 → 2027-01-15 (159 days left)
ext usage client_auth
CHAIN svc-client
└─ Corp Issuing CA [verified]
└─ Corp Root [NOT SUPPLIED; trust anchor, need not be sent]
TRUSTS internal-ca.pem — 1 anchor: Corp Root
PROBLEMS none
It works when loading does not — several identities with no selector, or a
missing password, produce a report rather than an exception. client.explain()
does the same for a live session, and the CLI exits non-zero when there are
problems, so it works as a CI check.
→ Explaining a whole configuration
Just the SSL context
Don't want the client wrapper? build_ssl_context() gives you the hard part,
ready for a plain httpx.Client or a custom transport:
import httpx
from httpx_pki import build_ssl_context
ctx = build_ssl_context("client.p12", password="secret")
client = httpx.Client(verify=ctx)
⚠️ Passing a custom transport= makes httpx ignore verify= — put the context
on the inner transport, not the client.
Testing helpers
httpx_pki.testing mints throwaway certificates, including multi-identity
bundles that nothing else readily produces:
from httpx_pki.testing import make_ca, make_client_cert
ca = make_ca()
bundle = make_client_cert("svc-client", ca=ca, dns_names=["svc.internal"])
expired = make_client_cert("old", ca=ca, expired=True)
⚠️ Security note on pickling
To support pickling, a client stores its certificate material and rebuilds the
SSL context on unpickle. The pickle therefore contains the decrypted private
key in cleartext — treat it as a secret. repr() never reveals key material.
Passwords are not retained, with one exception: enabling auto_reload keeps the
password on the client so unattended reloads can decrypt the rotated source.
→ Security notes · SECURITY.md
How it works
Stdlib ssl can't load PKCS#12 or in-memory key material, so httpx-pki uses
cryptography to extract the key and certificates,
stages them where OpenSSL can read them, and passes the resulting
ssl.SSLContext to httpx via verify=.
On Linux the decrypted key never touches disk — it's staged in an anonymous
memfd that OpenSSL reads through /proc/self/fd and that ceases to exist when
closed. Elsewhere it's a 0600 temp file, deleted immediately after loading.
Non-goals
Scoped to credentials whose private key can be exported into memory. Not
supported: PKCS#11 / smartcards / HSMs / TPMs (incompatible with stdlib
ssl, which needs the raw key bytes), Java keystores (convert to PKCS#12
with keytool), workload-identity protocol clients (point auto_reload at
the files they write), and OCSP / CRL revocation (nothing in stdlib ssl to
build on).
Supply chain
Released to PyPI exclusively from GitHub Actions via
Trusted Publishing (OIDC — no
long-lived tokens) with PEP 740
attestations, from a tagged commit whose version is verified against
__version__ at build time. All Actions are pinned to full commit SHAs.
Install via a lockfile that records hashes, as with any security-sensitive dependency.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file httpx_pki-0.9.0.tar.gz.
File metadata
- Download URL: httpx_pki-0.9.0.tar.gz
- Upload date:
- Size: 225.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
26ee11eaee867445da1fe9a9d4ce9b1a53a623059a9bff01732a5d3fc69cc6a0
|
|
| MD5 |
b588a837c8e86d6b31b39c639b095b96
|
|
| BLAKE2b-256 |
9419c5b805d0dec29f97a97f61f96acfd16abd29715e9ad19366bdc5e8d3d25f
|
Provenance
The following attestation bundles were made for httpx_pki-0.9.0.tar.gz:
Publisher:
publish.yml on ccbest/httpx-pki
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
httpx_pki-0.9.0.tar.gz -
Subject digest:
26ee11eaee867445da1fe9a9d4ce9b1a53a623059a9bff01732a5d3fc69cc6a0 - Sigstore transparency entry: 2444784543
- Sigstore integration time:
-
Permalink:
ccbest/httpx-pki@1c1756cb5bc46206b9410dd0bea564dd74fb1ca2 -
Branch / Tag:
refs/tags/v0.9.0 - Owner: https://github.com/ccbest
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1c1756cb5bc46206b9410dd0bea564dd74fb1ca2 -
Trigger Event:
release
-
Statement type:
File details
Details for the file httpx_pki-0.9.0-py3-none-any.whl.
File metadata
- Download URL: httpx_pki-0.9.0-py3-none-any.whl
- Upload date:
- Size: 111.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5ad0fd47c99bd0c1584c22933eecac0145dfd06acaf12e9ee97521e0ec6109f9
|
|
| MD5 |
7af9d7561ee07e0302b27c836e87e468
|
|
| BLAKE2b-256 |
0fe806e721ee040bb53c07e7c58b5444927178533ace16a1cee79748f5c671ab
|
Provenance
The following attestation bundles were made for httpx_pki-0.9.0-py3-none-any.whl:
Publisher:
publish.yml on ccbest/httpx-pki
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
httpx_pki-0.9.0-py3-none-any.whl -
Subject digest:
5ad0fd47c99bd0c1584c22933eecac0145dfd06acaf12e9ee97521e0ec6109f9 - Sigstore transparency entry: 2444784603
- Sigstore integration time:
-
Permalink:
ccbest/httpx-pki@1c1756cb5bc46206b9410dd0bea564dd74fb1ca2 -
Branch / Tag:
refs/tags/v0.9.0 - Owner: https://github.com/ccbest
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1c1756cb5bc46206b9410dd0bea564dd74fb1ca2 -
Trigger Event:
release
-
Statement type: