HttpsURLConnection TrustManagerでPublic Key Pinning

2015-07-07T09:55:21+09:00 Java

参考1: 不正なSSL証明書を見破るPublic Key Pinningを試す

参考2: https://developer.mozilla.org/ja/docs/Web/Security/Public_Key_Pinning

関連エントリー: HttpsURLConnection TrustManager

要はHTTPS通信をする際にサーバーから送られてくる証明書が正当な物であるかをどう検証するべきかっていう所が色々あるのですが、それを検証する手法としてあるのがPublic Key Pinningっていう事っぽい(あってるかは微妙)

とりあえず色々検証してみた

※あくまでセキュリティ対策を前提としたネタではない

※個人的なメモなので上記参考を読むべき

検証の下準備

mitmproxyが使える環境とHTTPSなサーバーを用意しておく。んでサーバー側の証明書がある環境下において以下のコマンドを実行して証明書のシグニチャとなるbase64を取得する(※証明書じゃなくても秘密鍵等からも生成可能。詳しくは上記参考2を参照)

openssl x509 -in server.crt -pubkey -noout | openssl pkey -pubin -outform der| openssl dgst -sha256 -binary | openssl base64

これを実行した値をSSLコンテキストにおいて証明書のチェックを行う際にこの値で検証するような感じかと

Main.java

package sample;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URL;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.commons.codec.binary.Base64;

public class Main {

    private static final Proxy proxy = new Proxy(
        Proxy.Type.HTTP,
        new InetSocketAddress("localhost", 8080)
    );

    private static final String url = "https://localhost/report.json";

    public static void main(String[] args) {
        try {

            SSLContext context = SSLContext.getInstance("TLS");
            context.init(
                null,
                new TrustManager[] { new SampleTrustManager() },
                new SecureRandom()
            );

            HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());
            HttpsURLConnection conn = (HttpsURLConnection)new URL(url).openConnection(proxy);
            conn.connect();

            System.out.println(conn.getResponseCode());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static class SampleTrustManager implements X509TrustManager {

        private final String trustBase64 = "コマンドで生成したbase64";

        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType)
            throws CertificateException {
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType)
            throws CertificateException {

            // SSLコンテキストから作用する証明書のチェックを行う
            X509Certificate cert = chain[0];
            checkTrustCert(cert);
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[] {};
        }

        private void checkTrustCert(X509Certificate cert) throws CertificateException {
            String base64 = toBase64(cert.getPublicKey().getEncoded());

            // 正当な証明書では無いとbase64でチェックしマッチしないなら例外を送出する
            if (!trustBase64.equals(base64)) {
                throw new CertificateException("cert doesn`t match " + base64);
            }
        }

        // X509Certificate.getPublicKey().getEncoded()から取得した鍵からbase64を取得
        private String toBase64(byte[] b) {
            String base64 = null;

            try {
                MessageDigest md = MessageDigest.getInstance("SHA-256");
                md.update(b);

                byte[] sha256byte = md.digest();
                base64 = Base64.encodeBase64String(sha256byte);
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }

            return base64;
        }
    }
}

っていうようにTrustManagerで証明書のチェックを行う際にサーバー側の証明書で示されたbase64と照らし合わせる事で不正な証明書であるかをチェックするような感じかと。場合によってproxyの引数を入れたり消したりして確認するとproxyを指定している場合にはサーバーが示しているbase64とはマッチしない為に例外が発生する。今回はやってないけど、hostもちゃんと検証するべきかと思われる

前回の関連エントリーでも示しているような証明書自体をJCEを使って組み込むような方式を使うよりもよりシンプルに実装出来るのではないかと

あくまでHttpsURLConnectionをグダグダと使う場合はこういうような実装しなければならないけど、okhttpを採用する場合にはCerfificatePinnerっていうクラスでサポートされているので使うクライアントライブラリによってサポートされているか調べる等すれば良いんじゃないかと

Google Cloud Endpoints(Java)のApiTransformer