Move caching to CompressedCertificate. Add caching unit tests. Update benchmark test.

This commit is contained in:
Artur Barashev 2026-01-26 12:37:04 -05:00
parent c0fd9b868a
commit 4f99f4fb09
5 changed files with 351 additions and 54 deletions

View File

@ -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<Long, byte[]> 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
}
}
/**

View File

@ -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<Long, byte[]> cache; // Checksum -> deflated data.
CompressionAlgorithm(int id, String name, Map<Long, byte[]> 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<Integer, Function<byte[], byte[]>> 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<String, Function<byte[], byte[]>> 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<String, Function<byte[], byte[]>> 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;
}
}

View File

@ -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;
}
}

View File

@ -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});
}
}

View File

@ -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);
}
}