September 24, 2020

关于SSL Pinning的一切

背景

HTTPS当下已经非常普遍,HTTPS全称是Hypertext Transfer Protocol Secure,在HTTP基础上增加了TLS加密,虽然名字里有个Secure,但HTTPS并不是绝对安全的,依然存在被中间人攻击(
Man-in-the-middle attack)的风险,进而导致应用被抓包,HTTPS的加密流量被获取。

MITM中间人攻击

概念

这里举个简单的例子,帮助不了解MITM的读者理解:假如A和B需要通信,这个时候来了一个C,C告诉A自己是B,同时告诉B自己是A,A和B都以为自己在和对方通信,实际上看到的消息都是由C转发的,C就可以在这个过程中完成监听和篡改,这就是中间人攻击。

ARP欺骗

在真实的网络中,要完成上述的过程,需要借助ARP欺骗。ARP是局域网中用IP来查找MAC地址的协议,正常的ARP查找过程中,请求的主机会向局域网发送广播,查询对应IP的MAC地址,局域网的其他主机如果不是这个IP就会忽略请求,对应IP的主机会回应自己的MAC地址。但是ARP协议在机制上就没有考虑校验的情况,只要收到一个ARP回应,主机就会更新自己的ARP表。ARP协议的简单粗暴,让ARP欺骗变得非常简单。攻击者只需要往一个局域网不断发送ARP回应,就能更新各个主机的ARP表,从而达到上面一节说的目的,这个过程也被叫做ARP投毒。当然,更大范围的中间人攻击需要借助DNS投毒,这个就不细说,原理大致类似。

针对HTTPS的MITM

HTTPS在设计上是考虑到了中间人攻击的情况的,TLS是支持双向认证的(一般只需要客户端校验服务端身份),那么为什么还会存在中间人攻击的风险呢?TLS的认证机制是基于证书的,关于证书的细节我们会在后面的篇幅里细讲,这里不展开。我们如果没有信任一些奇奇怪怪的证书,TLS是可以保证通信安全的,否则就会导致TLS的认证机制失效,从而被中间人攻击。

抓包

抓包就是一个MITM的应用场景,这里以常用的Charles为例,看看HTTPS的加密是如何被绕过的。我们知道,如果要解密HTTPS流量,Charles会引导我们给手机安装一个根证书,用文本编辑器打开可以看出是标准的pem格式:

-----BEGIN CERTIFICATE-----
MIIFQDCCBCigAwIBAgIGAXSrxEHXMA0GCSqGSIb3DQEBCwUAMIGkMTUwMwYDVQQD
DCxDaGFybGVzIFByb3h5IENBICgyMCBTZXAgMjAyMCwgQzAyRDM3RUhNRDZSKTEl
MCMGA1UECwwcaHR0cHM6Ly9jaGFybGVzcHJveHkuY29tL3NzbDERMA8GA1UECgwI
WEs3MiBMdGQxETAPBgNVBAcMCEF1Y2tsYW5kMREwDwYDVQQIDAhBdWNrbGFuZDEL
MAkGA1UEBhMCTlowHhcNMDAwMTAxMDAwMDAwWhcNNDkxMTE3MTM0NjM5WjCBpDE1
MDMGA1UEAwwsQ2hhcmxlcyBQcm94eSBDQSAoMjAgU2VwIDIwMjAsIEMwMkQzN0VI
TUQ2UikxJTAjBgNVBAsMHGh0dHBzOi8vY2hhcmxlc3Byb3h5LmNvbS9zc2wxETAP
BgNVBAoMCFhLNzIgTHRkMREwDwYDVQQHDAhBdWNrbGFuZDERMA8GA1UECAwIQXVj
a2xhbmQxCzAJBgNVBAYTAk5aMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEAoMCTz31wG8zGwexoelqWd+q9WzQHtkFCReKjw0qRZ/8gjmUuj6pdmEg6FQFr
f9gnIiyPeME+J1gOfIp3z9i860VjviQGUwuPCBuU8G0eXBYZOE2kJvKx1G5QeI/c
hnGi3a3Sk5bGBUV1mMbS35OUkFVgvBygVyEjOF1SKDM/IT9jh5QV8uzhObDk+0F6
mjZ+uug2CDdQLNd4VqMClXrDaFk2gVpcnDatNI0p6doBlsxMedIFw0wPJaXfdl1M
CkPOwqgDpgh4J/3roGwJ5ky9zbE7l552jm/UjTRt5X7608IO5G0Kd5OutvxyqmZU
mYLDS0wcS+vZrPA6WwPUgT+TeQIDAQABo4IBdDCCAXAwDwYDVR0TAQH/BAUwAwEB
/zCCASwGCWCGSAGG+EIBDQSCAR0TggEZVGhpcyBSb290IGNlcnRpZmljYXRlIHdh
cyBnZW5lcmF0ZWQgYnkgQ2hhcmxlcyBQcm94eSBmb3IgU1NMIFByb3h5aW5nLiBJ
ZiB0aGlzIGNlcnRpZmljYXRlIGlzIHBhcnQgb2YgYSBjZXJ0aWZpY2F0ZSBjaGFp
biwgdGhpcyBtZWFucyB0aGF0IHlvdSdyZSBicm93c2luZyB0aHJvdWdoIENoYXJs
ZXMgUHJveHkgd2l0aCBTU0wgUHJveHlpbmcgZW5hYmxlZCBmb3IgdGhpcyB3ZWJz
aXRlLiBQbGVhc2Ugc2VlIGh0dHA6Ly9jaGFybGVzcHJveHkuY29tL3NzbCBmb3Ig
bW9yZSBpbmZvcm1hdGlvbi4wDgYDVR0PAQH/BAQDAgIEMB0GA1UdDgQWBBTtSzIK
BzFSToLLgoAPM4tSPWqDEDANBgkqhkiG9w0BAQsFAAOCAQEAnB+8XuuZAtE3WE03
xIu3rHw+sYdrSvV0es/xt1L2/gnnll/W7PvK4prG62sagblbbnLECLy8AKfN/gh9
aY9i6EXxee+vVy8GC8Cmo4TIv0asmPqUXBv+ggZCRNvnT1mtCvpkjgeEwGTXjqk6
Caq1X61WDzTg/EBPpqhSX10BTFRXLufVMfC/Qy5EdpgwCOm8SZnEwqgAW62GM81L
ngl+WIM+NLX5sdtSmkuhfikNR5rRvFPIjBU1t9qP77l/24Ov5BsGjcMfk3Pjzdqy
8V17WhQGRhb/k6nzlxrxWmQ4rdNVtKLWHD9ubozsX23z6B8l1GMDzYr3VbxdMpGP
V5eiGA==
-----END CERTIFICATE-----

