開発中のテスト環境とかで、よくあるネタ?的な。
Apache HttpComponents/Clientを使って、SSL自己署名証明書を使って通信したり、ホスト名の検証を無効化する方法について、メモとして書いておきます。
Apache HttpComponents – Apache HttpComponents
いつも微妙にやり方を忘れて、毎度毎度調べることになっているので、備忘録的にと。
もちろん、ご利用はテストなどでの範囲で、ですね。
ちなみに、java.net.URLConnectionを使う場合については、以前書きました。
JavaでSSL証明書の検証無効化、ホスト名検証の無効化…とデバッグ - CLOVER
準備
Apache HttpComponentsって、バージョンでけっこうコロコロAPIが変わるので、「これ!」というのは言いづらいのですが、今回はApache HttpComponents(というかClient)の4.5.1を対象にします。
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.1</version> </dependency>
テストコードには、JUnit+AssertJを利用。
<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文があることを前提にしています。
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;
ではでは、書いていってみます。
事象その1、自己署名証明書なのでエラーになる
通信時に、こんなスタックトレースが出力されるようなケース。
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"); } }
自己署名証明書でもOKにするには
これをパスするには、以下のようなコードを書いて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");
これで、証明書のエラーは回避できます。
事象その2、ホスト名の検証でエラーになる
これだけではまだエラーになる場合が、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()) {
以上でしたー。
参考)
JSSE_Fortify_SCA_Rules/ApacheHTTPClientFluentExample.java at master · GDSSecurity/JSSE_Fortify_SCA_Rules · GitHub
How do you create SSL socket factory in new Apache Http Client 4.3? - Stack Overflow