注:除了該博客提供的兩種方式之外,其實還有其餘的解決方案,原理都是忽略或者全部認證都設置爲受信,在處理這個問題走了很多彎路。html
EXCEPTION 1 :Host name does not match the certificate subject provided by the peerjava
//關閉主機名的驗證apache
HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;api
EXCEPTION 2:The server failed to respond with a valid HTTP responseide
防火牆的緣由,媽蛋公司的防火牆已經不止一次挑戰個人極限了,今天要是早想到這點如今就早閃人了,擦!無論怎麼說仍是感謝原博主,這裏僅做記錄。oop
Javaui
開発中のテスト環境とかで、よくあるネタ?的な。
Apache HttpComponents/Clientを使って、SSL本身署名証明書を使って通訊したり、ホスト名の検証を無効化する方法について、メモとして書いておきます。
Apache HttpComponents - Apache HttpComponents
いつも微妙にやり方を忘れて、毎度毎度調べることになっているので、備忘録的にと。
もちろん、ご利用はテストなどでの範囲で、ですね。
ちなみに、java.net.URLConnectionを使う場合については、之前書きました。
JavaでSSL証明書の検証無効化、ホスト名検証の無効化…とデバッグ - CLOVERspa
Apache HttpComponentsって、バージョンでけっこうコロコロAPIが変わるので、「これ!」というのは言いづらいのですが、今回はApache HttpComponents(というかClient)の4.5.1を対象にします。.net
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.1</version> </dependency>
テストコードには、JUnit+AssertJを利用。component
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.2.0</version> <scope>test</scope> </dependency>
また、テスト用のSSLを有効にしたWebサーバーは、Ubuntu LinuxでインストールできるSSLを有効にしたApacheとしました。
以降のサンプルコードでは、如下のimport文があることを前提にしています。
import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLPeerUnverifiedException; import org.apache.http.HttpStatus; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.TrustSelfSignedStrategy; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.ssl.SSLContexts; import org.apache.http.ssl.TrustStrategy; import org.apache.http.util.EntityUtils; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy;
ではでは、書いていってみます。
通訊時に、こんなスタックトレースが出力されるようなケース。
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 sun.security.ssl.Alerts.getSSLException(Alerts.java:192) at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1949) at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:302) at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:296) at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1509) at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:216) at sun.security.ssl.Handshaker.processLoop(Handshaker.java:979) at sun.security.ssl.Handshaker.process_record(Handshaker.java:914) at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1062) at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375) at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403) at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1387)
SSL証明書の検証で、NGとなる場合ですね。本身署名証明書だと、これに遭遇すると思います。
テストコードは、こんな感じ。
@Test public void testSelfSignedFailure() throws IOException { try (CloseableHttpClient client = HttpClients.createDefault()) { HttpGet get = new HttpGet("https://localhost"); assertThatThrownBy(() -> { try (CloseableHttpResponse response = client.execute(get)) { assertThat(response.getStatusLine().getStatusCode()) .isEqualTo(HttpStatus.SC_OK); assertThat(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)) .contains("apache2"); } }) .isInstanceOf(SSLHandshakeException.class) .hasMessageContaining("sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target"); } }
これをパスするには、如下のようなコードを書いてSSLContextを做成します。
TrustStrategy trustStrategy = new TrustSelfSignedStrategy(); SSLContext sslContext = SSLContexts .custom() .loadTrustMaterial(trustStrategy) .build();
TrustStrategyの実裝である、TrustSelfSignedStrategyを使用することで本身署名証明書でもOKになりますよ、と。
で、こちらを使ってHttpClientを做成する、と。
try (CloseableHttpClient client = HttpClients .custom() .setSSLContext(sslContext) .build()) { HttpGet get = new HttpGet("https://localhost");
これで、証明書のエラーは迴避できます。
これだけではまだエラーになる場合が、SSL証明書に書かれているホスト名が、実際にアクセスしているホストと合わない場合。
このケースだと、このようなスタックトレースが得られます。
javax.net.ssl.SSLPeerUnverifiedException: Host name 'localhost' does not match the certificate subject provided by the peer (CN=e611e15f9c9d) at org.apache.http.conn.ssl.SSLConnectionSocketFactory.verifyHostname(SSLConnectionSocketFactory.java:465) at org.apache.http.conn.ssl.SSLConnectionSocketFactory.createLayeredSocket(SSLConnectionSocketFactory.java:395) at org.apache.http.conn.ssl.SSLConnectionSocketFactory.connectSocket(SSLConnectionSocketFactory.java:353) at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:134) at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:353) at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:380) at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:236) at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:184) at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:88) at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110) at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:184) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:107)
先ほどのSSL証明書の検証を行わないだけのコードは、このような感じになっています。
@Test public void testHostNameFailure() throws IOException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { TrustStrategy trustStrategy = new TrustSelfSignedStrategy(); SSLContext sslContext = SSLContexts .custom() .loadTrustMaterial(trustStrategy) .build(); assertThatThrownBy(() -> { try (CloseableHttpClient client = HttpClients .custom() .setSSLContext(sslContext) .build()) { HttpGet get = new HttpGet("https://localhost"); try (CloseableHttpResponse response = client.execute(get)) { assertThat(response.getStatusLine().getStatusCode()) .isEqualTo(HttpStatus.SC_OK); assertThat(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)) .contains("apache2"); } } }) .isInstanceOf(SSLPeerUnverifiedException.class) .hasMessageContaining("Host name 'localhost' does not match the certificate subject provided by the peer (CN=e611e15f9c9d)"); }
が、これだと今回用意した環境では、ホスト名が証明書とアクセスパスで不一致のため、エラーになると。
そこで、ここではHostnameVerifierを使用します。
HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
NoopHostnameVerifierを使用すると、ホスト名の検証を無効化できます。
SSL証明書の検証と、ホスト名の検証を所有合わせたコードは、このような形になります。
@Test public void testSelfSignedSuccess() throws IOException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { TrustStrategy trustStrategy = new TrustSelfSignedStrategy(); SSLContext sslContext = SSLContexts .custom() .loadTrustMaterial(trustStrategy) .build(); HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE; try (CloseableHttpClient client = HttpClients .custom() .setSSLContext(sslContext) .setSSLHostnameVerifier(hostnameVerifier) .build()) { HttpGet get = new HttpGet("https://localhost"); try (CloseableHttpResponse response = client.execute(get)) { assertThat(response.getStatusLine().getStatusCode()) .isEqualTo(HttpStatus.SC_OK); assertThat(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)) .contains("apache2"); } } }
HostnameVerifierは、setSSLHostnameVerifierで指定します。
try (CloseableHttpClient client = HttpClients .custom() .setSSLContext(sslContext) .setSSLHostnameVerifier(hostnameVerifier) .build()) {
とりあえず、目的は達成できましたよっと。
SSLContextとSSLConnectionSocketFactoryの組み合わせでも、同様のことが実現できます。
@Test public void testSelfSignedSuccessAnother() throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException, IOException { TrustStrategy trustStrategy = new TrustSelfSignedStrategy(); SSLContext sslContext = SSLContexts .custom() .loadTrustMaterial(trustStrategy) .build(); HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE; SSLConnectionSocketFactory sslConnSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier); try (CloseableHttpClient client = HttpClients .custom() .setSSLSocketFactory(sslConnSocketFactory) .build()) { HttpGet get = new HttpGet("https://localhost"); try (CloseableHttpResponse response = client.execute(get)) { assertThat(response.getStatusLine().getStatusCode()) .isEqualTo(HttpStatus.SC_OK); assertThat(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)) .contains("apache2"); } } }
この場合は、setSSLSocketFactoryを使用します。
try (CloseableHttpClient client = HttpClients .custom() .setSSLSocketFactory(sslConnSocketFactory) .build()) {