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.
Plain-English First
Suppose you know a number gives remainder 2 when divided by 3, remainder 3 when divided by 5, and remainder 2 when divided by 7. Can you find the number? The Chinese Remainder Theorem says: yes, and gives a unique answer modulo 3×5×7=105. The answer is 23. CRT reconstructs a number from its remainders — like reverse engineering from shadows.
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.
crt.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from math import gcd
from functools import reduce
defextended_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
defcrt(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 = 1for m in moduli:
M *= m
x = 0for ai, mi inzip(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# Verifyprint(23 % 3, 23 % 5, 23 % 7) # 2 3 2 ✓
Output
23
2 3 2
Think of CRT as System of Projections
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.
Production Insight
Integer overflow in product M is the top cause of CRT bugs in production.
Use arbitrary-precision arithmetic (BigInteger in Java, int in Python 3) from the start.
Rule: never compute M = ∏ m_i using 64-bit unless moduli are guaranteed small.
Key Takeaway
CRT: unique solution mod M for pairwise coprime moduli.
Implement carefully with overflow checks or arbitrary precision.
Garner's algorithm avoids large M — prefer it for production RSA.
Choosing CRT Implementation Strategy
IfNumber of moduli is small (≤5) and moduli are bounded
→
UseUse direct formula with 128-bit arithmetic — fast and simple.
IfModuli are large (e.g., RSA-sized ~1024 bits)
→
UseUse Garner's algorithm to avoid giant intermediate product M.
IfModuli are not guaranteed coprime
→
UseUse generalised CRT (check divisibility condition first) or solve incrementally.
IfPerformance is critical (e.g., RSA decryption in real-time system)
→
UseUse native library with optimised Garner's algorithm and constant-time operations.
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.
crt_two.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
defcrt_two(a1, m1, a2, m2):
"""Solve x ≡ a1 (mod m1) and x ≡ a2 (mod m2).
Works even ifgcd(m1,m2) != 1 (if solution exists).
Returns (x, lcm) orNoneif 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
Output
(3, 12)
None
None
Watch Out for Negative Remainders
Programming languages often return negative remainders for negative numbers. Always ensure your remainders are in [0, m-1] before feeding them into CRT. Normalise with a = ((a % m) + m) % m.
Production Insight
When chaining multiple two-CRT merges, rounding errors accumulate.
Keep intermediate x values reduced modulo lcm at each step.
Rule: always reduce x modulo lcm after each merge to prevent overflow growth.
Key Takeaway
Two-CRT is the building block for multi-modulus systems.
Check divisibility condition first — don't assume solution exists.
Normalise remainders to [0, m-1] before merging.
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.
GarnerCRT.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.math.BigInteger;
import java.util.*;
publicclassGarnerCRT {
publicstaticBigIntegergarnersAlgorithm(
int[] remainders, int[] moduli) {
int k = moduli.length;
BigInteger[] mod = newBigInteger[k];
BigInteger[] rem = newBigInteger[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_iBigInteger 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, adjustif (ci.signum() < 0) ci = ci.add(mod[i]);
x = x.add(product.multiply(ci));
product = product.multiply(mod[i]);
}
return x;
}
publicstaticvoidmain(String[] args) {
int[] r = {2, 3, 2};
int[] m = {3, 5, 7};
System.out.println(garnersAlgorithm(r, m)); // 23
}
}
Output
23
Numerical Stability in Garner's Algorithm
The order of moduli affects numerical stability. Sorting moduli in ascending order minimises the growth of intermediate x. Also, inverses modulo mi should be precomputed for efficiency when processing many CRT systems with the same moduli.
Production Insight
Garner's algorithm eliminates giant M, but introduces sequential dependency.
Parallelisation is difficult — each coefficient depends on previous ones.
Rule: for large CRT systems (k > 10), consider divide-and-conquer CRT with binary tree merging.
Use it when moduli are large (e.g., 1024-bit RSA primes).
Precompute inverses for repeated CRT solves with same moduli set.
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).
RSACRTDecryption.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.math.BigInteger;
import java.security.SecureRandom;
publicclassRSACRTDecryption {
// Assume p, q, d are precomuted primes and exponent// qInv = q.modInverse(p)publicstaticBigIntegercrtDecrypt(
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 pBigInteger h = qInv.multiply(m1.subtract(m2)).mod(p);
if (h.signum() < 0) h = h.add(p);
return m2.add(h.multiply(q));
}
publicstaticvoidmain(String[] args) {
// Example (small primes for demonstration)BigInteger p = BigInteger.valueOf(61);
BigInteger q = BigInteger.valueOf(53);
BigInteger n = p.multiply(q); // 3233BigInteger e = BigInteger.valueOf(17);
BigInteger d = e.modInverse(p.subtract(BigInteger.ONE)
.multiply(q.subtract(BigInteger.ONE))); // 2753BigInteger qInv = q.modInverse(p); // 38BigInteger c = BigInteger.valueOf(855); // ciphertextBigInteger m = crtDecrypt(c, p, q, d, qInv);
System.out.println(m); // 123? Actually original message// For RSA, m must be < n
}
}
Output
123
Why CRT Gives 4x Speed-up
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.
Production Insight
Timing attacks exploit CRT implementation: constant-time is critical.
Attackers measure power or timing of the final combination step.
Rule: use constant-time modular arithmetic (avoid conditional branches) even in non-cryptographic CRT applications if side channels matter.
Key Takeaway
CRT speeds RSA decryption ~4x by parallelising over p and q.
Exponent reduction mod (p-1) and (q-1) is essential for performance.
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.
generalized_crt.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from math import gcd
defgeneralized_crt(remainders, moduli):
# Merge all congruences incrementally
x, lcm = remainders[0], moduli[0]
for i inrange(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 coprimeprint(generalized_crt([2, 2], [6, 8])) # expects solution mod LCM=24print(generalized_crt([2, 3], [6, 8])) # None because 2 ≠ 3 mod 2
Output
(2, 24)
None
LCM Can Grow Larger Than Product
When moduli share factors, LCM = product / d where d = gcd > 1. This means LCM is smaller than product, but in pathological cases (e.g., when moduli are all multiples of same number), the system may have fewer solutions. Always check divisibility condition before merging.
Production Insight
Generalized CRT is essential in cryptography when modulus generation might produce non-coprime values due to bugs.
Some libraries hide this with a 'assume coprime' precondition — a ticking bomb.
Rule: always check coprimality explicitly; use generalized CRT as fallback when moduli may not be coprime.
Use incremental merging with two-CRT to handle general systems.
● Production incidentPOST-MORTEMseverity: high
RSA Decrypt Returns Wrong Plaintext After CRT Optimisation
Symptom
Approximately 1 in 100,000 RSA decryptions yielded garbage plaintext. No pattern — no repoducible input sequence. The bug surfaced only under heavy load on 32-bit systems.
Assumption
The team assumed CRT implementation from OpenSSL's library was bulletproof. They'd wrapped the C library with a Java JNI layer and trusted the math.
Root cause
The native CRT code computed intermediate product (p * q) using 32-bit signed integers. When p and q were close to 2^1024, the product overflowed silently during the combination step, producing a wrong value for the final mod M.
Fix
Switch to 64-bit unsigned integers for all intermediate multiplications. Add explicit overflow checks — or better, use a library that handles arbitrary-precision integers (GMP, Bouncy Castle).
Key lesson
Never assume the CRT combination step is safe from overflow — test with maximum-sized moduli.
Always reduce intermediate results modulo M after each addition/multiplication, not just at the end.
Use language-provided checked arithmetic or libraries designed for large integers.
Production debug guideTriage symptoms that look like CRT bugs but aren't always CRT itself.4 entries
Symptom · 01
Decryption fails sporadically with large moduli
→
Fix
Check for integer overflow in product M and intermediate Mi calculations. Use BigInteger or arbitrary-precision arithmetic.
Symptom · 02
Verification fails: result mod each mi doesn't match expected remainder
→
Fix
Recompute each modular inverse step using extended Euclidean algorithm. Ensure yi = Mi^(-1) mod mi, not mod M.
Symptom · 03
Application uses non-coprime moduli but expects unique solution
→
Fix
Verify pairwise GCDs. If any GCD > 1, check if (a_j - a_i) % GCD == 0. If not, the system is unsolvable.
Symptom · 04
Garner's algorithm produces wrong output
→
Fix
Check that the mixed-radix coefficients are computed modulo mi and the final reduction uses modulus M. Compare with brute-force for small test cases.
★ CRT Debugging Cheat SheetQuick commands and checks for debugging CRT implementations in Java, C++, or Python.
Large modulo product M causes overflow−
Immediate action
Switch to BigInteger or GNU MP (gmp).
Commands
BigInteger M = m1.multiply(m2); // Java
BigInteger Mi = M.divide(mi);
Fix now
Use Java's BigInteger or Python's built-in int (unbounded).
Modular inverse doesn't exist+
Immediate action
Verify coprimality of mi and Mi.
Commands
gcd(mi, Mi) // should be 1
BigInteger yi = Mi.modInverse(mi); // Java
Fix now
If gcd ≠ 1, check pairwise coprimality of all moduli.
Final result not in [0, M-1]+
Immediate action
Ensure result % M is taken after summing terms.
Commands
x = x.mod(M); // Java
x %= M; // C++ after making x positive
Fix now
Always reduce modulo M after summation.
Non-coprime moduli set but system unsolvable+
Immediate action
Check divisibility condition for each pair.
Commands
(a2 - a1) % gcd(m1, m2) == 0?
If no, no solution exists.
Fix now
Either relax constraints or use generalised CRT for given moduli.
Key takeaways
1
CRT
unique solution mod M=Πmi to system x≡ai (mod mi) when moduli are pairwise coprime.
2
Solution
x = Σ ai × (M/mi) × (M/mi)^(-1) mod mi.
3
Extends to non-coprime moduli if solution exists (GCD divides difference of remainders).
4
RSA uses CRT internally to speed up decryption by ~4x.
5
Garner's algorithm avoids huge intermediate product M
prefer it for large moduli.
6
Always reduce final result modulo M
the most common bug.
7
Normalise remainders to [0, m-1] and check for integer overflow in intermediate products.
Common mistakes to avoid
5 patterns
×
Forgetting to reduce final result modulo M
Symptom
CRT returns a value larger than M, causing incorrect results in downstream computations (e.g., RSA plaintext larger than n).
Fix
Always apply x % M after summing all terms. In code: return x % M; not just x.
×
Assuming modular inverse exists for every Mi mod mi when moduli are not coprime
Symptom
The extended GCD returns g ≠ 1, and the algorithm fails or produces wrong inverse.
Fix
Check if Mi and mi are coprime. If not, either moduli aren't coprime (use generalised CRT) or there's a bug.
×
Using integer overflow-prone type (int/long) for product M when moduli can be large
Symptom
Seemingly random CRT results, especially in production with large moduli (e.g., RSA with 2048-bit n).
Fix
Use arbitrary-precision integers (BigInteger in Java, int in Python, libgmp in C++). If moduli are bounded small, use 128-bit or 256-bit built-in.
×
Ignoring negative remainders from programming languages
Symptom
CRT returns incorrect result because a remainder like -1 is treated as -1 mod m instead of m-1.
Fix
Normalise each remainder before use: a = ((a % m) + m) % m;
×
Applying CRT without verifying pairwise coprimality (standard formula)
Symptom
The algorithm gives a result that doesn't satisfy at least one congruence when tested.
Fix
Always compute gcd of all moduli pairs before using the classic formula. Use generalised CRT if non-coprime.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01JUNIOR
State the Chinese Remainder Theorem and its conditions.
Q02SENIOR
How does CRT speed up RSA decryption? Explain the math and the approxima...
Q03JUNIOR
Given x≡2(mod 3), x≡3(mod 5), find x mod 15.
Q04SENIOR
What happens if the moduli are not coprime — when does a solution still ...
Q05SENIOR
Implement a function to solve CRT for two congruences using only integer...
Q01 of 05JUNIOR
State the Chinese Remainder Theorem and its conditions.
ANSWER
The Chinese Remainder Theorem (CRT) states that for a set of pairwise coprime positive integers m1, m2, ..., mk and any integers a1, ..., ak, there exists a unique integer x modulo M = m1m2...mk such that x ≡ ai (mod mi) for all i. The solution is constructed as x = Σ ai (M/mi) * yi mod M, where yi is the modular inverse of (M/mi) modulo mi.
Q02 of 05SENIOR
How does CRT speed up RSA decryption? Explain the math and the approximate speed-up factor.
ANSWER
RSA decryption computes m = c^d mod n, where n = p*q. Instead of one exponentiation modulo n (a ≈2048-bit number), CRT lets us compute two exponentiations modulo p and q (each ≈1024-bit). Modular exponentiation cost is roughly O(log e × (log n)^2). Halving the modulus size reduces each exponentiation by about 4x in bit operations, and we do two exponentiations, so total is about 2x faster. Additionally, we can reduce the exponent d modulo p-1 and q-1, which further reduces the exponent size, yielding an overall speed-up of about 4x in practice. The combination step using CRT is trivial compared to exponentiation.
Q03 of 05JUNIOR
Given x≡2(mod 3), x≡3(mod 5), find x mod 15.
ANSWER
Solution: M = 35 = 15. M1 = 5, M2 = 3. Compute y1 = 5^(-1) mod 3 = 2 (since 52=10≡1 mod 3). y2 = 3^(-1) mod 5 = 2 (since 32=6≡1 mod 5). Then x = (2 5 2 + 3 3 * 2) mod 15 = (20 + 18) = 38 mod 15 = 8. Check: 8 mod 3 = 2, 8 mod 5 = 3. Answer is 8.
Q04 of 05SENIOR
What happens if the moduli are not coprime — when does a solution still exist?
ANSWER
If moduli are not pairwise coprime, the classic CRT formula fails because the inverses of M/mi modulo mi do not exist. However, a solution may still exist. The condition is that for any two congruences x ≡ a_i (mod m_i) and x ≡ a_j (mod m_j), we must have a_i ≡ a_j (mod gcd(m_i, m_j)). If this holds for all pairs, the system has a solution unique modulo LCM(m1,...,mk), not the product. The solution can be found by merging congruences incrementally using the two-CRT method that handles non-coprime moduli.
Q05 of 05SENIOR
Implement a function to solve CRT for two congruences using only integer arithmetic (no BigInteger). Assume moduli fit in 64-bit.
ANSWER
The function should compute gcd, check divisibility condition, compute lcm, compute p from extended gcd of m1/g and m2/g, then combine. See example:
``python
def crt_two(a1, m1, a2, m2):
g = gcd(m1, m2)
if (a2 - a1) % g != 0: return None
lcm = m1 // g m2
_, p, _ = extended_gcd(m1 // g, m2 // g)
x = (a1 + m1 ((a2 - a1) // g p % (m2 // g))) % lcm
return x, lcm
``
Note: compute lcm = m1 // g m2 to avoid overflow.
01
State the Chinese Remainder Theorem and its conditions.
JUNIOR
02
How does CRT speed up RSA decryption? Explain the math and the approximate speed-up factor.
SENIOR
03
Given x≡2(mod 3), x≡3(mod 5), find x mod 15.
JUNIOR
04
What happens if the moduli are not coprime — when does a solution still exist?
SENIOR
05
Implement a function to solve CRT for two congruences using only integer arithmetic (no BigInteger). Assume moduli fit in 64-bit.
SENIOR
FAQ · 4 QUESTIONS
Frequently Asked Questions
01
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.
Was this helpful?
02
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.
Was this helpful?
03
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.
Was this helpful?
04
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).