iOS中的HTTPS认证

一、HTTPS认证

1. 会话认证机制

iOS 中会话认证机制共有四种,大体分为两种类型:

  1. 单向认证双向认证;
  2. NTLM 和 Kerberos;

枚举类如下:

会话认证机制
  • 单向认证
    指客户端验证服务端的身份,服务端不验证客户端身份;

  • 双向认证
    指客户端验证服务端身份,服务端也认证客户端身份,未通过认证的客户端在握手阶段直接断开连接,禁止访问服务器;

  • NTLM 和 Kerberos
    用于早期的 Windows 中,不做深入研究;

说明:
HTTPS 通信中一般都是单向认证,这样可以保证数据的加密传输,也能够防止没有证书的钓鱼网站。而双向认证一般用于企业来禁止接口被第三方调用和解析。iOS 中的 NSURLSession 的默认实现、AFN 的默认实现都是单向认证,此时代理方法只会受到一种类型的回调,即: NSURLAuthenticationMethodServerTrust

双向认证建立在单向认证的基础上,需要自己去额外实现:

  1. 客户端保存、读取并将证书发送给服务端;
  2. 服务端对证书实现自己的校验逻辑;

双向认证中代理方法会收到两个类型的回调:

  1. NSURLAuthenticationMethodServerTrust :服务端发送证书给客户端,客户端进行证书校验,默认操作是使用跟证书(CA证书)进行合法校验;
  2. NSURLAuthenticationMethodClientCertificate :服务端要求客户端提供证书,此时客户端应将工程中的证书解析并传递给服务端。服务端一般也是做证书链校验,如果是有限访问,还会做证书主体的校验;

与会话层面的认证机制相对的是特殊任务认证机制:

特殊任务认证

这些枚举的意义是???(见后文)

2. SSL/TLS

TLS (Transport Layer Security)就是 SSL(Secure Socket Layer),只不过版本不同而已:

SSL和TLS

SSL 和 HTTPS 的概念等不再赘述;

3. 认证流程

HTTPS 中的单向认证流程图如下:

单向认证流程图

其具体的流程为:

  1. 客户端向服务端发送请求建立连接的请求,并且附上客户端支持的 SSL/TLS 协议版本、加密算法、摘要算法,并且附上一个随机数;
  2. 服务端从客户端支持的算法中选取一个回传给客户端,并且附上自己的证书,当然证书中包含签名、服务端公钥。如果是非 Root CA 签发的证书,一般会包含两个证书,一个是中间 CA 的证书(Root CA 签发给中间 CA),一个是中间 CA 签发给服务端的证书。另外,服务端也附上一个随机数;
  3. 无论单向认证还是双向认证,这一步都需要对服务端的证书进行 CA 验证,而这一步就是 AFN 和 NSURLSession 的主要功能点。证书包含 TBSCetificate、SignAlgorithm、SignValue,验签主要流程就是对证书使用 SignAlgorithm 进行加密,一般是对 TBSCetificate 进行摘要算法之后再 RSA,得到哈希值,对比证书中的 SignValue 是否一致;
  4. 客户端再次生成一个随机数(Premaster Random),至此,客户端已经知道了三个随机数,可以生成最终的会话秘钥了。如果是双向认证,这里需要客户端发送自己的证书到服务器;
  5. 客户端使用服务端证书中的公钥对第三个随机数进行加密,回传给服务端;
  6. 服务端使用私钥解密得到第三个随机数。至此,服务端也知道了这三个随机数,可以生成最终的会话秘钥了。如果是双向认证,这里服务器还需要对客户端传送过来的证书进行验证,验证错误可能会直接断开连接;
  7. 计算此前所有内容的握手消息 hash 值,并用"会话密钥"加密后发送至客户端用于验证,此时的加密方式一般是对称加密;
  8. 客户端解密并计算握手消息的 hash 值,如果与服务端发来的 hash 一致,此时握手过程结束,可以正式开始接受和发送内容了;

简化版流程图如下:

单向认证流程图

4. 各自使用场景

一般的 HTTPS 请求都采用单向认证,只有极少数对安全性要求很高的企业采用双向认证如金融企业,或者是企业对涉及到核心业务的接口采用双向认证;

