Skip to content

Commit c9901b4

Browse files
committed
added KeyPairGenerator for composite KEM, example for composite KEM
1 parent d15fac5 commit c9901b4

6 files changed

Lines changed: 407 additions & 0 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.bouncycastle.jcajce.examples;
2+
3+
import java.security.KeyPair;
4+
import java.security.KeyPairGenerator;
5+
import java.security.Security;
6+
7+
import javax.crypto.KeyGenerator;
8+
9+
import org.bouncycastle.jcajce.SecretKeyWithEncapsulation;
10+
import org.bouncycastle.jcajce.spec.KEMExtractSpec;
11+
import org.bouncycastle.jcajce.spec.KEMGenerateSpec;
12+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
13+
import org.bouncycastle.util.Arrays;
14+
import org.bouncycastle.util.encoders.Hex;
15+
16+
/**
17+
* Example of using Composite ML-KEM (draft-ietf-lamps-pq-composite-kem) through the JCE: generate a
18+
* composite key pair, encapsulate a shared secret to the public key, and recover it with the
19+
* private key.
20+
* <p>
21+
* A composite KEM pairs ML-KEM with a traditional KEM (here ML-KEM-768 with ECDH over P-256) so the
22+
* derived secret is secure as long as <i>either</i> component remains unbroken. The provider exposes
23+
* each composite parameter set under its algorithm name (e.g. {@code MLKEM768-ECDH-P256-SHA3-256})
24+
* and its OID; this example uses the name.
25+
*/
26+
public class CompositeKEMExample
27+
{
28+
private static final String COMPOSITE_ALG = "MLKEM768-ECDH-P256-SHA3-256";
29+
30+
public static void main(String[] args)
31+
throws Exception
32+
{
33+
Security.addProvider(new BouncyCastleProvider());
34+
35+
// 1. Generate a composite key pair. The public/private keys are composites whose components
36+
// are, in order, the ML-KEM-768 key and the ECDH P-256 key.
37+
KeyPairGenerator kpg = KeyPairGenerator.getInstance(COMPOSITE_ALG, "BC");
38+
KeyPair kp = kpg.generateKeyPair();
39+
40+
// 2. Sender: encapsulate. KeyGenerator with a KEMGenerateSpec produces the shared secret as
41+
// a SecretKey together with the encapsulation (ciphertext) to transmit to the recipient.
42+
// withNoKdf() uses the composite KEM shared secret (SHA3-256 output) directly as the key.
43+
KeyGenerator sender = KeyGenerator.getInstance(COMPOSITE_ALG, "BC");
44+
sender.init(new KEMGenerateSpec.Builder(kp.getPublic(), "AES", 256).withNoKdf().build());
45+
SecretKeyWithEncapsulation encapsulated = (SecretKeyWithEncapsulation)sender.generateKey();
46+
47+
byte[] sharedSecret = encapsulated.getEncoded();
48+
byte[] ciphertext = encapsulated.getEncapsulation();
49+
50+
// 3. Recipient: decapsulate using the private key and the received ciphertext.
51+
KeyGenerator recipient = KeyGenerator.getInstance(COMPOSITE_ALG, "BC");
52+
recipient.init(new KEMExtractSpec.Builder(kp.getPrivate(), ciphertext, "AES", 256).withNoKdf().build());
53+
SecretKeyWithEncapsulation decapsulated = (SecretKeyWithEncapsulation)recipient.generateKey();
54+
55+
byte[] recoveredSecret = decapsulated.getEncoded();
56+
57+
System.out.println("algorithm : " + COMPOSITE_ALG);
58+
System.out.println("ciphertext length: " + ciphertext.length + " bytes");
59+
System.out.println("sender secret : " + Hex.toHexString(sharedSecret));
60+
System.out.println("recipient secret : " + Hex.toHexString(recoveredSecret));
61+
62+
if (!Arrays.constantTimeAreEqual(sharedSecret, recoveredSecret))
63+
{
64+
throw new IllegalStateException("shared secrets do not match");
65+
}
66+
67+
System.out.println("shared secrets match - the AES-256 key can now be used to protect data.");
68+
}
69+
}

