SNI 问题及探究

最近 Android 一直报一个错误,错误显示为:

Hostname in certificate didn’t match

1
2
javax.net.ssl.SSLException: hostname in certificate didn't match: 
<hs.hjf.cn> != <*.b0.upaiyun.com> OR <*.b0.upaiyun.com>

简言之,就是你请求服务器中的证书集中未包含当前域名的证书。

初判断

吐槽下,国内的 CDN 全是问题,写出来都是一本厚厚的书了,出这个问题时,刚好所有端都碰上 CDN 问题,所以先是从这方面着手,随着时间演进,慢慢的成了问题只出现在 Android 上。

才开始想着从 Android 上找问题,后来证实是 Android 上用的一个 Network Library 不支持 SNI 所导致。

SNI 的来历

Server Name Indication (SNI) is an extension to the TLS computer networking protocol[1] by which a client indicates which hostname it is attempting to connect to at the start of the handshaking process.

简单来说,就是如果一台机器上部署多个 HTTPS 站点,对应多张证书,应用服务器在响应时,无法确定用哪一张证书来对数据进行签名。为解决这个问题就提出了 SNI,即在 Client Hello 时,告诉用哪一张证书。

这个协议于 2004 年提出,2006 年被 OpenSSL 所加入,次年发布。

支持

目前 SNI 几乎被所有的软件和库所支持。

- Desktop Browsers

  • Internet Explorer 7 and later
  • Firefox 2
  • Opera 8 with TLS 1.1 enabled
  • Google Chrome: Chrome 6 and later
  • Safari 2.1 and later (requires OS X 10.5.6 and later or Windows Vista and later).

- Mobile Browsers

  • Mobile Safari for iOS 4.0
  • Android 3.0 (Honeycomb) and later
  • Windows Phone 7

- Libraries

这个具体就不列了,可以具体去查。提一下本次 Android 上的库吧,5.0 之前,Android 上没有自己实现网络库,用的是 Apache HttpClient,项目中用的是 async-http,另一套封装。

android-async-http

从 Issue 和源码中确实没有关于 SNI 的实现,建议使用这个库的项目尽快升级。

okhttp

okhttp 是现在 Android 上的主流,系统的 HttpURLConnection 使用不是很高。不过可以看到它也是 2014 年,才支持 SNI 的。

解决方案

除了让服务器默认使用这张证书进行签名,还能做的就是让客户端禁止 HTTPS 校验。

这样的坏处就是任何证书签名的内容就都是被信任的,数据依然存在被修改的可能性,HTTPS 的意义也就不在了,可以说花了精力接入了个假的 HTTPS。

SNI 调试方法

SNI 发生在 OSI 模型中的第六层,在 TLSV2 中。可以通过 Wireshark 这样的抓包工具来进行分析。

如果想验证这个过程,可以通过 openssl 命令来进行:

1
$ openssl s_client -servername hs.hjf.cn -tlsextdebug -msg -connect hs.hjf.cn:443

安全原因,这里的域名是假的,如果需要真实数据,可自己配置域名

1
2
3
4
5
6
7
---
Certificate chain
0 s:/C=CN/ST=shanghai/L=shanghai/O=Shanghai xxx Communications Co.,Ltd/OU=Administration Department/CN=*.hjf.cn
i:/C=US/O=GeoTrust Inc./CN=GeoTrust SSL CA - G3
1 s:/C=US/O=GeoTrust Inc./CN=GeoTrust SSL CA - G3
i:/C=US/O=GeoTrust Inc./CN=GeoTrust Global CA
---

如果取消 servername 这个参数,会发现服务端用了一张默认的证书来进行签发了。这张证书上所包含的域名是 b0.upaiyun.com,这是又拍云提供的 CDN 服务。

1
2
3
4
5
6
7
8
9
---
Certificate chain
0 s:/C=CN/ST=zhejiang/L=hangzhou/O=Hangzhou Weiju Network Ltd./OU=\xE6\x8A\x80\xE6\x9C\xAF\xE9\x83\xA8/CN=*.b0.upaiyun.com
i:/C=US/O=GeoTrust Inc./CN=GeoTrust SSL CA - G3
1 s:/C=US/O=GeoTrust Inc./CN=GeoTrust SSL CA - G3
i:/C=US/O=GeoTrust Inc./CN=GeoTrust Global CA
2 s:/C=US/O=GeoTrust Inc./CN=GeoTrust Global CA
i:/C=US/O=GeoTrust Inc./CN=GeoTrust Global CA
---