AFNetworking3源码阅读-Security

AFNetworking3源码阅读-Security
自 iOS9 发布之后,由于新特性 App Transport Security 的引入,在默认行为下是不能发送 HTTP 请求的。很多网站都在转用 HTTPS,而 AFNetworking 中的 AFSecurityPolicy 就是为了阻止中间人攻击,以及其它漏洞的工具。
AFSecurityPolicy 主要作用就是验证 HTTPS 请求的证书是否有效,如果 app 中有一些敏感信息或者涉及交易信息,一定要使用 HTTPS 来保证交易或者用户信息的安全。

https & SSL Pinning

https

HTTPS其实是有两部分组成:HTTP + SSL / TLS,也就是在HTTP上又加了一层处理加密信息的模块。服务端和客户端的信息传输都会通过TLS进行加密

SSL Pinning

可 以理解为证书绑定,是指客户端直接保存服务端的证书,建立https连接时直接对比服务端返回的和客户端保存的两个证书是否一样,一样就表明证书 是真的,不再去系统的信任证书机构里寻找验证。这适用于非浏览器应用,因为浏览器跟很多未知服务端打交道,无法把每个服务端的证书都保存到本地,但CS架构的像手机APP事先已经知道要进行通信的服务端,可以直接在客户端保存这个服务端的证书用于校验。

为什么直接对比就能保证证书没问题? 如果中间人从客户端取出证书,再伪装成服务端跟其他客户端通信,它发送给客户端的这个证书不就能通过验证吗?确实可以通过验证,但后续的流程走不下去,因为下一步客户端会用证书里的公钥加密,中间人没有这个证书的私钥就解不出内容,也就截获不到数据,这个证书的私钥只有真正的服务端有,中间人伪造证书主要伪造的是公钥。

为什么要用SSL Pinning?正常的验证方式不够吗?如果服务端的证书是从受信任的的CA机构颁发的,验证是没问题的,但CA机构颁发证书比较昂贵,小企业或个人用 户 可能会选择自己颁发证书,这样就无法通过系统受信任的CA机构列表验证这个证书的真伪了,所以需要SSL Pinning这样的方式去验证。

AFSecurityPolicy

NSURLConnection 已经封装了https连接的建立、数据的加密解密功能,我们直接使用NSURLConnection是可以访问 https网站的,但NSURLConnection并没有验证证书是否合法,无法避免中间人攻击。
要做到真正安全通讯,需要我们手动去验证服务端返回的 证书,AFSecurityPolicy封装了证书验证的过程,让用户可以轻易使用,除了去系统信任CA机构列表验证,还支持SSL Pinning方式的验证

使用

把服务端证书(需要转换成cer格式)放到APP项目资源里,AFSecurityPolicy会自动寻找根目录下所有cer文件

1
2
3
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
securityPolicy.allowInvalidCertificates = YES;
[AFHTTPRequestOperationManager manager].securityPolicy = securityPolicy;

验证时 使用:

1
2
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain;

结构

UML图如下:

AFSSLPinningMode 验证模式

  • AFSSLPinningModeNone

    这个模式表示不做SSL pinning,只跟浏览器一样在系统的信任机构列表里验证服务端返回的证书。若证书是信任机构签发的就会通过,若是自己服务器生成的证书,这里是不会通过的。

  • AFSSLPinningModeCertificate

    这个模式表示用证书绑定方式验证证书,需要客户端保存有服务端的证书拷贝,这里验证分两步,第一步验证证书的域名/有效期等信息,第二步是对比服务端返回的证书跟客户端返回的是否一致。

  • AFSSLPinningModePublicKey

    这个模式同样是用证书绑定方式验证,客户端要有服务端的证书拷贝,只是验证时只验证证书里的公钥,不验证证书的有效期等信息。只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据。

验证服务端是否受信

验证服务端是否守信是通过- [AFSecurityPolicy evaluateServerTrust:forDomain:] 方法进行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 根据severTrust和domain来检查服务器端发来的证书是否可信
// 其中SecTrustRef是一个CoreFoundation类型,用于对服务器端传来的X.509证书评估的
// 而我们都知道,数字证书的签发机构CA,在接收到申请者的资料后进行核对并确定信息的真实有效,然后就会制作一份符合X.509标准的文件。证书中的证书内容包含的持有者信息和公钥等都是由申请者提供的,而数字签名则是CA机构对证书内容进行hash加密后得到的,而这个数字签名就是我们验证证书是否是有可信CA签发的数据。

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain{

1: 不能隐式地信任自己签发的证书

2: 设置 policy

3: 验证证书是否有效

4: 根据 SSLPinningMode 对服务端进行验证

return NO;

}

