Python
Is PQC enabled? — quick check
macOS / Linux / Windows
# 1) No-dependency check — identify this machine first.
uname -a 2>/dev/null || true
# 2) Dependency check — prompt before installing anything.
if ! command -v python3 >/dev/null 2>&1; then
echo 'Python 3 was not found.'
printf 'Install or enable Python 3 now? [y/N] '
read answer
case "$answer" in
[Yy]*) echo 'Install Python 3 from your OS package manager or python.org, then rerun this snippet.' ;;
*) echo 'Skipping Python 3-based check.'; exit 1 ;;
esac
fi
# 3) Capability — fully offline. OpenSSL ≥ 3.5 = native ML-KEM.
python3 -c "import ssl; print(ssl.OPENSSL_VERSION)"
# 4) Liveness — confirm a TLS 1.3 handshake works against the target.
# Python's ssl module has NO public API for the negotiated named group,
# so the authoritative answer comes from the local openssl below.
python3 - <<'EOF'
import ssl, socket
ctx = ssl.create_default_context()
with socket.create_connection(("example.com", 443)) as s, \
ctx.wrap_socket(s, server_hostname="example.com") as t:
print("proto=", t.version(), "cipher=", t.cipher()[0])
EOF
# 5) Authoritative named-group readout — still local, via openssl CLI.
# 1) No-dependency check — identify this machine first.
uname -a 2>/dev/null || true
# 2) Dependency check — prompt before installing anything.
if ! command -v openssl >/dev/null 2>&1; then
echo 'OpenSSL was not found. A local PQC proof needs OpenSSL 3.5+.'
printf 'Install OpenSSL now? [y/N] '
read answer
case "$answer" in
[Yy]*)
if command -v brew >/dev/null 2>&1; then brew install openssl@3
elif command -v apt-get >/dev/null 2>&1; then sudo apt-get update && sudo apt-get install -y openssl
elif command -v dnf >/dev/null 2>&1; then sudo dnf install -y openssl
elif command -v yum >/dev/null 2>&1; then sudo yum install -y openssl
else echo 'No supported package manager found. Install OpenSSL 3.5+ and retry.'; exit 1
fi ;;
*) echo 'Install OpenSSL 3.5+ and retry for a local PQC proof.'; exit 1 ;;
esac
fi
OPENSSL=openssl
if command -v brew >/dev/null 2>&1; then
BREW_OPENSSL="$(brew --prefix openssl@3 2>/dev/null)/bin/openssl"
[ -x "$BREW_OPENSSL" ] && OPENSSL="$BREW_OPENSSL"
fi
$OPENSSL version
if ! $OPENSSL list -tls-groups 2>/dev/null | grep -qiE 'X25519MLKEM768|MLKEM|Kyber'; then
echo 'This OpenSSL does not advertise ML-KEM groups. Upgrade to OpenSSL 3.5+ or load oqsprovider, then retry.'
exit 1
fi
$OPENSSL s_client -connect example.com:443 -tls1_3 -groups X25519MLKEM768 </dev/null 2>&1 | grep -E 'Negotiated TLS1\.3 group|Server Temp Key' Expected when PQC is ON
OpenSSL 3.5.0 8 Apr 2025
proto= TLSv1.3 cipher= TLS_AES_256_GCM_SHA384
Negotiated TLS1.3 group: X25519MLKEM768 What you'll see when PQC is OFF
OpenSSL 3.0.13 30 Jan 2024
proto= TLSv1.3 cipher= TLS_AES_256_GCM_SHA384
# (no group line) To make Python applications actively SEND the PQ group, configure it in /etc/ssl/openssl.cnf (Groups = X25519MLKEM768:…) — ssl.SSLContext has no Python-level setter for the groups list. macOS system Python links LibreSSL (no ML-KEM); use brew python@3.13 + openssl@3.
Python's ssl module is a thin wrapper over the system OpenSSL.
X25519MLKEM768 is negotiated automatically once OpenSSL is recent enough.
Build matrix
- macOS Homebrew —
brew install python@3.13links against Homebrew OpenSSL 3.5+. - Debian / Ubuntu — distro Python uses distro OpenSSL. 24.04 ships OpenSSL 3.0; you need 3.5 from sid or build oqsprovider.
- Fedora 41+ — OpenSSL 3.5 in base.
- Windows — official python.org installer bundles OpenSSL. Python 3.13.4+ bundles OpenSSL 3.5.
Limitation: set_ecdh_curve takes one curve
ssl.SSLContext.set_ecdh_curve only accepts a single curve name, so you can't
list a fallback. Workaround: configure groups globally in openssl.cnf:
[default_conf]
ssl_conf = ssl_sect
[ssl_sect]
system_default = system_default_sect
[system_default_sect]
Groups = X25519MLKEM768:X25519:secp256r1 aiohttp / requests / httpx
All three use the standard ssl module. Once OpenSSL is upgraded,
nothing in your application code changes.
Verify in code
import ssl, socket
ctx = ssl.create_default_context()
with socket.create_connection(('example.com', 443)) as s:
with ctx.wrap_socket(s, server_hostname='example.com') as ts:
print(ts.version(), ts.cipher())
# Wireshark / Keylog file confirms NamedGroup