椭圆曲线密码学:更小的密钥,同样的安全性
Published: Sat Feb 01 2025 | Modified: Sat Feb 07 2026 , 9 minutes reading.
1. 为什么要关心这个问题?
比较两个具有同等安全性的密钥:
RSA-3072 公钥(384 字节):
-----BEGIN PUBLIC KEY-----
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA2a3Y...
[大约 12 行 base64]
-----END PUBLIC KEY-----
ECC P-256 公钥(65 字节,或带编码 91 字节):
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
[2 行 base64]
-----END PUBLIC KEY-----同样的安全性,小 6 倍。这对以下场景很重要:
- HTTPS 证书(更小 = 更快的握手)
- 物联网设备(有限的存储和带宽)
- 移动应用(更快的操作,更省电)
- 区块链(每个字节都要钱)
2. 定义
椭圆曲线密码学(ECC) 使用有限域上椭圆曲线的代数结构来创建密码学操作。
其安全性基于 椭圆曲线离散对数问题(ECDLP):给定曲线上的点 P 和 Q = kP,找出 k 在计算上是不可行的。
单向函数:
容易: k × P = Q (标量乘法)
给定 k 和 P,计算 Q
困难: Q / P = k (离散对数)
给定 P 和 Q,找出 k3. 什么是椭圆曲线?
数学形式
在实数上,椭圆曲线是:
y² = x³ + ax + b
其中:4a³ + 27b² ≠ 0(确保没有奇点)视觉形状
y
│ *
│ * *
│ * *
────┼──*─────*──────── x
│ * *
│ * *
│ *
│密码学曲线使用有限域
在密码学中,我们不使用实数。
我们使用对某个质数 p 取模的整数:
y² ≡ x³ + ax + b (mod p)
例子:P-256 曲线
p = 2²⁵⁶ - 2²²⁴ + 2¹⁹² + 2⁹⁶ - 1
所有运算在 p 处回绕
结果总是 0 到 p-1 之间的整数4. 点加法:核心操作
几何直觉(在实数上)
要把点 P 和 Q 相加:
1. 画一条穿过 P 和 Q 的线
2. 这条线与曲线相交于第三点 R
3. 将 R 关于 x 轴反射得到 P + Q
y
│ P*
│ * *
│ * Q *
────┼──*─────*──────── x
│ * R' * (P + Q)
│ * *
│ R点倍增
要计算 P + P = 2P:
1. 在点 P 处画切线
2. 切线与曲线相交于点 R
3. 将 R 关于 x 轴反射得到 2P无穷远点
特殊点 O(单位元):
P + O = P
P + (-P) = O
-P 是 P 关于 x 轴的反射5. 标量乘法:安全性所在
计算 Q = kP
k = 7, P = 某个点
朴素方法:P + P + P + P + P + P + P = 7P
(7 次加法)
倍增加法(高效):
7 = 111(二进制)
步骤 1:P
步骤 2:2P(倍增)
步骤 3:2P + P = 3P(加,因为位是 1)
步骤 4:6P(倍增)
步骤 5:6P + P = 7P(加,因为位是 1)
只需要 ~log₂(k) 次操作困难问题
给定:P(基点,公开)
Q = kP(结果点,公开)
找出:k(标量,私钥)
对于 256 位曲线:
- k 有 ~2²⁵⁶ 种可能
- 没有已知的捷径(不像分解)
- 最佳攻击:~2¹²⁸ 次操作(生日界)6. 常用曲线
NIST 曲线
P-256(secp256r1,prime256v1):
- 256 位素数域
- 部署最广泛
- NIST 标准化,NSA 设计
- 因为未解释的常数而有一些不信任
P-384(secp384r1):
- 384 位素数域
- 更高的安全边际
- 用于政府应用
P-521(secp521r1):
- 521 位素数域
- 最高安全性,更慢
- 很少需要Curve25519(现代选择)
由 Daniel Bernstein 设计:
- 255 位素数域(2²⁵⁵ - 19)
- 设计上抵抗时序攻击
- 比 NIST 曲线更快
- 没有未解释的常数
- 用于:Signal、WhatsApp、SSH、WireGuard
变体:
- X25519:密钥交换(ECDH)
- Ed25519:签名(EdDSA)比特币的曲线
secp256k1:
- 256 位 Koblitz 曲线
- 比 P-256 稍快
- 被比特币、以太坊使用
- 和 P-256 (secp256r1) 不同7. ECC 密钥生成
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.backends import default_backend
# 生成密钥对
private_key = ec.generate_private_key(
ec.SECP256R1(), # P-256 曲线
default_backend()
)
public_key = private_key.public_key()
# 内部是什么:
# private_key:随机 256 位整数 k
# public_key:点 Q = k × G(G 是生成点)
# 提取数值
private_numbers = private_key.private_numbers()
public_numbers = private_numbers.public_numbers
print(f"私钥 (k): {private_numbers.private_value}")
print(f"公钥 (x): {public_numbers.x}")
print(f"公钥 (y): {public_numbers.y}")密钥大小
曲线 | 私钥 | 公钥 | 安全性
-----------+-----------+------------+-----------
P-256 | 32 字节 | 65 字节* | 128 位
P-384 | 48 字节 | 97 字节* | 192 位
P-521 | 66 字节 | 133 字节* | 256 位
Curve25519 | 32 字节 | 32 字节 | 128 位
* 未压缩格式 (04 || x || y)
压缩格式:(02/03 || x),大约一半大小8. ECDSA:椭圆曲线数字签名
签名过程
用私钥 k 签名消息 m:
1. 计算哈希:e = HASH(m)
2. 生成随机 nonce r(关键:必须随机)
3. 计算 R = r × G
4. 取 x 坐标:rx = Rx mod n
5. 计算 s = r⁻¹(e + rx × k) mod n
6. 签名是 (rx, s)验证过程
用公钥 Q 验证消息 m 上的签名 (rx, s):
1. 计算哈希:e = HASH(m)
2. 计算 u1 = e × s⁻¹ mod n
3. 计算 u2 = rx × s⁻¹ mod n
4. 计算 R = u1 × G + u2 × Q
5. 检查:Rx mod n == rx?代码示例
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
# 生成密钥
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()
# 签名
message = b"Hello, ECDSA!"
signature = private_key.sign(
message,
ec.ECDSA(hashes.SHA256())
)
# 验证
try:
public_key.verify(
signature,
message,
ec.ECDSA(hashes.SHA256())
)
print("签名有效!")
except Exception:
print("签名无效!")9. EdDSA:现代替代方案
为什么选择 EdDSA 而不是 ECDSA?
ECDSA 的问题:
1. 每次签名都需要安全的随机 nonce
- PlayStation 3 被黑:重用 nonce → 私钥泄露
- 同样的 nonce 用两次 = 游戏结束
2. 安全实现复杂
- 时序攻击
- 故障攻击
EdDSA (Ed25519) 的解决方案:
1. 从消息哈希确定性生成 nonce
- 签名时不需要随机数
- 不会意外重用
2. 设计为常数时间实现
- 抵抗时序攻击
3. 更快更简单Ed25519 示例
from cryptography.hazmat.primitives.asymmetric import ed25519
# 生成密钥
private_key = ed25519.Ed25519PrivateKey.generate()
public_key = private_key.public_key()
# 签名(不需要指定算法——它是内置的)
message = b"Hello, Ed25519!"
signature = private_key.sign(message)
# 验证
try:
public_key.verify(signature, message)
print("签名有效!")
except Exception:
print("签名无效!")10. ECDH:用曲线做密钥交换
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
# Alice 生成她的密钥对
alice_private = ec.generate_private_key(ec.SECP256R1())
alice_public = alice_private.public_key()
# Bob 生成他的密钥对
bob_private = ec.generate_private_key(ec.SECP256R1())
bob_public = bob_private.public_key()
# 双方计算相同的共享密钥
alice_shared = alice_private.exchange(ec.ECDH(), bob_public)
bob_shared = bob_private.exchange(ec.ECDH(), alice_public)
assert alice_shared == bob_shared # 相同!
# 从共享密钥派生实际密钥
derived_key = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=None,
info=b'handshake data'
).derive(alice_shared)X25519:现代 ECDH
from cryptography.hazmat.primitives.asymmetric import x25519
# Alice
alice_private = x25519.X25519PrivateKey.generate()
alice_public = alice_private.public_key()
# Bob
bob_private = x25519.X25519PrivateKey.generate()
bob_public = bob_private.public_key()
# 共享密钥
alice_shared = alice_private.exchange(bob_public)
bob_shared = bob_private.exchange(alice_public)
assert alice_shared == bob_shared11. ECC 安全考虑
ECDSA 中的 Nonce 重用
如果你用同一个 nonce r 签名两条不同的消息:
签名 1:s1 = r⁻¹(e1 + rx × k)
签名 2:s2 = r⁻¹(e2 + rx × k)
从这些可以得到:
s1 - s2 = r⁻¹(e1 - e2)
r = (e1 - e2) / (s1 - s2)
然后:
k = (s1 × r - e1) / rx
私钥被恢复了!
索尼的 PlayStation 3 使用固定的 r → 被黑了曲线选择很重要
要避免的弱曲线:
- 嵌入度小的曲线(配对攻击)
- 二进制域上的曲线(近期攻击)
- 常数可疑的曲线
安全的选择:
- Curve25519/Ed25519(推荐)
- P-256(广泛支持)
- P-384(更高安全需求)实现攻击
时序攻击:
- 测量操作需要多长时间
- 从时序推断密钥位
防御:
- 使用常数时间实现
- libsodium 等库处理了这个问题
- Curve25519 设计上抵抗
无效曲线攻击:
- 发送不在曲线上的点
- 弱群结构
防御:
- 始终验证点
- 使用 Montgomery 曲线 (Curve25519)12. ECC vs RSA:最终对比
| RSA | ECC
--------------------+------------------+-----------------
密钥大小(128 位) | 3072 位 | 256 位
密钥大小(256 位) | 15360 位 | 512 位
密钥生成 | 慢(素性测试) | 快
签名 | 较慢 | 较快
验证 | 较快 | 较慢
加密支持 | 直接(混合) | 只能密钥交换
量子抵抗 | 被 Shor 破解 | 被 Shor 破解
成熟度 | 1977 | 1985+
采用情况 | 遗留主导 | 现代主导13. 常见误区
| 误区 | 现实 |
|---|---|
| 「ECC 是新的未经验证的」 | ECC 始于 1985 年,2000 年代以来广泛部署 |
| 「更小的密钥 = 更不安全」 | 密钥大小比较只在同一算法内有效 |
| 「P-256 不安全」 | 它没问题,只是不如 Curve25519 受信任 |
| 「ECC 完全替代了 RSA」 | ECC 不能做直接加密 |
| 「Ed25519 和 ECDSA 是一样的」 | 不同的算法有不同的特性 |
14. 本章小结
三点要记住:
ECC 用更小的密钥达到同样的安全性。 256 位 ECC ≈ 3072 位 RSA。这意味着更快的操作、更小的证书、更少的带宽。
曲线选择很重要。 新项目使用 Curve25519/Ed25519。为了兼容性回退到 P-256。
绝不要重用 ECDSA nonce。 一次重用 = 私钥泄露。优先使用 Ed25519 的确定性签名。
15. 下一步
我们已经看到 RSA 如何使用分解,ECC 如何使用曲线。但两者都需要解决一个根本问题:两个从未见面的人如何协商出共享密钥?
在下一篇文章中:Diffie-Hellman 密钥交换——让安全通信无需预共享密钥成为可能的数学魔法。
