JHHK

欢迎来到我的个人网站
行者常至 为者常成

AFN源码分析(一)

目录

单项认证

img

单向认证的securityPolicy

一、默认方式(CA签发证书)

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.securityPolicy = [AFSecurityPolicy defaultPolicy];

看下defaultPolicy方法

// 创建默认的实例
+ (instancetype)defaultPolicy {
    AFSecurityPolicy *securityPolicy = [[self alloc] init];
    securityPolicy.SSLPinningMode = AFSSLPinningModeNone;// CA
    return securityPolicy;
}

SSL验证模式

typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
     //不使用固定证书(本地)验证服务器。直接从客户端系统中的受信任颁发机构 CA 列表中去验证
    AFSSLPinningModeNone,

    //代表会对服务器返回的证书中的PublicKey进行验证,通过则通过,否则不通过
    AFSSLPinningModePublicKey,

    //代表会对服务器返回的证书同本地证书全部进行校验,通过则通过,否则不通过
    AFSSLPinningModeCertificate,
};

使用CA签发的证书,可以使用默认的securityPolicy,不需要开发者进行额外的处理。

二、自定义方式

1、初始化mananger并设置securityPolicy

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.securityPolicy        = [self securityPolicy];

2、设置自定义的securityPolicy

- (AFSecurityPolicy *)securityPolicy{
//从本地获取证书数据
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"https" ofType:@"cer"];
NSData   *data    = [NSData dataWithContentsOfFile:cerPath];
//可能不止一个证书,所以使用集合
NSSet * cerSet  = [NSSet setWithObject:data];

//指定安全策略和证书集合生成自定义的AFSecurityPolicy
AFSecurityPolicy * security = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate
                                            withPinnedCertificates:cerSet];

//允许使用自签证书
security.allowInvalidCertificates = YES;

//不校验域名
security.validatesDomainName      = NO;
return security;
}

自定义的验签逻辑适用于自签证书,但并不是说CA签发的证书就不适用。如果我们本地存放了CA的证书也是可以使用自定义的验签逻辑的。

3、读取本地公钥

// 根据SSL验证模式和指定的证书集合创建实例
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode withPinnedCertificates:(NSSet *)pinnedCertificates {
    AFSecurityPolicy *securityPolicy = [[self alloc] init];

    securityPolicy.SSLPinningMode = pinningMode;

    // 设置证书集合 如果是默认的已经通过[self defaultPinnedCertificates]得到了
    [securityPolicy setPinnedCertificates:pinnedCertificates];

    return securityPolicy;
}
// 此函数设置securityPolicy中的pinnedCertificates属性
// 注意还将对应的self.pinnedPublicKeys属性也设置了,该属性表示的是对应证书的公钥(与pinnedCertificates中的证书是一一对应的)
- (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;
    }
}

这两个函数的主要作用是收集本地的证书放入一个集合,并将证书内的公钥提取放入另一个集合,当对服务端证书验证(身份认证)时从中取出相应的证书作为锚点证书对服务端证书验证。

4、比对验证

https的单向认证需要服务端将其证书返回给客户端,客户端进行验证通过后方可执行后续逻辑。服务器的证书就是通过这个回调方法以challenge(挑战)的方式返回的。 看下官方文档对该方法的说明。

该代理方法会在下面两种情况调用:

(1)当某个session使用SSL/TLS协议,第一次和服务器端建立连接的时候,服务器会发送给iOS客户端一个证书,此方法允许你的app验证服务端的证书链(certificate keychain)

(2)当服务器端要求客户端提供证书时,此方法允许你的app提供正确的挑战证书。

- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{


    //挑战处理类型为 默认
    /*
     NSURLSessionAuthChallengeUseCredential:使用指定的证书
     NSURLSessionAuthChallengePerformDefaultHandling:默认方式处理
     NSURLSessionAuthChallengeCancelAuthenticationChallenge:取消挑战
     NSURLSessionAuthChallengeRejectProtectionSpace:拒绝此挑战,并尝试下一个验证保护空间;忽略证书参数
     */

    //挑战处理类型为默认
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;//证书

    // 自定义方法,用来如何应对服务器端的认证挑战
    if (self.sessionDidReceiveAuthenticationChallenge) {
        disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
    } else {

        // 1.判断接收服务器挑战的方法是否是信任证书
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            //只需要验证服务端证书是否安全(即https的单向认证,这是AF默认处理的认证方式,其他的认证方式,只能由我们自定义Block的实现
            if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
                 // 2.信任评估通过,就从受保护空间里面拿出证书,回调给服务器,告诉服务,我信任你,你给我发送数据吧.
                credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
               // 确定挑战的方式
                if (credential) {
                    //证书挑战
                    disposition = NSURLSessionAuthChallengeUseCredential;
                } else {
                    //默认挑战
                    disposition = NSURLSessionAuthChallengePerformDefaultHandling;
                }
            } else {
                 //取消挑战
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        } else {
            //默认挑战方式
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    }
 //完成挑战
    // 3.将信任凭证发送给服务端
    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}