1# 不能隐式地信任自己签发的证书

1
2
3
4
5
6
7
8
9

//self.allowInvalidCertificates==YES表示如果此处允许使用自建证书(服务器自己弄的CA证书,非官方),并且还想验证domain是否有效(self.validatesDomainName == YES),也就是说你想验证自建证书的domain是否有效。
//那么你必须使用pinnedCertificates(就是在客户端保存服务器端颁发的证书拷贝)才可以。
//但是你的SSLPinningMode为AFSSLPinningModeNone,表示你不使用SSL pinning,只跟浏览器一样在系统的信任机构列表里验证服务端返回的证书。所以当然你的客户端上没有你导入的pinnedCertificates,同样表示你无法验证该自建证书。所以都返回NO。

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;
}

2# 设置 policy

1
2
3
4
5
6
7
8
9
10
11
// 此处设置验证证书的策略
NSMutableArray *policies = [NSMutableArray array];
if (self.validatesDomainName) {
// 如果需要验证domain,那么就使用SecPolicyCreateSSL函数创建验证策略,其中第一个参数为true表示验证整个SSL证书链,第二个参数传入domain,用于判断整个证书链上叶子节点表示的那个domain是否和此处传入domain一致
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
// 如果不需要验证domain,就使用默认的BasicX509验证策略
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
// 为serverTrust设置验证策略,即告诉客户端如何验证serverTrust
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

3# 验证证书是否有效

1
2
3
4
5
6
7
8
// 如果SSLPinningMode为 AFSSLPinningModeNone,表示你不使用SSL pinning,但是我允许自建证书,那么返回YES,或者使用AFServerTrustIsValid函数看看serverTrust是否可信任,如果信任,也返回YES
if (self.SSLPinningMode == AFSSLPinningModeNone) {
return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
}
// 既不允许自建证书,而且使用AFServerTrustIsValid函数又返回NO,那么该serverTrust就真的不能通过验证了
else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
return NO;
}

4# 根据 SSLPinningMode 对服务端进行验证

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
switch (self.SSLPinningMode) {
case AFSSLPinningModeNone:
default:
return NO;
case AFSSLPinningModeCertificate: {//验证整个证书
NSMutableArray *pinnedCertificates = [NSMutableArray array];
for (NSData *certificateData in self.pinnedCertificates) {
// 这里使用SecCertificateCreateWithData函数对原先的pinnedCertificates做一些处理,保证返回的证书都是DER编码的X.509证书
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
// 将pinnedCertificates设置成需要参与验证的Anchor Certificate(锚点证书,通过SecTrustSetAnchorCertificates设置了参与校验锚点证书之后,假如验证的数字证书是这个锚点证书的子节点,即验证的数字证书是由锚点证书对应CA或子CA签发的,或是该证书本身,则信任该证书),具体就是调用SecTrustEvaluate来验证。
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);

if (!AFServerTrustIsValid(serverTrust)) {
return NO;
}
//验证整个证书链
// 服务器端的证书链,注意此处返回的证书链顺序是从叶节点到根节点
NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
// 从服务器端证书链的根节点往下遍历,看看是否有与客户端的绑定证书一致的,有的话,就说明服务器端是可信的。因为遍历顺序正好相反,所以使用reverseObjectEnumerator
for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
return YES;
}
}

return NO;
}
// AFSSLPinningModePublicKey模式同样是用证书绑定(SSL Pinning)方式验证,客户端要有服务端的证书拷贝,只是验证时只验证证书里的公钥,不验证证书的有效期等信息。只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据。
case AFSSLPinningModePublicKey: {
NSUInteger trustedPublicKeyCount = 0;

// 从serverTrust中取出服务器端传过来的所有可用的证书,并依次得到相应的公钥
NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);

// 依次遍历这些公钥,如果和客户端绑定证书的公钥一致,那么就给trustedPublicKeyCount加一
for (id trustChainPublicKey in publicKeys) {
for (id pinnedPublicKey in self.pinnedPublicKeys) {
if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
trustedPublicKeyCount += 1;
}
}
}
//找到一个验证通过的public key就算通过
return trustedPublicKeyCount > 0;
}
}

