JDK-8190917 : Java 9 regression : SSL session resumption, through handshake, in SSLEngine is broken for any protocols lesser than TLSv1.2
  • Type: Bug
  • Component: security-libs
  • Sub-Component: javax.net.ssl
  • Affected Version: 9.0.1,10
  • Priority: P3
  • Status: Resolved
  • Resolution: Duplicate
  • OS: generic
  • CPU: generic
  • Submitted: 2017-11-01
  • Updated: 2018-06-20
  • Resolved: 2018-06-12
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 11
11Resolved
Related Reports
Duplicate :  
Relates :  
Relates :  
Description
FULL PRODUCT VERSION :
java version "9.0.1"
Java(TM) SE Runtime Environment (build 9.0.1+11)
Java HotSpot(TM) 64-Bit Server VM (build 9.0.1+11, mixed mode)

ADDITIONAL OS VERSION INFORMATION :
System information:

os.name = Mac OS X
os.arch = x86_64
os.version = 10.12.4
java.version = 9.0.1
java.vendor = Oracle Corporation
java.home = /Library/Java/JavaVirtualMachines/jdk-9.0.1.jdk/Contents/Home
java.vm.specification.version = 9
java.vm.specification.vendor = Oracle Corporation
java.vm.version = 9.0.1+11
java.vm.vendor = Oracle Corporation
java.vm.name = Java HotSpot(TM) 64-Bit Server VM

A DESCRIPTION OF THE PROBLEM :
Java SSLContext has the ability to cache and reuse SSL sessions when dealing with SSLEngine. The SSL protocol itself allows this to happen by letting a new client handshake message send a previous established session id. All this works fine in Java 8 (and I believe prior versions too).

However, starting Java 9, this seems to have broken. The session id is no longer reused, unless the client uses TLSv1.2 SSL protocol. Any other protocol (TLSv1, TLSv1.1) usage by the client, shows that session resumption is no longer functional.

Consider the following use case (which no longer works):

	- Client side establishes a successfully handshaked session against a server with TLSv1
	- Client side again tries to establish a communication with the same server with the same TLSv1 (with necessary "hints" for session reuse)

The above works with Java 8 and ends up reusing the session. However, it no longer works for Java 9, unless the client uses TLSv1.2.

This seems to be an unintentional change introduced in this commit http://hg.openjdk.java.net/jdk9/jdk9/jdk/rev/42268eb6e04e

What's now happening (after that commit) is that the ServerHandshaker, while checking for whether the resumingSession is allowed to resume or not does this check (http://hg.openjdk.java.net/jdk9/jdk9/jdk/file/65464a307408/src/java.base/share/classes/sun/security/ssl/ServerHandshaker.java#l595):

					// cannot resume session with different version
                    if (oldVersion != protocolVersion) {
                        resumingSession = false;
                    }

The `oldVersion` is rightly inferred from the previously succeeded and cached SSL session and is TLSv1 (in the use case above). However the field it's being compared against isn't the "negotiated" value between the client and the server, but instead a previously *defaulted* value (which happens to be TLSv1.2) in that class. Due to this, the resumption is denied. 

In prior versions of Java (before that commit), there was an explicit call to set a negotiated protocol version to the protocolVersion instance variable before doing this resumption check. That piece of code now resides *after* this above check, by which time it's too late http://hg.openjdk.java.net/jdk9/jdk9/jdk/file/65464a307408/src/java.base/share/classes/sun/security/ssl/ServerHandshaker.java#l725



REGRESSION.  Last worked in version 8u152

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :

- Copy the attached SSLEngineSimpleDemo.java to a local directory <test-dir>/net/jaikiran/ssl/
- cd <test-dir>
- The attached test uses a truststore called testkeys which needs to be in the "current dir". Please create a truststore with passphrase "passphrase" and place the file in <test-dir>. I would have attached it to this report, but I don't see a way to do that.
- Compile it using Java 9:
  javac net/jaikiran/ssl/SSLEngineSimpleDemo.java
- Run the compiled class:
   java net.jaikiran.ssl.SSLEngineSimpleDemo

You can repeat the compilation and running using Java 8 and you'll see that it now passes.

