JEP 452:密钥封装机制 API
总结
引入一个用于密钥封装机制(KEMs)的 API,这是一种使用公钥加密技术来保护对称密钥的加密方法。
目标
-
使应用程序能够使用 KEM 算法,例如 RSA 密钥封装机制(RSA-KEM)、椭圆曲线集成加密方案(ECIES),以及美国国家标准与技术研究院(NIST)后量子密码标准化过程中候选的 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 在《设计与分析针对适应性选择密文攻击的安全实用公钥加密方案》的 §7.1 中提出。Shoup 后来在《公钥加密的 ISO 标准提案》的 §3.1 中将其提议为 ISO 标准。该标准被接受为 ISO 18033-2,并于 2006 年 5 月发布。
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"
密钥对并发布公钥。
// Receiver side
KeyPairGenerator g = KeyPairGenerator.getInstance("ABC");
KeyPair kp = g.generateKeyPair();
publishKey(kp.getPublic());
// Sender side
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());
// Receiver side
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 and secR will be identical
KEM 配置
单个 KEM 算法可以有多种配置。每种配置可以接受不同类型的公钥或私钥,使用不同的方法推导共享密钥,并生成不同的密钥封装消息。每个配置应映射到一个特定的算法,该算法创建固定大小的共享密钥和固定大小的密钥封装消息。配置应通过以下三条信息无歧义地确定:
- 传递给
getInstance
方法的算法名称, - 传递给
newEncapsulator
或newDecapsulator
方法的密钥类型,以及 - 可选的传递给
newEncapsulator
或newDecapsulator
方法的AlgorithmParameterSpec
对象。
例如,Kyber 系列的 KEM 可以有一个名为 "Kyber"
的单一算法,但其实现可以基于密钥类型支持不同的配置,例如 Kyber-512、Kyber-768 和 Kyber-1024。
另一个例子是 KEM 的 RSA-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 已知答案测试。