I've coded a Java client application which connects to an Apache web server over HTTPS using a client certificate and performs an HTTP PUT of a file to the server. It works fine with small files, but crashes with large ones.
The Apache server log shows the following:
...
OpenSSL: Handshake: done
...
Changed client verification type will force renegotiation
...
filling buffer, max size 131072 bytes
...
request body exceeds maximum size (131072) for SSL buffer
could not buffer message body to allow SSL renegotiation to proceed
...
OpenSSL: I/O error, 5 bytes expected to read on BIO
(104)Connection reset by peer: SSL input filter read failed.
(32)Broken pipe: core_output_filter: writing data to the network
Connection closed to child 20 with standard shutdown
The response on the client is:
java.io.IOException: Server returned HTTP response code: 401 for URL
I'm not familiar with this process so I'm not sure if renegotiation is necessary here or if there is something I can do to prevent it. Or perhaps I can have the client wait until the renegotiation is complete before sending application data? Here is an excerpt of the client code (error handling removed):
URL url = new URL("my url goes here");
con = (HttpsURLConnection) url.openConnection();
con.setSSLSocketFactory(getMyCustomClientCertSocketFactory());
con.setRequestMethod("PUT");
con.setDoOutput(true);
con.connect();
writer = new OutputStreamWriter(con.getOutputStream());
writer.write(xml);
writer.close();
parseServerResponse(con.getInputStream());
I'm thinking maybe I need to use a lower level API like SSLSocket and leverage the HandshakeCompletedListener?
I'm also wondering if the Apache SSLVerifyDepth directive has anything to do with why a renegotiation is occurring. I've got the directive in a per-directory context (only one upload directory) with value 2 and The Apache manual says this about it:
In per-directory context it forces a SSL renegotation with the reconfigured client verification depth after the HTTP request was read but before the HTTP response is sent.
As requested here is the Java debugging output:
keyStore is :
keyStore type is : jks
keyStore provider is :
init keystore
init keymanager of type SunX509
trustStore is: C:\Program Files\Java\jdk1.6.0_35\jre\lib\security\cacerts
trustStore type is : jks
trustStore provider is :
init truststore
adding as trusted cert:
...
trigger seeding of SecureRandom
done seeding SecureRandom
***
found key for : key-alias
chain [0] = [
[
...
]
***
trigger seeding of SecureRandom
done seeding SecureRandom
Allow unsafe renegotiation: false
Allow legacy hello messages: true
Is initial handshake: true
Is secure renegotiation: false
%% No cached client session
*** ClientHello, TLSv1
RandomCookie: ...
Session ID: {}
Cipher Suites: [SSL_RSA_WITH_RC4_128_MD5, SSL_RSA_WITH_RC4_128_SHA, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_DSS_WITH_AES_128_CBC_SHA, SSL_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA, SSL_RSA_WITH_DES_CBC_SHA, SSL_DHE_RSA_WITH_DES_CBC_SHA, SSL_DHE_DSS_WITH_DES_CBC_SHA, SSL_RSA_EXPORT_WITH_RC4_40_MD5, SSL_RSA_EXPORT_WITH_DES40_CBC_SHA, SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA, SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA, TLS_EMPTY_RENEGOTIATION_INFO_SCSV]
Compression Methods: { 0 }
***
main, WRITE: TLSv1 Handshake, length = 75
main, WRITE: SSLv2 client hello message, length = 101
main, READ: TLSv1 Handshake, length = 81
*** ServerHello, TLSv1
RandomCookie: ...
Session ID: ...
Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA
Compression Method: 0
Extension renegotiation_info, renegotiated_connection: <empty>
***
%% Created: [Session-1, TLS_RSA_WITH_AES_128_CBC_SHA]
** TLS_RSA_WITH_AES_128_CBC_SHA
main, READ: TLSv1 Handshake, length = 4392
*** Certificate chain
chain [0] = [
[
...
Certificate Extensions: 8
[1]: ObjectId: 1.3.6.1.5.5.7.1.1 Criticality=false
AuthorityInfoAccess [
[
accessMethod: ...
accessLocation: URIName: ...
accessMethod: ...
accessLocation: URIName: ...
]
[2]: ObjectId: 2.5.29.35 Criticality=false
AuthorityKeyIdentifier [
KeyIdentifier [
...
]
]
[3]: ObjectId: 2.5.29.19 Criticality=false
BasicConstraints:[
CA:false
PathLen: undefined
]
[4]: ObjectId: 2.5.29.31 Criticality=false
CRLDistributionPoints [
[DistributionPoint:
[URIName: ...
]]
[5]: ObjectId: 2.5.29.32 Criticality=false
CertificatePolicies [
[CertificatePolicyId: ...
[PolicyQualifierInfo: [
qualifierID: ...
qualifier: ...
]] ]
]
[6]: ObjectId: 2.5.29.37 Criticality=false
ExtendedKeyUsages [
serverAuth
clientAuth
]
[7]: ObjectId: 2.5.29.15 Criticality=true
KeyUsage [
DigitalSignature
Key_Encipherment
]
[8]: ObjectId: 2.5.29.17 Criticality=false
SubjectAlternativeName [
DNSName: ...
]
]
Algorithm: [SHA1withRSA]
Signature:
...
]
...
***
main, READ: TLSv1 Handshake, length = 4
*** ServerHelloDone
*** ClientKeyExchange, RSA PreMasterSecret, TLSv1
main, WRITE: TLSv1 Handshake, length = 518
SESSION KEYGEN:
PreMaster Secret:
...
CONNECTION KEYGEN:
Client Nonce:
...
Server Nonce:
...
Master Secret:
...
Client MAC write Secret:
...
Server MAC write Secret:
...
Client write key:
...
Server write key:
...
Client write IV:
...
Server write IV:
...
main, WRITE: TLSv1 Change Cipher Spec, length = 1
*** Finished
verify_data: { 18, 162, 18, 251, 82, 111, 87, 133, 53, 240, 114, 155 }
***
main, WRITE: TLSv1 Handshake, length = 48
main, READ: TLSv1 Change Cipher Spec, length = 1
main, READ: TLSv1 Handshake, length = 48
*** Finished
verify_data: { 46, 206, 8, 40, 63, 252, 99, 190, 251, 183, 110, 201 }
***
%% Cached client session: [Session-1, TLS_RSA_WITH_AES_128_CBC_SHA]
main, WRITE: TLSv1 Application Data, length = 256
main, WRITE: TLSv1 Application Data, length = 32
main, WRITE: TLSv1 Application Data, length = 16416
main, WRITE: TLSv1 Application Data, length = 16416
...
main, WRITE: TLSv1 Application Data, length = 16416
main, WRITE: TLSv1 Application Data, length = 16416
main, WRITE: TLSv1 Application Data, length = 512
main, READ: TLSv1 Application Data, length = 304
As requested here is the getMyCustomClientCertSocketFactory source (obtains certificate and key from a PEM file):
public static SSLSocketFactory getMyCustomClientCertSocketFactory(String pemPath,
boolean verifyPeer)
throws NoSuchAlgorithmException, FileNotFoundException, IOException,
KeyStoreException, CertificateException, UnrecoverableKeyException,
KeyManagementException, InvalidKeySpecException {
SSLContext context = SSLContext.getInstance("TLS");
byte[] certAndKey = IOUtil.fileToBytes(new File(pemPath));
byte[] certBytes = parseDERFromPEM(certAndKey,
"-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----");
byte[] keyBytes = parseDERFromPEM(certAndKey,
"-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----");
X509Certificate cert = generateX509CertificateFromDER(certBytes);
RSAPrivateKey key = generateRSAPrivateKeyFromDER(keyBytes);
KeyStore keystore = KeyStore.getInstance("JKS");
keystore.load(null);
keystore.setCertificateEntry("cert-alias", cert);
keystore.setKeyEntry("key-alias", key, "changeit".toCharArray(),
new Certificate[]{cert});
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(keystore, "changeit".toCharArray());
KeyManager[] km = kmf.getKeyManagers();
TrustManager[] tm = null;
if (!verifyPeer) {
tm = new TrustManager[]{new TrustyTrustManager()};
}
context.init(km, tm, null);
return context.getSocketFactory();
}
It would seem that the HttpsUrlConnection facility built into Sun Java cannot handle the large HTTP PUT with client certificate scenario in a server friendly way (i.e. without overflowing the servers SSL renegotiate buffer).
I examined what curl was doing to see what "server friendly meant", and it turns out there is an HTTP 1.1 header named "Expect", which curl sends with value "100-continue" (see spec http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.20). This header essentially says "I've got a huge payload, but before I send it please let me know if you can handle it". This gives the endpoints time to renegotiate the client certificate before the payload is sent.
In the Sun HttpUrlConnection implementation it seems this header is not allowed, and is actually in the restricted headers list; meaning even if you set it with the HttpUrlConnection.setRequestProperty method the header is not actually sent to the server. You can override the restricted headers with the system property sun.net.http.allowRestrictedHeaders, but then the client just crashes with a socket exception since the Sun implementation doesn't know how to handle this part of the protocol.
Interestingly it seems that the OpenJDK implementation of Java does support this header. Also, the Apache HTTP Client library supports this header (http://hc.apache.org/); I've implemented a test program with the Apache HTTP client library and it can successfully perform and HTTP PUT request of a large file using a client certificate and the Expect header.
To recap, the solutions are: