AFNetWorking源碼之AFSecurityPolicy

1 HTTPS以及SSL/TSL

SSL(Secure Sockets Layer, 安全套接字層),因為原先互聯(lián)網(wǎng)上使用的HTTP協(xié)議是明文的诫舅,存在很多缺點,比如傳輸內(nèi)容會被偷窺和篡改宫患。SSL協(xié)議的作用就是在傳輸層對網(wǎng)絡(luò)連接進行加密刊懈。

到了1999年,SSL 因為應(yīng)用廣泛娃闲,已經(jīng)成為互聯(lián)網(wǎng)上的事實標(biāo)準(zhǔn)虚汛。IETF就在那年把SSL標(biāo)準(zhǔn)化。標(biāo)準(zhǔn)化之后的名稱改為 TLS(Transport Layer Security皇帮,傳輸層安全協(xié)議)卷哩。SSL與TLS可以視作同一個東西的不同階段。

簡單來說属拾,HTTPS = HTTP + SSL/TLS, 也就是HTTP over SSLHTTP over TLS,這是后面加S的由來将谊。

HTTPS和HTTP異同:HTTP和HTTPS使用的是完全不同的連接方式冷溶,用的端口也不一樣,前者是80尊浓,后者是443逞频。HTTP的連接很簡單,是無狀態(tài)的栋齿;HTTPS協(xié)議是由SSL+HTTP協(xié)議構(gòu)建的可進行加密傳輸苗胀、身份認(rèn)證的網(wǎng)絡(luò)協(xié)議,比HTTP協(xié)議安全瓦堵。

2 HTTPS的握手

img
img

1客戶端發(fā)出請求(ClientHello)

首先基协,客戶端(通常是瀏覽器)先向服務(wù)器發(fā)出加密通信的請求,這被叫做ClientHello請求菇用。在這一步堡掏,客戶端主要向服務(wù)器提供以下信息。

  • (1)支持的協(xié)議版本刨疼,比如TLS1.0版泉唁。
  • (2)一個客戶端生成的隨機數(shù),稍后用于生成"對話密鑰"揩慕。
  • (3)支持的加密方法亭畜,比如RSA公鑰加密。
  • (4)支持的壓縮方法迎卤。

2服務(wù)器回應(yīng)(SeverHello)

服務(wù)器收到客戶端請求后拴鸵,向客戶端發(fā)出回應(yīng),這叫做SeverHello蜗搔。服務(wù)器的回應(yīng)包含以下內(nèi)容劲藐。

  • (1)確認(rèn)使用的加密通信協(xié)議版本,比如TLS1.0版本樟凄。如果瀏覽器與服務(wù)器支持的版本不一致聘芜,服務(wù)器關(guān)閉加密通信。
  • (2)一個服務(wù)器生成的隨機數(shù)缝龄,稍后用于生成"對話密鑰"汰现。
  • (3)確認(rèn)使用的加密方法,比如RSA公鑰加密叔壤。
  • (4)服務(wù)器證書瞎饲。

3客戶端回應(yīng)

客戶端收到服務(wù)器回應(yīng)以后,首先驗證服務(wù)器證書炼绘。如果證書不是可信機構(gòu)頒布嗅战、或者證書中的域名與實際域名不一致、或者證書已經(jīng)過期俺亮,就會向訪問者顯示一個警告驮捍,由其選擇是否還要繼續(xù)通信疟呐。如果證書沒有問題,客戶端就會從證書中取出服務(wù)器的公鑰厌漂。然后萨醒,向服務(wù)器發(fā)送下面三項信息。

  • (1)一個隨機數(shù)苇倡。該隨機數(shù)用服務(wù)器公鑰加密富纸,防止被竊聽。
  • (2)編碼改變通知旨椒,表示隨后的信息都將用雙方商定的加密方法和密鑰發(fā)送晓褪。
  • (3)客戶端握手結(jié)束通知,表示客戶端的握手階段已經(jīng)結(jié)束综慎。這一項同時也是前面發(fā)送的所有內(nèi)容的hash值涣仿,用來供服務(wù)器校驗。

上面第一項的隨機數(shù)示惊,是整個握手階段出現(xiàn)的第三個隨機數(shù)好港,又稱"pre-master key"。有了它以后米罚,客戶端和服務(wù)器就同時有了三個隨機數(shù)钧汹,接著雙方就用事先商定的加密方法,各自生成本次會話所用的同一把"會話密鑰"录择。

4服務(wù)器的最后回應(yīng)

