8367059: DTLS: loss of NewSessionTicket message results in handshake failure

Reviewed-by: jnimeh, djelinski
This commit is contained in:
Artur Barashev 2025-10-29 17:25:31 +00:00
parent 28f2591bad
commit 436dc687ba
6 changed files with 332 additions and 45 deletions

View File

@ -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<RecordFragment> 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) {

View File

@ -286,7 +286,7 @@ enum SSLExtension implements SSLStringizer {
ProtocolVersion.PROTOCOLS_10_12,
SessionTicketExtension.shNetworkProducer,
SessionTicketExtension.shOnLoadConsumer,
null,
SessionTicketExtension.shOnLoadAbsence,
null,
null,
SessionTicketExtension.steStringizer),

View File

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

View File

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

View File

@ -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();
}
/*
* =============================================================

View File

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