iOS版发版在即,但却在一位同事的设备上出现发送HTTP请求时的严重卡顿,整个UI都被冻结住了,但Android一切正常。
Shawn查了很久,终于发现是Flutter for iOS的TLS握手时要做OCSP,但Let’s Encrypt签发的证书的OCSP地址被墙了导致的。参见:
问题定位到了,怎么解决?网上没有一个讲的很完整的,下面我们抛开Flutter,从服务端着手解决此事:
OCSP
全称 Online Certificate Status Protocol,译为 在线证书状态协议
。简单来说,当我们访问一个带CA证书的网站时,会访问相应的验证URL来验证此证书的状态。
所以当浏览器或其他客户端访问一个HTTPS网站,拿到证书之后,就会去指定URL验证一下,并将结果缓存一段时间。
看起来一切和谐,这个时候有人跳出来说了,每个客户端都请求验证一下效率太低了,我们能不能在服务端去请求好,然后和TLS握手一起下发?于是就有了OCSP Stapling
。
然后又有人跳出来说了,你们这个OCSP并不能增强安全性,会导致HTTPS请求时间变长,还不如我分发一个列表到本地来解决证书问题,这个人叫Google。于是从2012年开始Google旗下产品逐渐的去OCSP化,这也是为什么Flutter在Android上没有问题但iOS有。
最后,如果验证URL被防火墙屏蔽了,就会导致TLS握手过程很慢并且失败。
第一步首先要做的,是确认证书是否可用,如果可用,大概率不是OCSP的问题。
准备证书
# Get server cert
openssl s_client -connect tc.cen2.pw:443 < /dev/null 2>&1 | sed -n '/-----BEGIN/,/-----END/p' > certificate.pem
# Get intermediate cert
openssl s_client -showcerts -connect tc.cen2.pw:443 < /dev/null 2>&1 | sed -n '/-----BEGIN/,/-----END/p' | awk 'BEGIN { n=0 } { if ($0=="-----BEGIN CERTIFICATE-----") { n+=1 } if (n>=2) { print $0 } }' > chain.pem
获取OCSP验证URL
# Get the OCSP responder for server cert
openssl x509 -noout -ocsp_uri -in certificate.pem
# http://ocsp.int-x3.letsencrypt.org
# 或者
# openssl x509 -in certificate.crt -noout -text | grep OCSP
发起一个OCSP验证请求
openssl ocsp -issuer chain.pem -cert certificate.pem \
-verify_other chain.pem \
-header "Host" "ocsp.int-x3.letsencrypt.org" -text \
-url http://ocsp.int-x3.letsencrypt.org
如果是
openssl 1.1.1
(如MacOS),请将-header
那句改为:-header "Host=ocsp.int-x3.letsencrypt.org"
正常输出结果
OCSP Request Data:
Version: 1 (0x0)
Requestor List:
Certificate ID:
Hash Algorithm: sha1
Issuer Name Hash: 7EE66AE7729AB3FCF8A220646C16A12D6071085D
Issuer Key Hash: A84A6A63047DDDBAE6D139B7A64565EFF3A8ECA1
Serial Number: 0353F3B3D1D03160B982105841C733978C28
Request Extensions:
OCSP Nonce:
0410104F5B81F58C45149ACD6EF72B64A333
OCSP Response Data:
OCSP Response Status: successful (0x0)
Response Type: Basic OCSP Response
Version: 1 (0x0)
Responder Id: C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
Produced At: Jul 15 00:19:00 2020 GMT
Responses:
Certificate ID:
Hash Algorithm: sha1
Issuer Name Hash: 7EE66AE7729AB3FCF8A220646C16A12D6071085D
Issuer Key Hash: A84A6A63047DDDBAE6D139B7A64565EFF3A8ECA1
Serial Number: 0353F3B3D1D03160B982105841C733978C28
Cert Status: good
This Update: Jul 15 00:00:00 2020 GMT
Next Update: Jul 22 00:00:00 2020 GMT
Signature Algorithm: sha256WithRSAEncryption
1d:a8:35:ba:14:83:fe:1a:0b:95:e8:b8:9f:5a:18:3f:fa:ca:
3b:db:74:10:68:9c:dd:aa:e2:c3:af:2e:c7:c6:80:02:49:84:
e1:4b:98:0f:b4:e1:88:4d:14:7d:ae:18:12:ee:0c:21:6d:c0:
7a:00:48:17:a2:b9:0b:80:34:34:cb:00:a0:cf:ee:86:c0:ea:
6d:66:0e:eb:af:0c:30:93:4f:c1:86:46:15:e1:5f:60:3d:5f:
33:dc:3e:97:a5:8d:94:52:b9:b1:fe:1a:0a:b1:59:4b:a2:d2:
11:fe:09:87:9e:ce:5f:c7:8b:b5:3c:c0:a2:61:a8:37:0b:93:
3c:0b:82:2e:da:49:76:4a:23:e2:4d:45:4b:81:34:90:8d:0c:
a0:65:76:8a:de:0f:32:bb:1f:da:fa:91:32:d2:c3:4a:d5:d8:
04:66:ec:1d:d3:12:12:a6:6a:23:93:6e:d1:45:c7:12:ce:7a:
0a:c8:47:31:fc:1f:e3:19:a2:c0:02:2a:26:55:a6:58:7b:41:
31:1c:6e:55:cf:68:08:b3:05:dd:96:31:15:bb:14:9b:7c:65:
e6:18:de:fa:1a:9d:59:7a:b1:41:fc:d7:88:8c:5e:56:9f:c7:
69:f8:2f:be:6c:ae:0c:7f:9a:58:d1:39:c3:55:1a:5f:2c:42:
c8:3b:20:14
WARNING: no nonce in response
Response verify OK
certificate.pem: good
This Update: Jul 15 00:00:00 2020 GMT
Next Update: Jul 22 00:00:00 2020 GMT
如果OCSP验证URL不可访问,就会被卡在OCSP Request Data
输出之后,一直等待OCSP Response Data
由于我们使用的Let's Encrypt
的证书,验证URLhttp://ocsp.int-x3.letsencrypt.org
后面的实际地址,大部分已经被和谐了。因此必须用其他方式来解决。
如果我们的服务器在外网的,那就比较好办了,直接启用OCSP Stapling
,在服务端提前做OCSP验证,然后再把验证信息随TLS握手下发。编辑服务端nginx.conf
http {
# ...
# DNS解析器
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
server {
# ...
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /certs/cen2.pw.crt; # 和ssl_certificate保持一致
}
}
重启nginx即可
如果我们的服务器在内网,本身无法访问OCSP验证URL,以下是两种解决方案(for nginx)。
此方案你需要:
stapling responder
假设HTTP代理服务器已经有了,下面讲一下stapling responder
的配置。
首先得到验证用URL
openssl x509 -in certificate.crt -noout -text | grep OCSP
# eg. OCSP - URI:http://ocsp.int-x3.letsencrypt.org
我封装了一个responder的简单docker镜像 cooolin/ocsp-proxy,可以直接使用(源码及文档),将其在服务端启动起来
docker run -d --rm --name ocsp-proxy -p 8080:8080 \
-e HTTP_PROXY=http://YOUR_PROXY:8888 \
-e ocsphost='http://ocsp.int-x3.letsencrypt.org' \
-e http=':8080' \
cooolin/ocsp-proxy
注意YOUR_PROXY
需指向已存在的HTTP代理服务器,然后它就开始监听8080端口了
然后在nginx.conf
中配置:
ssl_stapling on;
ssl_stapling_verify on;
ssl_stapling_responder http://127.0.0.1:8080/;
ssl_trusted_certificate /etc/ssl/ca-certs.pem; # as the same as `ssl_certificate`
注意若nginx在docker容器中,127.0.0.1
需改为宿主机IP,然后重启nginx即可。
不使用代理的话,也可以简单的使用stapling file
。
准备证书与OCSP的URL
openssl s_client -connect tc.cen2.pw:443 < /dev/null 2>&1 | sed -n '/-----BEGIN/,/-----END/p' > certificate.pem
openssl s_client -showcerts -connect tc.cen2.pw:443 < /dev/null 2>&1 | sed -n '/-----BEGIN/,/-----END/p' | awk 'BEGIN { n=0 } { if ($0=="-----BEGIN CERTIFICATE-----") { n+=1 } if (n>=2) { print $0 } }' > chain.pem
openssl x509 -noout -ocsp_uri -in certificate.pem
下载文件
openssl ocsp -no_nonce -respout ./cen2.pw.der \
-verify_other chain.pem \
-issuer ./chain.pem -cert ./certificate.pem \
-header "HOST" "ocsp.int-x3.letsencrypt.org" \
-url http://ocsp.int-x3.letsencrypt.org/
如果是
openssl 1.1.1
(如MacOS),请将-header
那句改为:-header "Host=ocsp.int-x3.letsencrypt.org"
在nginx中配置:
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/ssl/ca-certs.pem; # as the same as `ssl_certificate`
ssl_stapling_file /home/work/.certs/cen2.pw.der;
验证OCSP Stapling是否配置成功
openssl s_client -connect tc.cen2.pw:443 -tls1 -tlsextdebug -status < /dev/null 2>&1 | awk '{ if ($0 ~ /OCSP response: no response sent/) { print "disabled" } else if ($0 ~ /OCSP Response Status: successful/) { print "enabled" } }'
enabled
表示配置成功,disabled
表示配置失败。
参考
https://akshayranganath.github.io/OCSP-Validation-With-Openssl/
https://jhuo.ca/post/ocsp-stapling-letsencrypt/