服務(wù)器收到客戶端的第三個隨機數(shù)pre-master key之后拔莱,計算生成本次會話所用的"會話密鑰"。然后隘竭,向客戶端最后發(fā)送下面信息塘秦。

  • (1)編碼改變通知,表示隨后的信息都將用雙方商定的加密方法和密鑰發(fā)送动看。
  • (2)服務(wù)器握手結(jié)束通知尊剔,表示服務(wù)器的握手階段已經(jīng)結(jié)束。這一項同時也是前面發(fā)送的所有內(nèi)容的hash值弧圆,用來供客戶端校驗赋兵。

至此,整個握手階段全部結(jié)束搔预。接下來,客戶端與服務(wù)器進入加密通信叶组,就完全是使用普通的HTTP協(xié)議拯田,只不過用"會話密鑰"加密內(nèi)容。

3 數(shù)字證書

上面握手階段的第二步服務(wù)器給客戶端的證書就是數(shù)字證書甩十,該證書包含了公鑰等信息船庇,一般是由服務(wù)器發(fā)給客戶端吭产,接收方通過驗證這個證書是不是由信賴的CA簽發(fā),或者與本地的證書相對比鸭轮,來判斷證書是否可信臣淤;假如需要雙向驗證,則服務(wù)器和客戶端都需要發(fā)送數(shù)字證書給對方驗證窃爷。

數(shù)字證書是一個電子文檔邑蒋,其中包含了持有者的信息、公鑰以及證明該證書有效的數(shù)字簽名按厘。而數(shù)字證書以及相關(guān)的公鑰管理和驗證等技術(shù)組成了PKI(公鑰基礎(chǔ)設(shè)施)規(guī)范體系医吊。一般來說,數(shù)字證書是由數(shù)字證書認(rèn)證機構(gòu)(Certificate authority逮京,即CA)來負(fù)責(zé)簽發(fā)和管理卿堂,并承擔(dān)PKI體系中公鑰合法性的檢驗責(zé)任;數(shù)字證書的類型有很多懒棉,而HTTPS使用的是SSL證書草描。

怎么來驗證數(shù)字證書是由CA簽發(fā)的,而不是第三方偽造的呢策严? 在回答這個問題前穗慕,我們需要先了解CA的組織結(jié)構(gòu)。首先享钞,CA組織結(jié)構(gòu)中揍诽,最頂層的就是根CA,根CA下可以授權(quán)給多個二級CA栗竖,而二級CA又可以授權(quán)多個三級CA暑脆,所以CA的組織結(jié)構(gòu)是一個樹結(jié)構(gòu)。對于SSL證書市場來說狐肢,主要被Symantec(旗下有VeriSign和GeoTrust)添吗、Comodo SSL、Go Daddy 和 GlobalSign 瓜分份名。 了解了CA的組織結(jié)構(gòu)后碟联,來看看數(shù)字證書的簽發(fā)流程:

img
img

數(shù)字證書的簽發(fā)機構(gòu)CA,在接收到申請者的資料后進行核對并確定信息的真實有效僵腺,然后就會制作一份符合X.509標(biāo)準(zhǔn)的文件鲤孵。證書中的證書內(nèi)容包括了持有者信息和公鑰等都是由申請者提供的,而數(shù)字簽名則是CA機構(gòu)對證書內(nèi)容進行hash加密后等到的辰如,而這個數(shù)字簽名就是我們驗證證書是否是有可信CA簽發(fā)的數(shù)據(jù)普监。

img
img

接收端接到一份數(shù)字證書Cer1后,對證書的內(nèi)容做Hash等到H1;然后在簽發(fā)該證書的機構(gòu)CA1的數(shù)字證書中找到公鑰凯正,對證書上數(shù)字簽名進行解密毙玻,得到證書Cer1簽名的Hash摘要H2;對比H1和H2廊散,假如相等桑滩,則表示證書沒有被篡改躯概。但這個時候還是不知道CA是否是合法的岖是,我們看到上圖中有CA機構(gòu)的數(shù)字證書,這個證書是公開的慈迈,所有人都可以獲取到擂找。而這個證書中的數(shù)字簽名是上一級生成的戳吝,所以可以這樣一直遞歸驗證下去,直到根CA贯涎。根CA是自驗證的听哭,即他的數(shù)字簽名是由自己的私鑰來生成的。合法的根CA會被瀏覽器和操作系統(tǒng)加入到權(quán)威信任CA列表中塘雳,這樣就完成了最終的驗證陆盘。所以,一定要保護好自己環(huán)境(瀏覽器/操作系統(tǒng))中根CA信任列表败明,信任了根CA就表示信任所有根CA下所有子級CA所簽發(fā)的證書隘马,不要隨便添加根CA證書。

4 SSL Pinning