(Note: The attached SSLEngineSimpleDemo.java is a slightly modified version of the demo example available at Oracle website here https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/samples/sslengine/SSLEngineSimpleDemo.java)

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
******
Run 1 of data exchange with TLSv1
Run 2 of data exchange with TLSv1
SunJSSE TLSv1 - Session resumption SUCCEEDED
******
Run 1 of data exchange with TLSv1.1
Run 2 of data exchange with TLSv1.1
SunJSSE TLSv1.1 - Session resumption SUCCEEDED
******
Run 1 of data exchange with TLSv1.2
Run 2 of data exchange with TLSv1.2
SunJSSE TLSv1.2 - Session resumption SUCCEEDED
Demo Completed.

ACTUAL -
******
Run 1 of data exchange with TLSv1
Run 2 of data exchange with TLSv1
SunJSSE TLSv1 - Session resumption FAILED
******
Run 1 of data exchange with TLSv1.1
Run 2 of data exchange with TLSv1.1
SunJSSE TLSv1.1 - Session resumption FAILED
******
Run 1 of data exchange with TLSv1.2
Run 2 of data exchange with TLSv1.2
SunJSSE TLSv1.2 - Session resumption SUCCEEDED

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
/*
 * Copyright (c) 2004, Oracle and/or its affiliates. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * -Redistribution of source code must retain the above copyright
 *  notice, this list of conditions and the following disclaimer.
 *
 * -Redistribution in binary form must reproduce the above copyright
 *  notice, this list of conditions and the following disclaimer in the
 *  documentation and/or other materials provided with the
 *  distribution.
 *
 * Neither the name of Oracle nor the names of
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * This software is provided "AS IS," without a warranty of any kind.
 * ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
 * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
 * MIDROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
 * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
 * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN
 * OR ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR
 * FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
 * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
 * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
 * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 *
 * You acknowledge that this software is not designed, licensed or
 * intended for use in the design, construction, operation or
 * maintenance of any nuclear facility.
 */

/**
 * A SSLEngine usage example which simplifies the presentation
 * by removing the I/O and multi-threading concerns.
 * <p>
 * The demo creates two SSLEngines, simulating a client and server.
 * The "transport" layer consists two ByteBuffers:  think of them
 * as directly connected pipes.
 * <p>
 * Note, this is a *very* simple example: real code will be much more
 * involved.  For example, different threading and I/O models could be
 * used, transport mechanisms could close unexpectedly, and so on.
 * <p>
 * When this application runs, notice that several messages
 * (wrap/unwrap) pass before any application data is consumed or
 * produced.  (For more information, please see the SSL/TLS
 * specifications.)  There may several steps for a successful handshake,
 * so it's typical to see the following series of operations:
 * <p>
 * client          server          message
 * ======          ======          =======
 * wrap()          ...             ClientHello
 * ...             unwrap()        ClientHello
 * ...             wrap()          ServerHello/Certificate
 * unwrap()        ...             ServerHello/Certificate
 * wrap()          ...             ClientKeyExchange
 * wrap()          ...             ChangeCipherSpec
 * wrap()          ...             Finished
 * ...             unwrap()        ClientKeyExchange
 * ...             unwrap()        ChangeCipherSpec
 * ...             unwrap()        Finished
 * ...             wrap()          ChangeCipherSpec
 * ...             wrap()          Finished
 * unwrap()        ...             ChangeCipherSpec
 * unwrap()        ...             Finished
 */

package net.jaikiran.ssl;

import javax.net.ssl.KeyManagerFactory;
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.SSLSession;
import javax.net.ssl.TrustManagerFactory;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.security.KeyStore;

public class SSLEngineSimpleDemo {

    /*
     * Enables logging of the SSLEngine operations.
     */
    private static boolean logging = false;

    /*
     * Enables the JSSE system debugging system property:
     *
     *     -Djavax.net.debug=all
     *
     * This gives a lot of low-level information about operations underway,
     * including specific handshake messages, and might be best examined
     * after gaining some familiarity with this application.
     */
    private static boolean debug = false;

    private SSLContext sslc;

    private SSLEngine clientEngine;     // client Engine
    private ByteBuffer clientOut;       // write side of clientEngine
    private ByteBuffer clientIn;        // read side of clientEngine

    private SSLEngine serverEngine;     // server Engine
    private ByteBuffer serverOut;       // write side of serverEngine
    private ByteBuffer serverIn;        // read side of serverEngine

    /*
     * For data transport, this example uses local ByteBuffers.  This
     * isn't really useful, but the purpose of this example is to show
     * SSLEngine concepts, not how to do network transport.
     */
    private ByteBuffer cTOs;            // "reliable" transport client->server
    private ByteBuffer sTOc;            // "reliable" transport server->client


