java - 收到握手警告:unrecognized_name
问题描述
有没有办法在 Java 11 中处理“javax.net.ssl.SSLHandshakeException:收到的握手警告:unrecognized_name”,而无需通过使用禁用 SNI 系统范围
System.setProperty("jsse.enableSNIExtension", "false")
使用此系统属性将导致对取决于 SNI 的主机的任何以下请求失败。所以基本上我确实需要一个按请求的解决方案。
非常具体:我正在尝试从站点https://www.minervamedica.it获取内容,这似乎对 Java > 8 有问题。
我确实尝试过这种方法:https ://javabreaks.blogspot.com/2015/12/java-ssl-handshake-with-server-name.html
final TrustManager[] trustAllCerts = new TrustManager[] { new X509ExtendedTrustManager() {
@Override
public void checkClientTrusted(final X509Certificate[] x509Certificates, final String s)
throws CertificateException {
}
@Override
public void checkServerTrusted(final X509Certificate[] x509Certificates, final String s)
throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkClientTrusted(final X509Certificate[] x509Certificates, final String s,
final Socket socket) throws CertificateException {
}
@Override
public void checkServerTrusted(final X509Certificate[] x509Certificates, final String s,
final Socket socket) throws CertificateException {
}
@Override
public void checkClientTrusted(final X509Certificate[] x509Certificates, final String s,
final SSLEngine sslEngine) throws CertificateException {
}
@Override
public void checkServerTrusted(final X509Certificate[] x509Certificates, final String s,
final SSLEngine sslEngine) throws CertificateException {
}
} };
final SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
URL url = new URL("https://www.minervamedica.it");
SSLParameters sslParameters = new SSLParameters();
List<SNIServerName> sniHostNames = new ArrayList<>();
sniHostNames.add(new SNIHostName(url.getHost()));
// sniHostNames.add(new SNIHostName("minervamedica.it"));
sslParameters.setServerNames(sniHostNames);
SSLSocketFactory wrappedSSLSocketFactory = new SSLSocketFactoryWrapper(sslContext.getSocketFactory(), sslParameters);
HttpsURLConnection connection = (HttpsURLConnection) url
.openConnection();
connection.setSSLSocketFactory(wrappedSSLSocketFactory);
connection.setDoOutput(true);
connection.setRequestMethod("GET");
System.out.print(connection.getResponseCode());
SSLSocketFactoryWrapper
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
public class SSLSocketFactoryWrapper extends SSLSocketFactory {
private final SSLSocketFactory wrappedFactory;
private final SSLParameters sslParameters;
public SSLSocketFactoryWrapper(SSLSocketFactory factory, SSLParameters sslParameters) {
this.wrappedFactory = factory;
this.sslParameters = sslParameters;
}
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(host, port);
setParameters(socket);
return socket;
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
throws IOException, UnknownHostException {
SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(host, port, localHost, localPort);
setParameters(socket);
return socket;
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(host, port);
setParameters(socket);
return socket;
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(address, port, localAddress, localPort);
setParameters(socket);
return socket;
}
@Override
public Socket createSocket() throws IOException {
SSLSocket socket = (SSLSocket) wrappedFactory.createSocket();
setParameters(socket);
return socket;
}
@Override
public String[] getDefaultCipherSuites() {
return wrappedFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return wrappedFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(s, host, port, autoClose);
setParameters(socket);
return socket;
}
private void setParameters(SSLSocket socket) {
socket.setSSLParameters(sslParameters);
}
}
编辑 30.04.2019:
也不适用于 Java 11 是这样的:
URL url = new URL("https://www.minervamedica.it");
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setHostnameVerifier((s, sslSession) -> true);
System.out.println(new String(conn.getInputStream().readAllBytes()));
尽管使用了始终返回 true 的自定义 HostnameVerifier,这也会导致“收到握手警告:unrecognized_name”的异常。
编辑 02.05.2019
它显然是一个配置错误的服务器(见下文)。
openssl s_client -servername www.minervamedica.it -connect www.minervamedica.it:443 -state
揭示
CONNECTED(00000003)
SSL_connect:before SSL initialization
SSL_connect:SSLv3/TLS write client hello
SSL3 alert read:warning:unrecognized name
SSL_connect:SSLv3/TLS write client hello
SSL_connect:SSLv3/TLS read server hello
depth=0 CN = minervamedica.it
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 CN = minervamedica.it
verify error:num=21:unable to verify the first certificate
verify return:1
SSL_connect:SSLv3/TLS read server certificate
SSL_connect:SSLv3/TLS read server key exchange
SSL_connect:SSLv3/TLS read server done
SSL_connect:SSLv3/TLS write client key exchange
SSL_connect:SSLv3/TLS write change cipher spec
SSL_connect:SSLv3/TLS write finished
SSL_connect:SSLv3/TLS write finished
SSL_connect:SSLv3/TLS read server session ticket
SSL_connect:SSLv3/TLS read change cipher spec
SSL_connect:SSLv3/TLS read finished
---
Certificate chain
0 s:/CN=minervamedica.it
i:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIFbTCCBFWgAwIBAgISA7svnlD9ZgJAww8LaYbVvgQ2MA0GCSqGSIb3DQEBCwUA
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xOTA1MDIwNjAyNDlaFw0x
OTA3MzEwNjAyNDlaMBsxGTAXBgNVBAMTEG1pbmVydmFtZWRpY2EuaXQwggEiMA0G
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCuJUYVVxEhkHmGlPBT/zKZ6NrGuKls
pYSeeJsa8mMWwCf5b0ZOe8jS8w36EHvTUbHHOqd63zXjsFDkt14SmNunogSCSYvq
8+UmHPudv2q4ygPLY728bU5YpXVXaBh6hcJmfckCs0WnxLbPFC3rJdlC77syDbpi
O/fX5XY7cmzB7gCH3MmKltGzk3oQDYst4IIFZZV11Hk1VVDJ7MAb23E4PINKEJwJ
5IqFJRjko3nVvKEY+FVv0Bl4N7PN8xl9M+Xw4Bcp8sUaGmgbRSPAbPj2S1LWoRq+
dUFyqsmmks0YsdMbuRfkjWUuJ5h0MUtpW0yCbJIFtUgEysJzREgfTX5NAgMBAAGj
ggJ6MIICdjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG
AQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFLjAn5EGedAmqtkEk2OHsrJw
cOW6MB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUFBwEB
BGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNyeXB0
Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNyeXB0
Lm9yZy8wLwYDVR0RBCgwJoISKi5taW5lcnZhbWVkaWNhLml0ghBtaW5lcnZhbWVk
aWNhLml0MEwGA1UdIARFMEMwCAYGZ4EMAQIBMDcGCysGAQQBgt8TAQEBMCgwJgYI
KwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQub3JnMIIBBQYKKwYBBAHW
eQIEAgSB9gSB8wDxAHYA4mlLribo6UAJ6IYbtjuD1D7n/nSI+6SPKJMBnd3x2/4A
AAFqd1pUHgAABAMARzBFAiEAsPuYjgg2alECcOV36YTwGawdYOi2dcPuTUHN7FL2
kJUCIGnckeJZKe1Xb9tJA3YkuxptbOFlHEUBAaDQEiz49CMJAHcAKTxRllTIOWW6
qlD8WAfUt2+/WHopctykwwz05UVH9HgAAAFqd1pUBQAABAMASDBGAiEAx0NQhPfK
FpAPHJ8ZU6BxLGl4gXMND4FxuMVsGb+pfxYCIQDv0lsXnvPmEIQdCMero8IyjrYk
L8K9f1zVbSAFn/6PxzANBgkqhkiG9w0BAQsFAAOCAQEAOf0IE45r4ytrFtFXrVMY
ATpt/UcTgJvqgapg4KsQSr4k007MZtxeALRn6B5KdekGhhKzIlHz6O/JD4+95Btv
mempZgo166Nr4sf4UMfNsENNqUX1jgT2i74Ss6058t6YtTanuNdrokL/mMxSynIt
5O49srcpEwhTvIaeKq84DLd6Es9OcBuRAJZCEw/SGtLypkC0PSSHayuGvJjssDX6
RB+CkftpKJC9c6M+5e1fXjMHDHrUPukS467vs0Ky5jK0ZzFna7NAQtip7XY1TyAi
b6AdnLLGbKt6DIb6eOnTf5aIMatyTCRAVcVKSSqdtAhX0aS33/iXFPbApF3E0GyQ
gA==
-----END CERTIFICATE-----
subject=/CN=minervamedica.it
issuer=/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
---
No client certificate CA names sent
Peer signing digest: SHA512
Server Temp Key: ECDH, P-256, 256 bits
---
SSL handshake has read 2106 bytes and written 331 bytes
Verification error: unable to verify the first certificate
---
New, TLSv1.2, Cipher is ECDHE-RSA-AES256-GCM-SHA384
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
Protocol : TLSv1.2
Cipher : ECDHE-RSA-AES256-GCM-SHA384
Session-ID: 008EB1ED9FEA6E2FE477231DB86F669C3C44792C2A02A80FCBB955186E141C86
Session-ID-ctx:
Master-Key: 7745BDCCE5390A15586866EBA311DDCA90BD75AA7D91D5825A23DEF83B6C88CD56BFFC53ECDBA67271BFD8AB720D8522
PSK identity: None
PSK identity hint: None
SRP username: None
TLS session ticket lifetime hint: 300 (seconds)
TLS session ticket:
0000 - ef 8d 8c e9 3a 3c 79 d9-74 cd 3f f2 f2 d7 55 4a ....:<y.t.?...UJ
0010 - a0 45 9b a9 f0 22 2b 13-19 8c d5 8b be 57 be 6c .E..."+......W.l
0020 - 38 ab f6 92 21 a4 ef 93-20 bf c2 f9 53 ee df 96 8...!... ...S...
0030 - a0 68 fe ab ff 5e e0 85-c7 7f 2f 4d f7 b6 c6 7f .h...^..../M....
0040 - 6b d1 42 ff ab 96 eb 1e-1b ef 98 f4 68 bb ee 45 k.B.........h..E
0050 - 0a f1 0b 4e 88 41 95 fc-b9 a2 9a 93 38 21 bd 6e ...N.A......8!.n
0060 - 84 9d 54 d7 27 d5 c9 94-87 b6 03 29 5d c7 87 07 ..T.'......)]...
0070 - 99 ee c3 27 5a 57 02 19-66 fe 89 43 d5 b6 bb 90 ...'ZW..f..C....
0080 - 4c ce fb 3c da 91 75 75-e7 99 a4 87 7c 92 57 d3 L..<..uu....|.W.
0090 - f3 5b 5d 62 45 82 27 97-d8 8a 0d c3 e1 f3 7b b8 .[]bE.'.......{.
00a0 - fd 28 1f 59 7f 74 a2 29-ae 11 c4 b4 ef c0 65 23 .(.Y.t.)......e#
00b0 - 48 6e c2 a3 fc fa cf 05-56 f0 ce 2c 36 54 02 b9 Hn......V..,6T..
00c0 - a2 12 ef 86 cb 8d bd ae-b0 ff 4c 0c a2 72 36 11 ..........L..r6.
Start Time: 1556795487
Timeout : 7200 (sec)
Verify return code: 21 (unable to verify the first certificate)
Extended master secret: no
---
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p>
</body></html>
SSL3 alert read:warning:close notify
closed
SSL3 alert write:warning:close notify
但是,Java 11 似乎无法像以前的版本那样处理这种情况。使用自定义 HostnameVerifier 似乎被完全忽略了。这是一个Java错误吗?
更新 11.05.2019
已打开错误报告:https ://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8223677
编辑 12.05.2019
正如 dave_thompson_085 正确指出的那样,Java 8 和 Java 11 中的行为是相同的:总是抛出异常。
在 Java 8 之前解决此问题的唯一方法是使用 org.apache.http,但这不适用于 Java 11 等较新的 Java 版本:
package test;
import java.io.IOException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import org.apache.http.HttpEntity;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpConnectionFactory;
import org.apache.http.conn.ManagedHttpClientConnection;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.DefaultHttpResponseParserFactory;
import org.apache.http.impl.conn.ManagedHttpClientConnectionFactory;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.io.DefaultHttpRequestWriterFactory;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.EntityUtils;
import util.NoSNISSLSocketFactory;
public class TestSNI {
public static void main(String[] args) {
HttpEntity entity = null;
try {
final BasicCookieStore cookieStore = new BasicCookieStore();
final HttpClientContext localContext = HttpClientContext.create();
localContext.setCookieStore(cookieStore);
// accept all certificates
final SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
public boolean isTrusted(final X509Certificate[] arg0, final String arg1)
throws CertificateException {
return true;
}
}).build();
// set NoopHostnameVerifier()
final Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder
.<ConnectionSocketFactory> create().register("http", new PlainConnectionSocketFactory())
.register("https", new SSLConnectionSocketFactory(
new NoSNISSLSocketFactory(sslContext.getSocketFactory()), new NoopHostnameVerifier()))
.build();
final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory = new ManagedHttpClientConnectionFactory(
new DefaultHttpRequestWriterFactory(),
new DefaultHttpResponseParserFactory());
final PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(
socketFactoryRegistry, connFactory);
final CloseableHttpClient httpclient = HttpClients.custom().setConnectionManager(cm).build();
final HttpGet httpGet = new HttpGet("https://www.minervamedica.it");
final CloseableHttpResponse rp = httpclient.execute(httpGet, localContext);
entity = rp.getEntity();
if (entity != null) {
System.out.println(EntityUtils.toString(entity));
}
} catch (final ClientProtocolException e) {
System.out.println(e);
} catch (final IOException e) {
System.out.println(e);
} catch (final Exception e) {
System.out.println(e);
} finally {
EntityUtils.consumeQuietly(entity);
}
}
}
所以似乎仍然没有办法处理 Java > 8 上的问题?
编辑 12.05.2019
进一步的调试显示,Java 11 上没有调用 NoopHostnameVerifier,它覆盖了 javax.net.ssl.HostnameVerifier 的方法 verify 以返回 alwas true。异常已经发生在 NoSNISSLSocketFactory 中,它使用空主机扩展 SSLSocketFactory:
@Override
public Socket createSocket(final Socket socket, final String host, final int port, final boolean autoClose)
throws IOException {
return sslSocketFactory.createSocket(socket, "", port, autoClose);
}
因此,在 Java 8 上创建一个空主机的 Socket 似乎确实禁用了 SNI,而在 Java 11 上似乎并非如此?
解决方案
好的,我浏览了源代码并找到了一种方法,尽管我不确定我有多热情地推荐它。
尽管 javadoc forSSLParameters.setServerNames
没有这么说,但如果值集是空的List
(没有元素),那么 ClientHandshaker 实际上根本不发送 SNI。我怀疑这是因为例如 1.2的 RFC将最小大小指定为 1,禁止空列表。(与 TLS 与 SSL 中的证书消息中的 certificate_list 相比;在 SSL 中,最小大小为 1,没有适合服务器请求的证书和密钥的客户端根本不发送消息,而在 TLS 中为 0,并且没有合适的证书和密钥的客户端被明确指定发送包含空列表的消息。)虽然这是合乎逻辑的,因为它既没有记录也没有明确评论,我不会很高兴依赖它。
由于直接确定所需的其他参数非常复杂(且脆弱),我认为最好的方法是从现有参数开始并修改,例如SSLSocket
:
SSLSocket s = SSLSocketFactory.getDefault() /* or other */ .createSocket("host", 443);
SSLParameters p = s.getSSLParameters();
p.setServerNames( new ArrayList<SNIServerName>() );
/* or j9+ p.setServerNames( List<SNIServerName>.of() ); */
s.setSSLParameters(p);
...
对于HttpsURLConnection
,你原来的 SSLSocketFactoryWrapper 方法非常接近,除了上面我会根据创建的实际参数进行修改SSLSocket
,你必须使用空new ArrayList<SNIServerName>()
而不是.add
任何东西。
非常相似的东西应该适用于 Apache HttpClient,但我没有经历过它,因为我发现这很烦人,就像一个曲折的小类迷宫一样。
PS:消息来源也证实了为什么改变 syspropjsse.enableSNIExtension
不起作用;(与许多其他人一样)在首次加载 JSSE 时被读取和缓存,随后不被读取。您可以使用反射闯入类并更改缓存值,但我们不要去那里。
推荐阅读
- javascript - 请解释类型断言
- javascript - 绑定函数移除自身
- javascript - javascript为什么word返回一个数组和word[0]未定义的javascript
- c# - Automapper 在 .net 中映射预定义值
- mysql - 在 Shiny 的 MySQL 查询中从一列中选择值并从另一列传递相应的值
- android - 将 RecyclerView 触摸传递给 MotionLayout 不起作用
- crystal-reports - 水晶报表:打印每个人的第一条非空白记录
- python - 为独特的应用程序管理 Heroku RAM
- node.js - 如何将响应存储到变量nodejs请求模块中
- php - 替代 template_from_string 来处理调用自定义 Twig 函数的字符串