特定の接続で異なる証明書を使用するにはどうすればよいですか?


164

大規模なJavaアプリケーションに追加するモジュールは、他の会社のSSLで保護されたWebサイトと対話する必要があります。問題は、サイトが自己署名証明書を使用していることです。中間者攻撃を受けていないことを確認するための証明書のコピーがあり、サーバーへの接続が成功するようにこの証明書をコードに組み込む必要があります。

基本的なコードは次のとおりです。

void sendRequest(String dataPacket) {
  String urlStr = "https://host.example.com/";
  URL url = new URL(urlStr);
  HttpURLConnection conn = (HttpURLConnection)url.openConnection();
  conn.setMethod("POST");
  conn.setRequestProperty("Content-Length", data.length());
  conn.setDoOutput(true);
  OutputStreamWriter o = new OutputStreamWriter(conn.getOutputStream());
  o.write(data);
  o.flush();
}

自己署名証明書の追加処理がない場合、これはconn.getOutputStream()で終了しますが、次の例外があります。

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
....
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
....
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

理想的には、私のコードは、アプリケーション内のこの1つの場所のために、この1つの自己署名証明書を受け入れるようにJavaに教える必要があります。

証明書をJREの認証局ストアにインポートでき、Javaがそれを受け入れることができることを知っています。それが私が手助けできるなら私がとりたいアプローチではありません。お客様のすべてのマシンで、使用してはならない1つのモジュールを実行することは非常に侵襲的です。同じJREを使用する他のすべてのJavaアプリケーションに影響します。このサイトにアクセスする他のJavaアプリケーションの確率がゼロであっても、私はそれが好きではありません。これも簡単な操作ではありません。UNIXでは、この方法でJREを変更するためにアクセス権を取得する必要があります。

また、カスタムチェックを行うTrustManagerインスタンスを作成できることも確認しました。この1つの証明書を除くすべてのインスタンスで、実際のTrustManagerに委任するTrustManagerを作成することもできるようです。しかし、TrustManagerはグローバルにインストールされるようで、アプリケーションからの他のすべての接続に影響を与えると思いますが、それも私にはまったく適切な匂いではありません。

自己署名証明書を受け入れるようにJavaアプリケーションを設定するための推奨、標準、または最良の方法は何ですか?上記で考えているすべての目標を達成できますか、それとも妥協する必要がありますか?ファイルとディレクトリ、構成設定、およびほとんどないコードを含むオプションはありますか?


小さな、ワーキング修正:rgagnon.com/javadetails/...

20
@Hasenpriester:このページを提案しないでください。すべての信頼検証が無効になります。必要な自己署名証明書を受け入れるだけでなく、MITMの攻撃者が提示する証明書も受け入れます。
Bruno

回答:


168

SSLSocket自分でファクトリを作成し、HttpsURLConnection接続する前に設定します。

...
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslFactory);
conn.setMethod("POST");
...

あなたはそれを作成SSLSocketFactoryしてそれを保持したいと思うでしょう。これを初期化する方法のスケッチは次のとおりです。

/* Load the keyStore that includes self-signed cert as a "trusted" entry. */
KeyStore keyStore = ... 
TrustManagerFactory tmf = 
  TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);
sslFactory = ctx.getSocketFactory();

キーストアの作成についてサポートが必要な場合は、コメントしてください。


キーストアをロードする例を以下に示します。

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(trustStore, trustStorePassword);
trustStore.close();

PEM形式の証明書を使用してキーストアを作成するには、を使用して独自のコードを作成するCertificateFactorykeytool、JDKからインポートするだけです(keytool 「キーエントリ」では機能しませんが、「信頼できるエントリ」では問題ありません。 )。

keytool -import -file selfsigned.pem -alias server -keystore server.jks

3
どうもありがとうございました!他の人たちが私を正しい方向に向けるのを助けましたが、結局あなたのやり方は私が取ったアプローチです。私はJKS JavaキーストアファイルにPEM証明書ファイルを変換するの厳しい仕事を持っていた、と私はここではそのための支援を発見した:stackoverflow.com/questions/722931/...
skiphoppy

1
それがうまくいったことをうれしく思います。キーストアで苦労してごめんなさい。私はそれを私の答えに含めるべきだった。CertificateFactoryを使用してもそれほど難しくありません。実際、私は後で来る人のために更新を行うと思います。
エリクソン2009年

1
このコードが行うことはすべて、JSSEリファレンスガイドで説明されている3つのシステムプロパティを設定することで実現できることを複製したものです。
ローン侯爵

2
@EJP:これは少し前のことなので、はっきりとは覚えていませんが、「大きなJavaアプリケーション」では、他のHTTP接続が確立されている可能性が高いという理由があったと思います。グローバルプロパティを設定すると、動作中の接続に干渉したり、この一方のパーティがサーバーを偽装したりする可能性があります。これは、ケースバイケースで組み込みのトラストマネージャーを使用する例です。
エリクソン