单向认证中客户端认证服务端证书的特性就决定了,只要客户端愿意且服务端的证书正常,那么任意客户端都可以访问该服务器;

双向认证中,除了客户端对服务端进行认证,服务端还要对客户端进行认证。因此,服务端对客户端证书的认证逻辑就决定了客户端是被动的,能否访问该服务器完全由服务端决定。有的服务端只是单纯的对客户端证书进行 CA 认证,还有的会对证书的主体进行认证,非白名单内的主体不允许访问服务器;

NTLM 和 Kerberos 用于早期的 Windows 中,本文不做过多了解:

NTLM工作流程

5. 双向认证和白名单

双向认证和白名单有这相同的作用,就是服务端限制某些客户端的访问,白名单机制的存在有这更方面成熟的实现机制,可能这就是双向认证不常用的原因?

6. 证书体系的演变过程

  1. 签名的产生

是指使用某种算法计算出元数据的哈希值,以此确保元数据没有被篡改,最初的签名算法采用摘要算法,证书体系中的签名采用摘要算法+ 非对称加密的方式进行签名;

  1. 证书的产生

因为黑客可以修改元数据的同时修改摘要算法得出的哈希值,所以出现了证书,其目的是通过非对称加密来保证元数据的哈希值不被破解;

  1. 证书的组成

证书分为 TBSCertificate(To-Be-Signed) 和 Certificate,即待签名的证书和签名过的证书。TBSCertificate 证书中包含拥有者的各种信息,同时还包含拥有者的公钥。我们一般说的证书是经过签名的,这种证书中除了包含 TBSCertificate ,还会包含签名使用的算法、签名值;

非对称加密效率较低,所以证书的签名一般先对 TBSCertificate 使用摘要算法得到固定长度的哈希值,然后使用私钥对这个哈希值进行非对称加密,最终得到签名,所以证书体系中的签名已经不是单纯的摘要算法了;

  1. 证书的权威性隐患

最初的证书是自签名证书,拥有者使用自己的私钥对 TBSCertificate 进行签名,如果黑客攻击了证书发送者,并将证书上的公钥替换为自己的公钥,甚至直接将拥有者的私钥给窃取或者替换,那么接收者接收到的也是被篡改过的数据;

  1. Root CA 机构的产生

自签名证书中,私人的私钥安全性隐患很大,因此才有了权威的 CA 机构,证书体系中默认 CA 机构的安全性不会被破解,所以理论上 CA 的私钥不会被窃取。CA 机构会对个人信息进行验证,并且使用自己的私钥对 TBSCertificate 证书进行签名之后颁发给申请者;

CA 机构的私钥理论上绝对安全,公钥会被嵌入到计算机基础体系中,如被嵌入到操作系统、浏览器中,所以浏览器可以直接使用 CA 的公钥对证书进行验签;

验签的流程就是先提取签名时使用的签名算法(signatureAlgorithm),这里主要是要知道签名时摘要算法采用的哪一种。然后提取出 TBSCertificate,使用摘要算法对 TBSCertificate 进行哈希,得到 Hash1。接着,使用 CA 公钥对签名值(signatureValue)进行解密,得到 Hash2,如果 Hash1 = Hash2,则证书校验成功;

至此,CA + 非对称加密 + 摘要算法就组成了证书体系;

  1. intermediate CA 的产生

Root CA 基本上就那几个,他们的公钥会被嵌入到计算机基础体系中。如果直接使用 Root CA 的公钥进行签发,那么一旦 Root CA 的私钥发生变化,如撤销、过期等,牵连范围极大。

所以 intermediate CA 出现了, intermediate CA 作为 Root CA 的代理,先向 Root CA 申请证书,其流程和上面的基本一致, intermediate CA 将自己的信息和公钥发送给 Root CA ,Root CA 验证信息后颁发 TBSCertificate 并使用自己的私钥进行签名,最后颁发给 intermediate CA;

Root CA 只对 intermediate CA 颁发证书, intermediate CA 使用自己的私钥对申请者的证书进行签名,这样就实现了代理的作用;

一张图做总结:

证书体系

7. HTTPS 中的三个随机数

  1. 为什么要三个随机数?
  2. 为什么不直接使用 RSA 加密一个随机数?

首先,三个随机数的正常使用流程如下:

