From d5f88bb9b8c05150e6627f10da6f8c2d646d6657 Mon Sep 17 00:00:00 2001 From: Hai-May Chao Date: Thu, 2 Oct 2025 14:53:42 -0700 Subject: [PATCH 1/3] 8314323: TLS 1.3 Hybrid Key Exchange --- .../classes/com/sun/crypto/provider/DH.java | 340 +++++++++++++++ .../security/spec/NamedParameterSpec.java | 24 + .../sun/security/ssl/KAKeyDerivation.java | 121 +++++- .../sun/security/ssl/KEMKeyExchange.java | 205 +++++++++ .../sun/security/ssl/KeyShareExtension.java | 107 +++-- .../classes/sun/security/ssl/NamedGroup.java | 136 +++++- .../sun/security/ssl/SSLKeyExchange.java | 6 +- .../classes/sun/security/ssl/ServerHello.java | 44 +- .../classes/sun/security/util/Hybrid.java | 411 ++++++++++++++++++ .../classes/sun/security/x509/X509Key.java | 4 + .../security/spec/TestNamedParameterSpec.java | 5 +- .../net/ssl/SSLParameters/NamedGroups.java | 57 ++- .../net/ssl/TLSv13/ClientHelloKeyShares.java | 17 +- .../javax/net/ssl/TLSv13/HRRKeyShares.java | 22 +- .../ssl/CipherSuite/DisabledCurve.java | 49 ++- .../NamedGroupsWithCipherSuite.java | 54 ++- .../ssl/CipherSuite/RestrictNamedGroup.java | 7 +- .../ssl/CipherSuite/SupportedGroups.java | 31 +- .../ssl/DHKeyExchange/UseStrongDHSizes.java | 6 +- .../bench/java/security/SSLHandshake.java | 25 +- .../bench/javax/crypto/full/KEMBench.java | 86 +++- .../crypto/full/KeyPairGeneratorBench.java | 30 ++ 22 files changed, 1680 insertions(+), 107 deletions(-) create mode 100644 src/java.base/share/classes/com/sun/crypto/provider/DH.java create mode 100644 src/java.base/share/classes/sun/security/ssl/KEMKeyExchange.java create mode 100644 src/java.base/share/classes/sun/security/util/Hybrid.java diff --git a/src/java.base/share/classes/com/sun/crypto/provider/DH.java b/src/java.base/share/classes/com/sun/crypto/provider/DH.java new file mode 100644 index 0000000000000..72f4004bca790 --- /dev/null +++ b/src/java.base/share/classes/com/sun/crypto/provider/DH.java @@ -0,0 +1,340 @@ +/* + * 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 com.sun.crypto.provider; + +import sun.security.util.ArrayUtil; +import sun.security.util.CurveDB; +import sun.security.util.ECUtil; +import sun.security.util.Hybrid; +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 javax.crypto.spec.SecretKeySpec; +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; +import java.util.List; +import java.util.Map; + +import static sun.security.util.SecurityConstants.PROVIDER_VER; + +/** + * The DH provider is a KEM abstraction layer over traditional DH based + * key exchange. It models DH/ECDH/XDH as KEMs, like post-quantum algorithms, + * so DH/ECDH/XDH can be used in hybrid key exchange, alongside post-quantum + * KEMs. + */ +public class DH implements KEMSpi { + + // DH in its own private provider so we always getInstance from here. + public static final Provider PROVIDER = new ProviderImpl(); + + private static class ProviderImpl extends Provider { + @java.io.Serial + private static final long serialVersionUID = 0L; + private ProviderImpl() { + super("InternalJCE", PROVIDER_VER, ""); + put("KEM.DH", DH.class.getName()); + + // Hybrid KeyPairGenerator/KeyFactory/KEM + + // The order of shares in the concatenation for group name + // X25519MLKEM768 has been reversed. This is due to historical + // reasons. + var attrs = Map.of("name", "X25519MLKEM768", "left", "ML-KEM-768", + "right", "X25519"); + putService(new HybridService(this, "KeyPairGenerator", + "X25519MLKEM768", "sun.security.util.Hybrid$KeyPairGeneratorImpl", + null, attrs)); + putService(new HybridService(this, "KEM", + "X25519MLKEM768", "sun.security.util.Hybrid$KEMImpl", + null, attrs)); + putService(new HybridService(this, "KeyFactory", + "X25519MLKEM768", "sun.security.util.Hybrid$KeyFactoryImpl", + null, attrs)); + + attrs = Map.of("name", "SecP256r1MLKEM768", "left", "secp256r1", + "right", "ML-KEM-768"); + putService(new HybridService(this, "KeyPairGenerator", + "SecP256r1MLKEM768", "sun.security.util.Hybrid$KeyPairGeneratorImpl", + null, attrs)); + putService(new HybridService(this, "KEM", + "SecP256r1MLKEM768", "sun.security.util.Hybrid$KEMImpl", + null, attrs)); + putService(new HybridService(this, "KeyFactory", + "SecP256r1MLKEM768", "sun.security.util.Hybrid$KeyFactoryImpl", + null, attrs)); + + attrs = Map.of("name", "SecP384r1MLKEM1024", "left", "secp384r1", + "right", "ML-KEM-1024"); + putService(new HybridService(this, "KeyPairGenerator", + "SecP384r1MLKEM1024", "sun.security.util.Hybrid$KeyPairGeneratorImpl", + null, attrs)); + putService(new HybridService(this, "KEM", + "SecP384r1MLKEM1024", "sun.security.util.Hybrid$KEMImpl", + null, attrs)); + putService(new HybridService(this, "KeyFactory", + "SecP384r1MLKEM1024", "sun.security.util.Hybrid$KeyFactoryImpl", + null, attrs)); + } + } + + @Override + public EncapsulatorSpi engineNewEncapsulator( + PublicKey publicKey, AlgorithmParameterSpec spec, + SecureRandom secureRandom) throws InvalidKeyException { + return new Handler(publicKey, null, spec, secureRandom); + } + + @Override + public DecapsulatorSpi engineNewDecapsulator(PrivateKey privateKey, + AlgorithmParameterSpec spec) throws InvalidKeyException { + return new Handler(null, privateKey, spec, null); + } + + static final class Handler + implements KEMSpi.EncapsulatorSpi, KEMSpi.DecapsulatorSpi { + private final PublicKey pkR; + private final PrivateKey skR; + private final AlgorithmParameterSpec spec; + private final SecureRandom sr; + private final Params params; + + Handler(PublicKey pk, PrivateKey sk, AlgorithmParameterSpec spec, + SecureRandom sr) throws InvalidKeyException { + this.pkR = pk; + this.skR = sk; + this.spec = spec; + 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.Nsecret; + } + + @Override + public int engineEncapsulationSize() { + return params.Npk; + } + + @Override + public SecretKey engineDecapsulate(byte[] encapsulation, int from, + int to, String algorithm) throws DecapsulateException { + if (encapsulation.length != params.Npk) { + 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.Nsecret) { + return key; + } else if ("RAW".equalsIgnoreCase(key.getFormat())) { + byte[] km = key.getEncoded(); + if (km == null) { + // Should not happen if format is "RAW" + throw new UnsupportedOperationException("Key extract failed"); + } else { + return new SecretKeySpec(km, from, to - from, + key.getAlgorithm()); + } + } else { + throw new UnsupportedOperationException("Cannot extract key"); + } + } + + // 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("X25519")) { + return Params.X25519; + } else if (ns.getName().equalsIgnoreCase("X448")) { + 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 Nsecret; + private final int Npk; + private final String kaAlgorithm; + private final String keyAlgorithm; + private final AlgorithmParameterSpec spec; + + + Params(int Nsecret, int Npk, String kaAlgorithm, String keyAlgorithm, + AlgorithmParameterSpec spec) { + this.spec = spec; + this.Nsecret = Nsecret; + this.Npk = Npk; + 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, Npk); + } + } + + 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); + } + } + + 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/java/security/spec/NamedParameterSpec.java b/src/java.base/share/classes/java/security/spec/NamedParameterSpec.java index c4091705df02e..b62336b4616b2 100644 --- a/src/java.base/share/classes/java/security/spec/NamedParameterSpec.java +++ b/src/java.base/share/classes/java/security/spec/NamedParameterSpec.java @@ -117,6 +117,30 @@ public class NamedParameterSpec implements AlgorithmParameterSpec { public static final NamedParameterSpec ML_KEM_1024 = new NamedParameterSpec("ML-KEM-1024"); + /** + * The X25519MLKEM768 parameters + * + * @since 26 + */ + public static final NamedParameterSpec X25519MLKEM768 + = new NamedParameterSpec("X25519MLKEM768"); + + /** + * The SecP256r1MLKEM768 parameters + * + * @since 26 + */ + public static final NamedParameterSpec SecP256r1MLKEM768 + = new NamedParameterSpec("SecP256r1MLKEM768"); + + /** + * The SecP384r1MLKEM1024 parameters + * + * @since 26 + */ + public static final NamedParameterSpec SecP384r1MLKEM1024 + = new NamedParameterSpec("SecP384r1MLKEM1024"); + private final String name; /** 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 623f83f547a8b..781648e322e84 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,11 @@ */ package sun.security.ssl; +import sun.security.util.Hybrid; +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 +36,9 @@ import java.io.IOException; import java.security.GeneralSecurityException; +import java.security.KeyFactory; import java.security.PrivateKey; import java.security.PublicKey; -import java.security.spec.AlgorithmParameterSpec; import sun.security.util.KeyUtil; /** @@ -46,15 +50,32 @@ public class KAKeyDerivation implements SSLKeyDerivation { private final HandshakeContext context; private final PrivateKey localPrivateKey; private final PublicKey peerPublicKey; + private final byte[] keyshare; + private final java.security.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 +115,14 @@ private SecretKey t12DeriveKey() throws IOException { } } - /** - * 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 +142,86 @@ private SecretKey t13DeriveKey(String type) // 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. + */ + public KEM.Encapsulated encapsulate(String algorithm) + 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); + 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); + } + } + + /** + * 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, earlySecret, saltSecret); + 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 0000000000000..c500a653bc3f0 --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/KEMKeyExchange.java @@ -0,0 +1,205 @@ +/* + * 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.*; +import java.security.spec.NamedParameterSpec; + +import sun.security.ssl.NamedGroup.NamedGroupSpec; +import sun.security.util.*; +import sun.security.x509.X509Key; + +import javax.crypto.SecretKey; + +/** + * 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 + 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; + } + + /** + * Parse the encoded Point into the KEMCredentials using the + * namedGroup. + */ + static KEMCredentials valueOf(NamedGroup namedGroup, + byte[] encodedPoint) throws IOException, + GeneralSecurityException { + + 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); + } + } + + 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); + try { + // For KEM: This receiver side (client) generates a key pair. + String algName = ((NamedParameterSpec)namedGroup.keAlgParamSpec). + getName(); + Provider provider = namedGroup.getProvider(); + KeyPairGenerator kpg = (provider != null) ? + KeyPairGenerator.getInstance(algName, provider) : + KeyPairGenerator.getInstance(algName); + + KeyPair kp = kpg.generateKeyPair(); + privateKey = kp.getPrivate(); + publicKey = kp.getPublic(); + } catch (GeneralSecurityException e) { + throw new RuntimeException( + "Could not generate XDH keypair", 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); + } + + public PublicKey getPublicKey() { + return publicKey; + } + + public PrivateKey getPrivateKey() { + return privateKey; + } + } + + static final class KEMSenderPossession extends KEMPossession { + + public SecretKey getKey() { + return key; + } + + public void setKey(SecretKey key) { + this.key = key; + } + + private SecretKey key; + + KEMSenderPossession(NamedGroup namedGroup) { + super(namedGroup); + } + + @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 sufficient XDHE 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 98e4693e9170c..21dceb0eeb9df 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 @@ 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,8 @@ private static byte[] getShare(ClientHandshakeContext chc, // 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 +362,16 @@ public void consume(ConnectionContext context, 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( - "key share entry of " + ng + " does not " + - " comply with algorithm constraints"); - } - kaCred = null; + 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"); } - } - - if (kaCred != null) { + } else { credentials.add(kaCred); } } catch (GeneralSecurityException ex) { @@ -513,7 +509,8 @@ private SHKeyShareProducer() { @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 +568,8 @@ public byte[] produce(ConnectionContext context, 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 +577,31 @@ public byte[] produce(ConnectionContext context, // 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. + if (pos instanceof KEMKeyExchange.KEMSenderPossession xp) { + for (SSLCredentials cred : shc.handshakeCredentials) { + if (cred 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.setKey(encaped.key()); + keyShare = new KeyShareEntry(ng.id, + encaped.encapsulation()); + } + } + } else { + keyShare = new KeyShareEntry(ng.id, pos.encode()); + } break; } @@ -663,19 +685,13 @@ public void consume(ConnectionContext context, 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 +712,33 @@ public void consume(ConnectionContext context, } } + 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 46280a0535516..5e0e1d3f99b66 100644 --- a/src/java.base/share/classes/sun/security/ssl/NamedGroup.java +++ b/src/java.base/share/classes/sun/security/ssl/NamedGroup.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2024, 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 @@ -24,6 +24,7 @@ */ package sun.security.ssl; +import com.sun.crypto.provider.DH; import java.io.IOException; import java.security.*; import java.security.spec.AlgorithmParameterSpec; @@ -214,6 +215,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, + NamedParameterSpec.X25519MLKEM768, + "DH"), + + SecP256r1MLKEM768(0x11eb, "SecP256r1MLKEM768", + NamedGroupSpec.NAMED_GROUP_KEM, + ProtocolVersion.PROTOCOLS_OF_13, + NamedParameterSpec.SecP256r1MLKEM768, + "DH"), + + SecP384r1MLKEM1024(0x11ed, "SecP384r1MLKEM1024", + NamedGroupSpec.NAMED_GROUP_KEM, + ProtocolVersion.PROTOCOLS_OF_13, + NamedParameterSpec.SecP384r1MLKEM1024, + "DH"), + // Elliptic Curves (RFC 4492) // // arbitrary prime and characteristic-2 curves @@ -234,22 +268,33 @@ enum NamedGroup { final AlgorithmParameterSpec keAlgParamSpec; final AlgorithmParameters keAlgParams; final boolean isAvailable; + final String defaultProviderName; // 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, + String defaultProviderName) { this.id = id; this.name = name; this.spec = namedGroupSpec; this.algorithm = namedGroupSpec.algorithm; this.supportedProtocols = supportedProtocols; this.keAlgParamSpec = keAlgParamSpec; + this.defaultProviderName = defaultProviderName; // Check if it is a supported named group. AlgorithmParameters algParams = null; @@ -266,9 +311,19 @@ enum NamedGroup { // Check the specific algorithm parameters. if (mediator) { try { - algParams = - AlgorithmParameters.getInstance(namedGroupSpec.algorithm); - algParams.init(keAlgParamSpec); + // Skip AlgorithmParameters for KEMs (not supported) + if (namedGroupSpec == NamedGroupSpec.NAMED_GROUP_KEM) { + if (defaultProviderName == null) { + KeyFactory.getInstance(name); + } else { + KeyFactory.getInstance(name, DH.PROVIDER); + } + } 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) { @@ -307,6 +362,14 @@ enum NamedGroup { this.keAlgParams = mediator ? algParams : null; } + Provider getProvider() { + if ("DH".equals(defaultProviderName)) { + return DH.PROVIDER; + } + // no fixed provider + return null; + } + // // The next set of methods search & retrieve NamedGroups. // @@ -545,6 +608,10 @@ SSLCredentials decodeCredentials( 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 +633,11 @@ SSLCredentials decodeCredentials(NamedGroup ng, 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 +650,8 @@ enum NamedGroupSpec implements NamedGroupScheme { // Finite Field Groups (XDH) NAMED_GROUP_XDH("XDH", XDHScheme.instance), + NAMED_GROUP_KEM("PQC", KEMScheme.instance), + // arbitrary prime and curves (ECDHE) NAMED_GROUP_ARBITRARY("EC", null), @@ -634,6 +708,15 @@ public SSLCredentials decodeCredentials(NamedGroup ng, 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 +822,42 @@ public SSLKeyDerivation createKeyDerivation( } } + 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); + } + + @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 +903,11 @@ static final class SupportedGroups { } else { // default groups NamedGroup[] groups = new NamedGroup[] { + // Hybrid key agreements + X25519MLKEM768, + SecP256r1MLKEM768, + SecP384r1MLKEM1024, + // Primary XDH (RFC 7748) curves X25519, @@ -799,8 +923,6 @@ static final class SupportedGroups { FFDHE_2048, FFDHE_3072, FFDHE_4096, - FFDHE_6144, - FFDHE_8192, }; groupList = new ArrayList<>(groups.length); 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 22a44590ce3c8..263308f0659f5 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 @@ static T13KeyAgreement valueOf(NamedGroup namedGroup) { @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 1d2faa5351f33..3b5fea6a31383 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,26 @@ public byte[] produce(ConnectionContext context, clientHello); shc.serverHelloRandom = shm.serverRandom; + // Key Encapsulation Mechanism (KEM): + // The decapsulator (the client) publishes a public key, and the + // encapsulator (the server) uses it to generate: + // - the shared secret + // - the encapsulation (ciphertext), which is sent back to the client. + // + // Traditional Key Agreement (KA): + // Both peers perform similar operations: generate a public key, + // send it, and compute a shared secret upon receiving the peer's + // public key. + // + // In JSSE, the server usually generates its key share and then + // derives the secret after receiving the client's share (KA). + // However, this is changed for KEM: the server (as encapsulator) + // must perform both actions — derive the secret and generate the + // encapsulated message at the same time during SHKeyShareProducer. + // 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 +610,27 @@ public byte[] produce(ConnectionContext context, "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 + && xp.getKey() != null) { + 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/util/Hybrid.java b/src/java.base/share/classes/sun/security/util/Hybrid.java new file mode 100644 index 0000000000000..4e6493970c65e --- /dev/null +++ b/src/java.base/share/classes/sun/security/util/Hybrid.java @@ -0,0 +1,411 @@ +/* + * 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.util; + +import com.sun.crypto.provider.DH; +import sun.security.x509.X509Key; + +import javax.crypto.DecapsulateException; +import javax.crypto.KEM; +import javax.crypto.KEMSpi; +import javax.crypto.SecretKey; +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.InvalidParameterException; +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; + +public class Hybrid { + + public record SecretKeyImpl(SecretKey k1, SecretKey k2) implements SecretKey { + @Override + public String getAlgorithm() { + return "Hybrid"; + } + + @Override + public String getFormat() { + return null; + } + + @Override + public byte[] getEncoded() { + return null; + } + } + + 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); + } + + private static KEM getKEM(String name) throws NoSuchAlgorithmException { + if (name.startsWith("secp") || name.equals("X25519") || + name.equals("X448")) { + return KEM.getInstance("DH", DH.PROVIDER); + } else { + return KEM.getInstance(name); + } + } + + public static class KeyPairGeneratorImpl extends KeyPairGeneratorSpi { + private final KeyPairGenerator left; + private final KeyPairGenerator right; + private final AlgorithmParameterSpec leftSpec; + private final AlgorithmParameterSpec rightSpec; + private boolean initialized = false; + + public KeyPairGeneratorImpl(String left, String right) + throws NoSuchAlgorithmException { + this.left = getKeyPairGenerator(left); + this.right = getKeyPairGenerator(right); + leftSpec = getSpec(left); + rightSpec = getSpec(right); + } + + @Override + public void initialize(int keysize, SecureRandom random) { + try { + left.initialize(leftSpec, random); + right.initialize(rightSpec, random); + initialized = true; + } catch (Exception e) { + throw new InvalidParameterException(e); + } + } + + @Override + public KeyPair generateKeyPair() { + if (!initialized) { + try { + left.initialize(leftSpec); + right.initialize(rightSpec); + initialized = true; + } catch (Exception e) { + throw new ProviderException(e); + } + } + 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 instanceof RawKeySpec rks) { + byte[] key = rks.getKeyArr(); + 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")) { + try { + leftKey = left.generatePublic(new RawKeySpec( + leftKeyBytes)); + } catch (Exception e) { + leftKey = left.generatePublic(new X509EncodedKeySpec( + 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")) { + try { + rightKey = right.generatePublic(new RawKeySpec( + rightKeyBytes)); + } catch (Exception e) { + rightKey = right.generatePublic(new X509EncodedKeySpec( + 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.toString()); + + } + + 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) { + ByteArrayOutputStream o = new ByteArrayOutputStream(); + Arrays.stream(inputs).forEach(o::writeBytes); + return o.toByteArray(); + } + + 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) { + var left = le.encapsulate(); + var right = re.encapsulate(); + if (from == 0 && to == le.secretSize() + re.secretSize()) { + return new KEM.Encapsulated( + new SecretKeyImpl(left.key(), right.key()), + concat(left.encapsulation(), right.encapsulation()), + null); + } else { + throw new IllegalArgumentException( + "Invalid range for encapsulation: from = " + from + + " to = " + to + ", expected total secret size = " + + (le.secretSize() + re.secretSize())); + } + } + + @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 { + var left = Arrays.copyOf(encapsulation, ld.encapsulationSize()); + var right = Arrays.copyOfRange(encapsulation, ld.encapsulationSize(), + encapsulation.length); + if (from == 0 && ld.secretSize() + rd.secretSize() == to) { + return new SecretKeyImpl(ld.decapsulate(left), + rd.decapsulate(right)); + } else { + throw new IllegalArgumentException( + "Invalid range for decapsulation: from = " + from + + " to = " + to + ", expected total secret size = " + + (ld.secretSize() + rd.secretSize())); + } + } + } + + /** + * 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. + */ + public record PublicKeyImpl(String algorithm, PublicKey left, + PublicKey right) implements PublicKey { + @Override + public String getAlgorithm() { + return algorithm; + } + + // getFormat() returns "RAW" if both keys are X509Key; otherwise null. + @Override + public String getFormat() { + if (left instanceof X509Key && right instanceof X509Key) { + return "RAW"; + } else { + return null; + } + } + + // getEncoded() returns the concatenation of the encoded bytes of the + // left and right public keys only if both are X509Key types. + @Override + public byte[] getEncoded() { + if (left instanceof X509Key xk1 && right instanceof X509Key xk2) { + return concat(xk1.getKeyAsBytes(), xk2.getKeyAsBytes()); + } else { + return null; + } + } + } + + /** + * Hybrid private key combines two underlying private keys (left and right). + * It is for internal use only. The private keys should never be exported. + */ + public 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 new byte[0]; + } + } +} 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 c83e06f651e80..1cfe3f9d95d7d 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 BitArray getKey() { 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/java/security/spec/TestNamedParameterSpec.java b/test/jdk/java/security/spec/TestNamedParameterSpec.java index 0da8b78bf5d2f..9ac561446d3b3 100644 --- a/test/jdk/java/security/spec/TestNamedParameterSpec.java +++ b/test/jdk/java/security/spec/TestNamedParameterSpec.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 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 8345057 + * @bug 8345057 8314323 * @summary Test the existence of constants inside the * java.security.spec.NamedParameterSpec class. * @run main TestNamedParameterSpec @@ -41,6 +41,7 @@ public class TestNamedParameterSpec { "ED25519", "ED448", "X25519", "X448", "ML_DSA_44", "ML_DSA_65", "ML_DSA_87", "ML_KEM_512", "ML_KEM_768", "ML_KEM_1024", + "X25519MLKEM768", "SecP256r1MLKEM768", "SecP384r1MLKEM1024", }; public static void main(String[] args) throws Exception { diff --git a/test/jdk/javax/net/ssl/SSLParameters/NamedGroups.java b/test/jdk/javax/net/ssl/SSLParameters/NamedGroups.java index 25f73606b967e..bc92f4c43d6d6 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 static void main(String[] args) throws Exception { "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, + false); + + runTest(new String[] { + "X25519MLKEM768", + "x25519" + }, + new String[] { + "X25519MLKEM768", + "x25519" + }, + false); } private static void runTest(String[] serverNamedGroups, diff --git a/test/jdk/javax/net/ssl/TLSv13/ClientHelloKeyShares.java b/test/jdk/javax/net/ssl/TLSv13/ClientHelloKeyShares.java index 56f37a9e4f154..8d524968b0f51 100644 --- a/test/jdk/javax/net/ssl/TLSv13/ClientHelloKeyShares.java +++ b/test/jdk/javax/net/ssl/TLSv13/ClientHelloKeyShares.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 @@ -27,16 +27,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.*; @@ -52,10 +57,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 { // Arguments to this test are an abitrary number of integer diff --git a/test/jdk/javax/net/ssl/TLSv13/HRRKeyShares.java b/test/jdk/javax/net/ssl/TLSv13/HRRKeyShares.java index 313b2c5084b06..b23094d99b78f 100644 --- a/test/jdk/javax/net/ssl/TLSv13/HRRKeyShares.java +++ b/test/jdk/javax/net/ssl/TLSv13/HRRKeyShares.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 @@ -27,10 +27,10 @@ /* * @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; @@ -63,6 +63,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; @@ -224,6 +228,18 @@ public static void main(String args[]) throws Exception { 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) { diff --git a/test/jdk/sun/security/ssl/CipherSuite/DisabledCurve.java b/test/jdk/sun/security/ssl/CipherSuite/DisabledCurve.java index 26304c5df957c..a13f8570f148a 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 static void main(String[] args) throws Exception { 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 static void main(String[] args) throws Exception { } } + + 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 5732f42982c7b..a4d0e2d69188c 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,7 @@ * questions. */ +import java.util.Arrays; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLSocket; @@ -29,7 +30,7 @@ /* * @test - * @bug 8224650 8242929 + * @bug 8224650 8242929 8314323 * @library /javax/net/ssl/templates * /javax/net/ssl/TLSCommon * /test/lib @@ -44,6 +45,9 @@ * @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 { @@ -77,6 +81,22 @@ public class NamedGroupsWithCipherSuite extends SSLSocketTemplate { CipherSuite.TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 }; + private static final String[] HYBRID_NAMEDGROUPS = { + "X25519MLKEM768", + "SecP256r1MLKEM768", + "SecP384r1MLKEM1024" + }; + + private static final Protocol[] HYBRID_PROTOCOL = new Protocol[] { + Protocol.TLSV1_3 + }; + + private static final CipherSuite[] HYBRID_CIPHER_SUITES = new CipherSuite[] { + CipherSuite.TLS_AES_128_GCM_SHA256, + CipherSuite.TLS_AES_256_GCM_SHA384, + CipherSuite.TLS_CHACHA20_POLY1305_SHA256 + }; + private String protocol; private String cipher; @@ -151,19 +171,27 @@ public static void main(String[] args) throws Exception { // 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 = Arrays.asList(HYBRID_NAMEDGROUPS). + contains(namedGroup); + Protocol[] protocolList = hybridGroup ? + HYBRID_PROTOCOL : PROTOCOLS; + 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)) { + && groupSupportdByCipher(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(); } } } @@ -171,6 +199,10 @@ && groupSupportdByCipher(namedGroup.toLowerCase(), private static boolean groupSupportdByCipher(String group, CipherSuite cipherSuite) { + if (Arrays.asList(HYBRID_NAMEDGROUPS).contains(group)) { + return cipherSuite.keyExAlgorithm == null; + } + return (group.startsWith("x") && xdhGroupSupportdByCipher(cipherSuite)) || (group.startsWith("secp") diff --git a/test/jdk/sun/security/ssl/CipherSuite/RestrictNamedGroup.java b/test/jdk/sun/security/ssl/CipherSuite/RestrictNamedGroup.java index c4c343bf84ee3..4ff0c6e6e159f 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 88b0bf2489aea..bbd62db3182eb 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 @@ public class SupportedGroups extends SSLSocketTemplate { private static volatile int index; - private static final String[][][] protocols = { + private static final String[][][] protocolForClassic = { {{"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[][][] protocolForHybrid = { + {{"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,17 @@ protected void configureServerSocket(SSLServerSocket serverSocket) { public static void main(String[] args) throws Exception { System.setProperty("jdk.tls.namedGroups", args[0]); + boolean hybridGroup = hybridNamedGroup(args[0]); + String[][][] protocols = hybridGroup ? protocolForHybrid : protocolForClassic; + 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/jdk/sun/security/ssl/DHKeyExchange/UseStrongDHSizes.java b/test/jdk/sun/security/ssl/DHKeyExchange/UseStrongDHSizes.java index 209db9d7461f4..aa0ba924a8820 100644 --- a/test/jdk/sun/security/ssl/DHKeyExchange/UseStrongDHSizes.java +++ b/test/jdk/sun/security/ssl/DHKeyExchange/UseStrongDHSizes.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 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 @@ -28,7 +28,7 @@ /* * @test - * @bug 8140436 + * @bug 8140436 8314323 * @modules jdk.crypto.ec * @library /javax/net/ssl/templates * @summary Negotiated Finite Field Diffie-Hellman Ephemeral Parameters for TLS @@ -47,10 +47,8 @@ * @run main/othervm -Djdk.tls.namedGroups=ffdhe4096 UseStrongDHSizes 4096 * @run main/othervm -Djdk.tls.namedGroups=ffdhe6144 UseStrongDHSizes 4096 * @run main/othervm -Djdk.tls.namedGroups=ffdhe8192 UseStrongDHSizes 4096 - * @run main/othervm UseStrongDHSizes 6144 * @run main/othervm -Djdk.tls.namedGroups=ffdhe6144 UseStrongDHSizes 6144 * @run main/othervm -Djdk.tls.namedGroups=ffdhe8192 UseStrongDHSizes 6144 - * @run main/othervm UseStrongDHSizes 8192 * @run main/othervm -Djdk.tls.namedGroups=ffdhe8192 UseStrongDHSizes 8192 */ diff --git a/test/micro/org/openjdk/bench/java/security/SSLHandshake.java b/test/micro/org/openjdk/bench/java/security/SSLHandshake.java index d8773781b58cc..b46704a01de77 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.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 @@ private static SSLContext getServerContext() { @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 @@ private void createSSLEngines() { */ 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 3386039c62ea8..19e40ac0912e6 100644 --- a/test/micro/org/openjdk/bench/javax/crypto/full/KEMBench.java +++ b/test/micro/org/openjdk/bench/javax/crypto/full/KEMBench.java @@ -23,17 +23,23 @@ 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 { @@ -42,15 +48,37 @@ public class KEMBench extends CryptoBase { @Param({"ML-KEM-512", "ML-KEM-768", "ML-KEM-1024" }) 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 { + public void setup() throws NoSuchAlgorithmException, InvalidKeyException, + InvalidAlgorithmParameterException { + String kpgAlg; + String kpgParams; kem = (prov == null) ? KEM.getInstance(algorithm) : KEM.getInstance(algorithm, prov); - KeyPairGenerator generator = (prov == null) ? KeyPairGenerator.getInstance(algorithm) : KeyPairGenerator.getInstance(algorithm, prov); + + Provider kpgProv = prov; // By default use the same provider for KEM and KPG + if (kpgSpec.isEmpty()) { + kpgAlg = algorithm; + kpgParams = ""; + } else { + String[] kpgTok = kpgSpec.split(":"); + kpgProv = Security.getProvider(kpgTok[0]); // kpgTok[0] = provider name + 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,6 +91,15 @@ public void setup() throws NoSuchAlgorithmException, InvalidKeyException { } } + protected static Provider getInternalJce() { + try { + Class dhClazz = Class.forName("com.sun.crypto.provider.DH"); + 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 { @@ -79,4 +116,49 @@ public void decapsulate(Blackhole bh) throws InvalidKeyException, DecapsulateExc } } + 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/com.sun.crypto.provider=ALL-UNNAMED"}) + public static class InternalDH 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/com.sun.crypto.provider=ALL-UNNAMED"}) + public static class 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 58daff28d8804..9ba4aef4b53ef 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 { @@ -50,6 +53,15 @@ public void setup() throws NoSuchAlgorithmException { } } + protected static Provider getInternalJce() { + try { + Class dhClazz = Class.forName("com.sun.crypto.provider.DH"); + return (Provider) (dhClazz.getField("PROVIDER").get(null)); + } catch (ReflectiveOperationException exc) { + throw new RuntimeException(exc); + } + } + @Benchmark public KeyPair generateKeyPair() { return generator.generateKeyPair(); @@ -118,4 +130,22 @@ public static class MLKEM extends KeyPairGeneratorBench { private int keyLength; } + @Fork(value = 5, jvmArgs = {"-XX:+AlwaysPreTouch", "--add-opens", "java.base/com.sun.crypto.provider=ALL-UNNAMED"}) + public static class 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; + } } From c7ba12a7cccf8de8ef1d4ce2e2a4eff2c9794723 Mon Sep 17 00:00:00 2001 From: Hai-May Chao Date: Thu, 2 Oct 2025 15:47:12 -0700 Subject: [PATCH 2/3] 8314323: TLS 1.3 Hybrid Key Exchange --- test/jdk/sun/security/pkcs11/tls/fips/FipsModeTLS.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/jdk/sun/security/pkcs11/tls/fips/FipsModeTLS.java b/test/jdk/sun/security/pkcs11/tls/fips/FipsModeTLS.java index c227e99d12bbf..d27ebec93d003 100644 --- a/test/jdk/sun/security/pkcs11/tls/fips/FipsModeTLS.java +++ b/test/jdk/sun/security/pkcs11/tls/fips/FipsModeTLS.java @@ -24,7 +24,7 @@ /* * @test - * @bug 8029661 8325164 8368073 8368514 8368520 + * @bug 8029661 8325164 8368073 8368514 8368520 8314323 * @summary Test TLS 1.2 and TLS 1.3 * @modules java.base/sun.security.internal.spec * java.base/sun.security.util @@ -35,7 +35,7 @@ * -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 + * @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; From 3c0e91ed0a123dccb051e3eade4915b7f40381e8 Mon Sep 17 00:00:00 2001 From: Hai-May Chao Date: Sun, 5 Oct 2025 05:11:55 -0700 Subject: [PATCH 3/3] Updates with review comments --- .../classes/com/sun/crypto/provider/DH.java | 16 ++++++------- .../classes/sun/security/ssl/NamedGroup.java | 2 ++ .../classes/sun/security/util/Hybrid.java | 24 +++++++++++++++---- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/java.base/share/classes/com/sun/crypto/provider/DH.java b/src/java.base/share/classes/com/sun/crypto/provider/DH.java index 72f4004bca790..d6e345640c83c 100644 --- a/src/java.base/share/classes/com/sun/crypto/provider/DH.java +++ b/src/java.base/share/classes/com/sun/crypto/provider/DH.java @@ -122,28 +122,26 @@ private ProviderImpl() { public EncapsulatorSpi engineNewEncapsulator( PublicKey publicKey, AlgorithmParameterSpec spec, SecureRandom secureRandom) throws InvalidKeyException { - return new Handler(publicKey, null, spec, secureRandom); + return new Handler(publicKey, null, secureRandom); } @Override public DecapsulatorSpi engineNewDecapsulator(PrivateKey privateKey, AlgorithmParameterSpec spec) throws InvalidKeyException { - return new Handler(null, privateKey, spec, null); + return new Handler(null, privateKey, null); } static final class Handler implements KEMSpi.EncapsulatorSpi, KEMSpi.DecapsulatorSpi { private final PublicKey pkR; private final PrivateKey skR; - private final AlgorithmParameterSpec spec; private final SecureRandom sr; private final Params params; - Handler(PublicKey pk, PrivateKey sk, AlgorithmParameterSpec spec, - SecureRandom sr) throws InvalidKeyException { + Handler(PublicKey pk, PrivateKey sk, SecureRandom sr) + throws InvalidKeyException { this.pkR = pk; this.skR = sk; - this.spec = spec; this.sr = sr; this.params = paramsFromKey(pk == null ? sk : pk); } @@ -221,9 +219,11 @@ private Params paramsFromKey(Key k) throws InvalidKeyException { } } else if (k instanceof XECKey xkey && xkey.getParams() instanceof NamedParameterSpec ns) { - if (ns.getName().equalsIgnoreCase("X25519")) { + if (ns.getName().equalsIgnoreCase( + NamedParameterSpec.X25519.getName())) { return Params.X25519; - } else if (ns.getName().equalsIgnoreCase("X448")) { + } else if (ns.getName().equalsIgnoreCase( + NamedParameterSpec.X448.getName())) { return Params.X448; } } 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 5e0e1d3f99b66..6bb03ed8d19f0 100644 --- a/src/java.base/share/classes/sun/security/ssl/NamedGroup.java +++ b/src/java.base/share/classes/sun/security/ssl/NamedGroup.java @@ -650,6 +650,8 @@ enum NamedGroupSpec implements NamedGroupScheme { // 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("PQC", KEMScheme.instance), // arbitrary prime and curves (ECDHE) diff --git a/src/java.base/share/classes/sun/security/util/Hybrid.java b/src/java.base/share/classes/sun/security/util/Hybrid.java index 4e6493970c65e..83303a8dfce09 100644 --- a/src/java.base/share/classes/sun/security/util/Hybrid.java +++ b/src/java.base/share/classes/sun/security/util/Hybrid.java @@ -95,9 +95,15 @@ private static KeyFactory getKeyFactory(String name) throws 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 DH.PROVIDER. + * For ML-KEM post-quantum algorithms, we obtain a KEM instance + * using the given algorithm name. + */ private static KEM getKEM(String name) throws NoSuchAlgorithmException { - if (name.startsWith("secp") || name.equals("X25519") || - name.equals("X448")) { + if (name.startsWith("secp") || name.equals("X25519")) { return KEM.getInstance("DH", DH.PROVIDER); } else { return KEM.getInstance(name); @@ -286,9 +292,17 @@ public DecapsulatorSpi engineNewDecapsulator(PrivateKey privateKey, } private static byte[] concat(byte[]... inputs) { - ByteArrayOutputStream o = new ByteArrayOutputStream(); - Arrays.stream(inputs).forEach(o::writeBytes); - return o.toByteArray(); + 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,