    private byte[] previousSessionId;

    /*
     * The following is to set up the keystores.
     */
    private static String keyStoreFile = "testkeys";
    private static String trustStoreFile = "testkeys";
    private static String passwd = "passphrase";

    /*
     * Main entry point for this demo.
     */
    public static void main(String args[]) throws Exception {
        if (debug) {
            System.setProperty("javax.net.debug", "all");
        }
        printSystemInfo();
        final String[] protocols = new String[]{"TLSv1", "TLSv1.1", "TLSv1.2"};
        for (final String protocol : protocols) {
            System.out.println("******");
            final SSLEngineSimpleDemo demo = new SSLEngineSimpleDemo(protocol);
            for (int i = 0; i < 2; i++) {
                System.out.println("Run " + (i + 1) + " of data exchange with " + protocol);
                demo.runDemo();
            }
        }
        System.out.println("Demo Completed.");
    }

    private static void printSystemInfo() {
        System.out.println("System information:");
        final String[] sysInfoProps = new String[]{"os.name", "os.arch", "os.version",
                "java.version", "java.vendor", "java.home", "java.vm.specification.version",
                "java.vm.specification.vendor", "java.vm.version", "java.vm.vendor", "java.vm.name"};
        for (final String prop : sysInfoProps) {
            System.out.println(prop + " = " + System.getProperty(prop));
        }
        System.out.println();
    }

    /*
     * Create an initialized SSLContext to use for this demo.
     */
    public SSLEngineSimpleDemo(final String sslProtocol) throws Exception {

        KeyStore ks = KeyStore.getInstance("JKS");
        KeyStore ts = KeyStore.getInstance("JKS");

        char[] passphrase = "passphrase".toCharArray();

        ks.load(new FileInputStream(keyStoreFile), passphrase);
        ts.load(new FileInputStream(trustStoreFile), passphrase);

        KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
        kmf.init(ks, passphrase);

        TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
        tmf.init(ts);

        SSLContext sslCtx = SSLContext.getInstance(sslProtocol);

        sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

        sslc = sslCtx;
    }

    /*
     * Run the demo.
     *
     * Sit in a tight loop, both engines calling wrap/unwrap regardless
     * of whether data is available or not.  We do this until both engines
     * report back they are closed.
     *
     * The main loop handles all of the I/O phases of the SSLEngine's
     * lifetime:
     *
     *     initial handshaking
     *     application data transfer
     *     engine closing
     *
     * One could easily separate these phases into separate
     * sections of code.
     */
    private void runDemo() throws Exception {
        boolean dataDone = false;

        createSSLEngines();
        createBuffers();

        SSLEngineResult clientResult;   // results from client's last operation
        SSLEngineResult serverResult;   // results from server's last operation

        /*
         * Examining the SSLEngineResults could be much more involved,
         * and may alter the overall flow of the application.
         *
         * For example, if we received a BUFFER_OVERFLOW when trying
         * to write to the output pipe, we could reallocate a larger
         * pipe, but instead we wait for the peer to drain it.
         */
        while (!isEngineClosed(clientEngine) ||
                !isEngineClosed(serverEngine)) {

            log("================");

            clientResult = clientEngine.wrap(clientOut, cTOs);
            if (clientResult.getHandshakeStatus() == HandshakeStatus.FINISHED) {
                this.verifySessionResumption(this.previousSessionId, clientEngine);
                this.previousSessionId = clientEngine.getSession().getId();
            }
            log("client wrap: ", clientResult);
            runDelegatedTasks(clientResult, clientEngine);

            serverResult = serverEngine.wrap(serverOut, sTOc);
            log("server wrap: ", serverResult);
            runDelegatedTasks(serverResult, serverEngine);

            cTOs.flip();
            sTOc.flip();

            log("----");

            clientResult = clientEngine.unwrap(sTOc, clientIn);
            if (clientResult.getHandshakeStatus() == HandshakeStatus.FINISHED) {
                this.verifySessionResumption(this.previousSessionId, clientEngine);
                this.previousSessionId = clientEngine.getSession().getId();
            }
            log("client unwrap: ", clientResult);
            runDelegatedTasks(clientResult, clientEngine);

            serverResult = serverEngine.unwrap(cTOs, serverIn);
            log("server unwrap: ", serverResult);
            runDelegatedTasks(serverResult, serverEngine);

            cTOs.compact();
            sTOc.compact();

            /*
             * After we've transfered all application data between the client
             * and server, we close the clientEngine's outbound stream.
             * This generates a close_notify handshake message, which the
             * server engine receives and responds by closing itself.
             *
             * In normal operation, each SSLEngine should call
             * closeOutbound().  To protect against truncation attacks,
             * SSLEngine.closeInbound() should be called whenever it has
             * determined that no more input data will ever be
             * available (say a closed input stream).
             */
            if (!dataDone && (clientOut.limit() == serverIn.position()) &&
                    (serverOut.limit() == clientIn.position())) {

                /*
                 * A sanity check to ensure we got what was sent.
                 */
                checkTransfer(serverOut, clientIn);
                checkTransfer(clientOut, serverIn);

                log("\tClosing clientEngine's *OUTBOUND*...");
                clientEngine.closeOutbound();
                // serverEngine.closeOutbound();
                dataDone = true;
            }
        }
    }

