Apache HttpComponents(旧httpClient)でssl通信時のエラー

アプリケーションからhttps通信を使って他のサーバと連携することはよくあると思います。
例えばfacebookgoogleのOAuth認証のOPとやりとりする場合とか。


私は開発時はMac上で行なっているがその時点ではjava7はOpenJDKしかなかったので(現在はMac版のoracle jdk7が出てますね)、ほとんどの開発をOpenJDK上で行なっていたんですが、なぜかHttpComponentsを使ってhttpsの通信を行うと下記エラーが出る。

Exception in thread "main" java.lang.RuntimeException: javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
	at Main.execute(Main.java:62)
	at Main.main(Main.java:19)
Caused by: javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
	at sun.security.ssl.SSLSessionImpl.getPeerCertificates(SSLSessionImpl.java:397)
	at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:128)
	at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:397)
	at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:148)
	at org.apache.http.impl.conn.AbstractPoolEntry.open(AbstractPoolEntry.java:150)
	at org.apache.http.impl.conn.AbstractPooledConnAdapter.open(AbstractPooledConnAdapter.java:121)
	at org.apache.http.impl.client.DefaultRequestDirector.tryConnect(DefaultRequestDirector.java:575)
	at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:425)
	at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:820)
	at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:754)
	at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:732)
	at Main.execute(Main.java:54)
	... 1 more


サンプルコードは以下

public class Main {
	public static void main(String[] args) throws Exception{
		Main m = new Main();
		m.execute(args);
	}
	public void execute(String[] args) throws Exception{
		System.out.println(System.getProperty("java.version"));

		HttpClient httpClient = new DefaultHttpClient();		
		//ちょうどmixi developer Centerにサーバ証明書についてのFAQがあったので使わしてもらう
		//http://developer.mixi.co.jp/openid/faq/
		HttpGet get = new HttpGet("https://mixi.jp/");
	
		HttpResponse res = httpClient.execute(get);
		if(res.getStatusLine().getStatusCode() >= 300) {
			System.out.println(res.getStatusLine().getStatusCode());
		}
		System.out.println(EntityUtils.toString(res.getEntity()));
	}
}

httpComponentsのバージョンは

	<dependency>
		<groupId>org.apache.httpcomponents</groupId>
		<artifactId>httpclient</artifactId>
		<version>4.1.3</version>
	</dependency>

を使っています


正直あまりsslについて知識がなかったので、とりあえずHttpClientでSSL通信(HttpClient3.0-rc3) にしたがってサーバ証明書を登録して事無きを得たのだが、どうにも気持ち悪い(いちいちこんな作業をしないとhttps通信できないのか?という気持ち悪さ)



ちなみにいちいち証明書の認証とかめんどくせぇという人は以下のように書くとオレオレ証明書などでも無視して全部受けて入れるようにできます。

		try {
	        TrustStrategy trustStrategy = new TrustStrategy() {
				@Override
				public boolean isTrusted(X509Certificate[] chain, String authType)
						throws CertificateException {
					return true;
				}
			};
	        X509HostnameVerifier hostnameVerifier = new AllowAllHostnameVerifier();
	        SSLSocketFactory sslSf = new SSLSocketFactory(trustStrategy, hostnameVerifier);
	       
	        SchemeRegistry schemeRegistry = new SchemeRegistry();
	        schemeRegistry.register(new Scheme("https", 8443, sslSf));
	        schemeRegistry.register(new Scheme("https", 443, sslSf));
	       
	        ClientConnectionManager connection = new ThreadSafeClientConnManager(schemeRegistry);
			HttpClient httpClient = new DefaultHttpClient(connection);		
		}catch(UnrecoverableKeyException uke) {
			//TODO
		}catch(KeyStoreException kse) {
			//TODO		
		}catch(KeyManagementException kme) {
			//TODO
		}catch(NoSuchAlgorithmException nsae) {
			//TODO
		}	

しかし今回はライブラリを開発しているので勝手に証明書を無視するようなコードを入れ込むわけにはいきません。


というわけで、改めてsslの仕組みを調べてjavassl通信の仕組みについてお勉強。
どうやらデフォルトでcacertsというファイルにいわゆるCAルート証明書がインストールされていることになっているようだ。
なので、httpComponentsを使ったアプリケーションのjavaランタイムのcacertsにCAルート証明書が入っていないと、サーバ証明書のクライアント認証が通せないのでエラーになるということみたいです。


実際にMac標準にインストールされているJDKのcacertsを確認するとかなりの量のルート証明書がインポートされている

$ keytool -v -list -keystore  /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/lib/security/cacerts  | more
キーストアのパスワードを入力してください:    #←デフォルトパスワードはchangeitなので、それを入力
キーストアのタイプ: JKS
キーストア・プロバイダ: SUN

キーストアには183エントリが含まれます

別名: keychainrootca-99
作成日: 2012/03/23
エントリ・タイプ: trustedCertEntry

所有者: CN=TWCA Root Certification Authority, OU=Root CA, O=TAIWAN-CA, C=TW
発行者: CN=TWCA Root Certification Authority, OU=Root CA, O=TAIWAN-CA, C=TW
シリアル番号: 1
有効期間の開始日: Thu Aug 28 16:24:33 JST 2008終了日: Wed Jan 01 00:59:59 JST 2031
証明書のフィンガプリント:
         MD5:  AA:08:8F:F6:F9:7B:B7:F2:B1:A7:1E:9B:EA:EA:BD:79
         SHA1: CF:9E:87:6D:D3:EB:FC:42:26:97:A3:B5:A3:7A:A0:76:A9:06:23:48
         SHA256: BF:D8:8F:E1:10:1C:41:AE:3E:80:1B:F8:BE:56:35:0E:E9:BA:D1:A6:B9:BD:51:5E:DC:5C:6D:5B:87:11:AC:44
         署名アルゴリズム名: SHA1withRSA
         バージョン: 3

拡張: 

#1: ObjectId: 2.5.29.14 Criticality=false
・
・
・以下略


openjdkのほうは空っぽ

$ keytool -v -list -keystore  /Library/Java/JavaVirtualMachines/1.7.0u4.jdk/Contents/Home/jre/lib/security/cacerts 
キーストアのパスワードを入力してください:  

キーストアのタイプ: JKS
キーストア・プロバイダ: SUN

キーストアには0エントリが含まれます

これが原因でサーバ証明書の認証ができないためエラーになったみたい。
試しに同じコードでMacの標準jdkにビルドパスを変更したら問題なく通信できました。

openjdkを使う場合はCAルート証明書を自前でインポートするとかしないといけないのだろうか。
そのへんまでは調べきれてないけど、とりあえず原因がわかったのですっきりした。
必要になったらまた追記します。