【OkHttp】REST-API Java SSL

# はじめに

OkHttp3を使用して、REST-API を SSL で呼び出すコードを記載しています。
世間には証明書の検証をせずに接続するサンプルがたくさんありますが、このコードは以下の検証を実施しています。

* サーバ証明書が有効期間内かどうか
* サーバ証明書が信頼できるかどうか
* サーバ証明書に署名した認証局が信頼できるかどうか


# 環境
* java8
* OkHttp（3.10.0）

# 準備

## サーバ証明書を入手  
APIを公開しているところからサーバ証明書を入手します。
今回はブラウザ (Chrome) でリクルートさんからゲットしました。
入手方法は「サーバ証明書 入手方法 ブラウザ」で検索したらいろいろ見つかります。
エクスポートファイルの形式は「Base 64 encoded X.509」にしました。

## サーバ証明書に署名した認証局の証明書を入手
「証明のパス」の当該証明書の１つ上のやつです。上記と同じように入手します。    

## サーバ証明書を合体
上記２つの証明書をテキストエディタで開いて、どっちかをどっちかにコピペして合体させます。
「証明のパス」的に親の方を下にもっていきました。

合体させたファイルを クラスパス が通っている場所に配備します。

# ソースコード

## API実行

```java:API(SSL)実行クラス
public class RestSecureApiExecutor extends RestApiExecutor {

    private CertificateManager certMgr;

    /**
     * コンストラクタ.
     *
     * @param apiAttr セキュアAPI属性
     */
    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の組み立て
        HttpUrl.Builder urlBuilder = createUrlBuilder(ProtocolType.Secure, param);
        // HTTPヘッダの組み立て
        Request.Builder requestBuilder = createRequestBuilder(header);
        // キャッシュ使用有無
        if (!isValidCache) {
            requestBuilder.cacheControl(new CacheControl.Builder().noCache().noStore().maxAge(0, TimeUnit.SECONDS).build());
        }
        // リクエストの組み立て
        Request request = requestBuilder.url(urlBuilder.build()).build();
        // キャッシュ使用有無
        OkHttpClient.Builder clientBuilder = createClientBuilder(isValidCache);

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

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

    /**
     * builder に CertificatePinner を追加します.
     *
     * @param builder OkHttpClient.Builderオブジェクト
     * @param certificates 証明書情報
     */
    private void addCertificatePinner(OkHttpClient.Builder builder, Certificate[] certificates) {
        CertificatePinner certificatePinner = new CertificatePinner.Builder()
                .add(this.hostname, CertificatePinner.pin(certificates[0]))
                .build();
        builder.certificatePinner(certificatePinner);
    }

    /**
     * builder に SslSocketFactory を追加します.
     *
     * @param builder OkHttpClient.Builderオブジェクト
     * @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);
        }
    }
```

```java:親クラス
public class RestApiExecutor {

    /**
     * プロトコルタイプ.
     */
    public enum ProtocolType {
        /** 通常. */
        Normal("http"),
        /** SSL. */
        Secure("https"),;

        private String type;

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

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

    /** ホスト名. */
    protected String hostname;
    /** セグメント. */
    protected String segment;

    /**
     * コンストラクタ.
     *
     * @param apiAttr API属性
     */
    public RestApiExecutor(ApiAttribute apiAttr) {
        // ホスト名、セグメント
        this.hostname = apiAttr.getHostname();
        this.segment = apiAttr.getSegment();
    }

    /**
     * REST API (get) を実行します.
     *
     * @param header HTTPヘッダパラメータ
     * @param param クエリパラメータ
     * @param isValidCache キャッシュ使用（true：使用する、false：使用しない）
     * @return レスポンス
     * @throws IOException 処理に失敗した場合
     */
    public Response get(Map<String, String> header, Map<String, String> param, boolean isValidCache) throws IOException {
        // URLの組み立て
        HttpUrl.Builder urlBuilder = createUrlBuilder(ProtocolType.Normal, param);
        // HTTPヘッダの組み立て
        Request.Builder requestBuilder = createRequestBuilder(header);
        // キャッシュ使用有無
        if (!isValidCache) {
            requestBuilder.cacheControl(new CacheControl.Builder().noCache().noStore().maxAge(0, TimeUnit.SECONDS).build());
        }
        // リクエストの組み立て
        Request request = requestBuilder.url(urlBuilder.build()).build();
        // キャッシュ使用有無
        OkHttpClient.Builder clientBuilder = createClientBuilder(isValidCache);
        // API実行
        return clientBuilder.build().newCall(request).execute();
    }

    /**
     * URLを組み立てます.
     *
     * @param type ProtocolType
     * @param param パラメータ
     * @return HttpUrl.Builderオブジェクト
     */
    protected HttpUrl.Builder createUrlBuilder(ProtocolType type, Map<String, String> param) {
        // URLビルダー
        HttpUrl.Builder builder = new HttpUrl.Builder();
        // スキーマ(http or https)
        builder.scheme(type.getValue());
        // ホスト名
        builder.host(this.hostname);
        // セグメント
        builder.addPathSegments(this.segment);
        if (param != null) {
            // クエリパラメータ
            param.forEach(builder::addQueryParameter);
        }
        return builder;
    }

    /**
     * HTTPヘッダーを組み立てます.
     *
     * @param param パラメータ
     * @return Request.Builderオブジェクト
     */
    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;
    }

    /**
     * HTTPクライアントを組み立てます.
     *
     * @param isValidCache キャッシュ使用有無
     * @return OkHttpClient.Builderオブジェクト
     */
    protected OkHttpClient.Builder createClientBuilder(boolean isValidCache) {
        // キャッシュ使用有無
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        if (!isValidCache) {
            builder.cache(null);
        }
        return builder;
    }
```

