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 保持紧凑。椭圆曲线密码学如何用更小的密钥达到相同的安全性?

在下一篇文章中:椭圆曲线密码学——曲线上的点如何能替代质因数分解,以及为什么密码学世界正在转向曲线。