Skip to content

Commit be03eae

Browse files
committed
ML-KEM KeyGenerator, Cipher, KEM and KeyFactory.translateKey now convert ML-KEM keys from other providers via their encodings, with TLS tests for keys supplied ahead of BC, relates to github #2466.
1 parent 6e15005 commit be03eae

13 files changed

Lines changed: 607 additions & 25 deletions

File tree

‎CONTRIBUTORS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,3 +552,4 @@ We also wish to acknowledge financial and collaborative support from [CISCO](htt
552552
- sfeng-c \<https://github.com/sfeng-c\> - initial implementation of the CRMF protocolEncrKey control (RFC 4211 sec. 6.6), with round-trip test coverage (PR #2443).
553553
- Carsten Hammer \<https://github.com/carstenartur\> - deriving the c6 and c7 constants of the generic RFC 9380 sqrt_ratio calculator from a shared z^c3, saving a modular exponentiation per instance, with tests comparing the stored constants against the direct powers (PR #2455).
554554
- pyj kor \<pyjkor&#064;gmail.com\> - report of the RFC 5990 RSA-KTS CMS recipient deriving to the keyLength declared in the message rather than the length the key-wrapping algorithm of its data encapsulation mechanism fixes, with the crafted message and the reproduction that showed what the declared length was worth.
555+
- Will Childs-Klein \<https://github.com/WillChilds-Klein\> - initial implementation of ML-KEM key conversion for BCJSSE when another provider supplies the keys, with the diagnosis and reproduction of the resulting handshake failure (PR #2466).

‎docs/releasenotes.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ Date: 2026, TBD
5959
- DSTU7624WrapEngine.unwrap had no bound check on its input and accepted a single block, whose unwrapping is empty, and wrap accepted empty input; unwrap now rejects an input shorter than two blocks or beyond the buffer, and wrap requires at least one block.
6060
- Cipher.getInstance("DSTU7624/CCM/NoPadding") and the -128/-256/-512 forms generated the generic 12 byte CCM nonce when initialised without parameters, while the OID-registered DSTU 7624 CCM ciphers generate a whole block; the mode-string form now generates a whole-block nonce too.
6161
- JceKeyAgreeRecipientInfoGenerator created its 1-pass ECMQV ephemeral key pair once per generator rather than once per message, so a generator used for more than one message sent the same ephemeral key in each and derived the same key-encryption key for every recipient each time, withdrawing the ephemeral contribution RFC 5753 sec. 3.2 relies on. The messages were well formed and decrypted correctly. A fresh key pair is now generated for each KeyAgreeRecipientInfo, still shared by all of its recipients.
62+
- The ML-KEM KeyGenerator (KEMGenerateSpec/KEMExtractSpec), Cipher (wrap/unwrap), javax.crypto.KEM and KeyFactory.translateKey services accepted only BC's own ML-KEM key objects, and the KeyGenerator failed with a ClassCastException at generateKey() rather than at init, so an ML-KEM key from another provider could not be used with BC even though its standard encoding was one BC reads. This broke BCJSSE handshakes over the ML-KEM and hybrid groups whenever another provider ahead of BC decoded the peer's key or generated the ephemeral key pair. A foreign key is now converted from its X.509 or PKCS#8 encoding, with the usual parameter-set checks, and an unusable one is rejected at init (github #2466).
6263

6364
### 2.1.3 Additional Features and Functionality
6465

‎prov/src/main/java/org/bouncycastle/jcajce/provider/asymmetric/mlkem/MLKEMCipherSpi.java‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -153,26 +153,28 @@ protected void engineInit(int opmode, Key key, AlgorithmParameterSpec paramSpec,
153153

154154
if (opmode == Cipher.WRAP_MODE)
155155
{
156-
if (key instanceof BCMLKEMPublicKey)
156+
try
157157
{
158-
wrapKey = (BCMLKEMPublicKey)key;
159-
kemGen = new MLKEMGenerator(random);
158+
wrapKey = Utils.toBCPublicKey(key);
160159
}
161-
else
160+
catch (InvalidKeyException e)
162161
{
163-
throw new InvalidKeyException("Only a " + algorithmName + " public key can be used for wrapping");
162+
throw SecurityExceptions.invalidKeyException("Only a " + algorithmName + " public key can be used for wrapping", e);
164163
}
164+
kemGen = new MLKEMGenerator(random);
165+
key = wrapKey;
165166
}
166167
else if (opmode == Cipher.UNWRAP_MODE)
167168
{
168-
if (key instanceof BCMLKEMPrivateKey)
169+
try
169170
{
170-
unwrapKey = (BCMLKEMPrivateKey)key;
171+
unwrapKey = Utils.toBCPrivateKey(key);
171172
}
172-
else
173+
catch (InvalidKeyException e)
173174
{
174-
throw new InvalidKeyException("Only a " + algorithmName + " private key can be used for unwrapping");
175+
throw SecurityExceptions.invalidKeyException("Only a " + algorithmName + " private key can be used for unwrapping", e);
175176
}
177+
key = unwrapKey;
176178
}
177179
else
178180
{

‎prov/src/main/java/org/bouncycastle/jcajce/provider/asymmetric/mlkem/MLKEMKeyFactorySpi.java‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.bouncycastle.crypto.params.MLKEMPrivateKeyParameters;
2121
import org.bouncycastle.crypto.params.MLKEMPublicKeyParameters;
2222
import org.bouncycastle.jcajce.provider.asymmetric.util.BasePQCKeyFactorySpi;
23+
import org.bouncycastle.jcajce.provider.util.SecurityExceptions;
2324
import org.bouncycastle.jcajce.spec.MLKEMPrivateKeySpec;
2425
import org.bouncycastle.jcajce.spec.MLKEMPublicKeySpec;
2526
import org.bouncycastle.util.Arrays;
@@ -102,6 +103,38 @@ public final Key engineTranslateKey(Key key)
102103
return key;
103104
}
104105

106+
// a key from another provider is re-read from its standard encoding
107+
try
108+
{
109+
if (key instanceof PublicKey && "X.509".equals(key.getFormat()))
110+
{
111+
byte[] enc = key.getEncoded();
112+
if (enc != null)
113+
{
114+
return engineGeneratePublic(new X509EncodedKeySpec(enc));
115+
}
116+
}
117+
else if (key instanceof PrivateKey && "PKCS#8".equals(key.getFormat()))
118+
{
119+
byte[] enc = key.getEncoded();
120+
if (enc != null)
121+
{
122+
try
123+
{
124+
return engineGeneratePrivate(new PKCS8EncodedKeySpec(enc));
125+
}
126+
finally
127+
{
128+
Arrays.clear(enc);
129+
}
130+
}
131+
}
132+
}
133+
catch (InvalidKeySpecException e)
134+
{
135+
throw SecurityExceptions.invalidKeyException("unsupported key type: " + e.getMessage(), e);
136+
}
137+
105138
throw new InvalidKeyException("unsupported key type");
106139
}
107140

‎prov/src/main/java/org/bouncycastle/jcajce/provider/asymmetric/mlkem/MLKEMKeyGeneratorSpi.java‎

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.bouncycastle.jcajce.provider.asymmetric.mlkem;
22

33
import java.security.InvalidAlgorithmParameterException;
4+
import java.security.InvalidKeyException;
45
import java.security.SecureRandom;
56
import java.security.spec.AlgorithmParameterSpec;
67

@@ -15,6 +16,7 @@
1516
import org.bouncycastle.crypto.params.MLKEMParameters;
1617
import org.bouncycastle.jcajce.SecretKeyWithEncapsulation;
1718
import org.bouncycastle.jcajce.provider.asymmetric.util.KdfUtil;
19+
import org.bouncycastle.jcajce.provider.util.SecurityExceptions;
1820
import org.bouncycastle.jcajce.spec.KEMExtractSpec;
1921
import org.bouncycastle.jcajce.spec.KEMGenerateSpec;
2022
import org.bouncycastle.jcajce.spec.MLKEMParameterSpec;
@@ -28,6 +30,8 @@ public class MLKEMKeyGeneratorSpi
2830
private KEMGenerateSpec genSpec;
2931
private SecureRandom random;
3032
private KEMExtractSpec extSpec;
33+
private BCMLKEMPublicKey pubKey;
34+
private BCMLKEMPrivateKey privKey;
3135

3236
public MLKEMKeyGeneratorSpi()
3337
{
@@ -50,29 +54,53 @@ protected void engineInit(AlgorithmParameterSpec algorithmParameterSpec, SecureR
5054
this.random = secureRandom;
5155
if (algorithmParameterSpec instanceof KEMGenerateSpec)
5256
{
53-
this.genSpec = (KEMGenerateSpec)algorithmParameterSpec;
54-
this.extSpec = null;
57+
KEMGenerateSpec spec = (KEMGenerateSpec)algorithmParameterSpec;
58+
BCMLKEMPublicKey key;
59+
try
60+
{
61+
key = Utils.toBCPublicKey(spec.getPublicKey());
62+
}
63+
catch (InvalidKeyException e)
64+
{
65+
throw SecurityExceptions.invalidAlgorithmParameterException(e.getMessage(), e);
66+
}
5567
if (mlkemParameters != null)
5668
{
5769
String canonicalAlgName = MLKEMParameterSpec.fromName(mlkemParameters.getName()).getName();
58-
if (!canonicalAlgName.equals(genSpec.getPublicKey().getAlgorithm()))
70+
if (!canonicalAlgName.equals(key.getAlgorithm()))
5971
{
6072
throw new InvalidAlgorithmParameterException("key generator locked to " + canonicalAlgName);
6173
}
6274
}
75+
this.genSpec = spec;
76+
this.pubKey = key;
77+
this.extSpec = null;
78+
this.privKey = null;
6379
}
6480
else if (algorithmParameterSpec instanceof KEMExtractSpec)
6581
{
66-
this.genSpec = null;
67-
this.extSpec = (KEMExtractSpec)algorithmParameterSpec;
82+
KEMExtractSpec spec = (KEMExtractSpec)algorithmParameterSpec;
83+
BCMLKEMPrivateKey key;
84+
try
85+
{
86+
key = Utils.toBCPrivateKey(spec.getPrivateKey());
87+
}
88+
catch (InvalidKeyException e)
89+
{
90+
throw SecurityExceptions.invalidAlgorithmParameterException(e.getMessage(), e);
91+
}
6892
if (mlkemParameters != null)
6993
{
7094
String canonicalAlgName = MLKEMParameterSpec.fromName(mlkemParameters.getName()).getName();
71-
if (!canonicalAlgName.equals(extSpec.getPrivateKey().getAlgorithm()))
95+
if (!canonicalAlgName.equals(key.getAlgorithm()))
7296
{
7397
throw new InvalidAlgorithmParameterException("key generator locked to " + canonicalAlgName);
7498
}
7599
}
100+
this.genSpec = null;
101+
this.pubKey = null;
102+
this.extSpec = spec;
103+
this.privKey = key;
76104
}
77105
else
78106
{
@@ -89,7 +117,6 @@ protected SecretKey engineGenerateKey()
89117
{
90118
if (genSpec != null)
91119
{
92-
BCMLKEMPublicKey pubKey = (BCMLKEMPublicKey)genSpec.getPublicKey();
93120
MLKEMGenerator kemGen = new MLKEMGenerator(random);
94121

95122
SecretWithEncapsulation secEnc = kemGen.generateEncapsulated(pubKey.getKeyParams());
@@ -117,7 +144,6 @@ protected SecretKey engineGenerateKey()
117144
}
118145
else
119146
{
120-
BCMLKEMPrivateKey privKey = (BCMLKEMPrivateKey)extSpec.getPrivateKey();
121147
MLKEMExtractor kemExt = new MLKEMExtractor(privKey.getKeyParams());
122148

123149
byte[] encapsulation = extSpec.getEncapsulation();

‎prov/src/main/java/org/bouncycastle/jcajce/provider/asymmetric/mlkem/Utils.java‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.bouncycastle.jcajce.provider.asymmetric.mlkem;
22

3+
import java.security.InvalidKeyException;
4+
import java.security.Key;
35
import java.util.HashMap;
46
import java.util.Map;
57

@@ -10,6 +12,8 @@ class Utils
1012
{
1113
private static Map parameters = new HashMap();
1214

15+
private static final MLKEMKeyFactorySpi keyFactory = new MLKEMKeyFactorySpi();
16+
1317
static
1418
{
1519
parameters.put(MLKEMParameterSpec.ml_kem_512.getName(), MLKEMParameters.ml_kem_512);
@@ -21,4 +25,52 @@ static MLKEMParameters getParameters(String name)
2125
{
2226
return (MLKEMParameters)parameters.get(name);
2327
}
28+
29+
/**
30+
* Return the BC form of an ML-KEM public key, converting a key from another provider via its X.509 encoding.
31+
*
32+
* @param key the key to convert.
33+
* @return a BCMLKEMPublicKey for the same key.
34+
* @throws InvalidKeyException if key is not an ML-KEM public key.
35+
*/
36+
static BCMLKEMPublicKey toBCPublicKey(Key key)
37+
throws InvalidKeyException
38+
{
39+
if (key instanceof BCMLKEMPublicKey)
40+
{
41+
return (BCMLKEMPublicKey)key;
42+
}
43+
44+
Key bcKey = keyFactory.engineTranslateKey(key);
45+
if (!(bcKey instanceof BCMLKEMPublicKey))
46+
{
47+
throw new InvalidKeyException("unsupported key type");
48+
}
49+
50+
return (BCMLKEMPublicKey)bcKey;
51+
}
52+
53+
/**
54+
* Return the BC form of an ML-KEM private key, converting a key from another provider via its PKCS#8 encoding.
55+
*
56+
* @param key the key to convert.
57+
* @return a BCMLKEMPrivateKey for the same key.
58+
* @throws InvalidKeyException if key is not an extractable ML-KEM private key.
59+
*/
60+
static BCMLKEMPrivateKey toBCPrivateKey(Key key)
61+
throws InvalidKeyException
62+
{
63+
if (key instanceof BCMLKEMPrivateKey)
64+
{
65+
return (BCMLKEMPrivateKey)key;
66+
}
67+
68+
Key bcKey = keyFactory.engineTranslateKey(key);
69+
if (!(bcKey instanceof BCMLKEMPrivateKey))
70+
{
71+
throw new InvalidKeyException("unsupported key type");
72+
}
73+
74+
return (BCMLKEMPrivateKey)bcKey;
75+
}
2476
}

‎prov/src/main/jdk17/org/bouncycastle/jcajce/provider/asymmetric/mlkem/MLKEMSpi.java‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,7 @@ public abstract class MLKEMSpi
2828
public EncapsulatorSpi engineNewEncapsulator(PublicKey publicKey, AlgorithmParameterSpec spec,
2929
SecureRandom secureRandom) throws InvalidAlgorithmParameterException, InvalidKeyException
3030
{
31-
if (!(publicKey instanceof BCMLKEMPublicKey bcPublicKey))
32-
{
33-
throw new InvalidKeyException("unsupported key type");
34-
}
31+
BCMLKEMPublicKey bcPublicKey = Utils.toBCPublicKey(publicKey);
3532

3633
checkKeyParameters(bcPublicKey.getKeyParams());
3734

@@ -44,10 +41,7 @@ public EncapsulatorSpi engineNewEncapsulator(PublicKey publicKey, AlgorithmParam
4441
public DecapsulatorSpi engineNewDecapsulator(PrivateKey privateKey, AlgorithmParameterSpec spec)
4542
throws InvalidAlgorithmParameterException, InvalidKeyException
4643
{
47-
if (!(privateKey instanceof BCMLKEMPrivateKey bcPrivateKey))
48-
{
49-
throw new InvalidKeyException("unsupported key type");
50-
}
44+
BCMLKEMPrivateKey bcPrivateKey = Utils.toBCPrivateKey(privateKey);
5145

5246
checkKeyParameters(bcPrivateKey.getKeyParams());
5347

0 commit comments

Comments
 (0)