CA証明書とandroidの危うい関係 3

では端末にインストールされていない証明書をandroidアプリケーションで利用することは不可能なのだろうか? 否。

具体的にはHTTPS(SSL)通信で使用するソケットを生成する際に、通常使われるCA証明書の代わりに自ら用意した証明書を使って生成してやれば良い。コーディングに関してはandroid上の標準のHTTPクライアントであるApache HttpClientを使っても、javax.net.ssl.HttpsUrlConnectionを使っても実装できるが、後者はアドホックなコーディングで不具合を回避する必要があるのでApache HttpClientを使う方がお勧めだ。

以下、その手順。

使用するCA証明書からBouncy Castle形式のキーストアを作成する

androidで使用するCA証明書はBouncy Castle形式(.bks)のキーストアに格納することになっている。なので、jsdkのkeytoolでキーストアを作成する。なお、使用する証明書"caroot.cer"はCERでエンコードされた X.509形式の証明書である。(前回のエントリで書いたがJCE BouncyCastleProvicerをインストールしておく必要がある)

keytool -importcert -v -trustcacerts -file "caroot.cer" -alias ca -keystore "cacerts.bks" -storetype BKS -storepass <パスワード>

なお、この方法で作ったキーストアを使用すると、他の証明書を使ったSSL通信はできなくなる。他の証明書も引き続き使いたい場合は、前回のエントリで抽出したキーストアに今回使用するCA証明書をインポートしたものを配布する必要がある(その場合、キーストアの再配布の条件が発生する可能性があるため、ここではそちらの方法は紹介しない)

andrdroidプロジェクトにキーストアを配置する

アプリケーションからキーストアにアクセスするにはプロジェクト中に配置することが必要になる。プロジェクトルート下の"rawフォルダに"作成したキーストアをコピーしておくことで、以下のようにアプリケーション中のコンテキスト(ActicityやService)からキーストアをストリームとして読み込むことができるようになる。

Resources resources = context.getResources();
InputStream in = resources.openRawResource(R.raw.cacerts);
実装する

まずはApache HttpClientを使う例から。HttpClientは内部にcreateClientConnectionManagerメソッドを持ち、その中で使用する通信スキーマ毎のソケットファクトリを登録しているコードがあるので、これをオーバライドしてやることでソケット生成時に割り込むことができる。

  • Apache HttpClientを使用する
final Resources resources = context.getResources();
final InputStream in = resources.openRawResource(R.raw.cacerts);
final char[] passwdchars = "<パスワード>".toCharArray();

final HttpClient client = new DefaultHttpClient(){
    /* (non-Javadoc)
     * @see org.apache.http.impl.client.DefaultHttpClient#createClientConnectionManager()
     */
    @Override
    protected ClientConnectionManager createClientConnectionManager() {
        SchemeRegistry registry = new SchemeRegistry();
        registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
        if (resources != null) {
            registry.register(new Scheme("https", createSSLSocketFactory(), 443));
        } else {
            registry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
        }
        return new SingleClientConnManager(getParams(), registry);
    }
    private SSLSocketFactory createSSLSocketFactory() {
        KeyStore keyStore = KeyStore.getInstance("BKS");
        try {
            keyStore.load(in, passwdchars);
        } finally {
            in.close();
        }
        SSLSocketFactory socketFactory = new SSLSocketFactory(keyStore);
        socketFactory.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
        return socketFactory ;
    }       
}; 

Httpclientが生成できればあとは普通に通信を実行できるはずだ。