HttpsURLConnection TrustManager

2014-10-05T00:00:00+00:00 Java

参考: https://developer.android.com/training/articles/security-ssl.html

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

以前にオレオレHTTPSにJavaから接続するメモというネタで書いてる。そこでTrustManagerを使用するがnullを返すだけなパターンが色々と問題なのでそういう手法を使わない方法をちょっと調べてみたのでメモ

キーストアを作る

# JKSなキーストアを作る
keytool \
  -importcert \
  -v \
  -trustcacerts \
  -file server.crt \
  -alias client \
  -keystore server.jks \
  -storetype JKS \
  -storepass abcdef

# BKSなキーストアを作る(BouncyCastleなライブラリをインストールしておく必要あり)
keytool \
  -importcert \
  -v \
  -trustcacerts \
  -file server.crt \
  -alias client \
  -keystore server.bks \
  -storetype BKS \
  -storepass abcdef

JKSとBKSのアルゴリズムの2つなパターンを検証してみるので(ただし以下ソースではJKSのみ)

クライアント

import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;


public class Main {
    public static void main(String[] args) {
        try (InputStream is = Main.class.getResourceAsStream("server.jks")) {
            KeyStore ks = KeyStore.getInstance("JKS");
            ks.load(is, "abcdef".toCharArray());

            TrustManagerFactory tmf = TrustManagerFactory.getInstance(
                TrustManagerFactory.getDefaultAlgorithm()
            );
            tmf.init(ks);

            SSLContext context = SSLContext.getInstance("TLS");
            context.init(null, tmf.getTrustManagers(), new SecureRandom());

            HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());

            HttpsURLConnection conn = (HttpsURLConnection)
                new URL("https://localhost/test.txt").openConnection(
                    new Proxy(Proxy.Type.HTTP, new InetSocketAddress("localhost", 8080))
                );
            conn.connect();

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

っていう感じで作って、上記でプロキシを介してリクエストするのでmitmproxyを使ってサーバーから送られてくる証明書等?を偽装出来るようなプロキシを使うと使用しているキーストアとサーバーから送られてくる不適切な証明書を検証して問題があれば例外が発生してリクエストが成立しなくなる(もちろんプロキシする部分のコードを消してプロキシを介さないようにした場合はサーバー側の証明書とキーストアを検証して問題は起きないのでリクエストは成立する)

で上記コードだとJKSを使ってるだけなので、BKSを使う場合にはserver.bksを使うのとKeyStore.getInstanceの引数にBKSを指定するだけでほかは特に変わらないので省略(※ただし、BKSの場合はキーストアパスを空にしてもロード出来る模様。詳しくは https://github.com/bcgit/bc-java/blob/master/prov/src/main/java/org/bouncycastle/jcajce/provider/keystore/bc/BcKeyStoreSpi.java#L786 )

っていうような感じでHTTPS通信するのはいいけど証明書周りの検証をやらないで問答無用ですっ飛ばすような実装するべきじゃないってことで。ちなみにそういう系の専門な人がいうにはサーバーから来た証明書とかのコモンネームなどもちゃんと検証するべきだとのこと

余談: X509証明書からKeyStoreなインスタンスを作る方法

import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;

public class Main {
    public static void main(String[] args) {
        try (InputStream is = Main.class.getResourceAsStream("server.crt")) {
            X509Certificate cert = (X509Certificate)CertificateFactory
                .getInstance("X.509")
                .generateCertificate(is);

            KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
            ks.load(null);
            ks.setCertificateEntry("ca", cert);

            TrustManagerFactory tmf = TrustManagerFactory.getInstance(
                TrustManagerFactory.getDefaultAlgorithm()
            );
            tmf.init(ks);

            SSLContext context = SSLContext.getInstance("TLS");
            context.init(null, tmf.getTrustManagers(), new SecureRandom());

            HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());

            HttpsURLConnection conn = (HttpsURLConnection)
                new URL("https://localhost/test.txt").openConnection(
                    new Proxy(Proxy.Type.HTTP, new InetSocketAddress("localhost", 8080))
                );
            conn.connect();

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

っていうような感じでCertificateFactoryを使って生成出来る

余談: TrustManagerを独自実装してチェックする場合

import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
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;

public class Main {
    public static void main(String[] args) {
        try (InputStream is = Main.class.getResourceAsStream("server.crt")) {
            SSLContext context = SSLContext.getInstance("TLS");
            context.init(
                null,
                new TrustManager[] { new SampleTrustManager(is) },
                new SecureRandom()
            );

            HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());

            HttpsURLConnection conn = (HttpsURLConnection)
                new URL("https://localhost/test.txt").openConnection(
                    new Proxy(Proxy.Type.HTTP, new InetSocketAddress("localhost", 8080))
                );
            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 {

        X509Certificate trustedCert;

        public SampleTrustManager(InputStream is) {
            try {
                trustedCert = (X509Certificate)CertificateFactory
                                .getInstance("X.509")
                                .generateCertificate(is);
            } catch (CertificateException e) {
                e.printStackTrace();
            }
        }

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

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

            X509Certificate cert = chain[0];

            if (cert != trustedCert) {
                throw new CertificateException();
            }
        }

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

この方法がいいのかは微妙。まだ曖昧な部分も若干あるのでまたネタを色々書くかもしれないってことで終わり

arquillian-managed