‎prov/src/main/java/org/bouncycastle/jcajce/CompositeUtil.java‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@ class CompositeUtil
3131
algorithmOids.put("MLDSA87-RSA4096-PSS-SHA512", IANAObjectIdentifiers.id_MLDSA87_RSA4096_PSS_SHA512);
3232
algorithmOids.put("MLDSA87-ECDSA-P521-SHA512", IANAObjectIdentifiers.id_MLDSA87_ECDSA_P521_SHA512);
3333
algorithmOids.put("MLDSA87-RSA3072-PSS-SHA512", IANAObjectIdentifiers.id_MLDSA87_RSA3072_PSS_SHA512);
34+
35+
// Composite ML-KEM (draft-ietf-lamps-pq-composite-kem) names, so builder(String) resolves
36+
// them the same way it does the composite signature names above.
37+
algorithmOids.put("MLKEM768-RSA2048-SHA3-256", IANAObjectIdentifiers.id_MLKEM768_RSA2048_SHA3_256);
38+
algorithmOids.put("MLKEM768-RSA3072-SHA3-256", IANAObjectIdentifiers.id_MLKEM768_RSA3072_SHA3_256);
39+
algorithmOids.put("MLKEM768-RSA4096-SHA3-256", IANAObjectIdentifiers.id_MLKEM768_RSA4096_SHA3_256);
40+
algorithmOids.put("MLKEM768-X25519-SHA3-256", IANAObjectIdentifiers.id_MLKEM768_X25519_SHA3_256);
41+
algorithmOids.put("MLKEM768-ECDH-P256-SHA3-256", IANAObjectIdentifiers.id_MLKEM768_ECDH_P256_SHA3_256);
42+
algorithmOids.put("MLKEM768-ECDH-P384-SHA3-256", IANAObjectIdentifiers.id_MLKEM768_ECDH_P384_SHA3_256);
43+
algorithmOids.put("MLKEM768-ECDH-BP256-SHA3-256", IANAObjectIdentifiers.id_MLKEM768_ECDH_BP256_SHA3_256);
44+
algorithmOids.put("MLKEM1024-RSA3072-SHA3-256", IANAObjectIdentifiers.id_MLKEM1024_RSA3072_SHA3_256);
45+
algorithmOids.put("MLKEM1024-ECDH-P384-SHA3-256", IANAObjectIdentifiers.id_MLKEM1024_ECDH_P384_SHA3_256);
46+
algorithmOids.put("MLKEM1024-ECDH-BP384-SHA3-256", IANAObjectIdentifiers.id_MLKEM1024_ECDH_BP384_SHA3_256);
47+
algorithmOids.put("MLKEM1024-X448-SHA3-256", IANAObjectIdentifiers.id_MLKEM1024_X448_SHA3_256);
48+
algorithmOids.put("MLKEM1024-ECDH-P521-SHA3-256", IANAObjectIdentifiers.id_MLKEM1024_ECDH_P521_SHA3_256);
3449
}
3550

3651
static ASN1ObjectIdentifier getOid(String name)

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ public void configure(ConfigurableProvider provider)
4343
provider.addAlgorithm("Alg.Alias.KeyFactory", oid, "COMPOSITE");
4444
provider.addAlgorithm("Alg.Alias.KeyFactory." + algorithmName, "COMPOSITE");
4545

46+
provider.addAlgorithm("KeyPairGenerator." + algorithmName, PREFIX + "KeyPairGeneratorSpi$" + className);
47+
provider.addAlgorithm("Alg.Alias.KeyPairGenerator", oid, algorithmName);
48+
4649
provider.addAlgorithm("KeyGenerator." + algorithmName, PREFIX + "CompositeKeyGeneratorSpi$" + className);
4750
provider.addAlgorithm("Alg.Alias.KeyGenerator", oid, algorithmName);
4851

‎prov/src/main/java/org/bouncycastle/jcajce/provider/asymmetric/compositekem/CompositeIndex.java‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package org.bouncycastle.jcajce.provider.asymmetric.compositekem;
22

3+
import java.security.spec.AlgorithmParameterSpec;
4+
import java.security.spec.RSAKeyGenParameterSpec;
35
import java.util.HashMap;
46
import java.util.Map;
57
import java.util.Set;
68

79
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
810
import org.bouncycastle.internal.asn1.iana.IANAObjectIdentifiers;
11+
import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec;
912
import org.bouncycastle.util.Strings;
1013
import org.bouncycastle.util.encoders.Hex;
1114

