HTTPS 证书认证原理分析

这篇是我两年前发布在知乎上的一篇旧文,没想到被人抢先发到了公众号,结果微信检测到雷同不让标原创,看来技术文章还是要首发公众号,带上水印,让抄袭者见鬼去吧。

HTTPS 协议栈与 HTTP 的唯一区别在于多了一个安全层(
Security Layer
)—— TLS/SSL,SSL 是最早的安全层协议,TLS 由 SSL 发展而来,两者基本等价,所以下面我们统称 TLS。


OkHttp 用一个 enum 类型来表示 TLS 协议的不同版本,可以看出最早的版本是 SSLv3,诞生于 1996 年,最新的版本是 TLSv1.3,诞生于 2016 年。

public enum TlsVersion {

  TLS_1_3("TLSv1.3"), // 2016.

  TLS_1_2("TLSv1.2"), // 2008.

  TLS_1_1("TLSv1.1"), // 2006.

  TLS_1_0("TLSv1"),   // 1999.

  SSL_3_0("SSLv3"),   // 1996.

  ;

  final String javaName;

}


TLS 握手的作用之一是 身份认证

Au
thentication
),被验证的一方需要提供一个身份证明,在 HTTPS 的世界里,这个身份证明就是 「TLS 证书」,或者叫「HTTPS 证书」。

例如,我们在访问 Google 时,浏览器首先会得到一个 TLS 证书,这个数字证书用于验证我们正在访问的网站和证书的持有者是否匹配,如果不匹配,身份认证会失败,连接也就无法建立。



浏览器得到的是一个证书的链表,这个链表叫 证书链

Certificate Chain
),我们后面会分析它的作用。

与浏览器一样,OkHttp 请求 https 链接时也会得到一个证书链,那我们如何验证书是否合法呢?
先来分析一下 TLS 证书的格式。

TLS 证书格式


世界上所有可信的 CA 机构会遵守 X.509
规范来签发 公钥证书

Public Key Certificate

),证书内容的语法格式遵守 ASN.1
,证书内容如下:


JDK 用 java.security.cert.X509Certificate 
类来表示证书,它继承自抽象类 java.scurity.cert.Certificate
,通过 X509Certificate 
我们可以获取证书的信息。

例如,下面这代码可以得到 Certificate Issuer 的 DN:

caCert.getIssuerX500Principal()

Certificate issuer  是证书签发者,上例「*.google.com」证书的 issuer 是它的父节点 「GTS CA 1O1」,issuer 字段一组符合
X.500
规范的 DN(Distinguished Name):

Issuer: C=US, O=Google Trust Services, CN=GTS CA 1O1
A DN is a sequence of relative distinguished names (RDN) connected by commas.

DN 的属性(等号左侧的值)含义如下所示:


证书里的 Subject’s Name 也是一组 DN,它表示证书的拥有者,「*.google.com」的 Subject 是:

Subject: C=US, ST=California, L=Mountain View, O=Google LLC, CN=*.google.com

X509Certificate 也提供了获取 Subject 的方法:

public X500Principal getSubjectX500Principal() {
if (subjectX500Principal == null) {
subjectX500Principal = X509CertImpl.getSubjectX500Principal(this);
}
return subjectX500Principal;
}
我们用 Google 的证书来分析一下证书到底包含哪些内容。

第一步: 用 openssl 下载证书

echo | openssl s_client -servername google.com –connect google.com:443 |\
sed –ne ‘/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p’ > certificate.crt
我们这里用了 crt 格式,其实证书有四种格式:

  • Certificate (.cer)
  • Privacy Enhanced Mail (.pem)
  • Certificate Bundle (.p7b)
  • Personal Information Exchange (.p12)

第二步: 通过 openssl 命令查看证书内容

openssl x509 -in certificate.crt -text

上面这个命令会输入证书的所有信息(内容太多,省略了Public Key 和 Signature):


一个 Certificate 由 Data 和 Signature 两部分组成。
其中 Data 包含的内容有:

  • 证书版本号:
    X.509v3

  • 序列号:
    一个 CA 机构内是唯一的,但不是全局唯一

  • 签名算法:
    签名的计算公式为 RSA(sha256(Data), IssuerPrivateKey)

  • 签发者:
    DN(Distinguished Name)

  • 有效期:
    证书的有效期间 [Not Before, Not After]

  • 证书拥有者:
    也是一个 DN公钥长度一般是 2048bit,1024bit已经被证明不安全

  • 扩展字段:
    证书所携带的域名信息会配置在 SAN 中(X509v3 Subject Alternative Name)

Signature 位于证书最末尾,签名算法 sha256WithRSAEncryption  在 Data 域内已经指明 ,而 RSA 进行非对称加密所需的私钥(
Private Key
)则是由 Issuer 提供,Issuer 是一个可以签发证书的证书,由证书权威 CA 提供,CA 需要保证证书的有效性,因此 CA 的私钥需要绝密保存,一旦泄露出去,证书可能会被随意签发,也就意味 CA 机构要赔很多钱,我们买证书买的也是信用,跟买保险差不多。

生成签名的公式很简单:

Signature = RSA(sha256(Data), IssuerPrivateKey)

因为 Signature 是 RSA 算法生成的,那么 UA(
User Agent,这里指 OkHttp 这一端
)拿到 TLS 证书之后,需要 Issuer 的公钥(
Public Key
)才能解码出 Data 的摘要。


