I've built a Java client to a HTTP server protected with mutual authentication. This is not the first time I do this, but this is the first time I had this problem.The application is a Spring Boot with OpenFeign, but I've reduced this problem to a simple pure Java HttpClient.
Edit:It appears that the problem is not a mismatch of signature algorithms as stated below. The problem is that the client certificate is not being sent for some unknown reason...
Additionally to the methods described bellow, I also tried to load the certificate and key through their DER files... with the same error...
It appears that the problem is a mismatch of signature algorithms. Here's a piece of log (-Djavax.net.debug=ssl:handshake):
javax.net.ssl|DEBUG|51|HttpClient-1-Worker-0|2024-05-03 14:33:06.372 BRT|CertificateRequest.java:690|Consuming CertificateRequest handshake message ("CertificateRequest": {"certificate types": [rsa_sign, dss_sign, ecdsa_sign]"supported signature algorithms": [ecdsa_secp521r1_sha512, rsa_pkcs1_sha512, ecdsa_secp384r1_sha384, rsa_pkcs1_sha384, ecdsa_secp256r1_sha256, rsa_pkcs1_sha256, dsa_sha256, ecdsa_sha224, rsa_sha224, dsa_sha224, ecdsa_sha1, rsa_pkcs1_sha1, dsa_sha1]"certificate authorities": [...]})javax.net.ssl|ALL|51|HttpClient-1-Worker-0|2024-05-03 14:33:06.376 BRT|X509Authentication.java:221|No X.509 cert selected for [EC, RSA, DSA]javax.net.ssl|WARNING|51|HttpClient-1-Worker-0|2024-05-03 14:33:06.376 BRT|CertificateRequest.java:781|No available authentication schemejavax.net.ssl|DEBUG|51|HttpClient-1-Worker-0|2024-05-03 14:33:06.377 BRT|ServerHelloDone.java:151|Consuming ServerHelloDone handshake message (<empty>)javax.net.ssl|DEBUG|51|HttpClient-1-Worker-0|2024-05-03 14:33:06.377 BRT|CertificateMessage.java:293|No X.509 certificate for client authentication, use empty Certificate message insteadjavax.net.ssl|DEBUG|51|HttpClient-1-Worker-0|2024-05-03 14:33:06.377 BRT|CertificateMessage.java:324|Produced client Certificate handshake message ("Certificates": <empty list>)
When verifying the certificate information, the signing algorithm is "SHA256withRSA" which is not present in the list above.
But, (and here comes the odd part) some other tests I do work.
- Curl informing the certificate and key works
- Insomnia Rest with certificate and key works
- Postman with .p12 (PKCS12 / PFX) (the same configured in Java) works
And printing the server's SSL info with OpenSSL, the signature algorithms is different from the list I got with Java.
openssl s_client -connect xxxxx.com:443...Acceptable client certificate CA names...Client Certificate Types: RSA sign, DSA sign, ECDSA signRequested Signature Algorithms: ECDSA+SHA512:RSA+SHA512:ECDSA+SHA384:RSA+SHA384:ECDSA+SHA256:RSA+SHA256:DSA+SHA256:ECDSA+SHA224:RSA+SHA224:DSA+SHA224:ECDSA+SHA1:RSA+SHA1:DSA+SHA1Shared Requested Signature Algorithms: ECDSA+SHA512:RSA+SHA512:ECDSA+SHA384:RSA+SHA384:ECDSA+SHA256:RSA+SHA256:DSA+SHA256:ECDSA+SHA224:RSA+SHA224:DSA+SHA224Peer signing digest: SHA256Peer signature type: RSAServer Temp Key: ECDH, prime256v1, 256 bits...
Shouldn't these "signature algorithms" be the same? What am I missing here?
I tried to configure my certificate using Java Properties
System.setProperty("javax.net.ssl.trustStore", "trust.p12"); System.setProperty("javax.net.ssl.trustStorePassword", "password"); System.setProperty("javax.net.ssl.trustStoreType", "PKCS12"); System.setProperty("javax.net.ssl.keyStore", "keys.p12"); System.setProperty("javax.net.ssl.keyStorePassword", "password"); System.setProperty("javax.net.ssl.keyStoreType", "PKCS12");
And programmatically
KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(new FileInputStream("keys.p12"), "password".toCharArray()); KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); kmf.init(keyStore, "password".toCharArray()); KeyStore truststore = KeyStore.getInstance("PKCS12"); truststore.load(new FileInputStream("trust.p12"), "password".toCharArray()); TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); tmf.init(truststore); SSLContext sslContext = SSLContext.getInstance("SSL"); sslContext.init(kmf.getKeyManagers(), trustAllCerts, new SecureRandom()); HttpClient httpClient = HttpClient.newBuilder().sslContext(sslContext).build(); String endpoint = "https://xxxxx.com/v1/list"; String json = "{\"test\": \"test\"}"; HttpRequest.BodyPublisher requestBody = HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8); HttpRequest httpRequest = HttpRequest.newBuilder() .uri(URI.create(endpoint)) .POST(requestBody) .setHeader("Content-Type", "application/json") .build(); HttpResponse<String> response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
Both tries (java 17 and 21) resulted in
Exception in thread "main" javax.net.ssl.SSLHandshakeException: Remote host terminated the handshake at java.net.http/jdk.internal.net.http.HttpClientImpl.send(HttpClientImpl.java:956) at java.net.http/jdk.internal.net.http.HttpClientFacade.send(HttpClientFacade.java:133) at org.example.SslTest2.main(SslTest2.java:65)Caused by: javax.net.ssl.SSLHandshakeException: Remote host terminated the handshake at java.net.http/jdk.internal.net.http.common.SSLTube.checkForHandshake(SSLTube.java:595) at java.net.http/jdk.internal.net.http.common.SSLTube$SSLTubeFlowDelegate.checkForHandshake(SSLTube.java:156) at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate$Reader.errorCommon(SSLFlowDelegate.java:369) at java.net.http/jdk.internal.net.http.common.SubscriberWrapper.onError(SubscriberWrapper.java:412) at java.net.http/jdk.internal.net.http.SocketTube$InternalReadPublisher$ReadSubscription.signalCompletion(SocketTube.java:645) at java.net.http/jdk.internal.net.http.SocketTube$InternalReadPublisher$InternalReadSubscription.read(SocketTube.java:829) at java.net.http/jdk.internal.net.http.SocketTube$SocketFlowTask.run(SocketTube.java:181) at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:207) at java.net.http/jdk.internal.net.http.common.SequentialScheduler.runOrSchedule(SequentialScheduler.java:280) at java.net.http/jdk.internal.net.http.common.SequentialScheduler.runOrSchedule(SequentialScheduler.java:233) at java.net.http/jdk.internal.net.http.SocketTube$InternalReadPublisher$InternalReadSubscription.signalReadable(SocketTube.java:782) at java.net.http/jdk.internal.net.http.SocketTube$InternalReadPublisher$ReadEvent.signalEvent(SocketTube.java:965) at java.net.http/jdk.internal.net.http.SocketTube$SocketFlowEvent.handle(SocketTube.java:253) at java.net.http/jdk.internal.net.http.HttpClientImpl$SelectorManager.handleEvent(HttpClientImpl.java:1467) at java.net.http/jdk.internal.net.http.HttpClientImpl$SelectorManager.lambda$run$3(HttpClientImpl.java:1412) at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) at java.net.http/jdk.internal.net.http.HttpClientImpl$SelectorManager.run(HttpClientImpl.java:1412)Caused by: java.net.SocketException: Connection reset at java.base/sun.nio.ch.SocketChannelImpl.throwConnectionReset(SocketChannelImpl.java:401) at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:434) at java.net.http/jdk.internal.net.http.SocketTube.readAvailable(SocketTube.java:1178) at java.net.http/jdk.internal.net.http.SocketTube$InternalReadPublisher$InternalReadSubscription.read(SocketTube.java:841) ... 11 more