用openssl解析证书:

openssl x509 -text -in ~/Downloads/charles-ssl-proxying-certificate.pem

得到如下输出:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            04:00:00:00:00:01:0f:86:26:e6:0d
    Signature Algorithm: sha1WithRSAEncryption
        Issuer: OU=GlobalSign Root CA - R2, O=GlobalSign, CN=GlobalSign
        Validity
            Not Before: Dec 15 08:00:00 2006 GMT
            Not After : Dec 15 08:00:00 2021 GMT
        Subject: OU=GlobalSign Root CA - R2, O=GlobalSign, CN=GlobalSign
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:a6:cf:24:0e:be:2e:6f:28:99:45:42:c4:ab:3e:
                    21:54:9b:0b:d3:7f:84:70:fa:12:b3:cb:bf:87:5f:
                    c6:7f:86:d3:b2:30:5c:d6:fd:ad:f1:7b:dc:e5:f8:
                    60:96:09:92:10:f5:d0:53:de:fb:7b:7e:73:88:ac:
                    52:88:7b:4a:a6:ca:49:a6:5e:a8:a7:8c:5a:11:bc:
                    7a:82:eb:be:8c:e9:b3:ac:96:25:07:97:4a:99:2a:
                    07:2f:b4:1e:77:bf:8a:0f:b5:02:7c:1b:96:b8:c5:
                    b9:3a:2c:bc:d6:12:b9:eb:59:7d:e2:d0:06:86:5f:
                    5e:49:6a:b5:39:5e:88:34:ec:bc:78:0c:08:98:84:
                    6c:a8:cd:4b:b4:a0:7d:0c:79:4d:f0:b8:2d:cb:21:
                    ca:d5:6c:5b:7d:e1:a0:29:84:a1:f9:d3:94:49:cb:
                    24:62:91:20:bc:dd:0b:d5:d9:cc:f9:ea:27:0a:2b:
                    73:91:c6:9d:1b:ac:c8:cb:e8:e0:a0:f4:2f:90:8b:
                    4d:fb:b0:36:1b:f6:19:7a:85:e0:6d:f2:61:13:88:
                    5c:9f:e0:93:0a:51:97:8a:5a:ce:af:ab:d5:f7:aa:
                    09:aa:60:bd:dc:d9:5f:df:72:a9:60:13:5e:00:01:
                    c9:4a:fa:3f:a4:ea:07:03:21:02:8e:82:ca:03:c2:
                    9b:8f
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Certificate Sign, CRL Sign
            X509v3 Basic Constraints: critical
                CA:TRUE
            X509v3 Subject Key Identifier:
                9B:E2:07:57:67:1C:1E:C0:6A:06:DE:59:B4:9A:2D:DF:DC:19:86:2E
            X509v3 CRL Distribution Points:

                Full Name:
                  URI:http://crl.globalsign.net/root-r2.crl

            X509v3 Authority Key Identifier:
                keyid:9B:E2:07:57:67:1C:1E:C0:6A:06:DE:59:B4:9A:2D:DF:DC:19:86:2E

    Signature Algorithm: sha1WithRSAEncryption
         99:81:53:87:1c:68:97:86:91:ec:e0:4a:b8:44:0b:ab:81:ac:
         27:4f:d6:c1:b8:1c:43:78:b3:0c:9a:fc:ea:2c:3c:6e:61:1b:
         4d:4b:29:f5:9f:05:1d:26:c1:b8:e9:83:00:62:45:b6:a9:08:
         93:b9:a9:33:4b:18:9a:c2:f8:87:88:4e:db:dd:71:34:1a:c1:
         54:da:46:3f:e0:d3:2a:ab:6d:54:22:f5:3a:62:cd:20:6f:ba:
         29:89:d7:dd:91:ee:d3:5c:a2:3e:a1:5b:41:f5:df:e5:64:43:
         2d:e9:d5:39:ab:d2:a2:df:b7:8b:d0:c0:80:19:1c:45:c0:2d:
         8c:e8:f8:2d:a4:74:56:49:c5:05:b5:4f:15:de:6e:44:78:39:
         87:a8:7e:bb:f3:79:18:91:bb:f4:6f:9d:c1:f0:8c:35:8c:5d:
         01:fb:c3:6d:b9:ef:44:6d:79:46:31:7e:0a:fe:a9:82:c1:ff:
         ef:ab:6e:20:c4:50:c9:5f:9d:4d:9b:17:8c:0c:e5:01:c9:a0:
         41:6a:73:53:fa:a5:50:b4:6e:25:0f:fb:4c:18:f4:fd:52:d9:
         8e:69:b1:e8:11:0f:de:88:d8:fb:1d:49:f7:aa:de:95:cf:20:
         78:c2:60:12:db:25:40:8c:6a:fc:7e:42:38:40:64:12:f7:9e:
         81:e1:93:2e

