SSLSocket: 任意PATHのストアファイルの使用

デフォルトキーストアのPATHは ${JAVA_HOME}/jre/lib/security/cacerts であるが、これは当然同じPATHのVM上で動く他のプログラムと共有することになる。プライベートVMを用意できる環境ならば問題はないが、他の会社の作ったモジュールも同一VMで動かさなければならない場合など、好き勝手にcacertsファイルを触れない事がある。プログラム側で自分が見るストアファイルはここだと明示的に指定するにはどうするか。

システムプロパティによるPATH指定

起動時に -Djavax.net.ssl.trustStore=/export/home/hoge/oreoreKeyStore みたいに指定してやるか、プログラム内部で System#setProperty で設定。

キー名 内容 デフォルト
javax.net.ssl.trustStore 信頼済み証明書を格納したストアファイルのPATH ${JAVA_HOME}/jre/lib/security/cacerts
javax.net.ssl.trustStoreType ストアのタイプ jks
javax.net.ssl.trustStorePassword ストアのパスワード (none)

ref: JSSE リファレンスガイド - カスタマイズ

独自TrustManagerを使用してのPATH指定

コンテキストで切り替えられないシステムプロパティ クソUZEEEEEという時はこっちの手を使う。なんつてw。 他社担当部分と同一プロセス内に仲良く同居しなければならない場合(例: WEBアプリ)、システムプロパティは大域的性質を持つが故に、迂闊に値を変更するとどんな影響があるか分かったものではない。自社担当範囲外に影響を与えない(そして外部からの影響も受けない)手段が必要となる。引き篭もり志向開発。

import java.io.*;
import javax.net.ssl.*;
import java.security.*;
import java.security.cert.*;

public class HttpsClientSpecifiedTrustStore
{
  /**
   * 参照先のトラストストアがデフォルトと異なるSSLContextを生成する
   * 
   * @param trustedKeyStoreFile キーストアファイルのPATH
   * @param keyStoreType キーストアのタイプ。nullならばデフォルトのJKSが使用される
   * @param trustedKeyStorePass キーストアのパスワード
   * @return SSLContext
   */
  public static SSLContext getSSLContext(String trustedKeyStoreFile, String keyStoreType, String trustedKeyStorePass)
  throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException
  , UnrecoverableKeyException, KeyManagementException
  {
    // KeyStoreを読み込む
    KeyStore keyStore = KeyStore.getInstance(keyStoreType != null ? keyStoreType : KeyStore.getDefaultType());
    FileInputStream in = new FileInputStream(trustedKeyStoreFile);
    try
    {
      keyStore.load( new FileInputStream(trustedKeyStoreFile), trustedKeyStorePass.toCharArray() );
    }
    finally
    {
      in.close();
    }

    // 使用するキーストアを変更したTrustManagerを作成
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init( keyStore );
    TrustManager[] tm = tmf.getTrustManagers();

    // 自前TrustManagerを使うSSLContextの生成
    SSLContext context = SSLContext.getInstance("SSL");
    context.init(null, tm, null);
    return context;
  }
  
  public static void main(String[] args) throws Exception
  {
    SSLContext context = getSSLContext("C:/.keystore",null,"oreore");

    SSLSocket socket = (SSLSocket)context.getSocketFactory().createSocket("www.verisign.co.jp",443);
    try
    {
      OutputStream out = socket.getOutputStream();
      try
      {
        out.write( "GET / HTTP/1.0\r\n\r\n".getBytes() );
        out.flush();
        
        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        try
        {
          String line = null;
          while( (line=br.readLine()) != null )
            System.out.println( line );
        }
        finally
        {
          br.close();
        }
      }
      finally
      {
        out.close();
      }
    }
    finally
    {
      socket.close();
    }
  }
}

実験。まず適当な証明書だけがimportされてるストアを作成。

C:\>keytool -import -storepass oreore -keystore c:/.keystore -alias oreore -file
 c:/oreore.cer
所有者: CN=www.oreore.com, OU=OreOre CA Services 1, O=OreOre Japan K.K., L=Shina
gawa, ST=Tokyo, C=JP
実行者: CN=www.oreore.com, OU=OreOre CA Services 1, O=OreOre Japan K.K., L=Shina
gawa, ST=Tokyo, C=JP
シリアル番号: 44d4968f
有効日: Sat Aug 05 22:01:03 JST 2006 有効期限: Fri Nov 03 22:01:03 JST 2006
証明書のフィンガープリント:
         MD5:  D1:87:83:C1:50:B5:DA:DB:C4:90:C1:23:DC:94:2D:2E
         SHA1: A6:81:D0:6A:5F:71:59:F1:18:E0:27:BA:05:2B:5D:68:C5:1E:E3:BB
