8314323: Implement JEP 527: TLS 1.3 Hybrid Key Exchange

Co-authored-by: Jamil Nimeh <jnimeh@openjdk.org>
Co-authored-by: Weijun Wang <weijun@openjdk.org>
Reviewed-by: wetmore, mullan
This commit is contained in:
Hai-May Chao 2026-01-20 16:16:38 +00:00
parent 5ba91fed34
commit 21dc41f744
22 changed files with 1839 additions and 120 deletions

View File

@ -0,0 +1,254 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package sun.security.ssl;
import sun.security.util.ArrayUtil;
import sun.security.util.CurveDB;
import sun.security.util.ECUtil;
import sun.security.util.NamedCurve;
import javax.crypto.DecapsulateException;
import javax.crypto.KEM;
import javax.crypto.KEMSpi;
import javax.crypto.KeyAgreement;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.math.BigInteger;
import java.security.*;
import java.security.interfaces.ECKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.XECKey;
import java.security.interfaces.XECPublicKey;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.NamedParameterSpec;
import java.security.spec.XECPublicKeySpec;
import java.util.Arrays;
/**
* The DHasKEM class presents a KEM abstraction layer over traditional
* DH-based key exchange, which can be used for either straight
* ECDH/XDH or TLS hybrid key exchanges.
*
* This class can be alongside standard full post-quantum KEMs
* when hybrid implementations are required.
*/
public class DHasKEM implements KEMSpi {
@Override
public EncapsulatorSpi engineNewEncapsulator(
PublicKey publicKey, AlgorithmParameterSpec spec,
SecureRandom secureRandom) throws InvalidKeyException {
return new Handler(publicKey, null, secureRandom);
}
@Override
public DecapsulatorSpi engineNewDecapsulator(PrivateKey privateKey,
AlgorithmParameterSpec spec) throws InvalidKeyException {
return new Handler(null, privateKey, null);
}
private static final class Handler
implements KEMSpi.EncapsulatorSpi, KEMSpi.DecapsulatorSpi {
private final PublicKey pkR;
private final PrivateKey skR;
private final SecureRandom sr;
private final Params params;
Handler(PublicKey pk, PrivateKey sk, SecureRandom sr)
throws InvalidKeyException {
this.pkR = pk;
this.skR = sk;
this.sr = sr;
this.params = paramsFromKey(pk == null ? sk : pk);
}
@Override
public KEM.Encapsulated engineEncapsulate(int from, int to,
String algorithm) {
KeyPair kpE = params.generateKeyPair(sr);
PrivateKey skE = kpE.getPrivate();
PublicKey pkE = kpE.getPublic();
byte[] pkEm = params.SerializePublicKey(pkE);
try {
SecretKey dh = params.DH(algorithm, skE, pkR);
return new KEM.Encapsulated(
sub(dh, from, to),
pkEm, null);
} catch (Exception e) {
throw new ProviderException("internal error", e);
}
}
@Override
public int engineSecretSize() {
return params.secretLen;
}
@Override
public int engineEncapsulationSize() {
return params.publicKeyLen;
}
@Override
public SecretKey engineDecapsulate(byte[] encapsulation, int from,
int to, String algorithm) throws DecapsulateException {
if (encapsulation.length != params.publicKeyLen) {
throw new DecapsulateException("incorrect encapsulation size");
}
try {
PublicKey pkE = params.DeserializePublicKey(encapsulation);
SecretKey dh = params.DH(algorithm, skR, pkE);
return sub(dh, from, to);
} catch (IOException | InvalidKeyException e) {
throw new DecapsulateException("Cannot decapsulate", e);
} catch (Exception e) {
throw new ProviderException("internal error", e);
}
}
private SecretKey sub(SecretKey key, int from, int to) {
if (from == 0 && to == params.secretLen) {
return key;
}
// Key slicing should never happen. Otherwise, there might be
// a programming error.
throw new AssertionError(
"Unexpected key slicing: from=" + from + ", to=" + to);
}
// This KEM is designed to be able to represent every ECDH and XDH
private Params paramsFromKey(Key k) throws InvalidKeyException {
if (k instanceof ECKey eckey) {
if (ECUtil.equals(eckey.getParams(), CurveDB.P_256)) {
return Params.P256;
} else if (ECUtil.equals(eckey.getParams(), CurveDB.P_384)) {
return Params.P384;
} else if (ECUtil.equals(eckey.getParams(), CurveDB.P_521)) {
return Params.P521;
}
} else if (k instanceof XECKey xkey
&& xkey.getParams() instanceof NamedParameterSpec ns) {
if (ns.getName().equalsIgnoreCase(
NamedParameterSpec.X25519.getName())) {
return Params.X25519;
} else if (ns.getName().equalsIgnoreCase(
NamedParameterSpec.X448.getName())) {
return Params.X448;
}
}
throw new InvalidKeyException("Unsupported key");
}
}
private enum Params {
P256(32, 2 * 32 + 1,
"ECDH", "EC", CurveDB.P_256),
P384(48, 2 * 48 + 1,
"ECDH", "EC", CurveDB.P_384),
P521(66, 2 * 66 + 1,
"ECDH", "EC", CurveDB.P_521),
X25519(32, 32,
"XDH", "XDH", NamedParameterSpec.X25519),
X448(56, 56,
"XDH", "XDH", NamedParameterSpec.X448);
private final int secretLen;
private final int publicKeyLen;
private final String kaAlgorithm;
private final String keyAlgorithm;
private final AlgorithmParameterSpec spec;
Params(int secretLen, int publicKeyLen, String kaAlgorithm,
String keyAlgorithm, AlgorithmParameterSpec spec) {
this.spec = spec;
this.secretLen = secretLen;
this.publicKeyLen = publicKeyLen;
this.kaAlgorithm = kaAlgorithm;
this.keyAlgorithm = keyAlgorithm;
}
private boolean isEC() {
return this == P256 || this == P384 || this == P521;
}
private KeyPair generateKeyPair(SecureRandom sr) {
try {
KeyPairGenerator g = KeyPairGenerator.getInstance(keyAlgorithm);
g.initialize(spec, sr);
return g.generateKeyPair();
} catch (Exception e) {
throw new ProviderException("internal error", e);
}
}
private byte[] SerializePublicKey(PublicKey k) {
if (isEC()) {
ECPoint w = ((ECPublicKey) k).getW();
return ECUtil.encodePoint(w, ((NamedCurve) spec).getCurve());
} else {
byte[] uArray = ((XECPublicKey) k).getU().toByteArray();
ArrayUtil.reverse(uArray);
return Arrays.copyOf(uArray, publicKeyLen);
}
}
private PublicKey DeserializePublicKey(byte[] data) throws
IOException, NoSuchAlgorithmException,
InvalidKeySpecException {
KeySpec keySpec;
if (isEC()) {
NamedCurve curve = (NamedCurve) this.spec;
keySpec = new ECPublicKeySpec(
ECUtil.decodePoint(data, curve.getCurve()), curve);
} else {
data = data.clone();
ArrayUtil.reverse(data);
keySpec = new XECPublicKeySpec(
this.spec, new BigInteger(1, data));
}
return KeyFactory.getInstance(keyAlgorithm).
generatePublic(keySpec);
}
private SecretKey DH(String alg, PrivateKey skE, PublicKey pkR)
throws NoSuchAlgorithmException, InvalidKeyException {
KeyAgreement ka = KeyAgreement.getInstance(kaAlgorithm);
ka.init(skE);
ka.doPhase(pkR, true);
return ka.generateSecret(alg);
}
}
}

View File

