原文:http://itangqi.me/2016/05/20/the-notes-of-learning-afnetworking-seven/
前言
自 iOS 9 发布之后,由于新特性 App Transport Security 的引入,在默认行为下是不能发送 HTTP 请求的。很多网站都在转用 HTTPS,而 AFNetworking
中的 AFSecurityPolicy
就是为了阻止中间人攻击,以及其它漏洞的工具。
AFSecurityPolicy
主要作用就是验证 HTTPS 请求的证书是否有效,如果应用中有一些敏感信息或者涉及交易信息,一定要使用 HTTPS 来保证交易或者用户信息的安全。
AFSSLPinningMode
使用 AFSecurityPolicy
时,总共有三种验证服务器是否被信任的方式:
1 2 3 4 5 | typedef NS_ENUM(NSUInteger, AFSSLPinningMode) { AFSSLPinningModeNone, AFSSLPinningModePublicKey, AFSSLPinningModeCertificate, }; |
AFSSLPinningModeNone
- 这个模式不做本地证书验证(不做 SSL Pinning 操作)
- 直接从客户端系统中的受信任颁发机构 CA 列表中去验证
AFSSLPinningModePublicKey
- 客户端需要一份证书文件的拷贝
- 验证时只验证证书里的公钥,不验证证书的有效期等信息
- 即使伪造证书的公钥,也不能解密传输的数据,必须要私钥
AFSSLPinningModeCertificate
- 客户端需要一份证书文件的拷贝
- 第一步验证、先验证证书的域名/有效期等信息
- 第二步验证、对比服务端返回的证书跟客户端存储的证书是否一致
初始化以及设置
在使用 AFSecurityPolicy
验证服务端是否受到信任之前,要对其进行初始化,使用初始化方法时,主要目的是设置验证服务器是否受信任的方式:
1 2 3 4 5 6 7 8 9 10 11 12 | + (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode { return [self policyWithPinningMode:pinningMode withPinnedCertificates:[self defaultPinnedCertificates]]; } + (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode withPinnedCertificates:(NSSet *)pinnedCertificates { AFSecurityPolicy *securityPolicy = [[self alloc] init]; securityPolicy.SSLPinningMode = pinningMode; [securityPolicy setPinnedCertificates:pinnedCertificates]; return securityPolicy; } |
这里没有什么地方值得解释的。不过在调用 pinnedCertificate
的 setter 方法时,会从全部的证书中取出公钥保存到 pinnedPublicKeys
属性中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | - (void)setPinnedCertificates:(NSSet *)pinnedCertificates { _pinnedCertificates = pinnedCertificates; if (self.pinnedCertificates) { NSMutableSet *mutablePinnedPublicKeys = [NSMutableSet setWithCapacity:[self.pinnedCertificates count]]; for (NSData *certificate in self.pinnedCertificates) { id publicKey = AFPublicKeyForCertificate(certificate); if (!publicKey) { continue; } [mutablePinnedPublicKeys addObject:publicKey]; } self.pinnedPublicKeys = [NSSet setWithSet:mutablePinnedPublicKeys]; } else { self.pinnedPublicKeys = nil; } } |
在这里调用了 AFPublicKeyForCertificate
对证书进行操作,返回一个公钥。
操作 SecTrustRef
对 serverTrust
的操作的函数基本上都是 C 的 API,都定义在 Security
模块中,先来分析一下在上一节中 AFPublicKeyForCertificate
的实现:
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 | static id AFPublicKeyForCertificate(NSData *certificate) { // 1. 初始化临时变量 id allowedPublicKey = nil; SecCertificateRef allowedCertificate; SecCertificateRef allowedCertificates[1]; CFArrayRef tempCertificates = nil; SecPolicyRef policy = nil; SecTrustRef allowedTrust = nil; SecTrustResultType result; // 2. 因为此处传入的 certificate 参数是 NSData 类型的,所以使用 `SecCertificateCreateWithData` 通过 DER 表示的数据生成一个 `SecCertificateRef` 对象,然后判断返回值是否为 `NULL` allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate); __Require_Quiet(allowedCertificate != NULL, _out); // 3. 通过上面的 `allowedCertificate` 创建一个 `CFArray` allowedCertificates[0] = allowedCertificate; tempCertificates = CFArrayCreate(NULL, (const void **)allowedCertificates, 1, NULL); // 4. 创建一个默认的符合 X509 标准的 `SecPolicyRef`,通过默认的 `SecPolicyRef` 和证书创建一个 `SecTrustRef` 用于信任评估,对该对象进行信任评估,确认生成的 `SecTrustRef` 是值得信任的,这里使用的 __Require_noErr_Quiet 和上面的宏差不多,只是会根据返回值判断是否存在错误 policy = SecPolicyCreateBasicX509(); __Require_noErr_Quiet(SecTrustCreateWithCertificates(tempCertificates, policy, &allowedTrust), _out); __Require_noErr_Quiet(SecTrustEvaluate(allowedTrust, &result), _out); // 5. 获取公钥,这里的 __bridge_transfer 会将结果桥接成 NSObject 对象,然后将 SecTrustCopyPublicKey 返回的指针释放 allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust); _out: // 6. 释放各种 C 语言指针 if (allowedTrust) { CFRelease(allowedTrust); } if (policy) { CFRelease(policy); } if (tempCertificates) { CFRelease(tempCertificates); } if (allowedCertificate) { CFRelease(allowedCertificate); } return allowedPublicKey; } |
- 在 2 处,使用了一个非常神奇的宏
__Require_Quiet
,它会判断allowedCertificate != NULL
是否成立,如果allowedCertificate
为空就会跳到_out
标签处继续执行。 - 关于 DER 的概念:可参见 Wikipedia 或者 如何查看证书的16进制DER编码,及证书的各个域DER格式。
每一个
SecTrustRef
的对象都是包含多个SecCertificateRef
和SecPolicyRef
。其中SecCertificateRef
可以使用 DER 进行表示,并且其中存储着公钥信息。
对它的操作还有 AFCertificateTrustChainForServerTrust
和 AFPublicKeyTrustChainForServerTrust
但是它们几乎调用了相同的 API:
1 2 3 4 5 6 7 8 9 10 11 12 13 | static NSArray * AFCertificateTrustChainForServerTrust(SecTrustRef serverTrust) { // 1. 使用 SecTrustGetCertificateCount 函数获取到 serverTrust 中需要评估的证书链中的证书数目,并保存到 certificateCount 中 CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust); NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount]; // 2. 使用 SecTrustGetCertificateAtIndex 函数获取到证书链中的每个证书,并添加到 trustChain 中,最后返回 trustChain for (CFIndex i = 0; i < certificateCount; i++) { SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i); // 3. 使用 SecCertificateCopyData 从证书中或者 DER 表示的数据 [trustChain addObject:(__bridge_transfer NSData *)SecCertificateCopyData(certificate)]; } return [NSArray arrayWithArray:trustChain]; } |
验证服务端是否受信
验证服务端是否守信是通过 - [AFSecurityPolicy evaluateServerTrust:forDomain:]
方法进行的,也是 AFSecurityPolicy
类中的核心方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | - (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain { #1:不能隐式地信任自己签发的证书 #2:设置 policy #3:验证证书是否有效 #4:根据 SSLPinningMode 对服务端进行验证 return NO; } |
1. 不能隐式地信任自己签发的证书
1 2 3 4 | 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; } |
Do not implicitly trust self-signed certificates as anchors (kSecTrustOptionImplicitAnchors).
Instead, add your own (self-signed) CA certificate to the list of trusted anchors.
所以如果没有提供证书或者不验证证书,并且还设置 allowInvalidCertificates
为真,满足上面的所有条件,说明这次的验证是不安全的,会直接返回 NO
。
2. 设置 policy
1 2 3 4 5 6 | NSMutableArray *policies = [NSMutableArray array]; if (self.validatesDomainName) { [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)]; } else { [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()]; } |
如果要验证域名的话,就以域名为参数创建一个 SecPolicyRef
,否则会创建一个符合 X509 标准的默认 SecPolicyRef
对象。
3. 验证证书的有效性
1 2 3 4 5 | if (self.SSLPinningMode == AFSSLPinningModeNone) { return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust); } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) { return NO; } |
-
如果只根据信任列表中的证书进行验证,即
self.SSLPinningMode == AFSSLPinningModeNone
。如果允许无效的证书的就会直接返回YES
。不允许就会对服务端信任进行验证 -
如果服务器信任无效,并且不允许无效证书,就会返回
NO
4. 根据 SSLPinningMode
对服务器信任进行验证
1 2 3 4 5 6 7 8 9 10 11 | switch (self.SSLPinningMode) { case AFSSLPinningModeNone: default: return NO; case AFSSLPinningModeCertificate: { ... } case AFSSLPinningModePublicKey: { ... } } |
AFSSLPinningModeNone
直接返回 NO
AFSSLPinningModeCertificate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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; |
- 从
self.pinnedCertificates
中获取 DER 表示的数据 - 使用
SecTrustSetAnchorCertificates
为服务器信任设置证书 - 判断服务器信任的有效性
- 使用
AFCertificateTrustChainForServerTrust
获取服务器信任中的全部 DER 表示的证书 - 如果
pinnedCertificates
中有相同的证书,就会返回YES
AFSSLPinningModePublicKey
1 2 3 4 5 6 7 8 9 10 11 | 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; |
这部分的实现和上面的差不多,区别有两点:
- 会从服务器信任中获取公钥
pinnedPublicKeys
中的公钥与服务器信任中的公钥相同的数量大于 0,就会返回真
与 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
参数。
小结
AFSecurityPolicy
同样也作为一个即插即用的模块,在 AFNetworking 中作为验证 HTTPS 证书是否有效的模块存在,在 iOS 对 HTTPS 日渐重视的今天,在我看来,使用 HTTPS 会成为今后 API 开发的标配。
总结
到这里,AFNetworking 源码阅读之旅(UIKit+AFNetworking 除外)就要告一段落了,定有许多不足与遗憾。不过没关系,就让我们去实践中将理论升华,想必到时候一定会有一番新的体会。