iOS基础篇

AFN 中的 AFSecurityPolicy

2019-02-07  本文已影响0人  LoZR

今天是除夕日,本应打算出去逛一下,并感受一下春节气氛的;
但没人约,也约不到人,还是在家好好学习吧;
同时祝大家春节快乐,在新的一年工作顺利,步步高升,bug越来越少😊

切入正题,最近看了一下AFN的源码,为了加深知识点的印象和防止以后遗忘,还是决定写一篇关于 AFSecurityPolicy 的内容进行总结。

AFSecurityPolicy:
主要作用就是验证 HTTPS 请求的证书是否有效,如果 APP 中有一些敏感信息或者涉及交易信息,一定要使用 HTTPS 来保证交易或者用户信息的安全。

NSURLConnection 已经封装了https连接的建立、数据的加密解密功能,我们直接使用NSURLConnection是可以访问https网站的,但NSURLConnection并没有验证证书是否合法,无法避免中间人攻击。要做到真正安全通讯,需要我们手动去验证服务端返回的证书,AFSecurityPolicy封装了证书验证的过程,让用户可以轻易使用,除了去系统信任CA机构列表验证,还支持SSL Pinning方式的验证。使用方法:
把服务端证书(需要转换成cer格式)放到APP项目资源里,AFSecurityPolicy会自动寻找根目录下所有cer文件

     // .crt --->.cer
    // 证书 ---> 公钥  ---> 随机数 加密
    //  项目本地导入  ---> 谁安装谁就能获取到这个证书
    //  证书:proy.
    NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"https" ofType:@"cer"];
    NSData   *data    = [NSData dataWithContentsOfFile:cerPath];
    NSSet    *cerSet  = [NSSet setWithObject:data];
    
    AFSecurityPolicy *security = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:cerSet];
    // 默认配置
    [AFSecurityPolicy defaultPolicy];
    security.allowInvalidCertificates = YES;
    security.validatesDomainName      = NO;
    
    NSString *urlstr = @"https://xxx.xxx.34.197:9000/users/abc";
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    // 不安全 -- 授权
    manager.securityPolicy        = security;
    [manager GET:urlstr parameters:nil progress:^(NSProgress * _Nonnull downloadProgress) {
        
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        
        NSLog(@"success--%@",responseObject);
        
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"fail--%@",error);
        
    }];

AFSecurityPolicy

一. 属性 介绍

AFSecurityPolicy.h 文件

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

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

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


@interface AFSecurityPolicy : NSObject <NSSecureCoding, NSCopying>

// 返回SSL Pinning的类型。默认的是AFSSLPinningModeNone。
@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode;

/**
 这个属性保存着所有的可用做校验的证书的集合。AFNetworking默认会搜索工程中所有.cer的证书文件。
如果想指定某些证书,可使用certificatesInBundle在目标路径下加载证书,
然后调用policyWithPinningMode:withPinnedCertificates创建一个本类对象。
 
 注意: 只要在证书集合中任何一个校验通过,evaluateServerTrust:forDomain: 就会返回true,即通过校验。
 */
@property (nonatomic, strong, nullable) NSSet <NSData *> *pinnedCertificates;

// 使用允许无效或过期的证书,默认是不允许。
@property (nonatomic, assign) BOOL allowInvalidCertificates;

// 是否验证证书中的域名domain
@property (nonatomic, assign) BOOL validatesDomainName;

// 返回指定bundle中的证书。如果使用AFNetworking的证书验证 ,就必须实现此方法,
并且使用policyWithPinningMode:withPinnedCertificates 方法来创建实例对象。
+ (NSSet <NSData *> *)certificatesInBundle:(NSBundle *)bundle;

/**
 默认的实例对象,默认的认证设置为:
 1. 不允许无效或过期的证书
 2. 验证domain名称
 3. 不对证书和公钥进行验证
 */
+ (instancetype)defaultPolicy;

// 创建默认实例
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode;

// 根据指定的证书和pinningMode来创建实例
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode withPinnedCertificates:(NSSet <NSData *> *)pinnedCertificates;


// 校验的关键方法 sessionmanager会调用
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(nullable NSString *)domain;

@end

二. 初始化以及设置

在使用 AFSecurityPolicy 验证服务端是否受到信任之前,要对其进行初始化,使用初始化方法时,主要目的是设置验证服务器是否受信任的方式。

//AFSecurityPolicy.m