//验证服务端是否值得信任
/*
 SecTrustRef:其实就是一个容器,装了服务器端需要验证的证书的基本信息、公钥等等,不仅如此,它还可以装一些评估策略,还有客户端的锚点证书,这个客户端的证书,可以用来和服务端的证书去匹配验证的。
 每一个SecTrustRef对象包含多个SecCertificateRef 和 SecPolicyRef。其中 SecCertificateRef 可以使用 DER 进行表示。
 domain:服务器域名,用于域名验证
 */
 
// 根据severTrust和domain来检查服务器端发来的证书是否可信
// 其中SecTrustRef是一个CoreFoundation类型,用于对服务器端传来的X.509证书评估的
//而我们都知道,数字证书的签发机构CA,在接收到申请者的资料后进行核对并确定信息的真实有效,然后就会制作一份符合X.509标准的文件。证书中的证书内容包含的持有者信息和公钥等都是由申请者提供的,而数字签名则是CA机构对证书内容进行hash加密后得到的,而这个数字签名就是我们验证证书是否是有可信CA签发的数据。

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

       //如果有服务器域名、设置了允许信任无效或者过期证书(自签名证书)、需要验证域名、没有提供证书或者不验证证书,返回no。后两者和allowInvalidCertificates为真的设置矛盾,说明这次验证是不安全的。
    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."
        //如果想要实现自签名的HTTPS访问成功,必须设置pinnedCertificates,且不能使用defaultPolicy
        NSLog(@"In order to val idate a domain name for self signed certificates, you MUST use pinning.");
        //不受信任,返回
        return NO;
    }
    //用来装验证策略
    NSMutableArray *policies = [NSMutableArray array];
    //生成验证策略。如果要验证域名,就以域名为参数创建一个策略,否则创建默认的basicX509策略
    if (self.validatesDomainName) {
         // 如果需要验证domain,那么就使用SecPolicyCreateSSL函数创建验证策略,其中第一个参数为true表示为服务器证书验证创建一个策略,第二个参数传入domain,匹配主机名和证书上的主机名
        //1.__bridge:CF和OC对象转化时只涉及对象类型不涉及对象所有权的转化
        //2.__bridge_transfer:常用在讲CF对象转换成OC对象时,将CF对象的所有权交给OC对象,此时ARC就能自动管理该内存
        //3.__bridge_retained:(与__bridge_transfer相反)常用在将OC对象转换成CF对象时,将OC对象的所有权交给CF对象来管理
        [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);

//如果是AFSSLPinningModeNone(不做本地证书验证,从客户端系统中的受信任颁发机构 CA 列表中去验证服务端返回的证书)
    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        //不使用ssl pinning 但允许自建证书,直接返回YES;否则进行第二个条件判断,去客户端系统根证书里找是否有匹配的证书,验证serverTrust是否可信,直接返回YES
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        //如果验证无效AFServerTrustIsValid,而且allowInvalidCertificates不允许自签,返回NO
        return NO;
    }

    switch (self.SSLPinningMode) {
//上一部分已经判断过了,如果执行到这里的话就返回NO
        case AFSSLPinningModeNone:
        default:
            return NO;
            //验证证书类型
            // 这个模式表示用证书绑定(SSL Pinning)方式验证证书,需要客户端保存有服务端的证书拷贝
            // 注意客户端保存的证书存放在self.pinnedCertificates中
        case AFSSLPinningModeCertificate: {
            // 全部校验(nsbundle .cer)
            NSMutableArray *pinnedCertificates = [NSMutableArray array];
            
            //把证书data,用系统api转成 SecCertificateRef 类型的数据,SecCertificateCreateWithData函数对原先的pinnedCertificates做一些处理,保证返回的证书都是DER编码的X.509证书
            for (NSData *certificateData in self.pinnedCertificates) {
                //cf arc brige:cf对象和oc对象转化 __bridge_transfer:把cf对象转化成oc对象
                //brige retain:oc转成cf对象
                [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
            }
            // 将pinnedCertificates设置成需要参与验证的Anchor Certificate(锚点证书,通过SecTrustSetAnchorCertificates设置了参与校验锚点证书之后,假如验证的数字证书是这个锚点证书的子节点,即验证的数字证书是由锚点证书对应CA或子CA签发的,或是该证书本身,则信任该证书),具体就是调用SecTrustEvaluate来验证
             //serverTrust是服务器来的验证,有需要被验证的证书
            // 把本地证书设置为根证书,
            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);
            //reverseObjectEnumerator逆序
            // 倒序遍历
            //这里的证书链顺序是从叶节点到根节点
            for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
                //如果我们的证书中,有一个和它证书链中的证书匹配的,就返回YES
                // 是否本地包含相同的data
                if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
                    return YES;
                }
            }
            //没有匹配的
            return NO;
        }
            //公钥验证 AFSSLPinningModePublicKey模式同样是用证书绑定(SSL Pinning)方式验证,客户端要有服务端的证书拷贝,只是验证时只验证证书里的公钥,不验证证书的有效期等信息。只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据
 
        case AFSSLPinningModePublicKey: {
            NSUInteger trustedPublicKeyCount = 0;
            // 从serverTrust中取出服务器端传过来的所有可用的证书,并依次得到相应的公钥
            NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
            //遍历服务端公钥
            for (id trustChainPublicKey in publicKeys) {
                //遍历本地公钥
                for (id pinnedPublicKey in self.pinnedPublicKeys) {
                    //判断如果相同 trustedPublicKeyCount+1
                    if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
                        trustedPublicKeyCount += 1;
                    }
                }
            }
            return trustedPublicKeyCount > 0;
        }
    }
    
    return NO;
}

