简析Chrome ERR_SSL_PROTOCOL_ERROR 协议错误

Posted by NoPanic on Sat, Oct 21, 2023

背景

同事要求我改个官网的文案,发现我的 Chrome 浏览器打不开网页。

发现问题

用Chrome(内核版本:版本 118.0.5993.70(正式版本) (arm64)) 打开网址,发现出现了一个错误:ERR_SSL_PROTOCOL_ERROR,如下图:

错误文案
一开始我还以为是官网的问题,所以就看了下官网的地址是否可以正常解析到,结果发现可以解析到(47.244.205.xxx),如下:

 1$ dig www.xxxx.com +trace
 2
 3; <<>> DiG 9.10.6 <<>> www.xxxx.com +trace
 4;; global options: +cmd
 5.			280465	IN	NS	b.root-servers.net.
 6.			280465	IN	NS	h.root-servers.net.
 7.			280465	IN	NS	j.root-servers.net.
 8.			280465	IN	NS	d.root-servers.net.
 9.			280465	IN	NS	e.root-servers.net.
10.			280465	IN	NS	m.root-servers.net.
11.			280465	IN	NS	l.root-servers.net.
12.			280465	IN	NS	f.root-servers.net.
13.			280465	IN	NS	i.root-servers.net.
14.			280465	IN	NS	k.root-servers.net.
15.			280465	IN	NS	g.root-servers.net.
16.			280465	IN	NS	a.root-servers.net.
17.			280465	IN	NS	c.root-servers.net.
18;; Received 228 bytes from 202.106.46.151#53(202.106.46.151) in 45 ms
19
20www.xxxx.com.		111	IN	A	47.244.205.xxx
21xxxx.com.			115525	IN	NS	ns1.alidns.com.
22xxxx.com.			115525	IN	NS	ns2.alidns.com.
23;; Received 650 bytes from 202.12.27.33#53(m.root-servers.net) in 6 ms

分析问题

因为其他同事可以访问到,所以有可能是浏览器的问题?排查一下浏览器问题,尝试其他浏览器:

  • Safari 打开,正常访问
  • Firefox 打开,正常访问
  • Edge 打开,正常访问,看了下内核版本,比我目前的老:版本 118.0.2088.61 (正式版本) (arm64)
  • Chromium 打开,正常访问,版本如下图:
    Chromium 版本

所以基本上判断可能是浏览器的问题了,为什么会报这个错误呢?应该从哪里排查呢?

从应用层面跟本没法排查,只能从网络层面去排查了。

抓个包吧

服务器在 TLS 握手阶段,完成整个握手过程,将数据传输给了客户端,客户端报了一个Alert(Level:Fatal,Descriptions:Illegal Parameter)错误。

抓包截图

这个错误比较奇怪,以前没有遇到过,所以去看一下 TLS1.2 的 RFC5246文档。

Alert Protocol

Alert Protocol的描述如下:
One of the content types supported by the TLS record layer is the alert type. Alert messages convey the severity of the message (warning or fatal) and a description of the alert. Alert messages with a level of fatal result in the immediate termination of the connection. In this case, other connections corresponding to the session may continue, but the session identifier MUST be invalidated, preventing the failed session from being used to establish new connections. Like other messages, alert messages are encrypted and compressed, as specified by the current connection state.

Alert Protocol结构体

 1    enum { warning(1), fatal(2), (255) } AlertLevel;
 2
 3      enum {
 4          close_notify(0),
 5          unexpected_message(10),
 6          bad_record_mac(20),
 7          decryption_failed_RESERVED(21),
 8          record_overflow(22),
 9          decompression_failure(30),
10          handshake_failure(40),
11          no_certificate_RESERVED(41),
12          bad_certificate(42),
13          unsupported_certificate(43),
14          certificate_revoked(44),
15          certificate_expired(45),
16          certificate_unknown(46),
17          illegal_parameteERR_SSL_PROTOCOL_ERRORr(47),
18          unknown_ca(48),
19          access_denied(49),
20          decode_error(50),
21          decrypt_error(51),
22          export_restriction_RESERVED(60),
23          protocol_version(70),
24          insufficient_security(71),
25          internal_error(80),
26          user_canceled(90),
27          no_renegotiation(100),
28          unsupported_extension(110),
29          (255)
30      } AlertDescription;
31
32      struct {
33          AlertLevel level;
34          AlertDescription description;
35      } Alert;

