Understanding HTTPS

http_
Understanding HTTPS Encryption with Python

Understanding HTTPS Encryption: A Practical Guide with Python

Every time you see the padlock icon in your browser's address bar, HTTPS is quietly doing a lot of cryptographic work behind the scenes. In this post, we'll break down exactly how HTTPS encryption works, and then use Python to actually demonstrate the core ideas — key exchange, symmetric encryption, and certificate verification — so the concepts stop feeling abstract.

What you'll learn: The difference between HTTP and HTTPS, how the TLS handshake establishes a secure channel, the roles of symmetric and asymmetric encryption, and hands-on Python code you can run yourself.

1. What is HTTPS, really?

HTTPS (HyperText Transfer Protocol Secure) is just HTTP wrapped inside a security layer called TLS (Transport Layer Security) — the modern successor to SSL. It doesn't change what data is sent; it changes how that data travels between your browser and the server.

HTTPS provides three guarantees:

PropertyWhat it means
ConfidentialityData is encrypted, so eavesdroppers on the network can't read it
IntegrityData can't be silently modified in transit without detection
AuthenticationYou can verify you're actually talking to the real server, not an impostor

2. Symmetric vs. Asymmetric Encryption

HTTPS actually uses both types of encryption, at different stages:

  • Asymmetric encryption (e.g., RSA, ECDHE) uses a public/private key pair. It's used briefly at the start of the connection to safely agree on a shared secret — but it's computationally expensive.
  • Symmetric encryption (e.g., AES) uses a single shared key that both sides use to encrypt and decrypt. It's fast, so it's used for the actual bulk of data transfer once the connection is established.

Demo: Asymmetric key exchange concept in Python

Let's simulate the core idea using the cryptography library. Install it first with pip install cryptography.

from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes

# --- Server generates an RSA key pair ---
server_private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048
)
server_public_key = server_private_key.public_key()

# --- Client creates a random "session secret" ---
session_secret = b"a-random-32-byte-shared-secret!"

# --- Client encrypts it using the SERVER'S public key ---
encrypted_secret = server_public_key.encrypt(
    session_secret,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)

print("Encrypted secret sent over the network:")
print(encrypted_secret[:32], "...")

# --- Only the server's PRIVATE key can decrypt it ---
decrypted_secret = server_private_key.decrypt(
    encrypted_secret,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)

print("Server successfully decrypted:", decrypted_secret)

This mirrors what happens conceptually during a TLS handshake: the client uses the server's public key (found in its certificate) to safely transmit a secret that only the server's private key can unlock. No one intercepting the encrypted blob on the network can read it.

Modern TLS mostly uses ECDHE (Elliptic Curve Diffie-Hellman Ephemeral) instead of plain RSA encryption for key exchange, because it provides "forward secrecy" — but the RSA example above is easier to follow conceptually and demonstrates the same public/private key principle.

Demo: Symmetric encryption with AES

Once a shared secret is established, HTTPS switches to fast symmetric encryption for the actual data:

from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os

# The shared secret from the handshake becomes the AES key
key = AESGCM.generate_key(bit_length=256)
aesgcm = AESGCM(key)

nonce = os.urandom(12)  # unique per message
plaintext = b"GET /login HTTP/1.1\r\nHost: example.com\r\n"

# Encrypt (this is what actually travels over the wire)
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data=None)
print("Encrypted HTTP request:", ciphertext[:32], "...")

# Decrypt on the receiving end
decrypted = aesgcm.decrypt(nonce, ciphertext, associated_data=None)
print("Decrypted:", decrypted)

Notice how much faster and simpler this is compared to RSA — that's exactly why TLS uses asymmetric crypto only to bootstrap a symmetric key, then switches over.

3. The TLS Handshake, Step by Step

1Client Hello — your browser sends supported TLS versions and cipher suites to the server.

2Server Hello + Certificate — the server responds with its chosen cipher suite and its SSL/TLS certificate, containing its public key.

3Certificate Verification — the browser checks the certificate against trusted Certificate Authorities (CAs) to confirm the server is who it claims to be.

4Key Exchange — client and server derive a shared symmetric session key (using ECDHE/RSA as shown above).

5Finished — both sides confirm the handshake and switch to encrypted, symmetric communication using AES (or similar).

4. Inspecting a Real HTTPS Certificate with Python

You don't need to build TLS yourself — Python's standard library and requests already handle it. But you can inspect the certificate details for any live site:

import ssl
import socket

hostname = "www.google.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()
        print("TLS version:", ssock.version())
        print("Cipher used:", ssock.cipher())
        print("Issued to:", cert.get('subject'))
        print("Issued by:", cert.get('issuer'))
        print("Valid until:", cert.get('notAfter'))

Running this prints exactly which cipher suite and TLS version were negotiated, along with who issued the certificate — the same trust chain your browser checks silently every time you visit a site.

Making a simple HTTPS request

import requests

response = requests.get("https://example.com")
print("Status:", response.status_code)
print("Was the connection encrypted?", response.url.startswith("https"))
Never disable certificate verification (e.g., verify=False in requests) in production code. Doing so removes protection against man-in-the-middle attacks, defeating the entire purpose of HTTPS.

5. Why This Matters

Without HTTPS, anyone on the same network — a public Wi-Fi hotspot, a compromised router, an ISP — can read or tamper with the data you send, including passwords, cookies, and personal information. HTTPS closes that gap by combining:

  • Asymmetric cryptography to safely exchange a secret without ever transmitting it in the clear
  • Symmetric cryptography to encrypt the actual traffic efficiently
  • Digital certificates and Certificate Authorities to prevent impersonation

Conclusion

HTTPS isn't magic — it's a well-orchestrated handshake between two well-understood cryptographic techniques. By simulating each piece in Python, you can see exactly how a browser and server go from "hello" to a fully encrypted channel in milliseconds. The next time you see that padlock icon, you'll know precisely what it represents.


Tip for Blogger: paste the content between the <body> tags into the HTML view of your post editor. If you want the styling to carry over cleanly, keep the <style> block as well — Blogger will preserve it within the post.

Post a Comment

0 Comments