Skip to content

Commit 4817b8e

Browse files
committed
Merge branch '2083-xwing-update' into 'main'
Update XWing according to draft-connolly-cfrg-xwing-kem/07/ See merge request root/bc-java!105
2 parents fb4b20c + 8d2b53a commit 4817b8e

File tree

5 files changed

+405
-131
lines changed

5 files changed

+405
-131
lines changed
Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,54 @@
11
package org.bouncycastle.pqc.crypto.xwing;
22

33
import org.bouncycastle.crypto.EncapsulatedSecretExtractor;
4-
import org.bouncycastle.crypto.agreement.X25519Agreement;
5-
import org.bouncycastle.crypto.digests.SHA3Digest;
6-
import org.bouncycastle.crypto.params.X25519PrivateKeyParameters;
74
import org.bouncycastle.crypto.params.X25519PublicKeyParameters;
85
import org.bouncycastle.pqc.crypto.mlkem.MLKEMExtractor;
9-
import org.bouncycastle.pqc.crypto.mlkem.MLKEMPrivateKeyParameters;
106
import org.bouncycastle.util.Arrays;
11-
import org.bouncycastle.util.Strings;
127

8+
/**
9+
* Implements the decapsulation process of the X-Wing hybrid Key Encapsulation Mechanism (KEM).
10+
* <p>
11+
* This class allows the recipient to derive the shared secret from a given ciphertext using their private key,
12+
* as defined in the X-Wing KEM specification.
13+
* </p>
14+
*
15+
* @see <a href="https://datatracker.ietf.org/doc/draft-connolly-cfrg-xwing-kem/07/">X-Wing KEM Draft</a>
16+
*/
1317
public class XWingKEMExtractor
1418
implements EncapsulatedSecretExtractor
1519
{
20+
private static final int MLKEM_CIPHERTEXT_SIZE = 1088;
1621
private final XWingPrivateKeyParameters key;
17-
private final MLKEMExtractor kemExtractor;
22+
private final MLKEMExtractor mlkemExtractor;
1823

1924
public XWingKEMExtractor(XWingPrivateKeyParameters privParams)
2025
{
2126
this.key = privParams;
22-
this.kemExtractor = new MLKEMExtractor((MLKEMPrivateKeyParameters)key.getKyberPrivateKey());
27+
this.mlkemExtractor = new MLKEMExtractor(key.getKyberPrivateKey());
2328
}
2429

2530
@Override
2631
public byte[] extractSecret(byte[] encapsulation)
2732
{
28-
// Decryption
29-
byte[] kybSecret = kemExtractor.extractSecret(Arrays.copyOfRange(encapsulation, 0, encapsulation.length - X25519PublicKeyParameters.KEY_SIZE));
30-
X25519Agreement xdhAgree = new X25519Agreement();
33+
// 1. Split ciphertext into ML-KEM and X25519 parts
34+
byte[] ctM = Arrays.copyOfRange(encapsulation, 0, MLKEM_CIPHERTEXT_SIZE);
35+
byte[] ctX = Arrays.copyOfRange(encapsulation, MLKEM_CIPHERTEXT_SIZE, encapsulation.length);
3136

32-
byte[] k = new byte[kybSecret.length + xdhAgree.getAgreementSize()];
37+
// 2. Compute X25519 shared secret
38+
byte[] ssX = XWingKEMGenerator.computeSSX(new X25519PublicKeyParameters(ctX, 0), key.getXDHPrivateKey());
3339

34-
System.arraycopy(kybSecret, 0, k, 0, kybSecret.length);
40+
// 3. Compute combiner: SHA3-256(ssM || ssX || ctX || pkX || XWING_LABEL)
41+
byte[] kemSecret = XWingKEMGenerator.computeSharedSecret(key.getXDHPublicKey().getEncoded(),
42+
mlkemExtractor.extractSecret(ctM), ctX, ssX);
3543

36-
Arrays.clear(kybSecret);
37-
38-
xdhAgree.init(key.getXDHPrivateKey());
39-
40-
X25519PublicKeyParameters ephXdhPub = new X25519PublicKeyParameters(Arrays.copyOfRange(encapsulation, encapsulation.length - X25519PublicKeyParameters.KEY_SIZE, encapsulation.length));
41-
42-
xdhAgree.calculateAgreement(ephXdhPub, k, kybSecret.length);
43-
44-
SHA3Digest sha3 = new SHA3Digest(256);
45-
46-
sha3.update(Strings.toByteArray("\\.//^\\"), 0, 6);
47-
sha3.update(k, 0, k.length);
48-
sha3.update(ephXdhPub.getEncoded(), 0, X25519PublicKeyParameters.KEY_SIZE);
49-
sha3.update(((X25519PrivateKeyParameters)key.getXDHPrivateKey()).generatePublicKey().getEncoded(), 0, X25519PublicKeyParameters.KEY_SIZE);
50-
51-
byte[] kemSecret = new byte[32];
52-
53-
sha3.doFinal(kemSecret, 0);
44+
// 4. Cleanup intermediate values
45+
Arrays.clear(ssX);
5446

5547
return kemSecret;
5648
}
5749

5850
public int getEncapsulationLength()
5951
{
60-
return kemExtractor.getEncapsulationLength() + X25519PublicKeyParameters.KEY_SIZE;
52+
return mlkemExtractor.getEncapsulationLength() + X25519PublicKeyParameters.KEY_SIZE;
6153
}
6254
}