输出中是没有X509v3 Authority Key Identifier的字段的,根据RFC3280的定义,只有根证书允许省略这个字段:

The keyIdentifier field of the authorityKeyIdentifier extension MUST
be included in all certificates generated by conforming CAs to
facilitate certification path construction.  There is one exception;
where a CA distributes its public key in the form of a "self-signed"
certificate, the authority key identifier MAY be omitted. 

所以这是一个自签名的根证书。当系统信任了根证书,那么根证书链下的所有子证书都会被认为合法,证书链的概念下面细说。Charles的抓包功能是通过中间人攻击来完成的,根证书被信任,那么Charles只要用一个子证书来欺骗被抓包的客户端,就能绕过TLS对中间人攻击的防护。
在Android 6.0之前,用户信任的CA证书会被认为是合法的,安装一个根证书就能解密所有App的HTTPS流量,6.0之后加了个限制,只有系统内置的CA证书被信任,开发中的应用需要手动信任用户添加的CA证书才行,这样就只有手动信任了证书的应用流量能被解密:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">example.com</domain>
        <trust-anchors>
            <certificates src="@raw/my_ca"/>
        </trust-anchors>
    </domain-config>
</network-security-config>

那么有没有在高版本Android上全局抓包的方法呢?还真有,对于ROOT过的手机,我们可以导出.0格式的证书,放到系统的/etc/security/cacerts/目录下,这样就会被认为是系统内置CA证书,从而达到抓取所有应用HTTPS流量的目的。

TLS与证书

在讲本文的主题SSL Pinning之前,需要介绍一些背景知识,下面几节解释了TLS握手过程,以及证书的校验过程。

TLS握手

这里又要拿出Cloudflare的经典图了:
SSL
这里描述的是TLS1.3之前的情况,细节不再赘述,可以看我之前的文章:TLS握手过程,我们只需要注意其中一个重点:服务端会把自己的证书发送给客户端,而证书中包含了公钥信息。

证书校验

那么客户端得到证书是如何校验这个证书是合法的呢?这里就要引入证书链的概念,以Google为例:
Google Certs
可以看到,这里一共是有三级证书的,自底向上分别被称为最终实体证书,中间证书和根证书。把三个证书拖出来,分别存成文件:
Google Certs List
证书都是cer后缀,我们尝试用DER解码,先看看最终实体证书:

openssl x509 -text -inform der -in ~/Downloads/\*.google.com.cer

输出如下信息:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            31:79:87:25:0f:c0:be:e8:08:00:00:00:00:56:05:ed
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=US, O=Google Trust Services, CN=GTS CA 1O1
        Validity
            Not Before: Aug 26 08:08:49 2020 GMT
            Not After : Nov 18 08:08:49 2020 GMT
        Subject: C=US, ST=California, L=Mountain View, O=Google LLC, CN=*.google.com
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:8e:14:e9:f8:bb:ae:1f:c4:64:53:b7:d6:7a:76:
                    50:8b:ab:05:c6:2e:71:32:e0:3e:db:ef:1e:5a:34:
                    43:a4:74:6a:2b:52:38:75:03:f0:2d:fa:e6:da:82:
                    10:92:53:9b:a0:0e:28:ea:61:68:2b:0c:6d:df:22:
                    da:5f:14:1b:90
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage:
                TLS Web Server Authentication
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Subject Key Identifier:
                96:65:7B:C2:08:15:03:E1:C3:F8:50:DD:8F:B6:73:65:43:DF:8C:80
            X509v3 Authority Key Identifier:
                keyid:98:D1:F8:6E:10:EB:CF:9B:EC:60:9F:18:90:1B:A0:EB:7D:09:FD:2B

            Authority Information Access:
                OCSP - URI:http://ocsp.pki.goog/gts1o1core
                CA Issuers - URI:http://pki.goog/gsr2/GTS1O1.crt

            X509v3 Subject Alternative Name:
                DNS:*.google.com, DNS:*.android.com, DNS:*.appengine.google.com, DNS:*.bdn.dev, DNS:*.cloud.google.com, DNS:*.crowdsource.google.com, DNS:*.datacompute.google.com, DNS:*.g.co, DNS:*.gcp.gvt2.com, DNS:*.gcpcdn.gvt1.com, DNS:*.ggpht.cn, DNS:*.gkecnapps.cn, DNS:*.google-analytics.com, DNS:*.google.ca, DNS:*.google.cl, DNS:*.google.co.in, DNS:*.google.co.jp, DNS:*.google.co.uk, DNS:*.google.com.ar, DNS:*.google.com.au, DNS:*.google.com.br, DNS:*.google.com.co, DNS:*.google.com.mx, DNS:*.google.com.tr, DNS:*.google.com.vn, DNS:*.google.de, DNS:*.google.es, DNS:*.google.fr, DNS:*.google.hu, DNS:*.google.it, DNS:*.google.nl, DNS:*.google.pl, DNS:*.google.pt, DNS:*.googleadapis.com, DNS:*.googleapis.cn, DNS:*.googlecnapps.cn, DNS:*.googlecommerce.com, DNS:*.googlevideo.com, DNS:*.gstatic.cn, DNS:*.gstatic.com, DNS:*.gstaticcnapps.cn, DNS:*.gvt1.com, DNS:*.gvt2.com, DNS:*.metric.gstatic.com, DNS:*.urchin.com, DNS:*.url.google.com, DNS:*.wear.gkecnapps.cn, DNS:*.youtube-nocookie.com, DNS:*.youtube.com, DNS:*.youtubeeducation.com, DNS:*.youtubekids.com, DNS:*.yt.be, DNS:*.ytimg.com, DNS:android.clients.google.com, DNS:android.com, DNS:developer.android.google.cn, DNS:developers.android.google.cn, DNS:g.co, DNS:ggpht.cn, DNS:gkecnapps.cn, DNS:goo.gl, DNS:google-analytics.com, DNS:google.com, DNS:googlecnapps.cn, DNS:googlecommerce.com, DNS:source.android.google.cn, DNS:urchin.com, DNS:www.goo.gl, DNS:youtu.be, DNS:youtube.com, DNS:youtubeeducation.com, DNS:youtubekids.com, DNS:yt.be
            X509v3 Certificate Policies:
                Policy: 2.23.140.1.2.2
                Policy: 1.3.6.1.4.1.11129.2.5.3

            X509v3 CRL Distribution Points:

                Full Name:
                  URI:http://crl.pki.goog/GTS1O1core.crl

            1.3.6.1.4.1.11129.2.4.2:
