Java で bit.ly API を SSL で呼び出す

  • 投稿日:
  • by

URL 短縮サービスの bit.ly には OAuth 認証用とかで SSL でアクセスできる API のエンドポイント https://aps-ssl.bit.ly/ があるんですけど、ここのサーバ証明書が StartSSL って CA のを使ってて、その CA 証明書を Java 標準のキーストアが持っていないからさあ大変。
知らない CA に署名された証明書だから "sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target" なんて例外が出て通信できません。

Firefox とかでアクセスしてみると普通に成功するので、うさんくさい CA ではないようです。一般的な Web ブラウザがルート証明書を持っているなら信用して問題ないはず。

単に使う分には %JAVA_HOME%/lib/security/cacerts に CA のルート証明書をインポートすればいいんですが、Java のアップデートで cacerts が変わるたびにインポートしないといけないしめんどくさいです。
今回の bit.ly のように StartSSL 使ってるとこに SSL でアクセスするときだけ StartSSL のルート証明書を読むようにすればいいだけなので、専用のキーストアを作って読み込むようにしてみました。

キーストアの作り方は簡単で、Java 添付の keytool というコマンドラインアプリを使います。
証明書は IE や Firefox、Chrome といった Web ブラウザからエクスポートもできますが、CA 自身が配布してるファイルをダウンロードするのが正攻法でしょう。
https://www.startssl.com/certs/ からルート証明書 ca.crt と中間証明書 sub.class2.server.ca.crt の 2 ファイルをゲットしておきます。bit.ly のサーバ証明書は class2 ってやつで署名されてるので、両方ないと認証されません。

>keytool -importcert -keystore キーストアのファイル名 -store_pass キーストアのパスワード -trustcacerts -alias startcom.ca -file ca.crt
>keytool -importcert -keystore キーストアのファイル名 -store_pass キーストアのパスワード -alias startcom.ca.class2 -file sub.class2.server.ca.crt

「キーストアのファイル名」と「キーストアのパスワード」は Java コードから参照するのでメモっときましょう。それほど重要なファイルでもないのでパスワードは適当でもいいと思います。

キーストアが用意できたら、あとは SSL 通信する時にこのキーストアを使うようにすればいいだけです。
Java での SSL 通信は javax.net.ssl.HttpsURLConnection を使うと簡単、というか java.net.URL.openConnection() で https な URL を開けばこれが返ってきます。
HttpsURLConnection の connect() を呼び出す前に setSSLSocketFactory(SSLSocketFactory sf) を呼び出して、用意したキーストアを利用する SSLSocketFactory を渡してやります。

通信のたびにキーストアを読み込んだりするのもアレなので、下のようなクラスで必要なときだけ installCaCert() に HttpsURLConnection オブジェクトを渡してやるといいと思います。
キーストアファイルは class ファイルと同じディレクトリに置く必要がありますが、別の場所に置きたければ StartComCert.class.getResourceAsStream(storeName) の部分を書き換えてやってください。

import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

public class StartComCert {

    private static final ThreadLocal random = new ThreadLocal(){
        @Override
        protected SecureRandom initialValue() {
            return new SecureRandom();
        };
    };

    private static final KeyManager[] keyManager;
    private static final TrustManager[] trustManager;

    static {
        try {
            String storeName = "キーストアのファイル名";
            char[] storePass = "キーストアのパスワード".toCharArray();
            KeyStore keyStore = KeyStore.getInstance("JKS");
            keyStore.load(StartComCert.class.getResourceAsStream(storeName), storePass);

            KeyManagerFactory keyManFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            keyManFactory.init(keyStore, storePass);

            TrustManagerFactory trustManFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManFactory.init(keyStore);

            keyManager = keyManFactory.getKeyManagers();
            trustManager = trustManFactory.getTrustManagers();
        } catch (Exception e) {
            throw new RuntimeException("Failed to load KeyStore", e);
        }
    }

    private StartComCert() {
        throw new AssertionError("should not invoke");
    }

    public static void installCaCert(HttpsURLConnection conn) {
        try {
            SSLContext sslContext = SSLContext.getInstance("SSL");
            sslContext.init(keyManager, trustManager, random.get());
            conn.setSSLSocketFactory(sslContext.getSocketFactory());
        } catch (KeyManagementException ignore) {
            throw new AssertionError("will not happen");
        } catch (NoSuchAlgorithmException ignore) {
            throw new AssertionError("will not happen");
        }
    }
}

たぶん JRE 1.4 以降なら動きます。
これで https://aps-ssl.bit.ly/ にも問題なくアクセスできるはずです。配布する場合もキーストアごと jar にまとめればいいだけなのでラクですね。

KeyManager[] と TrustManager[] ってスレッド意識せず共有してしまっていいのかどうかかいまいち不明なので、もしアレならこれも ThreadLocal に持たせてしまったほうがいいんでしょうね。