IOS之HTTPS学习笔记

本文前半部分介绍HTTPS的概念以及工作原理,后半部分着重介绍IOS网络请求怎么使用HTTPS

HTTPS概念

  • HTTP: Hyper Text Transfer Protocol;
  • HTTPS: Hyper Text Transfer Protocol over Secure Socket Layer;

我们看到HTTPS与HTTP相比,简称上多了一个S,名称上面多了一个Secure Socket Layer,实际上HTTPS=HTTP+SSL/TLS

HTTP协议的概念以及工作原理我们前面的文章已经介绍的很详细了,我们知道HTTP是应用层协议,建立在传输层协议(TCP/IP)上,那么HTTPS就是在应用层协议(HTTP)与传输层协议(TCP/IP)中间增加一个安全加密层(SSL/TLS)

在没有SSL/TLS层的时候,传输层(TCP/IP)发送的报文的数据域是明文的HTTP报文,当有SSL/TLS层的时候,传输层(TCP/IP)发送的报文的数据域是被SSL/TLS层加密的HTTP报文

在没有SSL/TLS层的时候 应用层获取到的数据是明文HTTP报文,当有SSL/TLS层的时候,应用层,收到的数据需要经过SSL/TLS层的处理,还原HTTP报文

上面只是用通俗的语言简单的介绍了一下HTTPS的概念,当然其工作原理要复杂很多,主要的工作都集中在了SSL/TLS层,通过这里我们也能看出,计算机网络这种分层架构设计的巧妙,增加中间一层,其他层几乎不需要改动就能增加新的特性

SSL/TLS

概念

SSL (Secure Socket Layer,安全套接字层),由网景公司发明,用来解决HTTP协议的安全问题(明文传输,容易被窥探,篡改等),SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。SSL协议可分为两层,SSL记录协议(SSL Record Protocol):它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。 SSL握手协议(SSL Handshake Protocol):它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。

TLS (Transport Layer Security,传输层安全协议),它建立在SSL 3.0协议规范之上,是SSL 3.0的后续版本,可以理解为SSL 3.1。所以TLS实际上是对SSL的补充以及增强,具体差异这里不表。该协议由两层组成: TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake)。

所以我们经常看到一些文章会并列的提到两个概念,实际上,我们可以把这两个概念视为同一种协议的不同阶段的名称,下文我们对SSL/TLS的讨论不会深入到协议细节,只讨论大概的工作原理,方便大家理解,同时,将SSL/TLS作为一种协议看待

SSL/TLS协议的主要功能

  • 确认客户端和服务器身份,并且协商加密秘钥(握手协议)
  • 加密数据,防止数据被窃取,篡改(记录协议)

握手协议

握手协议主要用来在通讯开始前,确认双方身份,协商秘钥(用于后期加密通讯数据)

由于非对称加密的速度比较慢,这里的秘钥传输使用非对称加密,后期通讯数据,使用协商的秘钥对称加密

所以在握手协议的四次握手过程中,我们最重要的就是确认了双方身份(验证证书),合成了会话秘钥,合成会话秘钥需要三个随机数,并且服务器公钥加密传输第三个随机数,用三个随机数合成会话秘钥

握手流程图如下

第一次握手(客户端发起)

客户端发送请求,客户端向服务端发送如下信息

  • 客户端支持的TLS协议版本号
  • 客户端生成的随机数,用于生成最终的会话秘钥
  • 支持的加密方式,如RSA
  • 支持的压缩方式

如果服务端不支持TLS版本,或者不支持我们发送的加密方式,那么直接返回失败,通讯结束
会话秘钥由三个随机数组成,在后面的会话中由各方提供

第二次握手 (服务端发起)

服务端收到客户端的请求后,向客户端回应如下信息

  • 确认TLS版本
  • 服务端生成的随机数,用于生成最终的会话秘钥
  • 确认加密方式
  • 服务器证书
  • 需要客户端证书(可选)

服务端确认TLS版本与加密方式,如果不支持的话,则通信结束关闭链接,另外,如果服务端需要确认客户端身份,需要增加携带客户端证书的信息

第三次握手 (客户端发起)

客户端收到服务端回应后,必须要验证服务器的数字证书,一般来说,服务器的数字证书都是从CA机构申请得到的(收费),也有的是使用自建证书(免费),所以我们验证证书也要区分这两种情况,如果是自建证书的话,调用系统api验证证书肯定是失败的,需要我们自己验证。有关数字证书的内容会在下面讨论。

证书验证失败,如果是浏览器,会弹出警告框,选择信任,会继续通讯,如果是app请求接口,需要我们自己处理是否信任。

证书验证成功,说明服务器是可以信任的,从证书中取出服务端公钥,客户端回应服务端如下内容

  • 客户端随机数,并用服务器证书里面的公钥加密
  • 客户端证书 (如果服务端需要)
  • 握手结束通知

