Every packet crossing a network is potentially visible to anyone on the path — routers, switches, ISPs, attackers on shared Wi-Fi. Without encryption, passwords, API keys, personal data, and business communications travel in plaintext, readable by anyone with a packet sniffer.
Transport Layer Security (TLS) solves this by providing three guarantees:
TLS is the “S” in HTTPS, and it secures virtually every protocol on the modern Internet: HTTPS, SMTPS, IMAPS, LDAPS, MQTTS, gRPC, and more.
Before diving into TLS, let’s review the cryptographic primitives it uses.
A single shared key is used for both encryption and decryption. Fast and efficient for bulk data.
| Algorithm | Key Size | Notes |
|---|---|---|
| AES-128 | 128 bits | Standard, widely used |
| AES-256 | 256 bits | Higher security margin |
| ChaCha20 | 256 bits | Fast in software, used in mobile/embedded |
A key pair — public key and private key — where data encrypted with one can only be decrypted with the other. Slower than symmetric, used for key exchange and authentication.
| Algorithm | Common Key Size | Notes |
|---|---|---|
| RSA | 2048–4096 bits | Traditional, widely supported |
| ECDSA | 256–384 bits | Elliptic curve, smaller keys, same security |
| Ed25519 | 256 bits | Modern, fast, preferred for new systems |
Produce a fixed-size digest from arbitrary input. Any change to the input produces a completely different hash. Used for integrity verification.
| Algorithm | Output Size | Status |
|---|---|---|
| MD5 | 128 bits | Broken — do not use for security |
| SHA-1 | 160 bits | Deprecated — being phased out |
| SHA-256 | 256 bits | Standard, widely used |
| SHA-384 | 384 bits | Higher security margin |
Combine hashing with asymmetric keys: the signer hashes the data and encrypts the hash with their private key. Anyone can verify the signature using the signer’s public key. This proves both the identity of the signer and the integrity of the data.
When a client connects to a TLS-secured server, they perform a handshake to negotiate encryption parameters and exchange keys. Here’s the TLS 1.3 handshake (the current standard):
Client Server
│ │
├── ClientHello ──────────────────► │ Supported cipher suites, key share
│ │
│ ◄────────────── ServerHello ──┤ Selected cipher suite, key share
│ ◄──────── EncryptedExtensions ┤ Server parameters
│ ◄──────────────── Certificate ┤ Server's X.509 certificate
│ ◄────────── CertificateVerify ┤ Proof of private key
│ ◄──────────────────── Finished ┤ Server handshake complete
│ │
├── Finished ─────────────────────► │ Client handshake complete
│ │
│◄══════ Encrypted Application Data ══════►│
Key improvements in TLS 1.3 over 1.2:
An X.509 certificate binds a public key to an identity (domain name, organization). It contains:
CN=example.com)Certificates form a chain from the server’s certificate to a trusted root CA:
Root CA (trusted, pre-installed in OS/browser)
└── Intermediate CA (signed by Root)
└── Server Certificate (signed by Intermediate)
example.com
The client verifies each signature in the chain. If the chain leads to a trusted root CA, the certificate is accepted.
Certificate Authorities (CAs) are organizations trusted to issue certificates. Major CAs include DigiCert, Let’s Encrypt, Sectigo, and GlobalSign.
Let’s Encrypt provides free, automated certificates and has revolutionized TLS adoption:
# Install certbot and obtain a certificate
sudo apt install certbot
sudo certbot certonly --standalone -d example.com
# Certificate files:
# /etc/letsencrypt/live/example.com/fullchain.pem (cert + intermediates)
# /etc/letsencrypt/live/example.com/privkey.pem (private key)
For development and testing, you can create self-signed certificates — certificates signed by their own private key instead of a CA:
# Generate a self-signed certificate with OpenSSL
openssl req -x509 -newkey rsa:4096 -nodes \
-keyout server.key -out server.crt \
-days 365 -subj "/CN=localhost"
Self-signed certificates trigger browser warnings because they’re not trusted by any CA. They’re fine for development but should never be used in production.
Python’s ssl module wraps sockets with TLS:
import ssl
import socket
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain("server.crt", "server.key")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind(("0.0.0.0", 8443))
sock.listen(5)
with context.wrap_socket(sock, server_side=True) as ssock:
conn, addr = ssock.accept()
data = conn.recv(1024)
conn.sendall(b"Hello, TLS!")
conn.close()
import ssl
import socket
context = ssl.create_default_context()
with socket.create_connection(("example.com", 443)) as sock:
with context.wrap_socket(sock, server_hostname="example.com") as ssock:
print(ssock.version()) # e.g., 'TLSv1.3'
print(ssock.cipher()) # e.g., ('TLS_AES_256_GCM_SHA384', 'TLSv1.3', 256)
ssock.sendall(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
print(ssock.recv(4096).decode())
Full example: code/tls_echo_server.py
import ssl
import socket
import pprint
hostname = "example.com"
context = ssl.create_default_context()
with socket.create_connection((hostname, 443)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert = ssock.getpeercert()
pprint.pprint(cert)
OpenSSL is the Swiss Army knife for TLS/certificate operations:
# View certificate details
openssl x509 -in server.crt -text -noout
# Check a remote server's certificate
openssl s_client -connect example.com:443 -servername example.com
# Verify a certificate chain
openssl verify -CAfile ca-bundle.crt server.crt
# Generate a Certificate Signing Request (CSR)
openssl req -new -key server.key -out server.csr
# Convert between formats
openssl x509 -in cert.pem -outform DER -out cert.der # PEM → DER
openssl x509 -in cert.der -inform DER -outform PEM -out cert.pem # DER → PEM
Standard TLS authenticates only the server. Mutual TLS requires the client to also present a certificate, providing two-way authentication. This is common in:
# Server requiring client certificates
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain("server.crt", "server.key")
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations("ca.crt") # CA that signed client certs
# Client presenting its certificate
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.load_cert_chain("client.crt", "client.key")
context.load_verify_locations("ca.crt")
| Practice | Recommendation |
|---|---|
| Protocol version | TLS 1.3 (minimum TLS 1.2) |
| Cipher suites | AES-256-GCM, ChaCha20-Poly1305 |
| Key exchange | ECDHE (forward secrecy) |
| Certificate key | RSA 2048+ or ECDSA P-256+ |
| Certificate validity | 90 days (auto-renew with Let’s Encrypt) |
| HSTS | Enable Strict-Transport-Security header |
| OCSP stapling | Enable for faster certificate revocation checks |
| Certificate pinning | Consider for mobile apps, avoid for websites |
# Quick test with OpenSSL
openssl s_client -connect yourserver.com:443 -tls1_3
# Comprehensive test with testssl.sh
git clone https://github.com/drwetter/testssl.sh
./testssl.sh/testssl.sh yourserver.com
ssl module wraps sockets with TLS for both clients and servers| ← Previous: Firewalls, NAT, and DMZ | Table of Contents | Next: Socket Programming Fundamentals → |