@ -0,0 +1,474 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package sun.security.ssl;
import sun.security.util.ArrayUtil;
import sun.security.util.CurveDB;
import sun.security.util.ECUtil;
import sun.security.util.RawKeySpec;
import sun.security.x509.X509Key;
import javax.crypto.DecapsulateException;
import javax.crypto.KEM;
import javax.crypto.KEMSpi;
import javax.crypto.SecretKey;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyFactorySpi;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyPairGeneratorSpi;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.ProviderException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.*;
import java.util.Arrays;
import java.util.Locale;
// The Hybrid class wraps two underlying algorithms (left and right sides)
// in a single TLS hybrid named group.
// It implements:
// - Hybrid KeyPair generation
// - Hybrid KeyFactory for decoding concatenated hybrid public keys
// - Hybrid KEM implementation for performing encapsulation and
// decapsulation over two underlying algorithms (traditional
// algorithm and post-quantum KEM algorithm)
public class Hybrid {
public static final NamedParameterSpec X25519_MLKEM768 =
new NamedParameterSpec("X25519MLKEM768");
public static final NamedParameterSpec SECP256R1_MLKEM768 =
new NamedParameterSpec("SecP256r1MLKEM768");
public static final NamedParameterSpec SECP384R1_MLKEM1024 =
new NamedParameterSpec("SecP384r1MLKEM1024");
private static AlgorithmParameterSpec getSpec(String name) {
if (name.startsWith("secp")) {
return new ECGenParameterSpec(name);
} else {
return new NamedParameterSpec(name);
}
}
private static KeyPairGenerator getKeyPairGenerator(String name) throws
NoSuchAlgorithmException {
if (name.startsWith("secp")) {
name = "EC";
}
return KeyPairGenerator.getInstance(name);
}
private static KeyFactory getKeyFactory(String name) throws
NoSuchAlgorithmException {
if (name.startsWith("secp")) {
name = "EC";
}
return KeyFactory.getInstance(name);
}
/**
* Returns a KEM instance for each side of the hybrid algorithm.
* For traditional key exchange algorithms, we use the DH-based KEM
* implementation provided by DHasKEM class.
* For ML-KEM post-quantum algorithms, we obtain a KEM instance
* with "ML-KEM". This is done to work with 3rd-party providers that
* only have "ML-KEM" KEM algorithm.
*/
private static KEM getKEM(String name) throws NoSuchAlgorithmException {
if (name.startsWith("secp") || name.equals("X25519")) {
return KEM.getInstance("DH", HybridProvider.PROVIDER);
} else {
return KEM.getInstance("ML-KEM");
}
}
public static class KeyPairGeneratorImpl extends KeyPairGeneratorSpi {
private final KeyPairGenerator left;
private final KeyPairGenerator right;
private final AlgorithmParameterSpec leftSpec;
private final AlgorithmParameterSpec rightSpec;
public KeyPairGeneratorImpl(String leftAlg, String rightAlg)
throws NoSuchAlgorithmException {
left = getKeyPairGenerator(leftAlg);
right = getKeyPairGenerator(rightAlg);
leftSpec = getSpec(leftAlg);
rightSpec = getSpec(rightAlg);
}
@Override
public void initialize(AlgorithmParameterSpec params,
SecureRandom random)
throws InvalidAlgorithmParameterException {
left.initialize(leftSpec, random);
right.initialize(rightSpec, random);
}
@Override
public void initialize(int keysize, SecureRandom random) {
// NO-OP (do nothing)
}
@Override
public KeyPair generateKeyPair() {
var kp1 = left.generateKeyPair();
var kp2 = right.generateKeyPair();
return new KeyPair(
new PublicKeyImpl("Hybrid", kp1.getPublic(),
kp2.getPublic()),
new PrivateKeyImpl("Hybrid", kp1.getPrivate(),
kp2.getPrivate()));
}
}
public static class KeyFactoryImpl extends KeyFactorySpi {
private final KeyFactory left;
private final KeyFactory right;
private final int leftlen;
private final String leftname;
private final String rightname;
public KeyFactoryImpl(String left, String right)
throws NoSuchAlgorithmException {
this.left = getKeyFactory(left);
this.right = getKeyFactory(right);
this.leftlen = leftPublicLength(left);
this.leftname = left;
this.rightname = right;
}
@Override
protected PublicKey engineGeneratePublic(KeySpec keySpec)
throws InvalidKeySpecException {
if (keySpec == null) {
throw new InvalidKeySpecException("keySpec must not be null");
}
if (keySpec instanceof RawKeySpec rks) {
byte[] key = rks.getKeyArr();
if (key == null) {
throw new InvalidKeySpecException(
"RawkeySpec contains null key data");
}
if (key.length <= leftlen) {
throw new InvalidKeySpecException(
"Hybrid key length " + key.length +
" is too short and its left key length is " +
leftlen);
}
byte[] leftKeyBytes = Arrays.copyOfRange(key, 0, leftlen);
byte[] rightKeyBytes = Arrays.copyOfRange(key, leftlen,
key.length);
PublicKey leftKey, rightKey;
try {
if (leftname.startsWith("secp")) {
var curve = CurveDB.lookup(leftname);
var ecSpec = new ECPublicKeySpec(
ECUtil.decodePoint(leftKeyBytes,
curve.getCurve()), curve);
leftKey = left.generatePublic(ecSpec);
} else if (leftname.startsWith("ML-KEM")) {
leftKey = left.generatePublic(new RawKeySpec(
leftKeyBytes));
} else {
throw new InvalidKeySpecException("Unsupported left" +
" algorithm" + leftname);
}
if (rightname.equals("X25519")) {
ArrayUtil.reverse(rightKeyBytes);
var xecSpec = new XECPublicKeySpec(
new NamedParameterSpec(rightname),
new BigInteger(1, rightKeyBytes));
rightKey = right.generatePublic(xecSpec);
} else if (rightname.startsWith("ML-KEM")) {
rightKey = right.generatePublic(new RawKeySpec(
rightKeyBytes));
} else {
throw new InvalidKeySpecException("Unsupported right" +
" algorithm: " + rightname);
}
return new PublicKeyImpl("Hybrid", leftKey, rightKey);
} catch (Exception e) {
throw new InvalidKeySpecException("Failed to decode " +
"hybrid key", e);
}
}
throw new InvalidKeySpecException(
"KeySpec type:" +
keySpec.getClass().getName() + " not supported");
}
private static int leftPublicLength(String name) {
return switch (name.toLowerCase(Locale.ROOT)) {
case "secp256r1" -> 65;
case "secp384r1" -> 97;
case "ml-kem-768" -> 1184;
default -> throw new IllegalArgumentException(
"Unknown named group: " + name);
};
}
@Override
protected PrivateKey engineGeneratePrivate(KeySpec keySpec) throws
InvalidKeySpecException {
throw new UnsupportedOperationException();
}
@Override
protected <T extends KeySpec> T engineGetKeySpec(Key key,
Class<T> keySpec) throws InvalidKeySpecException {
throw new UnsupportedOperationException();
}
@Override
protected Key engineTranslateKey(Key key) throws InvalidKeyException {
throw new UnsupportedOperationException();
}
}
public static class KEMImpl implements KEMSpi {
private final KEM left;
private final KEM right;
public KEMImpl(String left, String right)
throws NoSuchAlgorithmException {
this.left = getKEM(left);
this.right = getKEM(right);
}
@Override
public EncapsulatorSpi engineNewEncapsulator(PublicKey publicKey,
AlgorithmParameterSpec spec, SecureRandom secureRandom) throws
InvalidAlgorithmParameterException, InvalidKeyException {
if (publicKey instanceof PublicKeyImpl pk) {
return new Handler(left.newEncapsulator(pk.left, secureRandom),
right.newEncapsulator(pk.right, secureRandom),
null, null);
}
throw new InvalidKeyException();
}
@Override
public DecapsulatorSpi engineNewDecapsulator(PrivateKey privateKey,
AlgorithmParameterSpec spec)
throws InvalidAlgorithmParameterException, InvalidKeyException {
if (privateKey instanceof PrivateKeyImpl pk) {
return new Handler(null, null, left.newDecapsulator(pk.left),
right.newDecapsulator(pk.right));
}
throw new InvalidKeyException();
}
}
private static byte[] concat(byte[]... inputs) {
int outLen = 0;
for (byte[] in : inputs) {
outLen += in.length;
}
byte[] out = new byte[outLen];
int pos = 0;
for (byte[] in : inputs) {
System.arraycopy(in, 0, out, pos, in.length);
pos += in.length;
}
return out;
}
private record Handler(KEM.Encapsulator le, KEM.Encapsulator re,
KEM.Decapsulator ld, KEM.Decapsulator rd)
implements KEMSpi.EncapsulatorSpi, KEMSpi.DecapsulatorSpi {
@Override
public KEM.Encapsulated engineEncapsulate(int from, int to,
String algorithm) {
int expectedSecretSize = engineSecretSize();
if (!(from == 0 && to == expectedSecretSize)) {
throw new IllegalArgumentException(
"Invalid range for encapsulation: from = " + from +
" to = " + to + ", expected total secret size = " +
expectedSecretSize);
}
var left = le.encapsulate();
var right = re.encapsulate();
return new KEM.Encapsulated(
new SecretKeyImpl(left.key(), right.key()),
concat(left.encapsulation(), right.encapsulation()),
null);
}
@Override
public int engineSecretSize() {
if (le != null) {
return le.secretSize() + re.secretSize();
} else {
return ld.secretSize() + rd.secretSize();
}
}
@Override
public int engineEncapsulationSize() {
if (le != null) {
return le.encapsulationSize() + re.encapsulationSize();
} else {
return ld.encapsulationSize() + rd.encapsulationSize();
}
}
@Override
public SecretKey engineDecapsulate(byte[] encapsulation, int from,
int to, String algorithm) throws DecapsulateException {
int expectedEncSize = engineEncapsulationSize();
if (encapsulation.length != expectedEncSize) {
throw new IllegalArgumentException(
"Invalid key encapsulation message length: " +
encapsulation.length +
", expected = " + expectedEncSize);
}
int expectedSecretSize = engineSecretSize();
if (!(from == 0 && to == expectedSecretSize)) {
throw new IllegalArgumentException(
"Invalid range for decapsulation: from = " + from +
" to = " + to + ", expected total secret size = " +
expectedSecretSize);
}
var left = Arrays.copyOf(encapsulation, ld.encapsulationSize());
var right = Arrays.copyOfRange(encapsulation,
ld.encapsulationSize(), encapsulation.length);
return new SecretKeyImpl(
ld.decapsulate(left),
rd.decapsulate(right)
);
}
}
// Package-private
record SecretKeyImpl(SecretKey k1, SecretKey k2)
implements SecretKey {
@Override
public String getAlgorithm() {
return "Generic";
}
@Override
public String getFormat() {
return null;
}
@Override
public byte[] getEncoded() {
return null;
}
}
/**
* Hybrid public key combines two underlying public keys (left and right).
* Public keys can be transmitted/encoded because the hybrid protocol
* requires the public component to be sent.
*/
// Package-private
record PublicKeyImpl(String algorithm, PublicKey left,
PublicKey right) implements PublicKey {
@Override
public String getAlgorithm() {
return algorithm;
}
// getFormat() returns "RAW" as hybrid key uses RAW concatenation
// of underlying encodings.
@Override
public String getFormat() {
return "RAW";
}
// getEncoded() returns the concatenation of the encoded bytes of the
// left and right public keys.
@Override
public byte[] getEncoded() {
return concat(onlyKey(left), onlyKey(right));
}
static byte[] onlyKey(PublicKey key) {
if (key instanceof X509Key xk) {
return xk.getKeyAsBytes();
}
// Fallback for 3rd-party providers
if (!"X.509".equalsIgnoreCase(key.getFormat())) {
throw new ProviderException("Invalid public key encoding " +
"format");
}
var xk = new X509Key();
try {
xk.decode(key.getEncoded());
} catch (InvalidKeyException e) {
throw new ProviderException("Invalid public key encoding", e);
}
return xk.getKeyAsBytes();
}
}
/**
* Hybrid private key combines two underlying private keys (left and right).
* It is for internal use only. The private keys should never be exported.
*/
private record PrivateKeyImpl(String algorithm, PrivateKey left,
PrivateKey right) implements PrivateKey {
@Override
public String getAlgorithm() {
return algorithm;
}
// getFormat() returns null because there is no standard
// format for a hybrid private key.
@Override
public String getFormat() {
return null;
}
// getEncoded() returns an empty byte array because there is no
// standard encoding format for a hybrid private key.
@Override
public byte[] getEncoded() {
return null;
}
}
}