客户端在校验了服务器发过来的证书以后,报了参数错误,很可能是因为浏览器做了处理导致的,因为其他浏览器都没有这样的错误,所以应该是浏览器做了处理

简单回顾TLS握手过程

以下为ECDHE的握手过程(为什么是ECDHE?):

ECDHE

Client Hello

Client Hello中,给了cipher_suites一共16 个支持的密码算法可选项。

 1struct {
 2          ProtocolVersion client_version;
 3          Random random;
 4          SessionID session_id;
 5          CipherSuite cipher_suites<2..2^16-2>;
 6          CompressionMethod compression_methods<1..2^8-1>;
 7          select (extensions_present) {
 8              case false:
 9                  struct {};
10              case true:
11                  Extension extensions<0..2^16-1>;
12          };
13      } ClientHello;

Client-Hello

Server Hello

因为在Server Hello中,确定了使用的加密算法,所以客户端在生成密钥的时候,使用了TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384算法。

Server Hello

TLS False Start

与 RSA握手不同的是,ECDHE 有一个 TLS False Start 的过程,叫做抢跑,参见此 RFC7918,客户端可以不用等到服务器发回 Finished 确认握手完毕,立即就发出 HTTP 报文,省去了一个消息往返的时间浪费。

分析源码

此时我先下载了最新的版本的代码,这个代码有点大,下来了以后,就看了下代码量,总共得有 2000 万+的代码,如下图:

Code

查找错误描述

发现了一个描述文档中有描述ERR_SSL_PROTOCOL_ERROR的,如下:

TLS-SHA1

对应 Github 中的链接

TLS-SHA1-GIT

因为 TLS SHA-1 算法已经不安全了,在 RFC9155中被放弃了,但是在 Chrome 中有一个临时的标签chrome://flags/#use-sha1-server-handshakes,将其改为enabled,即可启用 SHA-1 签名算法,但是此方法是不安全,且不可执行的(不能让用户去改这个吧?)。

AllowSHA-1

Chrome 的描述

Chrome is removing support for signature algorithms using SHA-1 for server signatures during the TLS handshake. This does not affect SHA-1 support in server certificates, which was already removed, or in client certificates, which continues to be supported.

SHA-1 can be temporarily re-enabled via the temporary InsecureHashesInTLSHandshakesEnabled enterprise policy. This policy will be removed in Chrome 123.

