Implementing RSA, AES-GCM, and a TLS 1.3 Handshake from Scratch in Python
A deep-dive into the full cryptographic stack powering every HTTPS connection — RSA-OAEP, AES-GCM, ECDHE key exchange, and a working TLS 1.3 handshake simulation, all built in pure Python from first principles.
Every time your browser connects to https://, a negotiation happens in under a hundred milliseconds that involves three distinct cryptographic systems working in sequence. An asymmetric key exchange establishes a shared secret without ever transmitting it. A key derivation function turns that secret into multiple independent encryption keys. A symmetric cipher uses those keys to encrypt every byte of actual data. And a message authentication code guarantees that none of it was tampered with in transit.
You've been on both ends of this hundreds of times today. You probably couldn't describe the internals of any of it.
We covered the foundations of RSA in the original article. That article was a clean introduction to the math — modular exponentiation, the Extended Euclidean Algorithm, key generation. But it used toy prime numbers, glossed over padding entirely, and stopped well short of anything production-relevant. Most importantly, it didn't answer the question developers actually need answered: how does RSA connect to the AES cipher that encrypts real data, and how does all of it fit into the TLS handshake protocol that runs your HTTPS connections?
This article answers that question end to end. We're building the full cryptographic stack:
- RSA done correctly — Miller-Rabin prime generation, OAEP padding, PSS signatures, timing attack awareness
- AES from first principles — S-box, SubBytes, ShiftRows, MixColumns, KeyExpansion, full AES-128
- AES-GCM — why authenticated encryption exists and how it works (GHASH + CTR mode)
- ECDHE key exchange — why TLS 1.3 killed static RSA key exchange and what replaced it
- A working TLS 1.3 handshake simulation — HKDF key derivation, encrypted handshake, the full protocol flow
Every section has working Python code you can run. None of it uses cryptographic libraries for the core implementations — the point is to understand what those libraries are doing, not to hide it behind an API call. At the end, we'll compare our from-scratch implementations to cryptography and PyCryptodome to verify correctness.
One disclaimer before we start: do not use this code in production. We're implementing these systems to understand them. Production cryptography requires constant-time implementations to prevent timing attacks, careful memory handling, hardened random number generation, and years of peer review. This is an education tool. The cryptography library exists for a reason.
With that said — let's build it.