core/src/main/java/org/bouncycastle/pqc/crypto/xwing/XWingKEMGenerator.java

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,61 +10,88 @@
1010
import org.bouncycastle.crypto.generators.X25519KeyPairGenerator;
1111
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
1212
import org.bouncycastle.crypto.params.X25519KeyGenerationParameters;
13+
import org.bouncycastle.crypto.params.X25519PrivateKeyParameters;
1314
import org.bouncycastle.crypto.params.X25519PublicKeyParameters;
1415
import org.bouncycastle.pqc.crypto.mlkem.MLKEMGenerator;
16+
import org.bouncycastle.pqc.crypto.mlkem.MLKEMPublicKeyParameters;
1517
import org.bouncycastle.pqc.crypto.util.SecretWithEncapsulationImpl;
1618
import org.bouncycastle.util.Arrays;
1719
import org.bouncycastle.util.Strings;
1820

21+
/**
22+
* Implements the encapsulation process of the X-Wing hybrid Key Encapsulation Mechanism (KEM).
23+
* <p>
24+
* X-Wing is a general-purpose hybrid post-quantum/traditional KEM that combines X25519 and ML-KEM-768,
25+
* as specified in the IETF draft: draft-connolly-cfrg-xwing-kem-07.
26+
* </p>
27+
* <p>
28+
* This class facilitates the generation of ciphertexts and shared secrets using a recipient's public key.
29+
* </p>
30+
*
31+
* @see <a href="https://datatracker.ietf.org/doc/draft-connolly-cfrg-xwing-kem/07/">X-Wing KEM Draft</a>
32+
*/
1933
public class XWingKEMGenerator
2034
implements EncapsulatedSecretGenerator
2135
{
22-
// the source of randomness
23-
private final SecureRandom sr;
36+
private final SecureRandom random;
37+
private static final byte[] XWING_LABEL = Strings.toByteArray("\\.//^\\");
2438

2539
public XWingKEMGenerator(SecureRandom random)
2640
{
27-
this.sr = random;
41+
this.random = random;
2842
}
2943

3044
public SecretWithEncapsulation generateEncapsulated(AsymmetricKeyParameter recipientKey)
3145
{
3246
XWingPublicKeyParameters key = (XWingPublicKeyParameters)recipientKey;
47+
MLKEMPublicKeyParameters kyberPub = key.getKyberPublicKey();
48+
X25519PublicKeyParameters xdhPub = key.getXDHPublicKey();
49+
byte[] xdhPubBytes = xdhPub.getEncoded();
3350

34-
MLKEMGenerator kybKem = new MLKEMGenerator(sr);
35-
36-
SecretWithEncapsulation kybSecWithEnc = kybKem.generateEncapsulated(key.getKyberPublicKey());
37-
X25519Agreement xdhAgree = new X25519Agreement();
38-
byte[] kybSecret = kybSecWithEnc.getSecret();
39-
byte[] k = new byte[kybSecret.length + xdhAgree.getAgreementSize()];
40-
41-
System.arraycopy(kybSecret, 0, k, 0, kybSecret.length);
42-
43-
Arrays.clear(kybSecret);
51+
// 1. Perform ML-KEM encapsulation
52+
MLKEMGenerator mlkemGen = new MLKEMGenerator(random);
53+
SecretWithEncapsulation mlkemSec = mlkemGen.generateEncapsulated(kyberPub);
54+
byte[] ctM = mlkemSec.getEncapsulation();
4455

56+
// 2. Generate ephemeral X25519 key pair
4557
X25519KeyPairGenerator xdhGen = new X25519KeyPairGenerator();
58+
xdhGen.init(new X25519KeyGenerationParameters(random));
59+
AsymmetricCipherKeyPair ephXdhKp = xdhGen.generateKeyPair();
60+
byte[] ctX = ((X25519PublicKeyParameters)ephXdhKp.getPublic()).getEncoded();
4661

47-
xdhGen.init(new X25519KeyGenerationParameters(sr));
62+
// 3. Perform X25519 agreement
63+
byte[] ssX = computeSSX(xdhPub, (X25519PrivateKeyParameters)ephXdhKp.getPrivate());
4864

49-
AsymmetricCipherKeyPair ephXdh = xdhGen.generateKeyPair();
65+
// 4. Compute shared secret: SHA3-256(ssM || ssX || ctX || pkX || label)
66+
byte[] ss = computeSharedSecret(xdhPubBytes, mlkemSec.getSecret(), ctX, ssX);
5067

51-
xdhAgree.init(ephXdh.getPrivate());
68+
// 5. Cleanup intermediate values
69+
Arrays.clear(ssX);
5270

53-
xdhAgree.calculateAgreement(key.getXDHPublicKey(), k, kybSecret.length);
71+
// 6. Return shared secret and encapsulation (ctM || ctX)
72+
return new SecretWithEncapsulationImpl(ss, Arrays.concatenate(ctM, ctX));
73+
}
5474

55-
X25519PublicKeyParameters ephXdhPub = (X25519PublicKeyParameters)ephXdh.getPublic();
75+
static byte[] computeSSX(X25519PublicKeyParameters xdhPub, X25519PrivateKeyParameters ephXdhPriv)
76+
{
77+
X25519Agreement xdhAgreement = new X25519Agreement();
78+
xdhAgreement.init(ephXdhPriv);
79+
byte[] ssX = new byte[xdhAgreement.getAgreementSize()];
80+
xdhAgreement.calculateAgreement(xdhPub, ssX, 0);
81+
return ssX;
82+
}
5683

84+
static byte[] computeSharedSecret(byte[] xdhPubBytes, byte[] ssM, byte[] ctX, byte[] ssX)
85+
{
5786
SHA3Digest sha3 = new SHA3Digest(256);
58-
59-
sha3.update(Strings.toByteArray("\\.//^\\"), 0, 6);
60-
sha3.update(k, 0, k.length);
61-
sha3.update(ephXdhPub.getEncoded(), 0, X25519PublicKeyParameters.KEY_SIZE);
62-
sha3.update(((X25519PublicKeyParameters)key.getXDHPublicKey()).getEncoded(), 0, X25519PublicKeyParameters.KEY_SIZE);
63-
64-
byte[] kemSecret = new byte[32];
65-
66-
sha3.doFinal(kemSecret, 0);
67-
68-
return new SecretWithEncapsulationImpl(kemSecret, Arrays.concatenate(kybSecWithEnc.getEncapsulation(), ephXdhPub.getEncoded()));
87+
sha3.update(ssM, 0, ssM.length);
88+
sha3.update(ssX, 0, ssX.length);
89+
sha3.update(ctX, 0, ctX.length);
90+
sha3.update(xdhPubBytes, 0, xdhPubBytes.length);
91+
sha3.update(XWING_LABEL, 0, XWING_LABEL.length);
92+
93+
byte[] ss = new byte[32];
94+
sha3.doFinal(ss, 0);
95+
return ss;
6996
}
7097
}

