TLS/SSL & Secure Communications
Handshake, Certificates, mTLS, Certificate Pinning
Introduction
TLS (Transport Layer Security) is the protocol that secures the internet. Every HTTPS request, every secure email, every VPN tunnel uses TLS. Understanding TLS deeply, the handshake, certificates, and trust chains, is essential for securing any networked application. This lesson explains how TLS works, implements it in Python, generates self-signed certificates, and covers mutual TLS for service-to-service authentication.
TLS 1.3 Handshake
The TLS handshake establishes a secure channel before any application data is sent. TLS 1.3 reduced this to 1 round-trip (from 2 in TLS 1.2), significantly improving latency.
X.509 Certificates & Chain of Trust
A certificate is a digitally signed document that binds a public key to an identity. The trust chain proves legitimacy: your browser trusts a Root CA, which signed an Intermediate CA, which signed the server's certificate.
Generating a Self-Signed Certificate
Install: pip install cryptography
# generate_cert.py, Generate a self-signed cert with Python
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from datetime import datetime, timezone, timedelta
import ipaddress
# Generate RSA private key
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
# Build certificate
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "ByteCode Solutions"),
x509.NameAttribute(NameOID.COMMON_NAME, "localhost"),
])
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.now(timezone.utc))
.not_valid_after(datetime.now(timezone.utc) + timedelta(days=365))
.add_extension(
x509.BasicConstraints(ca=True, path_length=None),
critical=True,
)
.add_extension(
x509.SubjectAlternativeName([
x509.DNSName("localhost"),
x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")),
]),
critical=False,
)
.sign(private_key, hashes.SHA256())
)
# Save key and certificate to PEM files
with open("server.key", "wb") as f:
f.write(private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
))
with open("server.crt", "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
print(f"Generated: server.key, server.crt")
print(f"Subject: {cert.subject.rfc4514_string()}")
print(f"Valid: {cert.not_valid_before_utc} → {cert.not_valid_after_utc}")
print(f"Key size: {private_key.key_size} bits")Expected Output:
Generated: server.key, server.crt Subject: CN=localhost,O=ByteCode Solutions,C=US Valid: 2024-01-15 00:00:00+00:00 → 2025-01-15 00:00:00+00:00 Key size: 2048 bits
Python TLS Server & Client
generate_cert.py from the previous section first to generate server.crt and server.key in your working directory.TLS Server
# tls_server.py, TLS echo server (requires server.key + server.crt)
import socket
import ssl
HOST = "127.0.0.1"
PORT = 9443
# Server SSL context, present server certificate
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain("server.crt", "server.key")
context.minimum_version = ssl.TLSVersion.TLSv1_3 # Require TLS 1.3
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as raw:
raw.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
raw.bind((HOST, PORT))
raw.listen(5)
print(f"TLS server listening on {HOST}:{PORT}")
with context.wrap_socket(raw, server_side=True) as tls_server:
conn, addr = tls_server.accept()
with conn:
print(f"TLS connection from {addr}")
print(f" Cipher: {conn.cipher()}")
print(f" TLS version: {conn.version()}")
data = conn.recv(1024)
conn.sendall(data) # echo backExpected Output:
TLS server listening on 127.0.0.1:9443
TLS connection from ('127.0.0.1', 42592)
Cipher: ('TLS_AES_256_GCM_SHA384', 'TLSv1.3', 256)
TLS version: TLSv1.3TLS Client
# tls_client.py, TLS client (trust the self-signed cert)
import socket
import ssl
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE # dev only: skip verification for self-signed cert
context.minimum_version = ssl.TLSVersion.TLSv1_2 # TLS 1.2+ (use TLSv1_3 in production)
with socket.create_connection(("127.0.0.1", 9443)) as raw:
with context.wrap_socket(raw, server_hostname="localhost") as tls:
print(f"Connected, TLS {tls.version()}, {tls.cipher()[0]}")
tls.sendall(b"Hello over TLS!")
response = tls.recv(1024)
print(f"Echo: {response.decode()}")Expected Output:
Connected, TLS TLSv1.3, TLS_AES_256_GCM_SHA384 Echo: Hello over TLS!
Mutual TLS (mTLS)
Standard TLS authenticates only the server. Mutual TLS (mTLS) requires both the client and server to present certificates. This is used in service meshes (Istio), Kubernetes, zero-trust networks, and B2B API authentication.
Generate Client Certificate
Install: pip install cryptography
# generate_client_cert.py, Generate a self-signed client cert for mTLS
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from datetime import datetime, timezone, timedelta
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "ByteCode Solutions"),
x509.NameAttribute(NameOID.COMMON_NAME, "service-a"), # identifies the client
])
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.now(timezone.utc))
.not_valid_after(datetime.now(timezone.utc) + timedelta(days=365))
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
.sign(private_key, hashes.SHA256())
)
with open("client.key", "wb") as f:
f.write(private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
))
with open("client.crt", "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
print("Generated: client.key, client.crt (CN=service-a)")Expected Output:
Generated: client.key, client.crt (CN=service-a)
mTLS Server
# mtls_server.py, Mutual TLS server
import socket
import ssl
# Server requires client to present a certificate
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_context.load_cert_chain("server.crt", "server.key")
server_context.verify_mode = ssl.CERT_REQUIRED # ← require client cert
server_context.load_verify_locations("client.crt") # trust this client CA
with socket.socket() as raw:
raw.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
raw.bind(("127.0.0.1", 9444))
raw.listen(5)
with server_context.wrap_socket(raw, server_side=True) as tls_srv:
conn, addr = tls_srv.accept()
with conn:
# Inspect client certificate
client_cert = conn.getpeercert()
cn = dict(x[0] for x in client_cert["subject"]).get("commonName")
print(f"Authenticated client CN: {cn}")
conn.sendall(b"mTLS handshake complete!")Expected Output:
Authenticated client CN: service-a
mTLS Client
# mtls_client.py, Mutual TLS client
import socket
import ssl
client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client_context.check_hostname = False
client_context.verify_mode = ssl.CERT_NONE # dev only: skip verification for self-signed cert
client_context.load_cert_chain("client.crt", "client.key") # present client cert
with socket.create_connection(("127.0.0.1", 9444)) as raw:
with client_context.wrap_socket(raw, server_hostname="localhost") as tls:
response = tls.recv(1024)
print(f"Server: {response.decode()}")Expected Output:
Server: mTLS handshake complete!
Key Takeaways
- TLS 1.3 is the current standard, 1-RTT handshake, forward secrecy by default; disable TLS 1.0/1.1
- Certificates bind identity to public key, signed by a CA in a chain of trust rooted at a trusted Root CA
- ECDHE provides forward secrecy, even if the server key is later compromised, past sessions remain secure
- Always verify certificates, check hostname and chain; never use
ssl.CERT_NONEin production - mTLS authenticates both sides, essential for zero-trust service meshes and B2B API authentication
- Self-signed certs are for testing only, production systems require CA-signed certificates (e.g., Let's Encrypt)
What's Next?
Lesson 10 moves from transport security to application security, authenticating users with sessions, JWTs, and OAuth2.
- Session-based authentication, server-side sessions with Flask
- JWT, creating, signing, and verifying tokens with PyJWT
- OAuth2 flows, Authorization Code and Client Credentials
- Token refresh patterns, maintaining long-lived authentication securely