TLS/SSL and Certificates#
Transport Layer Security (TLS) provides encrypted, authenticated communication
over networks. This guide covers creating secure HTTPS servers, generating
certificates, and proper TLS configuration in Python. We use the ssl module’s
SSLContext API (not the deprecated wrap_socket) and the cryptography
library for certificate operations. Always use TLS 1.2 or 1.3—older versions have
known vulnerabilities.
Warning
For production, always use certificates from a trusted Certificate Authority (CA) like Let’s Encrypt. Self-signed certificates are only for development and testing. Never disable certificate verification in production code.
Secure HTTPS Server#
Create an HTTPS server using SSLContext with secure defaults. The context
configures TLS version, cipher suites, and certificate verification. Always
load both the certificate chain and private key. For production, use certificates
from a real CA.
import ssl
from http.server import HTTPServer, SimpleHTTPRequestHandler
def create_secure_context(certfile, keyfile):
"""Create SSLContext with secure defaults."""
# TLS 1.2+ only, secure ciphers
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.minimum_version = ssl.TLSVersion.TLSv1_2
# Load certificate and private key
context.load_cert_chain(certfile=certfile, keyfile=keyfile)
# Disable insecure options
context.options |= ssl.OP_NO_SSLv2
context.options |= ssl.OP_NO_SSLv3
context.options |= ssl.OP_NO_TLSv1
context.options |= ssl.OP_NO_TLSv1_1
return context
# Create server
host, port = "localhost", 8443
context = create_secure_context("cert.pem", "key.pem")
httpd = HTTPServer((host, port), SimpleHTTPRequestHandler)
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
print(f"Serving HTTPS on https://{host}:{port}")
httpd.serve_forever()
Secure HTTPS Client#
When making HTTPS requests, Python verifies certificates by default. For custom
CA certificates or client authentication, configure an SSLContext. Never
set verify=False or disable hostname checking in production.
import ssl
import urllib.request
# Default secure context (verifies certificates)
context = ssl.create_default_context()
# Make HTTPS request
url = "https://example.com"
with urllib.request.urlopen(url, context=context) as response:
print(response.read().decode())
# Custom CA certificate (e.g., internal CA)
context = ssl.create_default_context()
context.load_verify_locations("internal-ca.pem")
# Client certificate authentication (mTLS)
context = ssl.create_default_context()
context.load_cert_chain(certfile="client.pem", keyfile="client-key.pem")
# Using requests library (recommended for HTTP)
import requests
# Default (secure)
response = requests.get("https://example.com")
# Custom CA
response = requests.get("https://internal.example.com", verify="internal-ca.pem")
# Client certificate
response = requests.get(
"https://secure.example.com",
cert=("client.pem", "client-key.pem"),
)
Generate Self-Signed Certificate#
Self-signed certificates are useful for development and testing. The certificate
is signed by its own private key rather than a CA. Browsers will show warnings
for self-signed certificates. Use the cryptography library for certificate
generation—it’s more Pythonic than calling OpenSSL.
import ipaddress
from datetime import datetime, timedelta
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
# Generate private key
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=4096,
)
# Certificate subject and issuer (same for self-signed)
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "California"),
x509.NameAttribute(NameOID.LOCALITY_NAME, "San Francisco"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "My Organization"),
x509.NameAttribute(NameOID.COMMON_NAME, "localhost"),
])
# Build certificate
cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(private_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.utcnow())
.not_valid_after(datetime.utcnow() + timedelta(days=365))
.add_extension(
x509.SubjectAlternativeName([
x509.DNSName("localhost"),
x509.DNSName("*.localhost"),
x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")),
]),
critical=False,
)
.add_extension(
x509.BasicConstraints(ca=False, path_length=None),
critical=True,
)
.sign(private_key, hashes.SHA256())
)
# Save private key
with open("key.pem", "wb") as f:
f.write(private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
))
# Save certificate
with open("cert.pem", "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
print("Generated key.pem and cert.pem")
Generate Certificate Signing Request (CSR)#
A CSR is sent to a Certificate Authority to obtain a signed certificate. It contains your public key and identity information. The CA verifies your identity and returns a signed certificate. Keep your private key secret—never send it to the CA.
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
# Generate private key (keep this secret!)
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=4096,
)
# Build CSR
csr = (
x509.CertificateSigningRequestBuilder()
.subject_name(x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "California"),
x509.NameAttribute(NameOID.LOCALITY_NAME, "San Francisco"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "My Company"),
x509.NameAttribute(NameOID.COMMON_NAME, "www.example.com"),
]))
.add_extension(
x509.SubjectAlternativeName([
x509.DNSName("www.example.com"),
x509.DNSName("example.com"),
x509.DNSName("api.example.com"),
]),
critical=False,
)
.sign(private_key, hashes.SHA256())
)
# Save private key
with open("private.key", "wb") as f:
f.write(private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
))
# Save CSR (send this to CA)
with open("request.csr", "wb") as f:
f.write(csr.public_bytes(serialization.Encoding.PEM))
print("Generated private.key and request.csr")
print("Send request.csr to your CA, keep private.key secret!")
Read Certificate Information#
Parse and inspect X.509 certificates to view subject, issuer, validity period, extensions, and other attributes. Useful for debugging certificate issues.
from cryptography import x509
from cryptography.hazmat.primitives import serialization
# Load certificate from file
with open("cert.pem", "rb") as f:
cert = x509.load_pem_x509_certificate(f.read())
# Basic information
print(f"Subject: {cert.subject}")
print(f"Issuer: {cert.issuer}")
print(f"Serial: {cert.serial_number}")
print(f"Not Before: {cert.not_valid_before}")
print(f"Not After: {cert.not_valid_after}")
# Get specific subject attributes
cn = cert.subject.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME)
if cn:
print(f"Common Name: {cn[0].value}")
# Check extensions
try:
san = cert.extensions.get_extension_for_oid(
x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME
)
print(f"SANs: {san.value.get_values_for_type(x509.DNSName)}")
except x509.ExtensionNotFound:
print("No SAN extension")
# Check if self-signed
is_self_signed = cert.subject == cert.issuer
print(f"Self-signed: {is_self_signed}")
# Verify certificate signature (self-signed only)
if is_self_signed:
public_key = cert.public_key()
try:
# This verifies the certificate was signed by its own key
public_key.verify(
cert.signature,
cert.tbs_certificate_bytes,
cert.signature_algorithm_parameters,
)
print("Signature valid")
except Exception as e:
print(f"Signature invalid: {e}")
TLS Version and Cipher Information#
Inspect TLS connection details including protocol version, cipher suite, and peer certificate. Useful for debugging and security auditing.
import ssl
import socket
def get_tls_info(hostname, port=443):
"""Get TLS connection information for a host."""
context = ssl.create_default_context()
with socket.create_connection((hostname, port)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
print(f"TLS Version: {ssock.version()}")
print(f"Cipher: {ssock.cipher()}")
# Peer certificate
cert = ssock.getpeercert()
print(f"Subject: {dict(x[0] for x in cert['subject'])}")
print(f"Issuer: {dict(x[0] for x in cert['issuer'])}")
print(f"Not Before: {cert['notBefore']}")
print(f"Not After: {cert['notAfter']}")
# Subject Alternative Names
if 'subjectAltName' in cert:
sans = [x[1] for x in cert['subjectAltName']]
print(f"SANs: {sans}")
get_tls_info("www.google.com")
Certificate Pinning#
Certificate pinning adds an extra layer of security by verifying the server’s certificate matches an expected value. This prevents attacks using fraudulently issued certificates. Pin the public key (SPKI) rather than the certificate to survive certificate renewals.
import ssl
import socket
import hashlib
from cryptography import x509
from cryptography.hazmat.primitives import serialization
def get_certificate_pin(hostname, port=443):
"""Get the SPKI pin for a certificate."""
context = ssl.create_default_context()
with socket.create_connection((hostname, port)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
# Get certificate in DER format
der_cert = ssock.getpeercert(binary_form=True)
# Parse certificate
cert = x509.load_der_x509_certificate(der_cert)
# Get public key in DER format (SPKI)
spki = cert.public_key().public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
# SHA-256 hash of SPKI
pin = hashlib.sha256(spki).digest()
return pin
def verify_pin(hostname, expected_pin, port=443):
"""Verify certificate matches expected pin."""
actual_pin = get_certificate_pin(hostname, port)
if actual_pin != expected_pin:
raise ssl.SSLError(f"Certificate pin mismatch for {hostname}")
print(f"Pin verified for {hostname}")
# Get pin (do this once, store the result)
pin = get_certificate_pin("www.google.com")
print(f"Pin (base64): {__import__('base64').b64encode(pin).decode()}")
# Verify pin on subsequent connections
verify_pin("www.google.com", pin)