也就是说上面的那个标签,在 123 版本就会被移除,在 117 是一个过渡版本,也就是在两周前,这个(#use-sha1-server-handshakes)默认值被改成了:Disabled。

EstimatedMilestones

修改的代码

在 Chromium 代码提交中可以看到,2023 年 9 月 28 日提交了一个修改,如下:

CommitFiles

Commit

抓包检查一下

CheckTLS-SHA-1

Chrome 实现

 1#ssl_config.h
 2  // If true, causes SHA-1 signatures to be rejected from servers during
 3  // a TLS handshake.
 4  bool disable_sha1_server_signatures = false;
 5
 6# ssl_connect_job.cc
 7int SSLConnectJob::DoSSLConnect() {
 8  //...
 9  // We do the fallback in both cases here to ensure we separate the effect of
10  // disabling sha1 from the effect of having a single automatic retry
11  // on a potentially unreliably network connection.
12  ssl_config.disable_sha1_server_signatures =
13      disable_legacy_crypto_with_fallback_ ||
14      !ssl_client_context()->config().InsecureHashesInTLSHandshakesEnabled();
15  //...
16}
17# ssl_client_socket_impl.cc
18int SSLClientSocketImpl::Init() {
19    //...
20    if (ssl_config_.disable_sha1_server_signatures) {
21        static const uint16_t kVerifyPrefs[] = {
22            SSL_SIGN_ECDSA_SECP256R1_SHA256, SSL_SIGN_RSA_PSS_RSAE_SHA256,
23            SSL_SIGN_RSA_PKCS1_SHA256,       SSL_SIGN_ECDSA_SECP384R1_SHA384,
24            SSL_SIGN_RSA_PSS_RSAE_SHA384,    SSL_SIGN_RSA_PKCS1_SHA384,
25            SSL_SIGN_RSA_PSS_RSAE_SHA512,    SSL_SIGN_RSA_PKCS1_SHA512,
26        };
27        if (!SSL_set_verify_algorithm_prefs(ssl_.get(), kVerifyPrefs,
28                                            std::size(kVerifyPrefs))) {
29        return ERR_UNEXPECTED;
30        }
31    }
32    //...
33  }
34# 

chromium论坛

查找相同错误

看到论坛中有人反馈相同的错误ERR_SSL_PROTOCOL_ERROR,所以应该是 SHA-1 的问题没跑了。

Bugs
BugsDetails
大家可以使用此网站复现一下(此网站已修复此问题,大家可以自己搭建网站做这个实验)。

根因分析

  • SHA-1 的问题
  • 服务端配置问题?猜测因为其他的网站都 ok,而且在 ServerHello 返回的加密套件

Nginx

Nginx 的 https 加密套件配置增加如下配置:

1ssl_ciphers  !RSA+SHA1

正常禁用此配置以后,服务端应当禁止使用 rsa_pkcs1_sha1,然而实际情况并没有解决问题。

查看一下 OpenSSL 的版本1.0.1c:

1$nginx -V
2nginx version: nginx/1.9.15
3built by gcc 4.8.5 20150623 (xxxxx) (GCC) 
4built with OpenSSL 1.0.1c  1 Aug xxxx
5TLS SNI support enabled
6configure arguments: ................ --with-openssl=/usr/local/src/openssl-1.0.1c

配置没有生效,有可能是 OpensSSL 的问题?

OpenSSL

问题所在

查看一下 Github 上关于 1.0.1 和 SHA1 的 issue,一共有 17 个左右,一个一个的翻看,如下图:

GitSha1Error

发现有人反馈了这个问题,如下图:

GitSha1ErrorDetails

问题:当 ClientHello 启用 SNI 时,证书被重置了,SHA1 始终用作签名算法的摘要,主要的修复如下:

GitSha1Commit

再看一眼抓包中的ClientHello,如下所示:

SNI

这样看来,大概就是这个版本处理的问题了。

简单分析ServerKeyExchange流程

RFC 5246

OpenSSL 版本用的是比较老的版本了,1.0.1c 的版本,大概是 2012 年的了,参见此链接查看版本

SSL-Version
抓包中查看是在 Server Key Exchange 中对 EC Diffie-Hellman 的参数进行签名时返回的,再次回到 RFC5246 中对 Server Key Exchange的描述如下:
ServerKeyExchange
再回顾一下抓包的那个图:
ServerKeyExchangeAfterCertificate
仔细看下这个ServerKeyExchange的结构,跟抓包中的结构不符,看了一下注释:

1/* message is omitted for rsa, dh_dss, and dh_rsa */
2/* may be extended, e.g., for ECDH -- see [TLSECC] */
 1struct {
 2          select (KeyExchangeAlgorithm) {
 3              case dh_anon:
 4                  ServerDHParams params;
 5              case dhe_dss:
 6              case dhe_rsa:
 7                  ServerDHParams params;
 8                  digitally-signed struct {
 9                      opaque client_random[32];
10                      opaque server_random[32];
11                      ServerDHParams params;
12                  } signed_params;
13              case rsa:
14              case dh_dss:
15              case dh_rsa:
16                  struct {} ;
17                 /* message is omitted for rsa, dh_dss, and dh_rsa */
18              /* may be extended, e.g., for ECDH -- see [TLSECC] */
19          };
20      } ServerKeyExchange;

在 TLSECC 中的描述如下,需要参考 RFC 4492 这个文档:

1[TLSECC]   Blake-Wilson, S., Bolyard, N., Gupta, V., Hawk, C., and B.
2              Moeller, "Elliptic Curve Cryptography (ECC) Cipher Suites
3              for Transport Layer Security (TLS)", RFC 4492, May 2006.

RFC 4492

根据抓包中的字段,找到对应的描述如下:

  • public: 公钥
  • params: 特殊的参数(公钥和域名相关的参数)
  • signed_params: 签名算法 SHA(ClientHello.random + ServerHello.random + ServerKeyExchange.params);
 1public:   The ephemeral ECDH public key.
 2The ServerKeyExchange message is extended as follows.
 3
 4   enum { ec_diffie_hellman } KeyExchangeAlgorithm;
 5
 6   ec_diffie_hellman:   Indicates the ServerKeyExchange message contains an ECDH public key.
 7        select (KeyExchangeAlgorithm) {
 8            case ec_diffie_hellman:
 9                ServerECDHParams    params;
10                Signature           signed_params;
11        } ServerKeyExchange;
12
13params:   Specifies the ECDH public key and associated domain
14      parameters.
15
16signed_params:   A hash of the params, with the signature appropriate
17      to that hash applied.  The private key corresponding to the
18      certified public key in the server's Certificate message is used
19      for signing.
20
21        enum { ecdsa } SignatureAlgorithm;
22
23        select (SignatureAlgorithm) {
24              case ecdsa:
25                  digitally-signed struct {
26                      opaque sha_hash[sha_size];
27                  };
28        } Signature;
29
30
31        ServerKeyExchange.signed_params.sha_hash
32            SHA(ClientHello.random + ServerHello.random +
33                                              ServerKeyExchange.params);

待补充:

  • 计算签名的流程源码
  • 默认值设置 SHA1源码

解决方案

更新 OpenSSL

将 OpenSSL 更新指至1.1.1v。

更新 Nginx

将 Nginx 更新至 1.24.0,顺便将 Http协议升级至 Http2,再次访问发现问题解决了,同时也提升了访问速度。

1$ nginx -V
2nginx version: nginx/1.24.0
3built by gcc 4.8.5 20150623 (xxxx) (GCC) 
4built with OpenSSL 1.1.1v  1 Aug 2023
5TLS SNI support enabled
6configure arguments: 
7--sbin-path=/usr/sbin/nginx --conf-path=/etc/nginx/nginx.conf --pid-path=/run/nginx/nginx.pid --with-http_ssl_module --with-http_v2_module --with-openssl=/usr/local/src/openssl-1.1.1v

需要注意的是,一定要重启 Nginx:systemctl restart nginx,reload 是没有用的。

总结归纳

服务器OpenSSL 版本过旧,同时 Chrome 启用了安全机制,禁用SHA1 算法,两个条件凑在了一起。

  • 原因:OpenSSL 1.0.1c 版本的 bug,SHA1 始终用作签名算法
  • 解决方案:更新 OpenSSL 至 1.1.1v 版本
  • 顺便将Nginx 至 1.24.0 版本,并开启 Http2
  • Chrome 启用安全机制后,会默认禁用 SHA1 算法,服务端更新以后即可解决问题。

反思

  • 服务端的问题,看似正常,实际上客户端更新,也会导致未知的问题产生,需要关注经常变化的节点,关注更新的内容,评判对现有系统是否有影响。
  • 服务端需要定期更新至新版本,同步更新即可避免这些问题,同时也可以解决很多安全问题。