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 加密算法

HTTPS 中如果采用 RSA 加密,也就是通过 premaster_random 这一个随机数来计算最终的对称秘钥 master_random ,最后客户端使用 RSA 加密传输给服务端。后续服务端和客户端都是用这个随机数作为对称加密的 key 进行对称加密,这种情况有两个问题:

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

流程如下:

RSA加密

根据 RSA 算法的规则,RSA 只能暴力枚举,算力很强的计算机都要枚举 70 年,所以 RSA 基本无法破解。但是,需要注意的是,破解 HTTPS 的关键不在于破解 RSA,而在于获得对称秘钥。因为 RSA 算法只用在建立 HTTPS 的传输阶段,后续加密都是使用对称秘钥,所以一旦猜出了这个随机数,无需破解 RSA 就能破解 HTTPS。

因此,HTTPS 的安全攻防归根到底在于对称秘钥的加解密,只有这个随机数越随机,HTTPS 才越安全。上述算法中,这个随机数只在客户端生成,其最大的问题在于随机数的算法相对固定,那么就意味着:

  1. 伪随机性低,猜出了客户端使用的随机算法就有可能猜出对称秘钥;
  2. 因为算法固定,一旦猜出了一个对称秘钥,可以根据该算法反推出之前会话中生成的对称秘钥,即向前攻击;

因此,开发出了三个随机数的 HTTPS 加密。

  • 算法二:CS两端一起生成三个随机数
  1. 客户端传递随机数 client_random;
  2. 服务端传递随机数 server_random;
  3. 前两步因为双方没有建立连接,也就没有公私钥,所以只能明文传输两个随机数,但是到了第三部,客户端接收到了服务端的公钥,可以生成第三个随机数 pre_master,并且使用公钥对第三个随机数进行加密之后传输给服务端;
  4. 客户端可以优先通过三个随机数计算出最终的对称秘钥,并且对之前的报文进行加密,加这个哈希值传递给服务端;
  5. 服务端使用私钥解密获取到第三个随机数之后,也生成对称秘钥进行验签,验签通过则告诉客户端,后续直接使用对称秘钥加密;

上述过程中,客户端和服务端在握手 hello 消息中明文交换了client_randomserver_random,使用 RSA 公钥加密传输premaster secret,最后通过算法,客户端和服务端分别计算 master secret。其中,不直接使用 premaster secret 的原因是:保证 secret 的随机性不受任意一方的影响。

这种三个随机数下的 HTTPS 已经相当安全了,理论上,只要服务器的公钥足够长(比如2048位),那么Premaster secret可以保证不被破解。但是为了足够安全,我们可以考虑把握手阶段的算法从默认的RSA算法,改为 Diffie-Hellman算法(简称DH算法)。

  • 算法三: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 => 16807 对 5 取模,结果为 2。
    Client 生成随机数 X1,假设 X1 = 2,P1 = 7^2 mod 5 => 49 对 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 恒成立。

采用DH算法后,Premaster secret不需要传递,双方只要交换各自的参数,就可以算出这个随机数。上图中,第三步和第四步由传递Premaster secret变成了传递DH算法所需的参数,然后双方各自算出Premaster secret。这样就提高了安全性。

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

流程如下:


DH算法

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

更加详细的流程图:

DH算法

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

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

  1. 早期使用一个随机数,且采用 RSA 算法进行传输,存在两个严重的问题:伪随机数不一定随机,RSA 被破解后(伪随机数被猜出)存在向前攻击的漏洞(根据算法可以猜出上一个随机数);
  2. 上述基础上增加为 3 个随机数,使得对称秘钥的随机性不依赖于任何一方,增加了破解难度, HTTPS 默认采用这种握手规则;
  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 的默认实现,检测证书且校验域名;

代码如下:

-(void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {

    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    NSURLCredential * credential = nil;

    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        if (self.sslChallengeMode == XKHTTPClientSSLChallengeModeNone) {
            // 使用服务端的证书对服务端进行校验,那肯定是怎样都通过,自签名证书都能通过
            disposition = NSURLSessionAuthChallengeUseCredential;
            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
        } else if(self.sslChallengeMode == XKHTTPClientSSLChallengeModeSimple) {
            SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
            NSArray * policies = @[ (__bridge_transfer id)SecPolicyCreateBasicX509() ];
            SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
            SecTrustResultType result;
            if(SecTrustEvaluate(serverTrust, &result) == errSecSuccess &&
               (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed)) {
                // x509认证,不会对服务端证书中的域名和请求的域名进行一致性校验
                // 校验成功,则直接传递服务端证书来对服务端鉴权进行校验,即:怎样都会成功
                disposition = NSURLSessionAuthChallengeUseCredential;
                credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            } else {
                // x509校验失败,则走默认的校验逻辑
                disposition = NSURLSessionAuthChallengePerformDefaultHandling;
            }
        } else {
            // 默认校验逻辑:ATS,即对证书锚点、证书链进行校验且会对证书中的域名和请求的域名的一致性进行校验
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    } else {
        disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    }

    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}

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 才成了互联网基建一般的存在???;

参考:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容