この証明書を信頼しますか? [no]:  yes
証明書がキーストアに追加されました。

C:\>keytool -list -storepass oreore -keystore c:/.keystore

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

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

oreore, 2006/08/19, trustedCertEntry,
証明書のフィンガープリント (MD5): D1:87:83:C1:50:B5:DA:DB:C4:90:C1:23:DC:94:2D:2
E
C:\>

テストプログラムはwww.verisign.co.jp:443に向けて接続する。プログラム実行結果

Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at com.sun.net.ssl.internal.ssl.Alerts.getSSLException(Alerts.java:150)
	at com.sun.net.ssl.internal.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1518)
	at com.sun.net.ssl.internal.ssl.Handshaker.fatalSE(Handshaker.java:174)
	at com.sun.net.ssl.internal.ssl.Handshaker.fatalSE(Handshaker.java:168)
	at com.sun.net.ssl.internal.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:848)
	at com.sun.net.ssl.internal.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:106)
	at com.sun.net.ssl.internal.ssl.Handshaker.processLoop(Handshaker.java:495)
	at com.sun.net.ssl.internal.ssl.Handshaker.process_record(Handshaker.java:433)
	at com.sun.net.ssl.internal.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:818)
	at com.sun.net.ssl.internal.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1030)
	at com.sun.net.ssl.internal.ssl.SSLSocketImpl.writeRecord(SSLSocketImpl.java:622)
	at com.sun.net.ssl.internal.ssl.AppOutputStream.write(AppOutputStream.java:59)
	at java.io.OutputStream.write(OutputStream.java:58)
	at HttpsClientSpecifiedTrustStore.main(HttpsClientSpecifiedTrustStore.java:53)
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:221)
	at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:145)
	at sun.security.validator.Validator.validate(Validator.java:203)
	at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:172)
	at com.sun.net.ssl.internal.ssl.JsseX509TrustManager.checkServerTrusted(SSLContextImpl.java:320)
	at com.sun.net.ssl.internal.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:841)
	... 9 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:236)
	at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:194)
	at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:216)
	... 14 more

想定どおりあぼんぬ。あれなんか出てる例外が…まあいいか。
次に、デフォルトストアから エイリアス名 verisignclass3ca の証明書をexportしてきて、さっき作った勝手ストアに import。

C:\>keytool -export -storepass changeit -keystore %JAVA_HOME%\jre\lib\security\c
acerts -alias verisignclass3ca -file c:/verisignclass3ca.cer
証明書がファイル <c:/verisignclass3ca.cer> に保存されました。

C:\>keytool -import -storepass oreore -keystore c:/.keystore -alias verisignclas
s3ca -file c:/verisignclass3ca.cer
所有者: OU=Class 3 Public Primary Certification Authority, O="VeriSign, Inc.", C
=US
実行者: OU=Class 3 Public Primary Certification Authority, O="VeriSign, Inc.", C
=US
シリアル番号: 70bae41d10d92934b638ca7b03ccbabf
有効日: Mon Jan 29 09:00:00 JST 1996 有効期限: Wed Aug 02 08:59:59 JST 2028
証明書のフィンガープリント:
         MD5:  10:FC:63:5D:F6:26:3E:0D:F3:25:BE:5F:79:CD:67:67
         SHA1: 74:2C:31:92:E6:07:E4:24:EB:45:49:54:2B:E1:BB:C5:3E:61:74:E2
この証明書を信頼しますか? [no]:  yes
証明書がキーストアに追加されました。

C:\>keytool -list -storepass oreore -keystore c:/.keystore

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

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

oreore, 2006/08/19, trustedCertEntry,
証明書のフィンガープリント (MD5): D1:87:83:C1:50:B5:DA:DB:C4:90:C1:23:DC:94:2D:2
E
verisignclass3ca, 2006/08/19, trustedCertEntry,
証明書のフィンガープリント (MD5): 10:FC:63:5D:F6:26:3E:0D:F3:25:BE:5F:79:CD:67:6
7

C:\>

プログラム実行結果

HTTP/1.1 200 OK
Server: Sun-ONE-Web-Server/6.1
Date: Sat, 19 Aug 2006 10:31:46 GMT
Content-type: text/html
Connection: close

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html lang="ja">
<head>
 <meta http-equiv="Content-Type" content="text/html; charset=shift_jis">
<title>日本ベリサイン 【セキュリティ・電子証明書・電子署名】</title>

...以下略