JEP 452:密钥封装机制 API
概括
引入密钥封装机制 (KEM) 的 API,这是一种使用公钥加密来保护对称密钥的加密技术。
目标
-
使应用程序能够使用 KEM 算法,例如 RSA 密钥封装机制 (RSA-KEM)、椭圆曲线集成加密方案 (ECIES) 以及用于美国国家标准与技术研究院 (NIST) 后量子加密标准 化流程的候选 KEM 算法。
-
允许在更高级别的协议(例如传输级别安全性 (TLS))和加密方案(例如混合公钥加密(HPKE、 RFC 9180 ))中使用 KEM 。
-
允许安全提供商以 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 后来在A Proposal for an ISO Standard for Public Key Encryption的第 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
需要与密钥封装消息一起传输,以便接收者能够创建匹配的解封装器,那么它将作为字节数组包含在结果params
内部的字段中Encapsulated
。在这种情况下,安全提供者应该提供AlgorithmParameters
使用与 KEM 相同的算法名称的实现。接收者可以AlgorithmParameters
使用接收到的params
字节数组启动此类实例,并恢复AlgorithmParameterSpec
调用该方法时要使用的对象newDecapsulator
。
分别对特定对象的 或 方法进行encapsulate
多次并发调用应该是安全的。方法的每次调用都应该生成一个新的共享秘密和封装。decapsulate``Encapsulator``Decapsulator``encapsulate
以下是使用假设的 KEM 的示例"ABC"
。在密钥封装和解封装之前,接收方生成"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
传递给or方法的键的类型newDecapsulator
,以及AlgorithmParameterSpec
传递给newEncapsulator
or方法的可选对象newDecapsulator
。
例如,Kyber 系列 KEM 可以有一个名为 的算法"Kyber"
,但实现可以支持基于密钥类型的不同配置,例如 Kyber-512、Kyber-768 和 Kyber-1024。
另一个例子是 RSA-KEM 系列 KEM。算法名称可以简单地为"RSA-KEM"
,但实现可以支持基于不同 RSA 密钥大小和不同密钥派生函数 (KDF) 设置的不同配置。不同的 KDF 设置可以通过RSAKEMParameterSpec
对象来传达。
在这两种情况下,只有在调用newEncapsulator
或方法之一后才能确定配置。newDecapsulator
延迟提供者选择
为给定 KEM 算法选择的提供程序不仅取决于传递给方法的算法名称,还取决于传递给或方法的getInstance
密钥。因此,提供者的选择会被延迟,直到调用这些方法之一,就像在其他加密 API(例如和 )中一样。newEncapsulator``newDecapsulator
Cipher``KeyAgreement
每次调用newEncapsulator
ornewDecapsulator
方法都可以选择不同的提供者。您可以通过和类providerName()
的方法发现选择了哪个提供者。Encapsulator``Decapsulator
方法encapsulationSize()
一些更高级别的协议直接将密钥封装消息与其他数据连接起来,而不提供任何长度信息。例如,混合 TLS 密钥交换将两个密钥封装消息连接到单个key_exchange
字段中,而RSA-KEM将密钥封装消息与包装的密钥数据连接起来。这些协议假设一旦 KEM 配置固定,密钥封装消息的长度就是固定的并且是众所周知的。我们提供了encapsulationSize()
检索密钥封装消息大小的方法,以防应用程序需要从此类串联数据中提取密钥封装消息。
共享秘密可能无法提取
所有现有的 KEM 实现都以字节数组形式返回共享秘密。但是,Java 安全提供 程序可能由本机代码实现支持,并且共享密钥可能无法提取。因此,并不总是可以以字节数组形式返回共享秘密。因此,encapsulate
和decapsulate
方法始终返回对象中的共享秘密SecretKey
。
如果密钥是可提取的,则密钥的格式必须是"RAW"
,并且其方法必须返回完整的共享秘密或由扩展或方法的和参数getEncoded()
指定的共享秘密的片段。from``to``encapsulate``decapsulate
如果密钥不可提取,则密钥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
接口,并从其实现的engineNewEncapsulator
和engineNewDecapsulator
方法返回这些类型的对象KEMSpi
。对和对象的secretSize
、encapsulationSize
、encapsulate
和decapsulate
方法的调用被委托给和实现中的、、和方法。Encapsulator``Decapsulator``engineSecretSize``engineEncapsulationSize``engineEncapsulate``engineDecapsulate``EncapsulatorSpi``DecapsulatorSpi
engineEncapsulate
和方法的实现engineDecapsulate
必须能够使用"Generic"
算法、from
值 0 和to
共享密钥长度值来封装或解封装密钥。否则,UnsupportedOperationException
如果不支持参数组合,它可能会抛出异常,因为例如,算法名称无法映射到内部键类型,键的大小与算法不匹配,或者实现不支持共享切片自由地秘密。
未来的工作
加密选项
ISO 18033-2 为封装函数定义了一个_加密选项_,因为某些非对称密码允许将特定于方案的选项传递给加密算法。然而, RFC 9180或 NIST 的PQC KEM API Notes中均未提及此选项,因此我们不将其包含在此处。如果出现需要此选项的算法的令人信服的情况,那么未来的增强可能会引入该encapsulate
方法的另一个重载,允许包含特定于算法的参数。
AuthEncap
和AuthDecap
功能
RFC 9180定义了两个可选 的 KEM 函数AuthEncap
和AuthDecap
,它们允许发送方在封装过程中提供自己的私钥,以便接收方可以确信共享秘密是由该私钥持有者生成的。但是,这两个函数没有出现在任何其他 KEM 定义中,因此我们不将它们包含在此处。可以在未来的增强功能中添加对这些功能的支持。
备择方案
使用现有的API
我们考虑使用现有的KeyGenerator
、KeyAgreement
和Cipher
API 来表示 KEM,但它们都存在重大问题。它们要么不支持所需的功能集,要么 API 与 KEM 功能不匹配。
-
A
KeyGenerator
能够生成SecretKey
,但不能同时生成密钥封装消息。作为一种解决方法,我们可以将共享秘密和密钥封装消息编码为SecretKey
.然而,这仅在共享秘密可提取时才有效,并且如上所述,情况并不总是如此。对于可以提取的密钥,仍然需要应用程序从 的编码形式中提取秘密和密钥封装消息SecretKey
,过程复杂且容易出错。或者,我们可以将密钥封装消息存储在SecretKey
作为单独的字段中。但是,这需要一个SecretKey
具有公共方法的新子类来检索密钥封装消息。 -
A
KeyAgreement
可以通过不同的方法返回密钥封装消息作为阶段密钥和共享秘密。然而,KeyAgreement
对象应该使用调用者自己的私钥进行初始化,但对于 KEM 而言,无需在发送方创建私钥 。此外,KEM 的密钥封装消息被定义为不透明字节数组,但KeyAgreement
将阶段密钥作为Key
对象返回。新类KeyFactory
和EncodedKeySpec
子类需要在密钥封装消息和密钥之间进行转换。 -
A
Cipher
能够包装现有密钥然后将其解开。然而,在 KEM 中,共享秘密是由封装过程生成的。我们可以传入一个虚拟或null
密钥并将实际的共享密钥存储在输出中,但这具有相同的问题KeyGenerator
:它仅在共享密钥可提取时才有效,并且应用程序必须从密钥和密钥封装消息中提取密钥和密钥封装消息。包裹的结果。此外,包装密钥然后解开它应该返回相同的密钥,但将虚拟输入传递给包装方法不符合此约定。
简而言之,这些替代方案中的每一个都是针对并非旨在代表 KEM 的 API 的黑客攻击。需要额外的类和方法,并且实现会复杂且脆弱。如果没有标准的 KEM API,安全提供商可能会以不一致且笨拙的方式实现 KEM,这对开发人员来说将难以使用。
包含密钥对生成功能
所有 KEM 定义都包含密钥对生成函数。我们本可以在 KEM API 中包含这样的函数,但我们选择不这样做,因为现有的KeyPairGenerator
API是专门为此目的而设计的。在 KEM API 中包含相同的函数可能会导致提供商实现者和开发人员感到困惑。