前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >AFNetworking源码探究(十四) —— AFSecurityPolicy与安全认证 (二

AFNetworking源码探究(十四) —— AFSecurityPolicy与安全认证 (二

原创
作者头像
conanma
修改2021-09-03 17:59:02
8620
修改2021-09-03 17:59:02
举报
文章被收录于专栏:正则正则

回顾

上一篇主要讲述了HTTPS认证原理以及AFSecurityPolicy的实例化。这一篇就具体的看一下验证流程。


验证服务端

还记得上一篇那个验证服务端的那个方法吗?具体如下表示。

代码语言:javascript
复制
- (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判断

代码语言:javascript
复制
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) 安全策略

主要对应下面这段代码

代码语言:javascript
复制
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一致。

我们先看第一个函数

代码语言:javascript
复制
/*!
 @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验证策略

看一下这个函数

代码语言:javascript
复制
/*!
 @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。具体这个函数的接口如下所示:

代码语言:javascript
复制
/*!
    @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) 验证模式的判断

代码语言:javascript
复制
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是否有效,下面的是验证的函数。

代码语言:javascript
复制
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。

代码语言:javascript
复制
else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        return NO;
}

接着就是switch条件句判断SSLPinningMode这个验证模式。

  • 如果是AFSSLPinningModeNone
代码语言:javascript
复制
case AFSSLPinningModeNone:
default:
    return NO;

这种就是默认返回的NO。

  • 如果是AFSSLPinningModeCertificate
代码语言:javascript
复制
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表示验证证书类型。

首先实例化一个可变数组

代码语言:javascript
复制
NSMutableArray *pinnedCertificates = [NSMutableArray array];

下面看一个集合属性

代码语言:javascript
复制
/**
 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 <NSData *> *pinnedCertificates;

默认情况下,该属性设置为包含在目标编译AFNetworking中的任何(.cer)证书。 请注意,如果您使用AFNetworking作为嵌入式框架,则默认情况下不会锁定任何证书。 使用certificatesInBundle从你的目标加载证书,然后通过调用policyWithPinningMode:withPinnedCertificates来创建一个新的策略。

请注意,如果启用了锁定,如果任何固定证书匹配,evaluateServerTrust:forDomain:将返回true。

接着就是对该集合对象进行遍历

代码语言:javascript
复制
for (NSData *certificateData in self.pinnedCertificates) {
    [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}

把证书数据,用系统返回类型为SecCertificateRefSecCertificateCreateWithData函数对原先的certificateData做一些处理,保证返回的证书都是DER编码的X.509证书。下面看一下这个函数。

代码语言:javascript
复制
/*!
 @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签发的,或是该证书本身,则信任该证书)。先看一下这个函数。

代码语言:javascript
复制
/*!
    @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);

然后,接着进行判断

代码语言:javascript
复制
if (!AFServerTrustIsValid(serverTrust)) {
    return NO;
}

自签在之前是验证通过不了的,在这一步,把我们自己设置的证书加进去之后,就能验证成功了。再去调用之前的serverTrust去验证该证书是否有效,有可能经过这个方法过滤后,serverTrust里面的pinnedCertificates被筛选到只有信任的那一个证书。

最后,还是获取一个数组并遍历,这个方法和我们之前的锚点证书没关系了,是去从我们需要被验证的服务端证书,去拿证书链。这个数组是服务器端的证书链,注意此处返回的证书链顺序是从叶节点到根节点。

代码语言:javascript
复制
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)方式验证,客户端要有服务端的证书拷贝,只是验证时只验证证书里的公钥,不验证证书的有效期等信息。只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据。

代码语言:javascript
复制
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函数的自定义函数获取公钥的数组集合。

代码语言:javascript
复制
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。下面是比较函数。

代码语言:javascript
复制
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,那么需要在代理方法实现如下
代码语言:javascript
复制
- (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

代码语言:javascript
复制
//允许自签名证书,必须的
policy.allowInvalidCertificates = YES;

//是否验证域名的CN字段
//不是必须的,但是如果写YES,则必须导入证书。
policy.validatesDomainName = NO;
  • AF可以让你在系统验证证书之前,就去自主验证。然后如果自己验证不正确,直接取消网络请求。否则验证通过则继续进行系统验证。
  • 系统的验证,首先是去系统的根证书找,看是否有能匹配服务端的证书,如果匹配,则验证成功,返回https的安全数据。如果不匹配则去判断ATS是否关闭,如果关闭,则返回https不安全连接的数据。如果开启ATS,则拒绝这个请求,请求失败。

参考文章

1. AFNetworking之于https认证

后记

本篇详述了AFSecurityPolicy验证服务端的流程,借鉴了很多大神的博客和文章。这里面已经引用列出来了,表示感谢。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 回顾
  • 验证服务端
  • 总结
  • 参考文章
  • 后记
相关产品与服务
SSL 证书
腾讯云 SSL 证书(SSL Certificates)为您提供 SSL 证书的申请、管理、部署等服务,为您提供一站式 HTTPS 解决方案。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档