diff --git a/tls/src/main/java/org/bouncycastle/jsse/provider/NamedGroupInfo.java b/tls/src/main/java/org/bouncycastle/jsse/provider/NamedGroupInfo.java index f2af7facd9..b64c546282 100644 --- a/tls/src/main/java/org/bouncycastle/jsse/provider/NamedGroupInfo.java +++ b/tls/src/main/java/org/bouncycastle/jsse/provider/NamedGroupInfo.java @@ -81,7 +81,10 @@ private enum All OQS_mlkem1024(NamedGroup.OQS_mlkem1024, "ML-KEM"), MLKEM512(NamedGroup.MLKEM512, "ML-KEM"), MLKEM768(NamedGroup.MLKEM768, "ML-KEM"), - MLKEM1024(NamedGroup.MLKEM1024, "ML-KEM"); + MLKEM1024(NamedGroup.MLKEM1024, "ML-KEM"), + SecP256r1MLKEM768(NamedGroup.SecP256r1MLKEM768, "ML-KEM"), + X25519MLKEM768(NamedGroup.X25519MLKEM768, "ML-KEM"), + SecP384r1MLKEM1024(NamedGroup.SecP384r1MLKEM1024, "ML-KEM"); private final int namedGroup; private final String name; diff --git a/tls/src/main/java/org/bouncycastle/tls/NamedGroup.java b/tls/src/main/java/org/bouncycastle/tls/NamedGroup.java index b8fc46d1c5..eff9c7f303 100644 --- a/tls/src/main/java/org/bouncycastle/tls/NamedGroup.java +++ b/tls/src/main/java/org/bouncycastle/tls/NamedGroup.java @@ -116,6 +116,13 @@ public class NamedGroup public static final int MLKEM768 = 0x0768; public static final int MLKEM1024 = 0x1024; + /* + * draft-kwiatkowski-tls-ecdhe-mlkem-03 + */ + public static final int SecP256r1MLKEM768 = 0x11EB; + public static final int X25519MLKEM768 = 0x11EC; + public static final int SecP384r1MLKEM1024 = 0x11ED; + /* Names of the actual underlying elliptic curves (not necessarily matching the NamedGroup names). */ private static final String[] CURVE_NAMES = new String[]{ "sect163k1", "sect163r1", "sect163r2", "sect193r1", "sect193r2", "sect233k1", "sect233r1", "sect239k1", "sect283k1", "sect283r1", "sect409k1", "sect409r1", @@ -310,6 +317,12 @@ public static String getKemName(int namedGroup) case OQS_mlkem1024: case MLKEM1024: return "ML-KEM-1024"; + case SecP256r1MLKEM768: + return "SecP256r1MLKEM768"; + case X25519MLKEM768: + return "X25519MLKEM768"; + case SecP384r1MLKEM1024: + return "SecP384r1MLKEM1024"; default: return null; } @@ -382,6 +395,12 @@ public static String getName(int namedGroup) return "MLKEM768"; case MLKEM1024: return "MLKEM1024"; + case SecP256r1MLKEM768: + return "SecP256r1MLKEM768"; + case X25519MLKEM768: + return "X25519MLKEM768"; + case SecP384r1MLKEM1024: + return "SecP384r1MLKEM1024"; case arbitrary_explicit_prime_curves: return "arbitrary_explicit_prime_curves"; case arbitrary_explicit_char2_curves: @@ -502,6 +521,9 @@ public static boolean refersToASpecificKem(int namedGroup) case MLKEM512: case MLKEM768: case MLKEM1024: + case SecP256r1MLKEM768: + case X25519MLKEM768: + case SecP384r1MLKEM1024: return true; default: return false; diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsCrypto.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsCrypto.java index 6265353d9e..ef7190ebdf 100644 --- a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsCrypto.java +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsCrypto.java @@ -219,7 +219,16 @@ public TlsECDomain createECDomain(TlsECConfig ecConfig) public TlsKemDomain createKemDomain(TlsKemConfig kemConfig) { - return new BcTlsMLKemDomain(this, kemConfig); + switch (kemConfig.getNamedGroup()) + { + case NamedGroup.SecP256r1MLKEM768: + case NamedGroup.SecP384r1MLKEM1024: + return new BcTlsECDHMLKemDomain(this, kemConfig); + case NamedGroup.X25519MLKEM768: + return new BcTlsX25519MLKemDomain(this, kemConfig); + default: + return new BcTlsMLKemDomain(this, kemConfig); + } } public TlsNonceGenerator createNonceGenerator(byte[] additionalSeedMaterial) diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsECDHMLKem.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsECDHMLKem.java new file mode 100644 index 0000000000..0be69c06d4 --- /dev/null +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsECDHMLKem.java @@ -0,0 +1,75 @@ +package org.bouncycastle.tls.crypto.impl.bc; + +import java.io.IOException; + +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.SecretWithEncapsulation; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.pqc.crypto.mlkem.MLKEMPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.mlkem.MLKEMPublicKeyParameters; +import org.bouncycastle.tls.crypto.TlsAgreement; +import org.bouncycastle.tls.crypto.TlsSecret; +import org.bouncycastle.util.Arrays; + +public class BcTlsECDHMLKem implements TlsAgreement +{ + protected final BcTlsECDHMLKemDomain domain; + + protected AsymmetricCipherKeyPair ecLocalKeyPair; + protected ECPublicKeyParameters ecPeerPublicKey; + protected MLKEMPrivateKeyParameters mlkemPrivateKey; + protected MLKEMPublicKeyParameters mlkemPublicKey; + protected byte[] mlkemSecret; + + public BcTlsECDHMLKem(BcTlsECDHMLKemDomain domain) + { + this.domain = domain; + } + + public byte[] generateEphemeral() throws IOException + { + this.ecLocalKeyPair = domain.getECDomain().generateKeyPair(); + byte[] ecPublickey = domain.getECDomain().encodePublicKey((ECPublicKeyParameters)ecLocalKeyPair.getPublic()); + + if (domain.isServer()) + { + SecretWithEncapsulation encap = domain.getMLKemDomain().encapsulate(mlkemPublicKey); + this.mlkemPublicKey = null; + this.mlkemSecret = encap.getSecret(); + byte[] mlkemValue = encap.getEncapsulation(); + return Arrays.concatenate(ecPublickey, mlkemValue); + } + else + { + AsymmetricCipherKeyPair kp = domain.getMLKemDomain().generateKeyPair(); + this.mlkemPrivateKey = (MLKEMPrivateKeyParameters)kp.getPrivate(); + byte[] mlkemValue = domain.getMLKemDomain().encodePublicKey((MLKEMPublicKeyParameters)kp.getPublic()); + return Arrays.concatenate(ecPublickey, mlkemValue); + } + } + + public void receivePeerValue(byte[] peerValue) throws IOException + { + this.ecPeerPublicKey = domain.getECDomain().decodePublicKey(Arrays.copyOf(peerValue, domain.getECDomain().getPublicKeyByteLength())); + byte[] mlkemValue = Arrays.copyOfRange(peerValue, domain.getECDomain().getPublicKeyByteLength(), peerValue.length); + + if (domain.isServer()) + { + this.mlkemPublicKey = domain.getMLKemDomain().decodePublicKey(mlkemValue); + } + else + { + this.mlkemSecret = domain.getMLKemDomain().decapsulate(mlkemPrivateKey, mlkemValue); + this.mlkemPrivateKey = null; + } + } + + public TlsSecret calculateSecret() throws IOException + { + byte[] ecSecret = domain.getECDomain().calculateECDHAgreementBytes((ECPrivateKeyParameters)ecLocalKeyPair.getPrivate(), ecPeerPublicKey); + TlsSecret secret = domain.adoptLocalSecret(Arrays.concatenate(ecSecret, mlkemSecret)); + this.mlkemSecret = null; + return secret; + } +} diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsECDHMLKemDomain.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsECDHMLKemDomain.java new file mode 100644 index 0000000000..273f5d47ba --- /dev/null +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsECDHMLKemDomain.java @@ -0,0 +1,61 @@ +package org.bouncycastle.tls.crypto.impl.bc; + +import org.bouncycastle.tls.NamedGroup; +import org.bouncycastle.tls.crypto.TlsAgreement; +import org.bouncycastle.tls.crypto.TlsECConfig; +import org.bouncycastle.tls.crypto.TlsKemConfig; +import org.bouncycastle.tls.crypto.TlsKemDomain; + +public class BcTlsECDHMLKemDomain implements TlsKemDomain +{ + protected final BcTlsCrypto crypto; + protected final boolean isServer; + private final BcTlsECDomain ecDomain; + private final BcTlsMLKemDomain mlkemDomain; + + public BcTlsECDHMLKemDomain(BcTlsCrypto crypto, TlsKemConfig kemConfig) + { + this.crypto = crypto; + this.ecDomain = getBcTlsECDomain(crypto, kemConfig); + this.mlkemDomain = new BcTlsMLKemDomain(crypto, kemConfig); + this.isServer = kemConfig.isServer(); + } + + public BcTlsSecret adoptLocalSecret(byte[] secret) + { + return crypto.adoptLocalSecret(secret); + } + + public TlsAgreement createKem() + { + return new BcTlsECDHMLKem(this); + } + + public boolean isServer() + { + return isServer; + } + + public BcTlsECDomain getECDomain() + { + return ecDomain; + } + + public BcTlsMLKemDomain getMLKemDomain() + { + return mlkemDomain; + } + + private BcTlsECDomain getBcTlsECDomain(BcTlsCrypto crypto, TlsKemConfig kemConfig) + { + switch (kemConfig.getNamedGroup()) + { + case NamedGroup.SecP256r1MLKEM768: + return new BcTlsECDomain(crypto, new TlsECConfig(NamedGroup.secp256r1)); + case NamedGroup.SecP384r1MLKEM1024: + return new BcTlsECDomain(crypto, new TlsECConfig(NamedGroup.secp384r1)); + default: + return null; + } + } +} diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsECDomain.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsECDomain.java index 0714878296..362354f378 100644 --- a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsECDomain.java +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsECDomain.java @@ -27,6 +27,19 @@ */ public class BcTlsECDomain implements TlsECDomain { + public int getPublicKeyByteLength() + { + return (((domainParameters.getCurve().getFieldSize() + 7) / 8) * 2) + 1; + } + + public byte[] calculateECDHAgreementBytes(ECPrivateKeyParameters privateKey, ECPublicKeyParameters publicKey) + { + ECDHBasicAgreement basicAgreement = new ECDHBasicAgreement(); + basicAgreement.init(privateKey); + BigInteger agreementValue = basicAgreement.calculateAgreement(publicKey); + return BigIntegers.asUnsignedByteArray(basicAgreement.getFieldSize(), agreementValue); + } + public static BcTlsSecret calculateECDHAgreement(BcTlsCrypto crypto, ECPrivateKeyParameters privateKey, ECPublicKeyParameters publicKey) { diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsMLKem.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsMLKem.java index 897536f380..614a104da8 100644 --- a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsMLKem.java +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsMLKem.java @@ -47,7 +47,7 @@ public void receivePeerValue(byte[] peerValue) throws IOException } else { - this.secret = domain.decapsulate(privateKey, peerValue); + this.secret = domain.adoptLocalSecret(domain.decapsulate(privateKey, peerValue)); this.privateKey = null; } } diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsMLKemDomain.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsMLKemDomain.java index 4b2fc43294..7212520bf4 100644 --- a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsMLKemDomain.java +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsMLKemDomain.java @@ -25,9 +25,12 @@ public static MLKEMParameters getDomainParameters(TlsKemConfig kemConfig) return MLKEMParameters.ml_kem_512; case NamedGroup.OQS_mlkem768: case NamedGroup.MLKEM768: + case NamedGroup.SecP256r1MLKEM768: + case NamedGroup.X25519MLKEM768: return MLKEMParameters.ml_kem_768; case NamedGroup.OQS_mlkem1024: case NamedGroup.MLKEM1024: + case NamedGroup.SecP384r1MLKEM1024: return MLKEMParameters.ml_kem_1024; default: throw new IllegalArgumentException("No ML-KEM configuration provided"); @@ -57,11 +60,10 @@ public TlsAgreement createKem() return new BcTlsMLKem(this); } - public BcTlsSecret decapsulate(MLKEMPrivateKeyParameters privateKey, byte[] ciphertext) + public byte[] decapsulate(MLKEMPrivateKeyParameters privateKey, byte[] ciphertext) { MLKEMExtractor kemExtract = new MLKEMExtractor(privateKey); - byte[] secret = kemExtract.extractSecret(ciphertext); - return adoptLocalSecret(secret); + return kemExtract.extractSecret(ciphertext); } public MLKEMPublicKeyParameters decodePublicKey(byte[] encoding) diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsX25519MLKem.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsX25519MLKem.java new file mode 100644 index 0000000000..89627add65 --- /dev/null +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsX25519MLKem.java @@ -0,0 +1,75 @@ +package org.bouncycastle.tls.crypto.impl.bc; + +import java.io.IOException; + +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.SecretWithEncapsulation; +import org.bouncycastle.math.ec.rfc7748.X25519; +import org.bouncycastle.pqc.crypto.mlkem.MLKEMPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.mlkem.MLKEMPublicKeyParameters; +import org.bouncycastle.tls.crypto.TlsAgreement; +import org.bouncycastle.tls.crypto.TlsSecret; +import org.bouncycastle.util.Arrays; + +public class BcTlsX25519MLKem implements TlsAgreement +{ + protected final BcTlsX25519MLKemDomain domain; + + protected byte[] x25519PrivateKey; + protected byte[] x25519PeerPublicKey; + protected MLKEMPrivateKeyParameters mlkemPrivateKey; + protected MLKEMPublicKeyParameters mlkemPublicKey; + protected byte[] mlkemSecret; + + public BcTlsX25519MLKem(BcTlsX25519MLKemDomain domain) + { + this.domain = domain; + } + + public byte[] generateEphemeral() throws IOException + { + this.x25519PrivateKey = domain.generateX25519PrivateKey(); + byte[] x25519PublicKey = domain.getX25519PublicKey(x25519PrivateKey); + + if (domain.isServer()) + { + SecretWithEncapsulation encap = domain.getMLKemDomain().encapsulate(mlkemPublicKey); + this.mlkemPublicKey = null; + this.mlkemSecret = encap.getSecret(); + byte[] mlkemValue = encap.getEncapsulation(); + return Arrays.concatenate(mlkemValue, x25519PublicKey); + } + else + { + AsymmetricCipherKeyPair kp = domain.getMLKemDomain().generateKeyPair(); + this.mlkemPrivateKey = (MLKEMPrivateKeyParameters)kp.getPrivate(); + byte[] mlkemValue = domain.getMLKemDomain().encodePublicKey((MLKEMPublicKeyParameters)kp.getPublic()); + return Arrays.concatenate(mlkemValue, x25519PublicKey); + } + } + + public void receivePeerValue(byte[] peerValue) throws IOException + { + this.x25519PeerPublicKey = Arrays.copyOfRange(peerValue, peerValue.length - X25519.POINT_SIZE, peerValue.length); + byte[] mlkemValue = Arrays.copyOf(peerValue, peerValue.length - X25519.POINT_SIZE); + + if (domain.isServer()) + { + this.mlkemPublicKey = domain.getMLKemDomain().decodePublicKey(mlkemValue); + } + else + { + this.mlkemSecret = domain.getMLKemDomain().decapsulate(mlkemPrivateKey, mlkemValue); + this.mlkemPrivateKey = null; + } + } + + public TlsSecret calculateSecret() throws IOException + { + byte[] x25519Secret = domain.calculateX25519Secret(x25519PrivateKey, x25519PeerPublicKey); + TlsSecret secret = domain.adoptLocalSecret(Arrays.concatenate(mlkemSecret, x25519Secret)); + this.x25519PrivateKey = null; + this.mlkemSecret = null; + return secret; + } +} diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsX25519MLKemDomain.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsX25519MLKemDomain.java new file mode 100644 index 0000000000..978e561396 --- /dev/null +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/bc/BcTlsX25519MLKemDomain.java @@ -0,0 +1,67 @@ +package org.bouncycastle.tls.crypto.impl.bc; + +import java.io.IOException; +import org.bouncycastle.math.ec.rfc7748.X25519; +import org.bouncycastle.tls.AlertDescription; +import org.bouncycastle.tls.TlsFatalAlert; +import org.bouncycastle.tls.crypto.TlsAgreement; +import org.bouncycastle.tls.crypto.TlsKemConfig; +import org.bouncycastle.tls.crypto.TlsKemDomain; + +public class BcTlsX25519MLKemDomain implements TlsKemDomain +{ + protected final BcTlsCrypto crypto; + protected final boolean isServer; + protected final BcTlsMLKemDomain mlkemDomain; + + public BcTlsX25519MLKemDomain(BcTlsCrypto crypto, TlsKemConfig kemConfig) + { + this.crypto = crypto; + this.mlkemDomain = new BcTlsMLKemDomain(crypto, kemConfig); + this.isServer = kemConfig.isServer(); + } + + public BcTlsSecret adoptLocalSecret(byte[] secret) + { + return crypto.adoptLocalSecret(secret); + } + + public TlsAgreement createKem() + { + return new BcTlsX25519MLKem(this); + } + + public boolean isServer() + { + return isServer; + } + + public BcTlsMLKemDomain getMLKemDomain() + { + return mlkemDomain; + } + + public byte[] generateX25519PrivateKey() throws IOException + { + byte[] privateKey = new byte[X25519.SCALAR_SIZE]; + crypto.getSecureRandom().nextBytes(privateKey); + return privateKey; + } + + public byte[] getX25519PublicKey(byte[] privateKey) throws IOException + { + byte[] publicKey = new byte[X25519.POINT_SIZE]; + X25519.scalarMultBase(privateKey, 0, publicKey, 0); + return publicKey; + } + + public byte[] calculateX25519Secret(byte[] privateKey, byte[] peerPublicKey) throws IOException + { + byte[] secret = new byte[X25519.POINT_SIZE]; + if (!X25519.calculateAgreement(privateKey, 0, peerPublicKey, 0, secret, 0)) + { + throw new TlsFatalAlert(AlertDescription.handshake_failure); + } + return secret; + } +} diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaTlsCrypto.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaTlsCrypto.java index a42f4e201f..28b14a1d61 100644 --- a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaTlsCrypto.java +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JcaTlsCrypto.java @@ -458,6 +458,9 @@ else if (NamedGroup.refersToASpecificKem(namedGroup)) case NamedGroup.MLKEM512: case NamedGroup.MLKEM768: case NamedGroup.MLKEM1024: + case NamedGroup.SecP256r1MLKEM768: + case NamedGroup.X25519MLKEM768: + case NamedGroup.SecP384r1MLKEM1024: return null; } } @@ -862,7 +865,16 @@ public TlsECDomain createECDomain(TlsECConfig ecConfig) public TlsKemDomain createKemDomain(TlsKemConfig kemConfig) { - return new JceTlsMLKemDomain(this, kemConfig); + switch (kemConfig.getNamedGroup()) + { + case NamedGroup.SecP256r1MLKEM768: + case NamedGroup.SecP384r1MLKEM1024: + return new JceTlsECDHMLKemDomain(this, kemConfig); + case NamedGroup.X25519MLKEM768: + return new JceTlsX25519MLKemDomain(this, kemConfig); + default: + return new JceTlsMLKemDomain(this, kemConfig); + } } public TlsSecret hkdfInit(int cryptoHashAlgorithm) diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsECDHMLKem.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsECDHMLKem.java new file mode 100644 index 0000000000..533200f6d5 --- /dev/null +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsECDHMLKem.java @@ -0,0 +1,75 @@ +package org.bouncycastle.tls.crypto.impl.jcajce; + +import java.io.IOException; +import java.security.KeyPair; +import java.security.PublicKey; + +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.SecretWithEncapsulation; +import org.bouncycastle.pqc.crypto.mlkem.MLKEMPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.mlkem.MLKEMPublicKeyParameters; +import org.bouncycastle.tls.crypto.TlsAgreement; +import org.bouncycastle.tls.crypto.TlsSecret; +import org.bouncycastle.util.Arrays; + +public class JceTlsECDHMLKem implements TlsAgreement +{ + protected final JceTlsECDHMLKemDomain domain; + + protected KeyPair ecLocalKeyPair; + protected PublicKey ecPeerPublicKey; + protected MLKEMPrivateKeyParameters mlkemPrivateKey; + protected MLKEMPublicKeyParameters mlkemPublicKey; + protected byte[] mlkemSecret; + + public JceTlsECDHMLKem(JceTlsECDHMLKemDomain domain) + { + this.domain = domain; + } + + public byte[] generateEphemeral() throws IOException + { + this.ecLocalKeyPair = domain.getECDomain().generateKeyPair(); + byte[] ecPublicKey = domain.getECDomain().encodePublicKey(ecLocalKeyPair.getPublic()); + + if (domain.isServer()) + { + SecretWithEncapsulation encap = domain.getMLKemDomain().encapsulate(mlkemPublicKey); + this.mlkemPublicKey = null; + this.mlkemSecret = encap.getSecret(); + byte[] mlkemValue = encap.getEncapsulation(); + return Arrays.concatenate(ecPublicKey, mlkemValue); + } + else + { + AsymmetricCipherKeyPair kp = domain.getMLKemDomain().generateKeyPair(); + this.mlkemPrivateKey = (MLKEMPrivateKeyParameters)kp.getPrivate(); + byte[] mlkemValue = domain.getMLKemDomain().encodePublicKey((MLKEMPublicKeyParameters)kp.getPublic()); + return Arrays.concatenate(ecPublicKey, mlkemValue); + } + } + + public void receivePeerValue(byte[] peerValue) throws IOException + { + this.ecPeerPublicKey = domain.getECDomain().decodePublicKey(Arrays.copyOf(peerValue, domain.getECDomain().getPublicKeyByteLength())); + byte[] mlkemValue = Arrays.copyOfRange(peerValue, domain.getECDomain().getPublicKeyByteLength(), peerValue.length); + + if (domain.isServer()) + { + this.mlkemPublicKey = domain.getMLKemDomain().decodePublicKey(mlkemValue); + } + else + { + this.mlkemSecret = domain.getMLKemDomain().decapsulate(mlkemPrivateKey, mlkemValue); + this.mlkemPrivateKey = null; + } + } + + public TlsSecret calculateSecret() throws IOException + { + byte[] ecSecret = domain.getECDomain().calculateECDHAgreementBytes(ecLocalKeyPair.getPrivate(), ecPeerPublicKey); + TlsSecret secret = domain.adoptLocalSecret(Arrays.concatenate(ecSecret, mlkemSecret)); + this.mlkemSecret = null; + return secret; + } +} diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsECDHMLKemDomain.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsECDHMLKemDomain.java new file mode 100644 index 0000000000..cb9f9f756c --- /dev/null +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsECDHMLKemDomain.java @@ -0,0 +1,61 @@ +package org.bouncycastle.tls.crypto.impl.jcajce; + +import org.bouncycastle.tls.NamedGroup; +import org.bouncycastle.tls.crypto.TlsAgreement; +import org.bouncycastle.tls.crypto.TlsECConfig; +import org.bouncycastle.tls.crypto.TlsKemConfig; +import org.bouncycastle.tls.crypto.TlsKemDomain; + +public class JceTlsECDHMLKemDomain implements TlsKemDomain +{ + protected final JcaTlsCrypto crypto; + protected final boolean isServer; + private final JceTlsECDomain ecDomain; + private final JceTlsMLKemDomain mlkemDomain; + + public JceTlsECDHMLKemDomain(JcaTlsCrypto crypto, TlsKemConfig kemConfig) + { + this.crypto = crypto; + this.ecDomain = getJceTlsECDomain(crypto, kemConfig); + this.mlkemDomain = new JceTlsMLKemDomain(crypto, kemConfig); + this.isServer = kemConfig.isServer(); + } + + public JceTlsSecret adoptLocalSecret(byte[] secret) + { + return crypto.adoptLocalSecret(secret); + } + + public TlsAgreement createKem() + { + return new JceTlsECDHMLKem(this); + } + + public boolean isServer() + { + return isServer; + } + + public JceTlsECDomain getECDomain() + { + return ecDomain; + } + + public JceTlsMLKemDomain getMLKemDomain() + { + return mlkemDomain; + } + + private JceTlsECDomain getJceTlsECDomain(JcaTlsCrypto crypto, TlsKemConfig kemConfig) + { + switch (kemConfig.getNamedGroup()) + { + case NamedGroup.SecP256r1MLKEM768: + return new JceTlsECDomain(crypto, new TlsECConfig(NamedGroup.secp256r1)); + case NamedGroup.SecP384r1MLKEM1024: + return new JceTlsECDomain(crypto, new TlsECConfig(NamedGroup.secp384r1)); + default: + return null; + } + } +} diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsECDomain.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsECDomain.java index 8c332cbf04..11c92c159b 100644 --- a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsECDomain.java +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsECDomain.java @@ -52,6 +52,23 @@ public JceTlsECDomain(JcaTlsCrypto crypto, TlsECConfig ecConfig) throw new IllegalArgumentException("NamedGroup not supported: " + NamedGroup.getText(namedGroup)); } + public int getPublicKeyByteLength() + { + return (((ecCurve.getFieldSize() + 7) / 8) * 2) + 1; + } + + public byte[] calculateECDHAgreementBytes(PrivateKey privateKey, PublicKey publicKey) throws IOException + { + try + { + return crypto.calculateKeyAgreement("ECDH", privateKey, publicKey, "TlsPremasterSecret"); + } + catch (GeneralSecurityException e) + { + throw new TlsCryptoException("cannot calculate secret", e); + } + } + public JceTlsSecret calculateECDHAgreement(PrivateKey privateKey, PublicKey publicKey) throws IOException { diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsMLKem.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsMLKem.java index 44f73f2bbb..7bfef57d17 100644 --- a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsMLKem.java +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsMLKem.java @@ -47,7 +47,7 @@ public void receivePeerValue(byte[] peerValue) throws IOException } else { - this.secret = domain.decapsulate(privateKey, peerValue); + this.secret = domain.adoptLocalSecret(domain.decapsulate(privateKey, peerValue)); this.privateKey = null; } } diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsMLKemDomain.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsMLKemDomain.java index 5aaf97b7db..f303e994c9 100644 --- a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsMLKemDomain.java +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsMLKemDomain.java @@ -25,9 +25,12 @@ public static MLKEMParameters getDomainParameters(TlsKemConfig kemConfig) return MLKEMParameters.ml_kem_512; case NamedGroup.OQS_mlkem768: case NamedGroup.MLKEM768: + case NamedGroup.SecP256r1MLKEM768: + case NamedGroup.X25519MLKEM768: return MLKEMParameters.ml_kem_768; case NamedGroup.OQS_mlkem1024: case NamedGroup.MLKEM1024: + case NamedGroup.SecP384r1MLKEM1024: return MLKEMParameters.ml_kem_1024; default: throw new IllegalArgumentException("No ML-KEM configuration provided"); @@ -57,11 +60,10 @@ public TlsAgreement createKem() return new JceTlsMLKem(this); } - public JceTlsSecret decapsulate(MLKEMPrivateKeyParameters privateKey, byte[] ciphertext) + public byte[] decapsulate(MLKEMPrivateKeyParameters privateKey, byte[] ciphertext) { MLKEMExtractor kemExtract = new MLKEMExtractor(privateKey); - byte[] secret = kemExtract.extractSecret(ciphertext); - return adoptLocalSecret(secret); + return kemExtract.extractSecret(ciphertext); } public MLKEMPublicKeyParameters decodePublicKey(byte[] encoding) diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsX25519MLKem.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsX25519MLKem.java new file mode 100644 index 0000000000..c817447feb --- /dev/null +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsX25519MLKem.java @@ -0,0 +1,76 @@ +package org.bouncycastle.tls.crypto.impl.jcajce; + +import java.io.IOException; +import java.security.KeyPair; +import java.security.PublicKey; + +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.SecretWithEncapsulation; +import org.bouncycastle.math.ec.rfc7748.X25519; +import org.bouncycastle.pqc.crypto.mlkem.MLKEMPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.mlkem.MLKEMPublicKeyParameters; +import org.bouncycastle.tls.crypto.TlsAgreement; +import org.bouncycastle.tls.crypto.TlsSecret; +import org.bouncycastle.util.Arrays; + +public class JceTlsX25519MLKem implements TlsAgreement +{ + protected final JceTlsX25519MLKemDomain domain; + + protected KeyPair x25519LocalKeyPair; + protected PublicKey x25519PeerPublicKey; + protected MLKEMPrivateKeyParameters mlkemPrivateKey; + protected MLKEMPublicKeyParameters mlkemPublicKey; + protected byte[] mlkemSecret; + + public JceTlsX25519MLKem(JceTlsX25519MLKemDomain domain) + { + this.domain = domain; + } + + public byte[] generateEphemeral() throws IOException + { + this.x25519LocalKeyPair = domain.generateX25519KeyPair(); + byte[] x25519PublicKey = domain.encodeX25519PublicKey(x25519LocalKeyPair.getPublic()); + + if (domain.isServer()) + { + SecretWithEncapsulation encap = domain.getMLKemDomain().encapsulate(mlkemPublicKey); + this.mlkemPublicKey = null; + this.mlkemSecret = encap.getSecret(); + byte[] mlkemValue = encap.getEncapsulation(); + return Arrays.concatenate(mlkemValue, x25519PublicKey); + } + else + { + AsymmetricCipherKeyPair kp = domain.getMLKemDomain().generateKeyPair(); + this.mlkemPrivateKey = (MLKEMPrivateKeyParameters)kp.getPrivate(); + byte[] mlkemValue = domain.getMLKemDomain().encodePublicKey((MLKEMPublicKeyParameters)kp.getPublic()); + return Arrays.concatenate(mlkemValue, x25519PublicKey); + } + } + + public void receivePeerValue(byte[] peerValue) throws IOException + { + this.x25519PeerPublicKey = domain.decodeX25519PublicKey(Arrays.copyOfRange(peerValue, peerValue.length - X25519.POINT_SIZE, peerValue.length)); + byte[] mlkemValue = Arrays.copyOf(peerValue, peerValue.length - X25519.POINT_SIZE); + + if (domain.isServer()) + { + this.mlkemPublicKey = domain.getMLKemDomain().decodePublicKey(mlkemValue); + } + else + { + this.mlkemSecret = domain.getMLKemDomain().decapsulate(mlkemPrivateKey, mlkemValue); + this.mlkemPrivateKey = null; + } + } + + public TlsSecret calculateSecret() throws IOException + { + byte[] x25519Secret = domain.calculateX25519Agreement(x25519LocalKeyPair.getPrivate(), x25519PeerPublicKey); + TlsSecret secret = domain.adoptLocalSecret(Arrays.concatenate(mlkemSecret, x25519Secret)); + this.mlkemSecret = null; + return secret; + } +} diff --git a/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsX25519MLKemDomain.java b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsX25519MLKemDomain.java new file mode 100644 index 0000000000..4c8d26d688 --- /dev/null +++ b/tls/src/main/java/org/bouncycastle/tls/crypto/impl/jcajce/JceTlsX25519MLKemDomain.java @@ -0,0 +1,96 @@ +package org.bouncycastle.tls.crypto.impl.jcajce; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; + +import org.bouncycastle.math.ec.rfc7748.X25519; +import org.bouncycastle.tls.AlertDescription; +import org.bouncycastle.tls.TlsFatalAlert; +import org.bouncycastle.tls.crypto.TlsAgreement; +import org.bouncycastle.tls.crypto.TlsCryptoException; +import org.bouncycastle.tls.crypto.TlsKemConfig; +import org.bouncycastle.tls.crypto.TlsKemDomain; +import org.bouncycastle.util.Arrays; + +public class JceTlsX25519MLKemDomain implements TlsKemDomain +{ + protected final JcaTlsCrypto crypto; + protected final boolean isServer; + private final JceX25519Domain x25519Domain; + private final JceTlsMLKemDomain mlkemDomain; + + public JceTlsX25519MLKemDomain(JcaTlsCrypto crypto, TlsKemConfig kemConfig) + { + this.crypto = crypto; + this.x25519Domain = new JceX25519Domain(crypto); + this.mlkemDomain = new JceTlsMLKemDomain(crypto, kemConfig); + this.isServer = kemConfig.isServer(); + } + + public JceTlsSecret adoptLocalSecret(byte[] secret) + { + return crypto.adoptLocalSecret(secret); + } + + public TlsAgreement createKem() + { + return new JceTlsX25519MLKem(this); + } + + public boolean isServer() + { + return isServer; + } + + public JceTlsMLKemDomain getMLKemDomain() + { + return mlkemDomain; + } + + + public KeyPair generateX25519KeyPair() + { + try + { + return x25519Domain.generateKeyPair(); + } + catch (Exception e) + { + throw Exceptions.illegalStateException("unable to create key pair: " + e.getMessage(), e); + } + } + + public byte[] encodeX25519PublicKey(PublicKey publicKey) throws IOException + { + return XDHUtil.encodePublicKey(publicKey); + } + + public PublicKey decodeX25519PublicKey(byte[] x25519Key) throws IOException + { + return x25519Domain.decodePublicKey(x25519Key); + } + + public byte[] calculateX25519Agreement(PrivateKey privateKey, PublicKey publicKey) throws IOException + { + try + { + byte[] secret = crypto.calculateKeyAgreement("X25519", privateKey, publicKey, "TlsPremasterSecret"); + if (secret == null || secret.length != 32) + { + throw new TlsCryptoException("invalid secret calculated"); + } + if (Arrays.areAllZeroes(secret, 0, secret.length)) + { + throw new TlsFatalAlert(AlertDescription.handshake_failure); + } + return secret; + } + catch (GeneralSecurityException e) + { + throw new TlsCryptoException("cannot calculate secret", e); + } + } +} diff --git a/tls/src/test/java/org/bouncycastle/tls/test/MockTlsKemClient.java b/tls/src/test/java/org/bouncycastle/tls/test/MockTlsKemClient.java index 6e799c9511..d1a19c1cad 100644 --- a/tls/src/test/java/org/bouncycastle/tls/test/MockTlsKemClient.java +++ b/tls/src/test/java/org/bouncycastle/tls/test/MockTlsKemClient.java @@ -42,6 +42,9 @@ class MockTlsKemClient NamedGroup.MLKEM512, NamedGroup.MLKEM768, NamedGroup.MLKEM1024, + NamedGroup.SecP256r1MLKEM768, + NamedGroup.X25519MLKEM768, + NamedGroup.SecP384r1MLKEM1024, }; MockTlsKemClient(TlsSession session) diff --git a/tls/src/test/java/org/bouncycastle/tls/test/MockTlsKemServer.java b/tls/src/test/java/org/bouncycastle/tls/test/MockTlsKemServer.java index 5a41afd9fe..2df3d52304 100644 --- a/tls/src/test/java/org/bouncycastle/tls/test/MockTlsKemServer.java +++ b/tls/src/test/java/org/bouncycastle/tls/test/MockTlsKemServer.java @@ -34,6 +34,9 @@ class MockTlsKemServer NamedGroup.MLKEM512, NamedGroup.MLKEM768, NamedGroup.MLKEM1024, + NamedGroup.SecP256r1MLKEM768, + NamedGroup.X25519MLKEM768, + NamedGroup.SecP384r1MLKEM1024, NamedGroup.x25519, };