// 根据指定的SSL验证模式创建实例
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode {
  // 获得APP下的所有证书 [self defaultPinnedCertificates]
    return [self policyWithPinningMode:pinningMode withPinnedCertificates:[self defaultPinnedCertificates]];
}
// 根据SSL验证模式和指定的证书集合创建实例
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode withPinnedCertificates:(NSSet *)pinnedCertificates {
    AFSecurityPolicy *securityPolicy = [[self alloc] init];
    securityPolicy.SSLPinningMode = pinningMode;
    // 设置证书集合 如果是默认的 已经通过[self defaultPinnedCertificates]得到了
    [securityPolicy setPinnedCertificates:pinnedCertificates];
    // 公钥的取出
    return securityPolicy;
}

在调用 pinnedCertificatessetter 方法时,会从全部的证书中取出公钥保存到 pinnedPublicKeys 属性中。

// 此函数设置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) {
            // 取出合法的公钥
            // 传输 -- session -- 验证当前所有的信息
            id publicKey = AFPublicKeyForCertificate(certificate);
            if (!publicKey) {
                continue;
            }
            [mutablePinnedPublicKeys addObject:publicKey];
        }
        self.pinnedPublicKeys = [NSSet setWithSet:mutablePinnedPublicKeys];
    } else {
        self.pinnedPublicKeys = nil;
    }
}

在这里调用了 AFPublicKeyForCertificate 对证书进行操作,返回一个公钥。

三. 操作 SecTrustRef

static id AFPublicKeyForCertificate(NSData *certificate) 

在该函数是返回单个证书的公钥(所以传入的参数是一个证书),函数代码如下(含注释):

static id AFPublicKeyForCertificate(NSData *certificate) {
    id allowedPublicKey = nil;
    SecCertificateRef allowedCertificate;
    SecCertificateRef allowedCertificates[1];
    CFArrayRef tempCertificates = nil;
    SecPolicyRef policy = nil;
    SecTrustRef allowedTrust = nil;
    SecTrustResultType result;
    // 1. 根据二进制的certificate生成SecCertificateRef类型的证书
    // NSData *certificate 通过CoreFoundation (__bridge CFDataRef)转换成 CFDataRef
    allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate);
    // 2.如果allowedCertificate为空,则执行标记_out后边的代码
    __Require_Quiet(allowedCertificate != NULL, _out);

    
    allowedCertificates[0] = allowedCertificate;
    tempCertificates = CFArrayCreate(NULL, (const void **)allowedCertificates, 1, NULL);
// 新建policy为X.509
    policy = SecPolicyCreateBasicX509();
    //3. 创建SecTrustRef对象,如果出错就跳到_out标记处
    __Require_noErr_Quiet(SecTrustCreateWithCertificates(tempCertificates, policy, &allowedTrust), _out);
    // 4.校验证书是否可信任的过程,这个不是异步的。
    __Require_noErr_Quiet(SecTrustEvaluate(allowedTrust, &result), _out);
    // 5.在SecTrustRef对象中取出公钥
    // 公钥是授信的,下面一行代码就是为了证明这个公钥是合法的
    allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust);

// _out 用途  释放各种 C 语言指针
_out:
    if (allowedTrust) {
        CFRelease(allowedTrust);
    }

    if (policy) {
        CFRelease(policy);
    }

    if (tempCertificates) {
        CFRelease(tempCertificates);
    }

    if (allowedCertificate) {
        CFRelease(allowedCertificate);
    }

    
    /*
     ① NSData *certificate -> CFDataRef -> (SecCertificateCreateWithData) -> SecCertificateRef allowedCertificate
     
     ②判断SecCertificateRef allowedCertificate 是不是空,如果为空,直接跳转到后边的代码 即:_out
     
     ③SecTrustCreateWithCertificates(allowedCertificate, policy, &allowedTrust) -> 生成SecTrustRef allowedTrust
     
     ④SecTrustEvaluate(allowedTrust, &result) 校验证书
     
     ⑤(__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust) -> 得到公钥id allowedPublicKey
     */
    return allowedPublicKey;
}

四. 验证服务端是否受信

验证服务端是否受信是通过- [AFSecurityPolicy evaluateServerTrust:forDomain:]方法进行的。
SecTrustRef:其实就是一个容器,装了服务器端需要验证的证书的基本信息、公钥等等,不仅如此,它还可以装一些评估策略,还有客户端的锚点证书,这个客户端的证书,可以用来和服务端的证书去匹配验证的。
每一个SecTrustRef对象包含多个SecCertificateRefSecPolicyRef。其中 SecCertificateRef 可以使用 DER 进行表示。
domain:服务器域名,用于域名验证