可以理解為證書綁定妻顶,是指客戶端直接保存服務(wù)端的證書酸员,建立https連接時直接對比服務(wù)端返回的和客戶端保存的兩個證書是否一樣,一樣就表明證書是真的讳嘱,不再去系統(tǒng)的信任證書機構(gòu)里尋找驗證幔嗦。這適用于非瀏覽器應(yīng)用,因為瀏覽器跟很多未知服務(wù)端打交道沥潭,無法把每個服務(wù)端的證書都保存到本地邀泉,但CS架構(gòu)的像手機APP事先已經(jīng)知道要進行通信的服務(wù)端,可以直接在客戶端保存這個服務(wù)端的證書用于校驗钝鸽。

為什么直接對比就能保證證書沒問題汇恤?如果中間人從客戶端取出證書,再偽裝成服務(wù)端跟其他客戶端通信拔恰,它發(fā)送給客戶端的這個證書不就能通過驗證嗎因谎?確實可以通過驗證,但后續(xù)的流程走不下去颜懊,因為下一步客戶端會用證書里的公鑰加密蓝角,中間人沒有這個證書的私鑰就解不出內(nèi)容阱穗,也就截獲不到數(shù)據(jù)饭冬,這個證書的私鑰只有真正的服務(wù)端有使鹅,中間人偽造證書主要偽造的是公鑰。

為什么要用SSL Pinning昌抠?正常的驗證方式不夠嗎患朱?如果服務(wù)端的證書是從受信任的的CA機構(gòu)頒發(fā)的,驗證是沒問題的炊苫,但CA機構(gòu)頒發(fā)證書比較昂貴裁厅,小企業(yè)或個人用戶可能會選擇自己頒發(fā)證書,這樣就無法通過系統(tǒng)受信任的CA機構(gòu)列表驗證這個證書的真?zhèn)瘟饲劝孕枰猄SL Pinning這樣的方式去驗證执虹。

5 iOS的HTTPS請求

下面我會實現(xiàn)自簽名證書(12306)、SSL信任證書(baidu)唠梨、系統(tǒng)證書(蘋果)三種情況的實現(xiàn)來看他們的區(qū)別,百度和12306的證書已經(jīng)被我下載到我的項目里面了袋励,具體可以去Demo里面看實現(xiàn)過程。

1 自簽名證書

我們手動指定securityPolicy認(rèn)證屬性当叭。通過12306證書來實現(xiàn)茬故。

//自建證書認(rèn)證
- (IBAction)buttion1:(id)sender {
    NSURL *url = [NSURL URLWithString:@"https://kyfw.12306.cn/otn/leftTicket/init"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
   // [request setValue:@"text/html" forHTTPHeaderField:@"Accept"];
    AFURLSessionManager *manager = [[AFURLSessionManager alloc]initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    //指定安全策略
    manager.securityPolicy = [self ticketSecurityPolicy];
    //指定返回數(shù)據(jù)類型,默認(rèn)是AFJSONResponseSerializer類型,猶豫這里不是JSON類型的返回數(shù)據(jù)蚁鳖,所以需要手動指定返回類型
    AFHTTPResponseSerializer *responseSerializer = [AFHTTPResponseSerializer serializer];
    responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/html"];
    manager.responseSerializer = responseSerializer;
    NSURLSessionDataTask *dataTask = [manager dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
        NSLog(@"%@-----%@",[[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding],error);
    }];
    [dataTask resume];

}
/**
 12306的認(rèn)證證書磺芭,他的認(rèn)證證書是自簽名的

 @return 返回指定的認(rèn)證策略
 */
-(AFSecurityPolicy*)ticketSecurityPolicy {
    // /先導(dǎo)入證書
    NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"12306" ofType:@"cer"];//證書的路徑
    NSData *certData = [NSData dataWithContentsOfFile:cerPath];
    NSSet *set = [NSSet setWithObject:certData];
    
    AFSecurityPolicy *securityPolicy;
    if (true) {
        securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:set];
    }else{
        // AFSSLPinningModeCertificate 使用證書驗證模式。下面這個方法會默認(rèn)使用項目里面的所有證書
        securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
    }
    // allowInvalidCertificates 是否允許無效證書(也就是自建的證書)醉箕,默認(rèn)為NO
    // 如果是需要驗證自建證書钾腺,需要設(shè)置為YES
    securityPolicy.allowInvalidCertificates = YES;
    
    //validatesDomainName 是否需要驗證域名,默認(rèn)為YES讥裤;
    //假如證書的域名與你請求的域名不一致放棒,需把該項設(shè)置為NO;如設(shè)成NO的話坞琴,即服務(wù)器使用其他可信任機構(gòu)頒發(fā)的證書哨查,也可以建立連接,這個非常危險剧辐,建議打開寒亥。
    //置為NO,主要用于這種情況:客戶端請求的是子域名荧关,而證書上的是另外一個域名溉奕。因為SSL證書上的域名是獨立的,假如證書上注冊的域名是www.google.com忍啤,那么mail.google.com是無法驗證通過的加勤;當(dāng)然仙辟,有錢可以注冊通配符的域名*.google.com,但這個還是比較貴的鳄梅。
    //如置為NO叠国,建議自己添加對應(yīng)域名的校驗邏輯。
    securityPolicy.validatesDomainName = NO;
    
    return securityPolicy;
}

