排查https请求出现received fatal alert: internal_error的问题

目录

背景

https网络协议交互

net.debug查看信息

Apache Httpclient与 Netty的http请求

 server_name

错误原因

修复方案

方案一

方案二


背景

通知某个商户时,突然出现大量的https握手失败了,出现received fatal alert: internal_error错误。商户sre那边当时将多个域名绑定在一个主机上,开了SNI(Server Name Indication)。

https网络协议交互

https的握手阶段图,如下:

 对于每一步的解释如下:
步骤 1: 客户端通过发送 Client Hello 报文开始 SSL 通信。报文中包含客户端支持的 SSL 的指定版本、加密组件(Cipher Suite)列表(所使用的加密算法及密钥长度等)。

步骤 2: 服务器可进行 SSL 通信时,会以 Server Hello 报文作为应答。和客户端一样,在报文中包含 SSL 版本以及加密组件。服务器的加密组件内容是从接收到的客户端加密组件内筛选出来的。

步骤 3: 之后服务器发送 Certificate 报文。报文中包含公开密钥证书。

步骤 4: 最后服务器发送 Server Hello Done 报文通知客户端,最初阶段的SSL握手协商部分结束。

步骤 5: SSL 第一次握手结束之后,客户端以 Client Key Exchange 报文作为回应。报文中包含通信加密中使用的一种被称为 Pre-master secret 的随机密码串。该报文已用步骤 3 中的公开密钥进行加密。

步骤 6: 接着客户端继续发送 Change Cipher Spec 报文。该报文会提示服务器,在此报文之后的通信会采用 Pre-master secret 密钥加密。

步骤 7: 客户端发送 Finished 报文。该报文包含连接至今全部报文的整体校验值。这次握手协商是否能够成功,要以服务器是否能够正确解密该报文作为判定标准。

步骤 8: 服务器同样发送 Change Cipher Spec 报文。

步骤 9: 服务器同样发送 Finished 报文。

步骤 10: 服务器和客户端的 Finished 报文交换完毕之后,SSL 连接就算建立完成。当然,通信会受到 SSL 的保护。从此处开始进行应用层协议的通信,即发送 HTTP请求。

步骤 11: 应用层协议通信,即发送 HTTP 响应。

步骤 12: 最后由客户端断开连接。断开连接时,发送 close_notify 报文。上图做了一些省略,这步之后再发送 TCP FIN 报文来关闭与 TCP 的通信。在以上流程中,应用层发送数据时会附加一种叫做 MAC(Message Authentication Code)的报文摘要。MAC 能够查知报文是否遭到篡改,从而保护报文的完整性。

net.debug查看信息

了解了如上https的交互内容,那我们可以强行前进了。到底出现在了哪一部分?

在程序启动脚本中我们增加配置项:-djavax.net.debug=all

javax.net.debug=[ssl|all]

If ssl, turns on SSL debugging. If all, turns on SSL debugging with verbose messages.

开启了网络SSL的debugging,我们看到可客户端和服务端交互的更多细节,如下:

