Cấu trúc dữ liệu và Giải thuật Bài 150: Phương pháp Phân tích Thừa số Đường cong Elliptic (Lenstra's Elliptic Curve Factorization - ECM) - Khi hình học đại số kết hợp lý thuyết nhóm giải bài toán thừa số nguyên tố.
Ở Bài 149, chúng ta đã nghiên cứu Sàng trường số đại số tổng quát (GNFS) — thuật toán có độ phức tạp mạnh nhất để phân tích một hợp số . Tuy nhiên, GNFS có một đặc tính cố hữu: độ phức tạp thời gian phụ thuộc hoàn toàn vào độ lớn của hợp số , bất kể các ước số nguyên tố của nó lớn hay nhỏ. Nếu là một số 1000 bit nhưng lại chứa một thừa số nguyên tố chỉ khoảng 40 chữ số thập phân (130 bit), việc kích hoạt toàn bộ cỗ máy GNFS là vô cùng tốn kém và lãng phí.
Năm 1987, nhà toán học người Hà Lan Hendrik Lenstra đã công bố một phát minh mang tính bước ngoặt: Thuật toán Phân tích Thừa số Đường cong Elliptic (Lenstra ECM). Bằng cách thay thế nhóm nhân hữu hạn trong thuật toán Pollard's bằng nhóm các điểm trên đường cong Elliptic , Lenstra đã tạo ra một thuật toán có độ phức tạp dưới hàm mũ chỉ phụ thuộc vào kích thước của ước nguyên tố nhỏ nhất :
Cho đến nay, ECM vẫn là thuật toán phân tích số nhanh thứ ba từng được biết đến (sau GNFS và SNFS), và giữ vị trí vô địch tuyệt đối trong việc tìm kiếm các ước số nguyên tố có kích thước lên tới 50–70 chữ số thập phân nằm bên trong các hợp số khổng lồ hàng nghìn bit.
1. Nguồn cảm hứng: Từ Bế tắc của Pollard's đến Ý tưởng của Lenstra
Nhắc lại Thuật toán Pollard's
Thuật toán Pollard's (1974) dựa trên Định lý Fermat nhỏ: Nếu là một ước nguyên tố của , thì với mọi số nguyên thỏa mãn :
Nếu là một số -trơn (-smooth) (tức là mọi ước nguyên tố của đều nhỏ hơn hoặc bằng ), thì số sẽ là một bội số của . Khi đó:
Tính . Nếu , ta tìm được thừa số .
Điểm nghẽn chí mạng của Pollard's
Nhóm nhân là duy nhất và có cấp cố định chính xác bằng .
-
Nếu tình cờ chứa một ước nguyên tố lớn (ví dụ với là số nguyên tố an toàn - Safe Prime), thuật toán Pollard's hoàn toàn bất lực.
-
Người thực hiện không có cách nào thay đổi cấu trúc của nhóm . Một khi không trơn, thuật toán thất bại vĩnh viễn.
Đột phá của Hendrik Lenstra
Thay vì làm việc trên nhóm , Lenstra đề xuất làm việc trên nhóm các điểm của một đường cong Elliptic trên trường hữu hạn .
Theo Định lý Hasse về Đường cong Elliptic (Hasse's Bound), số lượng điểm của nhóm (ký hiệu là ) không cố định, mà dao động trong một khoảng xác định quanh :
p + 1 - 2\sqrt{p} \le #E(\mathbb{F}_p) \le p + 1 + 2\sqrt{p}Ý tưởng thiên tài của Lenstra:
Nếu đường cong có cấp không trơn, ta không hề bị bế tắc! Ta chỉ việc chọn ngẫu nhiên một đường cong khác (thay đổi các hệ số ). Cấp của đường cong mới sẽ nhận một giá trị ngẫu nhiên khác trong khoảng Hasse.
Bằng cách thử nhiều đường cong độc lập, theo quy luật xác suất, ta chắc chắn sẽ tìm được một đường cong mà cấp của nó là số -trơn!
Pollard's p-1:
Nhóm nhân (Z/pZ)* ---> Cấp cố định p - 1 ---> Nếu không trơn => THẤT BẠI HOÀN TOÀN
Lenstra ECM:
Đường cong E_1 ---> Cấp N_1 ∈ [p+1 - 2√p, p+1 + 2√p] ---> Không trơn?
Đường cong E_2 ---> Cấp N_2 ∈ [p+1 - 2√p, p+1 + 2√p] ---> Không trơn?
...
Đường cong E_k ---> Cấp N_k là B-trơn! ---> THÀNH CÔNG!
2. Hình học Đại số: Luật cộng trên Đường cong Elliptic modulo
Một đường cong Elliptic trên vành (với ) ở dạng chuẩn Weierstrass rút gọn có phương trình:
với biệt thức .
Tập hợp các điểm trên đường cong cùng với Điểm vô cực (đóng vai trò là phần tử trung hòa của phép cộng) tạo thành một nhóm giao hoán (Abelian Group).
Công thức Cộng Điểm (Chord-and-Tangent Group Law)
Cho hai điểm và trên đường cong:
-
Phần tử nghịch đảo: . Nếu và , thì .
-
Hệ số góc của tiếp tuyến hoặc cát tuyến ():
-
Nếu (Cộng hai điểm khác nhau - Addition):
-
Nếu (Nhân đôi một điểm - Doubling):
-
-
Tọa độ của điểm tổng :
y ^
| . * (Tangent / Chord)
| . /
| . /
| . P(x1, y1)
| . /
| . /
+-----+-------Q(x2, y2)------> x
| . \
| . \
| . R'(x3, -y3)
| . \
| R(x3, y3) = P + Q
3. Cơ chế Khai thác: Khi "Thất bại" Phép tính trở thành Thành công
Lưu ý rằng là một hợp số, do đó không phải là một trường (Field) mà chỉ là một vành giao hoán (Ring). Một số phần tử trong không có nghịch đảo nhân!
Khi tính toán hệ số góc :
-
Ta cần tìm nghịch đảo modulo của mẫu số (hoặc ).
-
Để tính , ta áp dụng Thuật toán Euclid mở rộng để tìm .
Xuất hiện 3 kịch bản:
-
: nguyên tố cùng nhau với . Phép nghịch đảo tồn tại và duy nhất. Phép cộng điểm trên đường cong diễn ra bình thường.
-
: Phép tính nghịch đảo thất bại, nhưng ta lập tức phân tích thành công ! Vì chia hết cho và , nên chính là một thừa số không tầm thường của .
-
: Điểm đồng thời triệt tiêu về vô cực trên cả hai ước số. Ta hủy đường cong hiện tại và chọn đường cong mới.
Trực giác toán học: Khi cấp của nhóm chia hết cho hệ số nhân , phép nhân vô hướng sẽ biến điểm thành Điểm vô cực trên trường con . Nhưng trên trường con , điểm này vẫn là một điểm hữu hạn bình thường. Tọa độ của điểm rơi vào trạng thái "vừa là vô cực modulo , vừa là hữu hạn modulo ". Trạng thái phân rã này buộc mẫu số phải chia hết cho nhưng không chia hết cho , khiến xuất hiện tự nhiên!
4. Thuật toán Lenstra ECM Toàn diện (Giai đoạn 1 & Giai đoạn 2)
Giai đoạn 1 (Stage 1)
-
Chọn một cận trơn (ví dụ: cho các ước 30 bit; cho các ước 100 bit).
-
Chọn ngẫu nhiên một điểm và một hệ số .
-
Tính hệ số để đảm bảo điểm luôn nằm trên đường cong .
-
Kiểm tra : Nếu , ta tìm được thừa số ngay; nếu bằng , chọn lại.
-
Tính bội số tích lũy:
-
Thực hiện nhân vô hướng bằng cách lần lượt nhân với từng lũy thừa số nguyên tố :
- Ở mỗi bước cộng điểm hoặc nhân đôi điểm trong thuật toán Double-and-Add, nếu gặp phép tính nghịch đảo : dừng lại và trả về ước số .
Giai đoạn 2 (Stage 2 - Two-Stage ECM)
Nếu sau Giai đoạn 1 mà vẫn chưa tìm được ước, cấp của nhóm có thể có dạng:
#E(\mathbb{F}_p) = M \cdot qvới là số -trơn, còn là một số nguyên tố đơn lẻ nằm trong khoảng (với ).
-
Điểm từ Giai đoạn 1 đã triệt tiêu phần -trơn (tức là có cấp đúng bằng trên ).
-
Trong Giai đoạn 2, ta kiểm tra xem có tồn tại số nguyên tố sao cho hay không.
-
Áp dụng kỹ thuật Baby-step Giant-step để tính toán nhanh tích khoảng cách giữa các điểm nguyên tố, giảm độ phức tạp của Giai đoạn 2 từ xuống .
5. Hiện thực hóa Thuật toán Lenstra ECM bằng Golang
Chương trình Golang dưới đây cài đặt hoàn chỉnh:
-
Cấu trúc số nguyên lớn
math/bighỗ trợ các số nguyên có kích thước tùy ý. -
Phép nhân đôi (Doubling) và Phép cộng (Addition) với cơ chế bẫy lỗi nghịch đảo Euclid mở rộng để trích xuất ước số.
-
Thuật toán nhân vô hướng Double-and-Add.
-
Vòng lặp thử nghiệm đa đường cong Lenstra ECM Stage 1.
Go
package main
import (
"crypto/rand"
"fmt"
"math/big"
)
var (
zero = big.NewInt(0)
one = big.NewInt(1)
two = big.NewInt(2)
three = big.NewInt(3)
)
// Point biểu diễn một điểm (X, Y) trên đường cong Elliptic
// IsInfinity = true đại diện cho điểm vô cực O
type Point struct {
x, y *big.Int
isInfinity bool
}
func NewPoint(x, y *big.Int) Point {
return Point{
x: new(big.Int).Set(x),
y: new(big.Int).Set(y),
isInfinity: false,
}
}
func Infinity() Point {
return Point{x: nil, y: nil, isInfinity: true}
}
// EllipticCurve biểu diễn y^2 = x^3 + a*x + b (mod N)
type EllipticCurve struct {
a, b *big.Int
n *big.Int
}
// FactorFoundError là một ngoại lệ có chủ đích khi nghịch đảo modulo thất bại
type FactorFoundError struct {
Factor *big.Int
}
func (e *FactorFoundError) Error() string {
return fmt.Sprintf("Tìm thấy thừa số nguyên tố không tầm thường: %s", e.Factor.String())
}
// modularInverse tính nghịch đảo modulo N. Nếu gcd(val, N) > 1, trả về FactorFoundError
func (c *EllipticCurve) modularInverse(val *big.Int) (*big.Int, error) {
v := new(big.Int).Mod(val, c.n)
g := new(big.Int).GCD(nil, nil, v, c.n)
if g.Cmp(one) > 0 && g.Cmp(c.n) < 0 {
return nil, &FactorFoundError{Factor: g}
}
if g.Cmp(c.n) == 0 {
return nil, fmt.Errorf("Điểm suy biến hoàn toàn mod N")
}
inv := new(big.Int).ModInverse(v, c.n)
return inv, nil
}
// Add cộng hai điểm P và Q trên đường cong modulo N
func (c *EllipticCurve) Add(P, Q Point) (Point, error) {
if P.isInfinity {
return Q, nil
}
if Q.isInfinity {
return P, nil
}
// Kiểm tra nếu P == -Q
diffY := new(big.Int).Add(P.y, Q.y)
diffY.Mod(diffY, c.n)
if P.x.Cmp(Q.x) == 0 && diffY.Sign() == 0 {
return Infinity(), nil
}
var lambda *big.Int
if P.x.Cmp(Q.x) == 0 && P.y.Cmp(Q.y) == 0 {
// Nhân đôi điểm (P == Q): lambda = (3*x1^2 + a) / (2*y1) mod N
num := new(big.Int).Mul(P.x, P.x)
num.Mul(num, three)
num.Add(num, c.a)
num.Mod(num, c.n)
denom := new(big.Int).Mul(two, P.y)
denom.Mod(denom, c.n)
invDenom, err := c.modularInverse(denom)
if err != nil {
return Infinity(), err
}
lambda = new(big.Int).Mul(num, invDenom)
lambda.Mod(lambda, c.n)
} else {
// Cộng hai điểm khác nhau: lambda = (y2 - y1) / (x2 - x1) mod N
num := new(big.Int).Sub(Q.y, P.y)
num.Mod(num, c.n)
denom := new(big.Int).Sub(Q.x, P.x)
denom.Mod(denom, c.n)
invDenom, err := c.modularInverse(denom)
if err != nil {
return Infinity(), err
}
lambda = new(big.Int).Mul(num, invDenom)
lambda.Mod(lambda, c.n)
}
// x3 = lambda^2 - x1 - x2 mod N
x3 := new(big.Int).Mul(lambda, lambda)
x3.Sub(x3, P.x)
x3.Sub(x3, Q.x)
x3.Mod(x3, c.n)
// y3 = lambda*(x1 - x3) - y1 mod N
y3 := new(big.Int).Sub(P.x, x3)
y3.Mul(lambda, y3)
y3.Sub(y3, P.y)
y3.Mod(y3, c.n)
return NewPoint(x3, y3), nil
}
// ScalarMul tính k * P bằng thuật toán Double-and-Add
func (c *EllipticCurve) ScalarMul(k *big.Int, P Point) (Point, error) {
R := Infinity()
curr := P
kBits := k.Text(2)
for i := len(kBits) - 1; i >= 0; i-- {
if kBits[i] == '1' {
var err error
R, err = c.Add(R, curr)
if err != nil {
return Infinity(), err
}
}
var err error
curr, err = c.Add(curr, curr)
if err != nil {
return Infinity(), err
}
}
return R, nil
}
// Sàng số nguyên tố đơn giản sinh các số nguyên tố <= B
func sieveOfEratosthenes(limit int) []int64 {
isPrime := make([]bool, limit+1)
for i := 2; i <= limit; i++ {
isPrime[i] = true
}
for p := 2; p*p <= limit; p++ {
if isPrime[p] {
for i := p * p; i <= limit; i += p {
isPrime[i] = false
}
}
}
var primes []int64
for p := 2; p <= limit; p++ {
if isPrime[p] {
primes = append(primes, int64(p))
}
}
return primes
}
// LenstraECM thực thi thuật toán phân tích số
func LenstraECM(N *big.Int, B1 int, maxCurves int) (*big.Int, error) {
// Kiểm tra chia hết cho 2 hoặc 3
if new(big.Int).Mod(N, two).Sign() == 0 {
return two, nil
}
if new(big.Int).Mod(N, three).Sign() == 0 {
return three, nil
}
primes := sieveOfEratosthenes(B1)
for curveIdx := 1; curveIdx <= maxCurves; curveIdx++ {
// 1. Sinh ngẫu nhiên tọa độ điểm xuất phát P = (x0, y0) và tham số a
x0, _ := rand.Int(rand.Reader, N)
y0, _ := rand.Int(rand.Reader, N)
a, _ := rand.Int(rand.Reader, N)
// 2. Tính b = y0^2 - x0^3 - a*x0 mod N
y2 := new(big.Int).Mul(y0, y0)
x3 := new(big.Int).Mul(x0, x0)
x3.Mul(x3, x0)
ax := new(big.Int).Mul(a, x0)
b := new(big.Int).Sub(y2, x3)
b.Sub(b, ax)
b.Mod(b, N)
// 3. Kiểm tra biệt thức Delta = 4a^3 + 27b^2 mod N
disc4a3 := new(big.Int).Mul(a, a)
disc4a3.Mul(disc4a3, a)
disc4a3.Mul(disc4a3, big.NewInt(4))
disc27b2 := new(big.Int).Mul(b, b)
disc27b2.Mul(disc27b2, big.NewInt(27))
delta := new(big.Int).Add(disc4a3, disc27b2)
delta.Mod(delta, N)
g := new(big.Int).GCD(nil, nil, delta, N)
if g.Cmp(one) > 0 && g.Cmp(N) < 0 {
return g, nil // May mắn tìm thấy ước ngay khi sinh đường cong
}
if g.Cmp(N) == 0 {
continue // Đường cong suy biến, chọn lại
}
curve := &EllipticCurve{a: a, b: b, n: N}
P := NewPoint(x0, y0)
// 4. Giai đoạn 1: Nhân tích lũy các lũy thừa số nguyên tố q^e <= B1
currentPoint := P
factorFound := false
var discoveredFactor *big.Int
for _, p := range primes {
// Tính p^e <= B1 lớn nhất
pPower := p
for pPower*p <= int64(B1) {
pPower *= p
}
k := big.NewInt(pPower)
nextPoint, err := curve.ScalarMul(k, currentPoint)
if err != nil {
if fErr, ok := err.(*FactorFoundError); ok {
discoveredFactor = fErr.Factor
factorFound = true
break
}
// Gặp lỗi suy biến mod N, bỏ qua đường cong này
break
}
currentPoint = nextPoint
}
if factorFound {
return discoveredFactor, nil
}
}
return nil, fmt.Errorf("Không tìm thấy ước số sau %d đường cong với B1 = %d", maxCurves, B1)
}
func main() {
// Khởi tạo hợp số N gồm hai số nguyên tố 30-bit:
// p = 1000000007 (Prime 30-bit)
// q = 1000000009 (Prime 30-bit)
// N = 1000000016000000063 (Số 60-bit)
pTrue := big.NewInt(1000000007)
qTrue := big.NewInt(1000000009)
N := new(big.Int).Mul(pTrue, qTrue)
fmt.Printf("=== PHÂN TÍCH THỪA SỐ BẰNG THUẬT TOÁN LENSTRA ECM ===\n")
fmt.Printf("Hợp số mục tiêu N = %s (Độ dài: %d bit)\n", N.String(), N.BitLen())
fmt.Printf("Ước nguyên tố kỳ vọng: p = %s, q = %s\n\n", pTrue.String(), qTrue.String())
B1 := 2000 // Cận trơn B1 cho ước số 30-bit
maxCurves := 50 // Thử tối đa 50 đường cong ngẫu nhiên
fmt.Printf("Bắt đầu thực thi ECM với B1 = %d, số đường cong tối đa = %d...\n", B1, maxCurves)
factor, err := LenstraECM(N, B1, maxCurves)
if err != nil {
fmt.Printf("[THẤT BẠI] %v\n", err)
} else {
otherFactor := new(big.Int).Div(N, factor)
fmt.Printf("\n[THÀNH CÔNG] Đã phân tích thành công hợp số N:\n")
fmt.Printf(" -> Thừa số thứ nhất: %s\n", factor.String())
fmt.Printf(" -> Thừa số thứ hai: %s\n", otherFactor.String())
fmt.Printf(" -> Kiểm tra nhân lại: %t\n",
new(big.Int).Mul(factor, otherFactor).Cmp(N) == 0)
}
}
6. Tối ưu hóa Thực tế: Tọa độ Chiếu Montgomery (Montgomery Ladder)
Trong mã nguồn thực tế của các thư viện hàng đầu như GMP-ECM, dạng phương trình Weierstrass không được sử dụng vì nó đòi hỏi tính toán cả tọa độ và thực hiện phép nghịch đảo modulo trong từng bước cộng.
Thay vào đó, thuật toán sử dụng Đường cong Montgomery:
chuyển sang Tọa độ Chiếu (Projective Coordinates) với :
-
Loại bỏ hoàn toàn biến : Điểm chỉ cần biểu diễn bằng cặp .
-
Montgomery Ladder: Phép nhân vô hướng chỉ sử dụng các phép tính cộng, trừ và nhân số nguyên lớn:
-
Nhân đôi điểm (): Tốn đúng 2 phép nhân và 2 phép bình phương ().
-
Cộng hai điểm khi biết trước hiệu ( biết ): Tốn .
-
-
Không cần phép chia modular trong suốt Giai đoạn 1:
Thay vì tính nghịch đảo modulo ở từng bước, ta liên tục nhân dồn các mẫu số vào một biến tích lũy duy nhất:
Đến cuối cùng, ta chỉ cần gọi duy nhất một lần hàm để kiểm tra toàn bộ quá trình, giúp tăng tốc độ thực thi lên gấp 10 đến 20 lần.
7. Phân tích Độ phức tạp và Bảng So sánh Thuật toán
| Thuật toán | Phân loại | Độ phức tạp thời gian | Phụ thuộc vào | Ưu ứng dụng tối ưu nhất |
|---|---|---|---|---|
| Trial Division | Xác định | Các ước cực nhỏ | ||
| Pollard's | Ngẫu nhiên hóa | Ước nhỏ đến vừa ( bit) | ||
| Pollard's | Đại số | Khi là số -trơn | ||
| Lenstra ECM | Hình học đại số | (Ước nhỏ nhất) | Tìm ước digits trong số khổng lồ | |
| Quadratic Sieve (QS) | Đại số (Sàng) | (Toàn bộ) | Hợp số digits không có ước nhỏ | |
| GNFS | Đại số (Trường số) | (Toàn bộ) | Hợp số digits (Phá vỡ RSA) |
Tham số Lựa chọn Khuyến nghị cho ECM
| Kích thước ước (chữ số) | Kích thước ước (bit) | Cận trơn | Cận trơn | Số đường cong dự kiến |
|---|---|---|---|---|
| 15 digits | ~50 bit | ~25 | ||
| 25 digits | ~83 bit | ~160 | ||
| 35 digits | ~116 bit | ~1.000 | ||
| 45 digits | ~150 bit | ~4.400 | ||
| 55 digits | ~182 bit | ~18.000 | ||
| 65 digits | ~215 bit | ~70.000 |
8. Ứng dụng Thực tế
-
Giai đoạn Tiền xử lý (Pre-testing) bắt buộc trong GNFS: Khi phân tích các số nguyên lớn của dự án RSA Challenge hay GIMPS, người ta không bao giờ chạy trực tiếp GNFS ngay từ đầu. Bước chuẩn mực luôn là chạy Lenstra ECM với các cận tăng dần để "quét sạch" mọi ước nguyên tố tiềm năng dưới 60 chữ số. Nếu loại bỏ được một ước 40 chữ số, kích thước phần còn lại của giảm đi đáng kể, giúp tiết kiệm hàng trăm năm CPU khi chạy GNFS sau đó.
-
Phân tích các số Fermat (): Năm 1999, Richard Brent sử dụng ECM tìm ra ước số 40 chữ số của số Fermat thứ 10 (). Đây là minh chứng rõ nhất cho sức mạnh của ECM: có kích thước lên tới 309 chữ số thập phân (vượt xa tầm với của bất kỳ thuật toán sàng nào thời bấy giờ), nhưng ECM đã bóc tách thành công nhờ tính chất chỉ phụ thuộc vào .
-
Phát hiện Lỗ hổng sinh khóa RSA yếu: Một số thiết bị phần cứng (như thẻ thông minh Smart Card, vi điều khiển IoT) bị lỗi bộ sinh số ngẫu nhiên khiến một trong hai số nguyên tố hoặc bị lệch kích thước nghiêm trọng (Unbalanced RSA - ví dụ có 150 bit, có 1898 bit để tạo khóa 2048-bit). Các khóa này hoàn toàn miễn nhiễm với GNFS nhưng bị bẻ gãy bởi Lenstra ECM chỉ trong vài phút.
All rights reserved