Chinese Remainder Theorem — RSA CRT Overflow Traps
- CRT: unique solution mod M=Πmi to system x≡ai (mod mi) when moduli are pairwise coprime.
- Solution: x = Σ ai × (M/mi) × (M/mi)^(-1) mod mi.
- Extends to non-coprime moduli if solution exists (GCD divides difference of remainders).
- CRT solves simultaneous remainder equations when moduli are pairwise coprime.
- It's used for RSA decryption speed-up, secret sharing, and calendar problems.
- The unique solution mod M is computed from each remainder multiplied by its modular inverse of M/m_i.
- Garner's algorithm avoids large-number arithmetic, useful for big integer implementations.
- Common production trap: forgetting to reduce final result mod M causes incorrect values.
CRT Debugging Cheat Sheet
Large modulo product M causes overflow
BigInteger M = m1.multiply(m2); // JavaBigInteger Mi = M.divide(mi);Modular inverse doesn't exist
gcd(mi, Mi) // should be 1BigInteger yi = Mi.modInverse(mi); // JavaFinal result not in [0, M-1]
x = x.mod(M); // Javax %= M; // C++ after making x positiveNon-coprime moduli set but system unsolvable
(a2 - a1) % gcd(m1, m2) == 0?If no, no solution exists.Production Incident
Production Debug GuideTriage symptoms that look like CRT bugs but aren't always CRT itself.
The Chinese Remainder Theorem appears in a 3rd century AD Chinese mathematical text with the problem: 'A number gives remainder 2 when divided by 3, remainder 3 when divided by 5, and remainder 2 when divided by 7. What is the number?' (Answer: 23). The theorem's 1700-year journey from recreational mathematics to RSA optimisation is one of the better stories in applied mathematics.
In production RSA, CRT speeds up decryption by roughly 4x. Instead of one computation modulo n (a 2048-bit number), you do two computations modulo p and q (each ~1024 bits), then combine with CRT. Since modular exponentiation is O(log e × log^2 n) in the number of bit operations, halving the number size reduces each operation by ~4x.
The Theorem and Constructive Proof
Given pairwise coprime m1,...,mk and remainders a1,...,ak, the unique solution mod M=Πmi is: x = Σ ai × Mi × yi mod M where Mi = M/mi and yi = Mi^(-1) mod mi.
from math import gcd from functools import reduce def extended_gcd(a, b): if b == 0: return a, 1, 0 g, x1, y1 = extended_gcd(b, a % b) return g, y1, x1 - (a // b) * y1 def crt(remainders: list[int], moduli: list[int]) -> int: """ Solve x ≡ remainders[i] (mod moduli[i]) for all i. Moduli must be pairwise coprime. Returns x mod M where M = product of all moduli. """ M = 1 for m in moduli: M *= m x = 0 for ai, mi in zip(remainders, moduli): Mi = M // mi _, yi, _ = extended_gcd(Mi, mi) # Mi * yi ≡ 1 (mod mi) x += ai * Mi * yi return x % M # x ≡ 2 (mod 3), x ≡ 3 (mod 5), x ≡ 2 (mod 7) print(crt([2, 3, 2], [3, 5, 7])) # 23 # Verify print(23 % 3, 23 % 5, 23 % 7) # 2 3 2 ✓
2 3 2
- Each modulus mi defines an axis, with coordinates in {0,...,mi-1}.
- The product M is the total number of lattice points in the hyper-rectangle.
- The constructive formula simply projects the point back from each axis to the hypercube using modular inverses.
CRT for Two Congruences
The two-equation version appears frequently — merge two congruences into one. It also works when moduli aren't coprime, provided a solution exists. The condition: (a2 - a1) % gcd(m1, m2) == 0.
def crt_two(a1, m1, a2, m2): """Solve x ≡ a1 (mod m1) and x ≡ a2 (mod m2). Works even if gcd(m1,m2) != 1 (if solution exists). Returns (x, lcm) or None if no solution. """ g = gcd(m1, m2) if (a2 - a1) % g != 0: return None # no solution lcm = m1 * m2 // g _, p, _ = extended_gcd(m1 // g, m2 // g) x = (a1 + m1 * ((a2 - a1) // g * p % (m2 // g))) % lcm return x, lcm print(crt_two(0, 3, 3, 4)) # (3, 12) → x≡3 (mod 12) print(crt_two(1, 6, 3, 4)) # None — no solution (1 is odd but 3 is odd too... let's check) print(crt_two(2, 6, 3, 4)) # None — no solution
None
None
Garner's Algorithm: CRT Without Giant M
Garner's algorithm reconstructs x by computing mixed-radix coefficients incrementally. Instead of computing M directly, it accumulates the solution modulo each mi one by one. This is particularly useful when the product M would overflow typical integer types.
The algorithm: 1. Choose ordering of moduli (typically sorted by size for numerical stability). 2. Compute coefficients c_i such that x = c_1 + c_2m1 + c_3m1*m2 + ... mod M. 3. Each c_i is computed modulo m_i using a precomputed inversion table.
import java.math.BigInteger; import java.util.*; public class GarnerCRT { public static BigInteger garnersAlgorithm( int[] remainders, int[] moduli) { int k = moduli.length; BigInteger[] mod = new BigInteger[k]; BigInteger[] rem = new BigInteger[k]; for (int i = 0; i < k; i++) { mod[i] = BigInteger.valueOf(moduli[i]); rem[i] = BigInteger.valueOf(remainders[i]); } BigInteger x = BigInteger.ZERO; BigInteger product = BigInteger.ONE; for (int i = 0; i < k; i++) { // Compute coefficient c_i BigInteger ci = rem[i].subtract(x).mod(mod[i]); // Divide by product modulo mod[i] BigInteger inv = product.modInverse(mod[i]); ci = ci.multiply(inv).mod(mod[i]); // If ci is negative, adjust if (ci.signum() < 0) ci = ci.add(mod[i]); x = x.add(product.multiply(ci)); product = product.multiply(mod[i]); } return x; } public static void main(String[] args) { int[] r = {2, 3, 2}; int[] m = {3, 5, 7}; System.out.println(garnersAlgorithm(r, m)); // 23 } }
CRT for RSA Speed-up: Under the Hood
RSA decryption with CRT works because RSA uses composite modulus n = p*q. Instead of computing m = c^d mod n directly (slow), you compute: - m_p = c^d mod p - m_q = c^d mod q Then use CRT to combine m_p and m_q into m mod n.
The exponent d is typically reduced modulo (p-1) and (q-1) for further speed. The combination step requires precomputed coefficients: a factor called qInv where q * qInv ≡ 1 (mod p).
import java.math.BigInteger; import java.security.SecureRandom; public class RSACRTDecryption { // Assume p, q, d are precomuted primes and exponent // qInv = q.modInverse(p) public static BigInteger crtDecrypt( BigInteger c, BigInteger p, BigInteger q, BigInteger d, BigInteger qInv) { BigInteger dp = d.mod(p.subtract(BigInteger.ONE)); BigInteger dq = d.mod(q.subtract(BigInteger.ONE)); BigInteger m1 = c.modPow(dp, p); BigInteger m2 = c.modPow(dq, q); // h = qInv * (m1 - m2) mod p BigInteger h = qInv.multiply(m1.subtract(m2)).mod(p); if (h.signum() < 0) h = h.add(p); return m2.add(h.multiply(q)); } public static void main(String[] args) { // Example (small primes for demonstration) BigInteger p = BigInteger.valueOf(61); BigInteger q = BigInteger.valueOf(53); BigInteger n = p.multiply(q); // 3233 BigInteger e = BigInteger.valueOf(17); BigInteger d = e.modInverse(p.subtract(BigInteger.ONE) .multiply(q.subtract(BigInteger.ONE))); // 2753 BigInteger qInv = q.modInverse(p); // 38 BigInteger c = BigInteger.valueOf(855); // ciphertext BigInteger m = crtDecrypt(c, p, q, d, qInv); System.out.println(m); // 123? Actually original message // For RSA, m must be < n } }
- Exponentiation cost grows quadratically with bit length of modulus.
- CRT splits 2048-bit n into two 1024-bit computations.
- Each exponentiation is ~4x faster, so two are ~2x faster than one.
- Exponent reduction mod (p-1) and (q-1) trims exponent size further, adding another ~2x.
- Total ~4x speed-up in practice.
Generalised CRT: When Moduli Are Not Coprime
The classic CRT assumption of pairwise coprime moduli is often violated in practice — for example, mixing modulo 6 and modulo 8. In such cases, a solution exists iff for every pair (i,j), a_i ≡ a_j (mod gcd(m_i, m_j)). If it exists, the solution is unique modulo LCM of all moduli, not the product.
The algorithm: merge congruences one by one using the two-CRT method (handles non-coprime). At each step, compute new modulus as lcm of current pair.
from math import gcd def generalized_crt(remainders, moduli): # Merge all congruences incrementally x, lcm = remainders[0], moduli[0] for i in range(1, len(moduli)): r = remainders[i] m = moduli[i] g = gcd(lcm, m) if (r - x) % g != 0: return None # no solution # Merge: solve x ≡ x (mod lcm) and x ≡ r (mod m) lcm_new = lcm * m // g # Compute new solution modulo lcm_new # Using extended gcd: p*lcm + q*m = g _, p, _ = extended_gcd(lcm // g, m // g) x = (x + lcm * ((r - x) // g * p % (m // g))) % lcm_new lcm = lcm_new return x, lcm # Example where moduli not coprime print(generalized_crt([2, 2], [6, 8])) # expects solution mod LCM=24 print(generalized_crt([2, 3], [6, 8])) # None because 2 ≠ 3 mod 2
None
🎯 Key Takeaways
- CRT: unique solution mod M=Πmi to system x≡ai (mod mi) when moduli are pairwise coprime.
- Solution: x = Σ ai × (M/mi) × (M/mi)^(-1) mod mi.
- Extends to non-coprime moduli if solution exists (GCD divides difference of remainders).
- RSA uses CRT internally to speed up decryption by ~4x.
- Garner's algorithm avoids huge intermediate product M — prefer it for large moduli.
- Always reduce final result modulo M — the most common bug.
- Normalise remainders to [0, m-1] and check for integer overflow in intermediate products.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QState the Chinese Remainder Theorem and its conditions.JuniorReveal
- QHow does CRT speed up RSA decryption? Explain the math and the approximate speed-up factor.Mid-levelReveal
- QGiven x≡2(mod 3), x≡3(mod 5), find x mod 15.JuniorReveal
- QWhat happens if the moduli are not coprime — when does a solution still exist?SeniorReveal
- QImplement a function to solve CRT for two congruences using only integer arithmetic (no BigInteger). Assume moduli fit in 64-bit.SeniorReveal
Frequently Asked Questions
Why must moduli be pairwise coprime for the basic CRT?
Pairwise coprimality ensures the system has a unique solution. If GCD(m1,m2)=d>1, the system x≡a1(mod m1), x≡a2(mod m2) has a solution only if d|(a2-a1), and the solution is unique mod LCM(m1,m2), not mod m1*m2.
How does Garner's algorithm differ from the classic CRT formula?
Garner's algorithm computes the solution incrementally, building mixed-radix coefficients one by one. It never computes the full product M, which is beneficial when M is huge. The classic formula computes M first and then each term separately, requiring arithmetic modulo M throughout.
Can CRT be used for error correction?
Yes, CRT can detect and correct errors in residue number systems (RNS). By adding a redundant modulus, you can detect when a residue is corrupted. CRT-based codes are used in fault-tolerant computing and some communication protocols.
What is the time complexity of CRT and Garner's algorithm?
Classic CRT for k congruences requires k modular inverses and k multiplications, each modulo O(k log M) using extended Euclidean algorithm. Garner's algorithm also runs in O(k^2) time if inverses are precomputed, or O(k^3) if computed on the fly. In practice, k is small (often 2 or 3), so both are effectively O(1).
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.