@@ -21,6 +24,7 @@ public class CompositeIndex
2124
private static final Map<ASN1ObjectIdentifier, String[]> pairings = new HashMap<ASN1ObjectIdentifier, String[]>();
2225
private static final Map<ASN1ObjectIdentifier, String> algorithmNames = new HashMap<ASN1ObjectIdentifier, String>();
2326
private static final Map<ASN1ObjectIdentifier, byte[]> kemLabels = new HashMap<ASN1ObjectIdentifier, byte[]>();
27+
private static final Map<ASN1ObjectIdentifier, AlgorithmParameterSpec[]> kpgInitSpecs = new HashMap<ASN1ObjectIdentifier, AlgorithmParameterSpec[]>();
2428

2529
static
2630
{
@@ -114,6 +118,32 @@ public class CompositeIndex
114118
Strings.toByteArray("MLKEM1024-P521"),
115119
new String[]{"ML-KEM-1024", "EC"}
116120
);
121+
122+
// Per-component KeyPairGenerator init specs (in pairing order: ML-KEM first, traditional
123+
// second). The ML-KEM component is generated through its parameter-set-specific
124+
// KeyPairGenerator name ("ML-KEM-768" / "ML-KEM-1024") so it needs no spec.
125+
kpgInitSpecs.put(IANAObjectIdentifiers.id_MLKEM768_RSA2048_SHA3_256, new AlgorithmParameterSpec[]{null, new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4)});
126+
kpgInitSpecs.put(IANAObjectIdentifiers.id_MLKEM768_RSA3072_SHA3_256, new AlgorithmParameterSpec[]{null, new RSAKeyGenParameterSpec(3072, RSAKeyGenParameterSpec.F4)});
127+
kpgInitSpecs.put(IANAObjectIdentifiers.id_MLKEM768_RSA4096_SHA3_256, new AlgorithmParameterSpec[]{null, new RSAKeyGenParameterSpec(4096, RSAKeyGenParameterSpec.F4)});
128+
kpgInitSpecs.put(IANAObjectIdentifiers.id_MLKEM768_X25519_SHA3_256, new AlgorithmParameterSpec[]{null, null});
129+
kpgInitSpecs.put(IANAObjectIdentifiers.id_MLKEM768_ECDH_P256_SHA3_256, new AlgorithmParameterSpec[]{null, new ECNamedCurveGenParameterSpec("P-256")});
130+
kpgInitSpecs.put(IANAObjectIdentifiers.id_MLKEM768_ECDH_P384_SHA3_256, new AlgorithmParameterSpec[]{null, new ECNamedCurveGenParameterSpec("P-384")});
131+
kpgInitSpecs.put(IANAObjectIdentifiers.id_MLKEM768_ECDH_BP256_SHA3_256, new AlgorithmParameterSpec[]{null, new ECNamedCurveGenParameterSpec("brainpoolP256r1")});
132+
kpgInitSpecs.put(IANAObjectIdentifiers.id_MLKEM1024_RSA3072_SHA3_256, new AlgorithmParameterSpec[]{null, new RSAKeyGenParameterSpec(3072, RSAKeyGenParameterSpec.F4)});
133+
kpgInitSpecs.put(IANAObjectIdentifiers.id_MLKEM1024_ECDH_P384_SHA3_256, new AlgorithmParameterSpec[]{null, new ECNamedCurveGenParameterSpec("P-384")});
134+
kpgInitSpecs.put(IANAObjectIdentifiers.id_MLKEM1024_ECDH_BP384_SHA3_256, new AlgorithmParameterSpec[]{null, new ECNamedCurveGenParameterSpec("brainpoolP384r1")});
135+
kpgInitSpecs.put(IANAObjectIdentifiers.id_MLKEM1024_X448_SHA3_256, new AlgorithmParameterSpec[]{null, null});
136+
kpgInitSpecs.put(IANAObjectIdentifiers.id_MLKEM1024_ECDH_P521_SHA3_256, new AlgorithmParameterSpec[]{null, new ECNamedCurveGenParameterSpec("P-521")});
137+
}
138+
139+
/**
140+
* Per-component {@link AlgorithmParameterSpec}s used to initialise the component
141+
* KeyPairGenerators, in pairing order. An entry may be {@code null} when the component's
142+
* KeyPairGenerator name already fixes the parameter set (ML-KEM, X25519, X448).
143+
*/
144+
public static AlgorithmParameterSpec[] getKeyPairSpecs(ASN1ObjectIdentifier algorithm)
145+
{
146+
return kpgInitSpecs.get(algorithm);
117147
}
118148

