AFNetworking源码解读之AFSecurityPolicy模块中的Https验证

来源:互联网 发布:淘宝手机端红包设置 编辑:程序博客网 时间:2024/06/16 03:17

Https简单介绍

可以简单理解为在http的基础上加了证书的认证过程,网上很多Https的介绍以及单向双向认证介绍,这里简单的以单向认证为例
这里写图片描述

第一阶段:ClientHello

客户端发起请求,以明文传输请求信息,包含版本信息,加密套件候选列表,压缩算法候选列表,随机数random_C,扩展字段等信息。

第二阶段:ServerHello-ServerHelloDone

如上图可以看出这个阶段包含4个过程( 有的服务器是单条发送,有的是合并一起发送)。服务端返回协商的信息结果,包括选择使用的协议版本,选择的加密套件,选择的压缩算法、随机数random_S等,其中随机数用于后续的密钥协商。服务器也会配置并返回对应的证书链Certificate,用于身份验证与密钥交换。然后会发送ServerHelloDone信息用于通知服务器信息发送结束。

第三阶段:证书校验

在上图中的5-6之间,客户端这边还需要对服务器返回的证书进行校验。只有证书验证通过后,才能进行后续的通信。(具体分析可参看后续的证书验证过程)

第四阶段:ClientKeyExchange-Finished

服务器返回的证书验证合法后, 客户端计算产生随机数字Pre-master,并用server证书中公钥加密,发送给服务器。同时客户端会根据已有的三个随机数根据相应的生成协商密钥。客户端会通知服务器后续的通信都采用协商的通信密钥和加密算法进行加密通信。然后客户端发送Finished消息用于通知客户端信息发送结束。

第五阶段:服务器端生成协商密钥

服务器也会根据已有的三个随机数使用相应的算法生成协商密钥,会通知客户端后续的通信都采用协商的通信密钥和加密算法进行加密通信。然后发送Finished消息用于通知服务器信息发送结束。

第六阶段:握手结束

在握手阶段结束后,客户端和服务器数据传输开始使用协商密钥进行加密通信。

总结

简单来说,HTTPS请求整个过程主要分为两部分。一是握手过程:用于客户端和服务器验证双方身份,协商后续数据传输时使用到的密钥等。二是数据传输过程:身份验证通过并协商好密钥后,通信双方使用协商好的密钥加密数据并进行通信。在握手过程协商密钥时,使用的是非对称密钥交换算法, 密钥交换算法本身非常复杂,密钥交换过程涉及到随机数生成,模指数运算,空白补齐,加密,签名等操作。在数据传输过程中,客户端和服务器端使用协商好的密钥进行对称加密解密。

iOS 支持Https

在OC中当使用NSURLConnection或NSURLSession建立URL并向服务器发送https请求获取资源时,服务器会使用HTTP状态码401进行响应(即访问拒绝)。此时NSURLConnection或NSURLSession会接收到服务器需要授权的响应,当客户端授权通过后,才能继续从服务器获取数据。
这里写图片描述
这张图真的是精髓,最后会给出出处链接,我们主要理解下第四步之后的认证过程分析,这里用的是NSURLConnection为例做的图,AF3.0之后用的时候NSURLSession,证书挑战是在didReceiveChallenge

先看下原生的NSURLConnection最简单的逻辑实现

1.系统默认方式验证证书(非自签,CA颁发)

//回调- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {    //1)获取trust object    SecTrustRef trust = challenge.protectionSpace.serverTrust;    SecTrustResultType result;    //2)SecTrustEvaluate对trust进行验证    OSStatus status = SecTrustEvaluate(trust, &result);    if (status == errSecSuccess &&        (result == kSecTrustResultProceed ||        result == kSecTrustResultUnspecified)) {        //3)验证成功,生成NSURLCredential凭证cred,告知challenge的sender使用这个凭证来继续连接        NSURLCredential *cred = [NSURLCredential credentialForTrust:trust];        [challenge.sender useCredential:cred forAuthenticationChallenge:challenge];    } else {        //5)验证失败,取消这次验证流程        [challenge.sender cancelAuthenticationChallenge:challenge];  }}

流程分析
1.第一步,先获取需要验证的信任对象(Trust Object)。这个
Trust Object在不同的应用场景下获取的方式都不一样,对于NSURLConnection来说,是从delegate方法
-connection:willSendRequestForAuthenticationChallenge:
回调回来的参数challenge中获取
([challenge.protectionSpace serverTrust])。