HTTPS流程
  1. Client_Hello 阶段,客户端明文传输 client_random 给服务端;
  2. Server_Hello 阶段,服务端明文传输 server_random 给客户端;
  3. Client_Hello_Done 阶段,客户端生成 premaster_random 并使用公钥加密传输给服务端;
  4. Server_Hello_Done 阶段,服务端使用私钥解密得到 premaster_random;

最终客户端和服务端都有三个随机数,然后根据三个随机数使用协议中的算法计算出一个 master_random,后续都使用 master_random 对报文进行对称加密之后传输;

这里有几个重点:

  1. 只用公钥加密 premaster_random ,那么只要 premaster_random被破解,整个过程就都被破解了;
  2. 既然 client_random 和 server_random 是明文的,那么 client_random 和 server_random 是否有必要?

要回答这两个问题,这里就涉及到 HTTPS 中计算对称加密最终 key 的两种算法:

  • 算法一:RSA 加密算法

如果只是用一个随机数,这个随机数由客户端生成之后,使用 RSA 加密之后传送给服务端,服务端和客户端都是用这个随机数作为对称加密的 key 进行对称加密,这种情况有两个问题:

  1. 伪随机性:伪随机数不一定随机,被猜出来之后就被破解了;
  2. 向前攻击:私钥被窃取之后,之前拦截的报文都可以被解密;

流程如下:

RSA加密

HTTPS 中的 RSA 加密,通过 premaster_random 这一个随机数来计算 master_random 的算法就是只使用一个随机数,并且使用 RSA 加密传输。

这种算法存在上述两个问题,因此,开发出了 DH 算法,现在 HTTPS 一般通过 DH 算法计算最后的 master_random;

破解的关键不是破解 RSA,这玩意在你拿不到私钥的情况下,如果量子计算机没被研发出来,你根本破解不了。因为 RSA 只能暴力枚举,根据 RSA 算法的规则,算力很强的计算机都要枚举 70 年。所以,破解的关键在于猜出这个对称加密的秘钥,也就是伪随机数;

  • 算法二:DH 加密算法
  1. 定义素数g、p(只有1和自身的因数)
    g和p双方都知道,这里假如g = 7, p = 5,实际上这两个值都是很大的。

  2. 双方计算大数 P = g^X mod p
    Server生成随机数X2,假设X2 = 5,P2 = 7^5 mod 5 = 2。
    Client生成随机数X1,假设X1 = 2,P1 = 7^2 mod 5 = 4。

  3. 交换P1 P2
    Client有p、g、X1、P2,Server有p、g、X2、P1。

  4. 计算密钥S
    利用S = g^X mod p 计算密钥,对应Client,S1 = P2^X1 mod p = 4,
    对应Server,S2 = P1^X2 mod p = 4,S1=S2恒成立。

很显然,除非中间人攻击,即便知道g、p,窃取到X1、X2,也很那倒推出来P1、P2,也就没法计算出最后密钥。

本质上还是借鉴了分解质因数只能暴力枚举的原理;

流程如下:


DH算法

DH 算法原理:https://blog.csdn.net/mrpre/article/details/52608867

如果不清楚上面的计算原理,只需要知道 DH 算法通过 3 个随机数来计算最终的 master_random 作为对称加密的 key,解决了伪随机数不一定随机的问题,还解决了 RSA 加密的向前攻击的问题;

为什么需要三个随机数,总结:

  1. 早期使用一个随机数,且采用 RSA 算法进行传输,存在两个严重的问题:伪随机数不一定随机,RSA 被破解后(伪随机数被猜出)存在向前攻击的漏洞(根据算法可以猜出上一个随机数);
  2. 为了解决上述问题,开发出了 DH 算法,该算法使用 3 个随机数解决了上述的两个问题,更加安全可靠(每次连接都是三个新的随机数);

为什么伪随机数不一定随机?只要这个随机数是由确定算法生成的,那就是伪随机,也就是有规则的随机。所以知道了算法,下一个随机数是可以被计算出来的。随机这个属性和算法本身就是矛盾的,DH 算法中增加三个随机数本质上只是增加随机数的复杂维度。

附-破解 HTTPS 的三种方法:

破解 HTTPS 的三种方法

8. 三次握手四次分手

TCP报文格式:

TCP报文

