AFNetworking之AFSecurityPolicy深入學習

OneAlon發表於2018-01-31

此篇文章主要是記錄一下AFSecurityPolicy的學習過程, 在學習AFSecurityPolicy之前對HTTP做了一些瞭解, 並做了兩篇筆記, 僅供參考.HTTP之網路基礎HTTP通訊.

在開始之前先對HTTPS的認證流程做一下梳理.

HTTPS認證

  1. 客戶端發起網路請求, 會生成一個隨機數N1和支援的版本號以及加密方式等傳給伺服器.
  2. 伺服器接收到客戶端的請求, 生成一個隨機數N2, 將經過認證的數字證書和N2傳給客戶端.
  3. 客戶端接收到伺服器的證書, 驗證證書的真偽, 如果證書沒有問題, 就用伺服器的公鑰加密一個隨機數N3, 這個時候客戶端是知道N1, N2和N3的. 並將N3傳給伺服器.
  4. 伺服器接收到加密後的N3, 使用自己的私鑰將N3解密, 得到這個隨機數. 使用N1+N2+N3作為對稱金鑰開始通訊. 將加密後的資料傳給客戶端.
  5. 客戶端接收到加密後的資料, 使用對稱金鑰解密. 獲取到解密後的資料.

1. AFN中的使用

來看一下AFN中是如何使用AFSecurityPolicy進行HTTPS認證的.

- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    ...
    if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
      ...
    }
    ...
    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}
複製程式碼

在AFN中呼叫AFSecurityPolicyevaluateServerTrust:forDomain:方法驗證伺服器證書.

2. AFSecurityPolicy的核心方法

我們來看一下evaluateServerTrust:forDomain:方法.

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    /*
     allowInvalidCertificates:是否使用一個無效或者過期的證書(也就是使用自建證書)
     validatesDomainName:驗證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];
    // 是否要驗籤domain域
    if (self.validatesDomainName) {
        // 如果需要驗證domain, 使用SecPolicyCreateSSL()建立驗證策略
        // 第一個引數代表驗證整個證書鏈, 第二個引數為域名
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        // 如果不驗證domain, 就使用預設的驗證策略, Returns a policy object for the default X.509 policy.
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }

    // 為serverTrust設定驗證策略, 告訴客戶端如何驗證serverTrust
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        // 如果SSLPinningMode==AFSSLPinningModeNone, 表示不適用SSL pinning, 但是允許自建證書或者使用AFServerTrustIsValid檢視serverTrust是否可信.
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        // 即不允許自建證書, 並且serverTrust不可信, 就返回NO.
        return NO;
    }

    switch (self.SSLPinningMode) {
        case AFSSLPinningModeNone:
        default:
            return NO;
        // 這種模式表示用證書繫結方式(SSL Pinning)驗證證書, 這裡需要客戶端儲存有伺服器的證書拷貝, 客戶端儲存的證書存在self.pinnedCertificates中
        case AFSSLPinningModeCertificate: {
            NSMutableArray *pinnedCertificates = [NSMutableArray array];
            for (NSData *certificateData in self.pinnedCertificates) {
                [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
            }
            // 設定錨點證書, 假如認證的證書是這個pinnedCertificates錨點證書的子節點, 那麼就信任該證書.
            // 設定錨點證書是幹嘛的??????
            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;
            // 從serverTrust取出伺服器傳過來的所有可用的證書, 並取出對應的公鑰
            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;
}
複製程式碼

在方法中, 加了一下自己理解的註釋, 如果有不對的地方, 望指正. 方法的作用是檢測伺服器的證書是否可以信任.

allowInvalidCertificatesAFSecurityPolicy的一個屬性, 是否允許信任一個無效證書(自建證書). 因為允許使用自建證書並且要驗證域名, 那麼SSLPinningMode的模式就不能為AFSSLPinningModeNone或者pinnedCertificates證書個數不能為0.

SSLPinningMode是一個列舉型別.

  • AFSSLPinningModeNone這種模式表示不適用SSL Pinning, 如果證書是CA認證機構頒發的證書, 就通過認證, 否則不通過.
  • AFSSLPinningModePublicKey這種模式是證書繫結模式驗證證書, 客戶端要儲存證書的副本, 只驗證證書的公鑰.
  • AFSSLPinningModeCertificate這種模式是證書繫結模式驗證證書, 客戶端要儲存證書的副本, 首先需要驗證伺服器證書的有效期, 身份資訊等. 然後再將伺服器證書和本地證書作比較, 檢視是否一致.

pinnedCertificates客戶端儲存的伺服器證書集合.

SecTrustSetPolicies是為serverTrust設定驗證策略, 以哪種方式驗證serverTrust.

使用AFSSLPinningModeNone模式驗證證書, 如果是允許自建證書就認為證書是值得信任的. AFServerTrustIsValid是AFN自定義的函式, 驗證serverTrust是否可信, 這個函式會在下邊講解. 如果serverTrust既不可信又不允許使用自建證書就表示此證書不可信.

使用AFSSLPinningModeCertificate模式驗證證書, 先遍歷pinnedCertificates獲取客戶端儲存的證書, 並且使用SecTrustSetAnchorCertificates將證書集合設定為錨點證書, 這部分我也不太瞭解. 使用AFCertificateTrustChainForServerTrust方法獲取伺服器證書鏈, 獲取到的證書是倒敘集合, 遍歷這個集合, 如果本地證書集合包含伺服器證書鏈中的證書, 說明這個證書是可信的.

使用AFSSLPinningModePublicKey模式驗證證書, 使用AFPublicKeyTrustChainForServerTrust方法獲取伺服器所有可用證書對應的公鑰並存入集合中, 兩層for迴圈遍歷伺服器證書對應的公鑰和客戶端儲存的公鑰集合作比對, 如果證書公鑰相同就將trustedPublicKeyCount信任公鑰個數加1, 如果trustedPublicKeyCount個數大於0, 伺服器公鑰和客戶端公鑰有相同公鑰, 表明證書可信.

3. AFSecurityPolicy中的輔助函式

AFSecurityPolicy中定義了一個函式供內部使用.

3.1 AFServerTrustIsValid函式

static BOOL AFServerTrustIsValid(SecTrustRef serverTrust) {
    // 預設無效
    BOOL isValid = NO;
    // 列舉型別, 用來儲存結果
    SecTrustResultType result;
    // 如果SecTrustEvaluate(serverTrust, &result)的結果為0, 就繼續執行, 如果不為0就執行_out.
    // SecTrustEvaluate:評估serverTrust
    __Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out);

    // 如果是kSecTrustResultUnspecified 表示評估成功, 證書可以信任.
    // 如果是kSecTrustResultProceed 表示評估成功.
    isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);

_out:
    return isValid;
}
複製程式碼

方法的作用是判斷serverTrust是否有效, 程式碼中已經加了註釋. __Require_noErr_Quiet是一個巨集定義, 使用到了goto語法, 當第一個引數不為0的時候就執行第二個引數指定的標籤. SecTrustEvaluate是系統的方法, 驗證serverTrust, 並將驗證結果賦值給第二個引數result.

3.2 AFCertificateTrustChainForServerTrust函式

static NSArray * AFCertificateTrustChainForServerTrust(SecTrustRef serverTrust) {
    // 使用SecTrustGetCertificateCount
    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];
}
複製程式碼

獲取serverTrust相關的證書鏈

3.3 AFPublicKeyTrustChainForServerTrust函式

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證書鏈公鑰

相關文章