2.使用系统默认验证方式验证Trust Object。SecTrustEvaluate会根据Trust Object的验证策略,一级一级往上,验证证书链上每一级数字签名的有效性(上一部分有讲解),从而评估证书的有效性。

3.如第二步验证通过了,一般的安全要求下,就可以直接验证通过,进入到下一步:使用Trust Object生成一份凭证
([NSURLCredential credentialForTrust:serverTrust]),
传入challenge的sender中([challenge.sender useCredential:cred forAuthenticationChallenge:challenge])处理,建立连接。

2.自签证书或者指定CA认证证书之导入本地

根据上面的CA系统认证步骤,基本上所有的CA认证都能通过,那么为了加强安全,就是把证书导入本地进行匹配
4.假如有更强的安全要求,可以继续对Trust Object进行更严格的验证。常用的方式是在本地导入证书,验证Trust Object与导入的证书是否匹配。
5.假如验证失败,取消此次Challenge-Response Authentication验证流程,拒绝连接请求。

假如是自建证书的,你如果调用SecTrustEvaluate 进行系统根证书认证,铁定失败,因为自建证书的根CA的数字签名未在操作系统的信任列表中。又或者,即使服务器返回的证书是信任CA签发的,又如何确定这证书就是我们想要的特定证书?这就需要先在本地导入证书,设置成需要参与验证的Anchor Certificate(锚点证书,通过SecTrustSetAnchorCertificates设置了参与校验锚点证书之后,假如验证的数字证书是这个锚点证书的子节点,即验证的数字证书是由锚点证书对应CA或子CA签发的,或是该证书本身,则信任该证书),再调用SecTrustEvaluate来验证

//先导入证书NSString * cerPath = ...; //证书的路径NSData * cerData = [NSData dataWithContentsOfFile:cerPath];SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)(cerData));self.trustedCertificates = @[CFBridgingRelease(certificate)];//回调- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {    //1)获取trust object    SecTrustRef trust = challenge.protectionSpace.serverTrust;    SecTrustResultType result;    //注意:这里将之前导入的证书设置成下面验证的Trust Object的anchor certificate    SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)self.trustedCertificates);    //2)SecTrustEvaluate会查找前面SecTrustSetAnchorCertificates设置的证书或者系统默认提供的证书,对trust进行验证    OSStatus status = SecTrustEvaluate(trust, &result);    if (status == errSecSuccess &&        (result == kSecTrustResultProceed ||        result == kSecTrustResultUnspecified)) {        //3)验证成功,生成NSURLCredential凭证cred,告知challenge的sender使用这个凭证来继续连接        NSURLCredential *cred = [NSURLCredential credentialForTrust:trust];        [challenge.sender useCredential:cred forAuthenticationChallenge:challenge];    } else {        //5)验证失败,取消这次验证流程        [challenge.sender cancelAuthenticationChallenge:challenge];  }}

以上就是最基本的流程也是最清晰的,以下就是AF暴露出来的几个字段分析

AFNetworking的方式来实现上述两种方式的认证

AFNetworking关键属性

首先看下AF里面AFSecurityPolicy暴露出来的方法

//https验证模式 默认是无,还有证书匹配和公钥匹配@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode;//可以去匹配服务端证书验证的证书 当我们不主动设置pinningMode的时候不会主动该字段是空的,如果主动设置,会调用默认读取certificatesInBundle .cer的证书进行赋值 源码里面有@property (nonatomic, strong, nullable) NSSet <NSData *> *pinnedCertificates;//allowInvalidCertificates 是否允许无效证书(也就是自建的证书),默认为NO//如果是需要验证自建证书,需要设置为YES 一般测试的时候YES,https开启就弄为NO@property (nonatomic, assign) BOOL allowInvalidCertificates;//validatesDomainName 是否需要验证域名,默认为YES;//假如证书的域名与你请求的域名不一致,需把该项设置为NO;如设成NO的话,即服务器使用其他可信任机构颁发的证书,也可以建立连接,这个非常危险,建议打开。//置为NO,主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的;当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。//如置为NO,建议自己添加对应域名的校验逻辑。@property (nonatomic, assign) BOOL validatesDomainName;

AFNetworking中核心认证代码