序号:seq,用来标识从TCP源端向目的端发送的字节流。tcp中传输数据时,会把数据中的每个字节用序号进行标志,确保数据按顺序传输

确认号:ack,小写的。只有ACK标志位为1时,确认序号字段才有效,ack=Seq+1。确认方Ack=发起方Req+1,无论哪一端的确认号都是如此。比如A向B发送建立连接的请求时,seq=x,B回复的报文中 ACK 标志位为1,且确认号就是ack=x+1,表示收到了序列号为x的报文。

标志位:表示报文的六种格式,为1时才有效,默认为0;注意,ACK是标志位,而ack是确认号。

(A)URG:紧急指针(urgent pointer)有效。

(B)ACK:确认序号有效。

(C)PSH:接收方应该尽快将这个报文交给应用层。

(D)RST:重置连接。

(E)SYN:发起一个新连接。

(F)FIN:释放一个连接。

  1. 客户端发送一个报文,SYN标志位被标志为1,表示这个报文的类型是请求建立链接。同时,生成一个序号seq=x,发送给服务器。因为这个seq是第一个序号,所以也叫做Initial Sequence Number,也就是ISN,ISN不能是一个固定的值,在建立连接时,两端都需要通过ISN算法来生成一个ISN并放在seq中,以此来防止ISN攻击。如果ISN固定,那么根据递进+length(携带数据时)或者+1(不携带数据时)的规律,攻击者很容易猜出后续的seq。最终的报文为:seq=x,SYN=1
  2. 服务器收到SYN=1的报文之后知道了客户端要建立连接,于是发送一个报文,SYN标志为1,ACK标志也为1,表示这个报文是对SYN(建立连接)进行应答。同时,因为ACK=1,需要设置确认序号,确认序号Ack=x+1,也就是客户端发送来的第一个报文的序号+1,以此来表示服务端收到了刚刚客户端所发送的序号为x(seq=x)的报文。另外,服务端也需要发送一个序号,序号是用来确保TCP数据按顺序传输的,此时服务端时第一次发送报文,所以也需要生成一个ISN,所以最终的报文为:seq=y,ack=x+1,另外标志位中ACK=1,SYN=1;
  3. 客户端接收到报文后,发送一个报文,ACK标志位设置为1,表示这个报文是对服务端上一个报文的应答,SYN设置为0,表示这已经是个应答包了,而不是请求建立连接的报文。于此同时,因为TCP中所有的数据都是按顺序传输的,正因为此时客户端和服务端发送的报文中都不包含length,也就是没有数据,所以此时seq仍然为x,ack=y+1,表示客户端接收到了服务端刚刚发送的序号为y(seq=y)的报文。最终报文为seq=x,ack=y+1,ACK=1,SYN=0(表示该位无效)
  4. 至此,完成了三次握手,开始正式发送数据,客户端再次发送一个报文,seq=x,lenght=50,ack=y+1,表示仍然是对序号为y的服务端报文的响应;

四次分手:

  1. 第一次挥手,客户端根据当前报文的序号,设置好 seq 和 ack,并且将 ACK 标志位设置为 1 表示该包为回包。另外,将 FIN 标志位设置为 1,表示断开连接。此时,客户端进入 FIN_WAIT_1 状态,表示客户端没有数据要发送给服务端了。
  2. 第二次挥手,服务端收到了客户端发送的 FIN 报文段,向客户端回了一个 ACK 报文段,表示收到上一个 FIN 的报文了;
  3. 第三次挥手,服务端所有数据发送完毕之后,向客户端发送 FIN 报文段,请求关闭连接,同时服务端进入 LAST_ACK 状态;
  4. 第四次挥手,客户端收到服务端发送的 FIN 报文段后,向服务端发送 ACK 报文段,然后客户端进入 TIME_WAIT 状态。服务端收到客户端的 ACK 报文段以后,就关闭连接。此时,客户端等待2MSL(指一个片段在网络中最大的存活时间)后依然没有收到回复,则说明服务端已经正常关闭,这样客户端就可以关闭连接了;

完整的通信流程:

TCP完整流程

