AFNetworking的安全策略

上面我们说到,AFNetworking3.x是对NSURLSession的一层包装,包括delegate的实现和回调,还有请求接口的包装。不过这只是一部分基本功能,实际上还有一些同样很复杂并且重要的部分。比如现在我们看到的就是其中的安全策略。
本质上AFNetworking的安全策略是根据https来完成的。先简单讲讲https和http的差别:
由于超文本传输协议http协议以明文的方法传送内容,不提供任何数据加密,所以很容易被攻击者截取到http传输的内容,在这个基础上,开发了新的https协议,即加入了SSL安全套接字方法,依靠证书来验证服务器的身份,这样在普通http传输的过程中传输的信息就是经过了证书加密之后的数据,提高了安全性。

简单总结一下就是:

  1. 用户发起请求,服务器响应并返回一个证书,包括基本信息和公钥
  2. 用户拿到证书之后验证是否合法
  3. 生成一个随机数作为加密的密钥,然后用先前服务器所给的密钥来加密,并返回给服务器
  4. 服务器用密钥解密这个随机数,然后再用解密之后的随机数把需要返回的数据来加密并返回给用户
  5. 终于用户拿到加密的数据,用最开始的随机数来解密,完成了数据的交接

这是一个单步认证,实际上服务器也会对用户端传过来的信息进行验证,过程是差不多的。本质上还是使用一个随机数和一个公钥进行加密和解密,并不难理解。

AFURLSessionManager中完成的一个delegate方法实现了https认证的过程:

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
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
// 处理challenge的选项,这里有performDefaultHandling,useCredential和cancelAuthenticationChallenge三种选择,分别为默认处理方式,使用指定证书以及直接取消challenge
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
// taskdidReceiveAuthenticationChallenge是一个预设的block,用户可以自定义如何对应来自服务器端的challenge,如果没有设定的话则使用默认代码
if (self.taskDidReceiveAuthenticationChallenge) {
disposition = self.taskDidReceiveAuthenticationChallenge(session, task, challenge, &credential);
} else {
// 如果服务器端要求的challenge方法是NSURLAuthenticationMethodServerTrust,那么就是说客户端应该根据challenge.protectionSpace.serverTrust来产生证书
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
// 查看安全策略,如果不信任服务器端就直接取消challenge
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
// 创建challenge证书
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
// 不信任,取消挑战
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
// 不要求NSURLAuthenticationMethodServerTrust的话就使用默认的handling方法
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
}
if (completionHandler) {
completionHandler(disposition, credential);
}
}

上述就是如何应对challenge的方法,总结一下基本功能(1)指定https为默认的认证方法 (2)如果自定义了处理challenge的block self.taskDidReceiveAuthenticationChallenge那么就直接调用,并给credential赋值,最后交给completionHandler进行认证 (3)如果没有自定义block,就根据是否认证方法为NSURLAuthenticationMethodServerTrust来判断是否实现这个单向认证。 (4)如果是直接信任服务器端则创建证书。
需要说明的是,(3)(4)之间还有一个查看securityPolicy的过程,如果被认证符合安全策略才会允许生成证书(证书通过serverTrust中包含的服务器证书认证信息),这样在底层系统验证证书合法性之前就可以先做一道验证,把应该取消的https过程给取消掉。

先看看securityPolicy的内容吧:

1
2
3
4
5
6
7
8
9
AFSecurityPolicy *policy = [AFSecurityPolicy defaultPolicy]; // 创建一个policy
// 创建方法
+ (instancetype)defaultPolicy {
AFSecurityPolicy *securityPolicy = [[self alloc] init];
securityPolicy.SSLPinningMode = AFSSLPinningModeNone;
return securityPolicy;
}

SSLPinningMode是securytyPolicy的一个重要属性,记录https验证模式,有三种验证方式

1
2
3
4
5
typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
AFSSLPinningModeNone, //不验证
AFSSLPinningModePublicKey, //只验证公钥
AFSSLPinningModeCertificate, // 验证证书
};

刚才调用的验证安全性的方法为:

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain
{
// 三个条件:(1)域名有效 (2)允许创建证书 (3)验证域名成功
// 因为要验证域名所以很显然不能为modenone或者添加的证书为0
if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
NSLog(@"In order to validate a domain name for self signed certificates, you MUST use pinning.");
return NO;
}
// 容器用来装验证策略
NSMutableArray *policies = [NSMutableArray array];
// 如果需要验证域名
if (self.validatesDomainName) {
// 使用secPolicyCreateSSL函数创建验证策略,第一个参数表示是否验证整个SSL证书链,第二个参数为域名,判断证书链上的域名是否和传入的一致
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
// 不需要验证域名,就使用默认的basicX509验证策略
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
// 将验证策略policies交给serverTrust,即明确客户端采取的策略
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
// 存在验证策略的情况下,如果是AFSSLPiningModeNone,意味着是自签名,直接返回信任,否则需要去证书里查找是否有匹配的证书
if (self.SSLPinningMode == AFSSLPinningModeNone) {
// 这里如果支持自签名则直接返回,根本不需要调用后面的方法
return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
} else if (!AFServerTrustIsValid(serverTrust) &&
!self.allowInvalidCertificates) {
//存在既验证无效又不允许自签名的情况,这样就返回no
return NO;
}
// 根据SSLPinningMode来采取不同策略
switch (self.SSLPinningMode) {
// 上面其实已经解决了ModeNone
case AFSSLPinningModeNone:
default:
return NO;
// 需要验证证书类型的case
case AFSSLPinningModeCertificate: {
NSMutableArray *pinnedCertificates = [NSMutableArray array];
// 通过SecCertificateCreateWithData方法把证书的data转换成SecCertificateRef类型的参数,然后保存在临时数组中
for (NSData *certificateData in self.pinnedCertificates) {
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
// 将pinnedCertificates设置为需要参与验证的锚点证书---假如需要验证的数字证书是锚点证书的子节点,则信任该证书---而这里需要验证的证书就在derverTrust中,是从服务器获取的证书信息
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
// 设置好锚点证书之后再去调用方法验证serverTrust包含的证书是否有效
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) {
// 如果证书相同,count += 1
if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
trustedPublicKeyCount += 1;
}
}
}
// 如果存在相同的公钥,返回yes
return trustedPublicKeyCount > 0;
}
}
return NO;
}