    /*
     * Using the SSLContext created during object creation,
     * create/configure the SSLEngines we'll use for this demo.
     */
    private void createSSLEngines() throws Exception {
        /*
         * Configure the serverEngine to act as a server in the SSL/TLS
         * handshake.
         */
        serverEngine = sslc.createSSLEngine();
        serverEngine.setUseClientMode(false);
        serverEngine.setNeedClientAuth(false);

        /*
         * Similar to above, but using client mode instead.
         */
        clientEngine = sslc.createSSLEngine("client", 80);
        clientEngine.setUseClientMode(true);
    }

    /*
     * Create and size the buffers appropriately.
     */
    private void createBuffers() {

        /*
         * We'll assume the buffer sizes are the same
         * between client and server.
         */
        SSLSession session = clientEngine.getSession();
        int appBufferMax = session.getApplicationBufferSize();
        int netBufferMax = session.getPacketBufferSize();

        /*
         * We'll make the input buffers a bit bigger than the max needed
         * size, so that unwrap()s following a successful data transfer
         * won't generate BUFFER_OVERFLOWS.
         *
         * We'll use a mix of direct and indirect ByteBuffers for
         * tutorial purposes only.  In reality, only use direct
         * ByteBuffers when they give a clear performance enhancement.
         */
        clientIn = ByteBuffer.allocate(appBufferMax + 50);
        serverIn = ByteBuffer.allocate(appBufferMax + 50);

        cTOs = ByteBuffer.allocateDirect(netBufferMax);
        sTOc = ByteBuffer.allocateDirect(netBufferMax);

        clientOut = ByteBuffer.wrap("Hi Server, I'm Client".getBytes());
        serverOut = ByteBuffer.wrap("Hello Client, I'm Server".getBytes());
    }

    /*
     * If the result indicates that we have outstanding tasks to do,
     * go ahead and run them in this thread.
     */
    private static void runDelegatedTasks(SSLEngineResult result,
                                          SSLEngine engine) throws Exception {

        if (result.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
            Runnable runnable;
            while ((runnable = engine.getDelegatedTask()) != null) {
                log("\trunning delegated task...");
                runnable.run();
            }
            HandshakeStatus hsStatus = engine.getHandshakeStatus();
            if (hsStatus == HandshakeStatus.NEED_TASK) {
                throw new Exception(
                        "handshake shouldn't need additional tasks");
            }
            log("\tnew HandshakeStatus: " + hsStatus);
        }
    }

    private static boolean isEngineClosed(SSLEngine engine) {
        return (engine.isOutboundDone() && engine.isInboundDone());
    }

    /*
     * Simple check to make sure everything came across as expected.
     */
    private static void checkTransfer(ByteBuffer a, ByteBuffer b)
            throws Exception {
        a.flip();
        b.flip();

        if (!a.equals(b)) {
            throw new Exception("Data didn't transfer cleanly");
        } else {
            log("\tData transferred cleanly");
        }

        a.position(a.limit());
        b.position(b.limit());
        a.limit(a.capacity());
        b.limit(b.capacity());
    }

    /*
     * Logging code
     */
    private static boolean resultOnce = true;

