Enforce TLS 1.3 with Python ssl.SSLContext
This guide outlines the precise settings required to strictly enforce
TLS 1.3 encryption using Python's standard ssl.SSLContext.
Enforcing modern transport security protects applications from legacy
cryptographic vulnerabilities by restricting negotiations solely to the
TLS 1.3 protocol. Below, you will find the prerequisite environment
requirements, context initialization methods, protocol constraints, and
a complete code example.
Prerequisites
To use TLS 1.3 in Python, ensure your environment meets the following baseline requirements:
- Python Version: Python 3.7 or higher.
- OpenSSL Version: OpenSSL 1.1.1 or higher (Python must be compiled against this version).
You can verify your OpenSSL version in Python with:
import ssl
print(ssl.OPENSSL_VERSION)Essential
ssl.SSLContext Configurations
To mandate TLS 1.3 and reject all previous protocols (such as TLS 1.2, TLS 1.0, and SSLv3), apply the following properties to your context:
1. Initialize Context with Modern Protocol Constants
Avoid legacy protocol constants such as
ssl.PROTOCOL_TLSv1_2 or ssl.PROTOCOL_SSLv23.
Instead, initialize using standard purpose-based constructors:
- For clients:
ssl.create_default_context()orssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - For servers:
ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
2. Restrict Protocol Versions
The most reliable way to enforce TLS 1.3 is by defining explicit
version bounds using ssl.TLSVersion:
context.minimum_version = ssl.TLSVersion.TLSv1_3
context.maximum_version = ssl.TLSVersion.TLSv1_3Setting both minimum_version and
maximum_version ensures that the handshake will immediately
fail if the remote peer cannot negotiate TLS 1.3.
3. Handling Cipher Suites
Unlike earlier protocols, TLS 1.3 uses a completely distinct set of
cipher suites (e.g., TLS_AES_256_GCM_SHA384,
TLS_CHACHA20_POLY1305_SHA256,
TLS_AES_128_GCM_SHA256).
The standard context.set_ciphers() method only affects
TLS 1.2 and earlier ciphers in OpenSSL. By locking the protocol version
strictly to ssl.TLSVersion.TLSv1_3, the context
automatically uses OpenSSL's secure, built-in TLS 1.3 cipher suites.
Complete Client Implementation
Below is a complete implementation showing how to enforce TLS 1.3 when establishing a client socket:
import socket
import ssl
hostname = "example.com"
port = 443
# 1. Initialize the client context
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
# 2. Load standard system CA certificates
context.load_default_certs()
# 3. Restrict negotiation strictly to TLS 1.3
context.minimum_version = ssl.TLSVersion.TLSv1_3
context.maximum_version = ssl.TLSVersion.TLSv1_3
# 4. Enforce certificate validation and hostname checking
context.verify_mode = ssl.CERT_REQUIRED
context.check_hostname = True
# 5. Connect and verify
with socket.create_connection((hostname, port)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
print(f"Negotiated Protocol: {ssock.version()}")
print(f"Negotiated Cipher: {ssock.cipher()}")Complete Server Implementation
For a server, supply your certificate and private key, while locking down the same version boundaries:
import socket
import ssl
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
# Load server certificate and private key
context.load_cert_chain(certfile="cert.pem", keyfile="key.pem")
# Enforce TLS 1.3 only
context.minimum_version = ssl.TLSVersion.TLSv1_3
context.maximum_version = ssl.TLSVersion.TLSv1_3
# Bind and listen
bind_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
bind_socket.bind(("0.0.0.0", 8443))
bind_socket.listen(5)
with context.wrap_socket(bind_socket, server_side=True) as server_sock:
conn, addr = server_sock.accept()
with conn:
print(f"Connection accepted from {addr} using {conn.version()}")