2 SSL信任證書

我們手動指定securityPolicy認(rèn)證屬性戴尸。通過百度證書來實現(xiàn)粟焊。

//認(rèn)證證書認(rèn)證
- (IBAction)button2:(id)sender {
    NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    //[request setValue:@"text/html" forHTTPHeaderField:@"Accept"];
    AFURLSessionManager *manager = [[AFURLSessionManager alloc]initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    //指定安全策略
    manager.securityPolicy = [self baiduSecurityPolicy];
    //指定返回數(shù)據(jù)類型,默認(rèn)是AFJSONResponseSerializer類型,猶豫這里不是JSON類型的返回數(shù)據(jù)孙蒙,所以需要手動指定返回類型
    AFHTTPResponseSerializer *responseSerializer = [AFHTTPResponseSerializer serializer];
    responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/html"];
    manager.responseSerializer = responseSerializer;
    NSURLSessionDataTask *dataTask = [manager dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
        NSLog(@"%@-----%@",[[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding],error);
    }];
    [dataTask resume];
}

/**
百度的的認(rèn)證證書项棠,他的認(rèn)證證書是花錢買的,也就是不是自簽名的證書挎峦。這種證書香追,如果我們要手動指定,pinmode只能是`AFSSLPinningModeNone`
 
 @return 返回指定的認(rèn)證策略
 */
-(AFSecurityPolicy*)baiduSecurityPolicy {
    // /先導(dǎo)入證書
    NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"baidu" ofType:@"cer"];//證書的路徑
    NSData *certData = [NSData dataWithContentsOfFile:cerPath];
    NSSet *set = [NSSet setWithObject:certData];
    
    AFSecurityPolicy *securityPolicy;
    if (true) {
        //這里只能用AFSSLPinningModeNone才能成功坦胶,而且我系統(tǒng)的證書列表里面已經(jīng)有百度的證書了
        securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone withPinnedCertificates:set];
    }else{
        // AFSSLPinningModeCertificate 使用證書驗證模式透典。下面這個方法會默認(rèn)使用項目里面的所有證書
        securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
    }
    // allowInvalidCertificates 是否允許無效證書(也就是自建的證書),默認(rèn)為NO
    // 如果是需要驗證自建證書迁央,需要設(shè)置為YES
    securityPolicy.allowInvalidCertificates = NO;
    
    //validatesDomainName 是否需要驗證域名掷匠,默認(rèn)為YES;
    //假如證書的域名與你請求的域名不一致岖圈,需把該項設(shè)置為NO讹语;如設(shè)成NO的話,即服務(wù)器使用其他可信任機構(gòu)頒發(fā)的證書蜂科,也可以建立連接顽决,這個非常危險,建議打開导匣。
    //置為NO才菠,主要用于這種情況:客戶端請求的是子域名,而證書上的是另外一個域名贡定。因為SSL證書上的域名是獨立的赋访,假如證書上注冊的域名是www.google.com,那么mail.google.com是無法驗證通過的缓待;當(dāng)然蚓耽,有錢可以注冊通配符的域名*.google.com,但這個還是比較貴的旋炒。
    //如置為NO步悠,建議自己添加對應(yīng)域名的校驗邏輯。
    securityPolicy.validatesDomainName = YES;
    
    return securityPolicy;
}

3 SSL證書AFN默認(rèn)處理

這里我們不做任何額外的處理瘫镇,直接使用AFN的默認(rèn)證書處理機制鼎兽。通過AFURLSessionManagersecurityPolicy默認(rèn)實現(xiàn)答姥。它會和存在系統(tǒng)中的做對比來驗證證書。