然而证书只携带了 Issuer 的 DN,并没有公钥,为了弄清楚 UA 如何获取公钥,我们需要先搞明白 Certificate Chain。

证书链(Certificate Chain)

X.509 除了规范证书的内容之外,还规范了如何获取 CRL 以及 Certificate Chain 的验证算法。(

X.509 规范由国际电信联盟(ITU)定义,
RFC 5280
只是定义了 X.509 的用法

文章最开始,我们访问
https://www.google.com
时,浏览器并非只拿到了一个证书,而是一个证书链。

证书「*.google.com」的 Issuer 就是它的父节点「GTS CA 1O1」。因为 UA(浏览器或操作系统)中会预先内置一些权威 CA 签发的根证书(
Root Certificate
)或中间证书(
Intermediate Certificate
),例如上面的 「GTS CA 1O1」和 「GlobalSign」。


当获得证书链之后,我们就可以很轻松的向上回溯,直到找到被 UA 信任的证书。
虽然 UA 内置的可能是中间证书,但是如果一个 End-Entity 证书即使回溯到根证书也没有在 UA 受信列表中找到的话,这个站点就会被标记为不安全。
例如 12306 的主页被标记为 “Not Secure”,因为它的根证书不在受信列表,不被 UA 信任。


TLS Pinning


我们上面所分析的校验方式属于单向校验,仅仅是客户端对服务端证书进行校验,这种方式无法避免中间人攻击(
MIM
A / 

Man-In-the-Middle-Attack
)。比如,我们日常开发中用 Charles 抓包时,Charles 就扮演了一个中间人的角色。抓包之前,需要在手机上安装一个 Charles 提供的根证书,这个根证书加入到手机的 Trust Store, 之后它所签发的证书都会被 UA 认作可信,如此一来, Charles 就可以肆无忌惮地代表真正的 UA 与服务端建立连接,因为是单向认证,所以服务端并不会要求 Charles 提供证书。


但是实现双向校验的成本会比较高,因为 UA 端的证书管理比较复杂,例如证书的获取、有效期管理等等问题,而且需要用户手动添加到 Trust Store,这样也会降低用户体验。
既然双向认证的成本如此之高,那我们不妨利用 SSL Pinning 来解决证书认证被“劫持”的问题。

OkHttp 在 UA 端用一个类 Pin
来表示服务端的 TLS 证书。

static final class Pin {

  /** A hostname like {@code example.com} or a pattern like {@code *.example.com}. */

  final String pattern;

  /** The canonical hostname, i.e. {@code EXAMPLE.com} becomes {@code example.com}. */

  final String canonicalHostname;

  /** Either {@code sha1/} or {@code sha256/}. */

  final String hashAlgorithm;

  /** The hash of the pinned certificate using {@link #hashAlgorithm}. */

  final ByteString hash;

}

证书的最终的表现形式是一个利用哈希算法(

hashAlgorithm
字段表示

)对证书公钥生成的哈希值(

hash
字段表示

),形式如下:

sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=

斜杠之前的字符串是
hashAlgorithm
,之后的字符串是
hash
值。

TLS 证书的 Extension 字段中有一个 SAN(
Subject Alternative Name

),用于配置域名,例如 「*.google.com」的证书中配置了多个域名 —— *.google.com 和 *.android.com,两者所匹配的域名是不同的,所以 Pin
用了一个 pattern
字段来表示两种模式。

我们知道,TLS 证书携带了端的公钥,而这个公钥是 TLS 能够通过握手协商出“对称加密密钥”的关键,证书验证仅仅是为了证明当前证书确实是这个公钥的携带者,或者叫 Owner。

所以我们只需要用一个 Pin
把服务端证书的公钥存储在本地,当得到证书链之后,用 Pin
里的 hash
去匹配证书的公钥即可。

因为本地可以配置多个 Pin
,因此 OkHttp 用了一个 CertificatePinner
来管理。

CertificatePinner certificatePinner = new CertificatePinner.Builder()

   .add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")

   .add("publicobject.com", "sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=")

   .add("publicobject.com", "sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=")

   .add("publicobject.com", "sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=")

   .build();

如此一来,在 TLS 握手过程中,校验证书那一步就可以保证服务端下发的证书是客户端想要的,从而避免了被中间人攻击

,因为本地没有存储中间人证书的 Pin
,所以证书匹配会失败,握手也会失败,从而连接无法建立。

总结

证书这块水比较深,奈何我道行太浅,还没有完全搞懂。
本篇只是很粗浅地把证书认证过程串了一下,还有很多概念没有涉及到,例如证书吊销的 CRL,证书管理的 PKI。
关于 X.500
规范也只是蜻蜓点水,如果要全部搞明白,恐怕短时间内也做不到,关于证书的事情暂时到此为止,后面等我有空了继续写。

参考链接

  • wiki/X.500
  • https://stackoverflow.com/a/11801944
  • okhttp3/CertificatePinner.html
  • What does “G2” mean when used with X509 certficates and certificate authorities?
  • Distinguished Names
  • How to Export Certificate Public Key from Chrome
  • Get SSL Certificate from Server (Site URL) – Export & Download
  • How do I display the contents of a SSL certificate?