上面的随机数是整个握手阶段的第三个随机数,并用服务器证书中的公钥加密。如果服务端需要验证客户端身份,客户端需要讲证书附带发送

第四次握手 (服务端发起)

服务器收到客户端回应,用私钥解开第三个随机数,然后用三个随机数”合成”最终会话秘钥,如果需要验证客户端证书,则验证客户端证书。然后回应客户端

  • 握手结束

接下来进入正式的通讯阶段,通讯仍然使用HTTP协议,但是报文内容用上面生成的“会话秘钥”对称加密

数字证书

上面握手过程中最重要的一步就是如何确认服务端身份,也就是如何验证服务器的数字证书,只有确认证书,我们才能保证我们通信的对方是我们信任的。

并且从证书中,取出公钥用于对称加密客户端传递的第三个随机数,保证最终三个随机数合成的会话秘钥不被破解。

要想理解数字证书的概念必须同时理解,数字签名,非对称加密,摘要算法这几个个概念,一般非对称加密我们使用RSA加密算法,摘要算法使用MD5,这两种算法都已经很普及了,不做过多介绍。

数字签名

一般来说,签名一定是签到某种内容载体上面,用来确认这个内容的真实,有效性,比如我们刷信用卡的时候需要签名,要把名字签到票据上面,来确认确实是本人消费。比如合同上面的签名,用来确认双方都承认合同的法律效应。

上面举的例子中,签名的载体是票据,合同书,签名的内容是我们的名字。

那么数字签名也是同样的逻辑,只不过,签名的内容不再是我们的名字,而是通过算法得到的一个字符串,这里的算法就是首先对载体的内容进行md5的到摘要,然后用签名人的私钥对摘要加密的到的就是数字签名,为什么要先得到摘要再用私钥签名呢,因为非对称加密是比较耗时的,由于内容长度不固定,可能很长,如果直接对内容进行非对称加密,效率会非常低,由于摘要算法得到的字符串长度是固定的,所以,能极大的减轻非对称加密的负担

比如我们想对一篇word文档进行数字签名,那么首先对整篇文章的内容进行md5的到摘要,然后用我的私钥对摘要进行加密的到的字符串就是数字签名,我们把数字签名附在文章最后。这样就完成了对这篇文章的数字签名

数字签名有什么用

我们可以看到即使已经有数字签名的文章,仍然是明文,仍然能被别人看到,所以数字签名的作用不是用来保证数据不被窥探,而是用来保证数据的完整性,保证数据不被篡改。

想象我已经将我的公钥发给了小王,然后我对文章进行数字签名,并将签名之后的文章发给小王,小王拿到文章之后,首先对文章内容进行md5的到摘要1,然后用我的公钥对数字签名解密的到摘要2,如果摘要1与摘要2相等,那么证明我发给小王的文章是没有经过篡改的。

其实这里面还有一个另外的问题,小王怎么确定手里的公钥就是我的,假如小王手里的公钥被小张偷换成自己的,那么小张截获我发送的文章后,进行篡改,用自己的私钥进行数字签名,小王拿到文章后,以为用的是我的公钥,其实用的是小张的公钥,验证数据是没问题的。这样就导致了数据被非法篡改。要想避免这种情况就需要引入数字证书

数字证书

在日常生活中,我们也会接触到各种各样的证书,毕业证书,英语4,6级证书,等等,所有证书都是用来证明某些内容的真实性,注意,这里必须有一个公开的,具有公信力的第三方来做担保,比如英语四六级证书是教育部盖章做公证,如果我自己做一个四六级证书,并盖我自己的章,那恐怕没有人相信。数字证书也是同样的道理,数字证书保证证书申请人的信息,公钥是真实,可靠的,并且需要有公信力的第三方机构来做担保(数字签名)。

数字证书的内部格式是由CCITT X.509国际标准所规定的,它主要包含了以下信息

  • 证书申请人(拥有人)的信息
  • 证书申请人(拥有人)的公钥
  • 证书有效期
  • 颁发证书的单位
  • 颁发证书的单位对证书的数字签名
  • 证书的扩展信息

理论上,任何人都能颁发证书,我们可以自己为自己颁发一个证书(上面说的自建证书),但是这样的证书是不具备公信力的。

我们平常所说的数字证书都是一些权威的机构颁发,这些证书颁发机构叫CA,如国际知名的VeriSign,这些机构都是世界上指定的公认的,可信的具有证书颁发权限的机构。