119149
public static Set<ASN1ObjectIdentifier> getSupportedIdentifiers()
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package org.bouncycastle.jcajce.provider.asymmetric.compositekem;
2+
3+
import java.security.InvalidAlgorithmParameterException;
4+
import java.security.KeyPair;
5+
import java.security.KeyPairGenerator;
6+
import java.security.PrivateKey;
7+
import java.security.PublicKey;
8+
import java.security.SecureRandom;
9+
import java.security.spec.AlgorithmParameterSpec;
10+
11+
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
12+
import org.bouncycastle.internal.asn1.iana.IANAObjectIdentifiers;
13+
import org.bouncycastle.jcajce.CompositePrivateKey;
14+
import org.bouncycastle.jcajce.CompositePublicKey;
15+
import org.bouncycastle.util.Exceptions;
16+
17+
/**
18+
* KeyPairGenerator for Composite ML-KEM (draft-ietf-lamps-pq-composite-kem). The concrete composite
19+
* is selected by the subclasses at the end of this file. The generated keys are a
20+
* {@link CompositePublicKey} / {@link CompositePrivateKey} pair whose components are, in order, the
21+
* ML-KEM key and the traditional (RSA / ECDH / X25519 / X448) key.
22+
*/
23+
public class KeyPairGeneratorSpi
24+
extends java.security.KeyPairGeneratorSpi
25+
{
26+
private final ASN1ObjectIdentifier algorithm;
27+
private final KeyPairGenerator[] generators;
28+
29+
KeyPairGeneratorSpi(ASN1ObjectIdentifier algorithm)
30+
{
31+
this.algorithm = algorithm;
32+
33+
String[] algorithms = CompositeIndex.getPairing(algorithm);
34+
AlgorithmParameterSpec[] initSpecs = CompositeIndex.getKeyPairSpecs(algorithm);
35+
36+
this.generators = new KeyPairGenerator[algorithms.length];
37+
for (int i = 0; i != algorithms.length; i++)
38+
{
39+
try
40+
{
41+
this.generators[i] = KeyPairGenerator.getInstance(CompositeIndex.getBaseName(algorithms[i]), "BC");
42+
43+
AlgorithmParameterSpec initSpec = initSpecs[i];
44+
if (initSpec != null)
45+
{
46+
this.generators[i].initialize(initSpec);
47+
}
48+
}
49+
catch (Exception e)
50+
{
51+
throw Exceptions.illegalStateException("unable to create base generator: " + e.getMessage(), e);
52+
}
53+
}
54+
}
55+
56+
/**
57+
* There is no notion of a key size for composite KEMs - the parameter set is fixed by the
58+
* algorithm. Use {@link #initialize(AlgorithmParameterSpec, SecureRandom)} (with a null spec)
59+
* only to supply a custom SecureRandom.
60+
*/
61+
public void initialize(int keySize, SecureRandom random)
62+
{
63+
throw new IllegalArgumentException("use AlgorithmParameterSpec");
64+
}
65+
66+
/**
67+
* A custom AlgorithmParameterSpec is not supported - the composite parameter set is determined
68+
* by the algorithm name. This method only serves to set a custom SecureRandom on the component
69+
* generators.
70+
*
71+
* @param paramSpec must be null.
72+
* @param secureRandom a SecureRandom used by the component key generators.
73+
*/
74+
public void initialize(AlgorithmParameterSpec paramSpec, SecureRandom secureRandom)
75+
throws InvalidAlgorithmParameterException
76+
{
77+
if (paramSpec != null)
78+
{
79+
throw new IllegalArgumentException("use initialize only for custom SecureRandom; AlgorithmParameterSpec must be null because it is determined by the algorithm name");
80+
}
81+
82+
AlgorithmParameterSpec[] initSpecs = CompositeIndex.getKeyPairSpecs(algorithm);
83+
for (int i = 0; i != initSpecs.length; i++)
84+
{
85+
AlgorithmParameterSpec initSpec = initSpecs[i];
86+
if (initSpec != null)
87+
{
88+
this.generators[i].initialize(initSpec, secureRandom);
89+
}
90+
}
91+
}
92+
93+
public KeyPair generateKeyPair()
94+
{
95+
PublicKey[] publicKeys = new PublicKey[generators.length];
96+
PrivateKey[] privateKeys = new PrivateKey[generators.length];
97+
for (int i = 0; i < generators.length; i++)
98+
{
99+
KeyPair keyPair = generators[i].generateKeyPair();
100+
publicKeys[i] = keyPair.getPublic();
101+
privateKeys[i] = keyPair.getPrivate();
102+
}
103+
CompositePublicKey compositePublicKey = new CompositePublicKey(this.algorithm, publicKeys);
104+
CompositePrivateKey compositePrivateKey = new CompositePrivateKey(this.algorithm, privateKeys);
105+
return new KeyPair(compositePublicKey, compositePrivateKey);
106+
}
107+
108+
public static final class MLKEM768_RSA2048_SHA3_256
109+
extends KeyPairGeneratorSpi
110+
{
111+
public MLKEM768_RSA2048_SHA3_256()
112+
{
113+
super(IANAObjectIdentifiers.id_MLKEM768_RSA2048_SHA3_256);
114+
}
115+
}
116+
117+
public static final class MLKEM768_RSA3072_SHA3_256
118+
extends KeyPairGeneratorSpi
119+
{
120+
public MLKEM768_RSA3072_SHA3_256()
121+
{
122+
super(IANAObjectIdentifiers.id_MLKEM768_RSA3072_SHA3_256);
123+
}
124+
}
125+
126+
public static final class MLKEM768_RSA4096_SHA3_256
127+
extends KeyPairGeneratorSpi
128+
{
129+
public MLKEM768_RSA4096_SHA3_256()
130+
{
131+
super(IANAObjectIdentifiers.id_MLKEM768_RSA4096_SHA3_256);
132+
}
133+
}
134+
135+
public static final class MLKEM768_X25519_SHA3_256
136+
extends KeyPairGeneratorSpi
137+
{
138+
public MLKEM768_X25519_SHA3_256()
139+
{
140+
super(IANAObjectIdentifiers.id_MLKEM768_X25519_SHA3_256);
141+
}
142+
}
143+
144+
public static final class MLKEM768_ECDH_P256_SHA3_256
145+
extends KeyPairGeneratorSpi
146+
{
147+
public MLKEM768_ECDH_P256_SHA3_256()
148+
{
149+
super(IANAObjectIdentifiers.id_MLKEM768_ECDH_P256_SHA3_256);
150+
}
151+
}
152+
153+
public static final class MLKEM768_ECDH_P384_SHA3_256
154+
extends KeyPairGeneratorSpi
155+
{
156+
public MLKEM768_ECDH_P384_SHA3_256()
157+
{
158+
super(IANAObjectIdentifiers.id_MLKEM768_ECDH_P384_SHA3_256);
159+
}
160+
}
161+
162+
public static final class MLKEM768_ECDH_BP256_SHA3_256
163+
extends KeyPairGeneratorSpi
164+
{
165+
public MLKEM768_ECDH_BP256_SHA3_256()
166+
{
167+
super(IANAObjectIdentifiers.id_MLKEM768_ECDH_BP256_SHA3_256);
168+
}
169+
}
170+
171+
public static final class MLKEM1024_RSA3072_SHA3_256
172+
extends KeyPairGeneratorSpi
173+
{
174+
public MLKEM1024_RSA3072_SHA3_256()
175+
{
176+
super(IANAObjectIdentifiers.id_MLKEM1024_RSA3072_SHA3_256);
177+
}
178+
}
179+
180+
public static final class MLKEM1024_ECDH_P384_SHA3_256
181+
extends KeyPairGeneratorSpi
182+
{
183+
public MLKEM1024_ECDH_P384_SHA3_256()
184+
{
185+
super(IANAObjectIdentifiers.id_MLKEM1024_ECDH_P384_SHA3_256);
186+
}
187+
}
188+
189+
public static final class MLKEM1024_ECDH_BP384_SHA3_256
190+
extends KeyPairGeneratorSpi
191+
{
192+
public MLKEM1024_ECDH_BP384_SHA3_256()
193+
{
194+
super(IANAObjectIdentifiers.id_MLKEM1024_ECDH_BP384_SHA3_256);
195+
}
196+
}
197+
198+
public static final class MLKEM1024_X448_SHA3_256
199+
extends KeyPairGeneratorSpi
200+
{
201+
public MLKEM1024_X448_SHA3_256()
202+
{
203+
super(IANAObjectIdentifiers.id_MLKEM1024_X448_SHA3_256);
204+
}
205+
}
206+
207+
public static final class MLKEM1024_ECDH_P521_SHA3_256
208+
extends KeyPairGeneratorSpi
209+
{
210+
public MLKEM1024_ECDH_P521_SHA3_256()
211+
{
212+
super(IANAObjectIdentifiers.id_MLKEM1024_ECDH_P521_SHA3_256);
213+
}
214+
}
215+
}

0 commit comments

Comments
 (0)