View File

@ -0,0 +1,130 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package sun.security.ssl;
import java.security.Provider;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map;
import static sun.security.util.SecurityConstants.PROVIDER_VER;
// This is an internal provider used in the JSSE code for DH-as-KEM
// and Hybrid KEM support. It doesn't actually get installed in the
// system's list of security providers that is searched at runtime.
// JSSE loads this provider internally.
// It registers Hybrid KeyPairGenerator, KeyFactory, and KEM
// implementations for hybrid named groups as Provider services.
public class HybridProvider {
public static final Provider PROVIDER = new ProviderImpl();
private static final class ProviderImpl extends Provider {
@java.io.Serial
private static final long serialVersionUID = 0L;
ProviderImpl() {
super("HybridAndDHAsKEM", PROVIDER_VER,
"Hybrid and DHAsKEM provider");
put("KEM.DH", DHasKEM.class.getName());
// Hybrid KeyPairGenerator/KeyFactory/KEM
// The order of shares in the concatenation for group name
// X25519MLKEM768 has been reversed as per the current
// draft RFC.
var attrs = Map.of("name", "X25519MLKEM768", "left", "ML-KEM-768",
"right", "X25519");
putService(new HybridService(this, "KeyPairGenerator",
"X25519MLKEM768",
"sun.security.ssl.Hybrid$KeyPairGeneratorImpl",
null, attrs));
putService(new HybridService(this, "KEM",
"X25519MLKEM768",
"sun.security.ssl.Hybrid$KEMImpl",
null, attrs));
putService(new HybridService(this, "KeyFactory",
"X25519MLKEM768",
"sun.security.ssl.Hybrid$KeyFactoryImpl",
null, attrs));
attrs = Map.of("name", "SecP256r1MLKEM768", "left", "secp256r1",
"right", "ML-KEM-768");
putService(new HybridService(this, "KeyPairGenerator",
"SecP256r1MLKEM768",
"sun.security.ssl.Hybrid$KeyPairGeneratorImpl",
null, attrs));
putService(new HybridService(this, "KEM",
"SecP256r1MLKEM768",
"sun.security.ssl.Hybrid$KEMImpl",
null, attrs));
putService(new HybridService(this, "KeyFactory",
"SecP256r1MLKEM768",
"sun.security.ssl.Hybrid$KeyFactoryImpl",
null, attrs));
attrs = Map.of("name", "SecP384r1MLKEM1024", "left", "secp384r1",
"right", "ML-KEM-1024");
putService(new HybridService(this, "KeyPairGenerator",
"SecP384r1MLKEM1024",
"sun.security.ssl.Hybrid$KeyPairGeneratorImpl",
null, attrs));
putService(new HybridService(this, "KEM",
"SecP384r1MLKEM1024",
"sun.security.ssl.Hybrid$KEMImpl",
null, attrs));
putService(new HybridService(this, "KeyFactory",
"SecP384r1MLKEM1024",
"sun.security.ssl.Hybrid$KeyFactoryImpl",
null, attrs));
}
}
private static class HybridService extends Provider.Service {
HybridService(Provider p, String type, String algo, String cn,
List<String> aliases, Map<String, String> attrs) {
super(p, type, algo, cn, aliases, attrs);
}
@Override
public Object newInstance(Object ctrParamObj)
throws NoSuchAlgorithmException {
String type = getType();
return switch (type) {
case "KeyPairGenerator" -> new Hybrid.KeyPairGeneratorImpl(
getAttribute("left"), getAttribute("right"));
case "KeyFactory" -> new Hybrid.KeyFactoryImpl(
getAttribute("left"), getAttribute("right"));
case "KEM" -> new Hybrid.KEMImpl(
getAttribute("left"), getAttribute("right"));
default -> throw new NoSuchAlgorithmException(
"Unexpected value: " + type);
};
}
}
}

View File

