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

Jaikiran Pai jai.forums2013 at gmail.com
Tue Sep 18 13:36:27 UTC 2018


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