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

SSLContext initialized with null KeyManager won't fall back to KeyManagerFactory

XMLWordPrintable

      A DESCRIPTION OF THE PROBLEM :
      SSLContext.init() is documented as follows:
          /**
           * Initializes this context. Either of the first two parameters
           * may be null in which case the installed security providers will
           * be searched for the highest priority implementation of the
           * appropriate factory. Likewise, the secure random parameter may
           * be null in which case the default implementation will be used.
           * <P>
           * Only the first instance of a particular key and/or trust manager
           * implementation type in the array is used. (For example, only
           * the first javax.net.ssl.X509KeyManager in the array will be used.)
           *
           * @param km the sources of authentication keys or null
           * @param tm the sources of peer authentication trust decisions or null
           * @param random the source of randomness for this generator or null
           * @throws KeyManagementException if this operation fails
           */

      However, that first comment is only true for the "tm" parameter. Per https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java#L85 a null "km" will instead fall back to a hardcoded dummy key manager that never returns key material. Because of this, a new SSLContext initialized with (null, null, null) will not present client certificates in the key store specified using javax.net.ssl.keyStore, in disagreement with SSLContext.init()'s documentation, while the default SSLContext will.

      This issue was noticed while migrating an application from IBM Java 8 to OpenJDK 11. The application uses Resteasy to connect to a REST endpoint requiring TLS client certificate authentication. Resteasy, by default, creates its SSL context using the following code:

                  final SSLContext tlsContext = SSLContext.getInstance(SSLConnectionSocketFactory.TLS);
                  tlsContext.init(null, null, null);
                  sslsf = new SSLConnectionSocketFactory(tlsContext, verifier);

      This picks up the contents of both javax.net.ssl.keyStore and javax.net.ssl.trustStore on IBM JDK, but only javax.net.ssl.trustStore on OpenJDK, even though the documentation for SSLContext.init() indicates it should pick up both. Because of this, after migrating to OpenJDK 11, the application would no longer present a client certificate, and therefore fail to establish a secure connection to the REST endpoint.

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Compile the following Java class:
      ===
      import javax.net.ssl.SSLContext;
      import javax.net.ssl.SSLSocket;
      import javax.net.ssl.SSLSocketFactory;
      import java.io.InputStream;
      import java.io.OutputStream;

      /**
       * Created on 21. 4. 2017.
       * Modified on 10. 8. 2022. for testing SSLContext.init(null, null, null)
       */
      public class SSLPoke {

          public static void main(String[] args) {
              if (args.length != 2) {
                  System.out.println("Usage: "+SSLPoke.class.getName()+" <host> <port>");
                  System.exit(1);
              }

              try {
                  SSLContext sslContext = SSLContext.getInstance("TLS");
                  SSLContext defaultSslContext = SSLContext.getDefault();
                  sslContext.init(null, null, null);
                  SSLSocketFactory sslsocketfactory = sslContext.getSocketFactory(); // change sslContext to defaultSslContext for control run
                  SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket(args[0], Integer.parseInt(args[1]));

                  InputStream in = sslsocket.getInputStream();
                  OutputStream out = sslsocket.getOutputStream();

                  // Write a test byte to get a reaction :)
                  out.write(1);

                  while (in.available() > 0) {
                      System.out.print(in.read());
                  }
                  System.out.println("Successfully connected");

              } catch (Exception exception) {
                  exception.printStackTrace();
              }
          }
      }
      ===

      Create a keystore file "keystore.jks" with the client certificate required to connect to an URL, and password "changeit". Add any trusted certificates required for the connection too.

      Then, execute the compiled class as follows:
      java -Djavax.net.ssl.trustStore=keystore.jks -Djavax.net.ssl.keyStore=keystore.jks -Djavax.net.ssl.keyStorePassword=changeit -Djavax.net.ssl.trustStorePassword=changeit -Djavax.net.debug=ssl:handshake -Djava.security.debug=access:stack SSLPoke <host name of the server> 443

      (This assumes that the protected service runs on port 443.)

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      "Successfully connected" printed at the end.
      ACTUAL -
      Error message printed:
      javax.net.ssl|DEBUG|01|main|2022-08-10 20:00:12.656 CEST|CertificateMessage.java:299|No X.509 certificate for client authentication, use empty Certificate message instead
      javax.net.ssl|DEBUG|01|main|2022-08-10 20:00:12.665 CEST|CertificateMessage.java:330|Produced client Certificate handshake message (
      "Certificates": <empty list>
      )

      followed by

      java.net.SocketException: Broken pipe (Write failed)
              at java.base/java.net.SocketOutputStream.socketWrite0(Native Method)
              at java.base/java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:110)
              at java.base/java.net.SocketOutputStream.write(SocketOutputStream.java:150)
              at java.base/sun.security.ssl.SSLSocketOutputRecord.encodeChangeCipherSpec(SSLSocketOutputRecord.java:231)
              at java.base/sun.security.ssl.OutputRecord.changeWriteCiphers(OutputRecord.java:187)
              at java.base/sun.security.ssl.ChangeCipherSpec$T10ChangeCipherSpecProducer.produce(ChangeCipherSpec.java:118)
              at java.base/sun.security.ssl.Finished$T12FinishedProducer.onProduceFinished(Finished.java:395)
              at java.base/sun.security.ssl.Finished$T12FinishedProducer.produce(Finished.java:379)
              at java.base/sun.security.ssl.SSLHandshake.produce(SSLHandshake.java:436)
              at java.base/sun.security.ssl.ServerHelloDone$ServerHelloDoneConsumer.consume(ServerHelloDone.java:182)
              at java.base/sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:392)
              at java.base/sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:443)
              at java.base/sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:421)
              at java.base/sun.security.ssl.TransportContext.dispatch(TransportContext.java:183)
              at java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:172)
              at java.base/sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1506)
              at java.base/sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1416)
              at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:456)
              at java.base/sun.security.ssl.SSLSocketImpl.ensureNegotiated(SSLSocketImpl.java:921)
              at java.base/sun.security.ssl.SSLSocketImpl$AppOutputStream.write(SSLSocketImpl.java:1291)
              at java.base/sun.security.ssl.SSLSocketImpl$AppOutputStream.write(SSLSocketImpl.java:1263)
              at SSLPoke.main(SSLPoke.java:30)

      ---------- BEGIN SOURCE ----------
      import javax.net.ssl.SSLContext;
      import javax.net.ssl.SSLSocket;
      import javax.net.ssl.SSLSocketFactory;
      import java.io.InputStream;
      import java.io.OutputStream;

      /**
       * Created by michalh on 21. 4. 2017.
       * Modified by BroadBit Kft on 10. 8. 2022. for testing SSLContext.init(null, null, null)
       */
      public class SSLPoke {

          public static void main(String[] args) {
              if (args.length != 2) {
                  System.out.println("Usage: "+SSLPoke.class.getName()+" <host> <port>");
                  System.exit(1);
              }

              try {
                  SSLContext sslContext = SSLContext.getInstance("TLS");
                  SSLContext defaultSslContext = SSLContext.getDefault();
                  sslContext.init(null, null, null);
                  SSLSocketFactory sslsocketfactory = sslContext.getSocketFactory(); // change sslContext to defaultSslContext for control run
                  SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket(args[0], Integer.parseInt(args[1]));

                  InputStream in = sslsocket.getInputStream();
                  OutputStream out = sslsocket.getOutputStream();

                  // Write a test byte to get a reaction :)
                  out.write(1);

                  while (in.available() > 0) {
                      System.out.print(in.read());
                  }
                  System.out.println("Successfully connected");

              } catch (Exception exception) {
                  exception.printStackTrace();
              }
          }
      }
      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      Explicitly use the default keystore (see comment in source code), or use sslContext.init(KeyManagerFactory.getInstance.getKeyManagers(), null, null) instead of sslContext.init(null, null, null)

      FREQUENCY : always


            hchao Haimay Chao
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            4 Start watching this issue

              Created:
              Updated: