Luke a Pro

Luke Sun

Developer & Marketer

🇺🇦
EN||

RSA 在現代系統中的角色:真實用途與為何被替代

| , 9 minutes reading.

1. 為什麼要關心這個問題?

你已經學會了 RSA 的數學基礎。但如果你觀察今天的實際系統:

  • TLS 1.3 完全移除了 RSA 金鑰交換
  • 大多數網站使用 ECDSA 或 EdDSA 憑證
  • SSH 預設使用 Ed25519 金鑰
  • Signal、WhatsApp 使用 Curve25519

RSA 怎麼了?它過時了嗎?理解 RSA 的演變能幫助你為系統做出更好的選擇。

2. RSA 的三種角色

RSA 技術上能做三件事,但現代用途很有限:

┌─────────────────────────────────────────────────────────────┐
│ 角色 1:直接加密                                              │
│ 狀態:絕對不要用                                              │
│ 原因:大小限制、效能問題、安全問題                              │
├─────────────────────────────────────────────────────────────┤
│ 角色 2:金鑰交換(加密會話金鑰)                                │
│ 狀態:已廢棄(TLS 1.3 移除了它)                               │
│ 原因:沒有前向保密                                            │
├─────────────────────────────────────────────────────────────┤
│ 角色 3:數位簽章                                              │
│ 狀態:仍在使用(但 ECC 更受歡迎)                              │
│ 用途:程式碼簽章、憑證、遺留系統                                │
└─────────────────────────────────────────────────────────────┘

3. 為什麼不能用 RSA 直接加密資料

大小限制

RSA-2048 只能加密:
  最大原始資料:256 位元組
  使用 OAEP 填充後:214 位元組

你的 1MB 檔案?做不到。
你的 1KB JSON?還是做不到。

效能災難

# 基準測試:加密 1MB 資料

import time
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os

# 產生金鑰
rsa_private = rsa.generate_private_key(public_exponent=65537, key_size=2048)
rsa_public = rsa_private.public_key()
aes_key = os.urandom(32)

data = os.urandom(1024 * 1024)  # 1MB

# RSA:必須分成 190 位元組的區塊(為了填充安全)
def rsa_encrypt_chunked(data):
    chunks = [data[i:i+190] for i in range(0, len(data), 190)]
    return [rsa_public.encrypt(
        chunk,
        padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()),
                     algorithm=hashes.SHA256(), label=None)
    ) for chunk in chunks]

# AES-GCM:一次加密全部
def aes_encrypt(data):
    nonce = os.urandom(12)
    return AESGCM(aes_key).encrypt(nonce, data, None)

# 時間對比
start = time.time()
rsa_encrypt_chunked(data)
rsa_time = time.time() - start

start = time.time()
aes_encrypt(data)
aes_time = time.time() - start

print(f"RSA 分塊: {rsa_time:.2f}s")
print(f"AES-GCM: {aes_time:.4f}s")
print(f"RSA 慢了 {rsa_time/aes_time:.0f} 倍")

# 典型輸出:
# RSA 分塊: 15.23s
# AES-GCM: 0.0021s
# RSA 慢了 7252 倍

正確的方式:混合加密

from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os

def hybrid_encrypt(public_key, plaintext: bytes) -> dict:
    """使用混合加密加密資料"""
    # 1. 產生隨機對稱金鑰
    session_key = os.urandom(32)

    # 2. 用對稱金鑰加密資料(快)
    nonce = os.urandom(12)
    ciphertext = AESGCM(session_key).encrypt(nonce, plaintext, None)

    # 3. 用 RSA 加密對稱金鑰(小,所以快)
    encrypted_key = public_key.encrypt(
        session_key,
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None
        )
    )

    return {
        'encrypted_key': encrypted_key,
        'nonce': nonce,
        'ciphertext': ciphertext
    }

def hybrid_decrypt(private_key, encrypted: dict) -> bytes:
    """解密混合加密的資料"""
    # 1. 用 RSA 解密對稱金鑰
    session_key = private_key.decrypt(
        encrypted['encrypted_key'],
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None
        )
    )

    # 2. 用對稱金鑰解密資料
    plaintext = AESGCM(session_key).decrypt(
        encrypted['nonce'],
        encrypted['ciphertext'],
        None
    )

    return plaintext

4. TLS 中的 RSA 金鑰交換(舊方式)

TLS 1.2 中的工作方式

用戶端                              伺服器
  │                                    │
  │───── ClientHello ─────────────────>│
  │                                    │
  │<──── ServerHello + 憑證 ───────────│
  │      (包含 RSA 公鑰)               │
  │                                    │
  │                                    │
  │   用戶端產生 PreMasterSecret        │
  │   用伺服器的 RSA 金鑰加密            │
  │                                    │
  │───── ClientKeyExchange ───────────>│
  │      (RSA 加密的金鑰)              │
  │                                    │
  │   雙方從 PreMasterSecret            │
  │   衍生會話金鑰                       │
  │                                    │
  │<════ 加密通訊 ════════════════════>│

致命缺陷:沒有前向保密

問題所在:

1. 攻擊者記錄你所有的加密流量(儲存很便宜)
2. 多年後,攻擊者竊取了伺服器的私鑰
3. 攻擊者解密所有歷史流量

這叫做「現在收集,未來解密」

RSA 金鑰交換意味著:
- 一個金鑰洩露 = 所有過去的會話都被破解
- 沒有前向保密

TLS 1.3 的解決方案:只允許臨時金鑰交換

TLS 1.3 完全移除了 RSA 金鑰交換
只允許 (EC)DHE - 臨時 Diffie-Hellman

用戶端                              伺服器
  │                                    │
  │───── ClientHello + KeyShare ──────>│
  │      (臨時 ECDH 公鑰)              │
  │                                    │
  │<──── ServerHello + KeyShare ───────│
  │      (臨時 ECDH 公鑰)              │
  │                                    │
  │   雙方計算共享金鑰                   │
  │   金鑰用後即棄                       │
  │                                    │
  │<════ 前向保密的加密 ═══════════════>│

即使伺服器的私鑰之後洩露,
過去的會話仍然安全!

5. RSA 簽章仍然存在

雖然 RSA 加密已被廢棄,RSA 簽章仍在使用:

當前用途:
├── 程式碼簽章
│   ├── Windows Authenticode(RSA 常見)
│   ├── macOS codesign(RSA 或 ECDSA)
│   └── Android APK(正在轉向 ECDSA)

├── TLS 憑證
│   ├── 遺留:RSA 簽章
│   ├── 現代:ECDSA 優先
│   └── 最新:EdDSA (Ed25519)

├── SSH 金鑰
│   ├── 遺留:ssh-rsa(OpenSSH 8.8 已廢棄)
│   ├── 現代:rsa-sha2-256, rsa-sha2-512
│   └── 推薦:ssh-ed25519

└── JWT/JWS
    ├── RS256, RS384, RS512 (RSA)
    └── ES256, ES384, ES512 (ECDSA,推薦)

RSA 簽章範例

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding

# 產生金鑰對
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048
)
public_key = private_key.public_key()

# 簽章
message = b"Important document content"
signature = private_key.sign(
    message,
    padding.PSS(
        mgf=padding.MGF1(hashes.SHA256()),
        salt_length=padding.PSS.MAX_LENGTH
    ),
    hashes.SHA256()
)

# 驗證
try:
    public_key.verify(
        signature,
        message,
        padding.PSS(
            mgf=padding.MGF1(hashes.SHA256()),
            salt_length=padding.PSS.MAX_LENGTH
        ),
        hashes.SHA256()
    )
    print("簽章有效!")
except Exception:
    print("簽章無效!")

6. 真實世界的 RSA 漏洞

填充預言攻擊(Bleichenbacher 攻擊)

針對 PKCS#1 v1.5 填充的攻擊:

1. 攻擊者截獲 RSA 密文
2. 向伺服器發送修改過的密文
3. 伺服器根據填充有效性給出不同回應
4. 經過數百萬次查詢後,攻擊者恢復明文

著名案例:
- DROWN 攻擊(2016)
- ROBOT 攻擊(2017)

防禦:使用 OAEP 填充,不要用 PKCS#1 v1.5

時序攻擊

# 有漏洞:解密過程中的時序洩露
def vulnerable_decrypt(ciphertext, private_key):
    plaintext = rsa_decrypt(ciphertext, private_key)
    if not valid_padding(plaintext):  # 這個時間會變化!
        raise PaddingError()
    return plaintext

