Skillversitas

TLS with the ssl Module

The standard library's ssl module wraps a socket with TLS, built on the platform's OpenSSL. It is the same engine behind requests and urllib, and it is what you reach for when you need a raw TLS connection - a custom protocol, a test server in a script, or SMTP/IMAP over TLS without a third-party client.

A Verifying Client

import ssl
import socket

ctx = ssl.create_default_context()   # system CAs + hostname verification

with socket.create_connection(("example.com", 443)) as sock:
    with ctx.wrap_socket(sock, server_hostname="example.com") as ssock:
        print(ssock.version())        # TLSv1.3 (or 1.2)
        print(ssock.cipher_name())    # e.g. TLS_AES_256_GCM_SHA384
        ssock.sendall(b"GET / HTTP/1.0\r\n\r\n")
        print(ssock.recv(200))

create_default_context() is the safe default: it loads the system CA bundle, requires a valid certificate, and checks the hostname. A SSLCertVerificationError means the chain, validity window, or name failed - treat it as a failure, not something to silence in production.

A TLS Server

import ssl
import socket

ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain("server.crt", "server.key")   # cert + private key
ctx.minimum_version = ssl.TLSVersion.TLSv1_2

with socket.create_server(("127.0.0.1", 8443)) as listener:
    conn, addr = listener.accept()
    with ctx.wrap_socket(conn, server_side=True) as tls:
        print(tls.recv(1024))

For clients you serve, add ctx.load_verify_locations("ca.crt") and ctx.verify_mode = ssl.CERT_REQUIRED to require and validate client certificates (mutual TLS).

Self-Signed Certificates

Local development usually runs against self-signed certs. The honest options, in order of preference:

  • Add the dev CA to the trust store - the system store, or your ctx via load_verify_locations - and keep verification on.
  • Use ssl._create_unverified_context(), the stdlib's explicit escape hatch, for throwaway tests only.
  • Manually: ctx.check_hostname = False plus ctx.verify_mode = ssl.CERT_NONE - a smell in production code.

Reading the Negotiated Session

After wrapping, the socket reports what was actually negotiated: version(), cipher_name(), and getpeercert() (the peer certificate as a dict). Logging these in your test suite makes "we negotiated TLS 1.0 with a weak cipher" visible before it reaches production, and getpeercert() is how you extract the client identity in a mutual-TLS setup.

Summary

Build clients with create_default_context() plus wrap_socket(server_hostname=...), serve with PROTOCOL_TLS_SERVER and a loaded cert chain, and keep hostname verification on in production. Self-signed convenience belongs in the test suite, never in the request path.