    private static void log(String str, SSLEngineResult result) {
        if (!logging) {
            return;
        }
        if (resultOnce) {
            resultOnce = false;
            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 log(String str) {
        if (logging) {
            System.out.println(str);
        }
    }


    private void verifySessionResumption(final byte[] expected, final SSLEngine engine) {
        if (expected == null) {
            // we haven't yet created a session previously, so there isn't any
            // session to be expected to resume
            return;
        }
        final byte[] sessionId = engine.getSession().getId();
        // compare and verify if they are same
        if (java.util.Arrays.equals(expected, sessionId)) {
            System.out.println(this.sslc.getProvider().getName() + " " + this.sslc.getProtocol() + " - Session resumption SUCCEEDED");
        } else {
            System.out.println(this.sslc.getProvider().getName() + " " + this.sslc.getProtocol() + " - Session resumption FAILED");
        }
    }

}

---------- END SOURCE ----------


Comments
This was resolved as part of the extended master secret feature (JDK-8148421)
12-06-2018

Update: I was testing with an older JDK 9 build. When running with the latest JDK 9 code, this issue no longer occurs. It appears to have been fixed as part of support for the extended master secret feature (JDK-8148421).
12-06-2018

This has been confirmed to fail on JDK 9. The good news is that the new handshaker model currently in the TLS 1.3 branch fixes the issue. When it is committed, we can call this issue resolved, at least for those versions that have the new TLS handshaker.
11-06-2018

The issue has been addressed in JDK-8148421. The OpenJDK contributed patch makes a behavior change about how to use the resumption requested version, which is fine but different from what we did in the past. The patch can be regarded as an improvement of the use of resumption version of JDK. It's fine to accept the patch in a future release for an improvement, but not a must-fix any more.
28-02-2018

Patch contributed, see http://mail.openjdk.java.net/pipermail/security-dev/2017-November/016527.html
06-12-2017

To reproduce the issue, run the attached test case. JDK 8u151 - Pass JDK 9.0.11 - Fail JDK 10-ea+30 - Fail Following is the output on JDK 8u151: System information: os.name = Windows 7 os.arch = amd64 os.version = 6.1 java.version = 1.8.0_151 java.vendor = Oracle Corporation java.home = D:\JDK8u151\jre java.vm.specification.version = 1.8 java.vm.specification.vendor = Oracle Corporation java.vm.version = 25.151-b12 java.vm.vendor = Oracle Corporation java.vm.name = Java HotSpot(TM) 64-Bit Server VM ****** Run 1 of data exchange with TLSv1 Run 2 of data exchange with TLSv1 SunJSSE TLSv1 - Session resumption SUCCEEDED ****** Run 1 of data exchange with TLSv1.1 Run 2 of data exchange with TLSv1.1 SunJSSE TLSv1.1 - Session resumption SUCCEEDED ****** Run 1 of data exchange with TLSv1.2 Run 2 of data exchange with TLSv1.2 SunJSSE TLSv1.2 - Session resumption SUCCEEDED Demo Completed. Following is the output on JDK 9.0.1+11: System information: os.name = Windows 7 os.arch = amd64 os.version = 6.1 java.version = 9.0.1 java.vendor = Oracle Corporation java.home = D:\jdk9.0.1 java.vm.specification.version = 9 java.vm.specification.vendor = Oracle Corporation java.vm.version = 9.0.1+11 java.vm.vendor = Oracle Corporation java.vm.name = Java HotSpot(TM) 64-Bit Server VM ****** Run 1 of data exchange with TLSv1 Run 2 of data exchange with TLSv1 SunJSSE TLSv1 - Session resumption FAILED ****** Run 1 of data exchange with TLSv1.1 Run 2 of data exchange with TLSv1.1 SunJSSE TLSv1.1 - Session resumption FAILED ****** Run 1 of data exchange with TLSv1.2 Run 2 of data exchange with TLSv1.2 SunJSSE TLSv1.2 - Session resumption SUCCEEDED Demo Completed. Following is the output on jdk10-ea+30 : System information: os.name = Windows 7 os.arch = amd64 os.version = 6.1 java.version = 10-ea java.vendor = Oracle Corporation java.home = D:\jdk-10 java.vm.specification.version = 10 java.vm.specification.vendor = Oracle Corporation java.vm.version = 10-ea+30 java.vm.vendor = Oracle Corporation java.vm.name = Java HotSpot(TM) 64-Bit Server VM ****** Run 1 of data exchange with TLSv1 Run 2 of data exchange with TLSv1 SunJSSE TLSv1 - Session resumption FAILED ****** Run 1 of data exchange with TLSv1.1 Run 2 of data exchange with TLSv1.1 SunJSSE TLSv1.1 - Session resumption FAILED ****** Run 1 of data exchange with TLSv1.2 Run 2 of data exchange with TLSv1.2 SunJSSE TLSv1.2 - Session resumption SUCCEEDED Demo Completed.
13-11-2017