87c739fc26519f4997518eed13ed0492
012 | 关于HTTPS你需要了解的必备知识

继续聊信息安全的话题,上一篇聊了基础层面的密码技术,这一篇就来聊聊组合应用了各种基础密码技术的 HTTPS,了解下 HTTPS 的一些简单原理,以及如何正确应用于实际项目中。

数字证书

数字证书,也称公钥证书,是 HTTPS 中最关键的安全因素,所以我们先来理解为什么需要数字证书?以及数字证书包含了哪些信息?

前一篇我们已经了解了公钥加密和数字签名技术,对这两种技术,其实都存在一个问题:如何校验公钥的合法性?假设在客户端和服务端之间有个中间人在监听两端的通信,当服务端将公钥发给客户端时,中间人拦截到请求,然后将服务端发出来的公钥自己保存起来,而将中间人自己的公钥发送给客户端,这样,客户端收到的公钥其实就是中间人的公钥,客户端发给服务端的消息中间人就可以解密了,解密后的明文就可以再用服务端的公钥加密再发回给服务端,这样,对于客户端和服务端来说,其实是不知道中间人的存在的。这就是所谓的中间人攻击(Man In The Middle, MITM),简化的过程如下图所示:

另外,就算服务端最后返回响应数据时还加上了自己的数字签名,那中间人也可以将其替换成自己的数字签名,然后再返回给客户端,客户端由于收到的是中间人的公钥,因此验证数字签名后的结果也是一致的。

为了防止中间人攻击,就需要有一种机制可以校验公钥的合法性,即我要能校验出这个公钥是 A 的,另一个公钥是 B 的。为了解决这个校验问题,就需要有机构为信任背书,于是就出现了认证授权(CA,Certificate Authority)机构,这些机构会为 A 或 B 分别颁发数字证书,就类似于政府机构为我们颁发身份证、驾照一样。当用户想得到属于自己的一份证书时,他就需要向 CA 机构提出申请。CA 审核了用户的身份信息后,就会给他生成一对公钥和私钥,然后用 CA 自己的私钥对用户的公钥进行数字签名。公钥信息(包括签名)加上用户信息,以及 CA 机构的信息、证书的有效期、扩展信息等组合在一起,并生成消息摘要,就形成了完整的数字证书。

为了让你有个感性的认识,给你看下 baidu.com 的证书,如下图:

如上面几张图所示,一个证书里面包含了很多信息,不过,关键信息有三部分:一是 Subject Name,包含了证书持有者的信息;二是 Issuer Name,包含了证书签发机构的信息,其中有一项 Signature Algorithm 表示签发机构对证书公钥进行签名时所使用的签名算法;三是 Public Key Info,即公钥信息,可以看到 Signature 一项,就是签发机构对该公钥的签名。公钥信息后面都是扩展信息,而证书最后还可以看到 Fingerprints,即该证书的消息摘要。

另外,也可以看到,baidu.com 上面还有两个证书,一个是 GlobalSign Organization Validation CA - SHA256 - G2,一个是 GlobalSign Root CA,这两个证书都是证书签发机构 GlobalSign 自己的证书。GlobalSign 是全球知名的顶级数字证书授权中心(简称 CA),另外全球知名的还有 VeriSign。国内知名的主要有三家:沃通、上海数字认证中心、中国金融认证中心(CFCA)。这三个证书形成了一个三级的证书链,其中,Root CA 称为根证书,也称为自签名证书,一般在客户端系统内部已经预先配置好了这些权威机构的根证书,以方便对根证书进行校验。另外,引入中间级证书是为了将证书与用户证书严格隔离开,以保证根证书的绝对安全性。上级的证书持有者会用自己的私钥对下级证书的公钥进行数字签名,当对一个数字证书校验其合法性时,需要对整个证书链都进行校验,一直校验到根证书和浏览器内置的根证书是否一致。

如果用户不向权威的 CA 机构申请数字证书,而是自己为自己签发证书,比如就使用 OpenSSL 生成自签名证书,那就无法被预先配置到客户端系统内部,那在一些浏览器上访问你的网站时就可能会被拒绝访问。不过,在移动端也可以安全使用,这点后面再聊。

HTTPS简单原理

了解了数字证书,接着再来了解 HTTPS 的原理。简单来说,HTTPS = HTTP + SSL/TLS,即在 HTTP 协议的基础上加了 SSL/TLS 安全协议。另外,SSL/TLS 安全协议不止可用于 HTTP,也可用于 WebSocket、Socket、IMAP、POP3/SMTP 等 TCP/IP 协议。SSL 其实是旧版本的命名,最高版本就是 SSL 3.0;而从 3.1 版本开始,则更名为 TLS,有 TLS 1.0、TLS 1.1 和 TLS 1.2 三个版本。SSL/TLS 基本应用了前一篇文章所提到的所有密码技术,保证了消息的机密性、完整性、认证。

SSL/TLS 安全协议可分为两层子协议:TLS记录协议TLS握手协议。TLS记录协议相对比较简单,主要就是对消息进行分段、压缩、计算MAC值、加密、添加协议头、然后发送数据。其中,使用哪种压缩算法、加密算法、密钥等都是在握手期间协商出来的。处理过程简化如下图:

TLS握手协议则用于在实际的数据传输开始前,通信双方进行身份认证、协商安全参数、加密算法、交换密钥等。其中,身份认证主要就是通过数字证书进行认证。TLS握手协议还分为了四个子协议:握手协议、密码规格变更协议、警告协议和应用数据协议