以上是AFN进行单向认证的流程,更具体的实现就去巴拉源码吧,这里只做一个引导。

在这里再推荐一篇博客,详细讲解了服务端使用CA证书和自签证书时客户端对证书链的验证过程以及存在的安全问题。

双向认证

img

双向认证securityPolicy

AFN只对单向认证做了处理,那么如果是双向认证的话怎么处理呢?

一、在AFN的源码内添加验证逻辑

在AFURLSessionManager.m文件的回调方法

- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler

内添加一个逻辑分支,如下

- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler{
    //挑战处理类型为默认
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;

    // 自定义方法,用来如何应对服务器端的认证挑战
    if (self.sessionDidReceiveAuthenticationChallenge) {
        disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
    } else {

        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            
           //校验后台证书的逻辑(使用AFN自己的代码逻辑处理,这里就不在贴出AFN的源码了)

        }else if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate]){
            //这里就是我们自己在源码内添加的逻辑分支

            //此处整理客户端证书
            NSString * p12Path = [[NSBundle mainBundle] pathForResource:@"client" ofType:@"p12"];
            NSData * clientP12Data = [NSData dataWithContentsOfFile:p12Path];
            

            //AFHTTPDisposition是自己创建的一个类并不是AFN的代码,具体实现会在文章最后贴出
            disposition = [AFHTTPDisposition disposeClientCer:clientP12Data
                                                  cerPassword:@"123456"
                                                    challenge:challenge
                                                   credential:credential];

        }else {
            //默认挑战方式
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    }

    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}

这样双认证的处理逻辑就添加完成了,这样处理有优点也有缺点:

优点:服务端的证书校验不需要我们自己处理了,会走之前的单向认证的证书校验逻辑。我们只添加客户端证书的处理逻辑即可。

缺点:修改了AFN的源码,当我们更新AFN的库时,会将我们自己修改的部分覆盖掉。

二、AFN提供了自定义的验证逻辑

另外我们注意到在验证回调的方法里有这么一个逻辑分支会被优先判断

if (self.sessionDidReceiveAuthenticationChallenge) {
  disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
}

这就是AFN为我们自己处理验证逻辑提供的一个block回调。可通过AFHTTPSessionManager的实例方法setSessionDidReceiveAuthenticationChallengeBlock:进行设置

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];

