RSA from Scratch — The Number Theory Behind Public Keys
RSA lets you publish a key that locks but cannot unlock. This builds it from congruence arithmetic up: key generation, the two modular exponentiations, and a line-by-line proof from Euler's theorem of why the message comes back — then a worked example small enough to check by hand, why factoring is the security assumption, and why textbook RSA must never be shipped without padding.
Key points
- Public-key cryptography gives everyone a public key and a private key: anyone can encrypt with the public key, and only the private key can decrypt.
- RSA rests on an asymmetry: multiplying two large primes is easy, but no efficient algorithm is known to factor their product.
- RSA encrypts with c ≡ m^e mod n and decrypts with m ≡ c^d mod n; the message returns because ed = 1 + kφ(n) and Euler's theorem.
- Finding φ(n) is equivalent to factoring, and 2048 bits is the current floor, but Shor's algorithm factors integers in polynomial time on a large enough quantum computer.
- Textbook RSA is deterministic, malleable, and leaks messages under small exponents, so padding is required: RSAES-OAEP for encryption and RSASSA-PSS for signatures.
"Rivest
Primary source — what this article is built on
undefined2026-09-23
"Rivest"RivestA Method for Obtaining Digital Signatures and Public-Key CryptosystemsShamir & Adleman 1978
https://dl.acm.org/doi/10.1145/359340.359342"CACM 21(2):120-126
PKCS #1: RSA Cryptography Specifications Version 2.2RFC 8017Paper page
The problem: agreeing on a secret you never shared
For most of history, encryption meant both parties knew the same secret. You and I agree on a key in advance, I scramble a message with it, you unscramble it with the same key. This is symmetric encryption, and it is fast and well understood — but it has a bootstrapping problem that looks unsolvable. To talk securely, we must first meet securely to exchange the key. On the internet, where you want to buy something from a shop you have never contacted before, there is no such prior meeting.
Public-key cryptography breaks that circle with an idea that sounds impossible on first hearing: a key that can lock but not unlock. Everyone gets a public key they publish to the world and a private key they never reveal. Anyone can use your public key to encrypt a message to you; only your private key can decrypt it. No prior meeting, no shared secret.
Rivest, Shamir and Adleman published the first practical construction of this in 1978, and RSA is still the name on it. It is built out of ordinary integer arithmetic — no exotic mathematics — and this article assembles it from the ground up: the arithmetic you need, the key generation, the two operations, a proof that they undo each other, a small worked example, and the reasons the textbook version must be padded before it touches real traffic. (For where RSA sits among the other tools, see Cryptography from Scratch.)
Prerequisite: arithmetic on a clock
Everything in RSA happens modulo some integer , which means: do the ordinary arithmetic, divide by , and keep only the remainder. Clock arithmetic is the familiar case with : four hours after ten o'clock is two o'clock, because divided by leaves remainder .
The notation for " and leave the same remainder when divided by " is
read " is congruent to modulo ." In words: if you only look at the remainder after dividing by , you cannot tell and apart. So . Three facts about congruences are all we need, and all three are easy to believe:
- Addition and multiplication survive. If and , then and . You may reduce mod at any point in a calculation without changing the answer — which is what makes working with enormous numbers tractable at all.
- Exponentiation is therefore cheap. , so you can square repeatedly and reduce mod after every step. Computing takes about 11 squarings, not 2048 multiplications. This is modular exponentiation by squaring, and it is why RSA is usable with 2048-bit numbers.
- Division is the special one. There is no general "divide by ." What sometimes exists is a modular inverse: a number with . It exists exactly when and share no common factor besides 1, and the extended Euclidean algorithm finds it quickly.
One more object. Euler's totient function counts how many integers in share no common factor with . For a prime , every one of qualifies, so . For a product of two distinct primes — the only case RSA needs —
which you can see by counting what gets excluded from : the multiples of , the multiples of , with itself double-counted, giving . In words: strike out the multiples of and the multiples of , and exactly numbers are left.
The asymmetry RSA is built on
A public-key scheme needs an operation that is easy to do and hard to undo. RSA's is multiplication of primes.
Multiplying two 1024-bit primes together takes a computer no measurable time. Going the other way — given only the 2048-bit product, recover the two primes — is integer factorization, and no efficient algorithm for it is known. The best published classical method, the general number field sieve, runs in sub-exponential but still hopeless time at that size: the largest RSA modulus factored in public, the 829-bit RSA-250, took about 2700 core-years in 2020. 2048 bits is far beyond that.
So: the product is public, the factors are private. The rest of RSA is machinery for turning that gap into an encryption scheme.
Key generation
Five steps, each of which we now have the tools for.
1. Pick two large distinct primes and . In practice, generate random odd numbers of the right size and test them for primality (Miller–Rabin) until two pass. For a 2048-bit key, each is about 1024 bits.
2. Compute the modulus . This number is public. Its size is what people mean by "a 2048-bit RSA key."
3. Compute . This number must stay secret — and note that anyone who can factor can compute it, which is the whole crux of the security.
4. Choose the public exponent with and . The coprimality requirement is not aesthetic: it is exactly the condition for step 5 to have a solution. Nearly all real keys use , which is prime, and whose binary form 10000000000000001 has only two 1-bits — so encryption costs just 17 squarings and one multiply.
5. Compute the private exponent as the modular inverse of :
Unpacking the "equivalently": is the number that, multiplied by , lands exactly one step past a multiple of . Written without congruences, there is some integer with — and it is this form we will need in the proof. The extended Euclidean algorithm produces from and in microseconds. In words, is the exponent that exactly cancels out raising to the .
The result is two keys:
- Public key — publish it anywhere.
- Private key — never transmit it. And discard or protect , , : any one of them hands over .
Encryption and decryption
Represent the message as an integer with . (How you get from bytes to an integer is where padding lives; that comes later.) Then both directions are a single modular exponentiation:
That is the entire algorithm. In words: encryption and decryption are the same operation — raise to a power, keep the remainder mod — and only the exponent differs. Raise to the public exponent to lock, raise to the private exponent to unlock, and reduce mod throughout. The asymmetry between the two operations is nothing but the asymmetry between knowing and not knowing it.
The obvious question is why the second undoes the first.
Why the message comes back
The engine is a theorem of Euler, stated here and then used once.
Euler's theorem. If , then .
In words: raise any number coprime to to the power , and modulo you land exactly on 1. (Fermat's little theorem, for prime , is the special case .) What this gives us is a way to make exponents wrap around: multiplying by changes nothing mod , so exponents effectively live mod . That is precisely why was defined mod and not mod .
Now decrypt, one line at a time. Start from the ciphertext and substitute:
Because by definition of encryption.
Because a power of a power multiplies the exponents — no modular arithmetic involved yet.
Because step 5 of key generation gave us exactly for some integer . This is the step the whole construction was engineered to make available.
Because , and then — still just exponent rules.
Because Euler's theorem replaces with 1, provided .
Because 1 to any power is 1. So , and since we required , the residue we get back is rather than something congruent to it. The message returns. In words, the six lines say: raise to , then to , and the exponent goes full laps of and lands back at 1.
Notice what did the work: the whole scheme is an exercise in choosing so that is one more than a multiple of , because that is the exponent Euler's theorem collapses. And is computable from and — which is why the factors must stay secret.
One honest caveat, since the proof above has a gap. Euler's theorem needed , and a message could in principle be a multiple of or . RSA does still decrypt correctly in those cases; the argument uses the Chinese Remainder Theorem to handle each prime factor separately rather than Euler's theorem on directly. For random messages the odds of hitting such an are about — astronomically small at real key sizes, and irrelevant in practice because padding randomizes anyway. The gap is worth knowing about so you are not surprised when a rigorous treatment spends a page on it.
A worked example you can check by hand
Real keys use 617-digit numbers. Take tiny primes instead so every step is verifiable with a pencil.
Key generation. Let , .
- Choose . Check . ✓
- Find with . Try : . ✓
So the public key is and the private key is . Note with — the exact form the proof needed.
Encrypt .
, so . In words, the ciphertext is nothing more than "5 multiplied by itself three times, remainder after dividing by 33."
Decrypt. We need , computed by repeated squaring, reducing at every step:
We get 5 back. Every number here was small enough to check, and the same six lines with 2048-bit integers are what your browser runs.
Two things this toy example also makes visible. First, must be less than : with there are only 33 possible messages, and would come back as . Second, the encryption is deterministic — 5 always maps to 26 — and with only 33 possibilities an attacker simply encrypts all of them and looks up the answer. That is not an artifact of the small size, as the next-but-one section explains.
Why it is hard to break
The attacker sees and and wants . Consider the routes.
Factor . With and in hand, compute , run the extended Euclidean algorithm to get , decrypt. This is the primary attack and it is why RSA's security rests on the hardness of factoring. At 2048 bits it is out of reach of all known classical methods — the general number field sieve's cost grows sub-exponentially in the number of digits, and the 829-bit public record already cost thousands of core-years.
Compute some other way. No help: knowing both and gives you , and from a sum and a product you recover and with the quadratic formula. Finding is equivalent to factoring.
Take the -th root of directly. Over the integers, -th roots are easy. Modulo a composite whose factorization you do not know, the "RSA problem" of computing has no known efficient solution. Worth being precise, though: it is not proven that breaking RSA is as hard as factoring. Factoring breaks RSA; whether every attack on RSA implies a factoring algorithm is an open question.
Brute-force . Encrypt candidate messages and compare — devastating whenever the message space is small, and the reason padding exists.
Practical consequences: 1024-bit keys are deprecated, 2048 bits is the current floor, and 3072 bits corresponds to roughly the 128-bit security level in NIST's guidance. And the whole edifice has an expiry date: Shor's algorithm factors integers in polynomial time on a sufficiently large quantum computer, which is why standards bodies are migrating to post-quantum schemes. Nothing about the classical analysis above protects data that an adversary can record now and decrypt later.
Textbook RSA is not RSA you can ship
Everything up to here is what cryptographers call textbook RSA, and deploying it as written is a serious vulnerability. The mathematics is correct; the scheme built directly on it is broken. Three reasons, each fatal on its own.
It is deterministic. The same plaintext always produces the same ciphertext under the same key. So an attacker who can guess the message space wins without any number theory: encrypt every candidate with the public key — which is public — and match. A yes/no field, a credit card number, a room number, an AES key from a weak generator: all recoverable by table lookup. Formally, textbook RSA fails IND-CPA security, and the fix must involve randomness.
It is malleable. Because exponentiation distributes over multiplication,
In words: multiply two ciphertexts and you get the ciphertext of the product of their plaintexts. So an attacker who cannot read a ciphertext can still multiply it by and hand you something that decrypts to . Any protocol that treats "it decrypted" as "it is authentic" is exploitable.
Small exponents leak small messages. If — easy to arrange with and a short message — then holds over the integers, with no wrap-around at all. Take the ordinary integer cube root of and read the message. No factoring required.
The cure is padding: a standardized transformation that pads the message out to nearly the full width of , mixes in fresh random bytes, and adds structure the decryptor can verify.
- For encryption, use RSAES-OAEP (Optimal Asymmetric Encryption Padding). It is randomized, so the same plaintext encrypts differently every time, and it destroys the multiplicative structure.
- For signatures, use RSASSA-PSS.
- The older PKCS#1 v1.5 encryption padding is where Bleichenbacher's 1998 padding-oracle attack lives: a server that distinguishes "bad padding" from "bad message" in its error responses can be queried into decrypting a ciphertext it never had the plaintext for. Variants of this bug have been rediscovered in TLS stacks repeatedly (ROBOT, 2017). Treat v1.5 as legacy-compatibility only.
There is a further reason RSA never encrypts your actual data: caps a 2048-bit key at 256 bytes minus padding overhead, and modular exponentiation is orders of magnitude slower than a symmetric cipher. Real systems are hybrid — RSA (when used at all) encrypts a short symmetric key, and AES encrypts the payload. In modern TLS 1.3 even that role is gone: key establishment moved to ephemeral Diffie–Hellman for forward secrecy, and RSA survives mainly as a signature algorithm on certificates. (See TLS from Scratch for how those pieces fit.)
Signatures: the same two exponents, swapped
Because and are inverses, applying them in the other order also round-trips: . That gives a digital signature. The signer, who alone holds , produces
and anyone verifies by checking
using the public key. Only the holder of could have produced an that passes, so the signature binds the message to that key. Read together, the two equations say, in words: the signer locks the hash with the private exponent, and the verifier unlocks it with the public exponent and checks it against a hash computed locally.
Note the hash . You sign a digest, not the message, for two reasons: the message is almost always longer than allows, and signing raw values inherits the malleability above — an attacker can multiply two signatures together to forge a signature on the product. A collision-resistant hash plus PSS padding closes both. This is the operation that actually matters for the modern web: every certificate chain your browser validates is a stack of these verifications.
Pitfalls
Never implement it yourself. This is the standard advice and it is correct. Everything below is a real class of break in deployed RSA, and library authors have spent decades on each.
Standards use , not . FIPS and PKCS#1 define modulo , which divides . The proof still works and the resulting is smaller, hence faster. If you compare a library's against one you derived from they may differ while both being valid — the exponents are congruent mod .
Weak randomness is catastrophic, and it composes across keys. If a bad entropy source makes two independently generated keys share a prime, then reveals it, and both keys fall — to a computation costing nothing. A 2012 survey ("Mining your Ps and Qs") found tens of thousands of live TLS hosts broken exactly this way, mostly embedded devices generating keys at first boot before any entropy existed.
Timing is a side channel. Naive modular exponentiation takes a different amount of time depending on the bits of , and that has been used to extract private keys over a network. Real implementations use blinding — multiply by a random factor before exponentiating and divide it out after — plus constant-time code paths.
Fault attacks on the CRT speedup. Decryption is usually done via the Chinese Remainder Theorem, computing mod and mod separately for about a 4× speedup. If a single computation error corrupts one of the two halves, comparing the faulty signature with a correct one reveals by a GCD. Libraries defend by verifying the result before releasing it.
Small private exponents are breakable. Choosing a tiny to speed up decryption invites Wiener's attack, which recovers efficiently when is roughly below . Small public exponents are fine — is standard — but must be large.
Padding must be checked in constant time, and errors must not be distinguishable. This is the Bleichenbacher lesson: the vulnerability was never in the arithmetic, it was in the error message.
How it is used in practice
Almost nobody writes RSA arithmetic on the job. What you actually touch is key generation, key formats, and the choice of padding — and that is where the accidents happen.
When it comes up. Backend and infrastructure engineers meet RSA when generating a CSR for a TLS certificate, issuing SSH keys, or choosing a JWT signing algorithm. Security reviewers meet it when inventorying key sizes and padding modes across existing systems.
The commands you type and the names they print.
- Generate a key:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out key.pem. The public exponent defaults to 65537. - Inspect it:
openssl pkey -in key.pem -text -nooutlistsmodulus(),publicExponent(),privateExponent(),prime1/prime2(), plusexponent1/exponent2/coefficient— the precomputed values for the CRT speedup from the pitfalls section. A private key file carries not just but and outright, which is why a leaked one is game over. - JWT:
RS256in thealgheader means a PKCS#1 v1.5 signature,PS256means PSS. Both are verified with a public key, unlikeHS256, which uses a shared secret. - Application code: in Python's
cryptography, you call it with the padding spelled out, e.g.padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None).
Mistakes that bite people who don't know.
- Java's
Cipher.getInstance("RSA")without a padding spec gives you PKCS#1 v1.5 (the default provider's choice). New code should name it explicitly, e.g."RSA/ECB/OAEPWithSHA-256AndMGF1Padding". The "ECB" is a naming artifact; it means nothing as a block mode for RSA. - JWT algorithm confusion. If the verifier trusts the token's own
alg, an attacker can make it use theRS256public key as anHS256HMAC secret and forge tokens. Pin the allowed algorithms at verification time. - In SSH,
ssh-rsanames a SHA-1 signature scheme, not just a key type. Since OpenSSH 8.8 that SHA-1 signature is disabled by default, so an RSA key can stop connecting to old servers that don't supportrsa-sha2-256/rsa-sha2-512. The key is not broken. - Hitting the length limit by encrypting data directly. A 2048-bit key caps out at 256 bytes, padding included. Chunking a file and RSA-encrypting each piece is the wrong design; wrap a symmetric key instead, as in the hybrid scheme above.
Questions you get asked in interviews and design reviews, and how to answer.
- "What does RSA do in TLS 1.3?" → It no longer does key exchange. It signs certificates, and the server uses it to prove possession of its private key during the handshake (CertificateVerify).
- "Why not encrypt data with RSA directly?" → The length cap and the speed. RSA only wraps a symmetric key.
- "What is wrong with textbook RSA?" → Deterministic, malleable, weak with small exponents — and OAEP/PSS is the fix.
- "What would you pick for a new key?" → If RSA, 2048 bits or more with OAEP/PSS; for signatures alone, also consider ECDSA or Ed25519, and keep the post-quantum migration in view. Getting that far is a solid answer.
Summary
- Symmetric encryption needs a shared secret you cannot establish over a hostile network; public-key cryptography solves the bootstrap with a key that locks but cannot unlock
- RSA rests on one asymmetry: multiplying two large primes is trivial, factoring the product is not
- Key generation: , , pick coprime to , set . Public , private
- Encryption is and decryption — one modular exponentiation each
- It round-trips because , so by Euler's theorem
- Security means factoring is infeasible; recovering is equivalent to factoring, and 2048 bits is today's floor — but Shor's algorithm ends this on a large quantum computer
- Textbook RSA must never be shipped: it is deterministic, malleable, and breakable at small exponents. Use OAEP for encryption and PSS for signatures
- In practice RSA encrypts a symmetric key rather than data, and in TLS 1.3 its surviving role is signing certificates
For the surrounding landscape, start with Cryptography from Scratch; to see RSA signatures doing their job inside a real handshake, continue to TLS from Scratch.
Check your understanding
- Key generation requires . What exactly breaks if you pick an that violates it?
- Why is computed modulo rather than modulo ?
- Using the public key from the worked example, encrypt . Then use to decrypt your answer and confirm you get 2 back.
- An attacker learns for some 2048-bit RSA key but not or . Is the key compromised? Justify your answer.
- A service encrypts a single-digit PIN with textbook RSA using a 2048-bit public key and stores the ciphertext. The key is strong and factoring is infeasible. Explain how an attacker reads the PIN anyway, and which countermeasure prevents it.
Answers
- If shares a factor with , then has no modular inverse mod , so no with exists. Step 5 has no solution and there is no private key — the coprimality condition is exactly the existence condition for the inverse.
- Because Euler's theorem makes , so multiplying the exponent by another changes nothing: exponents wrap around mod , not mod . Decryption needs to be one more than a multiple of , which is the definition of being in that modulus.
- , so . Decrypting: ; ; ; . The message 2 comes back.
- Yes, completely. From and you get ; knowing the sum and product of two numbers, the quadratic formula gives and . Computing is equivalent to factoring , and either one yields immediately.
- Textbook RSA is deterministic and the public key is public, so the attacker encrypts each of the ten possible PINs and compares with the stored ciphertext — ten exponentiations, no factoring. The countermeasure is randomized padding (RSAES-OAEP), which mixes fresh random bytes into every encryption so the same PIN produces a different ciphertext each time and the lookup table cannot be built.
Comments
Sign in to comment