Luke a Pro

Luke Sun

Developer & Marketer

🇺🇦
EN||

椭圆曲线密码学:更小的密钥,同样的安全性

| , 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,找出 k

3. 什么是椭圆曲线?

数学形式

在实数上,椭圆曲线是:

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_shared

11. 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. 本章小结

三点要记住:

  1. ECC 用更小的密钥达到同样的安全性。 256 位 ECC ≈ 3072 位 RSA。这意味着更快的操作、更小的证书、更少的带宽。

  2. 曲线选择很重要。 新项目使用 Curve25519/Ed25519。为了兼容性回退到 P-256。

  3. 绝不要重用 ECDSA nonce。 一次重用 = 私钥泄露。优先使用 Ed25519 的确定性签名。

15. 下一步

我们已经看到 RSA 如何使用分解,ECC 如何使用曲线。但两者都需要解决一个根本问题:两个从未见面的人如何协商出共享密钥?

在下一篇文章中:Diffie-Hellman 密钥交换——让安全通信无需预共享密钥成为可能的数学魔法。