Skip to content

JEP 324: Key Agreement with Curve25519 and Curve448 | Curve25519 和 Curve448 的密钥协议

摘要

按照 RFC 7748 中描述的方式,使用 Curve25519 和 Curve448 实现密钥协商。

目标

RFC 7748 定义了一种密钥协商方案,它比现有的椭圆曲线 Diffie-Hellman(ECDH)方案更高效、更安全。本 JEP 的主要目标是为该标准提供一个 API 和实现。额外的实现目标包括:

  1. 开发一个与平台无关、全 Java 实现,在相同的安全强度下,性能优于现有的 ECC(原生 C)代码。
  2. 确保时间不受秘密值的影响,假设平台以恒定时间执行 64 位整数加法和乘法。此外,实现不会在秘密值上进行分支。这些属性对于防止侧信道攻击很有价值。

非目标

RFC 7748 将仅在 SunEC 提供程序中实现。本 JEP 的目标不是在其他提供程序中实现此标准。

SunEC 中的实现将不支持任意的域参数。JCA API 应该允许通过扩展来指定任意的域参数。此类扩展超出了本 JEP 的范围。

成功指标

  1. RFC 7748 中的所有测试向量都能通过。
  2. 在所有平台上,通过(现有密钥协商基准测试中每秒生成的密钥数量来衡量)的吞吐量将与现有 ECC 实现(具有相似安全强度)相当或更优。
  3. 一项需要开发的统计测试将表明,密钥协商操作的时间与私钥无关。

动机

由于 Curve25519 和 Curve448 在安全性和性能方面的特性,使用它们进行的加密操作备受需求。使用这些曲线的密钥交换已经在许多其他加密库中得到了支持,如 OpenSSL、BoringSSL 和 BouncyCastle。这种密钥交换机制是 TLS 1.3 的一个可选组件,并通过常用的扩展在早期的 TLS 版本中启用。

描述

X25519 和 X448 函数将按照 RFC 7748 中描述的方式进行实现,这些函数将用于在现有的 SunEC 提供程序中实现新的 KeyAgreementKeyFactoryKeyPairGenerator 服务。为了防止侧信道攻击,实现将使用 RFC 7748 中描述的恒时 Montgomery ladder 方法。实现将通过将结果与 0 进行比较来确保贡献性行为,如 RFC 所述。

大数运算将使用一个新的模运算库来完成。与 BigInteger 相比,这个库将有两个显著的优势:

  1. 大多数操作将在恒时内完成。如果某些操作数的大小在任何相关算法中都不是秘密,则某些操作可能会随其操作数的大小而变化。例如,幂运算的时间将随指数的大小而变化,但在 RFC 7748 中,指数值不是秘密。该库的 API 文档将描述每个操作的时序行为。
  2. 性能将通过避免进位操作并利用 EC 操作中使用的特定有限域的属性来提高。

这个新库将位于 JDK 的内部包中,并且仅由新的加密算法使用。我们预计在不久的将来,它将在 EdDSA(使用 Curve25519 和 Curve448 的签名)和 Poly1305(消息认证)实现中使用。该库使用了一种受 EdDSA 论文 中使用的减少基表示法启发的方法。

由于该算术避免了进位操作,如果它存在错误或被错误使用,可能会导致溢出并产生不正确的结果。例如,加法操作不产生进位,因此在进行每次乘法操作之前,只能执行有限数量(通常是一个)的加法操作。该库将包含防止误用的防御措施(例如,如果加法操作过多,则抛出异常),并且必须经过仔细测试以确保其不会溢出。

API

RFC 7748 的 JCA API 将使用“XDH”这个名字来标识与该机制相关的所有服务(如 KeyAgreementKeyPairGeneratorKeyFactory 等)。同时,还将定义算法名称“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 接口:将添加新的接口 XECPublicKeyXECPrivateKey 以提供对密钥对象中包含信息的访问。这些接口中密钥数据的表示将与 XECPublicKeySpecXECPrivateKeySpec 中的表示相同。这两个接口都将继承自新接口 XECKey,该接口将提供对定义曲线和其他算法参数的 AlgorithmParameterSpec 的访问。

API 使用示例:

java
KeyPairGenerator kpg = KeyPairGenerator.getInstance("XDH");
NamedParameterSpec paramSpec = new NamedParameterSpec("X25519");
kpg.initialize(paramSpec); // 相当于 kpg.initialize(255)
// 或者: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();

备选方案

在实现过程中,我们考虑了几个备选方案:

  1. 可以使用 BigInteger 来执行字段算术。初步原型测试表明,BigInteger 比自定义的模算术库大约慢 10 倍。例如,在初始测试中,使用 BigInteger 的 X25519 算法大约需要 2.5 毫秒,而在同一平台上使用自定义库则只需要 0.25 毫秒。此外,BigInteger 没有恒定时间乘法,这可能会消除这些函数的一些侧信道抵抗力。
  2. 本机实现(如现有的 ECC 代码)可能会提供更好的性能。初步原型测试表明,Java 实现对于典型用途来说已经足够快。例如,X25519(在 Java 中)大约需要 0.25 毫秒,而 secp256r1 组操作(在 C 中)在同一平台上大约需要 1 毫秒。因此,初步结果表明 Java 实现应该足够快。
  3. 虽然可以使用现有的 ECC 代码来实现此密钥协商,但这种方法无法提供 RFC 7748 的所有安全和性能优势。

测试

测试将包括 RFC 7748 中的测试向量。这些向量包括 X25519 和 X448 函数的一百万次迭代,大约需要 15 分钟才能完成。如果我们希望定期运行这些测试,我们可以将它们分成每批 1 万到 10 万次的批次进行并行处理。

对算术库的测试将稍微更具挑战性,因为导致溢出的条件可能不太可能自然发生。用于 Curve25519 和 Curve448 的表示应该与严格的数学证明一起开发,这些证明表明它们不会溢出。这些证明将包含与每个底层操作(加、乘、进位、约减)相关的边界条件,这些条件可以合并到回归测试中。

风险与假设

一个显著的风险是模算术实现的复杂性和微妙性。这一风险可以通过为算术开发严格的正确性证明以及进行彻底的测试来降低。