简体   繁体   中英

How to add both a CA certificate and a Client Certificate using Webclient in Java

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);

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM