JEP 324:使用 Curve25519 和 Curve448 进行密钥协商
概述
使用 Curve25519 和 Curve448 按照 RFC 7748 中的描述实现密钥协商。
目标
RFC 7748 定义了一种比现有的椭圆曲线 Diffie-Hellman(ECDH)方案更高效且更安全的密钥协商方案。此 JEP 的主要目标是为该标准提供一个 API 和实现。其他实现目标包括:
- 开发一个与平台无关的、全 Java 实现,其性能要优于现有相同安全强度下的 ECC(原生 C 代码)。
- 确保运行时间与密钥无关,假设平台在恒定时间内执行 64 位整数加法/乘法。此外,该实现不会基于密钥进行分支判断。这些特性对于防止侧信道攻击非常有价值。
非目标
RFC 7748 将仅在 SunEC 提供程序中实现。此 JEP 的目标并非在其他提供程序中实现此标准。
SunEC 中的实现将不支持任意域参数。JCA API 应该允许通过扩展来指定任意域参数。此类扩展超出了此 JEP 的范围。
成功指标
- RFC 7748 中的所有测试向量均通过。
- 在所有平台上,吞吐量(以现有密钥协商基准测试中每秒派生的密钥数来衡量)将与现有的 ECC 实现(具有类似安全强度)相比具有优势。
- 一项统计测试(需要开发)将表明密钥协商操作的时间不会因私钥的不同而变化。
动机
由于 Curve25519 和 Curve448 具有安全性和性能优势,它们的加密技术备受关注。使用这些曲线进行密钥交换已经受到许多其他加密库的支持,例如 OpenSSL、BoringSSL 和 BouncyCastle。这种密钥交换机制是 TLS 1.3 的可选组件,并通过常用扩展在早期的 TLS 版本中启用。
描述
X25519 和 X448 函数将按照 RFC 7748 中的描述进行实现,并且这些函数将用于在现有的 SunEC 提供程序中实现新的 KeyAgreement
、KeyFactory
和 KeyPairGenerator
服务。该实现将使用 RFC 7748 中描述的恒定时间 Montgomery 阶梯法,以防止侧信道攻击。实现将通过将结果与 0 进行比较来确保贡献性行为,如 RFC 中所述。
大数运算将使用一个新的模运算库来执行。这个库相比 BigInteger
将具有两个显著优势:
- 大多数操作将在常数时间内完成。如果某些操作的操作数在任何相关算法中都不是秘密,则这些操作的执行时间可能会随着操作数的大小而变化。例如,指数运算的时间会随着指数的大小而变化,但在 RFC 7748 中,指数值并不是秘密。本库的 API 文档将描述每个操作的时间行为。
- 通过避免进位操作并利用 EC 操作中使用的特定有限域的属性,性能将得到提升。
这个新库将会位于一个内部的 JDK 包中,并且只会被新的加密算法使用。我们预计在不久的将来将其用于 EdDSA(使用 Curve25519 和 Curve448 的签名)和 Poly1305(消息认证)的实现中。该库采用了一种受 EdDSA 论文 启发的降基表示法。
由于该运算避免了进位操作,因此如果存在错误或者使用不当,就有可能溢出并产生错误的结果。例如,加法运算不会进位,所以在每次乘法运算之前,只能执行有限次数(通常为一次)的加法运算。该库将包含防止误用的防护措施(例如,如果发生过多的加法操作则抛出异常),并且必须经过仔细测试以确保不会溢出。
API
针对 RFC 7748 的 JCA API 将使用名称“XDH”来标识与此机制相关的所有服务(如 KeyAgreement
、KeyPairGenerator
、KeyFactory
等)。算法名称“X25519”和“X448”也将被定义为分别表示使用 Curve25519 和 Curve448 的 XDH。这提供了一种便捷的简写形式(例如,KeyPairGenerator.getInstance("X448")
),同时也使得更容易找到支持所需曲线的提供者。像“X25519”和“X448”这样的名称不应简单地作为“XDH”的别名——通过这些名称返回的服务应使用正确的曲线进行初始化,并且可能会拒绝任何使用不同曲线的密钥。
AlgorithmParameterSpec
:将使用一个名为 NamedParameterSpec
的新类来指定所使用的曲线(X25519 或 X448)。该类使用单一的标准名称来指定一组参数,并且旨在供其他使用命名参数的算法重用。例如,它可以用于(有限域)Diffie-Hellman 中的命名组。NamedParameterSpec
将被插入到类层次结构中 ECGenParameterSpec
的上方,因此 ECGenParameterSpec
也将成为一个 NamedParameterSpec
。
KeySpec
:新的类 XECPublicKeySpec
可用于指定公钥。该类有一个 BigInteger
成员,用于保存点的 u
坐标。新的类 XECPrivateKeySpec
可用于指定私钥。该类有一个字节数组成员,用于保存 RFC 7748 中描述的 X25519 和 X448 函数的(编码后的)k
输入。这两个 KeySpec
类都有一个 AlgorithmParameterSpec
成员,用于指定曲线及其他算法参数。
现有的 X509EncodedKeySpec
类也可用于公钥。现有的 PKCS8EncodedKeySpec
类也可用于私钥。
Key
接口:将新增 XECPublicKey
和 XECPrivateKey
两个接口,用于提供对密钥对象中包含的信息的访问。这些接口中的密钥数据表示形式将与 XECPublicKeySpec
和 XECPrivateKeySpec
中的表示形式相同。这两个接口都将继承自新的接口 XECKey
,该接口将提供对定义曲线及其他算法参数的 AlgorithmParameterSpec
的访问。
示例 API 用法:
KeyPairGenerator kpg = KeyPairGenerator.getInstance("XDH");
NamedParameterSpec paramSpec = new NamedParameterSpec("X25519");
kpg.initialize(paramSpec); // equivalent to kpg.initialize(255)
// alternatively: kpg = KeyPairGenerator.getInstance("X25519")
KeyPair kp = kpg.generateKeyPair();
KeyFactory kf = KeyFactory.getInstance("XDH");
BigInteger u = ...
XECPublicKeySpec pubSpec = new XECPublicKeySpec(paramSpec, u);
PublicKey pubKey = kf.generatePublic(pubSpec);
KeyAgreement ka = KeyAgreement.getInstance("XDH");
ka.init(kp.getPrivate());
ka.doPhase(pubKey, true);
byte[] secret = ka.generateSecret();
替代方案
考虑了与实现相关的一些替代方案:
- 可以使用
BigInteger
来执行字段算术运算。初步原型设计表明,BigInteger
比自定义模运算库大约慢 10 倍。例如,在初始测试中,使用BigInteger
的 X25519 运算耗时约 2.5 毫秒,而同一平台上运行的自定义库仅需约 0.25 毫秒。此外,BigInteger
不具备恒定时间乘法,这将削弱这些函数的一些侧信道防护能力。 - 原生实现(如现有的 ECC 代码)可能会提供更好的性能。初步原型设计表明,对于典型用途而言,Java 实现的速度已经足够快。例如,在同一平台上,X25519(用 Java 实现)耗时约 0.25 毫秒,而 secp256r1 群组操作(用 C 实现)耗时约 1 毫秒。因此,初步结果表明 Java 实现应该已经足够快。
- 可以利用现有的 ECC 代码来实现此密钥协商,但这种方法无法完全提供 RFC 7748 所具有的安全性和性能优势。
测试
测试将包括来自 RFC 7748 的测试向量。这些向量包含 X25519 和 X448 函数的一百万次迭代,完成这些迭代大约需要 15 分钟。如果我们希望定期运行这些测试,可以通过将它们分成每批 10 到 100 万次迭代来并行处理。
测试算术库将稍微更具挑战性,因为导致溢出的条件可能不太可能自然发生。用于 Curve25519 和 Curve448 的表示法应该与严格的数学证明一起开发,以证明它们不会溢出。这些证明将包含与每个底层操作(加法、乘法、进位、约简)相关的边界条件,可以将其纳入回归测试中。
风险与假设
一个重要的风险是模运算实现的复杂性和微妙性。通过为算术开发严格的正确性证明和进行彻底的测试,可以减轻这种风险。