Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8190917

Java 9 regression : SSL session resumption, through handshake, in SSLEngine is broken for any protocols lesser than TLSv1.2

    Details

    • Subcomponent:
    • Introduced In Version:
      9
    • CPU:
      generic
    • OS:
      generic

      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 ----------

        Attachments

        1. JI9051425.java
          11 kB
          Pallavi Sonal
        2. keystore
          4 kB
          Pallavi Sonal
        3. truststore
          2 kB
          Pallavi Sonal

          Issue Links

            Activity

              People

              • Assignee:
                jnimeh Jamil Nimeh
                Reporter:
                webbuggrp Webbug Group
              • Votes:
                0 Vote for this issue
                Watchers:
                7 Start watching this issue

                Dates

                • Created:
                  Updated:
                  Resolved: