[OkHttp] REST-API Java SSL

Introduction

I have written the code to call REST-API over SSL using OkHttp3. There are many samples out there that connect without validating the certificate, but this code does the following validation:

environment

Preparation

Get the server certificate

Obtain the server certificate from the place where the API is published. This time I got it from Recruit in the browser (Chrome). You can find various ways to get it by searching "How to get the server certificate browser". The format of the export file is "Base 64 encoded X.509".

Get the certificate authority certificate that signed the server certificate

It is the one above the certificate in the "Certificate Pass". Get it in the same way as above.

Combine server certificates

Open the above two certificates with a text editor and copy and paste one of them to combine them. I brought the parents down as a "proof pass".

Deploy the merged files to a location in your classpath.

Source code

API execution

API(SSL)Execution class


public class RestSecureApiExecutor extends RestApiExecutor {

    private CertificateManager certMgr;

    /**
     *constructor.
     *
     * @param apiAttr Secure API Attribute
     */
    public RestSecureApiExecutor(SecureApiAttribute apiAttr) {
        super(apiAttr);
        this.certMgr = apiAttr.getCertMgr();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Response get(Map<String, String> header, Map<String, String> param, boolean isValidCache) throws IOException {
        //URL assembly
        HttpUrl.Builder urlBuilder = createUrlBuilder(ProtocolType.Secure, param);
        //HTTP header assembly
        Request.Builder requestBuilder = createRequestBuilder(header);
        //Whether cache is used
        if (!isValidCache) {
            requestBuilder.cacheControl(new CacheControl.Builder().noCache().noStore().maxAge(0, TimeUnit.SECONDS).build());
        }
        //Assembling the request
        Request request = requestBuilder.url(urlBuilder.build()).build();
        //Whether cache is used
        OkHttpClient.Builder clientBuilder = createClientBuilder(isValidCache);

        addCertificatePinner(clientBuilder, certMgr.getCertificates());
        addSslSocketFactory(clientBuilder, certMgr.getTrustManager());

        //API execution
        return clientBuilder.build().newCall(request).execute();
    }

    /**
     *Add CertificatePinner to the builder.
     *
     * @param builder OkHttpClient.Builder object
     * @param certificates Certificate information
     */
    private void addCertificatePinner(OkHttpClient.Builder builder, Certificate[] certificates) {
        CertificatePinner certificatePinner = new CertificatePinner.Builder()
                .add(this.hostname, CertificatePinner.pin(certificates[0]))
                .build();
        builder.certificatePinner(certificatePinner);
    }

    /**
     *Add SslSocketFactory to the builder.
     *
     * @param builder OkHttpClient.Builder object
     * @param trustManager TrustManager
     */
    private void addSslSocketFactory(OkHttpClient.Builder builder, X509TrustManager trustManager) {
        try {
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, new TrustManager[] { trustManager }, null);
            builder.sslSocketFactory(sslContext.getSocketFactory(), trustManager);
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            throw new RuntimeException("____ failed to create ssl socket factory.", e);
        }
    }

Parent class


public class RestApiExecutor {

    /**
     *Protocol type.
     */
    public enum ProtocolType {
        /**Normal. */
        Normal("http"),
        /** SSL. */
        Secure("https"),;

        private String type;

        private ProtocolType(String type) {
            this.type = type;
        }

        public String getValue() {
            return this.type;
        }
    }

    /**hostname. */
    protected String hostname;
    /**segment. */
    protected String segment;

    /**
     *constructor.
     *
     * @param apiAttr API attribute
     */
    public RestApiExecutor(ApiAttribute apiAttr) {
        //Hostname, segment
        this.hostname = apiAttr.getHostname();
        this.segment = apiAttr.getSegment();
    }

    /**
     * REST API (get)To run.
     *
     * @param header HTTP header parameters
     * @param param query parameters
     * @param isValidCache cache used (true: use, false: not use)
     * @return response
     * @throws IOException If processing fails
     */
    public Response get(Map<String, String> header, Map<String, String> param, boolean isValidCache) throws IOException {
        //URL assembly
        HttpUrl.Builder urlBuilder = createUrlBuilder(ProtocolType.Normal, param);
        //HTTP header assembly
        Request.Builder requestBuilder = createRequestBuilder(header);
        //Whether cache is used
        if (!isValidCache) {
            requestBuilder.cacheControl(new CacheControl.Builder().noCache().noStore().maxAge(0, TimeUnit.SECONDS).build());
        }
        //Assembling the request
        Request request = requestBuilder.url(urlBuilder.build()).build();
        //Whether cache is used
        OkHttpClient.Builder clientBuilder = createClientBuilder(isValidCache);
        //API execution
        return clientBuilder.build().newCall(request).execute();
    }

    /**
     *Assemble the URL.
     *
     * @param type ProtocolType
     * @param param parameter
     * @return HttpUrl.Builder object
     */
    protected HttpUrl.Builder createUrlBuilder(ProtocolType type, Map<String, String> param) {
        //URL builder
        HttpUrl.Builder builder = new HttpUrl.Builder();
        //Schema(http or https)
        builder.scheme(type.getValue());
        //hostname
        builder.host(this.hostname);
        //segment
        builder.addPathSegments(this.segment);
        if (param != null) {
            //Query parameters
            param.forEach(builder::addQueryParameter);
        }
        return builder;
    }