javax.net.ssl|FINE|3D|New I/O client worker #4-1|2022-01-23 15:53:10.703 CST|ClientHello.java:567|Produced ClientHello handshake message (
"ClientHello": {
  "client version"      : "TLSv1.2",
  "random"              : "F9 D9 EC 33 BD B7 E3 5F B2 0E F8 7B 5E AE 10 F0 FC 6F 89 5A B6 C6 F4 5C 17 EC A4 A6 85 60 CB 75",
  "session id"          : "",
  "cipher suites"       : "[TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384(0xC02C), TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256(0xC02B), TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384(0xC030), TLS_RSA_WITH_AES_256_GCM_SHA384(0x009D), TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384(0xC02E), TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384(0xC032), TLS_DHE_RSA_WITH_AES_256_GCM_SHA384(0x009F), TLS_DHE_DSS_WITH_AES_256_GCM_SHA384(0x00A3), TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256(0xC02F), TLS_RSA_WITH_AES_128_GCM_SHA256(0x009C), TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256(0xC02D), TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256(0xC031), TLS_DHE_RSA_WITH_AES_128_GCM_SHA256(0x009E), TLS_DHE_DSS_WITH_AES_128_GCM_SHA256(0x00A2), TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384(0xC024), TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384(0xC028), TLS_RSA_WITH_AES_256_CBC_SHA256(0x003D), TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384(0xC026), TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384(0xC02A), TLS_DHE_RSA_WITH_AES_256_CBC_SHA256(0x006B), TLS_DHE_DSS_WITH_AES_256_CBC_SHA256(0x006A), TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA(0xC00A), TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA(0xC014), TLS_RSA_WITH_AES_256_CBC_SHA(0x0035), TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA(0xC005), TLS_ECDH_RSA_WITH_AES_256_CBC_SHA(0xC00F), TLS_DHE_RSA_WITH_AES_256_CBC_SHA(0x0039), TLS_DHE_DSS_WITH_AES_256_CBC_SHA(0x0038), TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256(0xC023), TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256(0xC027), TLS_RSA_WITH_AES_128_CBC_SHA256(0x003C), TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256(0xC025), TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256(0xC029), TLS_DHE_RSA_WITH_AES_128_CBC_SHA256(0x0067), TLS_DHE_DSS_WITH_AES_128_CBC_SHA256(0x0040), TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA(0xC009), TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA(0xC013), TLS_RSA_WITH_AES_128_CBC_SHA(0x002F), TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA(0xC004), TLS_ECDH_RSA_WITH_AES_128_CBC_SHA(0xC00E), TLS_DHE_RSA_WITH_AES_128_CBC_SHA(0x0033), TLS_DHE_DSS_WITH_AES_128_CBC_SHA(0x0032), TLS_EMPTY_RENEGOTIATION_INFO_SCSV(0x00FF)]",
  "compression methods" : "00",
  "extensions"          : [
    "supported_groups (10)": {
      "versions": [secp256r1, secp384r1, secp521r1, ffdhe2048, ffdhe3072, ffdhe4096, ffdhe6144, ffdhe8192]
    },
    "ec_point_formats (11)": {
      "formats": [uncompressed]
    },
    "signature_algorithms (13)": {
      "signature schemes": [ecdsa_secp256r1_sha256, ecdsa_secp384r1_sha384, ecdsa_secp521r1_sha512, rsa_pss_rsae_sha256, rsa_pss_rsae_sha384, rsa_pss_rsae_sha512, rsa_pss_pss_sha256, rsa_pss_pss_sha384, rsa_pss_pss_sha512, rsa_pkcs1_sha256, rsa_pkcs1_sha384, rsa_pkcs1_sha512, dsa_sha256, ecdsa_sha1, rsa_pkcs1_sha1, dsa_sha1]
    },
    "signature_algorithms_cert (50)": {
      "signature schemes": [ecdsa_secp256r1_sha256, ecdsa_secp384r1_sha384, ecdsa_secp521r1_sha512, rsa_pss_rsae_sha256, rsa_pss_rsae_sha384, rsa_pss_rsae_sha512, rsa_pss_pss_sha256, rsa_pss_pss_sha384, rsa_pss_pss_sha512, rsa_pkcs1_sha256, rsa_pkcs1_sha384, rsa_pkcs1_sha512, dsa_sha256, ecdsa_sha1, rsa_pkcs1_sha1, dsa_sha1]
    },
    "extended_master_secret (23)": {
      <empty>
    },
    "supported_versions (43)": {
      "versions": [TLSv1.2, TLSv1.1, TLSv1]
    }
  ]
}
)
javax.net.ssl|FINE|3D|New I/O client worker #4-1|2022-01-23 15:53:10.704 CST|SSLEngineOutputRecord.java:505|WRITE: TLS12 handshake, length = 250
javax.net.ssl|FINE|3D|New I/O client worker #4-1|2022-01-23 15:53:10.779 CST|SSLEngineOutputRecord.java:523|Raw write (
  0000: 16 03 03 00 FA 01 00 00   F6 03 03 F9 D9 EC 33 BD  ..............3.
  0010: B7 E3 5F B2 0E F8 7B 5E   AE 10 F0 FC 6F 89 5A B6  .._....^....o.Z.
  0020: C6 F4 5C 17 EC A4 A6 85   60 CB 75 00 00 56 C0 2C  ..\.....`.u..V.,
  0030: C0 2B C0 30 00 9D C0 2E   C0 32 00 9F 00 A3 C0 2F  .+.0.....2...../
  0040: 00 9C C0 2D C0 31 00 9E   00 A2 C0 24 C0 28 00 3D  ...-.1.....$.(.=
  0050: C0 26 C0 2A 00 6B 00 6A   C0 0A C0 14 00 35 C0 05  .&.*.k.j.....5..
  0060: C0 0F 00 39 00 38 C0 23   C0 27 00 3C C0 25 C0 29  ...9.8.#.'.<.%.)
  0070: 00 67 00 40 C0 09 C0 13   00 2F C0 04 C0 0E 00 33  .g.@...../.....3
  0080: 00 32 00 FF 01 00 00 77   00 0A 00 12 00 10 00 17  .2.....w........
  0090: 00 18 00 19 01 00 01 01   01 02 01 03 01 04 00 0B  ................
  00A0: 00 02 01 00 00 0D 00 22   00 20 04 03 05 03 06 03  .......". ......
  00B0: 08 04 08 05 08 06 08 09   08 0A 08 0B 04 01 05 01  ................
  00C0: 06 01 04 02 02 03 02 01   02 02 00 32 00 22 00 20  ...........2.". 
  00D0: 04 03 05 03 06 03 08 04   08 05 08 06 08 09 08 0A  ................
  00E0: 08 0B 04 01 05 01 06 01   04 02 02 03 02 01 02 02  ................
  00F0: 00 17 00 00 00 2B 00 07   06 03 03 03 02 03 01     .....+.........
)
javax.net.ssl|FINE|3D|New I/O client worker #4-1|2022-01-23 15:53:10.791 CST|SSLEngineInputRecord.java:177|Raw read (
  0000: 15 03 03 00 02 02 50                               ......P
)
javax.net.ssl|FINE|3D|New I/O client worker #4-1|2022-01-23 15:53:10.791 CST|SSLEngineInputRecord.java:214|READ: TLSv1.2 alert, length = 2
javax.net.ssl|FINE|3D|New I/O client worker #4-1|2022-01-23 15:53:10.792 CST|Alert.java:238|Received alert message (
"Alert": {
  "level"      : "fatal",
  "description": "internal_error"
}
)

Apache Httpclient与 Netty的http请求

Apache Httpclient 请求,建立SSL连接时握手正常

​
CloseableHttpClient httpClient = HttpClients.custom().build();

String url = "https://test.com";

HttpGet httpGet = new HttpGet(url);

HttpResponse response = httpClient.execute(httpGet);

​

Netty的http异步请求,建立SSL连接时握手失败

private final AsyncHttpClient asyncHttpClient = new AsyncHttpClient(new NettyAsyncHttpProvider(new AsyncHttpClientConfig.Builder().build()));

AsyncHttpClient.BoundRequestBuilder boundRequestBuilder = asyncHttpClient.preparePost(url);

ListenableFuture<Response> future = boundRequestBuilder.execute();

Response response = future.get(5, TimeUnit.SECONDS);

两者的错误日志相比较,Apache httpClient的请求中,扩展字段多了个server_name。

 server_name

根据文档TLS扩展字段文档对server_name的描述,如下图片:

 如果一台服务器托管多个域,那么很明显server_name是对于每个域的所有者来说,这是必要的,以确保满足他们的安全需要。
因为客户端可以显示不同的server_name。在应用程序协议中,应用程序服务器实现了这一点,必须检查这些名称是相同的,以确保客户端在应用程序协议中没有显示不同的名称。
 

错误原因

商户那边将多个域名绑定在一个主机上,开了SNI(Server Name Indication)。米币请求建立ssl连接时,没有传送上server name,而商户服务端检查了server_name扩展,则nginx不知道使用哪个server,直接抛出异常。
在Client Hello阶段,通过SNI扩展,将域名信息提前告诉服务器,服务器根据域名取得对应的证书返回给客户端已完成校验过程。

导致没有SNI的原因

  1. jdk版本过低,1.8的低版本和1.7或者1.6,均有可能,参考https://www.codetd.com/article/9814188
  2. httpclient,4.3.12这个版本之前的,会有这个bug
  3. 系统类设置了System.setProperty("jsse.enableSNIExtension", "false");

修复方案

方案一

继续使用 下面依赖的相关异步请求方法

<dependency>

<groupId>com.ning</groupId>

<artifactId>async-http-client</artifactId>

<version>1.7.14</version>

</dependency>

让请求携带上server_name, 创建特定的asyncHttpClient供该商户使用,其他商户依旧保持用之前的aysncHttpClient来调用。

SSLContext sslcontext = SSLContexts.createSystemDefault();

SSLEngine sslEngine = sslcontext.createSSLEngine("test.com", 443);
// 配置sslEngine在握手时使用客户端模式。
sslEngine.setUseClientMode(true);

AsyncHttpClientConfig.Builder builder = new AsyncHttpClientConfig.Builder().setSSLEngineFactory(new SSLEngineFactory() {
    @Override
    public SSLEngine newSSLEngine() throws GeneralSecurityException {
        return sslEngine;
    }
});

AsyncHttpClient asyncHttpClient = new AsyncHttpClient(new NettyAsyncHttpProvider(builder.build()));

方案二

用apache的HttpAsyncClient来处理

maven依赖

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.4.1</version>
</dependency>
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpasyncclient</artifactId>
    <version>4.1.4</version>
</dependency>

相关调用代码如下:

CloseableHttpAsyncClient client = HttpAsyncClients.custom()
        .setSSLHostnameVerifier(new NoopHostnameVerifier())
        .setDefaultRequestConfig(REQUEST_CONFIG)
        .build();
        
        
        
HttpPost httpPost = new HttpPost(url);
try {
    client.start();
    Future<HttpResponse> future = client.execute(httpPost, 
        new FutureCallback<HttpResponse> {
            @Override
            public void completed(HttpResponse result) {
            }
        
            @Override
            public void failed(Exception e) {
            }
        
            @Override
            public void cancelled() {
            }
    });
    HttpResponse response = future.get(5, TimeUnit.SECONDS);
} catch (Exception e) {
    logger.error("httpAysncPost exception, url: {}, message: {}", url, e.getMessage(), e);
}

注意:需要防止HttpAsyncClient资源没关闭引起的内存泄漏(吃过这方面的亏,哭唧唧)。
诊断由 Apache HttpAsyncClient 引起的内存泄漏 - Think different - 生活的美好

批量执行完一部分请求后,需要进行client.close(); 不然线程会飙升到几万,然后服务会挂掉。

 

方案三:

System.setProperty("jsse.enableSNIExtension", "true");

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值