web-dev-qa-db-ja.com

Java8を使用したSNIクライアント側の謎

異なる証明書とSNIを持つ複数のTLS仮想ホストを実行するApacheWebサーバーがあります。

Curlを使用してさまざまな仮想ホストに問題なくアクセスできます(おそらくSNIで機能します)。また、基本的にURLでopenConnection()を実行する小さなコマンドラインJavaプログラムを使用して、これらに正常にアクセスできます。

私のTomcatアプリケーションでは、基本的な同じクライアント側コードがクライアントと同じApacheサーバーにアクセスしますが、URLで指定された仮想ホストの証明書ではなく、常にデフォルトの証明書(defaulthost.defaultdomain)で終わります。アクセスを試みます。 (これにより、SunCertPathBuilderExceptionが生成されます。基本的に、証明書への証明書パスを検証できません。これは、非公式の証明書であるため、もちろん当てはまります。ただし、デフォルトの証明書は使用しないでください。)

これは、アプリケーション/ TomcatでSNIがクライアント側で非アクティブ化されているかのようです。アプリとコマンドラインで動作が異なる理由がわかりません。同じJDK、同じホストなど。

プロパティを見つけましたjsse.enableSNIExtensionですが、どちらの場合もtrueに設定されていることを確認しました。質問:

  1. ワイルドなアイデアであっても、なぜこれら2つのプログラムの動作が異なるのでしょうか。

  2. これをどのようにデバッグするかについてのアイデアはありますか?

これは、86_64、JDK 8u77、Tomcat8.0.32上のArchLinuxです。

11
Johannes Ernst

この答えは遅れていますが、問題が発生したばかりです(信じられないほど、非常に大きなバグのようです)。

それが言ったことはすべて真実のようですが、それはデフォルトのHostnameVerifierが原因ではなく、トラブルシューティングです。 HttpsClientがafterConnectを実行するとき、最初にsetHostを確立しようとします(ソケットがSSLSocketImplの場合のみ)。

SSLSocketFactory factory = sslSocketFactory;
try {
    if (!(serverSocket instanceof SSLSocket)) {
        s = (SSLSocket)factory.createSocket(serverSocket,
                                            Host, port, true);
    } else {
        s = (SSLSocket)serverSocket;
        if (s instanceof SSLSocketImpl) {
            ((SSLSocketImpl)s).setHost(Host);
        }
    }
} catch (IOException ex) {
    // If we fail to connect through the tunnel, try it
    // locally, as a last resort.  If this doesn't work,
    // throw the original exception.
    try {
        s = (SSLSocket)factory.createSocket(Host, port);
    } catch (IOException ignored) {
        throw ex;
    }
}

CreateSocket()(パラメーターのないメソッド)をオーバーライドせずにカスタムSSLSocketFactoryを使用する場合、適切にパラメーター化されたcreateSocketが使用され、すべてが期待どおりに機能します(クライアントsni拡張機能を使用)。しかし、それが使用される2番目の方法(setHost en SSLSocketImplを試してください)の場合、実行されるコードは次のとおりです。

// ONLY used by HttpsClient to setup the URI specified hostname
//
// Please NOTE that this method MUST be called before calling to
// SSLSocket.setSSLParameters(). Otherwise, the {@code Host} parameter
// may override SNIHostName in the customized server name indication.
synchronized public void setHost(String Host) {
    this.Host = Host;
    this.serverNames =
        Utilities.addToSNIServerNameList(this.serverNames, this.Host);
}

コメントはすべてを言います。クライアントハンドシェイクの前にsetSSLParametersを呼び出す必要があります。デフォルトのHostnameVerifierを使用する場合、HttpsClientはsetSSLParametersを呼び出します。ただし、逆の方法でsetSSLParametersを実行することはできません。 Oracleにとって、修正は非常に簡単なはずです。

SSLParameters paramaters = s.getSSLParameters();
if (isDefaultHostnameVerifier) {
    // If the HNV is the default from HttpsURLConnection, we
    // will do the spoof checks in SSLSocket.
    paramaters.setEndpointIdentificationAlgorithm("HTTPS");

    needToCheckSpoofing = false;
}
s.setSSLParameters(paramaters);

Java9はSNIで期待どおりに機能しています。しかし、彼ら(Oracle)はこれを修正したくないようです:

8
Dawuid

JDKのデバッグを数時間行った後、残念な結果が出ました。これは機能します:

URLConnection c = new URL("https://example.com/").openConnection();
InputStream i = c.getInputStream();
...

これは失敗します:

URLConnection c = new URL("https://example.com/").openConnection();
((HttpsURLConnection)c).setHostnameVerifier( new HostnameVerifier() {
        public boolean verify( String s, SSLSession sess ) {
            return false; // or true, won't matter for this
        }
});
InputStream i = c.getInputStream(); // Exception thrown here
...

setHostnameVerifier呼び出しを追加すると、SNIが無効になりますが、カスタムHostnameVerifier呼び出されませんです。

犯人はSun.net.www.protocol.https.HttpsClientのこのコードのようです:

            if (hv != null) {
                String canonicalName = hv.getClass().getCanonicalName();
                if (canonicalName != null &&
                canonicalName.equalsIgnoreCase(defaultHVCanonicalName)) {
                    isDefaultHostnameVerifier = true;
                }
            } else {
                // Unlikely to happen! As the behavior is the same as the
                // default hostname verifier, so we prefer to let the
                // SSLSocket do the spoof checks.
                isDefaultHostnameVerifier = true;
            }
            if (isDefaultHostnameVerifier) {
                // If the HNV is the default from HttpsURLConnection, we
                // will do the spoof checks in SSLSocket.
                SSLParameters paramaters = s.getSSLParameters();
                paramaters.setEndpointIdentificationAlgorithm("HTTPS");
                s.setSSLParameters(paramaters);

                needToCheckSpoofing = false;
            }

ここで、構成されたHostnameVerifierのクラスがデフォルトのJDKクラスであるかどうかをチェックし(呼び出されると、上記のコードのようにfalseを返すだけです)、それに基づいてSSL接続のパラメーターを変更します。 、副作用として、SNIをオフにします。

クラスの名前をチェックし、それに依存するロジックを作成することは、私を逃れる良い考えです。 (「お母さん!仮想メソッドは必要ありません。クラス名を確認してディスパッチするだけです!」)しかし、さらに悪いことに、SNIはそもそもHostnameVerifierと何の関係があるのでしょうか。

おそらく回避策は、同じ名前で大文字の異なるカスタムHostnameVerifierを使用することです。これは、同じ明るい心が大文字と小文字を区別しない名前の比較も行うことを決定したためです。

'言っ途切れる。

6
Johannes Ernst
0