pyOpenSSL
pyOpenSSL is a thin Python wrapper over a subset of the OpenSSL library. Its useful half is OpenSSL.SSL: Context and Connection objects that give you direct control over a TLS handshake, including ALPN negotiation, SNI callbacks, per-connection verification, session reuse, OCSP stapling, and a memory BIO mode that lets you drive TLS without a socket at all. Its other half, OpenSSL.crypto, used to be the way Python people parsed and built X.509 certificates, and the maintainers are steadily removing it: the same organization publishes cryptography, and the README states plainly that if you are using pyOpenSSL for anything other than making a TLS connection you should move to cryptography and drop the dependency.
Keep it for the TLS control surface the standard library does not give you, especially memory BIO mode and handshake callbacks. Move every certificate and key operation to cryptography now, because pyOpenSSL is removing that half of itself release by release and telling you so in its own README.
Use it if
- You need TLS knobs the standard library ssl module does not expose, such as an SNI callback that swaps certificates per hostname, OCSP stapling callbacks, or per-connection verify overrides
- You are driving TLS without a socket: the memory BIO mode (Connection(ctx, None) plus bio_read and bio_write) is how Twisted, eventlet, and async frameworks do TLS over their own transports
- You already depend on something that requires it, such as Twisted's TLS support, and you want to use the same Context objects rather than bolt a second TLS stack onto the process
- You need to interoperate with a specific OpenSSL behaviour, for example a cipher string, a legacy protocol version, or a keylog callback for decrypting captures in Wireshark
- You are parsing, building, or inspecting certificates: use cryptography.x509 directly. The maintainers say this in the README, and they are enforcing it. X509Extension and X509Req were removed in 2026 releases, X509Name and every mutating method on X509 are deprecated, and PKey.generate_key and dump_privatekey are on the way out
- You just want a TLS client or server: the standard library ssl module does hostname verification, certificate validation, and sane defaults out of the box with no third-party dependency, and truststore lets it use the operating system trust store
- You expect hostname checking: pyOpenSSL verifies the certificate chain but does not match the hostname against the certificate for you. Every tutorial that stops after set_verify(VERIFY_PEER) leaves you accepting any valid certificate from any issuer in your trust store, which is not authentication
- You care about upgrading cryptography promptly: pyOpenSSL pins a maximum supported cryptography version (currently cryptography>=49.0.0,<51), so it can hold your whole dependency tree back until a new pyOpenSSL release moves the ceiling
- You want a library you can write against once and forget: this project is in deliberate wind-down for its non-TLS surface, so code you write today against OpenSSL.crypto has a documented expiry date
Setup reality
pip install pyOpenSSL needs Python 3.9 or newer and pulls cryptography, which ships wheels with a statically linked OpenSSL for the common platforms. That is where the friction lives: on an unusual architecture or an old pip, cryptography falls back to a source build that wants a Rust toolchain and OpenSSL headers, and the failure message points at cryptography rather than at you. The version ceiling matters too, since a resolver that wants cryptography 51 will simply refuse to install this. Once running, watch for behaviour that changed recently: as of 26.2.0 mutating a Context after it has created a Connection is an error rather than a warning, and 26.3.0 made reusing a Session across Contexts raise ValueError. Deprecation warnings are not cosmetic here, because the deprecated names are actually deleted a release or two later.
Patterns
Make a verified TLS client connectiontls-client-connection
import socket
from OpenSSL import SSL
ctx = SSL.Context(SSL.TLS_CLIENT_METHOD)
ctx.set_verify(SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT)
ctx.set_default_verify_paths()
ctx.set_min_proto_version(SSL.TLS1_2_VERSION)
sock = socket.create_connection(("example.com", 443))
conn = SSL.Connection(ctx, sock)
conn.set_tlsext_host_name(b"example.com") # SNI
conn.set_connect_state()
conn.do_handshake()Chain verification alone is not authentication. Without a hostname check, any certificate signed by any CA in your trust store passes, so pair this with the next pattern.
Check the hostname against the certificateverify-hostname
from cryptography.x509 import DNSName, ExtensionOID
cert = conn.get_peer_certificate(as_cryptography=True)
san = cert.extensions.get_extension_for_oid(
ExtensionOID.SUBJECT_ALTERNATIVE_NAME).value
names = san.get_values_for_type(DNSName)
if not any(n == "example.com" or
(n.startswith("*.") and "example.com".endswith(n[1:]))
for n in names):
conn.close()
raise SSL.Error("hostname mismatch")Wildcard matching, internationalized names, and IP SANs have real edge cases. The service-identity package implements the rules properly and is what Twisted uses; hand-rolling this is only acceptable for a fixed internal hostname.
Build a server context with a certificate and keytls-server-context
from OpenSSL import SSL
ctx = SSL.Context(SSL.TLS_SERVER_METHOD)
ctx.use_certificate_chain_file("fullchain.pem")
ctx.use_privatekey_file("privkey.pem")
ctx.check_privatekey() # raises if key does not match cert
ctx.set_min_proto_version(SSL.TLS1_2_VERSION)
ctx.set_options(SSL.OP_NO_COMPRESSION | SSL.OP_CIPHER_SERVER_PREFERENCE)As of 26.2.0 you cannot mutate a Context after it has created its first Connection; doing so raises. Configure the Context fully at startup and treat it as frozen afterwards.
Serve different certificates per hostnamesni-callback
contexts = {b"a.example.com": ctx_a, b"b.example.com": ctx_b}
def pick_cert(connection):
name = connection.get_servername()
chosen = contexts.get(name)
if chosen is not None:
connection.set_context(chosen)
main_ctx.set_tlsext_servername_callback(pick_cert)get_servername returns bytes or None, because a client is not required to send SNI. This callback is one of the main reasons to use pyOpenSSL over the standard library on the server side.
Negotiate HTTP/2 with ALPNalpn-negotiation
# client
client_ctx.set_alpn_protos([b"h2", b"http/1.1"])
# ... after handshake
print(conn.get_alpn_proto_negotiated()) # b'h2'
# server
def choose(connection, protos):
return b"h2" if b"h2" in protos else b"http/1.1"
server_ctx.set_alpn_select_callback(choose)Protocol identifiers are bytes, not strings. If the server callback returns a protocol the client did not offer, OpenSSL fails the handshake rather than falling back.
Run TLS without a socket using a memory BIOmemory-bio
from OpenSSL import SSL
conn = SSL.Connection(ctx, None) # no socket
conn.set_connect_state()
while True:
try:
conn.do_handshake()
break
except SSL.WantReadError:
transport.send(conn.bio_read(65536)) # flush outbound bytes
conn.bio_write(transport.recv(65536)) # feed inbound bytesThis is how async frameworks do TLS over their own transports. Always drain bio_read after every operation, including after bio_write, or the handshake stalls waiting for bytes you are still holding.
Handle WantRead and WantWrite on a non-blocking socketnonblocking-io
import select
from OpenSSL import SSL
sock.setblocking(False)
while True:
try:
data = conn.recv(4096)
break
except SSL.WantReadError:
select.select([sock], [], [], 5)
except SSL.WantWriteError:
select.select([], [sock], [], 5)
except SSL.ZeroReturnError:
data = b"" # clean TLS shutdown from the peer
breakZeroReturnError means an orderly close_notify and is not an error condition. A WantWriteError on a read is normal during renegotiation, so handle both directions on both operations.
Get the peer certificate as a cryptography objectinspect-peer-certificate
cert = conn.get_peer_certificate(as_cryptography=True)
print(cert.subject.rfc4514_string())
print(cert.not_valid_after_utc)
chain = conn.get_verified_chain(as_cryptography=True)
for c in chain or []:
print(c.issuer.rfc4514_string())Always pass as_cryptography=True. The default returns the legacy OpenSSL.crypto.X509 object whose accessors are being deprecated, and get_verified_chain returns None when verification did not succeed.
Validate a certificate chain without a connectionverify-chain-offline
from cryptography import x509
from OpenSSL.crypto import X509, X509Store, X509StoreContext, X509StoreContextError
store = X509Store()
store.add_cert(X509.from_cryptography(x509.load_pem_x509_certificate(root_pem)))
leaf = X509.from_cryptography(x509.load_pem_x509_certificate(leaf_pem))
try:
X509StoreContext(store, leaf, chain=[intermediate]).verify_certificate()
except X509StoreContextError as exc:
print("invalid:", exc)Load and build certificates with cryptography and convert at the boundary with from_cryptography. cryptography's own x509.verification module now covers most offline chain validation, so check whether you need this at all.
Dump TLS secrets for Wiresharkkeylog-callback
def keylog(connection, line):
with open("/tmp/sslkeys.log", "ab") as fh:
fh.write(line + b"\n")
ctx.set_keylog_callback(keylog)Point Wireshark at that file to decrypt the capture. This writes the session secrets in plaintext, so gate it behind a debug flag and never enable it on a production host.
Require and read a client certificateclient-certificates
def verify_cb(conn, cert, errnum, depth, ok):
if not ok:
print("chain error", errnum, "at depth", depth)
return bool(ok)
server_ctx.set_verify(
SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT | SSL.VERIFY_CLIENT_ONCE,
verify_cb,
)
server_ctx.load_verify_locations("client-ca.pem")The callback fires once per certificate in the chain, deepest first, and returning a truthy value overrides OpenSSL's own verdict. Returning True unconditionally, which appears in a lot of copied code, turns verification off entirely.
Replace deprecated OpenSSL.crypto callsmigrate-off-crypto
# old
from OpenSSL import crypto
cert = crypto.load_certificate(crypto.FILETYPE_PEM, pem_bytes)
subject = cert.get_subject().CN
# current
from cryptography import x509
from cryptography.x509.oid import NameOID
cert = x509.load_pem_x509_certificate(pem_bytes)
subject = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].valueCertificate signing requests are already gone: X509Req and load_certificate_request were removed in 26.3.0, and x509.CertificateSigningRequestBuilder is the replacement. Run your test suite with -W error::DeprecationWarning to find the rest before an upgrade does it for you.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| cryptography | PyPI | You are doing anything with certificates, keys, signing, or hashing rather than running a live TLS handshake; this is the maintainers' own recommendation. |
| truststore | PyPI | You only wanted the operating system's certificate store behind the standard library ssl module, which is the actual reason many projects reached for pyOpenSSL. |
| trustme | PyPI | You were generating throwaway certificates for tests and used OpenSSL.crypto to do it; this makes a CA and leaf certs in two lines. |