## 証明書周り

```java:証明書読み込み
public class CertificateLoader {

    /**
     * 証明書を読み込みます.
     * 
     * @param path 証明書パス
     * @return X509Certificate配列
     */
    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(); // 証明書が現在有効であるかどうかを判定
                certificateList.add(x509certificate);
            }
        } catch (CertificateExpiredException | CertificateNotYetValidException e) {
            throw new RuntimeException("____ failed to check valid cer file.", e);
        }
        return certificateList.toArray(new X509Certificate[certificateList.size()]);
    }

    /**
    * 証明書情報を取得します.
    * 
    * @param path 証明書パス
    * @return 証明書情報
    */
    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.");
        }
    }
```

```java:証明書管理
public class CertificateManager {

    private CertificateLoader loader;

    private X509Certificate[] certificates;

    private X509TrustManager trustManager;

    /**
     * コンストラクタ.
     * 
     * @param path 証明書パス
     */
    public CertificateManager(String path) {
        this.loader = new CertificateLoader();
        this.certificates = loader.load(path);

        // 証明書マネージャを作成、および信頼できるサーバ証明書か検証
        this.trustManager = createTrustManager(certificates);
        checkServerTrusted(trustManager, certificates);
    }

    /**
     * 証明書情報を取得します.
     * 
     * @return 証明書情報
     */
    public X509Certificate[] getCertificates() {
        return this.certificates;
    }

    /**
     * TrustManagerを取得します.
     * 
     * @return TrustManager
     */
    public X509TrustManager getTrustManager() {
        return this.trustManager;
    }

    /**
     * TrustManagerを生成します.
     * 
     * @param certificates 証明書情報
     * @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];
    }

    /**
     * 空のKeyStoreオブジェクトを生成します.
     * 
     * @param password パスワード
     * @return 空のKeyStoreオブジェクト
     */
    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;
    }

    /**
     * デフォルトのKeyStoreオブジェクトを取得します.
     * 
     * @return デフォルトKeyStoreオブジェクト
     */
    private KeyStore getDefaultKeyStoreInstance() {
        try {
            return KeyStore.getInstance(KeyStore.getDefaultType());
        } catch (KeyStoreException e) {
            throw new RuntimeException("____ failed to get KeyStore default instance.", e);
        }
    }

    /**
     * 証明書をチェックします.
     * 
     * @param trustManager TrustManager
     * @param certificates 証明書
     */
    private void checkServerTrusted(X509TrustManager trustManager, X509Certificate[] certificates) {
        try {
            trustManager.checkServerTrusted(certificates, "SHA256withRSA");
        } catch (CertificateException e) {
            throw new RuntimeException("____ failed to check server trust.", e);
        }
    }
```

# さいごに

ホスト名の検証は、HostnameVerifier の verifyメソッドを実装(オーバーライド)していないので、たぶん検証されているはずです。

以上