mirror of
https://github.com/openjdk/jdk.git
synced 2026-01-28 12:09:14 +00:00
Move caching to CompressedCertificate. Add caching unit tests. Update benchmark test.
This commit is contained in:
parent
c0fd9b868a
commit
4f99f4fb09
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user