Java 11 RC - Handshake failure in certain specific cases throws a different exception than previous versions
Jaikiran Pai
jai.forums2013 at gmail.com
Wed Sep 12 15:41:41 UTC 2018
Please consider the code that's at the end of this mail. It is a simple
client/server code where a HTTPS server is created and set to
"needClientAuth". The client then uses HttpsURLConnection and (in this
case intentionally) doesn't present any certificate, expecting the
handshake to fail. In previous versions of Java, the handshake failure
in this code would throw a java.net.SocketException as below:
Exception in thread "main" java.net.SocketException: Connection reset
at java.net.SocketInputStream.read(SocketInputStream.java:210)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.security.ssl.InputRecord.readFully(InputRecord.java:465)
at sun.security.ssl.InputRecord.read(InputRecord.java:503)
at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:983)
at sun.security.ssl.SSLSocketImpl.waitForClose(SSLSocketImpl.java:1779)
at
sun.security.ssl.HandshakeOutStream.flush(HandshakeOutStream.java:124)
at
sun.security.ssl.Handshaker.sendChangeCipherSpec(Handshaker.java:1156)
at
sun.security.ssl.ClientHandshaker.sendChangeCipherAndFinish(ClientHandshaker.java:1266)
at
sun.security.ssl.ClientHandshaker.serverHelloDone(ClientHandshaker.java:1178)
at
sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:348)
at sun.security.ssl.Handshaker.processLoop(Handshaker.java:1052)
at sun.security.ssl.Handshaker.process_record(Handshaker.java:987)
at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1072)
at
sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1385)
at
sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1413)
at
sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1397)
at
sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:559)
at
sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
at
sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1564)
at
sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1492)
at
sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:263)
at ClientCertTest.main(ClientCertTest.java:26)
However, in the Java 11 (release candidate) as well as Java 12
(upstream), this code now throws a javax.net.ssl.SSLProtocolException
with the java.net.SocketException wrapped in it:
Exception in thread "main" javax.net.ssl.SSLProtocolException:
Connection reset
at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:126)
at
java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:321)
at
java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:264)
at
java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:259)
at
java.base/sun.security.ssl.SSLSocketImpl.handleException(SSLSocketImpl.java:1314)
at
java.base/sun.security.ssl.SSLSocketImpl$AppInputStream.read(SSLSocketImpl.java:839)
at
java.base/java.io.BufferedInputStream.fill(BufferedInputStream.java:252)
at
java.base/java.io.BufferedInputStream.read1(BufferedInputStream.java:292)
at
java.base/java.io.BufferedInputStream.read(BufferedInputStream.java:351)
at
java.base/sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:746)
at java.base/sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:689)
at java.base/sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:717)
at
java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1604)
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)
at ClientCertTest.main(ClientCertTest.java:26)
Caused by: java.net.SocketException: Connection reset
at java.base/java.net.SocketInputStream.read(SocketInputStream.java:186)
at java.base/java.net.SocketInputStream.read(SocketInputStream.java:140)
at
java.base/sun.security.ssl.SSLSocketInputRecord.read(SSLSocketInputRecord.java:448)
at
java.base/sun.security.ssl.SSLSocketInputRecord.bytesInCompletePacket(SSLSocketInputRecord.java:68)
at
java.base/sun.security.ssl.SSLSocketImpl.readApplicationRecord(SSLSocketImpl.java:1104)
at
java.base/sun.security.ssl.SSLSocketImpl$AppInputStream.read(SSLSocketImpl.java:823)
... 10 more
Is this an intentional and expected change? As far as I could see, there
isn't any specific API which says SocketException will be thrown in this
case, so maybe the client applications which were catching this specific
exception are expected to change their catch block to something more
generic like a IOException?
Here's the code to reproduce this:
import java.io.*;
import java.net.*;
import javax.net.ssl.*;
import java.util.*;
import java.security.*;
import com.sun.net.httpserver.*;
import java.security.cert.*;
public class ClientCertTest {
private static final String keyFilename = "keystore";
private static final String keystorePass = "passphrase";
public static void main(final String[] args) throws Exception {
final int port = 12345;
final HttpsServer server = startServer("localhost", port);
try {
final URL targetURL = new URL("https://localhost:" + port +
"/");
final SSLContext sslctx = SSLContext.getInstance("TLS");
sslctx.init(null, new TrustManager[] {new TrustAll()}, null);
HttpsURLConnection.setDefaultHostnameVerifier((h, s) ->
{return true;});
final HttpsURLConnection conn = (HttpsURLConnection)
targetURL.openConnection();
// setup the HTTPS connection to use our SocketFactory which
doesn't present
// any cert when asked for (and thus is expected to fail
handshake)
conn.setSSLSocketFactory(sslctx.getSocketFactory());
try (final InputStream is = conn.getInputStream()) {
is.read();
} catch (SocketException se) {
System.out.println("*** Received the expected
SocketException: " + se.getMessage());
throw se;
}
} finally {
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("TLS");
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();
// need client auth
sslParameters.setNeedClientAuth(true);
server.setHttpsConfigurator(new HttpsConfigurator(sslctx) {
@Override
public void configure(final HttpsParameters params) {
params.setSSLParameters(sslParameters);
}
});
server.start();
System.out.println("Started server at " +
server.getAddress());
} catch(Exception e) {
throw new RuntimeException(e);
}
});
t.start();
return server;
}
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];
}
}
}
P.S: The "keystore" file in this example is the same one that's present
in the openjdk repo at test/jdk/javax/net/ssl/etc/keystore, so in order
to run the above code, that file can just be placed in the current
working dir.
-Jaikiran
More information about the security-dev
mailing list