a. 根据severTrustdomain来检查服务器端发来的证书是否可信
b. 其中SecTrustRef是一个CoreFoundation类型,用于对服务器端传来的X.509证书评估的
c. 而我们都知道,数字证书的签发机构CA,在接收到申请者的资料后进行核对并确定信息的真实有效,然后就会制作一份符合X.509标准的文件。证书中的证书内容包含的持有者信息和公钥等都是由申请者提供的,而数字签名则是CA机构对证书内容进行hash加密后得到的,而这个数字签名就是我们验证证书是否是有可信CA签发的数据。

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

    #1: 不能隐式地信任自己签发的证书

    #2: 设置 policy

    #3: 验证证书是否有效

    #4: 根据 SSLPinningMode 对服务端进行验证

    return NO;
}
1.不能隐式的信任自己签发的证书
    // domain  - 域名验证 
    if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
        //如果想要实现自签名的HTTPS访问成功,必须设置pinnedCertificates,且不能使用defaultPolicy
        NSLog(@"In order to val idate 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.

如果有服务器域名、设置了允许信任无效或者过期证书(自签名证书)、需要验证域名、没有提供证书或者不验证证书,返回NO。后两者和allowInvalidCertificates为真的设置矛盾,说明这次验证是不安全的。

2.设置 policy
//用来装验证策略
    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);

3.验证证书的有效性
//如果是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;
    }
4.根据 SSLPinningMode 对服务器信任进行验证
switch (self.SSLPinningMode) {
    //上一部分已经判断过了,如果执行到这里的话就返回NO
    case AFSSLPinningModeNone:
    default:
        return NO;

      //验证证书类型
     // 这个模式表示用证书绑定(SSL Pinning)方式验证证书,需要客户端保存有服务端的证书拷贝
     // 注意客户端保存的证书存放在self.pinnedCertificates中
    case AFSSLPinningModeCertificate: {
        ...
    }
//公钥验证 AFSSLPinningModePublicKey模式同样是用证书绑定(SSL Pinning)方式验证,
//客户端要有服务端的证书拷贝,只是验证时只验证证书里的公钥,不验证证书的有效期等信息。
//只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据
    case AFSSLPinningModePublicKey: {
        ...
    }
}

AFSSLPinningModeNone:直接返回 NO

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:

 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;

a.会从服务器信任中获取公钥
b.pinnedPublicKeys 中的公钥与服务器信任中的公钥相同的数量大于 0,就会返回真

五.与 AFURLSessionManager 协作

- URLSession:didReceiveChallenge:completionHandler:该代理方法会在下面两种情况调用:
1.当服务器端要求客户端提供证书时或者进行NTLM认证(Windows NT LAN Manager,微软提出的WindowsNT挑战/响应验证机制)时,此方法允许你的app提供正确的挑战证书。
2.当某个session使用SSL/TLS协议,第一次和服务器端建立连接的时候,服务器会发送给iOS客户端一个证书,此方法允许你的app验证服务期端的证书链(certificate keychain

注:如果你没有实现该方法,该session会调用其NSURLSessionTaskDelegate的代理方法URLSession:task:didReceiveChallenge:completionHandler:
这个方法其实就是做https认证的。看看上面的内容,大概能看明白这个方法做认证的步骤,我们还是如果有自定义的做认证的Block,则调用我们自定义的,否则去执行默认的认证步骤,最后调用完成认证。

服务端发起的一个验证挑战,客户端需要根据挑战的类型提供相应的挑战凭证。当然,挑战凭证不一定都是进行HTTPS证书的信任,也可能是需要客户端提供用户密码或者提供双向验证时的客户端证书。当这个挑战凭证被验证通过时,请求便可以继续顺利进行

在代理协议- URLSession:didReceiveChallenge:completionHandler:或者- URLSession:task:didReceiveChallenge:completionHandler:代理方法被调用时会运行这段代码:

// 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;
        }

NSURLAuthenticationChallenge 表示一个认证的挑战,提供了关于这次认证的全部信息。它有一个非常重要的属性 protectionSpace,这里保存了需要认证的保护空间, 每一个 NSURLProtectionSpace 对象都保存了主机地址,端口和认证方法等重要信息。
在上面的方法中,如果保护空间中的认证方法为 NSURLAuthenticationMethodServerTrust,那么就会使用在上一小节中提到的方法- [AFSecurityPolicy evaluateServerTrust:forDomain:]对保护空间中的 serverTrust 以及域名host进行认证
根据认证的结果,会在 completionHandler 中传入不同的dispositioncredential参数。

鸣谢:林大鹏天地
参考:https://www.jianshu.com/p/2137372396d2
本文章内容纯粹个人见解并仅用于分享交流,如有描述不当之处,欢迎指出与交流,谢谢!

上一篇下一篇

猜你喜欢

热点阅读