From 21dc41f744edd138e77970d4e25e3a7eda41621f Mon Sep 17 00:00:00 2001 From: Hai-May Chao Date: Tue, 20 Jan 2026 16:16:38 +0000 Subject: [PATCH] 8314323: Implement JEP 527: TLS 1.3 Hybrid Key Exchange Co-authored-by: Jamil Nimeh Co-authored-by: Weijun Wang Reviewed-by: wetmore, mullan --- .../classes/sun/security/ssl/DHasKEM.java | 254 ++++++++++ .../classes/sun/security/ssl/Hybrid.java | 474 ++++++++++++++++++ .../sun/security/ssl/HybridProvider.java | 130 +++++ .../sun/security/ssl/KAKeyDerivation.java | 127 ++++- .../sun/security/ssl/KEMKeyExchange.java | 223 ++++++++ .../sun/security/ssl/KeyShareExtension.java | 113 +++-- .../classes/sun/security/ssl/NamedGroup.java | 131 ++++- .../sun/security/ssl/SSLKeyExchange.java | 6 +- .../classes/sun/security/ssl/ServerHello.java | 51 +- .../classes/sun/security/x509/X509Key.java | 4 + .../net/ssl/SSLParameters/NamedGroups.java | 57 ++- .../javax/net/ssl/TLSCommon/NamedGroup.java | 8 +- .../net/ssl/TLSv13/ClientHelloKeyShares.java | 15 +- .../javax/net/ssl/TLSv13/HRRKeyShares.java | 25 +- .../security/pkcs11/tls/fips/FipsModeTLS.java | 8 +- .../ssl/CipherSuite/DisabledCurve.java | 49 +- .../NamedGroupsWithCipherSuite.java | 76 ++- .../ssl/CipherSuite/RestrictNamedGroup.java | 7 +- .../ssl/CipherSuite/SupportedGroups.java | 32 +- .../bench/java/security/SSLHandshake.java | 25 +- .../bench/javax/crypto/full/KEMBench.java | 110 +++- .../crypto/full/KeyPairGeneratorBench.java | 34 +- 22 files changed, 1839 insertions(+), 120 deletions(-) create mode 100644 src/java.base/share/classes/sun/security/ssl/DHasKEM.java create mode 100644 src/java.base/share/classes/sun/security/ssl/Hybrid.java create mode 100644 src/java.base/share/classes/sun/security/ssl/HybridProvider.java create mode 100644 src/java.base/share/classes/sun/security/ssl/KEMKeyExchange.java diff --git a/src/java.base/share/classes/sun/security/ssl/DHasKEM.java b/src/java.base/share/classes/sun/security/ssl/DHasKEM.java new file mode 100644 index 00000000000..763013f280c --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/DHasKEM.java @@ -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); + } + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/Hybrid.java b/src/java.base/share/classes/sun/security/ssl/Hybrid.java new file mode 100644 index 00000000000..e3e2cfa0b23 --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/Hybrid.java @@ -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 engineGetKeySpec(Key key, + Class 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; + } + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/HybridProvider.java b/src/java.base/share/classes/sun/security/ssl/HybridProvider.java new file mode 100644 index 00000000000..c77d6f66273 --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/HybridProvider.java @@ -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 aliases, Map 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); + }; + } + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/KAKeyDerivation.java b/src/java.base/share/classes/sun/security/ssl/KAKeyDerivation.java index 623f83f547a..39e82b50435 100644 --- a/src/java.base/share/classes/sun/security/ssl/KAKeyDerivation.java +++ b/src/java.base/share/classes/sun/security/ssl/KAKeyDerivation.java @@ -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); } } } diff --git a/src/java.base/share/classes/sun/security/ssl/KEMKeyExchange.java b/src/java.base/share/classes/sun/security/ssl/KEMKeyExchange.java new file mode 100644 index 00000000000..fb8de6cb104 --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/KEMKeyExchange.java @@ -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; + } + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/KeyShareExtension.java b/src/java.base/share/classes/sun/security/ssl/KeyShareExtension.java index 8d785f7515a..0d2cbb8f529 100644 --- a/src/java.base/share/classes/sun/security/ssl/KeyShareExtension.java +++ b/src/java.base/share/classes/sun/security/ssl/KeyShareExtension.java @@ -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 client’s 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. diff --git a/src/java.base/share/classes/sun/security/ssl/NamedGroup.java b/src/java.base/share/classes/sun/security/ssl/NamedGroup.java index 877236ebfad..abf973727f3 100644 --- a/src/java.base/share/classes/sun/security/ssl/NamedGroup.java +++ b/src/java.base/share/classes/sun/security/ssl/NamedGroup.java @@ -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 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, diff --git a/src/java.base/share/classes/sun/security/ssl/SSLKeyExchange.java b/src/java.base/share/classes/sun/security/ssl/SSLKeyExchange.java index 22a44590ce3..263308f0659 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLKeyExchange.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLKeyExchange.java @@ -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 diff --git a/src/java.base/share/classes/sun/security/ssl/ServerHello.java b/src/java.base/share/classes/sun/security/ssl/ServerHello.java index 76c266a628a..0567c861e18 100644 --- a/src/java.base/share/classes/sun/security/ssl/ServerHello.java +++ b/src/java.base/share/classes/sun/security/ssl/ServerHello.java @@ -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 server’s 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); diff --git a/src/java.base/share/classes/sun/security/x509/X509Key.java b/src/java.base/share/classes/sun/security/x509/X509Key.java index c83e06f651e..1cfe3f9d95d 100644 --- a/src/java.base/share/classes/sun/security/x509/X509Key.java +++ b/src/java.base/share/classes/sun/security/x509/X509Key.java @@ -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 diff --git a/test/jdk/javax/net/ssl/SSLParameters/NamedGroups.java b/test/jdk/javax/net/ssl/SSLParameters/NamedGroups.java index 25f73606b96..786b907b79a 100644 --- a/test/jdk/javax/net/ssl/SSLParameters/NamedGroups.java +++ b/test/jdk/javax/net/ssl/SSLParameters/NamedGroups.java @@ -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, diff --git a/test/jdk/javax/net/ssl/TLSCommon/NamedGroup.java b/test/jdk/javax/net/ssl/TLSCommon/NamedGroup.java index ec89fe0d5b5..432a2bd1b0d 100644 --- a/test/jdk/javax/net/ssl/TLSCommon/NamedGroup.java +++ b/test/jdk/javax/net/ssl/TLSCommon/NamedGroup.java @@ -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; diff --git a/test/jdk/javax/net/ssl/TLSv13/ClientHelloKeyShares.java b/test/jdk/javax/net/ssl/TLSv13/ClientHelloKeyShares.java index efb9895b33c..ed26cf90a8c 100644 --- a/test/jdk/javax/net/ssl/TLSv13/ClientHelloKeyShares.java +++ b/test/jdk/javax/net/ssl/TLSv13/ClientHelloKeyShares.java @@ -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) { diff --git a/test/jdk/javax/net/ssl/TLSv13/HRRKeyShares.java b/test/jdk/javax/net/ssl/TLSv13/HRRKeyShares.java index 560faf87049..bd14e465e65 100644 --- a/test/jdk/javax/net/ssl/TLSv13/HRRKeyShares.java +++ b/test/jdk/javax/net/ssl/TLSv13/HRRKeyShares.java @@ -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); diff --git a/test/jdk/sun/security/pkcs11/tls/fips/FipsModeTLS.java b/test/jdk/sun/security/pkcs11/tls/fips/FipsModeTLS.java index 8799f2305bf..a54bb501f78 100644 --- a/test/jdk/sun/security/pkcs11/tls/fips/FipsModeTLS.java +++ b/test/jdk/sun/security/pkcs11/tls/fips/FipsModeTLS.java @@ -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; diff --git a/test/jdk/sun/security/ssl/CipherSuite/DisabledCurve.java b/test/jdk/sun/security/ssl/CipherSuite/DisabledCurve.java index 26304c5df95..a13f8570f14 100644 --- a/test/jdk/sun/security/ssl/CipherSuite/DisabledCurve.java +++ b/test/jdk/sun/security/ssl/CipherSuite/DisabledCurve.java @@ -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]); + } } diff --git a/test/jdk/sun/security/ssl/CipherSuite/NamedGroupsWithCipherSuite.java b/test/jdk/sun/security/ssl/CipherSuite/NamedGroupsWithCipherSuite.java index 5732f42982c..9080f549683 100644 --- a/test/jdk/sun/security/ssl/CipherSuite/NamedGroupsWithCipherSuite.java +++ b/test/jdk/sun/security/ssl/CipherSuite/NamedGroupsWithCipherSuite.java @@ -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 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 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 HYBRID_NAMEDGROUPS = List.of( + "X25519MLKEM768", + "SecP256r1MLKEM768", + "SecP384r1MLKEM1024" + ); + + private static final List HYBRID_PROTOCOL = List.of( + Protocol.TLSV1_3 + ); + + private static final List 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 protocolList = hybridGroup ? + HYBRID_PROTOCOL : PROTOCOLS; + List 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 diff --git a/test/jdk/sun/security/ssl/CipherSuite/RestrictNamedGroup.java b/test/jdk/sun/security/ssl/CipherSuite/RestrictNamedGroup.java index c4c343bf84e..4ff0c6e6e15 100644 --- a/test/jdk/sun/security/ssl/CipherSuite/RestrictNamedGroup.java +++ b/test/jdk/sun/security/ssl/CipherSuite/RestrictNamedGroup.java @@ -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; diff --git a/test/jdk/sun/security/ssl/CipherSuite/SupportedGroups.java b/test/jdk/sun/security/ssl/CipherSuite/SupportedGroups.java index 88b0bf2489a..8cf6ee2b5e6 100644 --- a/test/jdk/sun/security/ssl/CipherSuite/SupportedGroups.java +++ b/test/jdk/sun/security/ssl/CipherSuite/SupportedGroups.java @@ -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"); + } } diff --git a/test/micro/org/openjdk/bench/java/security/SSLHandshake.java b/test/micro/org/openjdk/bench/java/security/SSLHandshake.java index d8773781b58..b46704a01de 100644 --- a/test/micro/org/openjdk/bench/java/security/SSLHandshake.java +++ b/test/micro/org/openjdk/bench/java/security/SSLHandshake.java @@ -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); } } diff --git a/test/micro/org/openjdk/bench/javax/crypto/full/KEMBench.java b/test/micro/org/openjdk/bench/javax/crypto/full/KEMBench.java index 3386039c62e..dc6d2060f5e 100644 --- a/test/micro/org/openjdk/bench/javax/crypto/full/KEMBench.java +++ b/test/micro/org/openjdk/bench/javax/crypto/full/KEMBench.java @@ -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; + } } diff --git a/test/micro/org/openjdk/bench/javax/crypto/full/KeyPairGeneratorBench.java b/test/micro/org/openjdk/bench/javax/crypto/full/KeyPairGeneratorBench.java index 58daff28d88..5a3c72fb263 100644 --- a/test/micro/org/openjdk/bench/javax/crypto/full/KeyPairGeneratorBench.java +++ b/test/micro/org/openjdk/bench/javax/crypto/full/KeyPairGeneratorBench.java @@ -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; + } }