    /**
     *Assemble HTTP headers.
     *
     * @param param parameter
     * @return Request.Builder object
     */
    protected Request.Builder createRequestBuilder(Map<String, String> param) {
        Request.Builder builder = new Request.Builder();
        builder.addHeader("Content-Type", "application/json");
        if (param != null) {
            param.forEach(builder::addHeader);
        }
        return builder;
    }

    /**
     *Assemble the HTTP client.
     *
     * @param isValidCache cache used or not
     * @return OkHttpClient.Builder object
     */
    protected OkHttpClient.Builder createClientBuilder(boolean isValidCache) {
        //Whether cache is used
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        if (!isValidCache) {
            builder.cache(null);
        }
        return builder;
    }

Around the certificate

Certificate reading


public class CertificateLoader {

    /**
     *Read the certificate.
     * 
     * @param path Certificate path
     * @return X509Certificate array
     */
    public X509Certificate[] load(String path) {
        List<X509Certificate> certificateList = new ArrayList<>();
        try {
            Collection<? extends Certificate> certificates = getCertificates(path);
            for (Certificate certificate : certificates) {
                X509Certificate x509certificate = (X509Certificate) certificate;
                x509certificate.checkValidity(); //Determine if the certificate is currently valid
                certificateList.add(x509certificate);
            }
        } catch (CertificateExpiredException | CertificateNotYetValidException e) {
            throw new RuntimeException("____ failed to check valid cer file.", e);
        }
        return certificateList.toArray(new X509Certificate[certificateList.size()]);
    }

    /**
    *Get certificate information.
    * 
    * @param path Certificate path
    * @return Certificate information
    */
    private Collection<? extends Certificate> getCertificates(String path) {
        try {
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            InputStream in = this.getClass().getClassLoader().getResourceAsStream(path);
            Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(in);
            if (certificates.isEmpty()) {
                throw new IllegalArgumentException("expected non-empty set of trusted certificates");
            }
            return certificates;
        } catch (CertificateException e) {
            throw new RuntimeException("____ failed to get certificates.");
        }
    }

Certificate management


public class CertificateManager {

    private CertificateLoader loader;

    private X509Certificate[] certificates;

    private X509TrustManager trustManager;

    /**
     *constructor.
     * 
     * @param path Certificate path
     */
    public CertificateManager(String path) {
        this.loader = new CertificateLoader();
        this.certificates = loader.load(path);

        //Create a certificate manager and verify that it is a trusted server certificate
        this.trustManager = createTrustManager(certificates);
        checkServerTrusted(trustManager, certificates);
    }

    /**
     *Get certificate information.
     * 
     * @return Certificate information
     */
    public X509Certificate[] getCertificates() {
        return this.certificates;
    }

    /**
     *Get TrustManager.
     * 
     * @return TrustManager
     */
    public X509TrustManager getTrustManager() {
        return this.trustManager;
    }

    /**
     *Generate TrustManager.
     * 
     * @param certificates Certificate information
     * @return TrustManager
     */
    private X509TrustManager createTrustManager(X509Certificate[] certificates) {
        TrustManager[] trustManagers = null;
        try {
            char[] password = "password".toCharArray();
            KeyStore keyStore = newEmptyKeyStore(password);

            int index = 0;
            for (X509Certificate certificate : certificates) {
                String certificateAlias = Integer.toString(index++);
                keyStore.setCertificateEntry(certificateAlias, certificate);
            }

            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            keyManagerFactory.init(keyStore, password);
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init(keyStore);

            trustManagers = trustManagerFactory.getTrustManagers();
            if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
                throw new IllegalStateException("Unexpected default trust managers : " + Arrays.toString(trustManagers));
            }
        } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
            throw new RuntimeException("____ failed to create trust manager.", e);
        }
        return (X509TrustManager) trustManagers[0];
    }

    /**
     *Create an empty KeyStore object.
     * 
     * @param password password
     * @return Empty KeyStore object
     */
    private KeyStore newEmptyKeyStore(char[] password) {
        KeyStore keyStore = getDefaultKeyStoreInstance();
        try {
            InputStream in = null;
            keyStore.load(in, password);
        } catch (NoSuchAlgorithmException | CertificateException | IOException e) {
            throw new RuntimeException("____ failed to load from KeyStore.", e);
        }
        return keyStore;
    }

    /**
     *Gets the default KeyStore object.
     * 
     * @return Default KeyStore object
     */
    private KeyStore getDefaultKeyStoreInstance() {
        try {
            return KeyStore.getInstance(KeyStore.getDefaultType());
        } catch (KeyStoreException e) {
            throw new RuntimeException("____ failed to get KeyStore default instance.", e);
        }
    }

    /**
     *Check the certificate.
     * 
     * @param trustManager TrustManager
     * @param certificates certificate
     */
    private void checkServerTrusted(X509TrustManager trustManager, X509Certificate[] certificates) {
        try {
            trustManager.checkServerTrusted(certificates, "SHA256withRSA");
        } catch (CertificateException e) {
            throw new RuntimeException("____ failed to check server trust.", e);
        }
    }

at the end

Hostname validation probably does, as it doesn't implement (override) the HostnameVerifier verify method.

that's all

Recommended Posts

[OkHttp] REST-API Java SSL
Java
Settings for SSL debugging in Java
Java
Memo when HTTP communication with Java (OkHttp)
Try using JobScheduler's REST-API --Java RestClient implementation--