JEP 452: Key Encapsulation Mechanism API | 密钥封装机制 API
摘要
引入一个用于密钥封装机制(KEM)的 API,这是一种使用公钥密码学保护对称密钥的加密技术。
目标
使应用程序能够使用如 RSA 密钥封装机制(RSA-KEM)、椭圆曲线集成加密方案(ECIES)以及美国国家标准与技术研究院(NIST)后量子密码学标准化过程中的候选 KEM 算法等 KEM 算法。
使 KEM 能够在更高层次的协议(如传输层安全性(TLS))和加密方案(如混合公钥加密(HPKE,RFC 9180))中使用。
允许安全提供者以 Java 代码或本地代码实现 KEM 算法。
包括在 RFC 9180 的§4.1 中定义的 Diffie-Hellman KEM(DHKEM)的实现。
非目标
不包括在 KEM API 中生成密钥对。现有的
KeyPairGenerator
API 已足够。不支持 ISO 18033-2 中定义的封装函数的加密选项。
不支持 RFC 9180 中定义的 经过认证的封装和解封装 功能。
动机
密钥封装 是一种现代加密技术,它使用非对称或公钥密码学来保护对称密钥。传统的方法是用公钥加密一个随机生成的对称密钥,但这需要填充,并且很难证明其安全性。密钥封装机制(KEM)则利用公钥的属性来推导出相关的对称密钥,这不需要填充。
KEM 的概念由 Crammer 和 Shoup 在 Design and Analysis of Practical Public-Key Encryption Schemes Secure against Adaptive Chosen Ciphertext Attack 的§7.1 中引入。Shoup 后来在 A Proposal for an ISO Standard for Public Key Encryption 的§3.1 中提议将其作为 ISO 标准。该标准于 2006 年 5 月被接受为 ISO 18033-2 并发布。
KEM 是 混合公钥加密(HPKE) 的组成部分。NIST 后量子密码学(PQC)标准化过程 明确要求评估 KEM 和数字签名算法作为下一代标准公钥密码学算法的候选者。TLS 1.3 中的 Diffie-Hellman 密钥交换步骤 也可以被建模为一个 KEM。
KEM 将成为防御量子攻击的重要工具。Java 平台中现有的加密 API 都无法以自然的方式表示 KEM(见 下文)。第三方安全提供者的实现者已经 表示需要标准的 KEM API。现在是时候在 Java 平台中添加一个了。
描述
密钥封装机制(KEM)由三个函数组成:
一个 密钥对生成函数,它返回一个包含公钥和私钥的密钥对。
一个 密钥封装函数,由发送方调用,它接收接收方的公钥和一个加密选项;它返回一个密钥 K 和一个 密钥封装消息(在 ISO 18033-2 中称为 密文)。发送方将密钥封装消息发送给接收方。
一个 密钥解封装函数,由接收方调用,它接收接收方的私钥和接收到的密钥封装消息;它返回密钥 K。
密钥对生成函数由现有的 KeyPairGenerator
API 涵盖。我们为封装和解封装函数定义了一个新类 KEM
:
package javax.crypto;
public class DecapsulateException extends GeneralSecurityException;
public final class KEM {
public static KEM getInstance(String alg)
throws NoSuchAlgorithmException;
public static KEM getInstance(String alg, Provider p)
throws NoSuchAlgorithmException;
public static KEM getInstance(String alg, String p)
throws NoSuchAlgorithmException, NoSuchProviderException;
public static final class Encapsulated {
public Encapsulated(SecretKey key, byte[] encapsulation, byte[] params);
public SecretKey key();
public byte[] encapsulation();
public byte[] params();
}
public static final class Encapsulator {
String providerName();
int secretSize(); // Size of the shared secret
int encapsulationSize(); // Size of the key encapsulation message
Encapsulated encapsulate();
Encapsulated encapsulate(int from, int to, String algorithm);
}
public Encapsulator newEncapsulator(PublicKey pk)
throws InvalidKeyException;
public Encapsulator newEncapsulator(PublicKey pk, SecureRandom sr)
throws InvalidKeyException;
public Encapsulator newEncapsulator(PublicKey pk, AlgorithmParameterSpec spec,
SecureRandom sr)
throws InvalidAlgorithmParameterException, InvalidKeyException;
public static final class Decapsulator {
String providerName();
int secretSize(); // Size of the shared secret
int encapsulationSize(); // Size of the key encapsulation message
SecretKey decapsulate(byte[] encapsulation) throws DecapsulateException;
SecretKey decapsulate(byte[] encapsulation, int from, int to,
String algorithm)
throws DecapsulateException;
}
public Decapsulator newDecapsulator(PrivateKey sk)
throws InvalidKeyException;
public Decapsulator newDecapsulator(PrivateKey sk, AlgorithmParameterSpec spec)
throws InvalidAlgorithmParameterException, InvalidKeyException;
}
getInstance
方法会创建一个新的 KEM
对象,该对象实现了指定的算法。
发送方调用其中一个 newEncapsulator
方法。这些方法接收接收方的公钥,并返回一个 Encapsulator
对象。然后,发送方可以调用该对象的两个 encapsulate
方法之一,以获取一个 Encapsulated
对象,该对象包含一个 SecretKey
和一个密钥封装消息。encapsulate()
方法返回一个包含完整共享密钥的密钥,其算法名称为 "Generic"
。此密钥通常传递给密钥派生函数。encapsulate(from, to, algorithm)
方法返回一个密钥,其密钥材料是共享密钥的子数组,具有指定的算法名称。
接收方调用其中一个 newDecapsulator
方法。这些方法接收接收方的私钥,并返回一个 Decapsulator
对象。然后,接收方可以调用该对象的两个 decapsulate
方法之一,它们接收接收到的密钥封装消息并返回共享密钥。decapsulate(encapsulation)
方法返回一个具有 "Generic"
算法的完整共享密钥,而 decapsulate(encapsulation, from, to, algorithm)
方法返回一个具有用户指定密钥材料和算法的密钥。
KEM 算法可以定义一个 AlgorithmParameterSpec
子类,以向完整的 newEncapsulator
方法提供额外信息。如果相同的密钥可以以不同方式派生共享密钥,则这尤其有用。AlgorithmParameterSpec
子类的实例应该是不可变的。如果 AlgorithmParameterSpec
对象内的任何信息需要与密钥封装消息一起传输,以便接收方能够创建匹配的解封装器,则这些信息将作为字节数组包含在 Encapsulated
结果的 params
字段中。在这种情况下,安全提供程序应使用与 KEM 相同的算法名称提供 AlgorithmParameters
实现。接收方可以使用接收到的 params
字节数组初始化此类 AlgorithmParameters
实例,并恢复一个 AlgorithmParameterSpec
对象,以便在调用 newDecapsulator
方法时使用。
对特定 Encapsulator
或 Decapsulator
对象的 encapsulate
或 decapsulate
方法的多次并发调用应该是安全的。每次调用 encapsulate
方法都应生成一个新的共享密钥和封装。
下面是一个使用假设的 "ABC"
KEM 的示例。在密钥封装和解封装之前,接收方生成一个 "ABC"
密钥对并发布公钥。
// 接收方
KeyPairGenerator g = KeyPairGenerator.getInstance("ABC");
KeyPair kp = g.generateKeyPair();
publishKey(kp.getPublic());
// 发送方
KEM kemS = KEM.getInstance("ABC-KEM");
PublicKey pkR = retrieveKey();
ABCKEMParameterSpec specS = new ABCKEMParameterSpec(...);
KEM.Encapsulator e = kemS.newEncapsulator(pkR, specS, null);
KEM.Encapsulated enc = e.encapsulate();
SecretKey secS = enc.key();
sendBytes(enc.encapsulation());
sendBytes(enc.params());
// 接收方
byte[] em = receiveBytes();
byte[] params = receiveBytes();
KEM kemR = KEM.getInstance("ABC-KEM");
AlgorithmParameters algParams = AlgorithmParameters.getInstance("ABC-KEM");
algParams.init(params);
ABCKEMParameterSpec specR = algParams.getParameterSpec(ABCKEMParameterSpec.class);
KEM.Decapsulator d = kemR.newDecapsulator(kp.getPrivate(), specR);
SecretKey secR = d.decapsulate(em);
// secS 和 secR 将是相同的
KEM 配置
单个 KEM 算法可以有多种配置。每种配置可以接受不同类型的公钥或私钥,使用不同的方法来派生共享密钥,并发出不同的密钥封装消息。每种配置应映射到创建固定大小共享密钥和固定大小密钥封装消息的特定算法。配置应由以下三项信息明确确定:
- 传递给
getInstance
方法的算法名称, - 传递给
newEncapsulator
或newDecapsulator
方法的密钥类型, - 以及可选的传递给
newEncapsulator
或newDecapsulator
方法的AlgorithmParameterSpec
对象。
例如,Kyber 系列的 KEM 可以有一个名为 "Kyber"
的单一算法,但实现可以根据密钥类型支持不同的配置,如 Kyber-512、Kyber-768 和 Kyber-1024。
另一个例子是 RSA-KEM 系列的 KEM。算法名称可以简单地是 "RSA-KEM"
,但实现可以根据不同的 RSA 密钥大小和不同的密钥派生函数(KDF)设置来支持不同的配置。不同的 KDF 设置可以通过 RSAKEMParameterSpec
对象来传达。
在这两种情况下,只有在调用 newEncapsulator
或 newDecapsulator
方法之一后才能确定配置。
延迟提供者选择
为给定 KEM 算法选择的提供者不仅取决于传递给 getInstance
方法的算法名称,还取决于传递给 newEncapsulator
或 newDecapsulator
方法的密钥。因此,提供者的选择会延迟到调用这些方法之一时进行,正如在其他加密 API(如 Cipher
和 KeyAgreement
)中一样。
每次调用 newEncapsulator
或 newDecapsulator
方法都可以选择不同的提供者。您可以通过 Encapsulator
和 Decapsulator
类的 providerName()
方法来发现选择了哪个提供者。
encapsulationSize()
方法
一些高级协议直接将密钥封装消息与其他数据连接起来,而不提供任何长度信息。例如,Hybrid TLS 密钥交换 将两个密钥封装消息连接成一个 key_exchange
字段,而 RSA-KEM 将密钥封装消息与包装的密钥数据连接起来。这些协议假定一旦 KEM 配置固定,密钥封装消息的长度就是固定且已知的。我们提供 encapsulationSize()
方法来检索密钥封装消息的大小,以防应用程序需要从此类连接数据中提取密钥封装消息。
共享密钥可能无法提取
所有现有的 KEM 实现都在字节数组中返回共享密钥。但是,Java 安全提供者可能由本机代码实现支持,并且共享密钥可能无法提取。因此,并非总是可以将共享密钥以字节数组的形式返回。出于这个原因,encapsulate
和 decapsulate
方法总是在 SecretKey
对象中返回共享密钥。
如果密钥可提取,则密钥的格式必须是 "RAW"
,并且其 getEncoded()
方法必须返回完整的共享密钥或由扩展的 encapsulate
或 decapsulate
方法的 from
和 to
参数指定的共享密钥切片。
如果密钥不可提取,则密钥的 getFormat()
和 getEncoded()
方法必须返回 null
,即使内部密钥材料是完整的共享密钥或共享密钥的切片。
KEM 服务提供者接口(SPI)
KEM 实现必须实现 KEMSpi
接口:
package javax.crypto;
public interface KEMSpi {
interface EncapsulatorSpi {
int engineSecretSize();
int engineEncapsulationSize();
KEM.Encapsulated engineEncapsulate(int from, int to, String algorithm);
}
interface DecapsulatorSpi {
int engineSecretSize();
int engineEncapsulationSize();
SecretKey engineDecapsulate(byte[] encapsulation, int from, int to,
String algorithm)
throws DecapsulateException;
}
EncapsulatorSpi engineNewEncapsulator(PublicKey pk, AlgorithmParameterSpec spec,
SecureRandom sr)
throws InvalidAlgorithmParameterException, InvalidKeyException;
DecapsulatorSpi engineNewDecapsulator(PrivateKey sk, AlgorithmParameterSpec spec)
throws InvalidAlgorithmParameterException, InvalidKeyException;
}
实现必须实现 EncapsulatorSpi
和 DecapsulatorSpi
接口,并从其 KEMSpi
实现的 engineNewEncapsulator
和 engineNewDecapsulator
方法中返回这些类型的对象。对 Encapsulator
和 Decapsulator
对象的 secretSize
、encapsulationSize
、encapsulate
和 decapsulate
方法的调用被委托给 EncapsulatorSpi
和 DecapsulatorSpi
实现中的 engineSecretSize
、engineEncapsulationSize
、engineEncapsulate
和 engineDecapsulate
方法。
engineEncapsulate
和 engineDecapsulate
方法的实现必须能够使用 "Generic"
算法、from
值为 0,以及 to
值为共享密钥长度的值来封装或解封装密钥。否则,如果参数组合不受支持(例如,算法名称无法映射到内部密钥类型、密钥大小与算法不匹配,或实现不支持自由切片共享密钥),则可以抛出 UnsupportedOperationException
。
未来工作
加密选项
ISO 18033-2 为封装函数定义了一个 加密选项,因为一些非对称密码允许将特定于方案的选项传递给加密算法。然而,这个选项在 RFC 9180 或 NIST 的 PQC KEM API 注释 中均未提及,因此我们在此不包含它。如果出现需要此选项的算法的强有力理由,则未来的增强可以引入 encapsulate
方法的另一个重载,允许包含特定于算法的参数。
AuthEncap
和 AuthDecap
函数
RFC 9180 定义了两种可选的 KEM 函数,AuthEncap
和 AuthDecap
,允许发送方在封装过程中提供自己的私钥,以便接收方可以确保共享密钥是由该私钥的持有者生成的。然而,这两个函数并未出现在其他任何 KEM 定义中,因此我们在此不包含它们。可以在未来的增强中添加对这些函数的支持。
备选方案
使用现有的 API
我们考虑过使用现有的 KeyGenerator
、KeyAgreement
和 Cipher
API 来表示 KEM,但它们各自都存在显著问题。要么它们不支持所需的功能集,要么 API 与 KEM 功能不匹配。
KeyGenerator
能够生成SecretKey
,但不能同时生成密钥封装消息。作为一种变通方法,我们可以将共享密钥和密钥封装消息都编码为SecretKey
的编码形式。然而,这仅当共享密钥可提取时有效,而如上所述,这并非总是如此。对于可提取的密钥,仍然需要应用程序从SecretKey
的编码形式中提取密钥和密钥封装消息,这既复杂又容易出错。或者,我们可以将密钥封装消息作为单独字段存储在SecretKey
中。但是,这将需要一个新的SecretKey
子类,该类具有一个公开的方法来检索密钥封装消息。KeyAgreement
可以通过不同的方法返回作为阶段密钥和共享密钥的密钥封装消息。但是,KeyAgreement
对象旨在使用调用者自己的私钥进行初始化,而对于 KEM 而言,发送方无需创建私钥。此外,KEM 的密钥封装消息被定义为不透明的字节数组,但KeyAgreement
将阶段密钥作为Key
对象返回。需要新的KeyFactory
和EncodedKeySpec
子类来在密钥封装消息和密钥之间进行转换。Cipher
能够包装现有密钥然后解包它。但是,在 KEM 中,共享密钥是通过封装过程生成的。我们可以传入一个虚拟或null
密钥,并在输出中存储实际的共享密钥,但这与KeyGenerator
存在同样的问题:它仅当共享密钥可提取时有效,且应用程序必须从包装结果中提取密钥和密钥封装消息。此外,包装密钥然后解包它应该返回相同的密钥,但向包装方法传递虚拟输入不符合此约定。
简而言之,这些备选方案中的每一个都是为了解决一个并非为表示 KEM 而设计的 API 的变通方法。将需要额外的类和方法,并且实现将复杂且易碎。没有标准的 KEM API,安全提供者可能会以不一致和笨拙的方式实现 KEM,这将使开发人员难以使用。
包含密钥对生成函数
所有 KEM 定义都包含密钥对生成函数。我们本可以将此类函数包含在 KEM API 中,但我们选择不这样做,因为现有的 KeyPairGenerator
API 正是为此目的而设计的。在 KEM API 中包含一个相同功能的函数可能会导致提供者实现者和开发人员感到困惑。
测试
我们将添加针对输入、输出和异常的符合性测试,以及来自 RFC 9180 的 DHKEM 已知答案测试。