[manager setSessionDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession * _Nonnull session,
                                                                                                NSURLAuthenticationChallenge * _Nonnull challenge, NSURLCredential * _Nullable __autoreleasing * _Nullable credential) {
    
    // 如果使用默认的处置方式,那么 credential 就会被忽略
    NSURLSessionAuthChallengeDisposition  disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        
        //此处校验服务端证书
        NSString * anchorCerPath     = [[NSBundle mainBundle] pathForResource:@"debugServer" ofType:@"cer"];
        NSData   * anchorCerPathData = [NSData dataWithContentsOfFile:anchorCerPath];
        
        //AFHTTPDisposition自定义类,并非AFN源码
        disposition = [AFHTTPDisposition verifyServerCerWithAnchorCer:anchorCerPathData
                                                            isOnlyAnchor:true
                                                            challenge:challenge
                                                            credential:credential];
        
        
    }else if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate]){
        
        //此处整理客户端证书
        NSString * p12Path = [[NSBundle mainBundle] pathForResource:@"lakalaclient" ofType:@"p12"];
        NSData * clientP12Data = [NSData dataWithContentsOfFile:p12Path];
        
        //AFHTTPDisposition自定义类,并非AFN源码
        disposition = [AFHTTPDisposition disposeClientCer:clientP12Data
                                                cerPassword:@"071910"
                                                challenge:challenge
                                                credential:credential];
    }else{
        disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    }
    
    return disposition;
}];

这样我们就通过自定义的方式,完成了双向认证。这里需要说明的是AFHTTPDisposition内的锚点这书和客户端证书都是针对单个证书设计的,但实际情况是锚点证书可以是多个,用来验证证书链中的不同证书。客户端提供给后台的证书也可以是多个。另外还需要说明的是关于域名是否验证的安全问题,具体看下文章开头的的参考文章,里面有部分说明。

AFHTTPDisposition实现

AFHTTPDisposition.h文件

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface AFHTTPDisposition : NSObject

/// 服务端证书验证
/// @param cerData 锚点证书
/// @param isOnly 是否只使用锚点证书验证(YES只使用锚定证书,NO允许使用系统的CA根证书)
/// @param challenge 质询
/// @param credential 凭证
+(NSURLSessionAuthChallengeDisposition)verifyServerCerWithAnchorCer:(NSData*)cerData
                                                       isOnlyAnchor:(BOOL)isOnly
                                                          challenge:(NSURLAuthenticationChallenge*)challenge
                                                         credential:(NSURLCredential* _Nullable __autoreleasing *_Nullable)credential;





/// 客户端证书准备(服务器发出对客户端证书的验证请求后)
/// @param cerData 证书二进制数据(P12)
/// @param cerPassword 证书密码(P12)
/// @param challenge 质询
/// @param credential 凭证
+(NSURLSessionAuthChallengeDisposition)disposeClientCer:(NSData*)cerData
                                            cerPassword:(NSString*)cerPassword
                                              challenge:(NSURLAuthenticationChallenge*)challenge
                                             credential:(NSURLCredential* _Nullable __autoreleasing *_Nullable)credential;

@end

NS_ASSUME_NONNULL_END

AFHTTPDisposition.m文件

#import "AFHTTPDisposition.h"

@implementation AFHTTPDisposition


#pragma mark - 服务端证书验证

/// 服务端证书验证
/// @param cerData 锚点证书
/// @param isOnly 是否只使用锚点证书验证(YES只是用锚定证书,NO允许使用系统的CA根证书)
/// @param challenge 质询
/// @param credential 凭证
+(NSURLSessionAuthChallengeDisposition)verifyServerCerWithAnchorCer:(NSData*)cerData
                                                       isOnlyAnchor:(BOOL)isOnly
                                                          challenge:(NSURLAuthenticationChallenge*)challenge
                                                         credential:(NSURLCredential* _Nullable __autoreleasing *_Nullable)credential{
    
    NSParameterAssert(cerData);
    
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
    
    SecTrustRef trust = challenge.protectionSpace.serverTrust;
    
    
    //打印验证策略
    //    CFArrayRef policiesRef;
    //    SecTrustCopyPolicies(trust, &policiesRef);
    //    NSLog(@"[lilog]:policiesRef = %@",policiesRef);
    
    
    //BasicX509不验证域名是否相同(存在安全性)
    //    NSMutableArray *policies = [NSMutableArray array];
    //    SecPolicyRef policy = SecPolicyCreateBasicX509();
    //    [policies addObject:(__bridge_transfer id)policy];
    //    SecTrustSetPolicies(trust, (__bridge CFArrayRef)policies);
    
    

    NSMutableArray *certificates = [NSMutableArray array];
    SecCertificateRef cerRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)cerData);
    [certificates addObject:(__bridge_transfer id)cerRef];
    
    // 设置锚点证书。
    SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)certificates);
    
    //true 代表仅被传入的证书作为锚点,
    //false 允许系统CA证书也作为锚点
    SecTrustSetAnchorCertificatesOnly(trust, isOnly);
    
    
    BOOL isValid = [self isVaildForServerTrust:trust];
    
    if (isValid) {
        *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
        if (*credential) {
            disposition = NSURLSessionAuthChallengeUseCredential;
        }
    }else{
        disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
    }
    
    return disposition;
}