core/src/main/java/org/bouncycastle/pqc/crypto/xwing/XWingKeyPairGenerator.java

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,28 @@
55
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
66
import org.bouncycastle.crypto.AsymmetricCipherKeyPairGenerator;
77
import org.bouncycastle.crypto.KeyGenerationParameters;
8+
import org.bouncycastle.crypto.digests.SHAKEDigest;
89
import org.bouncycastle.crypto.generators.X25519KeyPairGenerator;
910
import org.bouncycastle.crypto.params.X25519KeyGenerationParameters;
11+
import org.bouncycastle.crypto.params.X25519PrivateKeyParameters;
12+
import org.bouncycastle.crypto.params.X25519PublicKeyParameters;
13+
import org.bouncycastle.crypto.prng.FixedSecureRandom;
1014
import org.bouncycastle.pqc.crypto.mlkem.MLKEMKeyGenerationParameters;
1115
import org.bouncycastle.pqc.crypto.mlkem.MLKEMKeyPairGenerator;
1216
import org.bouncycastle.pqc.crypto.mlkem.MLKEMParameters;
17+
import org.bouncycastle.pqc.crypto.mlkem.MLKEMPrivateKeyParameters;
18+
import org.bouncycastle.pqc.crypto.mlkem.MLKEMPublicKeyParameters;
19+
import org.bouncycastle.util.Arrays;
1320

