I am trying to port to java some php code that connects to a remote web service which uses SSL mutual authentication. The service owner has given to me
- the server certificate
- the client certificate and private key I have to use in the calls
This is what I did:
- I created a truststore with the given server certificate in this way:
keytool -import -alias WS_SERVER -file WS_SERVER_CERT.pem -keystore truststore.jks -deststorepass somepassword
- then I created a keystore with the client certificate. Before doing it I converted the certificate into pkcs12 format
openssl pkcs12 -export -in CLIENT_CERT.pem -inkey CLIENT_PK.pem -out certificate.p12 -name client_certificate
then I created a keystore:
keytool -importkeystore -srckeystore certificate.p12 -srcstoretype pkcs12 -destkeystore cert.jks
Finally I tried this code snippet to try connect to the web service:
SSLFactory sslFactory = SSLFactory.builder()
.withIdentityMaterial(Path.of("src/main/resources/cert/cert.jks"), "somepassword".toCharArray())
.withTrustMaterial(Path.of("src/main/resources/cert/truststore.jks"), "somepassword".toCharArray())
.build();
OkHttpClient httpClient = new OkHttpClient.Builder()
.sslSocketFactory(sslFactory.getSslSocketFactory(), sslFactory.getTrustManager().orElseThrow())
.hostnameVerifier(sslFactory.getHostnameVerifier())
.build();
final String payload = Files.readString(Path.of("src/test/resources/payload.xml"));
String wsUrl = "https://server.ws";
final Request request = new Request.Builder()
.url(wsUrl)
.post(RequestBody.create(payload, MediaType.parse("text/xml")))
.build();
final Call call = httpClient.newCall(request);
final Response response = call.execute();
System.out.println("Response: " + response.body().string());
but I get an authentication error. What could be wrong in this procedure? Please note that this curl command works:
curl --cert CLIENT_CERT.pem --key CLIENT_CERT_PK.pem --cacert WS_SERVER_CERT.pem -H "Content-type: text/xml" https://server.ws -d 'some payload'
UPDATE
I've tried to load directly the PEM files (as allowed by the library https://github.com/Hakky54/sslcontext-kickstart) but with no success. The code I used is this:
X509ExtendedKeyManager keyManager = PemUtils.loadIdentityMaterial(Paths.get("src/main/resources/cert/CLIENT_CERT.pem"), Paths.get("src/main/resources/cert/CLIENT_PK.pem"));
X509ExtendedTrustManager trustManager = PemUtils.loadTrustMaterial(Paths.get("src/main/resources/cert/SERVER_CERT.pem"));
SSLFactory sslFactory = SSLFactory.builder()
.withIdentityMaterial(keyManager)
.withTrustMaterial(trustManager)
.build();
ConnectionSpec requireTls12 = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2)
.build();
OkHttpClient httpClient = new OkHttpClient.Builder()
.sslSocketFactory(sslFactory.getSslSocketFactory(), sslFactory.getTrustManager().orElseThrow())
.connectionSpecs(Arrays.asList(requireTls12))
.hostnameVerifier(sslFactory.getHostnameVerifier())
.build();
UPDATE2
I have issued the curl command with -v option to see a trace of the handshake. I see this:
TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS Unknown, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Unknown (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Client hello (1):
* TLSv1.3 (OUT), TLS Unknown, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS Unknown, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, CERT verify (15):
* TLSv1.3 (OUT), TLS Unknown, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
So I've tried to force the same TLS and cipher versions in java code, but I still get the error
UPDATE 3
I've tried to debug the TLS handshake adding the flag -Djavax.net.debug=all to the command line
But the output I get is not so clear to understand.. I don't post it here since I don't know if it can contain sensitive information...
perhaps this part could be safe:
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.814 CEST|ServerHelloDone.java:151|Consuming ServerHelloDone handshake message (
)
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.815 CEST|CertificateMessage.java:299|No X.509 certificate for client authentication, use empty Certificate message instead
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.815 CEST|CertificateMessage.java:330|Produced client Certificate handshake message (
"Certificates":
)
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.815 CEST|SSLSocketOutputRecord.java:261|WRITE: TLSv1.2 handshake, length = 7
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.816 CEST|SSLSocketOutputRecord.java:275|Raw write (
0000: 16 03 03 00 07 0B 00 00 03 00 00 00 ............
)
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.832 CEST|ECDHClientKeyExchange.java:410|Produced ECDHE ClientKeyExchange handshake message (
"ECDH ClientKeyExchange": {
"ecdh public": {
0000: 21 64 95 80 D9 B2 AC 8D A3 50 37 03 85 09 97 8C !d.......P7.....
0010: 93 8F F9 27 D8 93 C4 68 84 8C C7 C7 59 4F 33 36 ...'...h....YO36
},
}
)
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.833 CEST|SSLSocketOutputRecord.java:261|WRITE: TLSv1.2 handshake, length = 37
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.833 CEST|SSLSocketOutputRecord.java:275|Raw write (
0000: 16 03 03 00 25 10 00 00 21 20 21 64 95 80 D9 B2 ....%...! !d....
0010: AC 8D A3 50 37 03 85 09 97 8C 93 8F F9 27 D8 93 ...P7........'..
0020: C4 68 84 8C C7 C7 59 4F 33 36 .h....YO36
)
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.849 CEST|ChangeCipherSpec.java:115|Produced ChangeCipherSpec message
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.850 CEST|SSLSocketOutputRecord.java:235|Raw write (
0000: 14 03 03 00 01 01 ......
)
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.850 CEST|Finished.java:398|Produced client Finished handshake message (
"Finished": {
"verify data": {
0000: 56 33 5E 9F 3B 1B 8B 35 D7 56 61 82
}'}
)
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.850 CEST|SSLSocketOutputRecord.java:261|WRITE: TLSv1.2 handshake, length = 24
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.852 CEST|SSLCipher.java:1769|Plaintext before ENCRYPTION (
0000: 14 00 00 0C 56 33 5E 9F 3B 1B 8B 35 D7 56 61 82 ....V3^.;..5.Va.
)
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.853 CEST|SSLSocketOutputRecord.java:275|Raw write (
0000: 16 03 03 00 28 00 00 00 00 00 00 00 00 9C B0 33 ....(..........3
0010: FB 03 04 56 6C 98 72 AF 86 CF A2 55 47 D9 1B B6 ...Vl.r....UG...
0020: A3 0E 60 FA 15 2E 60 A2 AD 5B 8E 0B 24 .....
..[..$
)
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.899 CEST|SSLSocketInputRecord.java:494|Raw read (
0000: 14 03 03 00 01 .....
)
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.900 CEST|SSLSocketInputRecord.java:214|READ: TLSv1.2 change_cipher_spec, length = 1
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.900 CEST|SSLSocketInputRecord.java:494|Raw read (
0000: 01 .
)
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.900 CEST|SSLSocketInputRecord.java:247|READ: TLSv1.2 change_cipher_spec, length = 1
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.900 CEST|ChangeCipherSpec.java:149|Consuming ChangeCipherSpec message
javax.net.ssl|DEBUG|01|main|2023-05-03 21:45:56.901 CEST|SSLSocketInputRecord.java:494|Raw read (
0000: 16 03 03 00 28 ....(
)
after ServerHelloDone there is a message could be it the problem?
why it tells "No X.509 certificate for client authentication, use empty Certificate message instead" ??