9. 为什么握手需要三次分手需要四次

  • TCP中为什么握手需要三次
  1. 两次不够:两次请求时,server 和 client 各自发送和接收到一个报文。对于server而言,相当于接收到一个报文就从listen状态切换到开启状态。如果网络延迟,client发送了多次请求建立连接的报文,那么就会开启多个请求。另外,TCP是双向协议,如果此时就开启连接,如果server先发送数据,而client又没有接收到server的第一个回包,那么就只有server一方在发送数据,相当于一个人在电话的一头巴拉一大堆,但是对方信号已经中断,完全没听到。client完全接收不到;所以,在服务器接收到第三个报文之后才开启连接,允许访问,此时才是最合适的;
  2. 四次多了:三次够用,四次就多了;

总结:网络存在延迟、丢失的情况,两次握手会导致服务端开启多个无效连接,进而导致服务端在传送数据但是客户端并没有连接上的情况,而四次握手又多余了。

  • 为什么分手需要四次
  1. 客户端发送FIN报文意味着不再向server发送数据,但是还可以接受数据;
  2. 服务端在三次握手时,将ACK和SYN报文合并发送,而在四次分手时,因为服务端可能存在未发送的数据,所以ACK和 FIN 报文一般都分开发送,即在接收到client的FIN报文之后立马发送ACK,但是在数据传输完毕之后再发送 FIN 报文;

总结:客户端请求断开连接时,server有可能存在未发送完毕的数据,这种特性导致server将ACK和FIN报文分开发送,所以就会出现比三次握手时多一次的报文,三次握手时,server将ACK和SYN合并发送了;

  • HTTPS中的四次握手:
  1. 客户端请求并发送一个随机数;
  2. 服务端回应并发送证书、公钥和一个随机数;
  3. 客户端验证通过后,生成一个premaster_random,使用公钥加密之后传递给服务器,并根据 DH 算法计算出 master_key,此为客户端 finish 报文?
  4. 服务端接收到后用私钥解密得到premaster_random并根据 DH 算法生成master_random,hash 握手阶段所有报文之后使用秘钥加密传输给客户端,此为服务端 finish 报文;
  5. 客户端同样也 hash 所有报文之后使用秘钥进行对称加密,对比服务端给过来的值,如果一致则握手结束,如果不一致则断开连接;

结果:客户端和服务端的 master_random 是相同的,后面都使用这个数字作为对称加密的 key 来对数据进行对称加密之后进行传输;

二、ATS

1. ATS简介

ATS,即 App Transport Security,ATS 默认情况下要求 App 所有的请求都使用 HTTPS 连接,且对 HTTPS 认证中的摘要算法版本、对称算法版本等进行要求。Apple 利用自己的强势地位推动了客户端的安全性。

2. ATS 的使用

  1. 默认开启

如果不额外在 info.plist 中设置 ATS,那么就相当于开启 ATS。

此时,所有的连接/请求都会被 ATS 机制拦截并使用 ATS 中默认的设置来进行连接的认证和建立,符合 ATS 要求的请求才能够通过,否则会被拒绝,ATS 默认策略的检测包括且不限于:

  1. 证书链的验证(层层验签,最终使用系统中的跟证书对 intermediate CA 进行验签),这个应该对应代码中的 X509 认证模式;

  2. TLS 最低版本验证;

  3. 摘要算法最低版本验证;

  4. 默认会进行域名验证,域名认证就是对叶子节点的证书中的域名进行对比。AFN 早期版本默认是 X509 认证,即只验签,不认证域名,存在证书被劫持/替换的风险,该重大漏洞被曝出后 AFN 对其进行了修复;

  5. 默认策略没有锁定认证(SSL Pinning Mode),而 AFN 中对此模式进行了实现;

  6. ATS 的三个设置项

最常用的两个设置就是 NSAllowsArbitraryLoadsNSAllowsArbitraryLoadsInWebContent。两者组合时,会根据版本的不同(是否大于 iOS10)而有不同的表现。鉴于现在的 iOS9 版本已经很少了,可以直接使用 iOS10 的版本,两个设置的优先级:NSAllowsArbitraryLoadsInWebContent > NSAllowsArbitraryLoads。即设置了前者之后,后者会被忽略;

如果需要完全关闭 ATS,需要设置 NSAllowsArbitraryLoads = YES,并且在提交审核时进行说明。

对于浏览器类 App,如果只是允许浏览器中使用非安全的请求(HTTP),那么只需要设置 NSAllowsArbitraryLoadsInWebContent = YES 即可。