.v.....7~.b....a...{7.V..&[...K.ATn...t*........G0E. .i...V.i.U....g..}"..d.6.../R.V+.!..X..#.....S.}..7../.l.V=G....d....GF0M..j-u....~f.HZ
    Signature Algorithm: sha256WithRSAEncryption
         2f:de:47:43:cd:2d:0a:ed:6f:6d:3c:4b:39:0e:e6:05:17:74:
         58:a7:33:f0:a1:10:0a:52:94:55:80:52:8a:5c:a0:88:73:35:
         55:cd:d9:51:72:de:c2:96:5c:52:83:f2:ca:05:a1:72:60:06:
         8e:da:4d:80:05:6a:60:fe:60:ab:cc:dc:02:67:84:41:47:cd:
         eb:af:80:6b:ec:d5:0d:6e:56:5a:bd:00:47:d8:62:2f:4c:01:
         93:76:10:bb:16:15:ca:d4:d9:b2:92:0e:5d:96:56:06:95:c3:
         a6:d6:77:fb:97:b6:2f:66:06:7c:0c:21:91:ac:8c:84:16:61:
         40:02:a9:f1:ca:62:e3:e0:72:da:7b:ab:3f:64:27:bb:d0:ff:
         de:a0:c4:6d:a3:72:1d:bc:0e:1d:a7:6a:07:15:69:70:aa:63:
         d2:68:ed:50:d2:44:c4:21:ca:b4:ec:73:0b:0c:b2:86:17:fa:
         cd:4a:ca:57:2c:56:9d:17:10:0e:68:ce:6d:e1:00:d4:65:f1:
         11:63:9f:e4:07:d9:fb:eb:36:7e:77:bc:94:a3:c5:04:8c:ca:
         fa:ec:7a:a3:33:fb:b1:65:82:d0:2b:e7:02:29:f9:c4:91:da:
         3e:62:3e:8a:da:29:c2:91:bb:60:cf:d6:d2:f4:5b:a5:19:37:
         b1:ae:b8:7e

信息量非常大,我们关注几个关键字段:
Signature Algorithm: sha256WithRSAEncryption表示证书的签名是先使用SHA256做摘要,再对摘要做RSA加密生成的。
Public Key Algorithm: id-ecPublicKey,表示公钥的算法是ECDSA,这是一个ECC证书,相比RSA算法,ECDSA的证书更小,运算也更快。
X509v3 Authority Key Identifier,不需要关心内容,有这个字段表示这不是自签名的根证书。
X509v3 Subject Alternative Name,这里包含了证书适用的域名。
最后还有一段Signature Algorithm,这就是证书的签名,配合前面的证书签名算法可以完成证书链的校验。
中间证书的结构大同小异,这里列出用于校验的关键信息:

Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:d0:18:cf:45:d4:8b:cd:d3:9c:e4:40:ef:7e:b4:
                    dd:69:21:1b:c9:cf:3c:8e:4c:75:b9:0f:31:19:84:
                    3d:9e:3c:29:ef:50:0d:10:93:6f:05:80:80:9f:2a:
                    a0:bd:12:4b:02:e1:3d:9f:58:16:24:fe:30:9f:0b:
                    74:77:55:93:1d:4b:f7:4d:e1:92:82:10:f6:51:ac:
                    0c:c3:b2:22:94:0f:34:6b:98:10:49:e7:0b:9d:83:
                    39:dd:20:c6:1c:2d:ef:d1:18:61:65:e7:23:83:20:
                    a8:23:12:ff:d2:24:7f:d4:2f:e7:44:6a:5b:4d:d7:
                    50:66:b0:af:9e:42:63:05:fb:e0:1c:c4:63:61:af:
                    9f:6a:33:ff:62:97:bd:48:d9:d3:7c:14:67:dc:75:
                    dc:2e:69:e8:f8:6d:78:69:d0:b7:10:05:b8:f1:31:
                    c2:3b:24:fd:1a:33:74:f8:23:e0:ec:6b:19:8a:16:
                    c6:e3:cd:a4:cd:0b:db:b3:a4:59:60:38:88:3b:ad:
                    1d:b9:c6:8c:a7:53:1b:fc:bc:d9:a4:ab:bc:dd:3c:
                    61:d7:93:15:98:ee:81:bd:8f:e2:64:47:20:40:06:
                    4e:d7:ac:97:e8:b9:c0:59:12:a1:49:25:23:e4:ed:
                    70:34:2c:a5:b4:63:7c:f9:a3:3d:83:d1:cd:6d:24:
                    ac:07
                Exponent: 65537 (0x10001)

这一段给出了中间证书的公钥,因为是RSA算法,所以这里有一个Modulus和一个Exponent。最终实体证书的签名是用中间证书的私钥对最终实体证书的摘要加密得到,所以对应的解密过程是用中间证书的公钥解密最终实体证书的签名,能得出最终实体证书的摘要,如果能解密成功,就能确认最终实体证书确实是由中间证书签名的,再对最终实体证书做一次摘要,和解密出得摘要比对,如果一致即可确认证书没用被篡改过。
我们手动实现一下这个过程:
已知指数,模和密文,我们需要还原出原文,根据RSA算法,计算原文的算法如下,其中m是明文,c是密文,e是指数,n是模:

m = c ^ e (mod n)

代入上面证书里的值:

>>> n = 0x00d018cf45d48bcdd39ce440ef7eb4dd69211bc9cf3c8e4c75b90f3119843d9e3c29ef500d10936f0580809f2aa0bd124b02e13d9f581624fe309f0b747755931d4bf74de1928210f651ac0cc3b222940f346b981049e70b9d8339dd20c61c2defd1186165e7238320a82312ffd2247fd42fe7446a5b4dd75066b0af9e426305fbe01cc46361af9f6a33ff6297bd48d9d37c1467dc75dc2e69e8f86d7869d0b71005b8f131c23b24fd1a3374f823e0ec6b198a16c6e3cda4cd0bdbb3a4596038883bad1db9c68ca7531bfcbcd9a4abbcdd3c61d7931598ee81bd8fe264472040064ed7ac97e8b9c05912a1492523e4ed70342ca5b4637cf9a33d83d1cd6d24ac07
>>> e = 65537
>>> c = 0x2fde4743cd2d0aed6f6d3c4b390ee605177458a733f0a1100a52945580528a5ca088733555cdd95172dec2965c5283f2ca05a17260068eda4d80056a60fe60abccdc0267844147cdebaf806becd50d6e565abd0047d8622f4c01937610bb1615cad4d9b2920e5d96560695c3a6d677fb97b62f66067c0c2191ac8c8416614002a9f1ca62e3e072da7bab3f6427bbd0ffdea0c46da3721dbc0e1da76a07156970aa63d268ed50d244c421cab4ec730b0cb28617facd4aca572c569d17100e68ce6de100d465f111639fe407d9fbeb367e77bc94a3c5048ccafaec7aa333fbb16582d02be70229f9c491da3e623e8ada29c291bb60cfd6d2f45ba51937b1aeb87e
>>> pow(c, e, n)
986236757547332986472011617696226561292849812918563355472727826767720188564083584387121625107510786855734801053524719833194566624465665316622563244215340671405971599343902468620306327831715457360719532421388780770165778156818229863337344187575566725786793391480600129482653072861971002459947277805295727097226389568776499707662505334062639449916265137796823793276300221537201727072401742985542559596685092673521228140822200236743113743661549252453726123450722876929538747702356573783116197523966334991563351853851212597377279504828784763247643211048750059383511539240076118611220389103205312907651075225923933445
>>> print('0x%x' % pow(c, e, n))
0x1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003031300d060960864801650304020105000420cd55ba8e69bcc9a1c2aaba552982abd5519051f5708cb6f9885cd3a7d28cd505

最后这一段密文就是最终实体证书的摘要,我们再用openssl手动算一次摘要,看是否一致。先把证书转换成ANS.1格式,这是一种和protobuffer很相似的描述,都是tag, length, value的形式。

openssl asn1parse -inform der -in ~/Downloads/\*.google.com.cer

输出如下:

    0:d=0  hl=4 l=2416 cons: SEQUENCE
    4:d=1  hl=4 l=2136 cons: SEQUENCE
    8:d=2  hl=2 l=   3 cons: cont [ 0 ]
   10:d=3  hl=2 l=   1 prim: INTEGER           :02
   13:d=2  hl=2 l=  16 prim: INTEGER           :317987250FC0BEE808000000005605ED
   31:d=2  hl=2 l=  13 cons: SEQUENCE
   33:d=3  hl=2 l=   9 prim: OBJECT            :sha256WithRSAEncryption
   44:d=3  hl=2 l=   0 prim: NULL
   46:d=2  hl=2 l=  66 cons: SEQUENCE
   48:d=3  hl=2 l=  11 cons: SET
   50:d=4  hl=2 l=   9 cons: SEQUENCE
   52:d=5  hl=2 l=   3 prim: OBJECT            :countryName
   57:d=5  hl=2 l=   2 prim: PRINTABLESTRING   :US
   61:d=3  hl=2 l=  30 cons: SET
   63:d=4  hl=2 l=  28 cons: SEQUENCE
   65:d=5  hl=2 l=   3 prim: OBJECT            :organizationName
   70:d=5  hl=2 l=  21 prim: PRINTABLESTRING   :Google Trust Services
   93:d=3  hl=2 l=  19 cons: SET
   95:d=4  hl=2 l=  17 cons: SEQUENCE
   97:d=5  hl=2 l=   3 prim: OBJECT            :commonName
  102:d=5  hl=2 l=  10 prim: PRINTABLESTRING   :GTS CA 1O1
  114:d=2  hl=2 l=  30 cons: SEQUENCE
  116:d=3  hl=2 l=  13 prim: UTCTIME           :200826080849Z
  131:d=3  hl=2 l=  13 prim: UTCTIME           :201118080849Z
  146:d=2  hl=2 l= 102 cons: SEQUENCE
  148:d=3  hl=2 l=  11 cons: SET
  150:d=4  hl=2 l=   9 cons: SEQUENCE
  152:d=5  hl=2 l=   3 prim: OBJECT            :countryName
  157:d=5  hl=2 l=   2 prim: PRINTABLESTRING   :US
  161:d=3  hl=2 l=  19 cons: SET
  163:d=4  hl=2 l=  17 cons: SEQUENCE
  165:d=5  hl=2 l=   3 prim: OBJECT            :stateOrProvinceName
  170:d=5  hl=2 l=  10 prim: PRINTABLESTRING   :California
  182:d=3  hl=2 l=  22 cons: SET
  184:d=4  hl=2 l=  20 cons: SEQUENCE
  186:d=5  hl=2 l=   3 prim: OBJECT            :localityName
  191:d=5  hl=2 l=  13 prim: PRINTABLESTRING   :Mountain View
  206:d=3  hl=2 l=  19 cons: SET
  208:d=4  hl=2 l=  17 cons: SEQUENCE
  210:d=5  hl=2 l=   3 prim: OBJECT            :organizationName
  215:d=5  hl=2 l=  10 prim: PRINTABLESTRING   :Google LLC
  227:d=3  hl=2 l=  21 cons: SET
  229:d=4  hl=2 l=  19 cons: SEQUENCE
  231:d=5  hl=2 l=   3 prim: OBJECT            :commonName
  236:d=5  hl=2 l=  12 prim: UTF8STRING        :*.google.com
  250:d=2  hl=2 l=  89 cons: SEQUENCE
  252:d=3  hl=2 l=  19 cons: SEQUENCE
  254:d=4  hl=2 l=   7 prim: OBJECT            :id-ecPublicKey
  263:d=4  hl=2 l=   8 prim: OBJECT            :prime256v1
  273:d=3  hl=2 l=  66 prim: BIT STRING
  341:d=2  hl=4 l=1799 cons: cont [ 3 ]
  345:d=3  hl=4 l=1795 cons: SEQUENCE
  349:d=4  hl=2 l=  14 cons: SEQUENCE
  351:d=5  hl=2 l=   3 prim: OBJECT            :X509v3 Key Usage
  356:d=5  hl=2 l=   1 prim: BOOLEAN           :255
  359:d=5  hl=2 l=   4 prim: OCTET STRING      [HEX DUMP]:03020780
  365:d=4  hl=2 l=  19 cons: SEQUENCE
  367:d=5  hl=2 l=   3 prim: OBJECT            :X509v3 Extended Key Usage
  372:d=5  hl=2 l=  12 prim: OCTET STRING      [HEX DUMP]:300A06082B06010505070301
  386:d=4  hl=2 l=  12 cons: SEQUENCE
  388:d=5  hl=2 l=   3 prim: OBJECT            :X509v3 Basic Constraints
  393:d=5  hl=2 l=   1 prim: BOOLEAN           :255
  396:d=5  hl=2 l=   2 prim: OCTET STRING      [HEX DUMP]:3000
  400:d=4  hl=2 l=  29 cons: SEQUENCE
  402:d=5  hl=2 l=   3 prim: OBJECT            :X509v3 Subject Key Identifier
  407:d=5  hl=2 l=  22 prim: OCTET STRING      [HEX DUMP]:041496657BC2081503E1C3F850DD8FB6736543DF8C80
  431:d=4  hl=2 l=  31 cons: SEQUENCE
  433:d=5  hl=2 l=   3 prim: OBJECT            :X509v3 Authority Key Identifier
  438:d=5  hl=2 l=  24 prim: OCTET STRING      [HEX DUMP]:3016801498D1F86E10EBCF9BEC609F18901BA0EB7D09FD2B
  464:d=4  hl=2 l= 104 cons: SEQUENCE
  466:d=5  hl=2 l=   8 prim: OBJECT            :Authority Information Access
  476:d=5  hl=2 l=  92 prim: OCTET STRING      [HEX DUMP]:305A302B06082B06010505073001861F687474703A2F2F6F6373702E706B692E676F6F672F677473316F31636F7265302B06082B06010505073002861F687474703A2F2F706B692E676F6F672F677372322F475453314F312E637274
  570:d=4  hl=4 l=1218 cons: SEQUENCE
  574:d=5  hl=2 l=   3 prim: OBJECT            :X509v3 Subject Alternative Name
  579:d=5  hl=4 l=1209 prim: OCTET STRING      [HEX DUMP]:308204B5820C2A2E676F6F676C652E636F6D820D2A2E616E64726F69642E636F6D82162A2E617070656E67696E652E676F6F676C652E636F6D82092A2E62646E2E64657682122A2E636C6F75642E676F6F676C652E636F6D82182A2E63726F7764736F757263652E676F6F676C652E636F6D82182A2E64617461636F6D707574652E676F6F676C652E636F6D82062A2E672E636F820E2A2E6763702E677674322E636F6D82112A2E67637063646E2E677674312E636F6D820A2A2E67677068742E636E820E2A2E676B65636E617070732E636E82162A2E676F6F676C652D616E616C79746963732E636F6D820B2A2E676F6F676C652E6361820B2A2E676F6F676C652E636C820E2A2E676F6F676C652E636F2E696E820E2A2E676F6F676C652E636F2E6A70820E2A2E676F6F676C652E636F2E756B820F2A2E676F6F676C652E636F6D2E6172820F2A2E676F6F676C652E636F6D2E6175820F2A2E676F6F676C652E636F6D2E6272820F2A2E676F6F676C652E636F6D2E636F820F2A2E676F6F676C652E636F6D2E6D78820F2A2E676F6F676C652E636F6D2E7472820F2A2E676F6F676C652E636F6D2E766E820B2A2E676F6F676C652E6465820B2A2E676F6F676C652E6573820B2A2E676F6F676C652E6672820B2A2E676F6F676C652E6875820B2A2E676F6F676C652E6974820B2A2E676F6F676C652E6E6C820B2A2E676F6F676C652E706C820B2A2E676F6F676C652E707482122A2E676F6F676C656164617069732E636F6D820F2A2E676F6F676C65617069732E636E82112A2E676F6F676C65636E617070732E636E82142A2E676F6F676C65636F6D6D657263652E636F6D82112A2E676F6F676C65766964656F2E636F6D820C2A2E677374617469632E636E820D2A2E677374617469632E636F6D82122A2E67737461746963636E617070732E636E820A2A2E677674312E636F6D820A2A2E677674322E636F6D82142A2E6D65747269632E677374617469632E636F6D820C2A2E75726368696E2E636F6D82102A2E75726C2E676F6F676C652E636F6D82132A2E776561722E676B65636E617070732E636E82162A2E796F75747562652D6E6F636F6F6B69652E636F6D820D2A2E796F75747562652E636F6D82162A2E796F7574756265656475636174696F6E2E636F6D82112A2E796F75747562656B6964732E636F6D82072A2E79742E6265820B2A2E7974696D672E636F6D821A616E64726F69642E636C69656E74732E676F6F676C652E636F6D820B616E64726F69642E636F6D821B646576656C6F7065722E616E64726F69642E676F6F676C652E636E821C646576656C6F706572732E616E64726F69642E676F6F676C652E636E8204672E636F820867677068742E636E820C676B65636E617070732E636E8206676F6F2E676C8214676F6F676C652D616E616C79746963732E636F6D820A676F6F676C652E636F6D820F676F6F676C65636E617070732E636E8212676F6F676C65636F6D6D657263652E636F6D8218736F757263652E616E64726F69642E676F6F676C652E636E820A75726368696E2E636F6D820A7777772E676F6F2E676C8208796F7574752E6265820B796F75747562652E636F6D8214796F7574756265656475636174696F6E2E636F6D820F796F75747562656B6964732E636F6D820579742E6265
 1792:d=4  hl=2 l=  33 cons: SEQUENCE
 1794:d=5  hl=2 l=   3 prim: OBJECT            :X509v3 Certificate Policies
 1799:d=5  hl=2 l=  26 prim: OCTET STRING      [HEX DUMP]:30183008060667810C010202300C060A2B06010401D679020503
 1827:d=4  hl=2 l=  51 cons: SEQUENCE
 1829:d=5  hl=2 l=   3 prim: OBJECT            :X509v3 CRL Distribution Points
 1834:d=5  hl=2 l=  44 prim: OCTET STRING      [HEX DUMP]:302A3028A026A0248622687474703A2F2F63726C2E706B692E676F6F672F475453314F31636F72652E63726C
 1880:d=4  hl=4 l= 260 cons: SEQUENCE
 1884:d=5  hl=2 l=  10 prim: OBJECT            :1.3.6.1.4.1.11129.2.4.2
 1896:d=5  hl=3 l= 245 prim: OCTET STRING      [HEX DUMP]:0481F200F0007600B21E05CC8BA2CD8A204E8766F92BB98A2520676BDAFA70E7B249532DEF8B905E000001742A06F9BD000004030047304502205BB262C173701DC2F4D182C34760FA693875B409B650DA2DBE966D80CB6EE9C8022100CFD52D39644158ED44F23ABE9B4746304D8CAB6A2D75DA92F0187E6688485A0D007600E712F2B0377E1A62FB8EC90C6184F1EA7B37CB561D11265BF3E0F34BF241546E000001742A06F9D2000004030047304502200B69DB8E9756FB698955FA04BF8467C80E7D22C2F364CD36DACDD72F52D1562B022100925882AA2314AAB3009F53A47D93CE377FCB2FCA6C1E563D4716ACEBF264E087
 2144:d=1  hl=2 l=  13 cons: SEQUENCE
 2146:d=2  hl=2 l=   9 prim: OBJECT            :sha256WithRSAEncryption
 2157:d=2  hl=2 l=   0 prim: NULL
 2159:d=1  hl=4 l= 257 prim: BIT STRING

再看看RFC5280的定义:

Certificate  ::=  SEQUENCE  {
        tbsCertificate       TBSCertificate,
        signatureAlgorithm   AlgorithmIdentifier,
        signatureValue       BIT STRING  }

   TBSCertificate  ::=  SEQUENCE  {
        version         [0]  EXPLICIT Version DEFAULT v1,
        serialNumber         CertificateSerialNumber,
        signature            AlgorithmIdentifier,
        issuer               Name,
        validity             Validity,
        subject              Name,
        subjectPublicKeyInfo SubjectPublicKeyInfo,
        issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
                             -- If present, version MUST be v2 or v3

第二行就是TBSCertificate的位置和长度,真正用来计算摘要的部分,偏移是4,hl=4表示header长度是4,l=2136表示内容长度是2136,总长度是2140,我们用dd提取出这部分,并对输出结果做SHA256摘要:

dd if=/Users/shunix/Downloads/\*.google.com.cer of=/Users/shunix/tmp/tbs skip=4 bs=1 count=2140
openssl dgst -sha256 ~/tmp/tbs

输出如下:

SHA256(/Users/shunix/tmp/tbs)= cd55ba8e69bcc9a1c2aaba552982abd5519051f5708cb6f9885cd3a7d28cd505

而上面我们用公钥解出来的摘要去掉前面的

0x1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003031300d060960864801650304020105000420

最后结果也是cd55ba8e69bcc9a1c2aaba552982abd5519051f5708cb6f9885cd3a7d28cd505,二者完全一致,说明最终实体证书没有被篡改过。
而中间证书和根证书的校验也是同样的过程,这里就不赘述。根证书是自签名的,甚至都可以不用签名,因为根证书是内置到系统里的。
CA之所以不直接从根证书发布最终实体证书是因为这样风险太大,一旦出现错误发布容易导致根证书不受信任,所以CA会从根证书发布中间证书,再从中间证书发布最终实体证书,完成自我隔离。当然,根证书也是有不被信任的先例的,比如Firefox和Chrome在2015年移除了对CNNIC根证书的信任,具体原因就不细讲了。

SSL Pinning

综上所述,有几种情况SSL是无法保证安全的:

  1. 某些wifi或者网站要求装个根证书才能继续使用,比如某订票网站
  2. 被信任的CA随意发布证书,比如赛门铁克,17年居然发布了example.com的证书,挺缺德的

所以客户端提供了一种额外的机制来保证HTTPS通信的安全,SSL Pinning,SSL Pinning又可以细分为Certificate Pinning和Public Key Pinning。

Certificate Pinning

Certificate Pinning也就是证书锁定,简单来说就是把证书文件打包进安装包,通过加载本地证书自定义TrustManager,进而创建自定义的SSLSocketFactory来完成的,这里贴一些关键代码:

fun loadCertificate(): Certificate {
    // 假设证书放在assets下
    return BaseApplication.instance.assets
        .open("shunix.cert").use { input ->
        CertificateFactory.getInstance("X.509").generateCertificate(input)
    }
}

fun getTrustManager(): TrustManager {
    val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply {
        setCertificateEntry("shunix", loadCertificate())
    }
    return TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).run {
        init(keyStore)
        trustManagers[0]
    }
}

fun getSSLSocketFactory(): SSLSocketFactory {
    val sslContext = SSLContext.getInstance("TLS").apply {
        init(null, arrayOf(getTrustManager()), null)
    }
    return sslContext.socketFactory
}

fallback策略

内置证书的方案存在一个问题,证书是会过期的,一般最终实体证书也就是一年的有效期,而中间证书和根证书有效期都是10年左右,超过绝大多数应用的生命周期了,所以一个比较简单的做法是同时内置中间证书或者根证书作为子证书过期的应对方案。中间证书和根证书一般都是同一个机构的,所以选哪一个并没有本质的区别,但是这样在安全性上会有一定的妥协,可以参考前面提到的赛门铁克和CNNIC证书,当根证书不被信任,一样会出问题,还有就是如果更换证书的CA也是做不到的,局限性很大。
那么能不能只内置最终实体证书,同时能解决有效期的问题呢?很自然地会想到动态更新本地证书,那就带来了另一个问题,如何保证本地证书更新的安全性呢?这是一个鸡生蛋蛋生鸡的问题,其实不讲究点,更新证书的请求可以直接走没有证书锁定的HTTPS完成,因为HTTPS被劫持本身就是小概率情况。如果追求完美的话,有一个比较麻烦的解决方案,就是再打包一个自签名的证书到安装包,锁定这个自签名的证书来更新最终实体证书,自签名证书有效期可以自定义,定的足够长就好,当然服务端也许要做相应改造,是否需要这么严格的安全策略就看业务场景了。

Public Key Pinning

Certificate Pinning实现过于繁琐,同时局限性比较大,所以就有了锁定Subject Public Key Info的实现。申请过证书的都知道,需要提供算法和公钥,即使更换新证书,这两个东西也是可以保持不变的,Android 7.0以上提供了非常方便的实现,只需要在res/xml/network_security_config.xml里加如下配置:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">example.com</domain>
        <pin-set expiration="2018-01-01">
            <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
            <!-- backup pin -->
            <pin digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=</pin>
        </pin-set>
    </domain-config>
</network-security-config>

可以参考Android官方文档:https://developer.android.com/training/articles/security-config#CertificatePinning
这里pin的是Base64编码后的Subject Public Key Info(SPKI)的哈希,具体生成方法可以参考Mozilla的文档:https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning#Extracting_the_Base64_encoded_public_key_information

fallback策略

虽然基于公钥的锁定不存在证书过期的问题,但依然需要fallback策略,因为可能存在私钥泄漏的情况下,这种情况下需要重新发布证书,公钥私钥都会改变。Android允许pin多个SPKI,只要符合一个就能正常通信,所以这里可以pin几个CA的根证书或者中间证书的SPKI,会损失一点安全性,但是为私钥泄漏的情况留下了操作空间。

结语

即使做了SSL Pinning,依然有HTTPS被劫持,内容被篡改的可能,比如对于证书锁定的方式,可以直接替换掉apk里的证书,再做重打包,对于公钥锁定的方式,可以通过hook RootTrustManager的方式直接绕过验证。
服务端需要在业务上对客户端请求做验证,只依靠机制上的安全是不够的,道高一尺魔高一丈,客户端和服务端应该互不信任,对于输入需要做足够的校验,这并不是一种overhead。