1

I am trying to write API calls using Webclient in Java. Currently I'm having trouble finding documentation on how to add certificates to Webclient. I want to provide both a CA certificate file in PEM format, As well as a Client Certificate wherein I would provide a host, a CRT file, a key file, and a passphrase. I've got this setup working in postman, but I'd like to transfer it to a Java application. Below is the code I have.

       Gson gson = new Gson();
       LinkedHashMap<String, Object> reqBody
               = new LinkedHashMap<String, Object>();
       LinkedHashMap<String, String> variables
               = new LinkedHashMap<String, String>();
       reqBody.put("variables", variables);



       WebClient webClient = WebClient.builder()
               .baseUrl("sampleurl.com")
               .defaultHeader(HttpHeaders.USER_AGENT, "Spring 5 WebClient")
               .defaultHeader(HttpHeaders.ACCEPT, "application/json")
               .defaultHeader(HttpHeaders.CONTENT_TYPE, "application/json")
               .build();


       return webClient.post()
               .uri("/api")
               .headers(headers -> headers.setBasicAuth("userName", "password"))
               .body(Mono.just(reqBody), LinkedHashMap.class)//if directly putting the map doesn't work
               //can also convert to json string then to monoflux
               .retrieve()
               .bodyToMono(String.class);
AmateurPCB
  • 33
  • 1
  • 6
  • 1
    Have a look [here](https://stackoverflow.com/questions/45418523/spring-5-webclient-using-ssl) how to configure ssl with with `WebClient`. Might help. – Michał Krzywański Nov 10 '20 at 17:05

1 Answers1

1

Although michalk provided a link for an example ssl configuration for the webclient, the question still remain unanswered of how to load the CA Certificates, Key and passphrase.

If you only want to load the pem formated (ca and own trusted certificates)certificates you can use the classes which are available within the jdk. But you also want to load the key material as pem file and that is not possible with the default classes within the jdk. I would recommend Bouncy castle for this use case:

maven dependency:

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk15on</artifactId>
</dependency>
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.bouncycastle.pkcs.PKCSException;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

class App {

    private static final BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider();
    private static final JcaPEMKeyConverter KEY_CONVERTER = new JcaPEMKeyConverter().setProvider(BOUNCY_CASTLE_PROVIDER);
    private static final String CERTIFICATE_TYPE = "X.509";
    private static final Pattern CERTIFICATE_PATTERN = Pattern.compile("-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----", Pattern.DOTALL);
    private static final String NEW_LINE = "\n";
    private static final String EMPTY = "";

    public static PrivateKey parsePrivateKey(String privateKeyContent, char[] keyPassword) throws IOException, PKCSException, OperatorCreationException {
        PEMParser pemParser = new PEMParser(new StringReader(privateKeyContent));
        PrivateKeyInfo privateKeyInfo = null;

        Object object = pemParser.readObject();

        while (object != null && privateKeyInfo == null) {
            if (object instanceof PrivateKeyInfo) {
                privateKeyInfo = (PrivateKeyInfo) object;
            } else if (object instanceof PEMKeyPair) {
                privateKeyInfo = ((PEMKeyPair) object).getPrivateKeyInfo();
            } else if (object instanceof PKCS8EncryptedPrivateKeyInfo) {
                InputDecryptorProvider inputDecryptorProvider = new JceOpenSSLPKCS8DecryptorProviderBuilder()
                        .setProvider(BOUNCY_CASTLE_PROVIDER)
                        .build(Objects.requireNonNull(keyPassword));

                privateKeyInfo = ((PKCS8EncryptedPrivateKeyInfo) object).decryptPrivateKeyInfo(inputDecryptorProvider);
            } else if (object instanceof PEMEncryptedKeyPair) {
                PEMDecryptorProvider pemDecryptorProvider = new JcePEMDecryptorProviderBuilder()
                        .setProvider(BOUNCY_CASTLE_PROVIDER)
                        .build(keyPassword);

                PEMKeyPair pemKeyPair = ((PEMEncryptedKeyPair) object).decryptKeyPair(pemDecryptorProvider);
                privateKeyInfo = pemKeyPair.getPrivateKeyInfo();
            }

            if (privateKeyInfo == null) {
                object = pemParser.readObject();
            }
        }

        if (Objects.isNull(privateKeyInfo)) {
            throw new IllegalArgumentException("Received an unsupported private key type");
        }

        return KEY_CONVERTER.getPrivateKey(privateKeyInfo);
    }

    public static List<Certificate> parseCertificate(String certificateContent) throws IOException, CertificateException {
        List<Certificate> certificates = new ArrayList<>();
        Matcher certificateMatcher = CERTIFICATE_PATTERN.matcher(certificateContent);

        while (certificateMatcher.find()) {
            String sanitizedCertificate = certificateMatcher.group(1).replace(NEW_LINE, EMPTY).trim();
            byte[] decodedCertificate = Base64.getDecoder().decode(sanitizedCertificate);
            try(ByteArrayInputStream certificateAsInputStream = new ByteArrayInputStream(decodedCertificate)) {
                CertificateFactory certificateFactory = CertificateFactory.getInstance(CERTIFICATE_TYPE);
                Certificate certificate = certificateFactory.generateCertificate(certificateAsInputStream);
                certificates.add(certificate);
            }
        }

        return certificates;
    }

    public static <T extends Certificate> KeyStore createTrustStore(List<T> certificates) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException {
        KeyStore trustStore = createEmptyKeyStore();
        for (T certificate : certificates) {
            trustStore.setCertificateEntry(UUID.randomUUID().toString(), certificate);
        }
        return trustStore;
    }

    public static <T extends Certificate> KeyStore createKeyStore(PrivateKey privateKey, char[] privateKeyPassword, List<T> certificateChain) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException {
        KeyStore keyStore = createEmptyKeyStore();
        keyStore.setKeyEntry(UUID.randomUUID().toString(), privateKey, privateKeyPassword, certificateChain.toArray(new Certificate[0]));
        return keyStore;
    }

    public static KeyStore createEmptyKeyStore() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null, null);
        return keyStore;
    }

    public static void main(String[] args) throws PKCSException, OperatorCreationException, IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
        String certificateChainContent = "";
        String privateKeyContent = "";
        char[] privateKeyPassword = "secret".toCharArray();

        String caCertificateContent = "";

        PrivateKey privateKey = parsePrivateKey(privateKeyContent, privateKeyPassword);
        List<Certificate> certificates = parseCertificate(certificateChainContent);

        List<Certificate> caCertificates = parseCertificate(caCertificateContent);

        KeyStore keyStore = createKeyStore(privateKey, privateKeyPassword, certificates);
        KeyStore trustStore = createTrustStore(caCertificates);

        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, null);

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

        SslContext sslContext = SslContextBuilder.forClient()
                .keyManager(keyManagerFactory)
                .trustManager(trustManagerFactory)
                .build();

        HttpClient httpClient = HttpClient.create()
                .secure(sslSpec -> sslSpec.sslContext(sslContext));

        WebClient webClient = WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }

}

