美文网首页iOS项目
AFSecurityPolicy 验证 请求

AFSecurityPolicy 验证 请求

作者: 果哥爸 | 来源:发表于2017-09-12 19:56 被阅读898次

    主要参考:
    IOS-32-AFNetworking中与安全相关的AFSecurityPolicy模块详解
    验证 HTTPS 请求的证书

    iOS9 发布之后,由于新特性 App Transport Security 的引入,在默认行为下是不能发送HTTP 请求的。很多网站都在转用 HTTPS,而 AFNetworking
    中的AFSecurityPolicy 就是为了阻止中间人攻击,以及其它漏洞的工具。

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

    概念 讲解

    1. HTTPS

    HTTPS连接建立过程大致是,客户端和服务端建立一个连接,服务端返回一个证书,客户端里存有各个受信任的证书机构根证书,用这些根证书对服务端返回的证书进行验证,经验证如果证书是可信任的,就生成一个pre-master secret,用这个证书的公钥加密后发送给服务端,服务端用私钥解密后得到pre-master secret,再根据某种算法生成master secret,客户端也同样根据这种算法从pre-master secret生成master secret,随后双方的通信都用这个master secret对传输数据进行加密解密。
    以上是简单过程,中间还有很多细节,详细过程和原理已经有很多文章阐述得很好,就不再复述,推荐一些相关文章:
    关于非对称加密算法的原理:
    RSA算法原理 一
    RSA算法原理 二
    关于整个流程:HTTPS那些事 一
    关于整个流程:HTTPS那些事 二
    关于整个流程:HTTPS那些事 三
    关于数字证书:浅析数字证书

    2.证书是怎样验证的?怎样保证中间人不能伪造证书?

    首先要知道非对称加密算法的特点,非对称加密有一对公钥私钥,用公钥加密的数据只能通过对应的私钥解密,用私钥加密的数据只能通过对应的公钥解密。

    我们来看最简单的情况:一个证书颁发机构(CA),颁发了一个证书A,服务器用这个证书建立https连接。客户端在信任列表里有这个CA机构根证书

    首先CA机构颁发的证书A里包含有证书内容F,以及证书加密内容F1加密内容F1就是用这个证书机构的私钥对内容F加密的结果。(这中间还有一次hash算法,略过。)

    建立https连接时,服务端返回证书A给客户端,客户端的系统里的CA机构根证书有这个CA机构的公钥,用这个公钥对证书A的加密内容F1解密得到F2,跟证书A内容F对比,若相等就通过验证。整个流程大致是:F->CA私钥加密->F1->客户端CA公钥解密->F。因为中间人不会有CA机构的私钥,客户端无法通过CA公钥解密,所以伪造的证书肯定无法通过验证。

    3.什么是SSL Pinning?

    可以理解为证书绑定,是指客户端直接保存服务端的证书,建立https连接时直接对比服务端返回的和客户端保存的两个证书是否一样,一样就表明证书是真的,不再去系统的信任证书机构里寻找验证。这适用于非浏览器应用,因为浏览器跟很多未知服务端打交道,无法把每个服务端的证书都保存到本地,但CS架构的像手机APP事先已经知道要进行通信的服务端,可以直接在客户端保存这个服务端的证书用于校验。

    为什么直接对比就能保证证书没问题?如果中间人从客户端取出证书,再伪装成服务端跟其他客户端通信,它发送给客户端的这个证书不就能通过验证吗?确实可以通过验证,但后续的流程走不下去,因为下一步客户端会用证书里的公钥加密,中间人没有这个证书的私钥就解不出内容,也就截获不到数据,这个证书的私钥只有真正的服务端有,中间人伪造证书主要伪造的是公钥。

    为什么要用SSL Pinning?正常的验证方式不够吗?如果服务端的证书是从受信任的的CA机构颁发的,验证是没问题的,但CA机构颁发证书比较昂贵,小企业或个人用户可能会选择自己颁发证书,这样就无法通过系统受信任的CA机构列表验证这个证书的真伪了,所以需要SSL Pinning这样的方式去验证。

    4. AFSecurityPolicy

    NSURLConnection已经封装了https连接的建立、数据的加密解密功能,我们直接使用NSURLConnection是可以访问https网站的,但NSURLConnection并没有验证证书是否合法,无法避免中间人攻击。要做到真正安全通讯,需要我们手动去验证服务端返回的证书,AFSecurityPolicy封装了证书验证的过程,让用户可以轻易使用,除了去系统信任CA机构列表验证,还支持SSL Pinning方式的验证。使用方法:

    //把服务端证书(需要转换成cer格式)放到APP项目资源里,AFSecurityPolicy会自动寻找根目录下所有cer文件

    AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
    
    securityPolicy.allowInvalidCertificates = YES;
    
    [AFHTTPRequestOperationManager manager].securityPolicy = securityPolicy;
    
    [manager GET:@"[https://example.com/](https://example.com/)"parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
    
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    
    }];
    

    一. 属性 介绍

    1. AFSSLPinningMode

    使用 AFSecurityPolicy 时,总共有三种验证服务器是否被信任的方式:

    • AFSSLPinningModeNone
      这个模式表示不做SSL pinning,只跟浏览器一样在系统的信任机构列表里验证服务端返回的证书。若证书是信任机构签发的就会通过,若是自己服务器生成的证书,这里是不会通过的。

    • AFSSLPinningModeCertificate
      这个模式表示用证书绑定方式验证证书,需要客户端保存有服务端的证书拷贝,这里验证分两步,第一步验证证书的域名/有效期等信息,第二步是对比服务端返回的证书跟客户端返回的是否一致。
      这里还没弄明白第一步的验证是怎么进行的,代码上跟去系统信任机构列表里验证一样调用了SecTrustEvaluate,只是这里的列表换成了客户端保存的那些证书列表。

    • AFSSLPinningModePublicKey
      这个模式同样是用证书绑定方式验证,客户端要有服务端的证书拷贝,只是验证时只验证证书里的公钥,不验证证书的有效期等信息。只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据。

    2. allowInvalidCertificates
    allowInvalidCertificates 定义了客户端是否信任非法证书。一般来说,每个版本的iOS 设备中,都会包含一些既有的CA根证书。如果接收到的证书是iOS信任的CA根证书签名的,那么则为合法证书,否则为非法证书。
    allowInvalidCertificates 就是用来确认是否信任这样的证书的。

    3. pinnedCertificates
    pinnedCertificates 就是用来校验服务器返回证书的证书。通常都保存在mainBundle下。通常默认情况下,AFNetworking会自动寻找在mainBundle的根目录下所有.cer文件并保存在pinnedCertificates数组里,以校验服务器返回来的证书。

    4. validatesDomainName
    validatesDomainName 是指是否校验在证书中的domain这一个字段。每个证书都会包含一个DomainName,它可以是一个IP地址,一个域名或者一端代用通配符的域名。如*.google.com, www.google.com都可以成为这个证书的DomainName。设置validatesDomainNameYES将严格地保证其安全性。

    5.validatesCertificateChain
    validatesCertificateChain 是否校验器证书链。
    通常来讲,一个CA证书颁发机构有很多子机构,用来签发不同用途的子证书,然后这些子证书又再用来签发相应的证书,只有证书链上的证书都是正确,CertificateChain 才算验证完成。以 Google 为例:

    image.png

    从图中可以看出,Google.com的证书的根CA证书GeoTrust Global CA,而GeoTrust Global CA并没有直接给google.com签证书,而是先签名了Google Internet Authority G2, 然后G2再签名了google.com。这时候就需要设备中保存有Google Internet Authority G2证书才能通过校验。

    一般将validatesCertificateChain设置为NO,因为并不是太有必要做CertificateChain的校验。

    二. 初始化以及设置

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

    + (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode {
        return [self policyWithPinningMode:pinningMode withPinnedCertificates:[self defaultPinnedCertificates]];
    }
    
    + (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode withPinnedCertificates:(NSSet *)pinnedCertificates {
        AFSecurityPolicy *securityPolicy = [[self alloc] init];
        securityPolicy.SSLPinningMode = pinningMode;
    
        [securityPolicy setPinnedCertificates:pinnedCertificates];
    
        return securityPolicy;
    }
    

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

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

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

    三. 操作 SecTrustRef

    serverTrust 的操作的函数基本上都是 CAPI,都定义在 Security 模块中,接下来分析AFPublicKeyForCertificate 的实现

    static id AFPublicKeyForCertificate(NSData *certificate) {
       id allowedPublicKey = nil;
        SecCertificateRef allowedCertificate;
        SecCertificateRef allowedCertificates[1];
        CFArrayRef tempCertificates = nil;
        SecPolicyRef policy = nil;
        SecTrustRef allowedTrust = nil;
        SecTrustResultType result;
    
        allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate);
        __Require_Quiet(allowedCertificate != NULL, _out);
    
        allowedCertificates[0] = allowedCertificate;
        tempCertificates = CFArrayCreate(NULL, (const void **)allowedCertificates, 1, NULL);
    
        policy = SecPolicyCreateBasicX509();
        __Require_noErr_Quiet(SecTrustCreateWithCertificates(tempCertificates, policy, &allowedTrust), _out);
        __Require_noErr_Quiet(SecTrustEvaluate(allowedTrust, &result), _out);
    
        allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust);
    
    _out:
        if (allowedTrust) {
            CFRelease(allowedTrust);
        }
    
        if (policy) {
            CFRelease(policy);
        }
    
        if (tempCertificates) {
            CFRelease(tempCertificates);
        }
    
        if (allowedCertificate) {
            CFRelease(allowedCertificate);
        }
    
        return allowedPublicKey;
    }
    
    1. 初始化临时变量
    id allowedPublicKey = nil;
    SecCertificateRef allowedCertificate;
    SecCertificateRef allowedCertificates[1];
    CFArrayRef tempCertificates = nil;
    SecPolicyRef policy = nil;
    SecTrustRef allowedTrust = nil;
    SecTrustResultType result;
    
    1. 使用 SecCertificateCreateWithData 通过 DER 表示的数据生成一个 SecCertificateRef,然后判断返回值是否为 NULL
    allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate);
    __Require_Quiet(allowedCertificate != NULL, _out);
    

    这里使用了一个非常神奇的宏__Require_Quiet,它会判断 allowedCertificate != NULL 是否成立,如果 allowedCertificate 为空就会跳到 _out 标签处继续执行

     #ifndef __Require_Quiet
        #define __Require_Quiet(assertion, exceptionLabel)                            \
          do                                                                          \
          {                                                                           \
              if ( __builtin_expect(!(assertion), 0) )                                \
              {                                                                       \
                  goto exceptionLabel;                                                \
              }                                                                       \
          } while ( 0 )
     #endif
    
    1. 通过上面的 allowedCertificate 创建一个 CFArray
    allowedCertificates[0] = allowedCertificate;
    tempCertificates = CFArrayCreate(NULL, (const void **)allowedCertificates, 1, NULL);
    

    下面的 SecTrustCreateWithCertificates只会接收数组作为参数。

    1. 创建一个默认的符合 X509 标准的 SecPolicyRef,通过默认的 SecPolicyRef 和证书创建一个 SecTrustRef 用于信任评估,对该对象进行信任评估,确认生成的 SecTrustRef 是值得信任的。
     policy = SecPolicyCreateBasicX509();
    __Require_noErr_Quiet(SecTrustCreateWithCertificates(tempCertificates, policy, &allowedTrust), _out);
    __Require_noErr_Quiet(SecTrustEvaluate(allowedTrust, &result), _out);
    

    这里使用的 __Require_noErr_Quiet 和上面的宏差不多,只是会根据返回值判断是否存在错误。

    1. 获取公钥
    allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust);
    

    这里的 __bridge_transfer 会将结果桥接成 NSObject 对象,然后将 SecTrustCopyPublicKey 返回的指针释放。

    1. 释放各种 C 语言指针
    if (allowedTrust) {
        CFRelease(allowedTrust);
    }
    
    if (policy) {
        CFRelease(policy);
    }
    
    if (tempCertificates) {
        CFRelease(tempCertificates);
    }
    
    if (allowedCertificate) {
        CFRelease(allowedCertificate);
    }
    

    每一个 SecTrustRef的对象都是包含多个 SecCertificateRefSecPolicyRef。其中 SecCertificateRef可以使用 DER 进行表示,并且其中存储着公钥信息。

    对它的操作还有 AFCertificateTrustChainForServerTrustAFPublicKeyTrustChainForServerTrust但是它们几乎调用了相同的 API

    static NSArray * AFCertificateTrustChainForServerTrust(SecTrustRef serverTrust) {
        CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
        NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
    
        for (CFIndex i = 0; i < certificateCount; i++) {
            SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
            [trustChain addObject:(__bridge_transfer NSData *)SecCertificateCopyData(certificate)];
        }
    
        return [NSArray arrayWithArray:trustChain];
    }
    
    SecTrustGetCertificateAtIndex 获取 SecTrustRef 中的证书
    SecCertificateCopyData 从证书中或者 DER 表示的数据
    

    四. 验证服务端是否受信

    验证服务端是否受信是通过- [AFSecurityPolicy evaluateServerTrust:forDomain:]方法进行的。

    - (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
    {
    
        #1: 不能隐式地信任自己签发的证书
    
        #2: 设置 policy
    
        #3: 验证证书是否有效
    
        #4: 根据 SSLPinningMode 对服务端进行验证
    
        return NO;
    }
    
    1. 不能隐式的信任自己签发的证书
    if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
        NSLog(@"In order to validate 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.

    所以如果没有提供证书或者不验证证书,并且还设置 allowInvalidCertificates 为真,满足上面的所有条件,说明这次的验证是不安全的,会直接返回NO

    1. 设置 policy
    NSMutableArray *policies = [NSMutableArray array];
    if (self.validatesDomainName) {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }
    

    如果要验证域名的话,就以域名为参数创建一个 SecPolicyRef,否则会创建一个符合 X509 标准的默认 SecPolicyRef 对象

    1. 验证证书的有效性
    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        return NO;
    }
    

    如果只根据信任列表中的证书进行验证,即 self.SSLPinningMode == AFSSLPinningModeNone。如果允许无效的证书的就会直接返回 YES。不允许就会对服务端信任进行验证。

    如果服务器信任无效,并且不允许无效证书,就会返回 NO

    1. 根据 SSLPinningMode 对服务器信任进行验证
    switch (self.SSLPinningMode) {
        case AFSSLPinningModeNone:
        default:
            return NO;
        case AFSSLPinningModeCertificate: {
            ...
        }
        case AFSSLPinningModePublicKey: {
            ...
        }
    }
    

    AFSSLPinningModeNone:直接返回 NO

    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;
    

    a. 从 self.pinnedCertificates 中获取 DER 表示的数据
    b. 使用 SecTrustSetAnchorCertificates为服务器信任设置证书
    c. 判断服务器信任的有效性
    d. 使用 AFCertificateTrustChainForServerTrust 获取服务器信任中的全部 DER 表示的证书
    f. 如果 pinnedCertificates 中有相同的证书,就会返回 YES

    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;
    

    这部分的实现和上面的差不多,区别有两点

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

    五. 与 AFURLSessionManager 协作

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

    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
            disposition = NSURLSessionAuthChallengeUseCredential;
            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
        } else {
            disposition = NSURLSessionAuthChallengeRejectProtectionSpace;
        }
    } else {
        disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    }
    

    NSURLAuthenticationChallenge 表示一个认证的挑战,提供了关于这次认证的全部信息。它有一个非常重要的属性 protectionSpace,这里保存了需要认证的保护空间, 每一个 NSURLProtectionSpace 对象都保存了主机地址,端口和认证方法等重要信息。

    在上面的方法中,如果保护空间中的认证方法为 NSURLAuthenticationMethodServerTrust,那么就会使用在上一小节中提到的方法- [AFSecurityPolicy evaluateServerTrust:forDomain:]对保护空间中的 serverTrust 以及域名host进行认证

    根据认证的结果,会在 completionHandler 中传入不同的dispositioncredential参数。

    六. 小结

    AFSecurityPolicy 同样也作为一个即插即用的模块,在AFNetworking 中作为验证HTTPS证书是否有效的模块存在,在iOSHTTPS 日渐重视的今天,在我看来,使用 HTTPS 会成为今后 API 开发的标配。

    相关文章

      网友评论

        本文标题:AFSecurityPolicy 验证 请求

        本文链接:https://www.haomeiwen.com/subject/ujpcsxtx.html