diff --git a/src/java.base/share/classes/sun/security/ssl/DTLSInputRecord.java b/src/java.base/share/classes/sun/security/ssl/DTLSInputRecord.java index 101a42a5407..4e82fd25a7b 100644 --- a/src/java.base/share/classes/sun/security/ssl/DTLSInputRecord.java +++ b/src/java.base/share/classes/sun/security/ssl/DTLSInputRecord.java @@ -170,7 +170,7 @@ final class DTLSInputRecord extends InputRecord implements DTLSRecord { // Buffer next epoch message if necessary. if (this.readEpoch < recordEpoch) { - // Discard the record younger than the current epcoh if: + // Discard the record younger than the current epoch if: // 1. it is not a handshake message, or // 3. it is not of next epoch. if ((contentType != ContentType.HANDSHAKE.id && @@ -1445,7 +1445,7 @@ final class DTLSInputRecord extends InputRecord implements DTLSRecord { // if (expectCCSFlight) { // Have the ChangeCipherSpec/Finished flight been received? - boolean isReady = hasFinishedMessage(bufferedFragments); + boolean isReady = hasFinishedMessage(); if (SSLLogger.isOn && SSLLogger.isOn("verbose")) { SSLLogger.fine( "Has the final flight been received? " + isReady); @@ -1492,7 +1492,7 @@ final class DTLSInputRecord extends InputRecord implements DTLSRecord { // // an abbreviated handshake // - if (hasFinishedMessage(bufferedFragments)) { + if (hasFinishedMessage()) { if (SSLLogger.isOn && SSLLogger.isOn("verbose")) { SSLLogger.fine("It's an abbreviated handshake."); } @@ -1565,7 +1565,7 @@ final class DTLSInputRecord extends InputRecord implements DTLSRecord { } } - if (!hasFinishedMessage(bufferedFragments)) { + if (!hasFinishedMessage()) { // not yet have the ChangeCipherSpec/Finished messages if (SSLLogger.isOn && SSLLogger.isOn("verbose")) { SSLLogger.fine( @@ -1601,35 +1601,33 @@ final class DTLSInputRecord extends InputRecord implements DTLSRecord { return false; } - // Looking for the ChangeCipherSpec and Finished messages. + // Looking for the ChangeCipherSpec, Finished and + // NewSessionTicket messages. // // As the cached Finished message should be a ciphertext, we don't // exactly know a ciphertext is a Finished message or not. According // to the spec of TLS/DTLS handshaking, a Finished message is always // sent immediately after a ChangeCipherSpec message. The first // ciphertext handshake message should be the expected Finished message. - private boolean hasFinishedMessage(Set fragments) { - + private boolean hasFinishedMessage() { boolean hasCCS = false; boolean hasFin = false; - for (RecordFragment fragment : fragments) { + + for (RecordFragment fragment : bufferedFragments) { if (fragment.contentType == ContentType.CHANGE_CIPHER_SPEC.id) { - if (hasFin) { - return true; - } hasCCS = true; - } else if (fragment.contentType == ContentType.HANDSHAKE.id) { - // Finished is the first expected message of a new epoch. - if (fragment.isCiphertext) { - if (hasCCS) { - return true; - } - hasFin = true; - } + } else if (fragment.contentType == ContentType.HANDSHAKE.id + && fragment.isCiphertext) { + hasFin = true; } } - return false; + // NewSessionTicket message presence in the Finished flight + // should only be expected on the client side, and only + // if stateless resumption is enabled. + return hasCCS && hasFin && (!tc.sslConfig.isClientMode + || !tc.handshakeContext.statelessResumption + || hasCompleted(SSLHandshake.NEW_SESSION_TICKET.id)); } // Is client CertificateVerify a mandatory message? @@ -1674,7 +1672,7 @@ final class DTLSInputRecord extends InputRecord implements DTLSRecord { int presentMsgSeq, int endMsgSeq) { // The caller should have checked the completion of the first - // present handshake message. Need not to check it again. + // present handshake message. Need not check it again. for (RecordFragment rFrag : fragments) { if ((rFrag.contentType != ContentType.HANDSHAKE.id) || rFrag.isCiphertext) { 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 082914b4b4b..fb0490d70f1 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLExtension.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLExtension.java @@ -286,7 +286,7 @@ enum SSLExtension implements SSLStringizer { ProtocolVersion.PROTOCOLS_10_12, SessionTicketExtension.shNetworkProducer, SessionTicketExtension.shOnLoadConsumer, - null, + SessionTicketExtension.shOnLoadAbsence, null, null, SessionTicketExtension.steStringizer), diff --git a/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java b/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java index 444af5d6dae..9a84bbad8fd 100644 --- a/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java +++ b/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java @@ -72,6 +72,8 @@ final class SessionTicketExtension { new T12SHSessionTicketProducer(); static final ExtensionConsumer shOnLoadConsumer = new T12SHSessionTicketConsumer(); + static final HandshakeAbsence shOnLoadAbsence = + new T12SHSessionTicketOnLoadAbsence(); static final SSLStringizer steStringizer = new SessionTicketStringizer(); // No need to compress a ticket if it can fit in a single packet. @@ -529,4 +531,27 @@ final class SessionTicketExtension { chc.statelessResumption = true; } } + + /** + * The absence processing if a "session_ticket" extension is + * not present in the ServerHello handshake message. + */ + private static final class T12SHSessionTicketOnLoadAbsence + implements HandshakeAbsence { + + @Override + public void absent(ConnectionContext context, + HandshakeMessage message) { + ClientHandshakeContext chc = (ClientHandshakeContext) context; + + // Disable stateless resumption if server doesn't send the extension. + if (chc.statelessResumption) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { + SSLLogger.info( + "Server doesn't support stateless resumption"); + } + chc.statelessResumption = false; + } + } + } } diff --git a/test/jdk/javax/net/ssl/DTLS/DTLSNoNewSessionTicket.java b/test/jdk/javax/net/ssl/DTLS/DTLSNoNewSessionTicket.java new file mode 100644 index 00000000000..dba191d0193 --- /dev/null +++ b/test/jdk/javax/net/ssl/DTLS/DTLSNoNewSessionTicket.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * 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 8367059 + * @summary DTLS: loss of NewSessionTicket message results in handshake failure + * @modules java.base/sun.security.util + * @library /test/lib + * @build DTLSOverDatagram + * + * @comment Make sure client doesn't expect NewSessionTicket in the final + * flight if server doesn't send the "session_ticket" extension with + * ServerHello handshake message. + * + * @run main/othervm -Djdk.tls.client.enableSessionTicketExtension=false + * DTLSNoNewSessionTicket + * @run main/othervm -Djdk.tls.server.enableSessionTicketExtension=false + * DTLSNoNewSessionTicket + */ + +public class DTLSNoNewSessionTicket extends DTLSOverDatagram { + public static void main(String[] args) throws Exception { + var testCase = new DTLSNoNewSessionTicket(); + testCase.runTest(testCase); + } +} diff --git a/test/jdk/javax/net/ssl/DTLS/DTLSOverDatagram.java b/test/jdk/javax/net/ssl/DTLS/DTLSOverDatagram.java index 9780586cd57..89fe39a792c 100644 --- a/test/jdk/javax/net/ssl/DTLS/DTLSOverDatagram.java +++ b/test/jdk/javax/net/ssl/DTLS/DTLSOverDatagram.java @@ -21,9 +21,6 @@ * questions. */ -// SunJSSE does not support dynamic system properties, no way to re-use -// system properties in samevm/agentvm mode. - /* * @test * @bug 8043758 @@ -132,8 +129,15 @@ public class DTLSOverDatagram { * The remainder is support stuff for DTLS operations. */ SSLEngine createSSLEngine(boolean isClient) throws Exception { - SSLContext context = getDTLSContext(); - SSLEngine engine = context.createSSLEngine(); + SSLContext context = + isClient ? getClientDTLSContext() : getServerDTLSContext(); + + // Note: client and server ports are not to be used for network + // communication, but only to be set in client and server SSL engines. + // We must use the same context, host and port for initial and resuming + // sessions when testing session resumption (abbreviated handshake). + SSLEngine engine = context.createSSLEngine("localhost", + isClient ? 51111 : 52222); SSLParameters paras = engine.getSSLParameters(); paras.setMaximumPacketSize(MAXIMUM_PACKET_SIZE); @@ -507,7 +511,7 @@ public class DTLSOverDatagram { } // get DTSL context - SSLContext getDTLSContext() throws Exception { + static SSLContext getDTLSContext() throws Exception { String passphrase = "passphrase"; return SSLContextBuilder.builder() .trustStore(KeyStoreUtils.loadKeyStore(TRUST_FILENAME, passphrase)) @@ -517,6 +521,14 @@ public class DTLSOverDatagram { .build(); } + protected SSLContext getServerDTLSContext() throws Exception { + return getDTLSContext(); + } + + protected SSLContext getClientDTLSContext() throws Exception { + return getDTLSContext(); + } + /* * ============================================================= diff --git a/test/jdk/javax/net/ssl/DTLS/PacketLossRetransmission.java b/test/jdk/javax/net/ssl/DTLS/PacketLossRetransmission.java index 2dd0de830f1..828e4c316b2 100644 --- a/test/jdk/javax/net/ssl/DTLS/PacketLossRetransmission.java +++ b/test/jdk/javax/net/ssl/DTLS/PacketLossRetransmission.java @@ -21,56 +21,95 @@ * questions. */ -// SunJSSE does not support dynamic system properties, no way to re-use -// system properties in samevm/agentvm mode. - /* * @test - * @bug 8161086 + * @bug 8161086 8367059 * @summary DTLS handshaking fails if some messages were lost * @modules java.base/sun.security.util * @library /test/lib * @build DTLSOverDatagram * - * @run main/othervm PacketLossRetransmission client 1 client_hello - * @run main/othervm PacketLossRetransmission client 16 client_key_exchange - * @run main/othervm PacketLossRetransmission client 20 finished - * @run main/othervm PacketLossRetransmission client -1 change_cipher_spec - * @run main/othervm PacketLossRetransmission server 2 server_hello - * @run main/othervm PacketLossRetransmission server 3 hello_verify_request - * @run main/othervm PacketLossRetransmission server 11 certificate - * @run main/othervm PacketLossRetransmission server 12 server_key_exchange - * @run main/othervm PacketLossRetransmission server 14 server_hello_done - * @run main/othervm PacketLossRetransmission server 20 finished - * @run main/othervm PacketLossRetransmission server -1 change_cipher_spec + * @run main/othervm PacketLossRetransmission client full 1 client_hello + * @run main/othervm PacketLossRetransmission client full 16 client_key_exchange + * @run main/othervm PacketLossRetransmission client full 20 finished + * @run main/othervm PacketLossRetransmission client full -1 change_cipher_spec + * @run main/othervm PacketLossRetransmission server full 2 server_hello + * @run main/othervm PacketLossRetransmission server full 3 hello_verify_request + * @run main/othervm PacketLossRetransmission server full 11 certificate + * @run main/othervm PacketLossRetransmission server full 12 server_key_exchange + * @run main/othervm PacketLossRetransmission server full 14 server_hello_done + * @run main/othervm PacketLossRetransmission server full 20 finished + * @run main/othervm PacketLossRetransmission server full -1 change_cipher_spec + * @run main/othervm PacketLossRetransmission server full 4 new_session_ticket + * @run main/othervm PacketLossRetransmission client resume 1 client_hello + * @run main/othervm PacketLossRetransmission client resume 20 finished + * @run main/othervm PacketLossRetransmission client resume -1 change_cipher_spec + * @run main/othervm PacketLossRetransmission server resume 2 server_hello + * @run main/othervm PacketLossRetransmission server resume 3 hello_verify_request + * @run main/othervm PacketLossRetransmission server resume 20 finished + * @run main/othervm PacketLossRetransmission server resume -1 change_cipher_spec + * @run main/othervm PacketLossRetransmission server resume 4 new_session_ticket */ + +import java.nio.ByteBuffer; import java.util.List; -import java.util.ArrayList; import java.net.DatagramPacket; import java.net.SocketAddress; +import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; /** * Test that DTLS implementation is able to do retransmission internally * automatically if packet get lost. */ public class PacketLossRetransmission extends DTLSOverDatagram { + private static boolean isClient; private static byte handshakeType; private static final int TIMEOUT = 500; private boolean needPacketLoss = true; + private final SSLContext clientContext; + private final SSLContext serverContext; + + protected PacketLossRetransmission() throws Exception { + this.clientContext = getDTLSContext(); + this.serverContext = getDTLSContext(); + } public static void main(String[] args) throws Exception { isClient = args[0].equals("client"); - handshakeType = Byte.parseByte(args[1]); + boolean isResume = args[1].equals("resume"); + handshakeType = Byte.parseByte(args[2]); PacketLossRetransmission testCase = new PacketLossRetransmission(); testCase.setSocketTimeout(TIMEOUT); + + if (isResume) { + System.out.println("Starting initial handshake"); + // The initial session will populate the TLS session cache. + initialSession(testCase.createSSLEngine(true), + testCase.createSSLEngine(false)); + } + testCase.runTest(testCase); } + @Override + protected SSLContext getClientDTLSContext() throws Exception { + return clientContext; + } + + @Override + protected SSLContext getServerDTLSContext() throws Exception { + return serverContext; + } + @Override boolean produceHandshakePackets(SSLEngine engine, SocketAddress socketAddr, String side, List packets) throws Exception { @@ -90,4 +129,170 @@ public class PacketLossRetransmission extends DTLSOverDatagram { return finished; } + + private static void initialSession(SSLEngine clientEngine, + SSLEngine serverEngine) throws SSLException { + boolean clientDone = false; + boolean serverDone = false; + boolean cliDataReady = false; + boolean servDataReady = false; + SSLEngineResult clientResult; + SSLEngineResult serverResult; + SSLSession session = clientEngine.getSession(); + int appBufferMax = session.getApplicationBufferSize(); + int netBufferMax = session.getPacketBufferSize(); + ByteBuffer clientIn = ByteBuffer.allocate(appBufferMax + 50); + ByteBuffer serverIn = ByteBuffer.allocate(appBufferMax + 50); + ByteBuffer cTOs = ByteBuffer.allocateDirect(netBufferMax); + ByteBuffer sTOc = ByteBuffer.allocateDirect(netBufferMax); + HandshakeStatus hsStat; + final ByteBuffer clientOut = ByteBuffer.wrap( + "Hi Server, I'm Client".getBytes()); + final ByteBuffer serverOut = ByteBuffer.wrap( + "Hello Client, I'm Server".getBytes()); + + clientEngine.beginHandshake(); + serverEngine.beginHandshake(); + while (!clientDone && !serverDone) { + // Client processing + hsStat = clientEngine.getHandshakeStatus(); + log("Client HS Stat: " + hsStat); + switch (hsStat) { + case NOT_HANDSHAKING: + log("Closing client engine"); + clientEngine.closeOutbound(); + clientDone = true; + break; + case NEED_WRAP: + log(String.format("CTOS: p:%d, l:%d, c:%d", cTOs.position(), + cTOs.limit(), cTOs.capacity())); + clientResult = clientEngine.wrap(clientOut, cTOs); + log("client wrap: ", clientResult); + if (clientResult.getStatus() + == SSLEngineResult.Status.BUFFER_OVERFLOW) { + // Get a larger buffer and try again + int updateSize = 2 * netBufferMax; + log("Resizing buffer to " + updateSize + " bytes"); + cTOs = ByteBuffer.allocate(updateSize); + clientResult = clientEngine.wrap(clientOut, cTOs); + log("client wrap (resized): ", clientResult); + } + runDelegatedTasks(clientResult, clientEngine); + cTOs.flip(); + cliDataReady = true; + break; + case NEED_UNWRAP: + if (servDataReady) { + log(String.format("STOC: p:%d, l:%d, c:%d", + sTOc.position(), + sTOc.limit(), sTOc.capacity())); + clientResult = clientEngine.unwrap(sTOc, clientIn); + log("client unwrap: ", clientResult); + runDelegatedTasks(clientResult, clientEngine); + servDataReady = sTOc.hasRemaining(); + sTOc.compact(); + } else { + log("Server-to-client data not ready, skipping client" + + " unwrap"); + } + break; + case NEED_UNWRAP_AGAIN: + clientResult = clientEngine.unwrap(ByteBuffer.allocate(0), + clientIn); + log("client unwrap (again): ", clientResult); + runDelegatedTasks(clientResult, clientEngine); + break; + } + + // Server processing + hsStat = serverEngine.getHandshakeStatus(); + log("Server HS Stat: " + hsStat); + switch (hsStat) { + case NEED_WRAP: + log(String.format("STOC: p:%d, l:%d, c:%d", sTOc.position(), + sTOc.limit(), sTOc.capacity())); + serverResult = serverEngine.wrap(serverOut, sTOc); + log("server wrap: ", serverResult); + if (serverResult.getStatus() + == SSLEngineResult.Status.BUFFER_OVERFLOW) { + // Get a new buffer and try again + int updateSize = 2 * netBufferMax; + log("Resizing buffer to " + updateSize + " bytes"); + sTOc = ByteBuffer.allocate(updateSize); + serverResult = serverEngine.wrap(clientOut, sTOc); + log("server wrap (resized): ", serverResult); + } + runDelegatedTasks(serverResult, serverEngine); + sTOc.flip(); + servDataReady = true; + break; + case NOT_HANDSHAKING: + log("Closing server engine"); + serverEngine.closeOutbound(); + serverDone = true; + break; + case NEED_UNWRAP: + if (cliDataReady) { + log(String.format("CTOS: p:%d, l:%d, c:%d", + cTOs.position(), + cTOs.limit(), cTOs.capacity())); + serverResult = serverEngine.unwrap(cTOs, serverIn); + log("server unwrap: ", serverResult); + runDelegatedTasks(serverResult, serverEngine); + cliDataReady = cTOs.hasRemaining(); + cTOs.compact(); + } else { + log("Client-to-server data not ready, skipping server" + + " unwrap"); + } + break; + case NEED_UNWRAP_AGAIN: + serverResult = serverEngine.unwrap(ByteBuffer.allocate(0), + serverIn); + log("server unwrap (again): ", serverResult); + runDelegatedTasks(serverResult, serverEngine); + break; + } + } + } + + private static void log(String str) { + System.out.println(str); + } + + private static void log(String str, SSLEngineResult result) { + System.out.println("The format of the SSLEngineResult is: \n" + + "\t\"getStatus() / getHandshakeStatus()\" +\n" + + "\t\"bytesConsumed() / bytesProduced()\"\n"); + + HandshakeStatus hsStatus = result.getHandshakeStatus(); + + log(str + + result.getStatus() + "/" + hsStatus + ", " + + result.bytesConsumed() + "/" + result.bytesProduced() + + " bytes"); + + if (hsStatus == HandshakeStatus.FINISHED) { + log("\t...ready for application data"); + } + } + + private static void runDelegatedTasks(SSLEngineResult result, + SSLEngine engine) { + HandshakeStatus hsStatus = result.getHandshakeStatus(); + + if (hsStatus == HandshakeStatus.NEED_TASK) { + Runnable runnable; + while ((runnable = engine.getDelegatedTask()) != null) { + log("\trunning delegated task..."); + runnable.run(); + } + hsStatus = engine.getHandshakeStatus(); + if (hsStatus == HandshakeStatus.NEED_TASK) { + throw new RuntimeException( + "handshake shouldn't need additional tasks"); + } + log("\tnew HandshakeStatus: " + hsStatus); + } + } }