Chapter 6: TLS, Encryption, and Certificates


Why Encryption Matters

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:

  1. Confidentiality — data is encrypted; eavesdroppers see only ciphertext
  2. Integrity — data cannot be modified in transit without detection
  3. Authentication — you can verify you’re talking to the real server (not an impostor)

TLS is the “S” in HTTPS, and it secures virtually every protocol on the modern Internet: HTTPS, SMTPS, IMAPS, LDAPS, MQTTS, gRPC, and more.


Cryptographic Building Blocks

Before diving into TLS, let’s review the cryptographic primitives it uses.

Symmetric Encryption

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

Asymmetric Encryption (Public Key Cryptography)

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

Hash Functions

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

Digital Signatures

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.


The TLS Handshake

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:


X.509 Certificates and PKI

What is a Certificate?

An X.509 certificate binds a public key to an identity (domain name, organization). It contains:

The Chain of Trust

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

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)

Self-Signed Certificates

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.


TLS in Python

Server-Side TLS

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()

Client-Side TLS

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

Inspecting Certificates in Python

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)

Certificate Management with OpenSSL

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

Mutual TLS (mTLS)

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")

TLS Best Practices

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

Testing Your TLS Configuration

# 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

Key Takeaways


← Previous: Firewalls, NAT, and DMZ Table of Contents Next: Socket Programming Fundamentals →