服务器名称指示(英语:Server Name Indication,缩写:SNI)是TLS的一个扩展协议,在该协议下,在握手过程开始时客户端告诉它正在连接的服务器要连接的主机名称。这允许服务器在相同的 IP 地址和 TCP 端口号上呈现多个证书,并且因此允许在相同的 IP 地址上提供多个安全(HTTPS)网站(或其他任何基于 TLS 的服务),而不需要所有这些站点使用相同的证书。它与 HTTP/1.1 基于名称的虚拟主机的概念相同,但是用于 HTTPS。1
背景
在互联网兴起早期,每个域名都对应了独立的 IPv4 地址。但 IPv4 地址毕竟是有限的,且被霸道地分配或预留给了部分国家和组织。随着互联网的迅猛发展,逐渐出现 IPv4 地址短缺的问题,为了让多个域名复用一个 IP 地址,在 HTTP 服务器上引入了基于名称的虚拟主机(Name-based Virtual Hosting)的概念。服务器可以根据客户端请求头(Header)中不同的 Host,将请求分配给不同的域名(虚拟主机)来处理。
HTTP 是不安全的协议,主要是明文传输数据和缺乏检测消息完整性的机制,容易遭受中间人攻击。为了安全地进行通信,网景公司在1994年开始使用 HTTPS(Hyper Text Transfer Protocol over SecureSocket Layer),利用 SSL 在 HTTP 的基础上通过传输加密和身份认证保证了传输过程的安全性。后来由 IETF 在1999年标准化为 TLS(Transport Layer Security)。其中重要的一点是当进行 TLS 连接时,客户端从 Web 服务器请求数字证书。服务器一旦发送证书,客户端就会检查这个证书,并将其尝试连接的名称与证书中包含的名称进行对比。如果发生匹配,则连接正常进行。如果没有找到匹配,则可能会向用户警告该差异,并且可能会中止连接,因为该失配可能表明存在中间人攻击。
但是,当使用 HTTPS 时,TLS握手发生在服务器看到任何 HTTP 头之前,因此,服务器不可能使用 HTTP 主机头中的信息来决定呈现哪个证书。一种可能的方式是 SAN(Subject Alternative Name),允许一个证书中包含多个域名,但是每当域名列表有变化时,都需要到证书颁发机构重新申请新的证书,这意味着更多的费用,并且证书颁发机构会限制域名数量上限。部分证书颁发机构支持多域名通配符,但是价格昂贵。通常申请的是单域名通配符证书,支持任意多子域名,价格适中且不需要变更。另外一种做法是每个域名指向单独的 IPv4 地址,但是这意味着更多的维护开销,而且公网 IPv4 地址根本不够用。
解决方案
2007年 OpenSSL 发布0.9.8f版本,增加了对 SNI 的支持。SNI 通过让客户端发送域名的名称作为 TLS 协商的一部分来解决此问题。这使服务器能够提前选择正确的域名,并向客户端提供包含正确名称的证书。注意,这个域名信息是加密连接建立前明文传输的,使得可能被政府或网络供应商用于网络审查。TLS 1.3将通过支持 ESNI(Encrypted Server Name Indication)以解决这个问题。
此方案由于需要客户端支持,较老的操作系统(例如 Windows XP)或应用程序(例如 JAVA 1.6)需要升级才能正常使用。注意,SNI 兼容 TLS1.0 及以上协议,但不被 SSL 支持。
SNI 兼容性
- SNI 支持以下桌面版浏览器:
- Chrome 5及以上版本
- Chrome 6及以上版本(Windows XP)
- Firefox 2及以上版本
- IE 7及以上版本(运行在 Windows Vista/Server 2008及以上版本系统中,在 XP 系统中任何版本的 IE 浏览器都不支持 SNI)
- Konqueror 4.7及以上版本
- Opera 8及以上版本
- Safari 3.0 on Windows Vista/Server 2008及以上版本,Mac OS X 10.5.6 及以上版本
- SNI 支持以下手机端浏览器:
- Android Browser on 3.0 Honeycomb 及以上版本
- iOS Safari on iOS 4及以上版本
- Windows Phone 7及以上版本
- SNI 支持以下库:
- GNU TLS
- Java 7及以上版本,仅作为客户端
- HTTP client 4.3.2及以上版本
- libcurl 7.18.1及以上版本
- NSS 3.1.1及以上版本
- OpenSSL 0.9.8j及以上版本
- OpenSSL 0.9.8f及以上版本,需配置 flag
- Qt 4.8及以上版本
- Python3、Python 2.7.9及以上版本
- SNI 支持以下服务器:
- Apache 2.2.12及以上版本
- Apache Traffic Server 3.2.0及以上版本
- HAProxy 1.5及以上版本
- IIS 8.0及以上版本
- lighttpd 1.4.24及以上版本
- LiteSpeed 4.1及以上版本
- nginx 0.5.32及以上版本
- SNI 支持以下命令行:
- cURL 7.18.1及以上版本
- wget 1.14及以上版本
如何验证已支持 SNI
即使版本已经支持了,仍然可能被关闭,你需要检查配置项。以 Nginx 为例,可以运行命令/path/to/nginx -V
(例如/usr/local/nginx/sbin/nginx -V
)检查:
nginx version: nginx/1.14.0
built by gcc 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9)
built with OpenSSL 1.0.2n 7 Dec 2017
TLS SNI support enabled
如果不是自己的服务,可以通过SSL Labs进行测试。也可以用 openssl 命令进行测试,openssl s_client -servername {DOMAIN} -showcerts -connect {DOMAIN}:443
(例如openssl s_client -showcerts -servername yunsou.ap-guangzhou.tencentcloudapi.com -connect yunsou.ap-guangzhou.tencentcloudapi.com:443
),根据打印的证书信息可以判断服务器是否支持 SNI。
客户端可以通过抓包验证是否支持 SNI。在 TLS 握手阶段的 Client Hello 报文中,如果看到 SNI 扩展字段则表示客户端支持,否则需要升级客户端版本。下图引用自2。
服务器侧,Nginx 如果发现客户端没有发送 SNI 扩展,则会返回默认服务器default_server
的证书。如果没有默认服务器,则使用第一个配置文件中的配置。参考https://orchidflower.gitee.io/2017/06/03/SSL-Authentication-Failure-caused-by-Nginx-Proxy/
JAVA SNI 故障排查
有朋友反映使用 JAVA 8请求 HTTPS 服务时,遇到了证书错误,形如:
javax.net.ssl.SSLPeerUnverifiedException-Hostname vod.tencentcloudapi.com not verified:
certificate: sha1/L/l5PUsnDo67uXym1Vx/YkkE0Wg=
DN: CN=*.ap-beijing.tencentcloudapi.com, OU=R&D, O=Tencent Technology (Shenzhen) Company Limited, L=Shenzhen, ST=Guangdong, C=CN
subjectAltNames: [*.ap-beijing.tencentcloudapi.com, ap-beijing.tencentcloudapi.com]
由于所请求的服务器是支持 SNI 的,这其实很可能是客户端不支持,或者途经的网关不支持导致的(途经网关不支持导致的问题可以参考这里的例子https://blog.csdn.net/finded/article/details/100068385)。在启动JAVA程序时,指定VM参数-Djavax.net.debug=ssl
,观察 SSL 相关详细日志输出,在支持 SNI 的情况下,能观察到客户端发送Client Hello
信息时附带的sever_name
扩展,即下文的Extension server_name, server_name: [type=host_name (0), value=yunsou.ap-guangzhou.tencentcloudapi.com]
部分,形如:
*** ClientHello, TLSv1.2
RandomCookie: GMT: 1579810194 bytes = { 99, 177, 65, 0, 206, 20, 79, 235, 185, 228, 2, 37, 235, 15, 83, 199, 236, 121, 24, 218, 146, 33, 51, 228, 11, 126, 15, 201 }
Session ID: {}
Cipher Suites: [TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_DHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_DSS_WITH_AES_128_CBC_SHA, TLS_RSA_WITH_AES_128_GCM_SHA256, TLS_RSA_WITH_AES_128_CBC_SHA, SSL_RSA_WITH_3DES_EDE_CBC_SHA]
Compression Methods: { 0 }
Extension elliptic_curves, curve names: {secp256r1, secp384r1, secp521r1, sect283k1, sect283r1, sect409k1, sect409r1, sect571k1, sect571r1, secp256k1}
Extension ec_point_formats, formats: [uncompressed]
Extension signature_algorithms, signature_algorithms: SHA512withECDSA, SHA512withRSA, SHA384withECDSA, SHA384withRSA, SHA256withECDSA, SHA256withRSA, SHA256withDSA, SHA1withECDSA, SHA1withRSA, SHA1withDSA
Extension server_name, server_name: [type=host_name (0), value=yunsou.ap-guangzhou.tencentcloudapi.com]
Extension renegotiation_info, renegotiated_connection: <empty>
注意:这里https://stackoverflow.com/questions/35366763/in-java-8-can-httpsurlconnection-be-made-to-send-server-name-indication-sni提到 JAVA 对本地局域网不会发送 SNI。
即使 JAVA 8已经支持了 SNI,但如果在启动JAVA程序时,指定了-Djsse.enableSNIExtension=false
,或者在程序中设定了这个系统参数System.setProperty("jsse.enableSNIExtension", "false");
,则JAVA在SSL握手时不会发送server_name
扩展。
另外在 JAVA 8较低版本中,如果指定了setHostnameVerifier
,则会关闭 SNI,在 JAVA 1.8.141中此问题已经修复,参考https://bugs.openjdk.java.net/browse/JDK-8144566,https://stackoverflow.com/questions/36323704/sni-client-side-mystery-using-java8和https://blog.csdn.net/Dancen/article/details/82459157
测试
因为curl
命令无法直接关闭 SNI,我们使用openssl
命令进行测试。openssl s_client -connect {YOUR_REAL_HOST_NAME}:443 -tlsextdebug
会发起不带 SNI 扩展头的请求到目标服务器,并打印详细交互过程。加上-servername {YOUR_REAL_HOST_NAME}
参数则会带上 SNI 扩展头。服务器返回的证书 CN 字段将会标识具体返回的是哪个域名匹配的证书。注意观察下面两个命令的输出差别,我们可以看到目标服务器上有两个证书,如果不支持 SNI,默认返回的是 *.ap-beijing.tencentcloudapi.com
域名的证书。
# openssl s_client -connect sms.tencentcloudapi.com:443 -tlsextdebug -servername sms.tencentcloudapi.com
CONNECTED(00000003)
TLS server extension "server name" (id=0), len=0
TLS server extension "renegotiation info" (id=65281), len=1
0001 - <SPACES/NULS>
TLS server extension "EC point formats" (id=11), len=4
0000 - 03 00 01 02 ....
TLS server extension "session ticket" (id=35), len=0
TLS server extension "heartbeat" (id=15), len=1
0000 - 01 .
depth=2 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
verify return:1
depth=1 C = US, O = DigiCert Inc, CN = DigiCert Secure Site CN CA G3
verify return:1
depth=0 C = CN, ST = Guangdong Province, L = Shenzhen, O = Tencent Technology (Shenzhen) Company Limited, CN = *.tencentcloudapi.com
verify return:1
# openssl s_client -connect sms.tencentcloudapi.com:443 -tlsextdebug
CONNECTED(00000003)
TLS server extension "renegotiation info" (id=65281), len=1
0001 - <SPACES/NULS>
TLS server extension "EC point formats" (id=11), len=4
0000 - 03 00 01 02 ....
TLS server extension "session ticket" (id=35), len=0
TLS server extension "heartbeat" (id=15), len=1
0000 - 01 .
depth=2 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
verify return:1
depth=1 C = US, O = DigiCert Inc, CN = DigiCert Secure Site CN CA G3
verify return:1
depth=0 C = CN, ST = Guangdong Province, L = Shenzhen, O = Tencent Technology (Shenzhen) Company Limited, CN = *.ap-beijing.tencentcloudapi.com
verify return:1
其他资料
下图引用自https://wwww.lvmoo.com/933.love:
关于 Windows XP 系统上 IE 浏览器不支持 SNI 的验证可以参考https://www.cnblogs.com/baihualin/p/10965236.html。
License: (CC 3.0) BY-NC-SA