我们拿到一个数字证书如何验证其是有效,可信的呢

  • 首先查看有效期,如果在有效期内,继续验证
  • 接着查看证书颁发机构是否是CA机构,一般来说,浏览器会内置绝大部分CA的根证书,如果是app的话,手机系统会内置绝大部分CA的根证书,当我们知道证书颁发机构的名称后,就会去我们内置的信任证书列表寻找是否有匹配的机构,如果有取出该机构的公钥。对数字证书的内容进行md5的到摘要1,用公钥对证书的数字签名进行解密的到摘要2,如果摘要1等于摘要2那么这个证书确实是合法的。所以证书里面申请人的公钥是可以信任的。
  • 如果证书颁发机构不是CA,那么这个时候的处理方式完全取决于客户端,如果是浏览器,会弹出警告框,选择信任,继续通信,否则断开通信,如果是app那么我们要自己决定是否信任。
  • 有得app服务器没有从CA申请证书,而是使用自建证书,这就需要把自建证书打包到app内部, 在调用系统验证证书的时候会返回失败,这时用打包到app内的证书与服务端返回的证书比较,如果一致的话,说明身份正确

另外数字证书的验证可能会是链式验证,因为证书的颁发机构会分为一级证书颁发机构,二级证书颁发机构,三级证书颁发机构,所以我们可能会一层一层往上验证到最顶层的证书颁发机构,最顶层的证书颁发机构是自验证的,即自己给自己颁发证书

现在来看上面数字签名最后抛出的问题,我们在文章的最后除了添加数字签名外,还要附上我从CA申请的数字证书,这样,小王在拿到文章后,先验证我的数字证书,首先要去CA机构的官网获取CA公钥,然后参照上面的方式验证我的数字证书,证书合法后,证明证书里面的公钥确实是我的,从证书中取出我的公钥,然后进一步验证数字签名。最后判断文章是否被篡改。

增加了数字证书后,我的公钥的真实性就能得到保证

IOS 适配HTTPS

理解了HTTPS的工作原理,再来看一下IOS 网络请求中是如何实现HTTPS的,就会变的很简答吗,IOS中的网络框架,为我们做了很多,我们需要做的只是对第三次握手的控制,当第二次握手服务器回应后,会携带服务器证书,这时候我们会收到回调,我们可以自定义判断证书的逻辑,如果我们压根不想判断证书,那么我们什么都不需要做就能支持HTTPS

下面分别介绍 NSURLSession 以及AFNetworking 两种方式下如何实现HTTPS

NSURLSession

实现如下代理,当第二次握手服务端回应后会调用,下面是用系统的信任证书列表来验证服务器数字证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler
{
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
NSURLCredential *credential;

//1)获取trust object trust object里面包含了服务端返回的证书
SecTrustRef trust = challenge.protectionSpace.serverTrust;
SecTrustResultType result;

//2)SecTrustEvaluate对trust进行验证 这里会验证服务端证书是否可信
//默认去系统的信任证书列表查询
OSStatus status = SecTrustEvaluate(trust, &result);
if (status == errSecSuccess &&
(result == kSecTrustResultProceed ||
result == kSecTrustResultUnspecified)) {
//3)验证成功,生成NSURLCredential凭证cred,告知challenge的sender使用这个凭证来继续连接
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {

//5)验证失败,取消这次验证流程
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
completionHandler(disposition,credential);
}

验证服务器自建证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//先导入证书
NSString * cerPath = ...; //证书的路径
NSData * cerData = [NSData dataWithContentsOfFile:cerPath];
SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)(cerData));
self.trustedCertificates = @[CFBridgingRelease(certificate)];

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

//1)获取trust object trust object里面包含了服务端返回的证书
SecTrustRef trust = challenge.protectionSpace.serverTrust;
SecTrustResultType result;

//注意:这里将之前导入的证书设置成下面验证的Trust Object的anchor certificate
//当 系统信任证书列表,或者我们这里设置的证书,其中有一个匹配的话,SecTrustEvaluate会返回成功
SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)self.trustedCertificates);