Can you try this and share the results?

I know that the above code snippet is a bit verbose, so If you don't want to include all of it I can also provide you an alternative which can achieve the same:

<dependency>
    <groupId>io.github.hakky54</groupId>
    <artifactId>sslcontext-kickstart-for-pem</artifactId>
    <version>5.3.0</version>
</dependency>
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import nl.altindag.sslcontext.util.PemUtils;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCSException;
import reactor.netty.http.client.HttpClient;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;

import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509ExtendedTrustManager;
import java.io.IOException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;

class App {
    
    
    public static void main(String[] args) throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException, OperatorCreationException, PKCSException {
        X509ExtendedKeyManager keyManager = PemUtils.loadIdentityMaterial("certificateChain.pem", "private-key.pem", "secret".toCharArray());
        X509ExtendedTrustManager trustManager = PemUtils.loadTrustMaterial("ca-certificates.pem");

        SslContext sslContext = SslContextBuilder.forClient()
                .keyManager(keyManager)
                .trustManager(trustManager)
                .build();

        HttpClient httpClient = HttpClient.create()
                .secure(sslSpec -> sslSpec.sslContext(sslContext));

        WebClient webClient = WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }
}

It uses the same code snippets under the covers which I shared within the first code snippet.

Hakan54
  • 3,121
  • 1
  • 23
  • 37
  • I ended up finding a solution before you answered. My solution for adding the sslContext to my webclient builder was identical to yours. However, instead of: using code to create the keystore, create certificates from my certificate and key combo, then add them to that keystore, I instead: used keytool and openssl commands to first create a keystore, then create a .pem certificate using the key and certificate(s) I was provided. I then converted that to .p12, and added it to my created Java Keystore. I then used java code like yours to create the sslcontext with my created keystore. – AmateurPCB Nov 16 '20 at 21:56
  • Ah yes, creating a keystore file and importing the p12 files is also a good alternative, well done! – Hakan54 Nov 17 '20 at 07:50