- (void)URLSession:(NSURLSession *)sessiondidReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler{//挑战处理类型为 默认    /*     NSURLSessionAuthChallengePerformDefaultHandling:默认方式处理     NSURLSessionAuthChallengeUseCredential:使用指定的证书     NSURLSessionAuthChallengeCancelAuthenticationChallenge:取消挑战     */    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;    // 服务器挑战的证书    __block NSURLCredential *credential = nil;// 这个Block是提供给用户自定义证书挑战方式的,比如是否需要自定义    if (self.sessionDidReceiveAuthenticationChallenge) {        disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);    } else {    // NSURLAuthenticationMethodServerTrust 单向认证关系 也就是说服务器端需要客户端返回一个根据认证挑战的保护空间提供的信任产生的挑战证书        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {        // 基于客户端的安全策略来决定是否信任该服务器,不信任的话,也就没必要响应挑战            if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {             // 创建挑战证书(注:挑战方式为UseCredential和PerformDefaultHandling都需要新建挑战证书)                credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];                if (credential) {                    disposition = NSURLSessionAuthChallengeUseCredential;                } else {                    disposition = NSURLSessionAuthChallengePerformDefaultHandling;                }            } else {                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;            }        } else {            disposition = NSURLSessionAuthChallengePerformDefaultHandling;        }    }// 完成挑战    if (completionHandler) {        completionHandler(disposition, credential);    }}

步骤:
1.当启动Https的链接访问的时候 会进到这个代理方法里面,默认设置证书挑战方式
2.通过是否有block的实现,需要外部自定义,如果不是默认的认证服务器证书,就需要自定义,没有就进入系统默认环节
3.判断是否是验证服务端证书单向认证
NSURLAuthenticationMethodServerTrust
4.是的话通过设置的认证政策进行认证,核心方法
evaluateServerTrust:该方法下面介绍
5.然后通过这个方法内部认证之后返回的证书以及认证方式返回到服务器进行下一步连接

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust                  forDomain:(NSString *)domain{// 如果需要强域名校验的时候,而且允许自建证书,pinningMode不能为Non,必须为后两个,不然就认证失败    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()];    }// 为serverTrust设置验证策略,即告诉客户端如何验证serverTrust    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);// 核心判断 如果是PinningModeNone,如果允许自建证书,直接反悔YES,如果不允许自建证书,就调用系统跟证书认证方式去验证服务器证书    if (self.SSLPinningMode == AFSSLPinningModeNone) {        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);        // 前提是不允许自建证书,而且服务器证书也是自建证书,自然无法通过系统根证书认证,一般自建证书无法通过这个判断,就返回NO    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {        return NO;    }// 强行过滤 避免上面的if出错又进来这里,直接返回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;}

步骤:
1.根据PinningMode 如果是Non,那么而且allowInvalidCertificates也是YES,就是允许自建证书,而且validatesDomainName有需要强域名校验,矛盾,直接返回认证失败
2.过了第一个If,如果validatesDomainName有强域名校验,添加政策
3.如果AFSSLPinningModeNone,allowInvalidCertificates如果为YES,允许自检证书,直接返回YES,一般就是测试的时候用,如果为NO,就校验系统证书判断,如果是自建证书,认证失败,如果是CA机构的,认证成功
4.如果认证方式不是AFSSLPinningModeNone,而且不允许自检证书,allowInvalidCertificates为NO,如果是CA证书颁发,那么进入后续判断,但是有时自建证书,又被干掉,返回NO,认证失败
5.最后的Switch根据证书或者公钥进行证书匹配,匹配到就是成功,反之

AF实现系统默认方式的认证证书

1.根据上面的代码,系统默认的方式无非就是调用Https链接的时候进入这个代理方法,然后我们根据服务器的证书,调用AF封装的系统认证方法AFServerTrustIsValid 认证证书,也就是进入下面的方法认证证书,因此,你什么都不用干,AF下的默认属性就会去拿服务器的证书进行系统证书认证

if (self.SSLPinningMode == AFSSLPinningModeNone) {        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {        return NO;    }

AF下实现指定证书认证

即使服务器返回的证书是信任CA签发的,又如何确定这证书就是我们想要的特定证书?这就需要先在本地导入证书,然后把它设置为锚点
1.指定认证认证肯定是要把证书导入本地的
2.你可以用sessionDidReceiveAuthenticationChallenge自定义Block的方式回调自己的验证方式
3.但是根据AF源码,当我们这是PinningMode的时候,AF内部会根据.cer后缀去拿指定的证书设置为校验的证书,继续点进去defaultPinnedCertificates,就能找到AF中拿证书的方法

+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode {    return [self policyWithPinningMode:pinningMode withPinnedCertificates:[self defaultPinnedCertificates]];}

因此,最后只需要拖进去证书,然后设置几个属性即可

AFSecurityPolicy  *securityPolicy_f = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];//AFSSLPinningModeNone    securityPolicy_f.validatesDomainName = YES;    securityPolicy_f.allowInvalidCertificates = NO;    manager.securityPolicy = securityPolicy_f;

这里预埋证书有个问题
这里写图片描述

虽然安全得到了保障。但是证书生命周期的结束,会导致线上的app无法访问了,这里虽然我们可以提前几个版本把最新的https证书也预埋进来,那么两个证书都进行校验,但是感觉这还是会导致一部分不更新的用户无法使用app,难道再来个app强制更新??他们的建议是不要用预埋的手段,也就是不要校验指定证书了,直接用系统的默认方法,校验系统根证书的方式,也就是AF的第一张方式,啥都不用做,就是默认的方式,但是也会有个问题,只要是CA颁发的都能通过认证,还是想不到更好的方式
1.如果预埋,证书更新的时候老版本如何兼容??
2.如果用系统默认的校验,如何保证安全性?混淆?强域名校验?

在安全策略允许的情况下取消证书检查功能,改为使用系统自带信任库方式验证,并配合使用其他比如域名强验证,使用Proguard混淆代码,使用编译库等手段来确保您的APP安全。如果有大神知道好的方案,可以留言,谢了

AF下实现自签证书的认证

个人感觉,自签证书就是用来测试
1.第一种你可以打开allowInvalidCertificates,不过也是仅供测试
2.第二种你可以用
setSessionDidReceiveAuthenticationChallengeBlock来提前把自签证书作为锚点证书进行校验,就能通过认证
3.有些人遇到自签证书一直无法通过,可以看看这个传送门

小知识点

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;}#ifndef __Require_noErr_Quiet    #define __Require_noErr_Quiet(errorCode, exceptionLabel)                      \      do                                                                          \      {                                                                           \          if ( __builtin_expect(0 != (errorCode), 0) )                            \          {                                                                       \              goto exceptionLabel;                                                \          }                                                                       \      } while ( 0 )#endif

这里有个方法就是AF校验证书的过程,这里的宏语法很怪异,单独写了一篇__builtin_expect语法

几个验证方法介绍

//1.创建一个验证SSL的策略,两个参数,第一个参数true则表示验证整个证书链
//第二个参数传入domain,用于判断整个证书链上叶子节点表示的那个domain是否和此处传入domain一致
SecPolicyCreateSSL(<#Boolean server#>, <#CFStringRef _Nullable hostname#>)
SecPolicyCreateBasicX509();
//2.默认的BasicX509验证策略,不验证域名。
SecPolicyCreateBasicX509();
//3.为serverTrust设置验证策略,即告诉客户端如何验证serverTrust
SecTrustSetPolicies(<#SecTrustRef _Nonnull trust#>, <#CFTypeRef _Nonnull policies#>)
//4.验证serverTrust,并且把验证结果返回给第二参数 result
SecTrustEvaluate(<#SecTrustRef _Nonnull trust#>, <#SecTrustResultType * _Nullable result#>)
//5.判断前者errorCode是否为0,为0则跳到exceptionLabel处执行代码
__Require_noErr(<#errorCode#>, <#exceptionLabel#>)
//6.根据证书data,去创建SecCertificateRef类型的数据。
SecCertificateCreateWithData(<#CFAllocatorRef _Nullable allocator#>, <#CFDataRef _Nonnull data#>)
//7.给serverTrust设置锚点证书,即如果以后再次去验证serverTrust,会从锚点证书去找是否匹配。
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
//8.拿到证书链中的证书个数
CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
//9.去取得证书链中对应下标的证书。
SecTrustGetCertificateAtIndex(serverTrust, i)
//10.根据证书获取公钥。
SecTrustCopyPublicKey(trust)



Jamin’sBLOG Https
iOS 中 HTTPS 证书验证浅析
AFNetworking之于https认证

阅读全文
0 0
原创粉丝点击