@ -24,7 +24,10 @@
*/
package sun.security.ssl;
import sun.security.util.RawKeySpec;
import javax.crypto.KDF;
import javax.crypto.KEM;
import javax.crypto.KeyAgreement;
import javax.crypto.SecretKey;
import javax.crypto.spec.HKDFParameterSpec;
@ -32,9 +35,11 @@ import javax.net.ssl.SSLHandshakeException;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.spec.AlgorithmParameterSpec;
import java.security.SecureRandom;
import sun.security.util.KeyUtil;
/**
@ -46,15 +51,32 @@ public class KAKeyDerivation implements SSLKeyDerivation {
private final HandshakeContext context;
private final PrivateKey localPrivateKey;
private final PublicKey peerPublicKey;
private final byte[] keyshare;
private final Provider provider;
// Constructor called by Key Agreement
KAKeyDerivation(String algorithmName,
HandshakeContext context,
PrivateKey localPrivateKey,
PublicKey peerPublicKey) {
this(algorithmName, null, context, localPrivateKey,
peerPublicKey, null);
}
// When the constructor called by KEM: store the client's public key or the
// encapsulated message in keyshare.
KAKeyDerivation(String algorithmName,
NamedGroup namedGroup,
HandshakeContext context,
PrivateKey localPrivateKey,
PublicKey peerPublicKey,
byte[] keyshare) {
this.algorithmName = algorithmName;
this.context = context;
this.localPrivateKey = localPrivateKey;
this.peerPublicKey = peerPublicKey;
this.keyshare = keyshare;
this.provider = (namedGroup != null) ? namedGroup.getProvider() : null;
}
@Override
@ -94,22 +116,15 @@ public class KAKeyDerivation implements SSLKeyDerivation {
}
}
/**
* Handle the TLSv1.3 objects, which use the HKDF algorithms.
*/
private SecretKey t13DeriveKey(String type)
throws IOException {
SecretKey sharedSecret = null;
private SecretKey deriveHandshakeSecret(String label,
SecretKey sharedSecret)
throws GeneralSecurityException, IOException {
SecretKey earlySecret = null;
SecretKey saltSecret = null;
try {
KeyAgreement ka = KeyAgreement.getInstance(algorithmName);
ka.init(localPrivateKey);
ka.doPhase(peerPublicKey, true);
sharedSecret = ka.generateSecret("TlsPremasterSecret");
CipherSuite.HashAlg hashAlg = context.negotiatedCipherSuite.hashAlg;
SSLKeyDerivation kd = context.handshakeKeyDerivation;
CipherSuite.HashAlg hashAlg = context.negotiatedCipherSuite.hashAlg;
SSLKeyDerivation kd = context.handshakeKeyDerivation;
try {
if (kd == null) { // No PSK is in use.
// If PSK is not in use, Early Secret will still be
// HKDF-Extract(0, 0).
@ -129,12 +144,90 @@ public class KAKeyDerivation implements SSLKeyDerivation {
// the handshake secret key derivation (below) as it may not
// work with the "sharedSecret" obj.
KDF hkdf = KDF.getInstance(hashAlg.hkdfAlgorithm);
return hkdf.deriveKey(type, HKDFParameterSpec.ofExtract()
.addSalt(saltSecret).addIKM(sharedSecret).extractOnly());
var spec = HKDFParameterSpec.ofExtract().addSalt(saltSecret);
if (sharedSecret instanceof Hybrid.SecretKeyImpl hsk) {
spec = spec.addIKM(hsk.k1()).addIKM(hsk.k2());
} else {
spec = spec.addIKM(sharedSecret);
}
return hkdf.deriveKey(label, spec.extractOnly());
} finally {
KeyUtil.destroySecretKeys(earlySecret, saltSecret);
}
}
/**
* This method is called by the server to perform KEM encapsulation.
* It uses the client's public key (sent by the client as a keyshare)
* to encapsulate a shared secret and returns the encapsulated message.
*
* Package-private, used from KeyShareExtension.SHKeyShareProducer::
* produce().
*/
KEM.Encapsulated encapsulate(String algorithm, SecureRandom random)
throws IOException {
SecretKey sharedSecret = null;
if (keyshare == null) {
throw new IOException("No keyshare available for KEM " +
"encapsulation");
}
try {
KeyFactory kf = (provider != null) ?
KeyFactory.getInstance(algorithmName, provider) :
KeyFactory.getInstance(algorithmName);
var pk = kf.generatePublic(new RawKeySpec(keyshare));
KEM kem = (provider != null) ?
KEM.getInstance(algorithmName, provider) :
KEM.getInstance(algorithmName);
KEM.Encapsulator e = kem.newEncapsulator(pk, random);
KEM.Encapsulated enc = e.encapsulate();
sharedSecret = enc.key();
SecretKey derived = deriveHandshakeSecret(algorithm, sharedSecret);
return new KEM.Encapsulated(derived, enc.encapsulation(), null);
} catch (GeneralSecurityException gse) {
throw new SSLHandshakeException("Could not generate secret", gse);
} finally {
KeyUtil.destroySecretKeys(sharedSecret, earlySecret, saltSecret);
KeyUtil.destroySecretKeys(sharedSecret);
}
}
/**
* Handle the TLSv1.3 objects, which use the HKDF algorithms.
*/
private SecretKey t13DeriveKey(String type)
throws IOException {
SecretKey sharedSecret = null;
try {
if (keyshare != null) {
// Using KEM: called by the client after receiving the KEM
// ciphertext (keyshare) from the server in ServerHello.
// The client decapsulates it using its private key.
KEM kem = (provider != null)
? KEM.getInstance(algorithmName, provider)
: KEM.getInstance(algorithmName);
var decapsulator = kem.newDecapsulator(localPrivateKey);
sharedSecret = decapsulator.decapsulate(
keyshare, 0, decapsulator.secretSize(),
"TlsPremasterSecret");
} else {
// Using traditional DH-style Key Agreement
KeyAgreement ka = KeyAgreement.getInstance(algorithmName);
ka.init(localPrivateKey);
ka.doPhase(peerPublicKey, true);
sharedSecret = ka.generateSecret("TlsPremasterSecret");
}
return deriveHandshakeSecret(type, sharedSecret);
} catch (GeneralSecurityException gse) {
throw new SSLHandshakeException("Could not generate secret", gse);
} finally {
KeyUtil.destroySecretKeys(sharedSecret);
}
}
}

View File

@ -0,0 +1,223 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package sun.security.ssl;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.ProviderException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.NamedParameterSpec;
import javax.crypto.SecretKey;
import sun.security.ssl.NamedGroup.NamedGroupSpec;
import sun.security.x509.X509Key;
/**
* Specifics for single or hybrid Key exchanges based on KEM
*/
final class KEMKeyExchange {
static final SSLKeyAgreementGenerator kemKAGenerator
= new KEMKAGenerator();
static final class KEMCredentials implements NamedGroupCredentials {
final NamedGroup namedGroup;
// Unlike other credentials, we directly store the key share
// value here, no need to convert to a key
private final byte[] keyshare;
KEMCredentials(byte[] keyshare, NamedGroup namedGroup) {
this.keyshare = keyshare;
this.namedGroup = namedGroup;
}
// For KEM, server performs encapsulation and the resulting
// encapsulated message becomes the key_share value sent to
// the client. It is not a public key, so no PublicKey object
// to return.
@Override
public PublicKey getPublicKey() {
throw new UnsupportedOperationException(
"KEMCredentials stores raw keyshare, not a PublicKey");
}
public byte[] getKeyShare() {
return keyshare;
}
@Override
public NamedGroup getNamedGroup() {
return namedGroup;
}
/**
* Instantiates a KEMCredentials object
*/
static KEMCredentials valueOf(NamedGroup namedGroup,
byte[] encodedPoint) {
if (namedGroup.spec != NamedGroupSpec.NAMED_GROUP_KEM) {
throw new RuntimeException(
"Credentials decoding: Not KEM named group");
}
if (encodedPoint == null || encodedPoint.length == 0) {
return null;
}
return new KEMCredentials(encodedPoint, namedGroup);
}
}
private static class KEMPossession implements SSLPossession {
private final NamedGroup namedGroup;
public KEMPossession(NamedGroup ng) {
this.namedGroup = ng;
}
public NamedGroup getNamedGroup() {
return namedGroup;
}
}
static final class KEMReceiverPossession extends KEMPossession {
private final PrivateKey privateKey;
private final PublicKey publicKey;
KEMReceiverPossession(NamedGroup namedGroup, SecureRandom random) {
super(namedGroup);
String algName = null;
try {
// For KEM: This receiver side (client) generates a key pair.
algName = ((NamedParameterSpec)namedGroup.keAlgParamSpec).
getName();
Provider provider = namedGroup.getProvider();
KeyPairGenerator kpg = (provider != null) ?
KeyPairGenerator.getInstance(algName, provider) :
KeyPairGenerator.getInstance(algName);
kpg.initialize(namedGroup.keAlgParamSpec, random);
KeyPair kp = kpg.generateKeyPair();
privateKey = kp.getPrivate();
publicKey = kp.getPublic();
} catch (GeneralSecurityException e) {
throw new RuntimeException(
"Could not generate keypair for algorithm: " +
algName, e);
}
}
@Override
public byte[] encode() {
if (publicKey instanceof X509Key xk) {
return xk.getKeyAsBytes();
} else if (publicKey instanceof Hybrid.PublicKeyImpl hk) {
return hk.getEncoded();
}
throw new ProviderException("Unsupported key type: " + publicKey);
}
// Package-private
PublicKey getPublicKey() {
return publicKey;
}
// Package-private
PrivateKey getPrivateKey() {
return privateKey;
}
}
static final class KEMSenderPossession extends KEMPossession {
private SecretKey key;
private final SecureRandom random;
KEMSenderPossession(NamedGroup namedGroup, SecureRandom random) {
super(namedGroup);
this.random = random;
}
// Package-private
SecureRandom getRandom() {
return random;
}
// Package-private
SecretKey getKey() {
return key;
}
// Package-private
void setKey(SecretKey key) {
this.key = key;
}
@Override
public byte[] encode() {
throw new UnsupportedOperationException("encode() not supported");
}
}
private static final class KEMKAGenerator
implements SSLKeyAgreementGenerator {
// Prevent instantiation of this class.
private KEMKAGenerator() {
// blank
}
@Override
public SSLKeyDerivation createKeyDerivation(
HandshakeContext context) throws IOException {
for (SSLPossession poss : context.handshakePossessions) {
if (poss instanceof KEMReceiverPossession kposs) {
NamedGroup ng = kposs.getNamedGroup();
for (SSLCredentials cred : context.handshakeCredentials) {
if (cred instanceof KEMCredentials kcred &&
ng.equals(kcred.namedGroup)) {
String name = ((NamedParameterSpec)
ng.keAlgParamSpec).getName();
return new KAKeyDerivation(name, ng, context,
kposs.getPrivateKey(), null,
kcred.getKeyShare());
}
}
}
}
context.conContext.fatal(Alert.HANDSHAKE_FAILURE,
"No suitable KEM key agreement "
+ "parameters negotiated");
return null;
}
}
}

View File

@ -27,8 +27,11 @@ package sun.security.ssl;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.AlgorithmConstraints;
import java.security.CryptoPrimitive;
import java.security.GeneralSecurityException;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.NamedParameterSpec;
import java.text.MessageFormat;
import java.util.*;
import javax.net.ssl.SSLProtocolException;
@ -297,7 +300,9 @@ final class KeyShareExtension {
// update the context
chc.handshakePossessions.add(pos);
// May need more possession types in the future.
if (pos instanceof NamedGroupPossession) {
if (pos instanceof NamedGroupPossession ||
pos instanceof
KEMKeyExchange.KEMReceiverPossession) {
return pos.encode();
}
}
@ -358,24 +363,16 @@ final class KeyShareExtension {
try {
SSLCredentials kaCred =
ng.decodeCredentials(entry.keyExchange);
if (shc.algorithmConstraints != null &&
kaCred instanceof
NamedGroupCredentials namedGroupCredentials) {
if (!shc.algorithmConstraints.permits(
EnumSet.of(CryptoPrimitive.KEY_AGREEMENT),
namedGroupCredentials.getPublicKey())) {
if (SSLLogger.isOn() &&
SSLLogger.isOn("ssl,handshake")) {
SSLLogger.warning(
if (!isCredentialPermitted(shc.algorithmConstraints,
kaCred)) {
if (SSLLogger.isOn() &&
SSLLogger.isOn("ssl,handshake")) {
SSLLogger.warning(
"key share entry of " + ng + " does not " +
" comply with algorithm constraints");
}
kaCred = null;
"comply with algorithm constraints");
}
}
if (kaCred != null) {
} else {
credentials.add(kaCred);
}
} catch (GeneralSecurityException ex) {
@ -513,7 +510,8 @@ final class KeyShareExtension {
@Override
public byte[] produce(ConnectionContext context,
HandshakeMessage message) throws IOException {
// The producing happens in client side only.
// The producing happens in server side only.
ServerHandshakeContext shc = (ServerHandshakeContext)context;
// In response to key_share request only
@ -571,7 +569,9 @@ final class KeyShareExtension {
SSLPossession[] poses = ke.createPossessions(shc);
for (SSLPossession pos : poses) {
if (!(pos instanceof NamedGroupPossession)) {
if (!(pos instanceof NamedGroupPossession ||
pos instanceof
KEMKeyExchange.KEMSenderPossession)) {
// May need more possession types in the future.
continue;
}
@ -579,7 +579,34 @@ final class KeyShareExtension {
// update the context
shc.handshakeKeyExchange = ke;
shc.handshakePossessions.add(pos);
keyShare = new KeyShareEntry(ng.id, pos.encode());
// For KEM, perform encapsulation using the clients public
// key (KEMCredentials). The resulting encapsulated message
// becomes the key_share value sent to the client. The
// shared secret derived from encapsulation is stored in
// the KEMSenderPossession for later use in the TLS key
// schedule.
// SSLKeyExchange.createPossessions() returns at most one
// key-agreement possession or one KEMSenderPossession
// per handshake.
if (pos instanceof KEMKeyExchange.KEMSenderPossession xp) {
if (cd instanceof KEMKeyExchange.KEMCredentials kcred
&& ng.equals(kcred.namedGroup)) {
String name = ((NamedParameterSpec)
ng.keAlgParamSpec).getName();
KAKeyDerivation handshakeKD = new KAKeyDerivation(
name, ng, shc, null, null,
kcred.getKeyShare());
var encaped = handshakeKD.encapsulate(
"TlsHandshakeSecret", xp.getRandom());
xp.setKey(encaped.key());
keyShare = new KeyShareEntry(ng.id,
encaped.encapsulation());
}
} else {
keyShare = new KeyShareEntry(ng.id, pos.encode());
}
break;
}
@ -663,19 +690,13 @@ final class KeyShareExtension {
try {
SSLCredentials kaCred =
ng.decodeCredentials(keyShare.keyExchange);
if (chc.algorithmConstraints != null &&
kaCred instanceof
NamedGroupCredentials namedGroupCredentials) {
if (!chc.algorithmConstraints.permits(
EnumSet.of(CryptoPrimitive.KEY_AGREEMENT),
namedGroupCredentials.getPublicKey())) {
chc.conContext.fatal(Alert.INSUFFICIENT_SECURITY,
"key share entry of " + ng + " does not " +
" comply with algorithm constraints");
}
}
if (kaCred != null) {
if (!isCredentialPermitted(chc.algorithmConstraints,
kaCred)) {
chc.conContext.fatal(Alert.INSUFFICIENT_SECURITY,
"key share entry of " + ng + " does not " +
"comply with algorithm constraints");
} else {
credentials = kaCred;
}
} catch (GeneralSecurityException ex) {
@ -696,6 +717,34 @@ final class KeyShareExtension {
}
}
private static boolean isCredentialPermitted(
AlgorithmConstraints constraints,
SSLCredentials cred) {
if (constraints == null) return true;
if (cred == null) return false;
if (cred instanceof NamedGroupCredentials namedGroupCred) {
if (namedGroupCred instanceof KEMKeyExchange.KEMCredentials
kemCred) {
AlgorithmParameterSpec paramSpec = kemCred.getNamedGroup().
keAlgParamSpec;
String algName = (paramSpec instanceof NamedParameterSpec nps) ?
nps.getName() : null;
return algName != null && constraints.permits(
EnumSet.of(CryptoPrimitive.KEY_AGREEMENT),
algName,
null);
} else {
return constraints.permits(
EnumSet.of(CryptoPrimitive.KEY_AGREEMENT),
namedGroupCred.getPublicKey());
}
}
return true;
}
/**
* The absence processing if the extension is not present in
* the ServerHello handshake message.

View File

@ -214,6 +214,39 @@ enum NamedGroup {
ProtocolVersion.PROTOCOLS_TO_13,
PredefinedDHParameterSpecs.ffdheParams.get(8192)),
ML_KEM_512(0x0200, "MLKEM512",
NamedGroupSpec.NAMED_GROUP_KEM,
ProtocolVersion.PROTOCOLS_OF_13,
null),
ML_KEM_768(0x0201, "MLKEM768",
NamedGroupSpec.NAMED_GROUP_KEM,
ProtocolVersion.PROTOCOLS_OF_13,
null),
ML_KEM_1024(0x0202, "MLKEM1024",
NamedGroupSpec.NAMED_GROUP_KEM,
ProtocolVersion.PROTOCOLS_OF_13,
null),
X25519MLKEM768(0x11ec, "X25519MLKEM768",
NamedGroupSpec.NAMED_GROUP_KEM,
ProtocolVersion.PROTOCOLS_OF_13,
Hybrid.X25519_MLKEM768,
HybridProvider.PROVIDER),
SECP256R1MLKEM768(0x11eb, "SecP256r1MLKEM768",
NamedGroupSpec.NAMED_GROUP_KEM,
ProtocolVersion.PROTOCOLS_OF_13,
Hybrid.SECP256R1_MLKEM768,
HybridProvider.PROVIDER),
SECP384R1MLKEM1024(0x11ed, "SecP384r1MLKEM1024",
NamedGroupSpec.NAMED_GROUP_KEM,
ProtocolVersion.PROTOCOLS_OF_13,
Hybrid.SECP384R1_MLKEM1024,
HybridProvider.PROVIDER),
// Elliptic Curves (RFC 4492)
//
// arbitrary prime and characteristic-2 curves
@ -234,22 +267,33 @@ enum NamedGroup {
final AlgorithmParameterSpec keAlgParamSpec;
final AlgorithmParameters keAlgParams;
final boolean isAvailable;
final Provider defaultProvider;
// performance optimization
private static final Set<CryptoPrimitive> KEY_AGREEMENT_PRIMITIVE_SET =
Collections.unmodifiableSet(EnumSet.of(CryptoPrimitive.KEY_AGREEMENT));
// Constructor used for all NamedGroup types
NamedGroup(int id, String name,
NamedGroupSpec namedGroupSpec,
ProtocolVersion[] supportedProtocols,
AlgorithmParameterSpec keAlgParamSpec) {
this(id, name, namedGroupSpec, supportedProtocols, keAlgParamSpec,
null);
}
// Constructor used for all NamedGroup types
NamedGroup(int id, String name,
NamedGroupSpec namedGroupSpec,
ProtocolVersion[] supportedProtocols,
AlgorithmParameterSpec keAlgParamSpec,
Provider defaultProvider) {
this.id = id;
this.name = name;
this.spec = namedGroupSpec;
this.algorithm = namedGroupSpec.algorithm;
this.supportedProtocols = supportedProtocols;
this.keAlgParamSpec = keAlgParamSpec;
this.defaultProvider = defaultProvider;
// Check if it is a supported named group.
AlgorithmParameters algParams = null;
@ -266,16 +310,28 @@ enum NamedGroup {
// Check the specific algorithm parameters.
if (mediator) {
try {
algParams =
AlgorithmParameters.getInstance(namedGroupSpec.algorithm);
algParams.init(keAlgParamSpec);
// Skip AlgorithmParameters for KEMs (not supported)
// Check KEM's availability via KeyFactory
if (namedGroupSpec == NamedGroupSpec.NAMED_GROUP_KEM) {
if (defaultProvider == null) {
KeyFactory.getInstance(name);
} else {
KeyFactory.getInstance(name, defaultProvider);
}
} else {
// ECDHE or others: use AlgorithmParameters as before
algParams = AlgorithmParameters.getInstance(
namedGroupSpec.algorithm);
algParams.init(keAlgParamSpec);
}
} catch (InvalidParameterSpecException
| NoSuchAlgorithmException exp) {
if (namedGroupSpec != NamedGroupSpec.NAMED_GROUP_XDH) {
mediator = false;
if (SSLLogger.isOn() && SSLLogger.isOn("ssl,handshake")) {
SSLLogger.warning(
"No AlgorithmParameters for " + name, exp);
"No AlgorithmParameters or KeyFactory for " + name,
exp);
}
} else {
// Please remove the following code if the XDH/X25519/X448
@ -307,6 +363,10 @@ enum NamedGroup {
this.keAlgParams = mediator ? algParams : null;
}
Provider getProvider() {
return defaultProvider;
}
//
// The next set of methods search & retrieve NamedGroups.
//
@ -545,6 +605,10 @@ enum NamedGroup {
return spec.decodeCredentials(this, encoded);
}
SSLPossession createPossession(boolean isClient, SecureRandom random) {
return spec.createPossession(this, isClient, random);
}
SSLPossession createPossession(SecureRandom random) {
return spec.createPossession(this, random);
}
@ -566,6 +630,11 @@ enum NamedGroup {
SSLKeyDerivation createKeyDerivation(
HandshakeContext hc) throws IOException;
default SSLPossession createPossession(NamedGroup ng, boolean isClient,
SecureRandom random) {
return createPossession(ng, random);
}
}
enum NamedGroupSpec implements NamedGroupScheme {
@ -578,6 +647,10 @@ enum NamedGroup {
// Finite Field Groups (XDH)
NAMED_GROUP_XDH("XDH", XDHScheme.instance),
// Post-Quantum Cryptography (PQC) KEM groups
// Currently used for hybrid named groups
NAMED_GROUP_KEM("KEM", KEMScheme.instance),
// arbitrary prime and curves (ECDHE)
NAMED_GROUP_ARBITRARY("EC", null),
@ -634,6 +707,15 @@ enum NamedGroup {
return null;
}
public SSLPossession createPossession(
NamedGroup ng, boolean isClient, SecureRandom random) {
if (scheme != null) {
return scheme.createPossession(ng, isClient, random);
}
return null;
}
@Override
public SSLPossession createPossession(
NamedGroup ng, SecureRandom random) {
@ -739,6 +821,42 @@ enum NamedGroup {
}
}
private static class KEMScheme implements NamedGroupScheme {
private static final KEMScheme instance = new KEMScheme();
@Override
public byte[] encodePossessionPublicKey(NamedGroupPossession poss) {
return poss.encode();
}
@Override
public SSLCredentials decodeCredentials(NamedGroup ng,
byte[] encoded) throws IOException, GeneralSecurityException {
return KEMKeyExchange.KEMCredentials.valueOf(ng, encoded);
}
@Override
public SSLPossession createPossession(NamedGroup ng,
SecureRandom random) {
// Must call createPossession with isClient
throw new UnsupportedOperationException();
}
@Override
public SSLPossession createPossession(
NamedGroup ng, boolean isClient, SecureRandom random) {
return isClient
? new KEMKeyExchange.KEMReceiverPossession(ng, random)
: new KEMKeyExchange.KEMSenderPossession(ng, random);
}
@Override
public SSLKeyDerivation createKeyDerivation(
HandshakeContext hc) throws IOException {
return KEMKeyExchange.kemKAGenerator.createKeyDerivation(hc);
}
}
static final class SupportedGroups {
// the supported named groups, non-null immutable list
static final String[] namedGroups;
@ -784,6 +902,9 @@ enum NamedGroup {
} else { // default groups
NamedGroup[] groups = new NamedGroup[] {
// Hybrid key agreement
X25519MLKEM768,
// Primary XDH (RFC 7748) curves
X25519,

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -570,7 +570,9 @@ final class SSLKeyExchange implements SSLKeyAgreementGenerator,
@Override
public SSLPossession createPossession(HandshakeContext hc) {
return namedGroup.createPossession(hc.sslContext.getSecureRandom());
return namedGroup.createPossession(
hc instanceof ClientHandshakeContext,
hc.sslContext.getSecureRandom());
}
@Override

View File

@ -565,6 +565,34 @@ final class ServerHello {
clientHello);
shc.serverHelloRandom = shm.serverRandom;
// For key derivation, we will either use the traditional Key
// Agreement (KA) model or the Key Encapsulation Mechanism (KEM)
// model, depending on what key exchange group is used.
//
// For KA flows, the server first receives the client's share,
// then generates its key share, and finally comes here.
// However, this is changed for KEM: the server
// must perform both actions derive the secret and generate
// the key encapsulation message at the same time during
// encapsulation in SHKeyShareProducer.
//
// Traditional Key Agreement (KA):
// - Both peers generate a key share and exchange it.
// - Each peer computes a shared secret sometime after
// receiving the other's key share.
//
// Key Encapsulation Mechanism (KEM):
// The client publishes a public key via a KeyShareExtension,
// which the server uses to:
//
// - generate the shared secret
// - encapsulate the message which is sent to the client in
// another KeyShareExtension
//
// The derived shared secret must be stored in a
// KEMSenderPossession so it can be retrieved for handshake
// traffic secret derivation later.
// Produce extensions for ServerHello handshake message.
SSLExtension[] serverHelloExtensions =
shc.sslConfig.getEnabledExtensions(
@ -590,9 +618,26 @@ final class ServerHello {
"Not negotiated key shares");
}
SSLKeyDerivation handshakeKD = ke.createKeyDerivation(shc);
SecretKey handshakeSecret = handshakeKD.deriveKey(
"TlsHandshakeSecret");
SecretKey handshakeSecret = null;
// For KEM, the shared secret has already been generated and
// stored in the servers possession (KEMSenderPossession)
// during encapsulation in SHKeyShareProducer.
//
// Only one key share is selected by the server, so at most one
// possession will contain the pre-derived shared secret.
for (var pos : shc.handshakePossessions) {
if (pos instanceof KEMKeyExchange.KEMSenderPossession xp) {
handshakeSecret = xp.getKey();
break;
}
}
if (handshakeSecret == null) {
SSLKeyDerivation handshakeKD = ke.createKeyDerivation(shc);
handshakeSecret = handshakeKD.deriveKey(
"TlsHandshakeSecret");
}
SSLTrafficKeyDerivation kdg =
SSLTrafficKeyDerivation.valueOf(shc.negotiatedProtocol);

View File

@ -104,6 +104,10 @@ public class X509Key implements PublicKey, DerEncoder {
return (BitArray)bitStringKey.clone();
}
public byte[] getKeyAsBytes() {
return bitStringKey.toByteArray();
}
/**
* Construct X.509 subject public key from a DER value. If
* the runtime environment is configured with a specific class for

View File

@ -1,4 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (C) 2022, Tencent. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
@ -26,7 +27,7 @@
/*
* @test
* @bug 8281236
* @bug 8281236 8314323
* @summary Check TLS connection behaviors for named groups configuration
* @library /javax/net/ssl/templates
* @run main/othervm NamedGroups
@ -136,6 +137,60 @@ public class NamedGroups extends SSLSocketTemplate {
"secp256r1"
},
true);
runTest(new String[] {
"X25519MLKEM768"
},
new String[] {
"X25519MLKEM768"
},
false);
runTest(new String[] {
"SecP256r1MLKEM768"
},
new String[] {
"SecP256r1MLKEM768"
},
false);
runTest(new String[] {
"SecP384r1MLKEM1024"
},
new String[] {
"SecP384r1MLKEM1024"
},
false);
runTest(new String[] {
"X25519MLKEM768"
},
new String[] {
"SecP256r1MLKEM768"
},
true);
runTest(new String[] {
"X25519MLKEM768"
},
new String[0],
true);
runTest(new String[] {
"SecP256r1MLKEM768"
},
null,
true);
runTest(new String[] {
"X25519MLKEM768",
"x25519"
},
new String[] {
"X25519MLKEM768",
"x25519"
},
false);
}
private static void runTest(String[] serverNamedGroups,

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -37,7 +37,11 @@ public enum NamedGroup {
FFDHE3072("ffdhe3072"),
FFDHE4096("ffdhe4096"),
FFDHE6144("ffdhe6144"),
FFDHE8192("ffdhe8192");
FFDHE8192("ffdhe8192"),
X25519MLKEM768("X25519MLKEM768"),
SECP256R1MLKEM768("SecP256r1MLKEM768"),
SECP384R1MLKEM1024("SecP384r1MLKEM1024");
public final String name;

View File

@ -26,16 +26,21 @@
/*
* @test
* @bug 8247630
* @bug 8247630 8314323
* @summary Use two key share entries
* @run main/othervm ClientHelloKeyShares 29 23
* @run main/othervm ClientHelloKeyShares 4588 29
* @run main/othervm -Djdk.tls.namedGroups=secp384r1,secp521r1,x448,ffdhe2048 ClientHelloKeyShares 24 30
* @run main/othervm -Djdk.tls.namedGroups=sect163k1,sect163r1,x25519 ClientHelloKeyShares 29
* @run main/othervm -Djdk.tls.namedGroups=sect163k1,sect163r1,secp256r1 ClientHelloKeyShares 23
* @run main/othervm -Djdk.tls.namedGroups=sect163k1,sect163r1,ffdhe2048,ffdhe3072,ffdhe4096 ClientHelloKeyShares 256
* @run main/othervm -Djdk.tls.namedGroups=sect163k1,ffdhe2048,x25519,secp256r1 ClientHelloKeyShares 256 29
* @run main/othervm -Djdk.tls.namedGroups=secp256r1,secp384r1,ffdhe2048,x25519 ClientHelloKeyShares 23 256
*/
* @run main/othervm -Djdk.tls.namedGroups=X25519MLKEM768 ClientHelloKeyShares 4588
* @run main/othervm -Djdk.tls.namedGroups=x25519,X25519MLKEM768 ClientHelloKeyShares 29 4588
* @run main/othervm -Djdk.tls.namedGroups=SecP256r1MLKEM768,x25519 ClientHelloKeyShares 4587 29
* @run main/othervm -Djdk.tls.namedGroups=SecP384r1MLKEM1024,secp256r1 ClientHelloKeyShares 4589 23
* @run main/othervm -Djdk.tls.namedGroups=X25519MLKEM768,SecP256r1MLKEM768,X25519,secp256r1 ClientHelloKeyShares 4588 29
*/
import javax.net.ssl.*;
import javax.net.ssl.SSLEngineResult.*;
@ -62,10 +67,6 @@ public class ClientHelloKeyShares {
private static final int HELLO_EXT_SUPP_VERS = 43;
private static final int HELLO_EXT_KEY_SHARE = 51;
private static final int TLS_PROT_VER_13 = 0x0304;
private static final int NG_SECP256R1 = 0x0017;
private static final int NG_SECP384R1 = 0x0018;
private static final int NG_X25519 = 0x001D;
private static final int NG_X448 = 0x001E;
public static void main(String args[]) throws Exception {
if (debug) {

View File

@ -26,10 +26,12 @@
/*
* @test
* @bug 8247630
* @bug 8247630 8314323
* @summary Use two key share entries
* @library /test/lib
* @run main/othervm -Djdk.tls.namedGroups=x25519,secp256r1,secp384r1 HRRKeyShares
* @run main/othervm
* -Djdk.tls.namedGroups=x25519,secp256r1,secp384r1,X25519MLKEM768,SecP256r1MLKEM768,SecP384r1MLKEM1024
* HRRKeyShares
*/
import java.io.ByteArrayOutputStream;
@ -72,6 +74,10 @@ public class HRRKeyShares {
private static final int NG_SECP384R1 = 0x0018;
private static final int NG_X25519 = 0x001D;
private static final int NG_X448 = 0x001E;
private static final int NG_X25519_MLKEM768 = 0x11EC;
private static final int NG_SECP256R1_MLKEM768 = 0x11EB;
private static final int NG_SECP384R1_MLKEM1024 = 0x11ED;
private static final int NG_GC512A = 0x0026;
private static final int COMP_NONE = 0;
private static final int ALERT_TYPE_FATAL = 2;
@ -238,6 +244,18 @@ public class HRRKeyShares {
System.out.println("Test 4: Bad HRR using known / unasserted x448");
hrrKeyShareTest(NG_X448, false);
System.out.println();
System.out.println("Test 5: Good HRR exchange using X25519MLKEM768");
hrrKeyShareTest(NG_X25519_MLKEM768, true);
System.out.println();
System.out.println("Test 6: Good HRR exchange using SecP256r1MLKEM768");
hrrKeyShareTest(NG_SECP256R1_MLKEM768, true);
System.out.println();
System.out.println("Test 7: Good HRR exchange using SecP384r1MLKEM1024");
hrrKeyShareTest(NG_SECP384R1_MLKEM1024, true);
System.out.println();
}
private static void logResult(String str, SSLEngineResult result) {
@ -348,7 +366,8 @@ public class HRRKeyShares {
try {
// Now we're expecting to reissue the ClientHello, this time
// with a secp384r1 share.
// with a key share for the HRR requested named
// group (hrrNamedGroup).
cTOs.compact();
clientResult = engine.wrap(clientOut, cTOs);
logResult("client wrap: ", clientResult);

View File

@ -34,8 +34,12 @@
* -Djdk.tls.useExtendedMasterSecret=false
* -Djdk.tls.client.enableSessionTicketExtension=false FipsModeTLS
* @comment SunPKCS11 does not support (TLS1.2) SunTlsExtendedMasterSecret yet.
* Stateless resumption doesn't currently work with NSS-FIPS, see JDK-8368669
* @run main/othervm/timeout=120 -Djdk.tls.client.protocols=TLSv1.3 FipsModeTLS
* Stateless resumption doesn't currently work with NSS-FIPS, see JDK-8368669.
* NSS-FIPS does not support ML-KEM, so configures the list of named groups.
* @run main/othervm/timeout=120
* -Djdk.tls.client.protocols=TLSv1.3
* -Djdk.tls.namedGroups=x25519,secp256r1,secp384r1,secp521r1,x448,ffdhe2048,ffdhe3072,ffdhe4096,ffdhe6144,ffdhe8192
* FipsModeTLS
*/
import java.io.File;

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -23,12 +23,24 @@
/*
* @test
* @bug 8246330
* @bug 8246330 8314323
* @library /javax/net/ssl/templates /test/lib
* @run main/othervm -Djdk.tls.namedGroups="secp384r1"
DisabledCurve DISABLE_NONE PASS
* @run main/othervm -Djdk.tls.namedGroups="secp384r1"
DisabledCurve secp384r1 FAIL
* @run main/othervm -Djdk.tls.namedGroups="X25519MLKEM768"
DisabledCurve DISABLE_NONE PASS
* @run main/othervm -Djdk.tls.namedGroups="X25519MLKEM768"
DisabledCurve X25519MLKEM768 FAIL
* @run main/othervm -Djdk.tls.namedGroups="SecP256r1MLKEM768"
DisabledCurve DISABLE_NONE PASS
* @run main/othervm -Djdk.tls.namedGroups="SecP256r1MLKEM768"
DisabledCurve SecP256r1MLKEM768 FAIL
* @run main/othervm -Djdk.tls.namedGroups="SecP384r1MLKEM1024"
DisabledCurve DISABLE_NONE PASS
* @run main/othervm -Djdk.tls.namedGroups="SecP384r1MLKEM1024"
DisabledCurve SecP384r1MLKEM1024 FAIL
*/
import java.security.Security;
import java.util.Arrays;
@ -45,8 +57,10 @@ public class DisabledCurve extends SSLSocketTemplate {
private static final String[][][] protocols = {
{ { "TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1" }, { "TLSv1.2" } },
{ { "TLSv1.2" }, { "TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1" } },
{ { "TLSv1.2" }, { "TLSv1.2" } }, { { "TLSv1.1" }, { "TLSv1.1" } },
{ { "TLSv1" }, { "TLSv1" } } };
{ { "TLSv1.2" }, { "TLSv1.2" } },
{ { "TLSv1.1" }, { "TLSv1.1" } },
{ { "TLSv1" }, { "TLSv1" } },
{ { "TLSv1.3" }, { "TLSv1.3" } } };
@Override
protected SSLContext createClientSSLContext() throws Exception {
@ -94,17 +108,36 @@ public class DisabledCurve extends SSLSocketTemplate {
String expected = args[1];
String disabledName = ("DISABLE_NONE".equals(args[0]) ? "" : args[0]);
boolean disabled = false;
if (disabledName.equals("")) {
if (disabledName.isEmpty()) {
Security.setProperty("jdk.disabled.namedCurves", "");
Security.setProperty("jdk.certpath.disabledAlgorithms", "");
} else {
disabled = true;
Security.setProperty("jdk.certpath.disabledAlgorithms", "secp384r1");
Security.setProperty("jdk.certpath.disabledAlgorithms", disabledName);
if (!disabledName.contains("MLKEM")) {
Security.setProperty("jdk.disabled.namedCurves", disabledName);
} else {
Security.setProperty("jdk.disabled.namedCurves", "");
}
}
// Re-enable TLSv1 and TLSv1.1 since test depends on it.
SecurityUtils.removeFromDisabledTlsAlgs("TLSv1", "TLSv1.1");
String namedGroups = System.getProperty("jdk.tls.namedGroups", "");
boolean hybridGroup = namedGroups.contains("MLKEM");
for (index = 0; index < protocols.length; index++) {
if (hybridGroup) {
String[] clientProtos = protocols[index][0];
String[] serverProtos = protocols[index][1];
if (!(isTLS13(clientProtos) && isTLS13(serverProtos))) {
continue;
}
}
try {
(new DisabledCurve()).run();
if (expected.equals("FAIL")) {
@ -123,4 +156,8 @@ public class DisabledCurve extends SSLSocketTemplate {
}
}
private static boolean isTLS13(String[] protocols) {
return protocols.length == 1 && "TLSv1.3".equals(protocols[0]);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -21,6 +21,8 @@
* questions.
*/
import java.util.Arrays;
import java.util.List;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLSocket;
@ -29,7 +31,7 @@ import jdk.test.lib.security.SecurityUtils;
/*
* @test
* @bug 8224650 8242929
* @bug 8224650 8242929 8314323
* @library /javax/net/ssl/templates
* /javax/net/ssl/TLSCommon
* /test/lib
@ -44,17 +46,20 @@ import jdk.test.lib.security.SecurityUtils;
* @run main/othervm NamedGroupsWithCipherSuite ffdhe4096
* @run main/othervm NamedGroupsWithCipherSuite ffdhe6144
* @run main/othervm NamedGroupsWithCipherSuite ffdhe8192
* @run main/othervm NamedGroupsWithCipherSuite X25519MLKEM768
* @run main/othervm NamedGroupsWithCipherSuite SecP256r1MLKEM768
* @run main/othervm NamedGroupsWithCipherSuite SecP384r1MLKEM1024
*/
public class NamedGroupsWithCipherSuite extends SSLSocketTemplate {
private static final Protocol[] PROTOCOLS = new Protocol[] {
private static final List<Protocol> PROTOCOLS = List.of(
Protocol.TLSV1_3,
Protocol.TLSV1_2,
Protocol.TLSV1_1,
Protocol.TLSV1
};
);
private static final CipherSuite[] CIPHER_SUITES = new CipherSuite[] {
private static final List<CipherSuite> CIPHER_SUITES = List.of(
CipherSuite.TLS_AES_128_GCM_SHA256,
CipherSuite.TLS_AES_256_GCM_SHA384,
CipherSuite.TLS_CHACHA20_POLY1305_SHA256,
@ -75,7 +80,23 @@ public class NamedGroupsWithCipherSuite extends SSLSocketTemplate {
CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA,
CipherSuite.TLS_DHE_RSA_WITH_AES_256_CBC_SHA256,
CipherSuite.TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256
};
);
private static final List<String> HYBRID_NAMEDGROUPS = List.of(
"X25519MLKEM768",
"SecP256r1MLKEM768",
"SecP384r1MLKEM1024"
);
private static final List<Protocol> HYBRID_PROTOCOL = List.of(
Protocol.TLSV1_3
);
private static final List<CipherSuite> HYBRID_CIPHER_SUITES = List.of(
CipherSuite.TLS_AES_128_GCM_SHA256,
CipherSuite.TLS_AES_256_GCM_SHA384,
CipherSuite.TLS_CHACHA20_POLY1305_SHA256
);
private String protocol;
private String cipher;
@ -151,48 +172,59 @@ public class NamedGroupsWithCipherSuite extends SSLSocketTemplate {
// Re-enable TLSv1 and TLSv1.1 since test depends on it.
SecurityUtils.removeFromDisabledTlsAlgs("TLSv1", "TLSv1.1");
for (Protocol protocol : PROTOCOLS) {
for (CipherSuite cipherSuite : CIPHER_SUITES) {
// Named group converted to lower case just
// to satisfy Test condition
boolean hybridGroup = HYBRID_NAMEDGROUPS.contains(namedGroup);
List<Protocol> protocolList = hybridGroup ?
HYBRID_PROTOCOL : PROTOCOLS;
List<CipherSuite> cipherList = hybridGroup ?
HYBRID_CIPHER_SUITES : CIPHER_SUITES;
// non-Hybrid named group converted to lower case just
// to satisfy Test condition
String normalizedGroup = hybridGroup ?
namedGroup : namedGroup.toLowerCase();
for (Protocol protocol : protocolList) {
for (CipherSuite cipherSuite : cipherList) {
if (cipherSuite.supportedByProtocol(protocol)
&& groupSupportdByCipher(namedGroup.toLowerCase(),
cipherSuite)) {
&& groupSupportedByCipher(normalizedGroup,
cipherSuite)) {
System.out.printf("Protocol: %s, cipher suite: %s%n",
protocol, cipherSuite);
// Named group converted to lower case just
// to satisfy Test condition
new NamedGroupsWithCipherSuite(protocol,
cipherSuite, namedGroup.toLowerCase()).run();
cipherSuite, normalizedGroup).run();
}
}
}
}
private static boolean groupSupportdByCipher(String group,
private static boolean groupSupportedByCipher(String group,
CipherSuite cipherSuite) {
if (HYBRID_NAMEDGROUPS.contains(group)) {
return cipherSuite.keyExAlgorithm == null;
}
return (group.startsWith("x")
&& xdhGroupSupportdByCipher(cipherSuite))
&& xdhGroupSupportedByCipher(cipherSuite))
|| (group.startsWith("secp")
&& ecdhGroupSupportdByCipher(cipherSuite))
&& ecdhGroupSupportedByCipher(cipherSuite))
|| (group.startsWith("ffdhe")
&& ffdhGroupSupportdByCipher(cipherSuite));
&& ffdhGroupSupportedByCipher(cipherSuite));
}
private static boolean xdhGroupSupportdByCipher(
private static boolean xdhGroupSupportedByCipher(
CipherSuite cipherSuite) {
return cipherSuite.keyExAlgorithm == null
|| cipherSuite.keyExAlgorithm == KeyExAlgorithm.ECDHE_RSA;
}
private static boolean ecdhGroupSupportdByCipher(
private static boolean ecdhGroupSupportedByCipher(
CipherSuite cipherSuite) {
return cipherSuite.keyExAlgorithm == null
|| cipherSuite.keyExAlgorithm == KeyExAlgorithm.ECDHE_RSA
|| cipherSuite.keyExAlgorithm == KeyExAlgorithm.ECDHE_ECDSA;
}
private static boolean ffdhGroupSupportdByCipher(
private static boolean ffdhGroupSupportedByCipher(
CipherSuite cipherSuite) {
return cipherSuite.keyExAlgorithm == null
|| cipherSuite.keyExAlgorithm == KeyExAlgorithm.DHE_DSS

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -23,7 +23,7 @@
/*
* @test
* @bug 8226374 8242929
* @bug 8226374 8242929 8314323
* @library /javax/net/ssl/templates
* @summary Restrict signature algorithms and named groups
* @run main/othervm RestrictNamedGroup x25519
@ -36,6 +36,9 @@
* @run main/othervm RestrictNamedGroup ffdhe4096
* @run main/othervm RestrictNamedGroup ffdhe6144
* @run main/othervm RestrictNamedGroup ffdhe8192
* @run main/othervm RestrictNamedGroup X25519MLKEM768
* @run main/othervm RestrictNamedGroup SecP256r1MLKEM768
* @run main/othervm RestrictNamedGroup SecP384r1MLKEM1024
*/
import java.security.Security;

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -23,7 +23,7 @@
/*
* @test
* @bug 8171279
* @bug 8171279 8314323
* @library /javax/net/ssl/templates
* @summary Test TLS connection with each individual supported group
* @run main/othervm SupportedGroups x25519
@ -36,6 +36,9 @@
* @run main/othervm SupportedGroups ffdhe4096
* @run main/othervm SupportedGroups ffdhe6144
* @run main/othervm SupportedGroups ffdhe8192
* @run main/othervm SupportedGroups X25519MLKEM768
* @run main/othervm SupportedGroups SecP256r1MLKEM768
* @run main/othervm SupportedGroups SecP384r1MLKEM1024
*/
import java.net.InetAddress;
import java.util.Arrays;
@ -45,15 +48,24 @@ import javax.net.ssl.SSLServerSocket;
public class SupportedGroups extends SSLSocketTemplate {
private static volatile int index;
private static final String[][][] protocols = {
private static final String[][][] protocolsForClassic = {
{{"TLSv1.3"}, {"TLSv1.3"}},
{{"TLSv1.3", "TLSv1.2"}, {"TLSv1.2"}},
{{"TLSv1.2"}, {"TLSv1.3", "TLSv1.2"}},
{{"TLSv1.2"}, {"TLSv1.2"}}
};
public SupportedGroups() {
private static final String[][][] protocolsForHybrid = {
{{"TLSv1.3"}, {"TLSv1.3"}},
{{"TLSv1.3", "TLSv1.2"}, {"TLSv1.3"}},
{{"TLSv1.3"}, {"TLSv1.3", "TLSv1.2"}}
};
private final String[][][] protocols;
public SupportedGroups(String[][][] protocols) {
this.serverAddress = InetAddress.getLoopbackAddress();
this.protocols = protocols;
}
// Servers are configured before clients, increment test case after.
@ -85,8 +97,18 @@ public class SupportedGroups extends SSLSocketTemplate {
public static void main(String[] args) throws Exception {
System.setProperty("jdk.tls.namedGroups", args[0]);
boolean hybridGroup = hybridNamedGroup(args[0]);
String[][][] protocols = hybridGroup ?
protocolsForHybrid : protocolsForClassic;
for (index = 0; index < protocols.length; index++) {
(new SupportedGroups()).run();
(new SupportedGroups(protocols)).run();
}
}
private static boolean hybridNamedGroup(String namedGroup) {
return namedGroup.equals("X25519MLKEM768") ||
namedGroup.equals("SecP256r1MLKEM768") ||
namedGroup.equals("SecP384r1MLKEM1024");
}
}

View File

@ -44,6 +44,7 @@ import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManagerFactory;
@ -75,8 +76,15 @@ public class SSLHandshake {
@Param({"true", "false"})
boolean resume;
@Param({"TLSv1.2", "TLS"})
String tlsVersion;
@Param({
"TLSv1.2-secp256r1",
"TLSv1.3-x25519", "TLSv1.3-secp256r1", "TLSv1.3-secp384r1",
"TLSv1.3-X25519MLKEM768", "TLSv1.3-SecP256r1MLKEM768", "TLSv1.3-SecP384r1MLKEM1024"
})
String versionAndGroup;
private String tlsVersion;
private String namedGroup;
private static SSLContext getServerContext() {
try {
@ -96,6 +104,10 @@ public class SSLHandshake {
@Setup(Level.Trial)
public void init() throws Exception {
String[] components = versionAndGroup.split("-", 2);
tlsVersion = components[0];
namedGroup = components[1];
KeyStore ts = TestCertificates.getTrustStore();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
@ -195,5 +207,14 @@ public class SSLHandshake {
*/
clientEngine = sslClientCtx.createSSLEngine("client", 80);
clientEngine.setUseClientMode(true);
// Set the key exchange named group in client and server engines
SSLParameters clientParams = clientEngine.getSSLParameters();
clientParams.setNamedGroups(new String[]{namedGroup});
clientEngine.setSSLParameters(clientParams);
SSLParameters serverParams = serverEngine.getSSLParameters();
serverParams.setNamedGroups(new String[]{namedGroup});
serverEngine.setSSLParameters(serverParams);
}
}

View File

@ -23,34 +23,69 @@
package org.openjdk.bench.javax.crypto.full;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.OperationsPerInvocation;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.infra.Blackhole;
import javax.crypto.DecapsulateException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import javax.crypto.KEM;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.Security;
import java.security.spec.ECGenParameterSpec;
public class KEMBench extends CryptoBase {
public abstract class KEMBench extends CryptoBase {
public static final int SET_SIZE = 128;
@Param({"ML-KEM-512", "ML-KEM-768", "ML-KEM-1024" })
@Param({})
private String algorithm;
@Param({""}) // Used when the KeyPairGenerator Alg != KEM Alg
private String kpgSpec;
private KeyPair[] keys;
private byte[][] messages;
private KEM kem;
@Setup
public void setup() throws NoSuchAlgorithmException, InvalidKeyException {
kem = (prov == null) ? KEM.getInstance(algorithm) : KEM.getInstance(algorithm, prov);
KeyPairGenerator generator = (prov == null) ? KeyPairGenerator.getInstance(algorithm) : KeyPairGenerator.getInstance(algorithm, prov);
public void setup() throws NoSuchAlgorithmException, InvalidKeyException,
InvalidAlgorithmParameterException {
String kpgAlg;
String kpgParams;
kem = (prov == null) ? KEM.getInstance(algorithm) :
KEM.getInstance(algorithm, prov);
// By default use the same provider for KEM and KPG
Provider kpgProv = prov;
if (kpgSpec.isEmpty()) {
kpgAlg = algorithm;
kpgParams = "";
} else {
// The key pair generation spec is broken down from a colon-
// delimited string spec into 3 fields:
// [0] - the provider name
// [1] - the algorithm name
// [2] - the parameters (i.e. the name of the curve)
String[] kpgTok = kpgSpec.split(":");
kpgProv = Security.getProvider(kpgTok[0]);
kpgAlg = kpgTok[1];
kpgParams = kpgTok[2];
}
KeyPairGenerator generator = (kpgProv == null) ?
KeyPairGenerator.getInstance(kpgAlg) :
KeyPairGenerator.getInstance(kpgAlg, kpgProv);
if (kpgParams != null && !kpgParams.isEmpty()) {
generator.initialize(new ECGenParameterSpec(kpgParams));
}
keys = new KeyPair[SET_SIZE];
for (int i = 0; i < keys.length; i++) {
keys[i] = generator.generateKeyPair();
@ -63,20 +98,79 @@ public class KEMBench extends CryptoBase {
}
}
private static Provider getInternalJce() {
try {
Class<?> dhClazz = Class.forName("sun.security.ssl.HybridProvider");
return (Provider) dhClazz.getField("PROVIDER").get(null);
} catch (ReflectiveOperationException exc) {
throw new RuntimeException(exc);
}
}
@Benchmark
@OperationsPerInvocation(SET_SIZE)
public void encapsulate(Blackhole bh) throws InvalidKeyException {
for (KeyPair kp : keys) {
bh.consume(kem.newEncapsulator(kp.getPublic()).encapsulate().encapsulation());
bh.consume(kem.newEncapsulator(kp.getPublic()).encapsulate().
encapsulation());
}
}
@Benchmark
@OperationsPerInvocation(SET_SIZE)
public void decapsulate(Blackhole bh) throws InvalidKeyException, DecapsulateException {
public void decapsulate(Blackhole bh) throws InvalidKeyException,
DecapsulateException {
for (int i = 0; i < messages.length; i++) {
bh.consume(kem.newDecapsulator(keys[i].getPrivate()).decapsulate(messages[i]));
bh.consume(kem.newDecapsulator(keys[i].getPrivate()).
decapsulate(messages[i]));
}
}
public static class MLKEM extends KEMBench {
@Param({"ML-KEM-512", "ML-KEM-768", "ML-KEM-1024" })
private String algorithm;
@Param({""}) // ML-KEM uses the same alg for KPG and KEM
private String kpgSpec;
}
@Fork(value = 5, jvmArgs = {"-XX:+AlwaysPreTouch", "--add-opens",
"java.base/sun.security.ssl=ALL-UNNAMED"})
public static class JSSE_DHasKEM extends KEMBench {
@Setup
public void init() {
try {
prov = getInternalJce();
super.setup();
} catch (GeneralSecurityException gse) {
throw new RuntimeException(gse);
}
}
@Param({"DH"})
private String algorithm;
@Param({"SunEC:XDH:x25519", "SunEC:EC:secp256r1", "SunEC:EC:secp384r1"})
private String kpgSpec;
}
@Fork(value = 5, jvmArgs = {"-XX:+AlwaysPreTouch", "--add-opens",
"java.base/sun.security.ssl=ALL-UNNAMED"})
public static class JSSE_Hybrid extends KEMBench {
@Setup
public void init() {
try {
prov = getInternalJce();
super.setup();
} catch (GeneralSecurityException gse) {
throw new RuntimeException(gse);
}
}
@Param({"X25519MLKEM768", "SecP256r1MLKEM768", "SecP384r1MLKEM1024"})
private String algorithm;
@Param({""}) // ML-KEM uses the same alg for KPG and KEM
private String kpgSpec;
}
}

View File

@ -22,13 +22,16 @@
*/
package org.openjdk.bench.javax.crypto.full;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Setup;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
public class KeyPairGeneratorBench extends CryptoBase {
@ -45,11 +48,21 @@ public class KeyPairGeneratorBench extends CryptoBase {
setupProvider();
generator = (prov == null) ? KeyPairGenerator.getInstance(algorithm)
: KeyPairGenerator.getInstance(algorithm, prov);
if (keyLength > 0) { // not all key pair generators allow the use of key length
// not all key pair generators allow the use of key length
if (keyLength > 0) {
generator.initialize(keyLength);
}
}
private static Provider getInternalJce() {
try {
Class<?> dhClazz = Class.forName("sun.security.ssl.HybridProvider");
return (Provider) dhClazz.getField("PROVIDER").get(null);
} catch (ReflectiveOperationException exc) {
throw new RuntimeException(exc);
}
}
@Benchmark
public KeyPair generateKeyPair() {
return generator.generateKeyPair();
@ -118,4 +131,23 @@ public class KeyPairGeneratorBench extends CryptoBase {
private int keyLength;
}
@Fork(value = 5, jvmArgs = {"-XX:+AlwaysPreTouch", "--add-opens",
"java.base/sun.security.ssl=ALL-UNNAMED"})
public static class JSSE_Hybrid extends KeyPairGeneratorBench {
@Setup
public void init() {
try {
prov = getInternalJce();
super.setup();
} catch (GeneralSecurityException gse) {
throw new RuntimeException(gse);
}
}
@Param({"X25519MLKEM768", "SecP256r1MLKEM768", "SecP384r1MLKEM1024"})
private String algorithm;
@Param({"0"}) // Hybrid KPGs don't need key lengths
private int keyLength;
}
}