diff --git a/src/java.base/share/classes/sun/security/ssl/CompressedCertificate.java b/src/java.base/share/classes/sun/security/ssl/CompressedCertificate.java index ebd281b450f..eaec43f37f2 100644 --- a/src/java.base/share/classes/sun/security/ssl/CompressedCertificate.java +++ b/src/java.base/share/classes/sun/security/ssl/CompressedCertificate.java @@ -30,7 +30,10 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.text.MessageFormat; import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; +import java.util.zip.CRC32C; import javax.net.ssl.SSLProtocolException; import sun.security.ssl.SSLHandshake.HandshakeMessage; import sun.security.util.HexDumpEncoder; @@ -138,6 +141,18 @@ final class CompressedCertificate { private static final class CompressedCertProducer implements HandshakeProducer { + // Only local certificates are compressed, so it makes sense to store + // the deflated certificate data in a memory cache statically and avoid + // compressing local certificates repeatedly for every handshake. + private static final Map CACHE = + new ConcurrentHashMap<>(); + + // Limit the size of the cache in case certificate_request_context is + // randomized (should only happen during post-handshake authentication + // and only on the client side). Allow 4 cache mappings per algorithm. + private static final int MAX_CACHE_SIZE = + CompressionAlgorithm.values().length * 4; + // Prevent instantiation of this class. private CompressedCertProducer() { // blank @@ -156,8 +171,27 @@ final class CompressedCertificate { HandshakeOutStream hos = new HandshakeOutStream(null); message.send(hos); byte[] certMsg = hos.toByteArray(); - byte[] compressedCertMsg = - hc.certDeflater.getValue().apply(certMsg); + + long key = getCacheKey(certMsg, hc.certDeflater.getKey()); + byte[] compressedCertMsg = CACHE.get(key); + + if (compressedCertMsg == null) { + compressedCertMsg = hc.certDeflater.getValue().apply(certMsg); + + if (CACHE.size() < MAX_CACHE_SIZE) { + if (SSLLogger.isOn() && SSLLogger.isOn("ssl,handshake")) { + SSLLogger.fine("Caching CompressedCertificate message"); + } + + CACHE.put(key, compressedCertMsg); + } else { + if (SSLLogger.isOn() && SSLLogger.isOn("ssl,handshake")) { + SSLLogger.warning("Certificate message cache size limit" + + " of " + MAX_CACHE_SIZE + " reached"); + } + } + } + if (compressedCertMsg == null || compressedCertMsg.length == 0) { throw hc.conContext.fatal(Alert.HANDSHAKE_FAILURE, "No compressed Certificate data"); @@ -179,6 +213,16 @@ final class CompressedCertificate { // The handshake message has been delivered. return null; } + + private static long getCacheKey(byte[] input, int algId) { + CRC32C crc32c = new CRC32C(); + crc32c.update(input); + // Upper 32 bits of the 64 bit long returned by CRC32C are not used + // and set to zero, put input's length and algorithm id there. + return crc32c.getValue() // 32 bits + | (long) input.length << 32 // 16 bits + | (long) algId << 48; // 16 bits + } } /** diff --git a/src/java.base/share/classes/sun/security/ssl/CompressionAlgorithm.java b/src/java.base/share/classes/sun/security/ssl/CompressionAlgorithm.java index e227ed5da7c..ee4f686f8fd 100644 --- a/src/java.base/share/classes/sun/security/ssl/CompressionAlgorithm.java +++ b/src/java.base/share/classes/sun/security/ssl/CompressionAlgorithm.java @@ -30,9 +30,7 @@ import java.io.ByteArrayOutputStream; import java.util.AbstractMap; import java.util.LinkedHashMap; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; -import java.util.zip.CRC32C; import java.util.zip.Deflater; import java.util.zip.Inflater; @@ -40,22 +38,15 @@ import java.util.zip.Inflater; * Enum for TLS certificate compression algorithms. */ enum CompressionAlgorithm { - ZLIB(1, "zlib", new ConcurrentHashMap<>(3)), - // Placeholders, we currently support only ZLIB. - BROTLI(2, "brotli", null), - ZSTD(3, "zstd", null); + // Currently only ZLIB is supported. + ZLIB(1, "zlib"); final int id; final String name; - // We compress only local certificates, so it's ok to store the - // deflated certificate data in a memory cache permanently, i.e. such - // cache is going to be of a manageable size. - final Map cache; // Checksum -> deflated data. - CompressionAlgorithm(int id, String name, Map cache) { + CompressionAlgorithm(int id, String name) { this.id = id; this.name = name; - this.cache = cache; } static CompressionAlgorithm nameOf(String name) { @@ -87,7 +78,7 @@ enum CompressionAlgorithm { static Map> findInflaters( SSLConfiguration config) { if (config.certInflaters == null || config.certInflaters.isEmpty()) { - if (SSLLogger.isOn() && SSLLogger.isOn("ssl")) { + if (SSLLogger.isOn() && SSLLogger.isOn("ssl,handshake")) { SSLLogger.finest( "No supported certificate compression algorithms"); } @@ -102,7 +93,7 @@ enum CompressionAlgorithm { CompressionAlgorithm ca = CompressionAlgorithm.nameOf(entry.getKey()); if (ca == null) { - if (SSLLogger.isOn() && SSLLogger.isOn("ssl")) { + if (SSLLogger.isOn() && SSLLogger.isOn("ssl,handshake")) { SSLLogger.finest("Ignore unsupported certificate " + "compression algorithm: " + entry.getKey()); } @@ -141,36 +132,29 @@ enum CompressionAlgorithm { // Default Deflaters and Inflaters. static Map> getDefaultDeflaters() { - return Map.of(ZLIB.name, (input) -> - ZLIB.cache.computeIfAbsent(getChecksum(input), _ -> { - if (SSLLogger.isOn() && SSLLogger.isOn("ssl")) { - SSLLogger.info("Deflating and caching new " + ZLIB.name - + " certificate data of " + input.length - + " bytes"); - } + return Map.of(ZLIB.name, (input) -> { + try (Deflater deflater = new Deflater(); + ByteArrayOutputStream outputStream = + new ByteArrayOutputStream(input.length)) { - try (Deflater deflater = new Deflater(); - ByteArrayOutputStream outputStream = - new ByteArrayOutputStream(input.length)) { + deflater.setInput(input); + deflater.finish(); + byte[] buffer = new byte[1024]; - deflater.setInput(input); - deflater.finish(); - byte[] buffer = new byte[1024]; + while (!deflater.finished()) { + int compressedSize = deflater.deflate(buffer); + outputStream.write(buffer, 0, compressedSize); + } - while (!deflater.finished()) { - int compressedSize = deflater.deflate(buffer); - outputStream.write(buffer, 0, compressedSize); - } - - return outputStream.toByteArray(); - } catch (Exception e) { - if (SSLLogger.isOn() && SSLLogger.isOn("ssl")) { - SSLLogger.warning("Exception during certificate " - + "compression: ", e); - } - return null; - } - })); + return outputStream.toByteArray(); + } catch (Exception e) { + if (SSLLogger.isOn() && SSLLogger.isOn("ssl,handshake")) { + SSLLogger.warning("Exception during certificate " + + "compression: ", e); + } + return null; + } + }); } static Map> getDefaultInflaters() { @@ -189,7 +173,7 @@ enum CompressionAlgorithm { return outputStream.toByteArray(); } catch (Exception e) { - if (SSLLogger.isOn() && SSLLogger.isOn("ssl")) { + if (SSLLogger.isOn() && SSLLogger.isOn("ssl,handshake")) { SSLLogger.warning( "Exception during certificate decompression: ", e); } @@ -197,13 +181,4 @@ enum CompressionAlgorithm { } }); } - - // Fast checksum. - private static long getChecksum(byte[] input) { - CRC32C crc32c = new CRC32C(); - crc32c.update(input); - // The upper 32 bits are not used in the long returned by CRC32C, - // place input's length there to reduce the chance of collision. - return crc32c.getValue() | (long) input.length << 32; - } } diff --git a/test/jdk/sun/security/ssl/CertificateCompression/CompressedCertMsgCache.java b/test/jdk/sun/security/ssl/CertificateCompression/CompressedCertMsgCache.java new file mode 100644 index 00000000000..85c7495a9e6 --- /dev/null +++ b/test/jdk/sun/security/ssl/CertificateCompression/CompressedCertMsgCache.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2026, 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. + * + * 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. + */ + +/* + * @test + * @bug 8372526 + * @summary Make sure the same CompressedCertificate message is cached + * only once. + * @library /javax/net/ssl/templates + * /test/lib + * @run main/othervm CompressedCertMsgCache + */ + +import static jdk.test.lib.Asserts.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +public class CompressedCertMsgCache extends SSLSocketTemplate { + + public static void main(String[] args) throws Exception { + System.setProperty("javax.net.debug", "ssl"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream err = new PrintStream(baos); + System.setErr(err); + + // Complete 2 handshakes with the same certificate. + new CompressedCertMsgCache().run(); + new CompressedCertMsgCache().run(); + + err.close(); + + // Make sure the same CompressedCertificate message is cached only once + assertEquals(1, countSubstringOccurrences(baos.toString(), + "Caching CompressedCertificate message")); + } + + protected static int countSubstringOccurrences(String str, String sub) { + if (str == null || sub == null || sub.isEmpty()) { + return 0; + } + + int count = 0; + int lastIndex = 0; + + while ((lastIndex = str.indexOf(sub, lastIndex)) != -1) { + count++; + lastIndex += sub.length(); + } + + return count; + } +} diff --git a/test/jdk/sun/security/ssl/CertificateCompression/CompressedCertMsgCacheLimit.java b/test/jdk/sun/security/ssl/CertificateCompression/CompressedCertMsgCacheLimit.java new file mode 100644 index 00000000000..1ac6e7e4bd3 --- /dev/null +++ b/test/jdk/sun/security/ssl/CertificateCompression/CompressedCertMsgCacheLimit.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2026, 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. + * + * 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. + */ + +import static jdk.test.lib.Asserts.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import jdk.test.lib.security.CertificateBuilder; + +/* + * @test + * @bug 8372526 + * @summary Check CompressedCertificate message cache size limit. + * @modules java.base/sun.security.x509 + * java.base/sun.security.util + * @library /javax/net/ssl/templates + * /test/lib + * @run main/othervm CompressedCertMsgCacheLimit + */ + +public class CompressedCertMsgCacheLimit extends CompressedCertMsgCache { + + // Make sure every certificate has random serial number. + private static final SecureRandom RANDOM = new SecureRandom(); + + private final String protocol; + private final String keyAlg; + private final String certSigAlg; + private X509Certificate trustedCert; + private X509Certificate serverCert; + private X509Certificate clientCert; + private KeyPair serverKeys; + private KeyPair clientKeys; + + protected CompressedCertMsgCacheLimit( + String protocol, String keyAlg, + String certSigAlg) throws Exception { + super(); + this.protocol = protocol; + this.keyAlg = keyAlg; + this.certSigAlg = certSigAlg; + setupCertificates(); + } + + public static void main(String[] args) throws Exception { + System.setProperty("javax.net.debug", "ssl"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream err = new PrintStream(baos); + System.setErr(err); + + // Complete 6 handshakes, all with different certificates. + for (int i = 0; i < 6; i++) { + new CompressedCertMsgCacheLimit( + "TLSv1.3", "EC", "SHA256withECDSA").run(); + } + + err.close(); + + // Make sure first 4 CompressedCertificate messages are cached. + assertEquals(4, countSubstringOccurrences(baos.toString(), + "Caching CompressedCertificate message")); + + // Last 2 CompressedCertificate messages must not be cached. + assertEquals(2, countSubstringOccurrences(baos.toString(), + "Certificate message cache size limit of 4 reached")); + } + + @Override + public SSLContext createServerSSLContext() throws Exception { + return getSSLContext( + trustedCert, serverCert, serverKeys.getPrivate(), protocol); + } + + @Override + public SSLContext createClientSSLContext() throws Exception { + return getSSLContext( + trustedCert, clientCert, clientKeys.getPrivate(), protocol); + } + + private static SSLContext getSSLContext( + X509Certificate trustedCertificate, X509Certificate keyCertificate, + PrivateKey privateKey, String protocol) + throws Exception { + + // create a key store + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(null, null); + + // import the trusted cert + ks.setCertificateEntry("TLS Signer", trustedCertificate); + + // generate certificate chain + Certificate[] chain = new Certificate[2]; + chain[0] = keyCertificate; + chain[1] = trustedCertificate; + + // import the key entry. + final char[] passphrase = "passphrase".toCharArray(); + ks.setKeyEntry("Whatever", privateKey, passphrase, chain); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); + tmf.init(ks); + + // create SSL context + SSLContext ctx = SSLContext.getInstance(protocol); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(ks, passphrase); + + ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + + return ctx; + } + + // Certificate-building helper methods. + + private void setupCertificates() throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(keyAlg); + KeyPair caKeys = kpg.generateKeyPair(); + this.serverKeys = kpg.generateKeyPair(); + this.clientKeys = kpg.generateKeyPair(); + + this.trustedCert = createTrustedCert(caKeys, certSigAlg); + + this.serverCert = customCertificateBuilder( + "O=Some-Org, L=Some-City, ST=Some-State, C=US", + serverKeys.getPublic(), caKeys.getPublic()) + .addBasicConstraintsExt(false, false, -1) + .build(trustedCert, caKeys.getPrivate(), certSigAlg); + + this.clientCert = customCertificateBuilder( + "CN=localhost, OU=SSL-Client, ST=Some-State, C=US", + clientKeys.getPublic(), caKeys.getPublic()) + .addBasicConstraintsExt(false, false, -1) + .build(trustedCert, caKeys.getPrivate(), certSigAlg); + } + + private static X509Certificate createTrustedCert( + KeyPair caKeys, String certSigAlg) throws Exception { + return customCertificateBuilder( + "O=CA-Org, L=Some-City, ST=Some-State, C=US", + caKeys.getPublic(), caKeys.getPublic()) + .addBasicConstraintsExt(true, true, 1) + .build(null, caKeys.getPrivate(), certSigAlg); + } + + private static CertificateBuilder customCertificateBuilder( + String subjectName, PublicKey publicKey, PublicKey caKey) + throws CertificateException, IOException { + return new CertificateBuilder() + .setSubjectName(subjectName) + .setPublicKey(publicKey) + .setNotBefore( + Date.from(Instant.now().minus(1, ChronoUnit.HOURS))) + .setNotAfter(Date.from(Instant.now().plus(1, ChronoUnit.HOURS))) + .setSerialNumber(BigInteger.valueOf( + RANDOM.nextLong(1000000) + 1)) + .addSubjectKeyIdExt(publicKey) + .addAuthorityKeyIdExt(caKey) + .addKeyUsageExt(new boolean[]{ + true, true, true, true, true, true, true}); + } +} diff --git a/test/micro/org/openjdk/bench/java/security/SSLHandshake.java b/test/micro/org/openjdk/bench/java/security/SSLHandshake.java index b46704a01de..8771b7a2eef 100644 --- a/test/micro/org/openjdk/bench/java/security/SSLHandshake.java +++ b/test/micro/org/openjdk/bench/java/security/SSLHandshake.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2026, 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 @@ -76,6 +76,9 @@ public class SSLHandshake { @Param({"true", "false"}) boolean resume; + @Param({"true", "false"}) + boolean certCompression; + @Param({ "TLSv1.2-secp256r1", "TLSv1.3-x25519", "TLSv1.3-secp256r1", "TLSv1.3-secp384r1", @@ -211,10 +214,12 @@ public class SSLHandshake { // Set the key exchange named group in client and server engines SSLParameters clientParams = clientEngine.getSSLParameters(); clientParams.setNamedGroups(new String[]{namedGroup}); + clientParams.setEnableCertificateCompression(certCompression); clientEngine.setSSLParameters(clientParams); SSLParameters serverParams = serverEngine.getSSLParameters(); serverParams.setNamedGroups(new String[]{namedGroup}); + serverParams.setEnableCertificateCompression(certCompression); serverEngine.setSSLParameters(serverParams); } }