Details
-
Bug
-
Status: Closed
-
Minor
-
Resolution: Fixed
-
4.3.6, 4.4 Beta1
-
Fedora 20 on a Thinkpad T540p, Sun JDK 1.6,1.7,1.8
Description
Overview
The http client submits the client certificate when requested about half the time. This may well be a bug in the underlying SSLSocketImpl or nearby code. I welcome assistance in determining which layer has the bug.
Stack trace
javax.net.ssl.SSLHandshakeException: Received fatal alert: bad_certificate at sun.security.ssl.Alerts.getSSLException(Alerts.java:192) at sun.security.ssl.Alerts.getSSLException(Alerts.java:154) at sun.security.ssl.SSLSocketImpl.recvAlert(SSLSocketImpl.java:1959) at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1077) at sun.security.ssl.SSLSocketImpl.waitForClose(SSLSocketImpl.java:1705) at sun.security.ssl.HandshakeOutStream.flush(HandshakeOutStream.java:122) at sun.security.ssl.Handshaker.sendChangeCipherSpec(Handshaker.java:982) at sun.security.ssl.ClientHandshaker.sendChangeCipherAndFinish(ClientHandshaker.java:1154) at sun.security.ssl.ClientHandshaker.serverHelloDone(ClientHandshaker.java:1066) at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:341) at sun.security.ssl.Handshaker.processLoop(Handshaker.java:878) at sun.security.ssl.Handshaker.process_record(Handshaker.java:814) at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1016) at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1312) at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1339) at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1323) at org.apache.http.conn.ssl.SSLConnectionSocketFactory.createLayeredSocket(SSLConnectionSocketFactory.java:275) at org.apache.http.conn.ssl.SSLConnectionSocketFactory.connectSocket(SSLConnectionSocketFactory.java:254) at org.apache.http.impl.conn.HttpClientConnectionOperator.connect(HttpClientConnectionOperator.java:123) at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:318) at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:363) at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:219) at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:195) at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:86) at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:108) at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:184) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:106)
Steps to reproduce
1. Set up certificates
create server key/cert pair. use FQDN as First Last name
keytool -keystore server_keystore -alias server -genkey -keyalg RSA
export server cert to a curl usable format.
keytool -exportcert -alias server -keystore server_keystore -rfc -file server.pem
create client key/cert pair. use email as First Last name
keytool -keystore client_keystore -alias client -genkey -keyalg RSA
export client key/cert pair to a firefox usablabe format.
keytool -importkeystore -srckeystore client_keystore -destkeystore client_keystore.pkcs12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass password -deststorepass password -srcalias client -destalias client -srckeypass password -destkeypass password
export client key/cert pair to a curl usable format.
openssl pkcs12 -in client_keystore.pkcs12 -out client.crt.pem -clcerts -nokeys
openssl pkcs12 -in client_keystore.pkcs12 -out client.key.pem -nocerts -nodes
2. Set up Jetty, configure it to require a client cert
vim jetty-ssl.xml
<Set name="KeyStorePath"><Property name="jetty.base" default="." />/<Property name="jetty.keystore" default="etc/server_keystore"/></Set> <Set name="KeyStorePassword"><Property name="jetty.keystore.password" default="password"/></Set> <Set name="KeyManagerPassword"><Property name="jetty.keymanager.password" default="password"/></Set> <Set name="TrustStorePath"><Property name="jetty.base" default="." />/<Property name="jetty.truststore" default="etc/client_keystore"/></Set> <Set name="TrustStorePassword"><Property name="jetty.truststore.password" default="password"/></Set> <Set name="NeedClientAuth"><Property name="jetty.ssl.needClientAuth" default="true"/></Set>
vim jetty-https.xml
<Set name="port"><Property name="https.port" default="8443" /></Set>
vim start.ini
jetty.dump.stop=false etc/jetty-ssl.xml etc/jetty-https.xml
3. Verify Jetty works
Copy the exported certs to your machine.
Go to https://FQDN:8443 in the browser with and without the cert to make sure it works
or use curl
curl -L -v --cacert server.pem -E ./client.crt.pem --key ./client.key.pem https://localhost:8443
4. Run this code against it:
import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import java.net.Socket; import com.google.common.io.Resources; import junit.framework.AssertionFailedError; import org.junit.Test; import static org.junit.Assert.*; public class HttpsClientCertificateTest { @Test public void testClientCertificate() throws Exception { int success=0; int failure=0; for (int i=0;i<40;i++) { try { String testKeystorePath = Resources.getResource("test-keystore").toString(); String testClientCertPath = Resources.getResource("test-clientstore").toString(); assertEquals("protected\n",secureContentFor("https://localhost:8443/", testKeystorePath, testClientCertPath)); success++; } catch (AssertionFailedError e) { e.printStackTrace(); failure++; } catch (SSLHandshakeException e) { e.printStackTrace(); failure++; } } assertEquals("failed " + failure + "/" + (success+failure) + " succeeded " + (success*100/(success+failure)) + "%",0,failure); } static String secureContentFor(String url, String clientKeyStore, String clientTrustStore) throws Exception { KeyStore trustStore = readKeyStore(clientTrustStore); KeyStore keyStore = readKeyStore(clientKeyStore); // Trust own CA and all self-signed certs SSLContext sslcontext = SSLContexts.custom() .loadTrustMaterial(trustStore, new TrustSelfSignedStrategy()) .loadKeyMaterial(keyStore, "password".toCharArray()) .loadKeyMaterial(trustStore, "password".toCharArray()) .useTLS() .build(); // Allow TLSv1 protocol only SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( sslcontext, new String[] { "TLSv1" }, // supported protocols null, // supported cipher suites SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); CloseableHttpClient httpClient = HttpClients.custom() .setSSLSocketFactory(sslsf) .build(); HttpGet get = new HttpGet(url); HttpResponse response = httpClient.execute(get); String content = EntityUtils.toString(response.getEntity()); return content; } static KeyStore readKeyStore(String resourceURL) throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); FileInputStream instream = new FileInputStream(new URL(resourceURL).getFile()); try { trustStore.load(instream, "password".toCharArray()); } finally { instream.close(); } return trustStore; } }
Actual results
Exceptions as noted above, and some ultimate failure note like this:
java.lang.AssertionError: failed 19/40 succeeded 52%
Expected :0
Actual :19
Expected results
No exceptions, test passes