javasslclient-certificateshandshakemtls

Java two-way SSL with client certificate results in HTTP 403, but works in Postman


I am integrating with a SOAP server that uses two-way SSL (mutual TLS).

To validate my certificates were okay, I make a request using Postman and curl.

Via POSTMAN: I enable SSL certificate validation in Postman, add the CA certificate, and then add my client PFX certificate for the target host.

The request works successfully in Postman. If I remove the client certificate, the server returns HTTP 403, so I know the client certificate is required and being used.

With curl I just use openssl to transform pfx file in p12. The curl worked.

In Java, I add the CA certificate to the truststore. After doing this, I can complete the handshake, but I get HTTP 403 from the server, but it is okay since I hadn't had added the client pfx certificate.

Then, I add the same client PFX certificate to the keystore, but I continue to get HTTP 403. I tried to convert it to PKCS12 and to JKS, but I always got the same error.

Enabling SSL and handshake logs in Java, I can only see the CA certificate being sent—there is no evidence that the client certificate is being presented. Of course it is loaded and logged in the begin, but I can't see it in the handshake.

It seems like my java app is not using this.

Any suggestions on what might be happening?

Below is the simple Java code I am using (I made a minimal main method just to ensure the request is sent under the same conditions as Postman):


        String url = "https://.....";

        // Corpo da requisiç ão (como String)
        String body = "...";

        Map<String, String> headers = Map.of(
                "Content-Type", "application/soap+xml; charset=utf-8"
        );

        KeyStore clientStore = KeyStore.getInstance("PKCS12");
        InputStream keyStoreStream = new FileInputStream("client.pfx");
        clientStore.load(keyStoreStream, "A123456789".toCharArray());

        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(clientStore, "mypassword".toCharArray());

        KeyStore trustStore = KeyStore.getInstance("JKS");
        trustStore.load(new FileInputStream("ca_intermediate.jks"), "mypassword".toCharArray());

        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(trustStore);

        SSLContext sslContext = SSLContext.getInstance("TLS");
        //sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
        sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());

        SSLConnectionSocketFactory socketFactory =
                new SSLConnectionSocketFactory(sslContext, new DefaultHostnameVerifier());

        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("https", socketFactory)
                .build();

        PoolingHttpClientConnectionManager connectionManager =
                new PoolingHttpClientConnectionManager(socketFactoryRegistry);

        CloseableHttpClient httpClient = HttpClients.custom()
                .setSSLSocketFactory(socketFactory)
                .setConnectionManager(connectionManager)
                .build();


        HttpPost post = new HttpPost(url);
        post.setEntity(new StringEntity(body));

        headers.forEach(post::addHeader);
        HttpResponse response =httpClient.execute(post);

        System.out.println(response.getStatusLine().getStatusCode());


Solution

  • The issue was resolved as follows:

    The client certificate depended on a full chain of three certificates (two intermediates and one root).

    So, using Windows, I first imported the client certificate and then exported it again, this time including the entire certificate chain.

    Using OpenSSL, I converted the certificate to PEM format with the following command:

    openssl pkcs12 -in certifiate_full.pfx -out certifiate_full-export.pem -nodes --legacy

    This allowed me to confirm that all four certificates were present in the exported file.

    In Java, I kept the code I had originally written — the truststore still used the default cacert of the intermediate certified, but for the keystore I used the .pfx file that contained the full certificate chain.

    With this setup, I was able to successfully authenticate with the target service and received an HTTP 200 response.

    I believe that curl is more flexible and can handle incomplete or implicit chains better, whereas Java requires the full chain to be explicitly and correctly configured in the keystore. That explains the different behavior I observed.

    Thanks to everyone for the help!