21+
/**
22+
* Generates key pairs compatible with the X-Wing hybrid Key Encapsulation Mechanism (KEM).
23+
* <p>
24+
* This class produces key pairs that include both X25519 and ML-KEM-768 components,
25+
* suitable for use in the X-Wing KEM as specified in the IETF draft.
26+
* </p>
27+
*
28+
* @see <a href="https://datatracker.ietf.org/doc/draft-connolly-cfrg-xwing-kem/07/">X-Wing KEM Draft</a>
29+
*/
1430
public class XWingKeyPairGenerator
1531
implements AsymmetricCipherKeyPairGenerator
1632
{
@@ -22,22 +38,39 @@ private void initialize(
2238
this.random = param.getRandom();
2339
}
2440

25-
private AsymmetricCipherKeyPair genKeyPair()
41+
static AsymmetricCipherKeyPair genKeyPair(byte[] seed)
2642
{
27-
MLKEMKeyPairGenerator kyberKeyGen = new MLKEMKeyPairGenerator();
43+
// Step 2: Expand seed to 96 bytes using SHAKE256
44+
SHAKEDigest shake = new SHAKEDigest(256);
45+
shake.update(seed, 0, seed.length);
46+
byte[] expanded = new byte[96];
47+
shake.doOutput(expanded, 0, expanded.length);
2848

29-
kyberKeyGen.init(new MLKEMKeyGenerationParameters(random, MLKEMParameters.ml_kem_768));
49+
// Step 3: Split expanded bytes
50+
byte[] mlkemSeed = Arrays.copyOfRange(expanded, 0, 64);
51+
byte[] skX = Arrays.copyOfRange(expanded, 64, 96);
3052

31-
X25519KeyPairGenerator x25519KeyGen = new X25519KeyPairGenerator();
53+
// Step 4a: Generate ML-KEM key pair deterministically
54+
SecureRandom mlkemRandom = new FixedSecureRandom(mlkemSeed);
55+
MLKEMKeyPairGenerator mlkemKeyGen = new MLKEMKeyPairGenerator();
56+
mlkemKeyGen.init(new MLKEMKeyGenerationParameters(mlkemRandom, MLKEMParameters.ml_kem_768));
57+
AsymmetricCipherKeyPair mlkemKp = mlkemKeyGen.generateKeyPair();
58+
MLKEMPublicKeyParameters mlkemPub = (MLKEMPublicKeyParameters)mlkemKp.getPublic();
59+
MLKEMPrivateKeyParameters mlkemPriv = (MLKEMPrivateKeyParameters)mlkemKp.getPrivate();
3260

33-
x25519KeyGen.init(new X25519KeyGenerationParameters(random));
34-
35-
AsymmetricCipherKeyPair kybKp = kyberKeyGen.generateKeyPair();
36-
AsymmetricCipherKeyPair xdhKp = x25519KeyGen.generateKeyPair();
61+
// Step 4b: Generate X25519 key pair deterministically
62+
SecureRandom xdhRandom = new FixedSecureRandom(skX);
63+
X25519KeyPairGenerator xdhKeyGen = new X25519KeyPairGenerator();
64+
xdhKeyGen.init(new X25519KeyGenerationParameters(xdhRandom));
65+
AsymmetricCipherKeyPair xdhKp = xdhKeyGen.generateKeyPair();
66+
X25519PublicKeyParameters xdhPub = (X25519PublicKeyParameters)xdhKp.getPublic();
67+
X25519PrivateKeyParameters xdhPriv = (X25519PrivateKeyParameters)xdhKp.getPrivate();
3768

69+
// Step 5: Create X-Wing keys
3870
return new AsymmetricCipherKeyPair(
39-
new XWingPublicKeyParameters(kybKp.getPublic(), xdhKp.getPublic()),
40-
new XWingPrivateKeyParameters(kybKp.getPrivate(), xdhKp.getPrivate()));
71+
new XWingPublicKeyParameters(mlkemPub, xdhPub),
72+
new XWingPrivateKeyParameters(seed, mlkemPriv, xdhPriv, mlkemPub, xdhPub)
73+
);
4174
}
4275

4376
public void init(KeyGenerationParameters param)
@@ -47,7 +80,9 @@ public void init(KeyGenerationParameters param)
4780

