fizzpy

Post-quantum TLS 1.3 for Python: a real X25519MLKEM768 handshake via Facebook's Fizz, pluggable under requests and httpx.

TLS Library Active Updated 19d ago

fizzpy is a Python TLS library built on Fizz, Facebook's C++ TLS 1.3 implementation, wrapped with pybind11. By default it offers X25519MLKEM768, the standardized hybrid post-quantum key exchange, and negotiates it against servers that support it — Cloudflare, Google — falling back to classical X25519 against those that don't. It also works as a pluggable TLS layer: mount FizzAdapter on a requests.Session, or hand FizzHTTPTransport to httpx.Client, and the rest of the stack — cookies, redirects, retries, connection pooling — keeps running unchanged while Fizz does the handshake underneath.

Proving the handshake

TLS-fingerprint libraries like curl_cffi and clients built on uTLS prove themselves by mimicking a browser's ClientHello byte for byte. fizzpy's claim runs the other way — that a real hybrid ML-KEM handshake happened — and that needs a different kind of evidence: a packet capture, not a fingerprint match. Client(extensions=[...]) surfaces Fizz's ClientHello extension hook directly, so examples/pq_marker.py stamps an unmistakable marker (0xFE5A) alongside the real handshake and captures the result in Wireshark.

A fizzpy ClientHello captured in Wireshark
The TLS 1.3 ClientHello offers the post-quantum X25519MLKEM768 group (0x11ec), alongside the 0xFE5A marker extension that makes the capture unmistakably fizzpy's.

The event-loop core

Every fizzpy connection is driven by one shared folly event-loop thread, not one thread per connection. connect/read/write cross into C++ as a (resolve, reject) pair; the loop thread invokes whichever one fires while holding the GIL, and each operation's heap-allocated callback deletes itself in that same critical section, so a py::function refcount is never touched off the GIL. That discipline, not the event loop itself, is the part that took getting right. Both facades ride the same core: the sync Client blocks a threading.Event while the GIL is released, and AsyncClient bridges through loop.call_soon_threadsafe. A pooled FizzAdapter is consequently safe to share across threads — proven by a test that hammers a 16-connection pool from 24 threads at once.

Occupying the TLS slot

wrap_socket is the actual primitive: hand it an already-connected socket, get back a blocking, ssl.SSLSocket-shaped object. The two adapters are both thin shims onto it, but they get there through different holes in their host library. requests/urllib3 exposes a genuine ssl_context= parameter, so FizzAdapter installs a duck-typed context there, alongside requests' own per-request verify=. httpx has no such second slot — verify= is the only door in — so FizzHTTPTransport has to occupy it outright.

Handshake failures still come out shaped like the client's native exception, not a Fizz-flavored one: a bad certificate raises ssl.SSLCertVerificationError (or requests.exceptions.SSLError / httpx.ConnectError, however each library wraps it), stripped of the underlying C++ exception's internal formatting. An existing except requests.exceptions.SSLError keeps working without ever learning fizzpy is underneath.

Self-contained wheels

The post-quantum handshake needs a Fizz built against liboqs; Homebrew's own Fizz bottle isn't. So the shipped wheels build folly, Fizz, and liboqs from source and link them statically into one manylinux_2_28 extension — a fresh pip install fizzpy needs no system packages for post-quantum TLS to work. CI extends the same source tree to macOS (arm64 and Intel) and Windows.

Cross-platform build quirks

macOS x86_64: folly compiles one crypto-math file with -mno-sse2. The macOS 15 SDK declares _Float16 overloads unconditionally, and Clang can't codegen them without SSE2 — every x86_64 CPU has it, so the build patches that one file's flag to -msse2 before compiling.

Windows: glog and gflags export as shared libraries even inside the static-linked tree, so _core.pyd still needs glog.dll at import time. delvewheel --add-path vendors it in from the build's install tree — the Windows analogue of the Linux/macOS repair step's LD_LIBRARY_PATH/DYLD_LIBRARY_PATH injection.

Limitations

Fizz speaks TLS 1.3 only — not a temporary gap, a property of the library it's built on — so a request to a TLS-1.2-only server fails loudly by default. fallback=True opts into a stdlib ssl retry, but only on a protocol_version alert; a certificate or hostname failure still raises, never downgrades. HTTP/2 isn't implemented, so http2=True is one of the parameters FizzHTTPTransport refuses. There's no client-certificate (mutual TLS) support, and CA trust is a single bundle file rather than a directory. An HTTPS proxy — TLS to the proxy itself, as opposed to a plain CONNECT tunnel — isn't supported either; fizzpy would attempt a post-quantum handshake against the proxy rather than the origin.

On this page