//系統(tǒng)證書認(rèn)證
- (IBAction)button3:(id)sender {
    NSURL *url = [NSURL URLWithString:@"https://www.apple.com/"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    AFURLSessionManager *manager = [[AFURLSessionManager alloc]initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    //指定返回數(shù)據(jù)類型,默認(rèn)是AFJSONResponseSerializer類型谚咬,猶豫這里不是JSON類型的返回數(shù)據(jù)鹦付,所以需要手動指定返回類型
    AFHTTPResponseSerializer *responseSerializer = [AFHTTPResponseSerializer serializer];
    responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/html"];
    manager.responseSerializer = responseSerializer;
    NSURLSessionDataTask *dataTask = [manager dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
        NSLog(@"%@-----%@",[[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding],error);
    }];
    [dataTask resume];
}

6 AFSecurityPolicy源碼解析

AFSecurityPolicy分三種驗證模式

  • AFSSLPinningModeNone:
    這個模式表示不做SSL pinning,只跟瀏覽器一樣在系統(tǒng)的信任機構(gòu)列表里驗證服務(wù)端返回的證書序宦。若證書是信任機構(gòu)簽發(fā)的就會通過睁壁,若是自己服務(wù)器生成的證書,這里是不會通過的互捌。
  • AFSSLPinningModeCertificate:
    這個模式表示用證書綁定方式驗證證書,需要客戶端保存有服務(wù)端的證書拷貝行剂,這里驗證分兩步秕噪,第一步驗證證書的域名/有效期等信息,第二步是對比服務(wù)端返回的證書跟客戶端返回的是否一致厚宰。這里還沒弄明白第一步的驗證是怎么進行的腌巾,代碼上跟去系統(tǒng)信任機構(gòu)列表里驗證一樣調(diào)用了SecTrustEvaluate,只是這里的列表換成了客戶端保存的那些證書列表铲觉。若要驗證這個澈蝙,是否應(yīng)該把服務(wù)端證書的頒發(fā)機構(gòu)根證書也放到客戶端里?
  • AFSSLPinningModePublicKey:
    這個模式同樣是用證書綁定方式驗證撵幽,客戶端要有服務(wù)端的證書拷貝灯荧,只是驗證時只驗證證書里的公鑰,不驗證證書的有效期等信息盐杂。只要公鑰是正確的逗载,就能保證通信不會被竊聽,因為中間人沒有私鑰链烈,無法解開通過公鑰加密的數(shù)據(jù)厉斟。

SecTrustRef

這是一個需要驗證的信任對象,包含待驗證的證書和支持的驗證方法等。

SecTrustResultType

表示驗證結(jié)果强衡。其中 kSecTrustResultProceed表示serverTrust驗證成功擦秽,且該驗證得到了用戶認(rèn)可(例如在彈出的是否信任的alert框中選擇always trust)。 kSecTrustResultUnspecified表示 serverTrust驗證成功漩勤,此證書也被暗中信任了感挥,但是用戶并沒有顯示地決定信任該證書。 兩者取其一就可以認(rèn)為對serverTrust驗證成功锯七。

SecTrustEvaluate

證書校驗函數(shù),在函數(shù)的內(nèi)部遞歸地從葉節(jié)點證書到根證書驗證链快。需要驗證證書本身的合法性(驗證簽名完整性,驗證證書有效期等);驗證證書頒發(fā)者的合法性(查找頒發(fā)者的證書并檢查其合法性眉尸,這個過程是遞歸的).而遞歸的終止條件是證書驗證過程中遇到了錨點證書(錨點證書:嵌入到操作系統(tǒng)中的根證書,這個根證書是權(quán)威證書頒發(fā)機構(gòu)頒發(fā)的自簽名證書)域蜗。

AFSecurityPolicy的源碼細(xì)節(jié)如下:

/**
 證書的驗證類型

 - AFSSLPinningModeNone: 不使用`pinned certificates`來驗證證書
 - AFSSLPinningModePublicKey: 使用`pinned certificates`來驗證證書的公鑰
 - AFSSLPinningModeCertificate: 使用`pinned certificates`來驗證整個證書
 */
typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
    AFSSLPinningModeNone,
    AFSSLPinningModePublicKey,
    AFSSLPinningModeCertificate,
};

/**
 獲取指定證書的公鑰

 @param certificate 證書數(shù)據(jù)
 @return 公鑰
 */
static id AFPublicKeyForCertificate(NSData *certificate) {
    id allowedPublicKey = nil;
    SecCertificateRef allowedCertificate;
    SecPolicyRef policy = nil;
    SecTrustRef allowedTrust = nil;
    SecTrustResultType result;
    //獲取證書對象
    allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate);
    __Require_Quiet(allowedCertificate != NULL, _out);
    //獲取X.509的認(rèn)證策略
    policy = SecPolicyCreateBasicX509();
    //獲取allowedTrust對象的值
    __Require_noErr_Quiet(SecTrustCreateWithCertificates(allowedCertificate, policy, &allowedTrust), _out);
    __Require_noErr_Quiet(SecTrustEvaluate(allowedTrust, &result), _out);
    //根據(jù)allowedTrust獲取對應(yīng)的公鑰
    allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust);
