Loading learning content...
Key generation is the single most critical phase of the RSA cryptosystem. Every security guarantee RSA provides—the confidentiality of encrypted messages, the authenticity of digital signatures, the integrity of secure connections—flows directly from the quality of the generated keys.
A brilliantly implemented encryption algorithm using a weak key is worthless. A simple implementation using a properly generated key is secure. This asymmetry places enormous weight on the key generation process, where subtle errors or shortcuts can catastrophically undermine security.
Consider this sobering reality: in 2012, researchers analyzed millions of publicly available RSA moduli from TLS certificates and SSH host keys. They found that 0.2% of keys—thousands of certificates—shared prime factors due to poor random number generation. These keys could be trivially broken, exposing any data they were meant to protect.
The lesson is clear: understanding RSA key generation isn't optional—it's essential. This page explores every step of the process, the mathematical requirements each step must satisfy, and the security considerations that separate robust implementations from vulnerable ones.
By the end of this page, you will understand: how cryptographically secure random numbers are generated, the algorithms for finding large prime numbers, why specific requirements exist for RSA primes, how to compute the public and private exponents, critical security considerations for each step, and common implementation pitfalls that have led to real-world security failures.
RSA key generation consists of five sequential steps, each building on the previous. Before we dive into each step's details, let's establish the complete picture.
The Five Steps of RSA Key Generation:
Step 1: Generate Prime p
Step 2: Generate Prime q
Step 3: Compute Modulus n
Step 4: Compute Euler's Totient φ(n)
Step 5: Select Public Exponent e
Step 6: Compute Private Exponent d
| Component | Formula | Secrecy | Bit Length (2048-bit RSA) |
|---|---|---|---|
| p (first prime) | Randomly generated | MUST be secret | 1024 bits |
| q (second prime) | Randomly generated | MUST be secret | 1024 bits |
| n (modulus) | n = p × q | Public | 2048 bits |
| φ(n) (totient) | (p-1)(q-1) | MUST be secret | ~2048 bits |
| λ(n) (Carmichael) | lcm(p-1, q-1) | MUST be secret | ~2047 bits |
| e (public exponent) | Chosen (commonly 65537) | Public | 17 bits (for 65537) |
| d (private exponent) | e⁻¹ mod φ(n) | MUST be secret | ~2048 bits |
After key generation completes, p, q, and φ(n) should be securely destroyed. Retaining them creates additional attack surfaces with no benefit. The private key (d, n) is sufficient for all decryption operations. Some implementations store p and q for performance optimization (using the Chinese Remainder Theorem), but this must be done with extreme care.
The security of RSA rests entirely on the randomness of the primes p and q. If an attacker can predict or narrow down the possible values of p or q, they can factor n and break the system. This makes cryptographic random number generation (CSPRNG) the foundation upon which all RSA security is built.
Why Regular Random Numbers Fail
Standard programming language random functions (like random.random() in Python or Math.random() in JavaScript) are pseudorandom number generators (PRNGs). They produce sequences that appear random but are entirely deterministic given the seed. If you know the seed, you know every number the generator will ever produce.
For cryptography, we need cryptographically secure pseudorandom number generators (CSPRNGs). These have additional properties:
Sources of True Randomness (Entropy)
CSPRNGs must be seeded with entropy—randomness derived from unpredictable physical phenomena:
| Source | Type | Safe for Cryptography? | Common Use |
|---|---|---|---|
| Math.random() (JavaScript) | Xorshift128+ PRNG | ❌ NO | Games, simulations |
| random.random() (Python) | Mersenne Twister PRNG | ❌ NO | Statistical sampling |
| rand() (C) | Linear Congruential | ❌ NO | Legacy applications |
| /dev/random (Linux) | Entropy pool + CSPRNG | ✓ YES | Cryptographic keys |
| /dev/urandom (Linux) | CSPRNG (ChaCha20) | ✓ YES | General cryptography |
| CryptGenRandom (Windows) | CSPRNG | ✓ YES | Windows cryptography |
| secrets module (Python 3.6+) | CSPRNG wrapper | ✓ YES | Python cryptography |
| crypto.getRandomValues (Web) | CSPRNG | ✓ YES | Browser cryptography |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
# Cryptographically Secure Random Number Generation# ALWAYS use these sources for cryptographic purposes import secretsimport os # Python's secrets module (Python 3.6+)# This is the recommended approach for cryptographic randomness def generate_random_bits(bit_length: int) -> int: """ Generate a cryptographically secure random integer of specified bit length. Uses secrets.randbits() which sources entropy from the operating system's cryptographic random number generator. """ return secrets.randbits(bit_length) def generate_random_bytes(byte_count: int) -> bytes: """ Generate cryptographically secure random bytes. Equivalent to reading from /dev/urandom on Unix systems. """ return secrets.token_bytes(byte_count) # Alternative: Direct OS entropy (equivalent to secrets internally)def generate_random_bytes_os(byte_count: int) -> bytes: """ Read random bytes directly from the OS entropy source. - Linux/macOS: /dev/urandom - Windows: CryptGenRandom """ return os.urandom(byte_count) # For RSA key generation, we need large random odd numbers as prime candidatesdef generate_prime_candidate(bit_length: int) -> int: """ Generate a random odd number of exactly the specified bit length. Requirements: 1. Must be exactly bit_length bits (MSB and LSB are 1) 2. Must be odd (LSB = 1) - even numbers > 2 aren't prime 3. Must come from a CSPRNG """ # Generate random bits candidate = secrets.randbits(bit_length) # Set MSB to ensure exact bit length candidate |= (1 << (bit_length - 1)) # Set LSB to ensure odd (even numbers aren't prime) candidate |= 1 return candidate # Example usageif __name__ == "__main__": # Generate a 1024-bit prime candidate (half of 2048-bit key) candidate = generate_prime_candidate(1024) print(f"Prime candidate: {candidate}") print(f"Bit length: {candidate.bit_length()}") # Exactly 1024 print(f"Is odd: {candidate % 2 == 1}") # True # WARNING: Never use these for cryptography!import random # Uses predictable Mersenne Twister# random.random(), random.randint(), random.choice() - ALL INSECURE! # The Debian OpenSSL Disaster (2008):# A developer commented out code that added entropy to OpenSSL's PRNG.# For 2 years, all SSL keys generated on Debian/Ubuntu systems had only# 32,767 possible values instead of 2^1024. All were trivially breakable.In 2012, researchers found that 0.2% of TLS certificates in the wild shared prime factors. The cause? Poor random number generators, particularly on embedded devices with limited entropy. They factored over 17,000 keys trivially. In 2008, Debian accidentally disabled OpenSSL's entropy collection, reducing keys to only 32,767 possibilities. Every affected key was breakable in seconds. These aren't theoretical attacks—they're historical facts.
With a secure random number generator established, we can now tackle the core challenge of RSA key generation: finding large prime numbers. For a 2048-bit RSA key, we need two primes of approximately 1024 bits each—numbers with over 300 decimal digits.
The Prime Number Theorem and Prime Density
The Prime Number Theorem tells us that primes, while becoming sparser as numbers grow, remain dense enough for practical generation. Among n-bit odd numbers, approximately 1 in ln(2^n) ≈ 0.693n are prime.
For 1024-bit numbers:
The Generation Strategy: Generate and Test
There's no efficient formula to directly generate a prime of a specific size. Instead, we:
This process typically requires a few hundred candidates for 1024-bit primes.
Primality Testing: The Miller-Rabin Test
Deterministically proving a large number is prime would be too slow for practical use. Instead, we use probabilistic primality tests that give us overwhelming confidence with negligible error probability.
The Miller-Rabin primality test is the industry standard. It's based on properties that all primes must satisfy:
For an odd prime p, write p - 1 = 2^s × d (where d is odd). For any witness a:
If a number fails this test for any witness a, it's definitely composite. If it passes for k random witnesses:
This error probability is so astronomically small that you'remore likely to have a computer error flip a bit than to get a false positive.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
import secrets def miller_rabin_test(n: int, witness: int) -> bool: """ Perform one round of the Miller-Rabin primality test. Returns True if n passes the test for this witness (probably prime). Returns False if n definitely composite. Mathematical basis: For prime p, and p-1 = 2^s * d (d odd), for any witness a: Either: a^d ≡ 1 (mod p) Or: a^(2^r * d) ≡ -1 (mod p) for some r in [0, s-1] """ if n < 2: return False if n == 2: return True if n % 2 == 0: return False # Write n-1 as 2^s * d where d is odd s = 0 d = n - 1 while d % 2 == 0: s += 1 d //= 2 # Compute a^d mod n x = pow(witness, d, n) # If a^d ≡ 1 (mod n), passes this test if x == 1: return True # Check if a^(2^r * d) ≡ -1 (mod n) for any r in [0, s-1] for _ in range(s): if x == n - 1: # -1 ≡ n-1 (mod n) return True x = pow(x, 2, n) return False # Definitely composite def is_probably_prime(n: int, iterations: int = 64) -> bool: """ Test if n is probably prime using Miller-Rabin. With 64 iterations, probability of false positive is < 2^(-128). This is astronomically unlikely - more certain than most physical measurements. """ if n < 2: return False if n == 2 or n == 3: return True if n % 2 == 0: return False # Test against small primes first (fast rejection of many composites) small_primes = [3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47] for p in small_primes: if n == p: return True if n % p == 0: return False # Perform Miller-Rabin with random witnesses for _ in range(iterations): witness = secrets.randbelow(n - 3) + 2 # Random in [2, n-2] if not miller_rabin_test(n, witness): return False return True def generate_prime(bit_length: int) -> int: """ Generate a random prime number of exactly the specified bit length. Process: 1. Generate random odd number with exact bit length 2. Test primality with Miller-Rabin 3. Repeat until prime found Expected iterations: ~0.693 * bit_length / 2 ≈ 355 for 1024 bits """ attempts = 0 while True: attempts += 1 # Generate random odd number with exact bit length candidate = secrets.randbits(bit_length) candidate |= (1 << (bit_length - 1)) # Set MSB (exact bit length) candidate |= 1 # Set LSB (ensure odd) if is_probably_prime(candidate): print(f"Found prime after {attempts} attempts") return candidate def generate_rsa_primes(key_bits: int = 2048): """ Generate two suitable primes for RSA key generation. Requirements for RSA primes: 1. Each prime is approximately key_bits/2 bits 2. Primes must be sufficiently different (|p - q| should be large) 3. Both must be strong primes (resistant to specific attacks) """ prime_bits = key_bits // 2 print(f"Generating first prime ({prime_bits} bits)...") p = generate_prime(prime_bits) print(f"Generating second prime ({prime_bits} bits)...") while True: q = generate_prime(prime_bits) # Ensure p ≠ q if p == q: continue # Ensure |p - q| is large (primes too close are vulnerable) # Rule of thumb: difference should be at least 2^(prime_bits - 100) min_diff = 1 << (prime_bits - 100) if abs(p - q) < min_diff: print("Primes too close, regenerating q...") continue break return p, q # Example usageif __name__ == "__main__": # Generate 2048-bit RSA primes (this takes several seconds) # p, q = generate_rsa_primes(2048) # For demonstration, use smaller primes p = generate_prime(512) q = generate_prime(512) n = p * q print(f"p = {p}") print(f"q = {q}") print(f"n = p * q ({n.bit_length()} bits)")For maximum security, some implementations require 'strong primes'—primes p where p-1 has a large prime factor (resistant to Pollard's p-1 factoring) and p+1 has a large prime factor (resistant to Williams's p+1 factoring). Modern consensus is that these requirements are less critical with sufficiently large primes (2048+ bits), but they add defense in depth.
With primes p and q generated, we now compute the remaining RSA key components. Each step involves specific mathematical operations with important security implications.
Step 1: Computing the Modulus n
The simplest step—just multiply:
n = p × q
The modulus n becomes part of both the public and private keys. Its bit length is essentially the key strength (e.g., 2048 bits for modern security).
Step 2: Computing Euler's Totient φ(n)
For n = p × q where p and q are distinct primes:
φ(n) = (p - 1) × (q - 1)
Euler's totient counts the integers from 1 to n-1 that are coprime to n. This value is essential for computing the private exponent but must remain absolutely secret—knowing φ(n) is equivalent to knowing the factorization of n.
Modern Alternative: Carmichael's Totient λ(n)
Many modern implementations use Carmichael's totient function instead:
λ(n) = lcm(p - 1, q - 1)
The Carmichael function always divides Euler's totient: λ(n) | φ(n). Using λ(n) produces a smaller private exponent d, which can improve performance. Both are mathematically valid for RSA.
Step 3: Selecting the Public Exponent e
The public exponent e must satisfy:
1 < e < φ(n) (or equivalently, e < λ(n))gcd(e, φ(n)) = 1 (e is coprime to the totient)In practice, e is typically chosen from a small set of standard values:
| Value | Binary | Notes |
|---|---|---|
| 3 | 11 | Fastest, but requires careful padding |
| 17 | 10001 | Rarely used |
| 65537 | 10000000000000001 | Standard choice |
Why 65537 (2^16 + 1)?
65537 is a Fermat prime (primes of form 2^(2^k) + 1). Its binary representation has only two 1-bits, making modular exponentiation fast using the square-and-multiply algorithm. It's large enough to avoid certain attacks that affect small exponents like e = 3, yet small enough for efficient encryption.
e = 3 is tempting for its speed (only 2 multiplications for encryption), but it requires very careful implementation to avoid attacks like Coppersmith's attack on small messages and broadcast attacks when the same message is encrypted to multiple recipients.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
import mathimport secrets def extended_gcd(a: int, b: int) -> tuple[int, int, int]: """ Extended Euclidean Algorithm. Returns (gcd, x, y) where gcd(a,b) = ax + by. Used to compute modular multiplicative inverses. """ if a == 0: return b, 0, 1 gcd, x1, y1 = extended_gcd(b % a, a) x = y1 - (b // a) * x1 y = x1 return gcd, x, y def mod_inverse(a: int, m: int) -> int: """ Compute the modular multiplicative inverse of a modulo m. Returns x such that (a * x) % m == 1. Requires gcd(a, m) == 1 (a and m must be coprime). Uses the Extended Euclidean Algorithm. """ gcd, x, _ = extended_gcd(a % m, m) if gcd != 1: raise ValueError(f"Modular inverse doesn't exist: gcd({a}, {m}) = {gcd}") return (x % m + m) % m # Ensure positive result def compute_rsa_keys(p: int, q: int, e: int = 65537): """ Compute all RSA key components from primes p and q. Returns: public_key: (e, n) private_key: (d, n) key_info: dict with all computed values (for educational purposes) """ # Step 1: Compute modulus n = p * q n = p * q # Step 2: Compute Euler's totient φ(n) = (p-1)(q-1) phi_n = (p - 1) * (q - 1) # Step 2b: Also compute Carmichael's totient λ(n) = lcm(p-1, q-1) lambda_n = math.lcm(p - 1, q - 1) # Step 3: Verify e is coprime to φ(n) if math.gcd(e, phi_n) != 1: raise ValueError(f"e={e} is not coprime to φ(n)={phi_n}") # Step 4: Compute private exponent d = e^(-1) mod φ(n) # Using Euler's totient d_euler = mod_inverse(e, phi_n) # Using Carmichael's totient (produces smaller d) d_carmichael = mod_inverse(e, lambda_n) # Verify: e * d ≡ 1 (mod φ(n)) assert (e * d_euler) % phi_n == 1 assert (e * d_carmichael) % lambda_n == 1 # Both d values work for decryption, Carmichael's is smaller/faster d = d_carmichael public_key = (e, n) private_key = (d, n) key_info = { 'p': p, 'q': q, 'n': n, 'phi_n': phi_n, 'lambda_n': lambda_n, 'e': e, 'd_euler': d_euler, 'd_carmichael': d_carmichael, 'n_bits': n.bit_length(), } return public_key, private_key, key_info def validate_key_pair(public_key, private_key): """ Verify that an RSA key pair is valid by testing encryption/decryption. """ e, n = public_key d, _ = private_key # Test with random messages for _ in range(10): # Random message less than n message = secrets.randbelow(n - 1) + 1 # Encrypt: c = m^e mod n ciphertext = pow(message, e, n) # Decrypt: m = c^d mod n decrypted = pow(ciphertext, d, n) assert decrypted == message, "Key pair validation failed!" return True # Example: Complete RSA key generationif __name__ == "__main__": # For demonstration, use small primes # In practice, use 1024-bit primes for 2048-bit RSA p = 61 q = 53 public_key, private_key, info = compute_rsa_keys(p, q) print("RSA Key Generation Complete") print("=" * 50) print(f"Primes: p = {info['p']}, q = {info['q']}") print(f"Modulus: n = {info['n']} ({info['n_bits']} bits)") print(f"Euler's totient: φ(n) = {info['phi_n']}") print(f"Carmichael's totient: λ(n) = {info['lambda_n']}") print(f"Public exponent: e = {info['e']}") print(f"Private exponent (Euler): d = {info['d_euler']}") print(f"Private exponent (Carmichael): d = {info['d_carmichael']}") print() print(f"Public Key: (e={public_key[0]}, n={public_key[1]})") print(f"Private Key: (d={private_key[0]}, n={private_key[1]})") print() # Validate if validate_key_pair(public_key, private_key): print("✓ Key pair validated successfully!")Not all prime pairs are equally secure for RSA. Beyond simply being prime and of sufficient size, RSA primes must satisfy several important security requirements to resist various factorization attacks.
Requirement 1: Sufficient Size
Each prime should be approximately half the target key size:
| RSA Key Size | Prime Size | Security Level |
|---|---|---|
| 1024 bits | ~512 bits each | Deprecated (min 80-bit security) |
| 2048 bits | ~1024 bits each | Standard (112-bit security) |
| 3072 bits | ~1536 bits each | High (128-bit security) |
| 4096 bits | ~2048 bits each | Maximum practical (140+ bit security) |
Requirement 2: Primes Must Be Sufficiently Different
If |p - q| is small (primes are close together), Fermat's factorization method can quickly factor n. The attack exploits that n = p × q can be written as:
n = ((p+q)/2)² - ((p-q)/2)²
If p and q are close, (p-q)/2 is small, and we can find factors by testing:
a = ceiling(√n), b² = a² - n
for small values until b² is a perfect square.
Mitigation: Ensure |p - q| > 2^(key_bits/2 - 100). For 2048-bit keys, this means |p - q| > 2^924.
| Attack | Vulnerability Exploited | Mitigation |
|---|---|---|
| Trial Division | Small prime factors | Use primes > 2^512 |
| Fermat's Method | |p - q| is small | Ensure |p-q| > 2^(bits/2 - 100) |
| Pollard's p-1 | p-1 has only small factors | Use strong primes or large primes |
| Williams's p+1 | p+1 has only small factors | Use strong primes or large primes |
| General Number Field Sieve | Modulus too small | Use 2048+ bit keys |
| GCD Attack | Shared prime across keys | True random generation for each key |
| Coppersmith's Attack | Small e with small message | Use proper padding (OAEP) |
In 2017, the ROCA vulnerability was discovered in Infineon's RSA key generation library used in TPM chips, ID cards, and hardware tokens. The library used a fast but weak prime generation algorithm that created primes with a special structure. This allowed factoring 1024-bit keys in seconds and 2048-bit keys in days. Over 750,000 Estonian ID cards were affected. The lesson: even well-known manufacturers can get key generation catastrophically wrong.
In practice, RSA private key operations can be significantly accelerated using the Chinese Remainder Theorem (CRT). This optimization requires storing additional precomputed values derived from p and q, trading secrecy surface area for a 4x performance improvement.
The CRT Optimization for RSA Decryption
Standard RSA decryption computes:
m = c^d mod n
With a 2048-bit modulus n and a ~2048-bit exponent d, this is computationally expensive. CRT optimization breaks this into two smaller computations:
m_p = c^(d mod p-1) mod p (operations with ~1024-bit numbers)m_q = c^(d mod q-1) mod q (operations with ~1024-bit numbers)m = m_q + q × (q_inv × (m_p - m_q) mod p)Since modular exponentiation is O(k³) where k is the bit length, using 1024-bit operations instead of 2048-bit is 8x faster per operation. With two such operations plus a cheap combination step, total speedup is approximately 4x.
The CRT Components (PKCS#1 RSA Private Key)
The full RSA private key in PKCS#1 format includes these CRT components:
| Component | Definition | Purpose | Security |
|---|---|---|---|
| n | p × q | Modulus (shared with public key) | Public |
| e | Public exponent | Encryption exponent | Public |
| d | e⁻¹ mod λ(n) | Standard decryption exponent | Secret |
| p | First prime | CRT computation | Secret |
| q | Second prime | CRT computation | Secret |
| dP | d mod (p-1) | CRT exponent for p | Secret |
| dQ | d mod (q-1) | CRT exponent for q | Secret |
| qInv | q⁻¹ mod p | CRT combination coefficient | Secret |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
def compute_crt_components(p: int, q: int, d: int): """ Compute CRT optimization components for RSA decryption. These values enable 4x faster decryption using the Chinese Remainder Theorem. """ # CRT exponents dP = d % (p - 1) # d mod (p-1) dQ = d % (q - 1) # d mod (q-1) # CRT coefficient qInv = mod_inverse(q, p) # q^(-1) mod p return dP, dQ, qInv def rsa_decrypt_crt(ciphertext: int, p: int, q: int, dP: int, dQ: int, qInv: int) -> int: """ RSA decryption using Chinese Remainder Theorem optimization. Approximately 4x faster than standard decryption for large keys. Process: 1. Compute m_p = c^dP mod p 2. Compute m_q = c^dQ mod q 3. Combine: m = m_q + q * ((qInv * (m_p - m_q)) mod p) """ # Step 1: Compute partial results m_p = pow(ciphertext, dP, p) m_q = pow(ciphertext, dQ, q) # Step 2: Combine using CRT # h = qInv * (m_p - m_q) mod p h = (qInv * (m_p - m_q)) % p # m = m_q + h * q message = m_q + h * q return message def rsa_decrypt_standard(ciphertext: int, d: int, n: int) -> int: """ Standard RSA decryption (without CRT). m = c^d mod n """ return pow(ciphertext, d, n) # Performance comparisonimport time def benchmark_decryption(iterations=100): """ Compare CRT vs standard RSA decryption performance. """ # Generate 2048-bit key for realistic benchmark # Using smaller key for quick demonstration p = generate_prime(512) q = generate_prime(512) n = p * q e = 65537 phi_n = (p - 1) * (q - 1) d = mod_inverse(e, phi_n) # CRT components dP, dQ, qInv = compute_crt_components(p, q, d) # Generate test ciphertexts ciphertexts = [pow(secrets.randbelow(n-1) + 1, e, n) for _ in range(iterations)] # Benchmark standard decryption start = time.time() for c in ciphertexts: rsa_decrypt_standard(c, d, n) standard_time = time.time() - start # Benchmark CRT decryption start = time.time() for c in ciphertexts: rsa_decrypt_crt(c, p, q, dP, dQ, qInv) crt_time = time.time() - start print(f"Standard decryption: {standard_time:.3f}s") print(f"CRT decryption: {crt_time:.3f}s") print(f"Speedup: {standard_time/crt_time:.2f}x") # SECURITY NOTE: CRT implementation must check for faults!# Bellcore attack: If a single-bit error occurs during CRT computation,# the faulty result can be used to factor n.# Mitigation: Always verify c^e = m mod n after CRT decryption.CRT optimization introduces a critical vulnerability: the Bellcore attack. If a hardware fault causes even a single bit error during CRT computation, the faulty output can be used to factor n entirely. Secure implementations MUST verify the result by checking that c = m^e mod n after every CRT decryption. This is not optional—without verification, attackers can use power analysis or laser injection to induce faults and extract private keys.
We've explored RSA key generation in depth—from cryptographic random number generation through prime finding, component computation, and advanced optimizations. Let's consolidate the essential takeaways.
What's Next:
With key generation thoroughly understood, the next page explores the Mathematical Basis of RSA in rigorous detail. We'll examine the number theory that makes RSA work: modular arithmetic, Euler's theorem, the Extended Euclidean Algorithm, and the Chinese Remainder Theorem—providing the mathematical foundation that transforms random primes into a working cryptosystem.
You now understand the complete RSA key generation process: cryptographic randomness, prime generation with Miller-Rabin testing, key component computation, security requirements, and CRT optimization. You're prepared to explore the deep mathematical foundations that make RSA work in the next page.