Java 11 - SSL handshake for ECDH cipher suites trigger Invalid ECDH ServerKeyExchange with non-default security provider

Jaikiran Pai jai.forums2013 at gmail.com
Thu Sep 20 15:22:02 UTC 2018


Hello Xuelei,

It doesn't happen if both the server side and client side use the JDK
crypto provider. However if any one side uses a different crypto
provider (bouncycastle in this case) then it throws this exception.

-Jaikiran


On 20/09/18 8:37 PM, Xuelei Fan wrote:
> Hi Jaikiran,
>
> Does it happen if using JDK crypto provider?
>
> Thanks,
> Xuelei
>
> On 9/20/2018 6:16 AM, Jaikiran Pai wrote:
>> Just checking - does this look like a genuine issue? Anything else I can
>> provide to help reproduce this?
>>
>> -Jaikiran
>>
>>
>> On 18/09/18 7:06 PM, Jaikiran Pai wrote:
>>> I have been testing some projects that I know of, with Java 11 RC.
>>> There's one specific test that has been failing for me, for a while
>>> now,
>>> during SSL handshake. Took me a while to narrow it down given the size
>>> of the application and the nature of the exception. The exception
>>> occurs
>>> during SSL handshake between a client and a server (both Java and both
>>> using Java 11 RC) and it kept throwing:
>>>
>>> Exception in thread "main" javax.net.ssl.SSLHandshakeException: Invalid
>>> ECDH ServerKeyExchange signature
>>>      at
>>> java.base/sun.security.ssl.Alert.createSSLException(Alert.java:128)
>>>      at
>>> java.base/sun.security.ssl.Alert.createSSLException(Alert.java:117)
>>>      at
>>> java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:308)
>>>
>>>      at
>>> java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:264)
>>>
>>>      at
>>> java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:255)
>>>
>>>      at
>>> java.base/sun.security.ssl.ECDHServerKeyExchange$ECDHServerKeyExchangeMessage.<init>(ECDHServerKeyExchange.java:329)
>>>
>>>      at
>>> java.base/sun.security.ssl.ECDHServerKeyExchange$ECDHServerKeyExchangeConsumer.consume(ECDHServerKeyExchange.java:535)
>>>
>>>      at
>>> java.base/sun.security.ssl.ServerKeyExchange$ServerKeyExchangeConsumer.consume(ServerKeyExchange.java:103)
>>>
>>>      at
>>> java.base/sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:392)
>>>      at
>>> java.base/sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:444)
>>>
>>>      at
>>> java.base/sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:421)
>>>
>>>      at
>>> java.base/sun.security.ssl.TransportContext.dispatch(TransportContext.java:178)
>>>
>>>      at
>>> java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:164)
>>>      at
>>> java.base/sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1152)
>>>
>>>      at
>>> java.base/sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1063)
>>>
>>>      at
>>> java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:402)
>>>
>>>      at
>>> java.base/sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:567)
>>>
>>>      at
>>> java.base/sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
>>>
>>>      at
>>> java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1581)
>>>
>>>      at
>>> java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1509)
>>>
>>>      at
>>> java.base/sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:245)
>>>
>>> ...
>>>
>>> This is consistently reproducible if, in the scheme of things:
>>>
>>>   1. the cipher suite selected is a ECDHE one. For example
>>> TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384. The TLS version itself is
>>> TLSv1.2
>>> (both server and client).
>>>
>>>   2. One side of the client/server, is backed by BouncyCastle as the
>>> security provider (very specifically for SignatureSpi).
>>>
>>> Assuming the server side is using BouncyCastle provider, what's
>>> happening is that during the handshake, the server side uses the
>>> RSASSA-PSS algorithm, backed by this provider and writes out the
>>> signature. The client side uses SunRsaSign (default provider) and tries
>>> to verify this RSASSA-PSS signature and fails with that above
>>> exception.
>>> FWIW, I don't believe the signature algorithm matters as long as the
>>> cipher is ECDHE backed and the client and server side use a different
>>> security provider.
>>>
>>> All this works perfectly fine when both the sides use the default
>>> provider and bouncycastle isn't involved.
>>>
>>> I was able to get this reproducible in a very simple server/client
>>> standalone program. I think this can even be demonstrated in a jtreg
>>> test but I don't have enough experience with it to see how to trigger a
>>> server in a separate JVM and then use a client for testing. The
>>> reproducer code (Server.java and Client.java) is at the end of this
>>> mail
>>> along with instructions on how to reproduce it.
>>>
>>> I was also able to narrow down this issue down to a specific part of
>>> the
>>> JDK code. sun.security.ssl.SignatureScheme#getSignature[1] inits the
>>> Signature instance for either signing or verifying. However it sets up
>>> the signature parameters _after_ the init is done and in fact, there's
>>> an explicit note[2] stating what/why it's doing it there. I admit, I
>>> don't have much knowledge of the Java SSL parts and none in these
>>> internal details and don't understand the details of that
>>> implementation
>>> notes. However, just to try it out, I switched the order of it by using
>>> this local patch:
>>>
>>> diff -r fbb71a7edc1a
>>> src/java.base/share/classes/sun/security/ssl/SignatureScheme.java
>>> ---
>>> a/src/java.base/share/classes/sun/security/ssl/SignatureScheme.java
>>> Sat Aug 25 20:16:43 2018 +0530
>>> +++
>>> b/src/java.base/share/classes/sun/security/ssl/SignatureScheme.java
>>> Tue Sep 18 18:47:52 2018 +0530
>>> @@ -467,18 +467,16 @@
>>>           }
>>>             Signature signer = JsseJce.getSignature(algorithm);
>>> +        if (signAlgParameter != null) {
>>> +            signer.setParameter(signAlgParameter);
>>> +        }
>>> +
>>>           if (key instanceof PublicKey) {
>>>               signer.initVerify((PublicKey)(key));
>>>           } else {
>>>               signer.initSign((PrivateKey)key);
>>>           }
>>>   -        // Important note:  Please don't set the parameters before
>>> signature
>>> -        // or verification initialization, so that the crypto
>>> provider can
>>> -        // be selected properly.
>>> -        if (signAlgParameter != null) {
>>> -            signer.setParameter(signAlgParameter);
>>> -        }
>>>             return signer;
>>>       }
>>>
>>> Built this version and gave it a try with the sample code below (and
>>> also against the actual application). Both worked fine. I tried cases:
>>>
>>>   - where one side had default provider and other side had
>>> bouncycastle.
>>>
>>>   - where both sides were default provider
>>>
>>>
>>> Any thoughts on this issue? Here's the code to reproduce it:
>>>
>>> Server.java
>>>
>>> -----------
>>>
>>> import java.io.*;
>>> import java.net.*;
>>> import javax.net.ssl.*;
>>> import java.util.*;
>>> import java.util.concurrent.*;
>>> import java.security.*;
>>> import com.sun.net.httpserver.*;
>>> import java.security.cert.*;
>>>
>>>
>>> public class Server {
>>>      private static final String keyFilename = "keystore";
>>>      private static final String keystorePass = "passphrase";
>>>      private static final String WEB_APP_CONTEXT = "/test";
>>>
>>>      public static void main(final String[] args) throws Exception {
>>>          if (args.length == 1 &&
>>> args[0].equals("--use-bouncy-castle")) {
>>>              // enable bouncycastle
>>>              Security.insertProviderAt(new
>>> org.bouncycastle.jce.provider.BouncyCastleProvider(), 2);
>>>              System.out.println("Using bouncycastle provider");
>>>          } else {
>>>              System.out.println("Using JRE security provider");
>>>          }
>>>
>>>          final int port = 12345;
>>>          // start the server
>>>          final HttpsServer server = startServer("localhost", port);
>>>          // stop server on shutdown
>>>          Runtime.getRuntime().addShutdownHook(new Thread(() -> {
>>>              if (server != null) {
>>>                  server.stop(0);
>>>                  System.out.println("Stopped server");
>>>              }
>>>          }));
>>>      }
>>>
>>>      private static HttpsServer startServer(final String host, final
>>> int
>>> port) throws Exception {
>>>          final HttpsServer server = HttpsServer.create(new
>>> InetSocketAddress(host, port), 0);
>>>          final Thread t = new Thread(() -> {
>>>              try {
>>>                  final SSLContext sslctx =
>>> SSLContext.getInstance("TLSv1.2");
>>>                  final KeyManagerFactory kmf =
>>> KeyManagerFactory.getInstance("SunX509");
>>>                  final KeyStore ks = KeyStore.getInstance("JKS");
>>>                  try (final FileInputStream fis = new
>>> FileInputStream(keyFilename)) {
>>>                      ks.load(fis, keystorePass.toCharArray());
>>>                  }
>>>                  kmf.init(ks, keystorePass.toCharArray());
>>>                  sslctx.init(kmf.getKeyManagers(), null, null);
>>>                  final SSLParameters sslParameters =
>>> sslctx.getDefaultSSLParameters();
>>>                  sslParameters.setProtocols(new String[] { "TLSv1.2"
>>> });
>>>                  // use ECDHE specific ciphersuite
>>>                  sslParameters.setCipherSuites(new String[] {
>>> "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"});
>>>                  server.setHttpsConfigurator(new
>>> HttpsConfigurator(sslctx) {
>>>                      @Override
>>>                      public void configure(final HttpsParameters
>>> params) {
>>>                          params.setSSLParameters(sslParameters);
>>>                      }
>>>                  });
>>>                  server.createContext(WEB_APP_CONTEXT, (exchange)->
>>> exchange.sendResponseHeaders(200, -1));
>>>                  server.start();
>>>                  System.out.println("Started server at " +
>>> server.getAddress());
>>>              } catch(Exception e) {
>>>                  throw new RuntimeException(e);
>>>              }
>>>          });
>>>          t.start();
>>>          return server;
>>>      }
>>> }
>>>
>>> To run this:
>>>
>>> (you'll need bouncycastle jar in your classpath. you can get it
>>> from[3])
>>>
>>> java -cp bcprov-jdk15on-1.58.jar Server.java --use-bouncy-castle
>>>
>>> You should see output like:
>>>
>>> Using bouncycastle provider
>>> Started server at /127.0.0.1:12345
>>>
>>> (not passing --use-bouncy-castle will start the server with the regular
>>> default JRE provided security provider).
>>>
>>> The server is now up and running and ready to accept the request. See
>>> how to run the client below. This server code uses a keystore file
>>> which
>>> is part of the OpenJDK repo and can be obtained from [4] and stored in
>>> the current working directory.
>>>
>>> Client.java
>>>
>>> ------------
>>>
>>> import java.io.*;
>>> import java.net.*;
>>> import javax.net.ssl.*;
>>> import java.util.*;
>>> import java.util.concurrent.*;
>>> import java.security.*;
>>> import com.sun.net.httpserver.*;
>>> import java.security.cert.*;
>>>
>>>
>>> public class Client {
>>>      private static final String WEB_APP_CONTEXT = "/test";
>>>
>>>      public static void main(final String[] args) throws Exception {
>>>          HttpsURLConnection.setDefaultHostnameVerifier((h, s) ->
>>> {return
>>> true;});
>>>                  final int port = 12345;
>>>          final URL targetURL = new URL("https://localhost:" + port +
>>> WEB_APP_CONTEXT);
>>>          final HttpsURLConnection conn = (HttpsURLConnection)
>>> targetURL.openConnection();
>>>                  // use a SSLSocketFactory which "trusts all"
>>>          final SSLContext sslctx = SSLContext.getInstance("TLSv1.2");
>>>          sslctx.init(null, new TrustManager[] {new TrustAll()}, null);
>>>          conn.setSSLSocketFactory(sslctx.getSocketFactory());
>>>
>>>          // read
>>>          try (final InputStream is = conn.getInputStream()) {
>>>              is.read();
>>>          }
>>>          System.out.println("Received status code " +
>>> conn.getResponseCode());
>>>      }
>>>
>>>      private static class TrustAll implements X509TrustManager {
>>>
>>>          @Override
>>>          public void checkClientTrusted(X509Certificate[] chain, String
>>> authType) throws CertificateException {
>>>              return;
>>>          }
>>>
>>>          @Override
>>>          public void checkServerTrusted(X509Certificate[] chain, String
>>> authType) throws CertificateException {
>>>              return;
>>>          }
>>>
>>>          @Override
>>>          public X509Certificate[] getAcceptedIssuers() {
>>>              return new X509Certificate[0];
>>>          }
>>>      }
>>> }
>>>
>>> To run the client:
>>>
>>> java Client.java
>>>
>>> A successful execution will show:
>>>
>>> Received status code 200
>>>
>>> whereas a failed execution should throw the exception shown previously
>>> in the mail.
>>>
>>> [1]
>>> http://hg.openjdk.java.net/jdk/jdk/file/fbec908e2783/src/java.base/share/classes/sun/security/ssl/SignatureScheme.java#l463
>>>
>>>
>>> [2]
>>> http://hg.openjdk.java.net/jdk/jdk/file/fbec908e2783/src/java.base/share/classes/sun/security/ssl/SignatureScheme.java#l476
>>>
>>>
>>> [3]
>>> http://repo1.maven.org/maven2/org/bouncycastle/bcprov-jdk15on/1.58/bcprov-jdk15on-1.58.jar
>>>
>>>
>>> [4]
>>> http://hg.openjdk.java.net/jdk/jdk/file/fbec908e2783/test/jdk/javax/net/ssl/etc/keystore
>>>
>>>
>>> -Jaikiran
>>>
>>>
>>>
>>>
>>>
>>




More information about the security-dev mailing list