いまだに Java 6 使ってます、はい…。
移行しなければならないのは重々承知しています、はい…。
そんな中、HTTPS のセキュリティプロトコルを TLS 1.2 に移行するサイトが増え、TLS 1.2 に未対応の Java 6 ではスクレイピング出来なくなって困っていました。色々と検索した所、暗号化ライブラリを導入すると Java 6 でも TLS 1.2 に対応する事が可能な様です。
また、Let's Encrypt といった新しめの認証局のサーバー証明書も、 Java 6 では対応出来ていないので、それらも導入します。
お品書き。
- Bouncy Castle という暗号化ライブラリを導入します。
ライセンスは Apache Software License, Version 1.1. の様です。
- Bouncy Castle を利用して SSL 通信を行う SSLSocketFactory の実装クラス TLSSocketFactory、および関連クラスを導入します。
ライセンスは MIT X11 License の様です。
- 有志作成のスクリプトを参考にし、Let's Encrypt のサーバー証明書を導入します。
Windows 版 bat ファイル(証明書ファイル付き)、Linux 版 sh ファイル(証明書ファイルダウンロード)を参考にしました。
- Let's Encrypt の中間認証局のサーバー証明書を導入します。
IdenTrust DST Root および、ISRG Root です。
「2.」の Bouncy Castle を利用するクラスについては、別のクラスについて日本語で発信している方が何人かいらっしゃったのですが、私の環境ではことごとく機能しませんでした。細かくは検証していないので原因は不明ですが…。
「4.」の中間認証局のサーバー証明書については、ネットの情報ではどうしてもうまく機能せず、最終手段として Java 8 のキーストアからサーバー証明書を抜いてきて、java 6 のキーストアに突っ込む、とい強引な方法をとっています。
まあ、自己責任ですね…。
Bouncy Castle をプロジェクトへ導入します。
- Bouncy Castle の jar ファイルをダウンロードします。
Latest Releases からダウンロードできます。
今回私が使用したのは bcprov-jdk15on-160.jar です。
- 上記 jar ファイルをプロジェクトのライブラリとして追加します。
- また、実行環境のクラスパスにも上記 jar ファイルを追加します。
SSLSocketFactory の実装クラス TLSSocketFactory、および関連クラスを導入します。
TLSSocketFactory からダウンロードするなり、クローンするなり、コピペするなりし、プロジェクトへ 5つのクラスを追加します。
- メインとなるクラス TLSSocketFactory。
javax.net.ssl.SSLSocketFactory 抽象クラスの実装クラスです。
- 関連クラスTLSAuthentication。
org.bouncycastle.crypto.tls.TlsAuthentication インターフェイスの実装クラスです。
- 関連クラス TLSClient。
org.bouncycastle.crypto.tls.DefaultTlsClient 抽象クラスの実装クラスです。
- 関連クラス TLSSession。
javax.net.ssl.SSLSession インターフェイスの実装クラスです。
- こっちの方がメインとなるクラスとも言える TLSSocket。
javax.net.ssl.SSLSocket 抽象クラスの実装クラスです。
基本的にコードの質は良くないです…。インデントがタブ、スペース混じりだったり、コメントが英語だったり、日本語だったり、無かったり…。API コメントはほぼ無い…。
とりあえずインデントが崩れてるのは、スペース 4つをタブ 1つに置換するか、逆にタブ 1つをスペース 4つに置換すれば揃います…。
あと、1 箇所問題が有って、仮対応として修正しました。
TLSSession クラスのオーバーライドメソッド getLocalPrincipal() が未実装なんですが、このメソッドが呼ばれた場合に、未実装であることを表す UnsupportedOperationException をスローしているので、例外でコケてしまいます。
これを null を返す様にすると、一応は動作する様になります。ただし、これは仮対応なので、本来は正しく実装しないと問題が起きる場合が有ると思います。調査が追いついていませんが、Google が化けるのはこれが原因かなぁ…。
TLSSession # getLocalPrincipal() メソッド。
@Override
public Principal getLocalPrincipal()
{
// throw new UnsupportedOperationException();
return null;
}
Let's Encrypt のサーバー証明書を導入します。
サーバー証明書へのリンクや、サーバー証明書についての説明は
Let's Encrypt Chain of Trust に有るんですが、Java のキーストアへインポート出来る der ファイルへのリンクが無いっていうね…。
私は有志作成のスクリプト
import-letsencrypt-java.sh を参考にし、Let's Encrypt のサーバー証明書 4つのみインポートするスクリプトを書きました。
私の場合は Java 開発環境をインストールしない派(置いておくだけ)、環境変数 JAVA_HOME を使わない派(パスはスクリプト等で自己管理)なので、実際には、ディレクトリ等を直に指定したり、ダウンロードしたファイルを残しておいたり、ログファイルを出力する等、大幅に書き直しました。が、そんなスクリプトは参考にならないと思うので、オリジナルの一部をコメントアウトしたコードを参考までに載せておきます。
オリジナル からの変更点は、letsencryptauthorityx1.der および、letsencryptauthorityx2.der をダウンロード、インポート、削除しない様にコメントアウトしただけです。
Let's Encrypt のサーバー証明書をインポートする参考コード。
#!/bin/bash -e
# JAVA_HOME can be passed as argument if not set
if [ ! -d $JAVA_HOME ]; then
JAVA_HOME=${1}
fi
KEYSTORE=$JAVA_HOME/jre/lib/security/cacerts
if [ ! -f "$KEYSTORE" ]; then
echo "Keystore not found in '$KEYSTORE'"
exit 1
fi
cp $KEYSTORE $KEYSTORE.`date +"%Y%m%d%H%m%S"`
#wget https://letsencrypt.org/certs/letsencryptauthorityx1.der
#wget https://letsencrypt.org/certs/letsencryptauthorityx2.der
wget https://letsencrypt.org/certs/lets-encrypt-x1-cross-signed.der
wget https://letsencrypt.org/certs/lets-encrypt-x2-cross-signed.der
wget https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.der
wget https://letsencrypt.org/certs/lets-encrypt-x4-cross-signed.der
# to be idempotent
#keytool -delete -alias isrgrootx1 -keystore $KEYSTORE -storepass changeit 2> /dev/null || true
#keytool -delete -alias isrgrootx2 -keystore $KEYSTORE -storepass changeit 2> /dev/null || true
keytool -delete -alias letsencryptauthorityx1 -keystore $KEYSTORE -storepass changeit 2> /dev/null || true
keytool -delete -alias letsencryptauthorityx2 -keystore $KEYSTORE -storepass changeit 2> /dev/null || true
keytool -delete -alias letsencryptauthorityx3 -keystore $KEYSTORE -storepass changeit 2> /dev/null || true
keytool -delete -alias letsencryptauthorityx4 -keystore $KEYSTORE -storepass changeit 2> /dev/null || true
#keytool -trustcacerts -keystore $KEYSTORE -storepass changeit -noprompt -importcert -alias isrgrootx1 -file letsencryptauthorityx1.der
#keytool -trustcacerts -keystore $KEYSTORE -storepass changeit -noprompt -importcert -alias isrgrootx2 -file letsencryptauthorityx2.der
keytool -trustcacerts -keystore $KEYSTORE -storepass changeit -noprompt -importcert -alias letsencryptauthorityx1 -file lets-encrypt-x1-cross-signed.der
keytool -trustcacerts -keystore $KEYSTORE -storepass changeit -noprompt -importcert -alias letsencryptauthorityx2 -file lets-encrypt-x2-cross-signed.der
keytool -trustcacerts -keystore $KEYSTORE -storepass changeit -noprompt -importcert -alias letsencryptauthorityx3 -file lets-encrypt-x3-cross-signed.der
keytool -trustcacerts -keystore $KEYSTORE -storepass changeit -noprompt -importcert -alias letsencryptauthorityx4 -file lets-encrypt-x4-cross-signed.der
#rm -f letsencryptauthorityx1.der letsencryptauthorityx2.der lets-encrypt-x1-cross-signed.der lets-encrypt-x2-cross-signed.der lets-encrypt-x3-cross-signed.der lets-encrypt-x4-cross-signed.der
rm -f lets-encrypt-x1-cross-signed.der lets-encrypt-x2-cross-signed.der lets-encrypt-x3-cross-signed.der lets-encrypt-x4-cross-signed.der
キーストア内の一覧を表示する参考コード。
keytool -list -keystore $KEYSTORE -storepass changeit
キーストア内の詳細を表示する参考コード。
keytool -v -list -keystore $KEYSTORE -storepass changeit
Let's Encrypt の中間認証局のサーバー証明書を導入します。
IdenTrust DST Root および、
ISRG Root です。
これについてネットで検索すると、特に英語のサイトでは色々なやり方が紹介されているんですが、ことごとく機能しませんでした…。得に IdenTrust DST Root のサーバー証明書には手こずって手こずって、諦めてしまいました…。
で、Java 8 のキーストアからサーバー証明書をぶっこ抜いて、java6 のキーストアへ突っ込む、という強引な方法にたどり着いた訳です…。邪道です…。
これも参考コードを載せておきます。が、あくまで自己責任で…。
あと、パスを直接指定しているので、全てのパスを変更しないと動きません。
Java 8 のキーストアから、Java 6 のキーストアへサーバー証明書をコピーする参考コード。
#! /bin/sh
# Java 8 のキーツールのパス
J8KEYTOOL=/hogehoge/jdk1.8.0/jre/bin/keytool
# Java 8 のキーストアのパス
J8KEYSTORE=/hogehoge/jdk1.8.0/jre/lib/security/cacerts
# Java 6 のキーツールのパス
J6KEYTOOL=/hogehoge/jdk1.6.0/jre/bin/keytool
# Java 6 のキーストアのパス
J6KEYSTORE=/hogehoge/jdk1.6.0/jre/lib/security/cacerts
# 作業ディレクトリ
WORKDIR=/hogehoge
cd ${WORKDIR}
# Java 8 から証明書をエクスポート
${J8KEYTOOL} -exportcert -alias letsencryptisrgx1\ [jdk] -keystore ${J8KEYSTORE} -storepass changeit -file ${WORKDIR}/jdk8_letsencryptisrgx1.cer
${J8KEYTOOL} -exportcert -alias identrustdstx3\ [jdk] -keystore ${J8KEYSTORE} -storepass changeit -file ${WORKDIR}/jsk8_identrustdstx3.cer
# Java 6 のキーストアから既存を削除
${J6KEYTOOL} -delete -alias letsencryptisrgx1 -keystore ${J6KEYSTORE} -storepass changeit
${J6KEYTOOL} -delete -alias identrustdstx3 -keystore ${J6KEYSTORE} -storepass changeit
# Java 6 のキーストアへ追加
${J6KEYTOOL} -trustcacerts -keystore ${J6KEYSTORE} -storepass changeit -noprompt -importcert -alias letsencryptisrgx1 -file ${WORKDIR}/jdk8_letsencryptisrgx1.cer
${J6KEYTOOL} -trustcacerts -keystore ${J6KEYSTORE} -storepass changeit -noprompt -importcert -alias identrustdstx3 -file ${WORKDIR}/jsk8_identrustdstx3.cer
# 証明書ファイルの削除
rm -f ${WORKDIR}/jdk8_letsencryptisrgx1.cer ${WORKDIR}/jsk8_identrustdstx3.cer
おまけ。
Apache HTTPClient で HttpClientBuilder を使用して、素の HttpClient のインスタンスを生成する場合は簡単なんですが、ConnectionManager を使用する場合は工夫が必要なので、参考コードを載せておきます。
素の HttpClient の参考コード。
SSLConnectionSocketFactory sockFctr = new SSLConnectionSocketFactory
(
new TLSSocketFactory()
, new String[]{"TLSv1.2"}
, null
, new DefaultHostnameVerifier()
);
HttpClient client = HttpClientBuilder
.create()
.setSSLSocketFactory(sockFctr)
.build();
ConnectionManager を指定する HttpClient の
ダメな参考コード。
PoolingHttpClientConnectionManager connMngr = new PoolingHttpClientConnectionManager();
SSLConnectionSocketFactory sockFctr = new SSLConnectionSocketFactory
(
new TLSSocketFactory()
, new String[]{"TLSv1.2"}
, null
, new DefaultHostnameVerifier()
);
HttpClient client = HttpClientBuilder
.create()
.setConnectionManager(connMngr)
.setSSLSocketFactory(sockFctr)
.build();
上記コードの場合、setConnectionManager メソッドで指定した ConnectionManager の SocketFactory でオーバーライドされてしまい、setSSLSocketFactory メソッドで指定した SocketFactory は使用されません。
ConnectionManager を指定する HttpClient の参考コード。
Registry sockFctrReg = RegistryBuilder
.create()
.register
(
"https"
, new SSLConnectionSocketFactory
(
new TLSSocketFactory()
, new String[]{"TLSv1.2"}
, null
, new DefaultHostnameVerifier()
)
)
.register
(
"http"
, PlainConnectionSocketFactory.getSocketFactory()
)
.build();
PoolingHttpClientConnectionManager connMngr = new PoolingHttpClientConnectionManager
(
sockFctrReg
);
HttpClient client = HttpClientBuilder
.create()
.setConnectionManager(connMngr)
.build();
org.apache.http.config.Registry へ HTTPS 用の SocketFactory と、HTTP 用の SocketFactory を登録し、その Registry を ConnectionManager のコンストラクタへ渡すという、かなり回りくどい方法です。他の方法も有るかとは思いますが、まあ参考まで。