Java HTTPS请求:证书中的主机名不匹配

时间:2019-06-26 13:53:10

标签: java ssl https ssl-certificate apache-httpclient-4.x

我正在尝试从Java 8客户端(使用Apache HttpClient 4.3)向公共服务器(我不拥有)发出HTTPS请求。通常,我从来没有对请求有任何问题,但是目标服务器似乎进行了一些更改,并且请求开始失败。

最初,错误是ValidatorException: PKIX Path Building Failed,但我通过将服务器证书添加到Java信任库中来解决此问题。我现在遇到的错误是hostname in certificate didn't match: <target.server.com> != <*.something.else.com> OR <something.else.com>

something.else.com主机是某种类型的主机站点,无论如何,我很满意没有任何可疑的事情。我通过运行以下命令为target.server.com提取了证书:

openssl s_client -connect target.server.com:443 -showcerts

我没有在证书信息中看到对target.server.com的任何引用,仅针对something.else.com。我在网络浏览器(Chrome)中访问target.server.com没有任何问题,也没有关于SSL证书的投诉。我使用以下命令将服务器证书添加到我的信任存储中:

sudo keytool -import -file /tmp/target.cer -keystore /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/security/cacerts -alias targetserv

是否有某种方法可以解决此问题而无需更改Java代码,例如在证书导入命令等中添加一些内容,如果没有,那么我如何才能使这些请求再次起作用。


这是我的HTTP请求代码(使用HttpClient 4.3):

CloseableHttpClient httpClient = HttpClients.custom().setUserAgent(HTTP_USER_AGENT).build();
HttpGet request = new HttpGet(url);
HttpResponse response = httpClient.execute(request);

...抛出java.io.IOException: hostname in certificate didn't match

此外,这是我要访问的实际目标服务器:https://www.isi.gov.ie


我尝试使用HttpClient v4.5,但仍然收到与4.3相同的错误。我尝试使用Java内置的HTTP请求类发出请求,请求成功(返回302):

URL obj = new URL("https://www.isi.gov.ie");
HttpURLConnection con = (HttpURLConnection) obj.openConnection();

con.setRequestMethod("GET");
con.setRequestProperty("User-Agent", USER_AGENT);

int responseCode = con.getResponseCode();
System.out.println("\nSending 'GET' request to URL : " + url);
System.out.println("Response Code : " + responseCode);

BufferedReader in = new BufferedReader(
        new InputStreamReader(con.getInputStream()));

String inputLine;
StringBuffer response = new StringBuffer();

while ((inputLine = in.readLine()) != null) {
    response.append(inputLine);
}
in.close();

//print result
System.out.println(response.toString());

我可以用HttpClient解决这些问题吗?


事实证明,在我应用程序的其他地方,我一直在使用Java系统属性jsse.enableSNIExtension来禁用SNI,而我却忘记了它。我这样做的原因是,这是我可以找到的唯一解决方法,它将阻止使用HttpClient的所有HTTP请求失败并显示以下错误:

  

sun.security.validator.ValidatorException:PKIX路径构建失败:sun.security.provider.certpath.SunCertPathBuilderException:无法找到到请求目标的有效认证路径

如何在不出现这些错误的情况下重新启用SNI?
我正在使用以下Java版本:

openjdk version "1.8.0_212"
OpenJDK Runtime Environment (build 1.8.0_212-8u212-b03-0ubuntu1.18.04.1-b03)
OpenJDK 64-Bit Server VM (build 25.212-b03, mixed mode)

2 个答案:

答案 0 :(得分:0)

TLDR:可能是SNI (和连锁店)

该服务器显然像今天的许多服务器一样,使用TLS扩展名'Server Name Indication'(又名SNI)来支持具有不同证书的多个“虚拟”主机。如果使用www.isi.gov.ie的SNI与之联系,则将提供对该域和isi.gov.ie有效的证书,但是如果不使用SNI的接触,则将为该域{{ 1}}和*.rdccremote.ie;我认为这些就是您所说的“托管站点”,尽管我看不到任何迹象。我最喜欢它的Google是this parliamentary question,它似乎权威地说它在政府部门的“共享服务”上-尽管它解析为相同的地址(146.0.55.149 )使用我容易访问的DNS。

此外,这两个证书都是由DigiCert颁发的,但是在这两种情况下,服务器都不会发送TLS规范要求的验证所需的链/中间证书。浏览器通常可以“填写”缺失的链证书,但是Java JSSE(和大多数其他软件工具)不能;这可能导致您的路径建立错误,但不会影响主机名不匹配。

rdccremote.ie 在低于1.1.0的版本中默认情况下不会发送SNI,并且已编辑的命令未指示您为其指定了选项。 (并且openssl s_client在服务器上比没有发送链证书更没用)。还应注意,-showcerts仅以解码形式显示证书的“主题”字段(和“发行者”分别为s_clients:),而不显示SubjectAlt [ernative] Name或SAN扩展,自本世纪初以来,“ SSL”用于SSL / TLS服务器证书的主机名。要查看SAN,请从i:s_client馈送PEM块,或者使用openssl x509 -text -noout之类的其他解码工具或Windows上的证书查看器。

Java 8 up通常会发送SNI,尽管这可能受调用代码(应用程序或中间件)的影响。 AHC 4.3已经很老了,我不确定,但是4.5在Java 7和8(Oracle,但AFAIK JSSE在Oracle和OpenJDK之间完全相同)上对我适用。您(或任何人)是否可以将您的JVM配置为使用sysprop keytool -printcert -file $pemfile prevent SNI?您可以使用sysprop jsse.enableSNIExtension=false运行并捕获或记录(大型)输出,还是可以使用wireshark,tcpdump或类似方法捕获并检查线数据,以验证Java在ClientHello中实际发送的内容?

答案 1 :(得分:0)

我通过以下方式解决了同样的问题。 设置sslContext时,使用TrustStrategy(这是loadTrustMaterial的第二个参数)的loadTrustMaterial-传递始终返回True的trusStrategy,这样您就可以忽略客户端的证书验证。

            KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
        FileInputStream keyStoreFile = new FileInputStream(CERT_PATH);
        ks.load(keyStoreFile, CERT_PASSWORD.toCharArray());

        SSLContext sslContext = SSLContexts
                .custom()
                .loadKeyMaterial(ks, CERT_PASSWORD.toCharArray())
                .loadTrustMaterial(null, (chain, authType) -> true)
                .build();

        SSLConnectionSocketFactory sslConnectionFactory = new SSLConnectionSocketFactory(sslContext);

        Registry<ConnectionSocketFactory> registry = RegistryBuilder
                .<ConnectionSocketFactory>create()
                .register(REST.HTTPS.LABEL, sslConnectionFactory)
                .register(REST.HTTP.LABEL, new PlainConnectionSocketFactory())
                .build();

        BasicHttpClientConnectionManager connManager = new BasicHttpClientConnectionManager(registry);

        httpClient = HttpClients
                .custom()
                .setConnectionManager(connManager)
                .setSSLSocketFactory(sslConnectionFactory)
                .build();