1

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" ??

Nik
  • 247
  • 1
  • 8
  • For the runtime, or test-runtime, your `Path` can't be `src/test/resources/...`, if you sun some Maven app, your path should be something like `Path.of("./resources/...")`. Have look on how maven package runnable code. – Zorglube May 02 '23 at 15:56
  • Thanks a lot but the problem is not the way the keystore files are read. This is just a proof of concept, not definitive code in any sense. My concern is to establish the connection with this service in java .... Once I get this I will refactor the code – Nik May 02 '23 at 15:58
  • Have you tried the pemutils of the same library? Then you can use the same files as which you have used for curl – Hakan54 May 02 '23 at 18:00
  • Thanks for the suggestion, going to try – Nik May 03 '23 at 07:03
  • No luck .. I've tried building key and trust manager from pem but with no success... – Nik May 03 '23 at 07:58
  • Can you try to add `.withUnsafeTrustMaterial()` to the sslfactory to see what your error will be if we disable the validation. Can you retry and share your result and if there is an error can you paste the stacktrace? – Hakan54 May 03 '23 at 18:48
  • I have tried already that option but result is the same. I don't get any exception, the authentication simply fails. – Nik May 03 '23 at 19:46
  • could be the same problem as in https://stackoverflow.com/questions/9299133/why-doesnt-java-send-the-client-certificate-during-ssl-handshake ? – Nik May 03 '23 at 19:59
  • 1
    Well in java there is a key manager and that one is picking up the first key matching based on keytype and some other properties. What you can do is to add a breakpoint on the method chooseClientAlias within your actual keymanager implementation and discover why it is returning empty instead of a value – Hakan54 May 03 '23 at 22:08

1 Answers1

1

I eventually succeed in making this call. Following the advice by @Hakan54 to debug KeyManager I found that, in my IntelliJ ide, there was a mismatch of JDK versions. My project was targeted to JDK11 (pom.xml and in SDK target in project settings) BUT, in project modules there was a JDK17 which apparently caused this error. I noticed it because, during debugging, I got the IntelliJ warning that "source code does not match compiled code". So I started investigating and, once I changed JDK17 to JDK11 in the modules section, the connection starts working.

Nik
  • 247
  • 1
  • 8