//C++的gumpto跳轉(zhuǎn)巨双,當(dāng)前面的操作出錯以后,直接跳入_out執(zhí)行
_out:
    if (allowedTrust) {
        CFRelease(allowedTrust);
    }
    if (policy) {
        CFRelease(policy);
    }
    if (allowedCertificate) {
        CFRelease(allowedCertificate);
    }
    //返回公鑰
    return allowedPublicKey;
}

/**
 在指定的證書和認(rèn)證策略下霉祸,驗證SecTrustRef對象是否是受信任的筑累、合法的。

 @param serverTrust SecTrustRef對象
 @return 結(jié)果
 */
static BOOL AFServerTrustIsValid(SecTrustRef serverTrust) {
    BOOL isValid = NO;
    SecTrustResultType result;
    //獲取serverTrust的認(rèn)證結(jié)果丝蹭,調(diào)用`SecTrustEvaluate`表示通過系統(tǒng)的證書來比較認(rèn)證
    __Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out);
    isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);

_out:
    return isValid;
}

/**
 根據(jù)`serverTrust`獲取認(rèn)證的證書鏈

 @param serverTrust serverTrust對象
 @return 認(rèn)證證書鏈
 */
static NSArray * AFCertificateTrustChainForServerTrust(SecTrustRef serverTrust) {
    //獲取認(rèn)證鏈的總層次
    CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
    NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
    //獲取每一級認(rèn)證鏈慢宗,把獲取的證書數(shù)據(jù)存入數(shù)組中
    for (CFIndex i = 0; i < certificateCount; i++) {
        SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
        [trustChain addObject:(__bridge_transfer NSData *)SecCertificateCopyData(certificate)];
    }
    //返回證書鏈數(shù)組
    return [NSArray arrayWithArray:trustChain];
}

/**
 獲取serverTrust對象的認(rèn)證鏈的公鑰數(shù)組

 @param serverTrust serverTrust對象
 @return 公鑰數(shù)組
 */
static NSArray * AFPublicKeyTrustChainForServerTrust(SecTrustRef serverTrust) {
    //X.509標(biāo)準(zhǔn)的安全策略
    SecPolicyRef policy = SecPolicyCreateBasicX509();
    //獲取證書鏈的證書數(shù)量
    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;
        //通過一個證書、認(rèn)證策略新建一個SecTrustRef對象
        __Require_noErr_Quiet(SecTrustCreateWithCertificates(certificates, policy, &trust), _out);
        
        SecTrustResultType result;
        //驗證SecTrustRef對象是否成功
        __Require_noErr_Quiet(SecTrustEvaluate(trust, &result), _out);
        //把SecTrustRef對應(yīng)的公鑰加入數(shù)組中
        [trustChain addObject:(__bridge_transfer id)SecTrustCopyPublicKey(trust)];

    _out:
        if (trust) {
            CFRelease(trust);
        }

        if (certificates) {
            CFRelease(certificates);
        }

        continue;
    }
    CFRelease(policy);

    return [NSArray arrayWithArray:trustChain];
}

#pragma mark -

@interface AFSecurityPolicy()
//認(rèn)證策略
@property (readwrite, nonatomic, assign) AFSSLPinningMode SSLPinningMode;
//公鑰集合
@property (readwrite, nonatomic, strong) NSSet *pinnedPublicKeys;
@end

@implementation AFSecurityPolicy
/**
 從MainBundle中獲取所有證書
 
 @param bundle 返回包含在bundle中的證書集合奔穿。如果AFNetworking使用的是靜態(tài)庫镜沽,我們必須通過這個方法來加載證書。并且通過`policyWithPinningMode:withPinnedCertificates`方法來指定認(rèn)證類型贱田。
 @return 返回bundle里面的證書
 */
+ (NSSet *)certificatesInBundle:(NSBundle *)bundle {
    //獲取項目里的所有.cer證書
    NSArray *paths = [bundle pathsForResourcesOfType:@"cer" inDirectory:@"."];
    NSMutableSet *certificates = [NSMutableSet setWithCapacity:[paths count]];
    for (NSString *path in paths) {
        //獲取證書對應(yīng)的NSData缅茉,并且加入集合中
        NSData *certificateData = [NSData dataWithContentsOfFile:path];
        [certificates addObject:certificateData];
    }
    //返回證書集合
    return [NSSet setWithSet:certificates];
}
/**
 返回當(dāng)前類所在bundle所在的證書集合
 
 @return 證書集合
 */
+ (NSSet *)defaultPinnedCertificates {
    static NSSet *_defaultPinnedCertificates = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //獲取當(dāng)前類所在bundle
        NSBundle *bundle = [NSBundle bundleForClass:[self class]];
        _defaultPinnedCertificates = [self certificatesInBundle:bundle];
    });

    return _defaultPinnedCertificates;
}
/**
 返回默認(rèn)的安全認(rèn)證策略,在這里是驗證系統(tǒng)的證書。這個策略不允許非法證書男摧、驗證主機名蔬墩、不驗證證書內(nèi)容和公鑰
 
 @return 返回認(rèn)證策略
 */
