版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.03.02 |
前言
我们做APP发起网络请求,都离不开一个非常有用的框架AFNetworking,可以说这个框架的知名度已经超过了苹果的底层网络请求部分,很多人可能不知道苹果底层是如何发起网络请求的,但是一定知道
AFNetworking
,接下来几篇我们就一起详细的解析一下这个框架。感兴趣的可以看上面写的几篇。
1. AFNetworking源码探究(一) —— 基本介绍
2. AFNetworking源码探究(二) —— GET请求实现之NSURLSessionDataTask实例化(一)
3. AFNetworking源码探究(三) —— GET请求实现之任务进度设置和通知监听(一)
4. AFNetworking源码探究(四) —— GET请求实现之代理转发思想(一)
5. AFNetworking源码探究(五) —— AFURLSessionManager中NSURLSessionDelegate详细解析(一)
6. AFNetworking源码探究(六) —— AFURLSessionManager中NSURLSessionTaskDelegate详细解析(一)
7. AFNetworking源码探究(七) —— AFURLSessionManager中NSURLSessionDataDelegate详细解析(一)
8. AFNetworking源码探究(八) —— AFURLSessionManager中NSURLSessionDownloadDelegate详细解析(一)
9. AFNetworking源码探究(九) —— AFURLSessionManagerTaskDelegate中三个转发代理方法详细解析(一)
10. AFNetworking源码探究(十) —— 数据解析之数据解析架构的分析(一)
11. AFNetworking源码探究(十一) —— 数据解析之子类中协议方法的实现(二)
12. AFNetworking源码探究(十二) —— 数据解析之子类中协议方法的实现(三)
13. AFNetworking源码探究(十三) —— AFSecurityPolicy与安全认证 (一)
回顾
上一篇主要讲述了HTTPS认证原理以及AFSecurityPolicy
的实例化。这一篇就具体的看一下验证流程。
验证服务端
还记得上一篇那个验证服务端的那个方法吗?具体如下表示。
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain
{
if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
// https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html
// According to the docs, you should only trust your provided certs for evaluation.
// Pinned certificates are added to the trust. Without pinned certificates,
// there is nothing to evaluate against.
//
// From Apple Docs:
// "Do not implicitly trust self-signed certificates as anchors (kSecTrustOptionImplicitAnchors).
// Instead, add your own (self-signed) CA certificate to the list of trusted anchors."
// 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) {
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
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;
}
下面我们就看一下这个验证的过程。
(a) 第一个if判断
if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
// https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html
// According to the docs, you should only trust your provided certs for evaluation.
// Pinned certificates are added to the trust. Without pinned certificates,
// there is nothing to evaluate against.
//
// From Apple Docs:
// "Do not implicitly trust self-signed certificates as anchors (kSecTrustOptionImplicitAnchors).
// Instead, add your own (self-signed) CA certificate to the list of trusted anchors."
NSLog(@"In order to validate a domain name for self signed certificates, you MUST use pinning.");
return NO;
}
首先看一下判断条件,如果域名存在,且允许自建证书,且需要验证域名,且SSLPinningMode
模式为AFSSLPinningModeNone
或者添加到项目中的证书数量为0个。
只要满足上面的if条件,就返回NO,表示不信任。
(b) 安全策略
主要对应下面这段代码
NSMutableArray *policies = [NSMutableArray array];
if (self.validatesDomainName) {
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
首先就是实例化一个可变数组,用于后面函数SecTrustSetPolicies
中做参数,接着就是根据条件self.validatesDomainName
,为数组添加不同的元素。
- 如果
self.validatesDomainName == YES
,需要验证域名,那么调用下面函数,这个函数是Security框架中的,是苹果原生的,返回值类型为SecPolicyRef
,将该返回值加入到策略数组policies中。
如果需要验证domain,那么就使用SecPolicyCreateSSL
函数创建验证策略,其中第一个参数为true表示验证整个SSL证书链,第二个参数传入domain,用于判断整个证书链上叶子节点表示的那个domain是否和此处传入domain一致。
我们先看第一个函数
/*!
@function SecPolicyCreateSSL
@abstract Returns a policy object for evaluating SSL certificate chains.
// 返回用于评估SSL证书链的策略对象。
@param server Passing true for this parameter creates a policy for SSL
server certificates.
// 服务器为此参数传递true将为SSL服务器证书创建策略
@param hostname (Optional) If present, the policy will require the specified
hostname to match the hostname in the leaf certificate.
// 如果存在,策略将要求指定的主机名与叶证书中的主机名匹配。
@result A policy object. The caller is responsible for calling CFRelease
on this when it is no longer needed.
*/
SecPolicyRef SecPolicyCreateSSL(Boolean server, CFStringRef __nullable hostname)
// 策略对象。 调用者负责在不再需要时调用CFRelease
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_2_0);
- 如果
self.validatesDomainName == NO
,不需要验证域名,那么调用下面函数,这个函数是Security框架中的,是苹果原生的,返回值类型为SecPolicyRef
,将该返回值加入到策略数组policies中。如果不需要验证domain,就使用默认的BasicX509
验证策略
看一下这个函数
/*!
@function SecPolicyCreateBasicX509
@abstract Returns a policy object for the default X.509 policy.
// 返回默认X.509策略的策略对象
@result A policy object. The caller is responsible for calling CFRelease
on this when it is no longer needed.
*/
// 策略对象。 调用者负责调用CFRelease在不再需要它时进行调用释放
SecPolicyRef SecPolicyCreateBasicX509(void)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_2_0);
最后调用函数SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
函数设置策略,这里serverTrust:X.509服务器的证书信任;policies表示为serverTrust
设置验证策略,即告诉客户端如何验证serverTrust
。具体这个函数的接口如下所示:
/*!
@function SecTrustSetPolicies
@abstract Set the policies for which trust should be verified.
@param trust A trust reference.
@param policies An array of one or more policies. You may pass a
SecPolicyRef to represent a single policy.
@result A result code. See "Security Error Codes" (SecBase.h).
@discussion This function will invalidate the existing trust result,
requiring a fresh evaluation for the newly-set policies.
*/
OSStatus SecTrustSetPolicies(SecTrustRef trust, CFTypeRef policies)
__OSX_AVAILABLE_STARTING(__MAC_10_3, __IPHONE_6_0);
(c) 验证模式的判断
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;
}
}
这里,前面已经有验证策略了,可以去验证了,首先判断self.SSLPinningMode == AFSSLPinningModeNone
,然后看这一句self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust)
,如果支持自签名,直接返回YES,不允许才去判断第二个条件,判断serverTrust是否有效,下面的是验证的函数。
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;
}
如果验证无效AFServerTrustIsValid
,而且allowInvalidCertificates
不允许自签,返回NO。
else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
return NO;
}
接着就是switch条件句判断SSLPinningMode
这个验证模式。
- 如果是
AFSSLPinningModeNone
case AFSSLPinningModeNone:
default:
return NO;
这种就是默认返回的NO。
- 如果是
AFSSLPinningModeCertificate
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表示验证证书类型。
首先实例化一个可变数组
NSMutableArray *pinnedCertificates = [NSMutableArray array];
下面看一个集合属性
/**
The certificates used to evaluate server trust according to the SSL pinning mode.
// 用于根据SSL固定模式评估服务器信任的证书
By default, this property is set to any (`.cer`) certificates included in the target compiling AFNetworking. Note that if you are using AFNetworking as embedded framework, no certificates will be pinned by default. Use `certificatesInBundle` to load certificates from your target, and then create a new policy by calling `policyWithPinningMode:withPinnedCertificates`.
Note that if pinning is enabled, `evaluateServerTrust:forDomain:` will return true if any pinned certificate matches.
*/
@property (nonatomic, strong, nullable) NSSet *pinnedCertificates;
默认情况下,该属性设置为包含在目标编译AFNetworking中的任何(.cer
)证书。 请注意,如果您使用AFNetworking作为嵌入式框架,则默认情况下不会锁定任何证书。 使用certificatesInBundle
从你的目标加载证书,然后通过调用policyWithPinningMode:withPinnedCertificates
来创建一个新的策略。
请注意,如果启用了锁定,如果任何固定证书匹配,evaluateServerTrust:forDomain:
将返回true。
接着就是对该集合对象进行遍历
for (NSData *certificateData in self.pinnedCertificates) {
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
把证书数据,用系统返回类型为SecCertificateRef
的SecCertificateCreateWithData
函数对原先的certificateData
做一些处理,保证返回的证书都是DER编码的X.509证书。下面看一下这个函数。
/*!
@function SecCertificateCreateWithData
@abstract Create a certificate given it's DER representation as a CFData.
@param allocator CFAllocator to allocate the certificate with.
@param data DER encoded X.509 certificate.
@result Return NULL if the passed-in data is not a valid DER-encoded
X.509 certificate, return a SecCertificateRef otherwise.
*/
__nullable
SecCertificateRef SecCertificateCreateWithData(CFAllocatorRef __nullable allocator, CFDataRef data)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_2_0);
然后,将pinnedCertificates
设置成需要参与验证的Anchor Certificate
(锚点证书,通过SecTrustSetAnchorCertificates
设置了参与校验锚点证书之后,假如验证的数字证书是这个锚点证书的子节点,即验证的数字证书是由锚点证书对应CA或子CA签发的,或是该证书本身,则信任该证书)。先看一下这个函数。
/*!
@function SecTrustSetAnchorCertificates
@abstract Sets the anchor certificates for a given trust.
@param trust A reference to a trust object.
@param anchorCertificates An array of anchor certificates.
@result A result code. See "Security Error Codes" (SecBase.h).
@discussion Calling this function without also calling
SecTrustSetAnchorCertificatesOnly() will disable trusting any
anchors other than the ones in anchorCertificates.
*/
OSStatus SecTrustSetAnchorCertificates(SecTrustRef trust,
CFArrayRef anchorCertificates)
__OSX_AVAILABLE_STARTING(__MAC_10_3, __IPHONE_2_0);
然后,接着进行判断
if (!AFServerTrustIsValid(serverTrust)) {
return NO;
}
自签在之前是验证通过不了的,在这一步,把我们自己设置的证书加进去之后,就能验证成功了。再去调用之前的serverTrust去验证该证书是否有效,有可能经过这个方法过滤后,serverTrust里面的pinnedCertificates被筛选到只有信任的那一个证书。
最后,还是获取一个数组并遍历,这个方法和我们之前的锚点证书没关系了,是去从我们需要被验证的服务端证书,去拿证书链。这个数组是服务器端的证书链,注意此处返回的证书链顺序是从叶节点到根节点。
NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
return YES;
}
}
return NO;
如果我们的证书中,有一个和它证书链中的证书匹配的,就返回YES,否则就返回NO。
- 如果是
AFSSLPinningModePublicKey
公钥验证AFSSLPinningModePublicKey
模式同样是用证书绑定(SSL Pinning)
方式验证,客户端要有服务端的证书拷贝,只是验证时只验证证书里的公钥,不验证证书的有效期等信息。只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据。
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;
}
这里利用AFN函数的自定义函数获取公钥的数组集合。
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);
SecCertificateRef someCertificates[] = {certificate};
CFArrayRef certificates = CFArrayCreate(NULL, (const void **)someCertificates, 1, NULL);
SecTrustRef trust;
__Require_noErr_Quiet(SecTrustCreateWithCertificates(certificates, policy, &trust), _out);
SecTrustResultType result;
__Require_noErr_Quiet(SecTrustEvaluate(trust, &result), _out);
[trustChain addObject:(__bridge_transfer id)SecTrustCopyPublicKey(trust)];
_out:
if (trust) {
CFRelease(trust);
}
if (certificates) {
CFRelease(certificates);
}
continue;
}
CFRelease(policy);
return [NSArray arrayWithArray:trustChain];
}
从serverTrust
中取出服务器端传过来的所有可用的证书,并依次得到相应的公钥。然后遍历服务端的公钥,内部嵌套一个遍历是遍历本地公钥,如果相同那么trustedPublicKeyCount + 1
,如果trustedPublicKeyCount > 0
,就是YES,否则就是NO。下面是比较函数。
static BOOL AFSecKeyIsEqualToKey(SecKeyRef key1, SecKeyRef key2) {
#if TARGET_OS_IOS || TARGET_OS_WATCH || TARGET_OS_TV
return [(__bridge id)key1 isEqual:(__bridge id)key2];
#else
return [AFSecKeyGetData(key1) isEqual:AFSecKeyGetData(key2)];
#endif
}
总结
验证流程
- 根据模式,如果是
AFSSLPinningModeNone
,则肯定是返回YES,不论是自签还是公信机构的证书。 - 如果是
AFSSLPinningModeCertificate
,则从serverTrust
中去获取证书链,然后和我们一开始初始化设置的证书集合self.pinnedCertificates
去匹配,如果有一对能匹配成功的,就返回YES,否则NO。 - 如果是
AFSSLPinningModePublicKey
公钥验证,则和第二步一样还是从serverTrust
,获取证书链每一个证书的公钥,放到数组中。和我们的self.pinnedPublicKeys
,去配对,如果有一个相同的,就返回YES,否则NO。
还需注意
- 如果你用的是付费的公信机构颁发的证书,标准的HTTPS,那么无论你用的是AF还是NSURLSession,什么都不用做,代理方法也不用实现。你的网络请求就能正常完成。
- 如果你用的是自签名的证书
- 首先你需要在plist文件中,设置可以返回不安全的请求(关闭该域名的ATS)。
- 其次,如果是NSURLSesion,那么需要在代理方法实现如下
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
__block NSURLCredential *credential = nil;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
// 确定挑战的方式
if (credential) {
//证书挑战 则跑到这里
disposition = NSURLSessionAuthChallengeUseCredential;
}
//完成挑战
if (completionHandler) {
completionHandler(disposition, credential);
}
}
如果是AF,你则需要设置policy
//允许自签名证书,必须的
policy.allowInvalidCertificates = YES;
//是否验证域名的CN字段
//不是必须的,但是如果写YES,则必须导入证书。
policy.validatesDomainName = NO;
AF可以让你在系统验证证书之前,就去自主验证。然后如果自己验证不正确,直接取消网络请求。否则验证通过则继续进行系统验证。
系统的验证,首先是去系统的根证书找,看是否有能匹配服务端的证书,如果匹配,则验证成功,返回https的安全数据。如果不匹配则去判断ATS是否关闭,如果关闭,则返回https不安全连接的数据。如果开启ATS,则拒绝这个请求,请求失败。
参考文章
1. AFNetworking之于https认证
后记
本篇详述了
AFSecurityPolicy
验证服务端的流程,借鉴了很多大神的博客和文章。这里面已经引用列出来了,表示感谢。