4881
public AsymmetricCipherKeyPair generateKeyPair()
4982
{
50-
return genKeyPair();
83+
// Step 1: Generate 32-byte random seed
84+
byte[] seed = new byte[32];
85+
random.nextBytes(seed);
86+
return genKeyPair(seed);
5187
}
52-
5388
}
Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,72 @@
11
package org.bouncycastle.pqc.crypto.xwing;
22

3-
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
43
import org.bouncycastle.crypto.params.X25519PrivateKeyParameters;
5-
import org.bouncycastle.pqc.crypto.mlkem.MLKEMParameters;
4+
import org.bouncycastle.crypto.params.X25519PublicKeyParameters;
65
import org.bouncycastle.pqc.crypto.mlkem.MLKEMPrivateKeyParameters;
6+
import org.bouncycastle.pqc.crypto.mlkem.MLKEMPublicKeyParameters;
77
import org.bouncycastle.util.Arrays;
88

99
public class XWingPrivateKeyParameters
1010
extends XWingKeyParameters
1111
{
12-
private final MLKEMPrivateKeyParameters kybPriv;
13-
private final X25519PrivateKeyParameters xdhPriv;
12+
private final transient byte[] seed;
13+
private final transient MLKEMPrivateKeyParameters kyberPrivateKey;
14+
private final transient X25519PrivateKeyParameters xdhPrivateKey;
15+
private final transient MLKEMPublicKeyParameters kyberPublicKey;
16+
private final transient X25519PublicKeyParameters xdhPublicKey;
1417

15-
XWingPrivateKeyParameters(AsymmetricKeyParameter kybPriv, AsymmetricKeyParameter xdhPriv)
18+
public XWingPrivateKeyParameters(byte[] seed,
19+
MLKEMPrivateKeyParameters kyberPrivateKey,
20+
X25519PrivateKeyParameters xdhPrivateKey,
21+
MLKEMPublicKeyParameters kyberPublicKey,
22+
X25519PublicKeyParameters xdhPublicKey)
1623
{
1724
super(true);
18-
19-
this.kybPriv = (MLKEMPrivateKeyParameters)kybPriv;
20-
this.xdhPriv = (X25519PrivateKeyParameters)xdhPriv;
25+
this.seed = Arrays.clone(seed);
26+
this.kyberPrivateKey = kyberPrivateKey;
27+
this.xdhPrivateKey = xdhPrivateKey;
28+
this.kyberPublicKey = kyberPublicKey;
29+
this.xdhPublicKey = xdhPublicKey;
2130
}
2231

23-
public XWingPrivateKeyParameters(byte[] encoding)
32+
public XWingPrivateKeyParameters(byte[] seed)
2433
{
25-
super(false);
34+
super(true);
35+
XWingPrivateKeyParameters key = (XWingPrivateKeyParameters)XWingKeyPairGenerator.genKeyPair(seed).getPrivate();
36+
this.seed = key.seed;
37+
this.kyberPrivateKey = key.kyberPrivateKey;
38+
this.xdhPrivateKey = key.xdhPrivateKey;
39+
this.kyberPublicKey = key.kyberPublicKey;
40+
this.xdhPublicKey = key.xdhPublicKey;
41+
}
2642

27-
this.kybPriv = new MLKEMPrivateKeyParameters(MLKEMParameters.ml_kem_768, Arrays.copyOfRange(encoding, 0, encoding.length - X25519PrivateKeyParameters.KEY_SIZE));
28-
this.xdhPriv = new X25519PrivateKeyParameters(encoding, encoding.length - X25519PrivateKeyParameters.KEY_SIZE);
43+
public byte[] getSeed()
44+
{
45+
return Arrays.clone(seed);
2946
}
3047

3148
MLKEMPrivateKeyParameters getKyberPrivateKey()
3249
{
33-
return kybPriv;
50+
return kyberPrivateKey;
51+
}
52+
53+
MLKEMPublicKeyParameters getKyberPublicKey()
54+
{
55+
return kyberPublicKey;
3456
}
3557

3658
X25519PrivateKeyParameters getXDHPrivateKey()
3759
{
38-
return xdhPriv;
60+
return xdhPrivateKey;
61+
}
62+
63+
X25519PublicKeyParameters getXDHPublicKey()
64+
{
65+
return xdhPublicKey;
3966
}
4067

4168
public byte[] getEncoded()
4269
{
43-
return Arrays.concatenate(kybPriv.getEncoded(), xdhPriv.getEncoded());
70+
return Arrays.clone(seed);
4471
}
4572
}

0 commit comments

Comments
 (0)