Skip to main content

httpx-pki

CI codecov PyPI Python versions Docs License: MIT Checked with mypy

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)

📖 Full documentation

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()

→ Loading certificates

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"])

→ Server trust

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),
)

→ Expiry and rotation

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

→ Inspecting a certificate

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.

→ Advanced usage

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)

→ Testing helpers

⚠️ 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.

→ How it works

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

→ Non-goals

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.

→ Supply chain · Changelog

License

MIT

Metadata

Release files for httpx-pki 0.9.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for httpx-pki 0.9.1
File Size Uploaded Provenance
httpx_pki-0.9.1.tar.gz 227.9 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for httpx-pki 0.9.1
File Interpreter ABI Platform
httpx_pki-0.9.1-py3-none-any.whl Python 3 none any Details

Total release size: 339.7 kB

Release files / httpx_pki-0.9.1.tar.gz

Download URL httpx_pki-0.9.1.tar.gz
Size 227.9 kB
Tags Source
SHA-256 checksum
How to use checksums
93cdbe0ee18f971f2e93e69e700298b347d933eb850e7b38f12a3b706c6c5964
BLAKE2b-256 checksum
How to use checksums
141ebb1cb21d4107a51785248816f6a0b19cdf2fab8ad3edab7b697e889281c1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.

Transparency log

Release files / httpx_pki-0.9.1-py3-none-any.whl

Download URL httpx_pki-0.9.1-py3-none-any.whl
Size 111.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5008ae913cac9e835149e04b5b8b1bbd0e7e11727dd5ea1677e58aa4dfe6b7ec
BLAKE2b-256 checksum
How to use checksums
a547fde5e4ae8f3eba8a957c85abdcae0b4790e0f833f983e736f6fb2d5fa3b0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.

Transparency log

Security for httpx-pki 0.9.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for httpx_pki-0.9.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page