# 安全:常數時間操作
# 使用處理了這個問題的函式庫實作

常見實作錯誤

錯誤 1:使用不帶填充的原始 RSA
  C = M^e mod n
  → 乘法性質允許操縱
  → 攻擊者可以偽造:C' = C × 2^e = (2M)^e

錯誤 2:小公開指數配合小訊息
  如果 M^e < n,攻擊者直接計算 e 次方根即可

錯誤 3:相同訊息發給多個接收者
  當 e=3 且相同的 M 發給 3 個不同的 n 值時,
  中國餘數定理可以恢復 M

錯誤 4:相關訊息
  用同一個金鑰加密 M 和 M+1
  → Franklin-Reiter 攻擊可以恢復兩者

7. RSA vs ECC 對比

屬性              | RSA-2048        | ECC P-256
------------------+-----------------+------------------
金鑰大小          | 256 位元組       | 32 位元組
簽章大小          | 256 位元組       | 64 位元組
安全等級          | 112 位元         | 128 位元
金鑰產生          | 慢(找質數)      | 快
簽章速度          | 中等             | 快
驗證速度          | 快(e 小)        | 中等
效能趨勢          | 越來越差          | 保持良好

等效安全性:
RSA-3072(384 位元組)≈ ECC P-256(32 位元組)
RSA-15360(1920 位元組)≈ ECC P-521(66 位元組)

8. 何時仍應使用 RSA

合理的使用場景

1. 遺留系統相容性
   - 舊系統只支援 RSA
   - 政府/企業要求

2. 硬體限制
   - 某些 HSM 針對 RSA 最佳化
   - 某些智慧卡只支援 RSA

3. 特定協定要求
   - 某些簽章 PDF 標準
   - 某些企業 PKI 系統

建議

新專案檢查清單:

□ 金鑰交換:使用 ECDH(X25519 優先)
□ 簽章:使用 EdDSA (Ed25519) 或 ECDSA (P-256)
□ TLS:使用 TLS 1.3(沒有 RSA 金鑰交換)
□ SSH:使用 Ed25519 金鑰
□ JWT:使用 ES256 代替 RS256

只有在以下情況使用 RSA:
□ 被遺留相容性強制要求
□ 被外部法規要求
□ 硬體只支援 RSA

9. 從 RSA 遷移的路徑

TLS 憑證遷移

步驟 1:產生新的 ECC 金鑰
openssl ecparam -genkey -name prime256v1 -out ecdsa.key

步驟 2:建立 CSR
openssl req -new -key ecdsa.key -out ecdsa.csr

步驟 3:部署雙憑證(過渡期)
- 主要:ECDSA 憑證
- 備用:RSA 憑證(用於舊用戶端)

步驟 4:監控並在安全時移除 RSA

SSH 金鑰遷移

# 產生新的 Ed25519 金鑰
ssh-keygen -t ed25519 -C "your_email@example.com"

# 新增到伺服器的 authorized_keys
# 暫時保留 RSA 金鑰以確保相容性

# 測試 Ed25519 存取
ssh -i ~/.ssh/id_ed25519 user@server

# 確認無誤後刪除 RSA 金鑰

10. 常見誤區

誤區現實
「RSA 被破解了」RSA 數學沒問題,但有更好的替代品
「更長的金鑰 = 更好」RSA 金鑰增長是不可持續的
「RSA 加密是標準」混合加密才是標準
「TLS 使用 RSA 加密」TLS 1.3 只用 RSA 做簽章
「為了安全我應該用 RSA」你應該用 ECC

11. 本章小結

三點要記住:

  1. RSA 不是用來直接加密的。 使用混合加密:RSA 加密對稱金鑰,對稱金鑰加密資料。

  2. RSA 金鑰交換缺乏前向保密。 TLS 1.3 移除了它。使用臨時 (EC)DH 代替。

  3. RSA 簽章仍然有效,但 ECC 更好。 對於新專案,優先使用 Ed25519 或 ECDSA 而不是 RSA。

12. 下一步

RSA 服務了我們數十年,但它的金鑰尺寸不斷增長,而 ECC 保持緊湊。橢圓曲線密碼學如何用更小的金鑰達到相同的安全性?

在下一篇文章中:橢圓曲線密碼學——曲線上的點如何能替代質因數分解,以及為什麼密碼學世界正在轉向曲線。