+(BOOL)isVaildForServerTrust:(SecTrustRef)trust{
    
    BOOL allowConnection = NO;
    
    // 假设验证结果是无效的
    SecTrustResultType trustResult = kSecTrustResultInvalid;
    
    // 函数的内部递归地从叶节点证书到根证书的验证
    OSStatus statue = SecTrustEvaluate(trust, &trustResult);
    
    if (statue == noErr) {
        // kSecTrustResultProceed: 用户加入自己的信任锚点,显式地告诉系统这个证书是值得信任的
        // kSecTrustResultUnspecified: 系统隐式地信任这个证书
        allowConnection = (trustResult == kSecTrustResultProceed || trustResult == kSecTrustResultUnspecified);
    }
    return allowConnection;
}




#pragma mark - 客户端证书准备


/// 客户端证书准备(服务器发出对客户端证书的验证请求后)
/// @param cerData 证书二进制数据(P12)
/// @param cerPassword 证书密码(P12)
/// @param challenge 质询
/// @param credential 凭证
+(NSURLSessionAuthChallengeDisposition)disposeClientCer:(NSData*)cerData
                                            cerPassword:(NSString*)cerPassword
                                              challenge:(NSURLAuthenticationChallenge*)challenge
                                             credential:(NSURLCredential* _Nullable __autoreleasing *_Nullable)credential{
    
    NSParameterAssert(cerData);
    CFDataRef inPKCS12Data = (CFDataRef)CFBridgingRetain(cerData);
    SecIdentityRef identity;
    
    // 读取p12证书中的内容
    [self extractP12Data:inPKCS12Data password:cerPassword toIdentity:&identity];
    
    SecCertificateRef certificate = NULL;
    
    SecIdentityCopyCertificate (identity, &certificate);
    const void *certs[] = {certificate};
    CFArrayRef certArray = CFArrayCreate(kCFAllocatorDefault, certs, 1, NULL);
    
    
    //凭证
    *credential = [NSURLCredential credentialWithIdentity:identity
                                             certificates:(NSArray*)CFBridgingRelease(certArray)
                                              persistence:NSURLCredentialPersistencePermanent];
    
    
    if (*credential) {
        return NSURLSessionAuthChallengeUseCredential;
    }
    
    
    /* 无效的话,取消 */
    return NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}


+(OSStatus)extractP12Data:(CFDataRef)inP12Data password:(NSString*)password toIdentity:(SecIdentityRef*)identity {
    OSStatus securityError = errSecSuccess;
    //证书密码
    CFStringRef passwordRef =(__bridge CFStringRef)password;
    const void *keys[] = { kSecImportExportPassphrase };
    const void *values[] = { passwordRef };
    CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
    CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
    securityError = SecPKCS12Import(inP12Data, options, &items);
    if (securityError == 0) {
        CFDictionaryRef ident = CFArrayGetValueAtIndex(items,0);
        const void *tempIdentity = NULL;
        tempIdentity = CFDictionaryGetValue(ident, kSecImportItemIdentity);
        *identity = (SecIdentityRef)tempIdentity;
    }
    if (options) {
        CFRelease(options);
    }
    return securityError;
}

@end

说在最后

最后说下证书的存储问题,当我们的应用开发编译打包完成后,资源文件会被打包进APP包内。当用户下载了我们的APP,这些资源文件是可以拿到的。如果是公钥证书我们可以放在本地,即使被别人拿到了也不存在安全问题。但是如果是P12文件直接放在包内被别人拿到了就危险了。所以我们一般的做法是:

1、读取P12文件的二进制数据转码成base64字符串

2、对base64字符串进行变换或加密(比如简单的异或运算或其它你自己定义的规则)后,放在代码里

3、使用时逆变换或解密得到base64串,然后在转二进制数据。

这样可以最大限度的保证p12文件内的数据安全。


行者常至,为者常成!





R
Valine - A simple comment system based on Leancloud.