+ (instancetype)defaultPolicy {
    AFSecurityPolicy *securityPolicy = [[self alloc] init];
    securityPolicy.SSLPinningMode = AFSSLPinningModeNone;

    return securityPolicy;
}

/**
 根據(jù)指定的認(rèn)證策略和默認(rèn)的證書列表初始化一個`AFSecurityPolicy`對象

 @param pinningMode 認(rèn)證策略
 @return `AFSecurityPolicy`對象
 */
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode {
    return [self policyWithPinningMode:pinningMode withPinnedCertificates:[self defaultPinnedCertificates]];
}

/**
 通過制定的認(rèn)證策略`pinningMode`和證書集合`pinnedCertificates`來初始化一個`AFSecurityPolicy`對象

 @param pinningMode 認(rèn)證模型
 @param pinnedCertificates 證書集合
 @return AFSecurityPolicy對象
 */
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode withPinnedCertificates:(NSSet *)pinnedCertificates {
    AFSecurityPolicy *securityPolicy = [[self alloc] init];
    securityPolicy.SSLPinningMode = pinningMode;
    //設(shè)置`_pinnedCertificates`和`pinnedPublicKeys`屬性,分別對應(yīng)證書集合和公鑰集合
    [securityPolicy setPinnedCertificates:pinnedCertificates];
    //返回初始化成功的`AFSecurityPolicy`
    return securityPolicy;
}

- (instancetype)init {
    self = [super init];
    if (!self) {
        return nil;
    }
    //默認(rèn)是要認(rèn)證主機名稱
    self.validatesDomainName = YES;
    
    return self;
}

/**
通過指定的證書結(jié)合獲取到對應(yīng)的公鑰集合耗拓。然后賦值給`pinnedPublicKeys`屬性
 @param pinnedCertificates 證書集合
 */
- (void)setPinnedCertificates:(NSSet *)pinnedCertificates {
    _pinnedCertificates = pinnedCertificates;

    if (self.pinnedCertificates) {
        NSMutableSet *mutablePinnedPublicKeys = [NSMutableSet setWithCapacity:[self.pinnedCertificates count]];
        //迭代每一個證書
        for (NSData *certificate in self.pinnedCertificates) {
            //獲取證書對應(yīng)的公鑰
            id publicKey = AFPublicKeyForCertificate(certificate);
            if (!publicKey) {
                continue;
            }
            [mutablePinnedPublicKeys addObject:publicKey];
        }
        //賦值給對應(yīng)的屬性
        self.pinnedPublicKeys = [NSSet setWithSet:mutablePinnedPublicKeys];
    } else {
        self.pinnedPublicKeys = nil;
    }
}

#pragma mark -
/**
 為serverTrust對象指定認(rèn)證策略拇颅,如果domain不為nil,則包括對主機名的認(rèn)證。這個方法必須在接受到`authentication challenge`返回的時候調(diào)用乔询。
 SecTrustRef可以理解為橋接證書與認(rèn)證策略的對象樟插,他關(guān)聯(lián)指定的證書與認(rèn)證策略
 
 @param serverTrust 服務(wù)器的X.509標(biāo)準(zhǔn)的證書數(shù)據(jù)
 @param domain 認(rèn)證服務(wù)器的主機名。如果是nil,則不會對主機名進行認(rèn)證哥谷。
 @return serverTrust是否通過認(rèn)證
 */
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)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];
    if (self.validatesDomainName) {
        //使用需要認(rèn)證主機名的認(rèn)證策略
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        //使用默認(rèn)的認(rèn)證策略
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }
    //給serverTrust對象指定認(rèn)證策略
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        return NO;
    }
    //根據(jù)證書驗證策略岸夯、數(shù)字簽名認(rèn)證策略、其他認(rèn)證策略來處理不同情況
    switch (self.SSLPinningMode) {
        case AFSSLPinningModeNone://不驗證公鑰和證書
        default:
            return NO;
        case AFSSLPinningModeCertificate: {//驗證整個證書
            NSMutableArray *pinnedCertificates = [NSMutableArray array];
            //根據(jù)指定證書獲取们妥,獲取對應(yīng)的證書對象
            for (NSData *certificateData in self.pinnedCertificates) {
                [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
            }
            //把證書與serverTrust關(guān)聯(lián)起來
            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)
            //獲取serverTrust證書鏈猜扮。直到根證書。
            NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
            //如果`pinnedCertificates`包含`serverTrust`對象對應(yīng)的證書鏈的根證書监婶。則返回true
            for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
                if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
                    return YES;
                }
            }
            
            return NO;
        }
        case AFSSLPinningModePublicKey: {//只驗證證書里面的數(shù)字簽名
            NSUInteger trustedPublicKeyCount = 0;
            //根據(jù)serverTrust對象和SecPolicyCreateBasicX509認(rèn)證策略旅赢,獲取對應(yīng)的公鑰集合
            NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
            
            for (id trustChainPublicKey in publicKeys) {
                //把獲取的公鑰和系統(tǒng)獲取的默認(rèn)公鑰比較,如果相等惑惶,則通過認(rèn)證
                for (id pinnedPublicKey in self.pinnedPublicKeys) {
                    if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
                        trustedPublicKeyCount += 1;
                    }
                }
            }
            return trustedPublicKeyCount > 0;
        }
    }
    
    return NO;
}