另外,还可以使用 NSExceptionDomains 来设置允许 HTTP 请求的白名单域名,这种设置相对于直接关闭 ATS 更容易过审,使用如下:

NSExceptionDomains

3. 验证域名是否符合 ATS

可以直接使用该网站来检测所请求的域名是否符合 ATS 标准:https://myssl.com/ats.html?domain=mobi.yocaigs.com&port=443

如图:

ATS检测

ATS 的具体要求可以再官方文档中查阅:https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW35

  • 总结

综上,如果公司使用的证书是 CA 申请下来的,且服务器端支持的加密算法符合 ATS 的要求,那么无论是 NSURLSession 还是 AFNetworking,都是可以直接进行网络请求的,Apple 已经完成了 HTTPS 中客户端对服务器证书的验证操作,不需要开发者额外实现而且安全性能够得到保证。

所以,ATS 是 Apple 对客户端请求的安全标准和实现(封装);

ATS(NSURLSession)中对 HTTPS 的默认实现采用的是单向认证策略,如果需要使用双向认证,则需要自己拦截 NSURLAuthenticationMethodClientCertificate 并提供客户端的证书;

三、证书锁定认证(SSL Pinning Mode)

1. 证书锁定认证简介

  • 概念
    证书锁定(SSL/TLS Pinning)顾名思义,将服务器提供的 SSL/TLS 证书内置到移动端开发的APP客户端中,当客户端发起请求时,通过比对内置的证书和服务器端证书的内容,以确定这个连接的合法性。

  • 产生原因
    因为 CA 证书签发机构也存在被黑客入侵的可能性,同时移动设备也面临内置证书被篡改的风险,或者是用户信任了不该信任的证书,这些情况都有可能导致证书链的不可靠。锁定认证模式本质是为了规避用户信任链不可靠带来的安全问题;

  • 原理
    我们需要将 APP 代码内置仅接受指定域名的证书,而不接受操作系统或浏览器内置的CA根证书对应的任何证书,通过这种授权方式,保障了 APP 与服务端通信的唯一性和安全性,因此我们移动端APP与服务端(例如 API 网关)之间的通信是可以保证绝对安全。

2. 使用证书签名进行证书锁定认证(Certificate Mode)

此种模式下,App 需要验证证书的签名,步骤如下:

  1. 服务端申请证书并将其内置到 App(或客户端) 中;
  2. 客户端在 HTTPS 认证的第二步接收到服务端发送过来的证书;
  3. 将服务端证书的签名和内置证书的签名进行对比,一致则校验通过,否则断开连接;

理论上应该也可以在双向认证中,客户端发送证书给服务端进行验证;

3. 使用公钥进行证书锁定认证(PublicKey Mode)

使用签名校验的方式有一个缺点:

  • CA 签发证书都存在有效期问题,所以缺点是在证书续期后需要将证书重新内置到 APP 中;

而公钥验证可以规避掉这个缺点,起流程如下:

  1. 服务端申请证书并将其内置到 App(或客户端) 中;
  2. 客户端在 HTTPS 认证的第二步接收到服务端发送过来的证书并提取出公钥;
  3. 将提取出来的公钥和本地内置证书的公钥进行比对;

我们在制作证书密钥时,公钥在证书的续期前后都可以保持不变(即密钥对不变),所以可以避免证书有效期问题;

TBSCetificate 包含有效期这个字段,颁发机构是对 TBSCertificate 进行签名的,所以有效期发生变化则整个签名都会发生变化。所以证书一旦过期,就需要更新 App 中的内置证书,对于 iOS 而言就需要发布新的版本,加之版本的兼容会比较复杂,所以公钥验证是一种比较合适的方案;

4. 相关的 openSSL 指令

  1. 获取移动端所需证书

如果采用证书锁定方式(Certificate Mode),则获取证书的摘要 hash,以 infinisign.com 为例:

## 在线读取服务器端.cer格式证书
openssl s_client -connect infinisign.com:443 -showcerts < /dev/null | openssl x509 -outform DER > infinisign.der
## 提取证书的摘要hash并查看base64格式
openssl dgst -sha256 -binary infinisign.der | openssl enc -base64
wLgBEAGmLltnXbK6pzpvPMeOCTKZ0QwrWGem6DkNf6o=