握手协议是最复杂的一个子协议,要通过四次握手,而且关键的一点,握手阶段基本是明文传输的,一般的中间人攻击也是在这时候侵入的。握手时有几个关键的点需要明白,一是加密方案,不是单纯只使用对称加密或非对称加密,而是结合使用的,对消息是采用对称加密,而对称加密的密钥则是用非对称加密的。二是密钥协商的过程,首先客户端会生成一个随机数发给服务端,然后服务端也会返回一个随机数给客户端,之后会再将数字证书发给客户端,客户端再生成一个随机数作为预备主密码,用收到的数字证书中的公钥对预备主密码进行加密后再传给服务端,至此,客户端和服务端都知道了三个数:客户端随机数、服务端随机数、预备主密码,接着,客户端和服务端使用同样的计算方式计算出主密码,再结合主密码又计算出密钥材料,最后提取出客户端和服务端相应的对称密码的密钥、消息认证码的密钥、对称密码的CBC模式中使用的初始化向量(IV)共四个密钥和两个IV,简化过程如下图:

为什么是生成客户端和服务端两套不一样的密钥和IV,而不是只采用相同的一套?主要也是为了提高安全,保证密钥是单向的,客户端加密采用一套,服务端加密采用另一套,这样,就算在一个方向上遭受攻击,在另一个方向上基本没什么影响。

最后,也是最关键的一点,为了防止中间人攻击,客户端必须对服务端发过来的数字证书进行合法性校验,一般需要校验以下几点:

  1. 证书是否在有效期内?
  2. 证书是否已被吊销?验证吊销有 CRL 和 OCSP 两种方法。
  3. 证书链是否可信任?即层层校验到最后的根证书是否在客户端内置的信任名单内?
  4. 证书的域名和指纹等是否匹配?

浏览器本身一般只会校验前三项,而最后一项是需要客户端自己去实现的,如果不做这一项校验,那中间人采用有效可信任的证书时,依然能够通过浏览器的校验。一般实现第四种校验的方式就是证书绑定(Certificate Pinning)

除了握手协议,其他3个子协议都很简单。密码规格变更协议用于密码切换的同步。简单地说,就跟向对方喊“1、2、3!”差不多。当协议中途发生错误时,就会通过警告协议传达给对方。警告协议负责在发生错误时将错误传达给对方。如果没有发生错误,则会使用应用数据协议来进行通信。应用数据协议用于和通信对象之间传送应用数据。当TLS套接在HTTP时,HTTP的请求和相应就会通过TLS的应用数据协议和TLS记录协议来进行传送。

客户端的正确使用姿势

在客户端如果对 HTTPS 的使用不当,那就容易存在安全漏洞,尤其在移动端,你需要对原生的 HTTPS 请求和 WebView 里的 HTTPS 请求都做好安全配置,主要就是要实现上面所说的证书绑定

很多缺乏经验的人写代码时都习惯采用默认方式实现,而对 HTTPS 支持的默认实现普遍对证书的校验是不够完善的,那就给中间人攻击提供了机会。比如,攻击者可以通过很多方式在用户设备上安装证书,即将中间人服务器的证书放到设备的信任列表中,以此进行中间人攻击。

实现证书绑定其实也很简单,一般做法就是在设备上预埋服务端的证书,可以预埋整个证书,也可以只预埋证书公钥,或证书的hostname、指纹信息等,然后接收到握手阶段服务端发过来的证书后,校验和本地预埋的证书是否一致。以 Android 的网络框架 OkHttp 为例,添加证书绑定的代码非常简单,如下:

  • client = new OkHttpClient.Builder()
  • .certificatePinner(new CertificatePinner.Builder()
  • .add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
  • .build())
  • .build();

其实就是校验了域名和指纹,都不需要预埋整个证书。

而 iOS 的网络框架 Alamofire 也提供了证书绑定的功能实现,示例代码如下:

  • let serverTrustPolicies: [String: ServerTrustPolicy] = [
  • "test.example.com": .PinCertificates(
  • certificates: ServerTrustPolicy.certificatesInBundle(),
  • validateCertificateChain: true,
  • validateHost: true
  • ),
  • "insecure.expired-apis.com": .DisableEvaluation
  • ]
  • let manager = Manager(
  • serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies)
  • )

Alamofire 主要就是通过 ServerTrustPolicy 来设置绑定策略,其中,pinCertificates 会校验预埋证书与服务器证书是否一致,也是 Alamofire 官方最推荐使用的方式。另外,还提供了 pinPublicKeys 的绑定方式,主要就是校验公钥。DisableEvaluation 则是不做任何校验了,即无条件信任任何服务端。

另外,还需要保证 WebView 上的 HTTPS 安全,否则,也很容易被攻击,比如访问的网页被跳转到了伪造的钓鱼网站。因此,安全的做法就是 WebView 加载 HTTPS 时也强校验服务端证书。Android 可以在 WebViewClientonPageStarted() 方法中进行校验,如果校验不通过则不加载,以下是校验指纹的示例代码:
```Java
public static boolean pinCertificate(SslCertificate cert, String fingerprint) {
byte[] fingerprintBytes = hexToBytes(fingerprint);
Bundle bundle = SslCertificate.saveState(cert);
if (bundle != null) {
byte[] bytes = bundle.getByteArray("x509-certificate");

top Created with Sketch.