在上面用到了一些静态方法,如下:

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
//从证书中获取public key
//__Require_Quiet __Require_noErr_Quiet 这两个宏方法用于方便地处理调用各种证书方法过程中出现的错误
//出现错误后用goto语句直接跳转到相应位置
static id AFPublicKeyForCertificate(NSData *certificate) {
id allowedPublicKey = nil;
SecCertificateRef allowedCertificate;
SecPolicyRef policy = nil;
SecTrustRef allowedTrust = nil;
SecTrustResultType result;

//1. 获取证书SecCertificateRef对象
allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate);
__Require_Quiet(allowedCertificate != NULL, _out);
//2. 获取默认x.509证书标准
policy = SecPolicyCreateBasicX509();
//3. 生成SecTrustRef 对象
__Require_noErr_Quiet(SecTrustCreateWithCertificates(allowedCertificate, policy, &allowedTrust), _out);
//4. 评估SecTrustRef对象
__Require_noErr_Quiet(SecTrustEvaluate(allowedTrust, &result), _out);
//5. 获得公钥SecTrustCopyPublicKey对象
allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust);

_out:
if (allowedTrust) {
CFRelease(allowedTrust);
}

if (policy) {
CFRelease(policy);
}

if (allowedCertificate) {
CFRelease(allowedCertificate);
}

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

//kSecTrustResultUnspecified:证书通过验证,但用户没有设置这些证书是否被信任
//kSecTrustResultProceed:证书通过验证,用户有操作设置了证书被信任,例如在弹出的是否信任的alert框中选择always trust
isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);

_out:
return isValid;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//取出服务端返回的所有证书
static NSArray * AFCertificateTrustChainForServerTrust(SecTrustRef serverTrust) {

// 使用SecTrustGetCertificateCount函数t获取到serverTrust中需要评估的证书链中的证书数目,并保存到certificateCount中
CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];

// 使用SecTrustGetCertificateAtIndex函数获取到证书链中的每个证书,并添加到trustChain中,最后返回trustChain
for (CFIndex i = 0; i < certificateCount; i++) {
SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
[trustChain addObject:(__bridge_transfer NSData *)SecCertificateCopyData(certificate)];
}

return [NSArray arrayWithArray:trustChain];
}
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
//取出服务端返回证书里所有的public key
static NSArray * AFPublicKeyTrustChainForServerTrust(SecTrustRef serverTrust) {
// 接下来的一小段代码和上面AFCertificateTrustChainForServerTrust函数的作用基本一致,都是为了获取到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);

SecCertificateRef someCertificates[] = {certificate};
CFArrayRef certificates = CFArrayCreate(NULL, (const void **)someCertificates, 1, NULL);

SecTrustRef trust;
// 根据给定的certificates和policy来生成一个trust对象
__Require_noErr_Quiet(SecTrustCreateWithCertificates(certificates, policy, &trust), _out);

SecTrustResultType result;
// 使用SecTrustEvaluate来评估上面构建的trust
__Require_noErr_Quiet(SecTrustEvaluate(trust, &result), _out);
// 如果该trust符合X.509证书格式,那么先使用SecTrustCopyPublicKey获取到trust的公钥,再将此公钥添加到trustChain中
[trustChain addObject:(__bridge_transfer id)SecTrustCopyPublicKey(trust)];

_out:
// 注意释放资源
if (trust) {
CFRelease(trust);
}

if (certificates) {
CFRelease(certificates);
}

continue;
}
CFRelease(policy);
// 返回对应的一组公钥
return [NSArray arrayWithArray:trustChain];
}

与 AFURLSessionManager 协作

在代理协议 - URLSession:didReceiveChallenge:completionHandler: 或者 - URLSession:task:didReceiveChallenge:completionHandler: 代理方法被调用时会运行这段代码

1
2
3
4
5
6
7
8
9
10
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {  
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
disposition = NSURLSessionAuthChallengeRejectProtectionSpace;
}
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}

NSURLAuthenticationChallenge 表示一个认证的挑战,提供了关于这次认证的全部信息。它有一个非常重要的属性 protectionSpace,这里保存了需要认证的保护空间, 每一个 NSURLProtectionSpace 对象都保存了主机地址,端口和认证方法等重要信息。

在上面的方法中,如果保护空间中的认证方法为 NSURLAuthenticationMethodServerTrust,那么就会使用在上一小节中提到的方法 - [AFSecurityPolicy evaluateServerTrust:forDomain:] 对保护空间中的 serverTrust 以及域名 host 进行认证

根据认证的结果,会在 completionHandler 中传入不同的 disposition 和 credential 参数。