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 SSL
或HTTP 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的握手
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ā)流程:
數(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ù)普监。
接收端接到一份數(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)證書處理機制鼎兽。通過AFURLSessionManager
的securityPolicy
默認(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