2
OPが言ったように、「私のコードは、アプリケーションのこの1つの場所のために、この1つの自己署名証明書を受け入れるようにJavaに教える必要があります。
エリクソン

18

この問題を解決するためにオンラインでたくさんの場所を読みました。これは私がそれを動作させるために書いたコードです:

ByteArrayInputStream derInputStream = new ByteArrayInputStream(app.certificateString.getBytes());
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate) certificateFactory.generateCertificate(derInputStream);
String alias = "alias";//cert.getSubjectX500Principal().getName();

KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null);
trustStore.setCertificateEntry(alias, cert);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(trustStore, null);
KeyManager[] keyManagers = kmf.getKeyManagers();

TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(trustStore);
TrustManager[] trustManagers = tmf.getTrustManagers();

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, null);
URL url = new URL(someURL);
conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(sslContext.getSocketFactory());

app.certificateStringは、証明書を含む文字列です。次に例を示します。

static public String certificateString=
        "-----BEGIN CERTIFICATE-----\n" +
        "MIIGQTCCBSmgAwIBAgIHBcg1dAivUzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE" +
        "BhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsTIlNlY3VyZSBE" +
        ... a bunch of characters...
        "5126sfeEJMRV4Fl2E5W1gDHoOd6V==\n" +
        "-----END CERTIFICATE-----";

上記の正確な構造を維持している限り、自己署名されている場合は、証明書文字列に任意の文字を含めることができることをテストしました。ラップトップのターミナルコマンドラインで証明書文字列を取得しました。


1
@Joshを共有していただきありがとうございます。使用中のコードを示す小さなGithubプロジェクトを作成しました:github.com/aasaru/ConnectToTrustedServerExample
Master Drools

14

を作成できSSLSocketFactoryない場合は、JVMにキーをインポートするだけです

  1. 公開鍵を取得します。 $openssl s_client -connect dev-server:443次に、次のようなファイルdev-server.pemを作成します。

    -----BEGIN CERTIFICATE----- 
    lklkkkllklklklklllkllklkl
    lklkkkllklklklklllkllklkl
    lklkkkllklk....
    -----END CERTIFICATE-----
  2. キーをインポートします#keytool -import -alias dev-server -keystore $JAVA_HOME/jre/lib/security/cacerts -file dev-server.pem。パスワード:changeit

  3. JVMを再起動します

ソース:javax.net.ssl.SSLHandshakeExceptionを解決する方法?


1
これは元の質問を解決するとは思わないが、私の問題は解決したので、ありがとう!
エドノリス2013年

これを行うのも良い考えではありません。これで、すべての Javaプロセスがデフォルトで信頼する、このランダムなシステム全体の自己署名証明書が手に入りました。
user268396

12

JREのトラストストアをコピーして、そのトラストストアにカスタム証明書を追加し、システムプロパティでカスタムトラストストアを使用するようにアプリケーションに指示します。このように、デフォルトのJREトラストストアはそのままにしておきます。

欠点は、JREを更新するときに、新しいトラストストアがカスタムトラストストアに自動的にマージされないことです。

おそらく、トラストストア/ jdkを検証して不一致をチェックするか、トラストストアを自動的に更新するインストーラーまたは起動ルーチンを用意することで、このシナリオを処理できます。アプリケーションの実行中にトラストストアを更新するとどうなるかわかりません。

このソリューションは、100%エレガントでもフールプルーフでもありませんが、シンプルで機能し、コードを必要としません。


12

commons-httpclientを使用して、自己署名証明書で内部のhttpsサーバーにアクセスするとき、私はこのようなことをしなければなりませんでした。はい、私たちのソリューションは、単にすべてを渡す(デバッグメッセージをログに記録する)カスタムTrustManagerを作成することでした。

これは、ローカルのTrustManagerのみが関連付けられるように設定されたローカルのSSLContextからSSLソケットを作成する独自のSSLSocketFactoryを持つことになります。キーストア/証明書ストアの近くに行く必要はまったくありません。

これは、LocalSSLSocketFactoryにあります。

static {
    try {
        SSL_CONTEXT = SSLContext.getInstance("SSL");
        SSL_CONTEXT.init(null, new TrustManager[] { new LocalSSLTrustManager() }, null);
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException("Unable to initialise SSL context", e);
    } catch (KeyManagementException e) {
        throw new RuntimeException("Unable to initialise SSL context", e);
    }
}

public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
    LOG.trace("createSocket(host => {}, port => {})", new Object[] { host, new Integer(port) });

    return SSL_CONTEXT.getSocketFactory().createSocket(host, port);
}

SecureProtocolSocketFactoryを実装する他のメソッドとともに。LocalSSLTrustManagerは、前述のダミーの信頼マネージャーの実装です。


8
すべての信頼検証を無効にした場合、そもそもSSL / TLSを使用しても意味がありません。ローカルでのテストは問題ありませんが、外部で接続する場合は問題です。
ブルーノ

Java 7で実行すると、この例外が発生します。javax.net.ssl.SSLHandshakeException:共通の暗号スイートがありません。
Uri Lukach 2013
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.