所以其中的wLgBEAGmLltnXbK6pzpvPMeOCTKZ0QwrWGem6DkNf6o=就是我们将要进行证书锁定的指纹 (Hash) 信息。

  1. 获取移动端所需公钥

如果采用公钥锁定方式(PublicKey Mode),则获取证书公钥的摘要hash,以infinisign.com为例

// 读取服务器端证书的公钥
openssl x509 -pubkey -noout -in infinisign.der -inform DER | openssl rsa -outform DER -pubin -in /dev/stdin 2>/dev/null > infinisign.pubkey

// 提取公钥的摘要hash并查看base64格式
openssl dgst -sha256 -binary infinisign.pubkey | openssl enc -base64
bAExy9pPp0EnzjAlYn1bsSEGvqYi1shl1OOshfH3XDA=

所以其中的bAExy9pPp0EnzjAlYn1bsSEGvqYi1shl1OOshfH3XDA=就是我们将要进行证书锁定的指纹 (Hash) 信息。

5. 锁定认证总结

  1. 锁定认证是为了解决终端证书链不可靠所引发的问题,是一种相对终极的解决方案;
  2. 证书锁定模式就是验证 TBSCertificate 的签名,其缺点是证书证书过期、失效时,需要频繁更新终端内嵌的证书;
  3. 公钥锁定的方式只认证公钥,只要证书拥有者的私钥不变,就不需要更新终端内嵌的证书;

四、AFN 鉴权源码解读

见文章:AFN中的鉴权

五、源码解读

1. 可选的鉴权策略

typedef NS_ENUM(NSInteger, XKHTTPClientSSLChallengeMode) {
    XKHTTPClientSSLChallengeModeNone, // 忽略
    XKHTTPClientSSLChallengeModeSimple, // 不校验host
    XKHTTPClientSSLChallengeModeFull // 正常校验
};
  1. 不校验,对于访问的 server 直接信任。
    此时,可以访问自签名证书的网站、证书过期的网站;

  2. 不校验域名
    此时,使用 X509 的规则对证书进行校验,只要证书符合规则,且该证书的根节点存在于当前机器的证书链中。另外,如果自身机器中信任了某个证书,那么该证书会被添加到证书链中,使用该证书作为根节点的证书也会通过校验,所以是存在风险的;

  3. 正常校验
    此时走 Apple 中 ATS 的默认实现,检测证书且校验域名;

2. Hybrid 容器的鉴权策略

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    NSURLCredential * credential = nil;

    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        disposition = NSURLSessionAuthChallengeUseCredential;
        credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
    }

    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}
  1. 如果是客户端验证服务端,直接信任;
  2. 如果是其他验证操作,直接取消;

对于容器而言,这样做是合理的,因为浏览器可能会访问不安全的网站;

3. SDWebImage 的鉴权策略

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
    
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;
    
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        if (!(self.options & UPSDWebImageDownloaderAllowInvalidSSLCertificates)) {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        } else {
            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            disposition = NSURLSessionAuthChallengeUseCredential;
        }
    } else {
        if (challenge.previousFailureCount == 0) {
            if (self.credential) {
                credential = self.credential;
                disposition = NSURLSessionAuthChallengeUseCredential;
            } else {
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        } else {
            disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
        }
    }
    
    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}
  1. 如果允许无效证书,则直接信任;
  2. 如果不允许,则采用 NSURLSession/Task 的默认策略;
  3. 如果不是客户端验证服务端,则使用自身的凭证(self.credential)校验,如果没有凭证,则直接取消;

4. 疑问

疑问:

  1. 当下为什么很少使用双向验证;主要是为了限制客户端访问,白名单或与是个更好的解决方案?
  2. 为什么很少使用锁定认证?自签名证书时使用更多,正规的 HTTPS 的话,必要性不大了~~
  3. 中间人攻击如何避免?HTTPS 下的 DH 算法,中间人攻击已经很难了吧?三个随机数、服务端和中间人公钥不一致、最后双端都会通过 finish 报文会对整个握手过程使用最终的对称秘钥进行加密(签名)并对比,基本没办法破解,所以 HTTPS 才成了互联网基建一般的存在???;

参考:

推荐阅读更多精彩内容