はじめに
Apache HttpComponentsにより、SSL通信を行う際には当然ですが、ホスト名の検証が行われます。したがって、例えばhttps://google.com/
の代わりにhttps://216.58.220.227/
に接続した場合、SSLExceptionが発生します。ただし、環境の移行時等にはIPアドレスでアクセスする必要が生じたりするため、SSL証明書を無視することなく接続する方法を調べてみました。
簡単に
ここで、証明書のホスト名を無視するだけであれば、SSLSocketFactory
クラスのsetHostnameVerifier
メソッドにALLOW_ALL_HOSTNAME_VERIFIER
を渡せば、例外は生じなくなります。次にHttpClient
の生成例を示します。
HttpParams params = new BasicHttpParams();
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();
// ホスト名の検証を行わない。
sslSocketFactory.setHostnameVerifier(ALLOW_ALL_HOSTNAME_VERIFIER);
registry.register(new Scheme("https", sslSocketFactory, 443));
ThreadSafeClientConnManager clientConnManager = new ThreadSafeClientConnManager(params, registry);
HttpClient httpClient = new DefaultHttpClient(clientConnManager , params);
安全に
前述の方法によると、ホスト名の検証を行わないので、証明書がルート証明書から正しくつながっていれば、その通信を許可してしまいます。ですから、宛先ホスト以外の任意のドメインに属するSSL証明書を持った人間でも、通信を傍受することが可能になります。
そこで、ホスト名の変換テーブルを検証器に持たせ、IPアドレスで接続したいホストを変換テーブルに登録しておくことにしました。これにより、意図したSSL証明書でのみ、ホスト名の問題を無視して通信を行うことができます。次にこの実装を行った検証器、CustomHostnameVerifier
クラスを示します。
package net.leak4mk0.util;
import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.Map;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
public class CustomHostnameVerifier implements X509HostnameVerifier {
private BrowserCompatHostnameVerifier mHostnameVerifier;
private Map<String, String> mHostMap;
public CustomHostnameVerifier(Map<String, String> hostMap) {
mHostnameVerifier = new BrowserCompatHostnameVerifier();
mHostMap = hostMap;
}
@Override
public boolean verify(String host, SSLSession session) {
return mHostnameVerifier.verify(getCustomHost(host), session);
}
@Override
public void verify(String host, SSLSocket ssl) throws IOException {
mHostnameVerifier.verify(getCustomHost(host), ssl);
}
@Override
public void verify(String host, X509Certificate cert) throws SSLException {
mHostnameVerifier.verify(getCustomHost(host), cert);
}
@Override
public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException {
mHostnameVerifier.verify(getCustomHost(host), cns, subjectAlts);
}
private String getCustomHost(String originalHost) {
if (!mHostMap.containsKey(originalHost)) {
return originalHost;
}
return mHostMap.get(originalHost);
}
}
X509HostnameVerifier
を実装したBrowserCompatHostnameVerifier
クラスを内部で利用しています。継承するつもりでしたが、verify
メソッドがfinal
でしたので、このような形になっています。コードを見ればわかることですが、getCustomHost
メソッドで変換テーブルにあれば置換後のホストを返し、検証を続行しています。
前述のsetHostnameVerifier
メソッドで変換テーブルを持ったCustomHostnameVerifier
のインスタンスを渡せば、検証器を変えることができます。
Map<String, String> HOST_MAP =
new HashMap<String, String>() {{
put("216.58.220.227", "google.com");
}};
HttpParams params = new BasicHttpParams();
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();
// ホスト名の変換テーブルを持った検証器を利用する。
sslSocketFactory.setHostnameVerifier(new CustomHostnameVerifier(HOST_MAP));
registry.register(new Scheme("https", sslSocketFactory, 443));
ThreadSafeClientConnManager clientConnManager = new ThreadSafeClientConnManager(params, registry);
HttpClient httpClient = new DefaultHttpClient(clientConnManager , params);
さいごに
私は、この方法で安全性を確保したまま、サーバー移行時の問題を解決することができました。
この記事で示したコードはご自由に使っていただいて大丈夫です。ただし、一切の責任を取りかねますのでご了承ください。