Chapter 19 — RSA
In one sentence
RSA is the original practical public-key algorithm: its security rests on the difficulty of factoring the product of two large primes, and while it still works, newer systems prefer elliptic curves.
The idea
Multiplying two large primes is easy. Factoring the result back into those primes is — as far as anyone knows publicly — extraordinarily hard.
61 x 53 = 3233 <- a child can do this
factor 3233 -> ? <- have to search
Now imagine each prime is ~1024 bits (~300 decimal digits).
Multiplying: instant. Factoring: no classical computer will finish
before the sun burns out.
How it actually works (small numbers, fully worked)
KEY GENERATION
1. Pick two primes: p = 61, q = 53
2. Modulus: n = p*q = 3233
3. Euler's totient: phi = (p-1)(q-1) = 60*52 = 3120
4. Pick public exponent: e = 17 (must be coprime to phi;
real world: almost always 65537)
5. Private exponent: d = e^-1 mod phi = 2753
(check: 17 * 2753 = 46801 = 15*3120 + 1 OK)
PUBLIC KEY = (n=3233, e=17)
PRIVATE KEY = (n=3233, d=2753) [p, q, phi are kept or destroyed]
ENCRYPT a message m = 65 (must have m < n)
c = m^e mod n = 65^17 mod 3233 = 2790
DECRYPT
m = c^d mod n = 2790^2753 mod 3233 = 65 <- back to the original
SIGN m = 65 (private-key operation -- the mirror of decryption)
s = m^d mod n = 65^2753 mod 3233 = 588
VERIFY
m = s^e mod n = 588^17 mod 3233 = 65 <- matches, so signature valid
The symmetry is the elegance: encryption and signing are the same operation with the keys swapped. (In practice they use different padding schemes and you should never use one key for both.)
The parts you must never skip: padding
"Textbook RSA" — c = m^e mod n with no padding — is badly broken.
- Deterministic: same message -> same ciphertext. Attacker can build a
dictionary, or detect repeats.
- Malleable: c1 * c2 mod n decrypts to m1 * m2. Attacker can manipulate
plaintext without decrypting.
- Small messages: if m^e < n (e.g. m=2, e=3), just take the cube root.
Real RSA wraps the message in a randomised, structured padding:
| Scheme | Use | Status |
|---|---|---|
| OAEP | RSA encryption | Use this |
| PKCS#1 v1.5 (encryption) | legacy encryption | Avoid — Bleichenbacher padding-oracle attacks, still resurfacing (ROBOT, 2017) |
| PSS | RSA signatures | Use this |
| PKCS#1 v1.5 (signatures) | very common, still OK | Acceptable; PSS preferred |
What RSA is actually used for
RSA does not encrypt your file. It encrypts (or "encapsulates") a small random symmetric key, which then encrypts the file. The maximum message size is tiny:
RSA-2048 with OAEP-SHA256: can encrypt at most 190 bytes.
That is enough for an AES-256 key (32 bytes) and nothing more.
This is BY DESIGN -- RSA is a key-transport mechanism, not a data cipher.
Uses today: TLS certificates and signatures (still the majority of the web's
certs), code signing, JWT RS256, SSH keys, PGP.
Key sizes and the slow decline
| RSA key size | Symmetric-equivalent strength | Verdict |
|---|---|---|
| 1024-bit | ~80 bits | Broken-ish — do not use, feasible for nation-states |
| 2048-bit | ~112 bits | Minimum acceptable today |
| 3072-bit | ~128 bits | Good; matches AES-128 |
| 4096-bit | ~140 bits | Fine, noticeably slower, common for long-lived roots |
Note how much key you need for relatively modest strength — and RSA operations get slow fast as the key grows. A 256-bit elliptic-curve key matches RSA-3072. That efficiency gap is why new systems pick ECC (Chapter 20).
RSA's real-world failure modes
Every one of these has caused real breaches:
[ ] Weak randomness at key generation -> two keys share a prime -> both
factor instantly (the 2012 "Mining your Ps and Qs" study found tens of
thousands of such keys live on the internet).
[ ] PKCS#1 v1.5 padding oracle (Bleichenbacher '98, revived as ROBOT 2017)
-> decrypt or sign without the key.
[ ] Using the SAME keypair for both encryption and signing.
[ ] Small or shared public exponent (e = 3) with no/broken padding.
[ ] Signature verification that doesn't check the padding structure
-> forged signatures (the "BERserk" / e=3 class).
[ ] Timing / power side channels during the private-key operation
-> use a library with blinding.
Worked example (run it)
cd ~/sec-lab/scratch
# --- the toy version, by hand ---
python3 - <<'PY'
def inv(a, m):
g, x = m, 0
x0, a0 = 1, a
while a0:
q = g // a0
g, a0 = a0, g - q*a0
x, x0 = x0, x - q*x0
return x % m
p, q = 61, 53
n = p*q
phi = (p-1)*(q-1)
e = 17
d = inv(e, phi)
print(f"n={n} phi={phi} e={e} d={d}")
m = 65
c = pow(m, e, n); print(f"encrypt {m} -> {c}")
print(f"decrypt {c} -> {pow(c, d, n)}")
s = pow(m, d, n); print(f"sign {m} -> {s}")
print(f"verify {s} -> {pow(s, e, n)}")
# malleability of TEXTBOOK RSA -- why padding exists
c2 = pow(2, e, n)
print(f"\n(c * enc(2)) mod n decrypts to {pow(c*c2, d, n)} == 65*2 = 130")
print("attacker doubled the plaintext without decrypting. OAEP prevents this.")
PY
# --- real RSA with OpenSSL ---
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out rsa.pem
openssl pkey -in rsa.pem -pubout -out rsa.pub
# inspect it
openssl pkey -in rsa.pem -text -noout | head -20
# encrypt a SMALL secret with OAEP (this is real OpenSSL 3.x syntax)
echo -n "this is a 32-byte AES key ......." > aeskey.bin
openssl pkeyutl -encrypt -pubin -inkey rsa.pub -in aeskey.bin -out aeskey.enc \
-pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256
openssl pkeyutl -decrypt -inkey rsa.pem -in aeskey.enc \
-pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256
# sign a document with PSS
echo "transfer 100 to alice" > doc.txt
openssl dgst -sha256 -sign rsa.pem -sigopt rsa_padding_mode:pss -out doc.sig doc.txt
openssl dgst -sha256 -verify rsa.pub -sigopt rsa_padding_mode:pss \
-signature doc.sig doc.txt # -> "Verified OK"
# tamper and re-verify
echo "transfer 100 to mallory" > doc.txt
openssl dgst -sha256 -verify rsa.pub -sigopt rsa_padding_mode:pss \
-signature doc.sig doc.txt # -> "Verification Failure"
macOS note: the
pkeyutl -encryptline needs OpenSSL 3.x; the LibreSSL build fails on the key format. The Python equivalent below always works.
.venv/bin/python - <<'PY'
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
pub = key.public_key()
# encrypt a 32-byte key with OAEP
secret = b"x" * 32
ct = pub.encrypt(secret, padding.OAEP(
mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None))
print("ciphertext bytes:", len(ct)) # 256 for RSA-2048
print("decrypts back:", key.decrypt(ct, padding.OAEP(
mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(),
label=None)) == secret)
# how big can the message be?
for size in (32, 190, 191, 245):
try:
pub.encrypt(b"x"*size, padding.OAEP(
mgf=padding.MGF1(hashes.SHA256()),
algorithm=hashes.SHA256(), label=None))
print(f" {size} bytes: OK")
except ValueError:
print(f" {size} bytes: TOO BIG for RSA-2048 + OAEP-SHA256")
PY
Practice (35 min)
- Do the toy RSA by hand on paper with
p=11, q=17, e=7. Computen,phi,d. Encryptm=8. Check yourdwith Python. - Generate a real 2048-bit key. Time 1000 signatures vs 1000 verifications
(
openssl speed rsa2048). Notice signing (private key) is ~30x slower than verifying. Explain why that shapes protocol design. - Run the malleability demo. Then read one paragraph on OAEP and write, in
notes.md, how it breaks the attack. - Find the largest message RSA-2048+OAEP-SHA256 will encrypt (the Python loop above). Explain why this makes RSA a key-transport primitive.
- Generate a 1024-bit key and a 4096-bit key. Compare
openssl speednumbers across all three sizes and tabulate them.
Common confusions
- "RSA encrypts my files." It encrypts a ~32-byte symmetric key. AES encrypts the files.
- "Textbook RSA is RSA." Textbook (unpadded) RSA is a broken toy. Real RSA is RSA + OAEP or RSA + PSS.
- "RSA-4096 is twice as strong as RSA-2048." It's roughly 112 → 140 bits. Diminishing returns, rising cost.
- "RSA is quantum-safe if the key is big enough." No. Shor's algorithm factors any RSA key in polynomial time on a large quantum computer. Size does not help. (Part 6.)
- "I'll use my RSA key for both TLS and signing releases." Use separate keys with separate purposes. Cross-protocol attacks are real.
Check yourself
- What hard problem is RSA based on?
- What is the usual value of
e, and why that number? - Why is textbook (unpadded) RSA insecure? Name two reasons.
- What is the maximum useful message size for RSA-2048, and what does that tell you about RSA's role?
- Which padding scheme for encryption? Which for signatures?
- Does increasing the RSA key size protect against quantum attacks?
Further reading
- Original paper: "A Method for Obtaining Digital Signatures and Public-Key Cryptosystems" — Rivest, Shamir, Adleman, 1978.
- Article: "Seriously, stop using RSA" (Trail of Bits blog) — a well-argued case for preferring elliptic curves, with a clear tour of RSA's footguns.
- Study: "Mining Your Ps and Qs" (Heninger et al., 2012) — the internet-wide scan that found thousands of factorable live keys.
- Site:
robotattack.org— the 2017 revival of Bleichenbacher's 1998 attack. - Challenges:
cryptopals.comSet 5–6 — implement (and break) RSA yourself. The single best way to understand it.