#pragma mark - NSKeyValueObserving

+ (NSSet *)keyPathsForValuesAffectingPinnedPublicKeys {
    return [NSSet setWithObject:@"pinnedCertificates"];
}

#pragma mark - NSSecureCoding

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (instancetype)initWithCoder:(NSCoder *)decoder {

    self = [self init];
    if (!self) {
        return nil;
    }

    self.SSLPinningMode = [[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(SSLPinningMode))] unsignedIntegerValue];
    self.allowInvalidCertificates = [decoder decodeBoolForKey:NSStringFromSelector(@selector(allowInvalidCertificates))];
    self.validatesDomainName = [decoder decodeBoolForKey:NSStringFromSelector(@selector(validatesDomainName))];
    self.pinnedCertificates = [decoder decodeObjectOfClass:[NSArray class] forKey:NSStringFromSelector(@selector(pinnedCertificates))];

    return self;
}

- (void)encodeWithCoder:(NSCoder *)coder {
    [coder encodeObject:[NSNumber numberWithUnsignedInteger:self.SSLPinningMode] forKey:NSStringFromSelector(@selector(SSLPinningMode))];
    [coder encodeBool:self.allowInvalidCertificates forKey:NSStringFromSelector(@selector(allowInvalidCertificates))];
    [coder encodeBool:self.validatesDomainName forKey:NSStringFromSelector(@selector(validatesDomainName))];
    [coder encodeObject:self.pinnedCertificates forKey:NSStringFromSelector(@selector(pinnedCertificates))];
}
#pragma mark - NSCopying
- (instancetype)copyWithZone:(NSZone *)zone {
    AFSecurityPolicy *securityPolicy = [[[self class] allocWithZone:zone] init];
    securityPolicy.SSLPinningMode = self.SSLPinningMode;
    securityPolicy.allowInvalidCertificates = self.allowInvalidCertificates;
    securityPolicy.validatesDomainName = self.validatesDomainName;
    securityPolicy.pinnedCertificates = [self.pinnedCertificates copyWithZone:zone];

    return securityPolicy;
}
@end

最后原文地址,demo地址煮盼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市带污,隨后出現(xiàn)的幾起案子僵控,更是在濱河造成了極大的恐慌,老刑警劉巖鱼冀,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件报破,死亡現(xiàn)場離奇詭異悠就,居然都是意外死亡,警方通過查閱死者的電腦和手機充易,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門梗脾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人盹靴,你說我怎么就攤上這事炸茧。” “怎么了稿静?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵梭冠,是天一觀的道長。 經(jīng)常有香客問我自赔,道長妈嘹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任绍妨,我火速辦了婚禮,結(jié)果婚禮上柬脸,老公的妹妹穿的比我還像新娘他去。我一直安慰自己,他們只是感情好倒堕,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布灾测。 她就那樣靜靜地躺著,像睡著了一般垦巴。 火紅的嫁衣襯著肌膚如雪媳搪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天骤宣,我揣著相機與錄音秦爆,去河邊找鬼。 笑死憔披,一個胖子當(dāng)著我的面吹牛等限,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播芬膝,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼望门,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了锰霜?” 一聲冷哼從身側(cè)響起筹误,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎癣缅,沒想到半個月后厨剪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體哄酝,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年丽惶,在試婚紗的時候發(fā)現(xiàn)自己被綠了炫七。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡钾唬,死狀恐怖万哪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情抡秆,我是刑警寧澤奕巍,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站儒士,受9級特大地震影響的止,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜着撩,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一诅福、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拖叙,春花似錦氓润、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至挖滤,卻和暖如春崩溪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背斩松。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工伶唯, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人砸民。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓抵怎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親岭参。 傳聞我的和親對象是個殘疾皇子反惕,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容