diff --git a/src/java.base/share/classes/javax/net/ssl/SSLParameters.java b/src/java.base/share/classes/javax/net/ssl/SSLParameters.java index 3ac38bd91dba9..7272d9793a557 100644 --- a/src/java.base/share/classes/javax/net/ssl/SSLParameters.java +++ b/src/java.base/share/classes/javax/net/ssl/SSLParameters.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 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 @@ -86,6 +86,7 @@ public class SSLParameters { private String[] applicationProtocols = new String[0]; private String[] signatureSchemes = null; private String[] namedGroups = null; + private boolean enableCertificateCompression = true; /** * Constructs SSLParameters. @@ -960,4 +961,38 @@ public void setNamedGroups(String[] namedGroups) { this.namedGroups = tempGroups; } + + /** + * Sets whether TLS certificate compression should be enabled. + *

+ * This method only applies to TLSv1.3. + * + * @param enableCertificateCompression + * {@code true} indicates that TLS certificate compression + * should be enabled; {@code false} indicates that TLS certificate + * compression should be disabled + * + * @see #getEnableCertificateCompression() + * + * @since 27 + */ + public void setEnableCertificateCompression( + boolean enableCertificateCompression) { + this.enableCertificateCompression = enableCertificateCompression; + } + + /** + * Returns whether TLS certificate compression should be enabled + *

+ * This method only applies to TLSv1.3. + * + * @return true, if TLS certificate compression should be enabled + * + * @see #setEnableCertificateCompression(boolean) + * + * @since 27 + */ + public boolean getEnableCertificateCompression() { + return this.enableCertificateCompression; + } } diff --git a/src/java.base/share/classes/sun/security/ssl/Alert.java b/src/java.base/share/classes/sun/security/ssl/Alert.java index d172206326f1f..52541a5f18ff2 100644 --- a/src/java.base/share/classes/sun/security/ssl/Alert.java +++ b/src/java.base/share/classes/sun/security/ssl/Alert.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 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 @@ -281,6 +281,8 @@ public void consume(ConnectionContext context, // consumer so the state machine doesn't expect it. tc.handshakeContext.handshakeConsumers.remove( SSLHandshake.CERTIFICATE.id); + tc.handshakeContext.handshakeConsumers.remove( + SSLHandshake.COMPRESSED_CERTIFICATE.id); tc.handshakeContext.handshakeConsumers.remove( SSLHandshake.CERTIFICATE_VERIFY.id); } diff --git a/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java b/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java index 2a2db34cab93b..bc6423945fadc 100644 --- a/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java +++ b/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 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 @@ -917,16 +917,26 @@ public byte[] produce(ConnectionContext context, HandshakeMessage message) throws IOException { // The producing happens in handshake context only. HandshakeContext hc = (HandshakeContext)context; - if (hc.sslConfig.isClientMode) { - return onProduceCertificate( - (ClientHandshakeContext)context, message); - } else { - return onProduceCertificate( + T13CertificateMessage cm = hc.sslConfig.isClientMode ? + onProduceCertificate( + (ClientHandshakeContext)context, message) : + onProduceCertificate( (ServerHandshakeContext)context, message); + + // Output the handshake message. + if (hc.certDeflater == null) { + cm.write(hc.handshakeOutput); + hc.handshakeOutput.flush(); + } else { + // Replace with CompressedCertificate message + CompressedCertificate.handshakeProducer.produce(hc, cm); } + + // The handshake message has been delivered. + return null; } - private byte[] onProduceCertificate(ServerHandshakeContext shc, + private T13CertificateMessage onProduceCertificate(ServerHandshakeContext shc, HandshakeMessage message) throws IOException { ClientHelloMessage clientHello = (ClientHelloMessage)message; @@ -984,12 +994,7 @@ private byte[] onProduceCertificate(ServerHandshakeContext shc, SSLLogger.fine("Produced server Certificate message", cm); } - // Output the handshake message. - cm.write(shc.handshakeOutput); - shc.handshakeOutput.flush(); - - // The handshake message has been delivered. - return null; + return cm; } private static SSLPossession choosePossession( @@ -1028,7 +1033,7 @@ private static SSLPossession choosePossession( return pos; } - private byte[] onProduceCertificate(ClientHandshakeContext chc, + private T13CertificateMessage onProduceCertificate(ClientHandshakeContext chc, HandshakeMessage message) throws IOException { ClientHelloMessage clientHello = (ClientHelloMessage)message; SSLPossession pos = choosePossession(chc, clientHello); @@ -1071,12 +1076,7 @@ private byte[] onProduceCertificate(ClientHandshakeContext chc, SSLLogger.fine("Produced client Certificate message", cm); } - // Output the handshake message. - cm.write(chc.handshakeOutput); - chc.handshakeOutput.flush(); - - // The handshake message has been delivered. - return null; + return cm; } } @@ -1096,6 +1096,7 @@ public void consume(ConnectionContext context, HandshakeContext hc = (HandshakeContext)context; // clean up this consumer + hc.handshakeConsumers.remove(SSLHandshake.COMPRESSED_CERTIFICATE.id); hc.handshakeConsumers.remove(SSLHandshake.CERTIFICATE.id); // Ensure that the Certificate message has not been sent w/o diff --git a/src/java.base/share/classes/sun/security/ssl/CertificateRequest.java b/src/java.base/share/classes/sun/security/ssl/CertificateRequest.java index a297d9d21b265..44ae7e117403f 100644 --- a/src/java.base/share/classes/sun/security/ssl/CertificateRequest.java +++ b/src/java.base/share/classes/sun/security/ssl/CertificateRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 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 @@ -945,6 +945,11 @@ public byte[] produce(ConnectionContext context, // update // shc.certRequestContext = crm.requestContext.clone(); + if (shc.certInflaters != null && !shc.certInflaters.isEmpty()) { + shc.handshakeConsumers.put( + SSLHandshake.COMPRESSED_CERTIFICATE.id, + SSLHandshake.COMPRESSED_CERTIFICATE); + } shc.handshakeConsumers.put(SSLHandshake.CERTIFICATE.id, SSLHandshake.CERTIFICATE); shc.handshakeConsumers.put(SSLHandshake.CERTIFICATE_VERIFY.id, diff --git a/src/java.base/share/classes/sun/security/ssl/CompressCertExtension.java b/src/java.base/share/classes/sun/security/ssl/CompressCertExtension.java new file mode 100644 index 0000000000000..6f69081995f13 --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/CompressCertExtension.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2022 THL A29 Limited, a Tencent company. All rights reserved. + * 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. 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.nio.ByteBuffer; +import java.text.MessageFormat; +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; +import javax.net.ssl.SSLProtocolException; +import sun.security.ssl.SSLExtension.ExtensionConsumer; +import sun.security.ssl.SSLExtension.SSLExtensionSpec; +import sun.security.ssl.SSLHandshake.HandshakeMessage; + +/** + * Pack of the "compress_certificate" extensions [RFC 5246]. + */ +final class CompressCertExtension { + + static final HandshakeProducer chNetworkProducer = + new CHCompressCertificateProducer(); + static final ExtensionConsumer chOnLoadConsumer = + new CHCompressCertificateConsumer(); + + static final HandshakeProducer crNetworkProducer = + new CRCompressCertificateProducer(); + static final ExtensionConsumer crOnLoadConsumer = + new CRCompressCertificateConsumer(); + + static final SSLStringizer ccStringizer = + new CompressCertificateStringizer(); + + /** + * The "signature_algorithms" extension. + */ + static final class CertCompressionSpec implements SSLExtensionSpec { + + private final int[] compressionAlgorithms; // non-null + + CertCompressionSpec( + Map> certInflaters) { + compressionAlgorithms = new int[certInflaters.size()]; + int i = 0; + for (Integer id : certInflaters.keySet()) { + compressionAlgorithms[i++] = id; + } + } + + CertCompressionSpec(HandshakeContext hc, + ByteBuffer buffer) throws IOException { + if (buffer.remaining() < 2) { // 2: the length of the list + throw hc.conContext.fatal(Alert.DECODE_ERROR, + new SSLProtocolException( + "Invalid compress_certificate: insufficient data")); + } + + byte[] algs = Record.getBytes8(buffer); + if (buffer.hasRemaining()) { + throw hc.conContext.fatal(Alert.DECODE_ERROR, + new SSLProtocolException( + "Invalid compress_certificate: unknown extra data")); + } + + if (algs.length == 0 || (algs.length & 0x01) != 0) { + throw hc.conContext.fatal(Alert.DECODE_ERROR, + new SSLProtocolException( + "Invalid compress_certificate: incomplete data")); + } + + int[] compressionAlgs = new int[algs.length / 2]; + for (int i = 0, j = 0; i < algs.length; ) { + byte hash = algs[i++]; + byte sign = algs[i++]; + compressionAlgs[j++] = ((hash & 0xFF) << 8) | (sign & 0xFF); + } + + this.compressionAlgorithms = compressionAlgs; + } + + @Override + public String toString() { + MessageFormat messageFormat = new MessageFormat( + "\"compression algorithms\": '['{0}']'", Locale.ENGLISH); + + if (compressionAlgorithms.length == 0) { + Object[] messageFields = { + "" + }; + return messageFormat.format(messageFields); + } else { + StringBuilder builder = new StringBuilder(512); + boolean isFirst = true; + for (int ca : compressionAlgorithms) { + if (isFirst) { + isFirst = false; + } else { + builder.append(", "); + } + + builder.append(CompressionAlgorithm.nameOf(ca)); + } + + Object[] messageFields = { + builder.toString() + }; + + return messageFormat.format(messageFields); + } + } + } + + private static final + class CompressCertificateStringizer implements SSLStringizer { + + @Override + public String toString(HandshakeContext hc, ByteBuffer buffer) { + try { + return (new CertCompressionSpec(hc, buffer)).toString(); + } catch (IOException ioe) { + // For debug logging only, so please swallow exceptions. + return ioe.getMessage(); + } + } + } + + /** + * Network data producer of a "compress_certificate" extension in + * the ClientHello handshake message. + */ + private static final + class CHCompressCertificateProducer implements HandshakeProducer { + + // Prevent instantiation of this class. + private CHCompressCertificateProducer() { + // blank + } + + @Override + public byte[] produce(ConnectionContext context, + HandshakeMessage message) throws IOException { + // The producing happens in client side only. + return produceCompCertExt(context, + SSLExtension.CH_COMPRESS_CERTIFICATE); + } + } + + /** + * Network data consumer of a "compress_certificate" extension in + * the ClientHello handshake message. + */ + private static final + class CHCompressCertificateConsumer implements ExtensionConsumer { + + // Prevent instantiation of this class. + private CHCompressCertificateConsumer() { + // blank + } + + @Override + public void consume(ConnectionContext context, + HandshakeMessage message, ByteBuffer buffer) + throws IOException { + // The consuming happens in server side only. + consumeCompCertExt(context, buffer, + SSLExtension.CH_COMPRESS_CERTIFICATE); + } + } + + /** + * Network data producer of a "compress_certificate" extension in + * the CertificateRequest handshake message. + */ + private static final + class CRCompressCertificateProducer implements HandshakeProducer { + + // Prevent instantiation of this class. + private CRCompressCertificateProducer() { + // blank + } + + @Override + public byte[] produce(ConnectionContext context, + HandshakeMessage message) throws IOException { + // The producing happens in server side only. + return produceCompCertExt(context, + SSLExtension.CR_COMPRESS_CERTIFICATE); + } + } + + /** + * Network data consumer of a "compress_certificate" extension in + * the CertificateRequest handshake message. + */ + private static final + class CRCompressCertificateConsumer implements ExtensionConsumer { + + // Prevent instantiation of this class. + private CRCompressCertificateConsumer() { + // blank + } + + @Override + public void consume(ConnectionContext context, + HandshakeMessage message, ByteBuffer buffer) + throws IOException { + // The consuming happens in client side only. + consumeCompCertExt(context, buffer, + SSLExtension.CR_COMPRESS_CERTIFICATE); + } + } + + private static byte[] produceCompCertExt( + ConnectionContext context, SSLExtension extension) + throws IOException { + + HandshakeContext hc = (HandshakeContext) context; + // Is it a supported and enabled extension? + if (!hc.sslConfig.isAvailable(extension)) { + if (SSLLogger.isOn() && SSLLogger.isOn("ssl,handshake")) { + SSLLogger.fine("Ignore unavailable " + + "compress_certificate extension"); + } + return null; + } + + // Produce the extension. + if (hc.certInflaters == null) { + hc.certInflaters = + CompressionAlgorithm.findInflaters(hc.sslConfig); + } + + if (hc.certInflaters.isEmpty()) { + if (SSLLogger.isOn() && SSLLogger.isOn("ssl,handshake")) { + SSLLogger.fine("Ignore unsupported " + + "compress_certificate extension"); + } + return null; + } + + int vectorLen = CompressionAlgorithm.sizeInRecord() * + hc.certInflaters.size(); + byte[] extData = new byte[vectorLen + 1]; + ByteBuffer m = ByteBuffer.wrap(extData); + Record.putInt8(m, vectorLen); + for (Integer algId : hc.certInflaters.keySet()) { + Record.putInt16(m, algId); + } + + // Update the context. + hc.handshakeExtensions.put( + extension, new CertCompressionSpec(hc.certInflaters)); + + return extData; + } + + private static void consumeCompCertExt(ConnectionContext context, + ByteBuffer buffer, SSLExtension extension) throws IOException { + + HandshakeContext hc = (HandshakeContext) context; + // Is it a supported and enabled extension? + if (!hc.sslConfig.isAvailable(extension)) { + if (SSLLogger.isOn() && SSLLogger.isOn("ssl,handshake")) { + SSLLogger.fine("Ignore unavailable " + + "compress_certificate extension"); + } + return; // ignore the extension + } + + if (hc.sslConfig.certDeflaters == null || + hc.sslConfig.certDeflaters.isEmpty()) { + if (SSLLogger.isOn() && SSLLogger.isOn("ssl,handshake")) { + SSLLogger.fine("Ignore unsupported " + + "compress_certificate extension"); + } + return; // ignore the extension + } + + // Parse the extension. + CertCompressionSpec spec = new CertCompressionSpec(hc, buffer); + + // Update the context. + hc.certDeflater = CompressionAlgorithm.selectDeflater( + hc.sslConfig, spec.compressionAlgorithms); + if (hc.certDeflater == null) { + if (SSLLogger.isOn() && SSLLogger.isOn("ssl,handshake")) { + SSLLogger.fine("Ignore, no supported " + + "certificate compression algorithms"); + } + } + // No impact on session resumption. + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/CompressedCertificate.java b/src/java.base/share/classes/sun/security/ssl/CompressedCertificate.java new file mode 100644 index 0000000000000..a2e57ce5b009f --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/CompressedCertificate.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2022 THL A29 Limited, a Tencent company. All rights reserved. + * 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package sun.security.ssl; + +import sun.security.ssl.SSLHandshake.HandshakeMessage; +import sun.security.util.HexDumpEncoder; + +import javax.net.ssl.*; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.text.MessageFormat; +import java.util.*; +import java.util.function.Function; + +/** + * Pack of the CompressedCertificate handshake message. + */ +final class CompressedCertificate { + static final SSLConsumer handshakeConsumer = + new CompressedCertConsumer(); + static final HandshakeProducer handshakeProducer = + new CompressedCertProducer(); + + /** + * The CompressedCertificate handshake message for TLS 1.3. + */ + static final class CompressedCertMessage extends HandshakeMessage { + private final int algorithmId; + private final int uncompressedLength; + private final byte[] compressedCert; + + CompressedCertMessage(HandshakeContext context, + int algorithmId, int uncompressedLength, + byte[] compressedCert) { + super(context); + + this.algorithmId = algorithmId; + this.uncompressedLength = uncompressedLength; + this.compressedCert = compressedCert; + } + + CompressedCertMessage(HandshakeContext handshakeContext, + ByteBuffer m) throws IOException { + super(handshakeContext); + + // struct { + // CertificateCompressionAlgorithm algorithm; + // uint24 uncompressed_length; + // opaque compressed_certificate_message<1..2^24-1>; + // } CompressedCertificate; + if (m.remaining() < 9) { + throw new SSLProtocolException( + "Invalid CompressedCertificate message: " + + "insufficient data (length=" + m.remaining() + ")"); + } + this.algorithmId = Record.getInt16(m); + this.uncompressedLength = Record.getInt24(m); + this.compressedCert = Record.getBytes24(m); + + if (m.hasRemaining()) { + throw handshakeContext.conContext.fatal( + Alert.HANDSHAKE_FAILURE, + "Invalid CompressedCertificate message: " + + "unknown extra data"); + } + } + + @Override + public SSLHandshake handshakeType() { + return SSLHandshake.COMPRESSED_CERTIFICATE; + } + + @Override + public int messageLength() { + return 8 + compressedCert.length; + } + + @Override + public void send(HandshakeOutStream hos) throws IOException { + hos.putInt16(algorithmId); + hos.putInt24(uncompressedLength); + hos.putBytes24(compressedCert); + } + + @Override + public String toString() { + MessageFormat messageFormat = new MessageFormat( + """ + "CompressedCertificate": '{' + "algorithm": "{0}", + "uncompressed_length": {1} + "compressed_certificate_message": [ + {2} + ] + '}'""", + Locale.ENGLISH); + + HexDumpEncoder hexEncoder = new HexDumpEncoder(); + Object[] messageFields = { + CompressionAlgorithm.nameOf(algorithmId), + uncompressedLength, + Utilities.indent(hexEncoder.encode(compressedCert), " ") + }; + + return messageFormat.format(messageFields); + } + } + + /** + * The "Certificate" handshake message producer for TLS 1.3. + */ + private static final + class CompressedCertProducer implements HandshakeProducer { + // Prevent instantiation of this class. + private CompressedCertProducer() { + // blank + } + + // Note this is a special producer, which can only be called from + // the CertificateMessage producer. The input 'message' parameter + // represents the Certificate handshake message. + @Override + public byte[] produce(ConnectionContext context, + HandshakeMessage message) throws IOException { + // The producing happens in handshake context only. + HandshakeContext hc = (HandshakeContext)context; + + // Compress the Certificate message. + HandshakeOutStream hos = new HandshakeOutStream(null); + message.send(hos); + byte[] certMsg = hos.toByteArray(); + byte[] compressedCertMsg = + hc.certDeflater.getValue().apply(certMsg); + if (compressedCertMsg == null || compressedCertMsg.length == 0) { + throw hc.conContext.fatal(Alert.HANDSHAKE_FAILURE, + "No compressed Certificate data"); + } + + CompressedCertMessage ccm = new CompressedCertMessage(hc, + hc.certDeflater.getKey(), certMsg.length, + compressedCertMsg); + + if (SSLLogger.isOn() && SSLLogger.isOn("ssl,handshake")) { + SSLLogger.fine("Produced Compressed Certificate message", ccm); + } + + ccm.write(hc.handshakeOutput); + hc.handshakeOutput.flush(); + + // The handshake message has been delivered. + return null; + } + } + + /** + * The "Certificate" handshake message consumer for TLS 1.3. + */ + private static final class CompressedCertConsumer implements SSLConsumer { + // Prevent instantiation of this class. + private CompressedCertConsumer() { + // blank + } + + @Override + public void consume(ConnectionContext context, + ByteBuffer message) throws IOException { + // The consuming happens in handshake context only. + HandshakeContext hc = (HandshakeContext)context; + + // clean up this consumer + hc.handshakeConsumers.remove(SSLHandshake.COMPRESSED_CERTIFICATE.id); + hc.handshakeConsumers.remove(SSLHandshake.CERTIFICATE.id); + + // Parse the handshake message + CompressedCertMessage ccm = new CompressedCertMessage(hc, message); + if (SSLLogger.isOn() && SSLLogger.isOn("ssl,handshake")) { + SSLLogger.fine( + "Consuming CompressedCertificate handshake message", ccm); + } + + // check the compression algorithm + Function inflater = + hc.certInflaters.get(ccm.algorithmId); + if (inflater == null) { + throw hc.conContext.fatal(Alert.BAD_CERTIFICATE, + "Unsupported certificate compression algorithm"); + } + + // decompress + byte[] certificateMessage = inflater.apply(ccm.compressedCert); + + // check the uncompressed length + if (certificateMessage == null || + certificateMessage.length != ccm.uncompressedLength) { + throw hc.conContext.fatal(Alert.BAD_CERTIFICATE, + "Improper certificate compression"); + } + + // Call the Certificate handshake message consumer. + CertificateMessage.t13HandshakeConsumer.consume(hc, + ByteBuffer.wrap(certificateMessage)); + } + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/CompressionAlgorithm.java b/src/java.base/share/classes/sun/security/ssl/CompressionAlgorithm.java new file mode 100644 index 0000000000000..d44a107dfd2f1 --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/CompressionAlgorithm.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2022 THL A29 Limited, a Tencent company. All rights reserved. + * 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. 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.ByteArrayOutputStream; +import java.util.AbstractMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.zip.Deflater; +import java.util.zip.Inflater; + +/** + * Enum for TLS certificate compression algorithms. + */ +enum CompressionAlgorithm { + ZLIB(1, "zlib"), + BROTLI(2, "brotli"), + ZSTD(3, "zstd"); + + final int id; + final String name; + + CompressionAlgorithm(int id, String name) { + this.id = id; + this.name = name; + } + + static CompressionAlgorithm nameOf(String name) { + for (CompressionAlgorithm cca : + CompressionAlgorithm.values()) { + if (cca.name.equals(name)) { + return cca; + } + } + + return null; + } + + static String nameOf(int id) { + for (CompressionAlgorithm cca : + CompressionAlgorithm.values()) { + if (cca.id == id) { + return cca.name; + } + } + + return ""; + } + + // Return the size of a compression algorithms structure in TLS record + static int sizeInRecord() { + return 2; + } + + // Get local supported algorithm collection. + static Map> findInflaters( + SSLConfiguration config) { + if (config.certInflaters == null || config.certInflaters.isEmpty()) { + if (SSLLogger.isOn() && SSLLogger.isOn("ssl,handshake")) { + SSLLogger.finest( + "No supported certificate compression algorithms"); + } + return Map.of(); + } + + Map> inflaters = + new LinkedHashMap<>(config.certInflaters.size()); + + for (Map.Entry> entry : + config.certInflaters.entrySet()) { + CompressionAlgorithm ca = + CompressionAlgorithm.nameOf(entry.getKey()); + if (ca == null) { + if (SSLLogger.isOn() && SSLLogger.isOn("ssl,handshake")) { + SSLLogger.finest("Ignore unsupported certificate " + + "compression algorithm: " + entry.getKey()); + } + continue; + } + + inflaters.putIfAbsent(ca.id, entry.getValue()); + } + + return inflaters; + } + + static Map.Entry> selectDeflater( + SSLConfiguration config, int[] compressionAlgorithmIds) { + if (config.certDeflaters == null) { + return null; + } + + for (Map.Entry> entry : + config.certDeflaters.entrySet()) { + CompressionAlgorithm ca = + CompressionAlgorithm.nameOf(entry.getKey()); + if (ca != null) { + for (int id : compressionAlgorithmIds) { + if (ca.id == id) { + return new AbstractMap.SimpleImmutableEntry<>( + id, entry.getValue()); + } + } + } + } + + return null; + } + + // Default Deflaters and Inflaters. + // We currently support only ZLIB internally. + + static Map> getDefaultDeflaters() { + return Map.of(ZLIB.name, (input) -> { + try (Deflater deflater = new Deflater(); + ByteArrayOutputStream outputStream = + new ByteArrayOutputStream(input.length)) { + + deflater.setInput(input); + deflater.finish(); + byte[] buffer = new byte[1024]; + + 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,handshake")) { + SSLLogger.warning( + "Exception during certificate compression: ", e); + } + return null; + } + }); + } + + static Map> getDefaultInflaters() { + return Map.of(ZLIB.name, (input) -> { + try (Inflater inflater = new Inflater(); + ByteArrayOutputStream outputStream = + new ByteArrayOutputStream(input.length)) { + + inflater.setInput(input); + byte[] buffer = new byte[1024]; + + while (!inflater.finished()) { + int decompressedSize = inflater.inflate(buffer); + outputStream.write(buffer, 0, decompressedSize); + } + + return outputStream.toByteArray(); + } catch (Exception e) { + if (SSLLogger.isOn() && SSLLogger.isOn("ssl,handshake")) { + SSLLogger.warning( + "Exception during certificate decompression: ", e); + } + return null; + } + }); + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/HandshakeContext.java b/src/java.base/share/classes/sun/security/ssl/HandshakeContext.java index a5f340d520343..0503e12c92be5 100644 --- a/src/java.base/share/classes/sun/security/ssl/HandshakeContext.java +++ b/src/java.base/share/classes/sun/security/ssl/HandshakeContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 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 @@ -33,6 +33,7 @@ import java.security.CryptoPrimitive; import java.util.*; import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.function.Function; import javax.crypto.SecretKey; import javax.net.ssl.SNIServerName; import javax.net.ssl.SSLHandshakeException; @@ -131,6 +132,10 @@ abstract class HandshakeContext implements ConnectionContext { List peerRequestedSignatureSchemes; List peerRequestedCertSignSchemes; + // CertificateCompressionAlgorithm + Map> certInflaters; + Map.Entry> certDeflater; + // Known authorities X500Principal[] peerSupportedAuthorities = null; diff --git a/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java b/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java index ace60e41af9b0..bb62801d16860 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 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 @@ -29,6 +29,7 @@ import java.security.NoSuchAlgorithmException; import java.util.*; import java.util.function.BiFunction; +import java.util.function.Function; import javax.crypto.KeyGenerator; import javax.net.ssl.HandshakeCompletedListener; import javax.net.ssl.SNIMatcher; @@ -64,6 +65,11 @@ final class SSLConfiguration implements Cloneable { // the configured named groups for the "supported_groups" extensions String[] namedGroups; + // The configured certificate compression algorithms for + // "compress_certificate" extensions + Map> certDeflaters; + Map> certInflaters; + // the maximum protocol version of enabled protocols ProtocolVersion maximumProtocolVersion; @@ -248,6 +254,16 @@ final class SSLConfiguration implements Cloneable { CustomizedServerSignatureSchemes.signatureSchemes != null ? CustomizedServerSignatureSchemes.signatureSchemes : SupportedSigSchemes.DEFAULT; + + if (Utilities.getBooleanProperty( + "jdk.tls.enableCertificateCompression", true)) { + this.certDeflaters = CompressionAlgorithm.getDefaultDeflaters(); + this.certInflaters = CompressionAlgorithm.getDefaultInflaters(); + } else { + this.certDeflaters = Map.of(); + this.certInflaters = Map.of(); + } + this.namedGroups = NamedGroup.SupportedGroups.namedGroups; this.maximumProtocolVersion = ProtocolVersion.NONE; for (ProtocolVersion pv : enabledProtocols) { @@ -305,6 +321,8 @@ SSLParameters getSSLParameters() { params.setMaximumPacketSize(this.maximumPacketSize); params.setSignatureSchemes(this.signatureSchemes); params.setNamedGroups(this.namedGroups); + params.setEnableCertificateCompression(!this.certInflaters.isEmpty() + && !this.certDeflaters.isEmpty()); return params; } @@ -380,6 +398,14 @@ void setSSLParameters(SSLParameters params) { this.namedGroups = NamedGroup.SupportedGroups.namedGroups; } + if (params.getEnableCertificateCompression()) { + this.certDeflaters = CompressionAlgorithm.getDefaultDeflaters(); + this.certInflaters = CompressionAlgorithm.getDefaultInflaters(); + } else { + this.certDeflaters = Map.of(); + this.certInflaters = Map.of(); + } + this.preferLocalCipherSuites = params.getUseCipherSuitesOrder(); this.enableRetransmissions = params.getEnableRetransmissions(); this.maximumPacketSize = params.getMaximumPacketSize(); diff --git a/src/java.base/share/classes/sun/security/ssl/SSLExtension.java b/src/java.base/share/classes/sun/security/ssl/SSLExtension.java index 47a0d0b0e4407..2c43605f75416 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLExtension.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 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 @@ -270,6 +270,27 @@ enum SSLExtension implements SSLStringizer { // Extensions defined in RFC 7924 (TLS Cached Information Extension) CACHED_INFO (0x0019, "cached_info"), + // Extensions defined in RFC 8879 (TLS Certificate Compression) + CH_COMPRESS_CERTIFICATE (0x001B, "compress_certificate", + SSLHandshake.CLIENT_HELLO, + ProtocolVersion.PROTOCOLS_OF_13, + CompressCertExtension.chNetworkProducer, + CompressCertExtension.chOnLoadConsumer, + null, + null, + null, + CompressCertExtension.ccStringizer), + + CR_COMPRESS_CERTIFICATE (0x001B, "compress_certificate", + SSLHandshake.CERTIFICATE_REQUEST, + ProtocolVersion.PROTOCOLS_OF_13, + CompressCertExtension.crNetworkProducer, + CompressCertExtension.crOnLoadConsumer, + null, + null, + null, + CompressCertExtension.ccStringizer), + // Extensions defined in RFC 5077 (TLS Session Resumption without Server-Side State) CH_SESSION_TICKET (0x0023, "session_ticket", SSLHandshake.CLIENT_HELLO, diff --git a/src/java.base/share/classes/sun/security/ssl/SSLHandshake.java b/src/java.base/share/classes/sun/security/ssl/SSLHandshake.java index 7c78f6c300526..2c6b58bafa51a 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLHandshake.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLHandshake.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 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 @@ -370,7 +370,22 @@ enum SSLHandshake implements SSLConsumer, HandshakeProducer { }), // RFC 8879 - TLS Certificate Compression - COMPRESSED_CERTIFICATE ((byte)0x19, "compressed_certificate"), + @SuppressWarnings({"unchecked", "rawtypes"}) + COMPRESSED_CERTIFICATE ((byte)0x19, "compressed_certificate", + (new Map.Entry[] { + new SimpleImmutableEntry<>( + CompressedCertificate.handshakeConsumer, + ProtocolVersion.PROTOCOLS_OF_13 + ) + }), + (new Map.Entry[] { + // Note that the producing of this message is delegated to + // CertificateMessage producer. + new SimpleImmutableEntry<>( + CertificateMessage.t13HandshakeProducer, + ProtocolVersion.PROTOCOLS_OF_13 + ) + })), // RFC 8870 - Encrypted Key Transport for DTLS/Secure RTP EKT_KEY ((byte)0x1A, "ekt_key"), 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 76c266a628a68..903f43fa7eb98 100644 --- a/src/java.base/share/classes/sun/security/ssl/ServerHello.java +++ b/src/java.base/share/classes/sun/security/ssl/ServerHello.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 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 @@ -1411,6 +1411,11 @@ public void consume(ConnectionContext context, chc.handshakeConsumers.put( SSLHandshake.CERTIFICATE_REQUEST.id, SSLHandshake.CERTIFICATE_REQUEST); + if (chc.certInflaters != null && !chc.certInflaters.isEmpty()) { + chc.handshakeConsumers.put( + SSLHandshake.COMPRESSED_CERTIFICATE.id, + SSLHandshake.COMPRESSED_CERTIFICATE); + } chc.handshakeConsumers.put( SSLHandshake.CERTIFICATE.id, SSLHandshake.CERTIFICATE); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java index 20b0338215c77..96894ed5ee37b 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 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 @@ -719,6 +719,8 @@ public static SSLParameters copySSLParameters(SSLParameters p) { p1.setUseCipherSuitesOrder(p.getUseCipherSuitesOrder()); p1.setSignatureSchemes(p.getSignatureSchemes()); p1.setNamedGroups(p.getNamedGroups()); + p1.setWantClientAuth(p.getWantClientAuth()); + p1.setEnableCertificateCompression(p.getEnableCertificateCompression()); return p1; } diff --git a/test/jdk/javax/net/ssl/HttpsURLConnection/HttpsCompressedCert.java b/test/jdk/javax/net/ssl/HttpsURLConnection/HttpsCompressedCert.java new file mode 100644 index 0000000000000..064bf2ebb6b42 --- /dev/null +++ b/test/jdk/javax/net/ssl/HttpsURLConnection/HttpsCompressedCert.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2022 THL A29 Limited, a Tencent company. All rights reserved. + * 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 java.net.http.HttpResponse.BodyHandlers.ofString; +import static jdk.test.lib.Asserts.assertEquals; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import javax.net.ssl.SSLParameters; + +/* + * @test + * @bug 8372526 + * @summary TLS certificate compression + * @library /test/lib + * /javax/net/ssl/templates + * @run main/othervm HttpsCompressedCert true + * @run main/othervm HttpsCompressedCert false + */ + +public class HttpsCompressedCert { + public static void main(String[] args) throws Exception { + SSLParameters sslParameters = new SSLParameters(); + sslParameters.setEnableCertificateCompression( + Boolean.parseBoolean(args[0])); + HttpClient httpClient = HttpClient.newBuilder() + .sslContext(SSLClientContext.createClientSSLContext()) + .version(HttpClient.Version.HTTP_2) + .sslParameters(sslParameters) + .build(); + + HttpRequest httpRequest = HttpRequest.newBuilder( + new URI("https://www.google.com/")) + .GET() + .build(); + HttpResponse response = httpClient.send( + httpRequest, ofString()); + assertEquals(response.statusCode(), 200); + } +} + diff --git a/test/jdk/javax/net/ssl/SSLParameters/CompressedCert.java b/test/jdk/javax/net/ssl/SSLParameters/CompressedCert.java new file mode 100644 index 0000000000000..3de208a3009a5 --- /dev/null +++ b/test/jdk/javax/net/ssl/SSLParameters/CompressedCert.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2022 THL A29 Limited, a Tencent company. All rights reserved. + * 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 javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLSocket; + +/* + * @test + * @bug 8372526 + * @summary TLS Certificate Compression + * @library /javax/net/ssl/templates + * @run main/othervm CompressedCert + */ + +public class CompressedCert extends SSLSocketTemplate { + + private final boolean enableClientCertComp; + private final boolean enableServerCertComp; + private final boolean requireClientCert; + + public CompressedCert( + boolean enableClientCertComp, + boolean enableServerCertComp, + boolean requireClientCert) { + this.enableClientCertComp = enableClientCertComp; + this.enableServerCertComp = enableServerCertComp; + this.requireClientCert = requireClientCert; + } + + @Override + protected void configureServerSocket(SSLServerSocket sslServerSocket) { + SSLParameters sslParameters = sslServerSocket.getSSLParameters(); + sslParameters.setEnableCertificateCompression(enableClientCertComp); + sslParameters.setNeedClientAuth(requireClientCert); + sslServerSocket.setSSLParameters(sslParameters); + } + + @Override + protected void configureClientSocket(SSLSocket socket) { + SSLParameters sslParameters = socket.getSSLParameters(); + sslParameters.setEnableCertificateCompression(enableServerCertComp); + socket.setSSLParameters(sslParameters); + } + + public static void main(String[] args) throws Exception { + runTest(false, false, false); + runTest(false, false, true); + runTest(false, true, false); + runTest(true, false, true); + runTest(true, true, false); + runTest(true, true, true); + } + + private static void runTest( + boolean enableClientCertComp, + boolean enableServerCertComp, + boolean requireClientCert) throws Exception { + new CompressedCert(enableClientCertComp, enableServerCertComp, + requireClientCert).run(); + } +} diff --git a/test/jdk/javax/net/ssl/templates/SSLClientContext.java b/test/jdk/javax/net/ssl/templates/SSLClientContext.java new file mode 100644 index 0000000000000..46fcbaedcfd17 --- /dev/null +++ b/test/jdk/javax/net/ssl/templates/SSLClientContext.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2022 THL A29 Limited, a Tencent company. 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 javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import java.io.ByteArrayInputStream; +import java.security.KeyStore; +import java.security.cert.CertificateFactory; + +public enum SSLClientContext { + GTS_ROOT_R1(""" + -----BEGIN CERTIFICATE----- + MIIFWjCCA0KgAwIBAgIQbkepxUtHDA3sM9CJuRz04TANBgkqhkiG9w0BAQwFADBH + MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM + QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy + MDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl + cnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEB + AQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaM + f/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vX + mX7wCl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7 + zUjwTcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0P + fyblqAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtc + vfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4 + Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUsp + zBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOO + Rc92wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYW + k70paDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+ + DVrNVjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgF + lQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV + HQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBADiW + Cu49tJYeX++dnAsznyvgyv3SjgofQXSlfKqE1OXyHuY3UjKcC9FhHb8owbZEKTV1 + d5iyfNm9dKyKaOOpMQkpAWBz40d8U6iQSifvS9efk+eCNs6aaAyC58/UEBZvXw6Z + XPYfcX3v73svfuo21pdwCxXu11xWajOl40k4DLh9+42FpLFZXvRq4d2h9mREruZR + gyFmxhE+885H7pwoHyXa/6xmld01D1zvICxi/ZG6qcz8WpyTgYMpl0p8WnK0OdC3 + d8t5/Wk6kjftbjhlRn7pYL15iJdfOBL07q9bgsiG1eGZbYwE8na6SfZu6W0eX6Dv + J4J2QPim01hcDyxC2kLGe4g0x8HYRZvBPsVhHdljUEn2NIVq4BjFbkerQUIpm/Zg + DdIx02OYI5NaAIFItO/Nis3Jz5nu2Z6qNuFoS3FJFDYoOj0dzpqPJeaAcWErtXvM + +SUWgeExX6GjfhaknBZqlxi9dnKlC54dNuYvoS++cJEPqOba+MSSQGwlfnuzCdyy + F62ARPBopY+Udf90WuioAnwMCeKpSwughQtiue+hMZL77/ZRBIls6Kl0obsXs7X9 + SQ98POyDGCBDTtWTurQ0sR8WNh8M5mQ5Fkzc4P4dyKliPUDqysU0ArSuiYgzNdws + E3PYJ/HQcu51OyLemGhmW/HGY0dVHLqlCFF1pkgl + -----END CERTIFICATE-----"""), + + GTS_ROOT_R2(""" + -----BEGIN CERTIFICATE----- + MIIFWjCCA0KgAwIBAgIQbkepxlqz5yDFMJo/aFLybzANBgkqhkiG9w0BAQwFADBH + MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM + QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy + MDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl + cnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEB + AQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3Lv + CvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3Kg + GjSY6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9Bu + XvAuMC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOd + re7kRXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXu + PuWgf9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1 + mKPV+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K + 8YzodDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqj + x5RWIr9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsR + nTKaG73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0 + kzCqgc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9Ok + twIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV + HQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBALZp + 8KZ3/p7uC4Gt4cCpx/k1HUCCq+YEtN/L9x0Pg/B+E02NjO7jMyLDOfxA325BS0JT + vhaI8dI4XsRomRyYUpOM52jtG2pzegVATX9lO9ZY8c6DR2Dj/5epnGB3GFW1fgiT + z9D2PGcDFWEJ+YF59exTpJ/JjwGLc8R3dtyDovUMSRqodt6Sm2T4syzFJ9MHwAiA + pJiS4wGWAqoC7o87xdFtCjMwc3i5T1QWvwsHoaRc5svJXISPD+AVdyx+Jn7axEvb + pxZ3B7DNdehyQtaVhJ2Gg/LkkM0JR9SLA3DaWsYDQvTtN6LwG1BUSw7YhN4ZKJmB + R64JGz9I0cNv4rBgF/XuIwKl2gBbbZCr7qLpGzvpx0QnRY5rn/WkhLx3+WuXrD5R + RaIRpsyF7gpo8j5QOHokYh4XIDdtak23CZvJ/KRY9bb7nE4Yu5UC56GtmwfuNmsk + 0jmGwZODUNKBRqhfYlcsu2xkiAhu7xNUX90txGdj08+JN7+dIPT7eoOboB6BAFDC + 5AwiWVIQ7UNWhwD4FFKnHYuTjKJNRn8nxnGbJN7k2oaLDX5rIMHAnuFl2GqjpuiF + izoHCBy69Y9Vmhh1fuXsgWbRIXOhNUQLgD1bnF5vKheW0YMjiGZt5obicDIvUiLn + yOd/xCxgXS/Dr55FBcOEArf9LAhST4Ldo/DUhgkC + -----END CERTIFICATE-----"""), + + GTS_ROOT_R3(""" + -----BEGIN CERTIFICATE----- + MIICDDCCAZGgAwIBAgIQbkepx2ypcyRAiQ8DVd2NHTAKBggqhkjOPQQDAzBHMQsw + CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU + MBIGA1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw + MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp + Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQA + IgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout + 736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2A + DDL24CejQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud + DgQWBBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEAgFuk + fCPAlaUs3L6JbyO5o91lAFJekazInXJ0glMLfalAvWhgxeG4VDvBNhcl2MG9AjEA + njWSdIUlUfUk7GRSJFClH9voy8l27OyCbvWFGFPouOOaKaqW04MjyaR7YbPMAuhd + -----END CERTIFICATE-----"""), + + GTS_ROOT_R4(""" + -----BEGIN CERTIFICATE----- + MIICCjCCAZGgAwIBAgIQbkepyIuUtui7OyrYorLBmTAKBggqhkjOPQQDAzBHMQsw + CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU + MBIGA1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw + MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp + Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQA + IgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzu + hXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/l + xKvRHYqjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud + DgQWBBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNnADBkAjBqUFJ0 + CMRw3J5QdCHojXohw0+WbhXRIjVhLfoIN+4Zba3bssx9BzT1YBkstTTZbyACMANx + sbqjYAuG7ZoIapVon+Kz4ZNkfF6Tpt95LY2F45TPI11xzPKwTdb+mciUqXWi4w== + -----END CERTIFICATE-----"""); + + final String certStr; + + SSLClientContext(String certStr) { + this.certStr = certStr; + } + + public static SSLContext createClientSSLContext() throws Exception { + // Generate certificate from cert string. + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + + // Import the trusted certs. + KeyStore ts = KeyStore.getInstance("PKCS12"); + ts.load(null, null); + for (SSLClientContext clientContext : SSLClientContext.values()) { + try (ByteArrayInputStream is = new ByteArrayInputStream( + clientContext.certStr.getBytes())) { + ts.setCertificateEntry("trusted-cert-" + + clientContext.name(), cf.generateCertificate(is)); + } + } + + // Create an SSLContext object. + TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); + tmf.init(ts); + + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, tmf.getTrustManagers(), null); + + return context; + } +} diff --git a/test/jdk/sun/security/ssl/CertificateCompression/DefaultCertCompression.java b/test/jdk/sun/security/ssl/CertificateCompression/DefaultCertCompression.java new file mode 100644 index 0000000000000..8c9b25013fc8c --- /dev/null +++ b/test/jdk/sun/security/ssl/CertificateCompression/DefaultCertCompression.java @@ -0,0 +1,154 @@ +/* + * 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 static jdk.test.lib.Asserts.assertTrue; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/* + * @test + * @bug 8372526 + * @summary Add support for ZLIB TLS Certificate Compression. + * @library /javax/net/ssl/templates + * /test/lib + * @run main/othervm DefaultCertCompression + * @run main/othervm DefaultCertCompression -Djdk.tls.enableCertificateCompression=false + */ + +public class DefaultCertCompression extends SSLEngineTemplate { + + protected static final int CLI_HELLO_MSG = 1; + protected static final int COMP_CERT_EXT = 27; + // zlib(1), brotli(2), zstd(3) + protected static final List DEFAULT_COMP_ALGS = List.of(1); + private final boolean certCompEnabled; + + protected DefaultCertCompression() throws Exception { + certCompEnabled = Boolean.parseBoolean(System.getProperty( + "jdk.tls.enableCertificateCompression", "true")); + super(); + } + + public static void main(String[] args) throws Exception { + new DefaultCertCompression().run(); + } + + protected void run() throws Exception { + + // Produce client_hello + clientEngine.wrap(clientOut, cTOs); + cTOs.flip(); + + checkClientHello(); + } + + protected void checkClientHello() throws Exception { + if (certCompEnabled) { + assertTrue(DEFAULT_COMP_ALGS.equals(getCompAlgsCliHello( + extractHandshakeMsg(cTOs, CLI_HELLO_MSG, false)))); + } else { + assertEquals(getCompAlgsCliHello( + extractHandshakeMsg(cTOs, CLI_HELLO_MSG, false)).size(), 0, + "compress_certificate extension present in ClientHello"); + } + } + + /** + * Parses the ClientHello message and extracts from it a list of + * compression algorithm values. It is assumed that the provided + * ByteBuffer has its position set at the first byte of the ClientHello + * message body (AFTER the handshake header) and contains the entire + * hello message. Upon successful completion of this method the ByteBuffer + * will have its position reset to the initial offset in the buffer. + * If an exception is thrown the position at the time of the exception + * will be preserved. + * + * @param data The ByteBuffer containing the ClientHello bytes. + * @return A List of the compression algorithm values. + */ + protected List getCompAlgsCliHello(ByteBuffer data) { + Objects.requireNonNull(data); + data.mark(); + + // Skip over the protocol version and client random + data.position(data.position() + 34); + + // Jump past the session ID (if there is one) + int sessLen = Byte.toUnsignedInt(data.get()); + if (sessLen != 0) { + data.position(data.position() + sessLen); + } + + // Jump past the cipher suites + int csLen = Short.toUnsignedInt(data.getShort()); + if (csLen != 0) { + data.position(data.position() + csLen); + } + + // ...and the compression + int compLen = Byte.toUnsignedInt(data.get()); + if (compLen != 0) { + data.position(data.position() + compLen); + } + + List extSigAlgs = getCompAlgsFromExt(data); + + // We should be at the end of the ClientHello + data.reset(); + return extSigAlgs; + } + + /** + * Gets compression algorithms from the given TLS extension. + * The buffer should be positioned at the start of the extension. + */ + protected List getCompAlgsFromExt(ByteBuffer data) { + + List extCompAlgs = new ArrayList<>(); + data.getShort(); // read length + + while (data.hasRemaining()) { + int extType = Short.toUnsignedInt(data.getShort()); + int extLen = Short.toUnsignedInt(data.getShort()); + + if (extType == COMP_CERT_EXT) { + int sigSchemeLen = data.get(); + + for (int ssOff = 0; ssOff < sigSchemeLen; ssOff += 2) { + Integer schemeName = Short.toUnsignedInt(data.getShort()); + extCompAlgs.add(schemeName); + } + } else { + // Not the extension we're looking for. Skip past the + // extension data + data.position(data.position() + extLen); + } + } + + return extCompAlgs; + } +}