//2)SecTrustEvaluate对trust进行验证 这里会验证服务端证书是否可信
//默认去系统的信任证书列表查询
OSStatus status = SecTrustEvaluate(trust, &result);
if (status == errSecSuccess &&
(result == kSecTrustResultProceed ||
result == kSecTrustResultUnspecified)) {
//3)验证成功,生成NSURLCredential凭证cred,告知challenge的sender使用这个凭证来继续连接
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {

//5)验证失败,取消这次验证流程
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
completionHandler(disposition,credential);
}

一个trust对象包括一组要验证的服务器证书以及一组用来验证trust的策略,也可以可选的包含一组其他证书(用来验证第一个证书)

上面的代码通过SecTrustSetAnchorCertificates来额外设置了一个可信任的证书数组,当再次调用SecTrustEvaluate的时候,验证范围变成了SecTrustSetAnchorCertificates配置的证书,如果我们仍然想信任系统的证书,需要调用SecTrustSetAnchorCertificatesOnly方法,清空这个标示(设置为NO)这时候调用SecTrustEvaluate 的验证范围就是系统证书+SecTrustSetAnchorCertificates配置的证书,当其中有一个是可信的,那么SecTrustEvaluate会返回成功

无论我们验证的结果如何必须调用 completionHandler 并传入NSURLSessionAuthChallengeDisposition以及一个NSURLCredential(可以为nil)对象,NSURLSessionAuthChallengeDisposition有如下定义

  • NSURLSessionAuthChallengeUseCredential 使用一个特定的NSURLCredential对象,可以是nil
  • NSURLSessionAuthChallengePerformDefaultHandling 默认的操作,效果与没有实现delegate类似,不做任何验证
  • NSURLSessionAuthChallengeCancelAuthenticationChallenge 取消验证,请求失败
  • NSURLSessionAuthChallengeRejectProtectionSpace 暂时不了解含义

系统验证证书api

1
2
3
//为trust object 设置除了系统内置信任证书以外的信任证书
OSStatus SecTrustSetAnchorCertificates(SecTrustRef trust,
CFArrayRef anchorCertificates)
1
2
3
4
//anchorCertificatesOnly  为yes 只信任SecTrustSetAnchorCertificates设置的证书
//如果为no 信任系统内置证书+SecTrustSetAnchorCertificates设置的证书
OSStatus SecTrustSetAnchorCertificatesOnly(SecTrustRef trust,
Boolean anchorCertificatesOnly)
1
2
//会根据上面两个方法设置的信任证书列表来验证服务端数字证书
SecTrustEvaluate(SecTrustRef trust, SecTrustResultType * __nullable result)

修改验证策略

默认的验证策略是验证域名,要求请求域名与服务器证书的域名一致,如果我们服务器的证书需要对应多个域名的话,我们需要修改验证策略,让其忽略域名

1
2
3
4
//获取验证策略
CFArrayRef policiesRef;
SecTrustCopyPolicies(trust, &policiesRef);
//打印 policiesRef 即可看到验证策略 默认是使用了域名验证,即证书中的域名,与我们请求的域名必须一致
1
2
3
4
5
6
7
8
9
//修改验证策略
NSMutableArray *policies = [NSMutableArray array];
//域名验证 如果不需要域名验证,不要添加这一项
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)@"https://www.12306.com")];
//标准验证
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];


SecTrustSetPolicies(trust, (__bridge CFArrayRef)policies);

AFNetWorking

AFNetworking主要使用AFSecurityPolicy来实现HTTPS

AFSecurityPolicy分三种验证模式:
AFSSLPinningModeNone
这个模式表示不做SSL pinning,只跟浏览器一样在系统的信任机构列表里验证服务端返回的证书。若证书是信任机构签发的就会通过,若是自己服务器生成的证书,这里是不会通过的。
AFSSLPinningModeCertificate
这个模式表示用证书绑定方式验证证书,需要客户端保存有服务端的证书拷贝,这里验证分两步,第一步验证证书的域名/有效期等信息,第二步是对比服务端返回的证书跟客户端保存的是否一致。
AFSSLPinningModePublicKey
这个模式同样是用证书绑定方式验证,客户端要有服务端的证书拷贝,只是验证时只验证证书里的公钥,不验证证书的有效期等信息。只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据。
整个AFSecurityPolicy就是实现这这几种验证方式,剩下的就是实现细节了,详见源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (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 (self.taskDidReceiveAuthenticationChallenge) {
//如果用户实现了block那么交给用户自己处理
disposition = self.taskDidReceiveAuthenticationChallenge(session, task, challenge, &credential);
} else {
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
//调用AFSecurityPolicy 进行验证处理
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
}

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

AFSecurityPolicy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain
{
//如果需要验证域名,添加Policies
NSMutableArray *policies = [NSMutableArray array];
if (self.validatesDomainName) {
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}

//设置信任Policies
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

if (self.SSLPinningMode == AFSSLPinningModeNone) {
return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
} else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
return NO;
}

switch (self.SSLPinningMode) {
case AFSSLPinningModeNone:
default:
return NO;
case AFSSLPinningModeCertificate: {
//取得本地的证书数组
NSMutableArray *pinnedCertificates = [NSMutableArray array];
for (NSData *certificateData in self.pinnedCertificates) {
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);

if (!AFServerTrustIsValid(serverTrust)) {
return NO;
}

// obtain the chain after being validated, which *should* contain the pinned certificate in the last position (if it's the Root CA)
NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);

for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
return YES;
}
}

return NO;
}
case AFSSLPinningModePublicKey: {
NSUInteger trustedPublicKeyCount = 0;
NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);

for (id trustChainPublicKey in publicKeys) {
for (id pinnedPublicKey in self.pinnedPublicKeys) {
if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
trustedPublicKeyCount += 1;
}
}
}
return trustedPublicKeyCount > 0;
}
}

return NO;
}