上面就是securityPolicy的核心部分,重要内容都标上了注释。总结来讲就是根据三种模式来进行证书的验证:(1)如果是AFSSLPinningModeNone,肯定返回yes(根据是否需要验证和是否自签有些差别) (2)如果是AFSSLPinningModeCertificate,那么从serverTrust中获取证书然后和本地的证书集合去匹配 (3)如果是AFSSLPinningModePublicKey,那么从serverTrust中去获取公钥然后和本地的公钥进行配对。
其实过程非常清晰,不过细节上调用了很多原生的security代码,从代码角度需要更详细地了解。
AFNetworking包装了一系列系统函数在上面的方法中调用,现在可以看看其中重要的几个方法的实现。

验证servetTrust的函数:

1
2
3
4
5
6
7
8
9
10
static BOOL AFServerTrustIsValid(SecTrustRef serverTrust) {
BOOL isValid = NO;
SecTrustResultType result;
__Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out);
isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
_out:
return isValid;
}

首先说说

```这个方法,如果第一个参数为0表示错误,则跳到后面表达式所在的位置去执行,这里就是_out:,也就是直接返回isValid = NO,否则就继续执行。
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
``` SecTrustEvaluate``` 这个方法去系统根目录去查找证书是否可信,然后把结果赋值给result,并且返回是否找到。如果没有出错就不会直接return false,接着往下进行,然后只有当result为kSecTrustResultUnspecified(表示serverTrust被信任,但不被用户认可)或者 kSecTrustResultProceed(同上并且被用户认可)二者其一的时候就可以认为serverTrust认证成功,然后返回true。
获取serverTrust中包含证书链和公钥的函数:
```objective-c
static NSArray * AFCertificateTrustChainForServerTrust(SecTrustRef serverTrust) {
CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
for (CFIndex i = 0; i < certificateCount; i++) {
SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
[trustChain addObject:(__bridge_transfer NSData *)SecCertificateCopyData(certificate)];
}
return [NSArray arrayWithArray:trustChain];
}
static NSArray * AFPublicKeyTrustChainForServerTrust(SecTrustRef serverTrust) {
SecPolicyRef policy = SecPolicyCreateBasicX509();
CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
// 在取出所有证书的基础上
for (CFIndex i = 0; i < certificateCount; i++) {
SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
// 创建CF数组
SecCertificateRef someCertificates[] = {certificate};
CFArrayRef certificates = CFArrayCreate(NULL, (const void **)someCertificates, 1, NULL);
// 生成一个trust对象,参数是certificate和policy,失败则直接跳过
SecTrustRef trust;
__Require_noErr_Quiet(SecTrustCreateWithCertificates(certificates, policy, &trust), _out);
// 验证trust,失败则跳过
SecTrustResultType result;
__Require_noErr_Quiet(SecTrustEvaluate(trust, &result), _out);
// 如果符合X.509证书格式,那么先使用SecTrustCopyPublicKey获取公钥然后添加到trustChain中
[trustChain addObject:(__bridge_transfer id)SecTrustCopyPublicKey(trust)];
// 释放资源,如果上面有错误直接跳到这里释放,否则就正常进行到这里进行释放
_out:
if (trust) {
CFRelease(trust);
}
if (certificates) {
CFRelease(certificates);
}
continue;
}
CFRelease(policy);
return [NSArray arrayWithArray:trustChain];
}

第一个函数获取证书链,内容很简单,先获取serverTrust中的证书个数,然后依次将证书对象转化成NSData并加入到trustChain中。
第二个函数获取公钥,稍微复杂一点,在上面一个函数取出所有证书的基础上,加入了认证和从证书链中提取公钥的过程。

总结来讲,基本上如果用户用的不是自签名的证书,那么基本什么都不用管,直接用AFNetworkign就能够完成(大部分情况)。如果自签名就需要设置policy,尽量不要用到吧…
AFNetworking总体而言能在系统底层之前就验证证书,提高了效率。

补充证书和证书链

可能上面说的证书,证书链等东西有些抽象,具体补充一下相关的信息:
数字证书的生成是分层级的,下一级的证书需要其上一级证书的私钥签名。
所以后者是前者的证书颁发者,也就是说上一级证书的 Subject Name 是其下一级证书的 Issuer Name。所以也就存在了我们上面数说的证书的根节点和叶子节点这么一说。
关于根证书:
数字证书认证机构(Certificate Authority, CA)签署和管理的 CA 根证书,会被纳入到你的浏览器和操作系统的可信证书列表中,并由这个列表判断根证书是否可信。也就是根证书是约定好的,不需要其他的数字证书的私钥进行签名。
在iOS上对证书的验证:
信任链中如果只含有有效证书并且以可信锚点(trusted anchor)结尾,那么这个证书就被认为是有效的。一般情况下信任锚点是系统隐式信任的证书,比如CA根节点,但是也可以在验证证书链的时候设置自签名的证书作为信任锚点(上面说到的自签名的验证中有提到)

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器