標籤:ons 應用 pre validate 驗證方式 second 製作 學習 tty
http://oncenote.com/2014/10/21/Security-1-HTTPS/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io
1. HTTPS
其實HTTPS從最終的資料解析的角度,與HTTP沒有任何的區別,HTTPS就是將HTTP協議資料包放到SSL/TSL層加密後,在TCP/IP層組成IP資料報去傳輸,以此保證傳輸資料的安全;而對於接收端,在SSL/TSL將接收的資料包解密之後,將資料傳給HTTP協議層,就是普通的HTTP資料。HTTP和SSL/TSL都處於OSI模型的應用程式層。從HTTP切換到HTTPS是一個非常簡單的過程,在做具體的切換操作之前,我們需要瞭解幾個概念:
SSL/TSL
關於SSL/TSL,阮一峰的兩篇部落格文章做了很好的介紹:
- SSL/TLS協議運行機制的概述
- 圖解SSL/TLS協議
簡單的來說,SSL/TSL通過四次握手,主要交換三個資訊:
- 數位憑證:該認證包含了公開金鑰等資訊,一般是由伺服器發給用戶端,接收方通過驗證這個認證是不是由信賴的CA簽發,或者與本地的認證相對比,來判斷認證是否可信;假如需要雙向驗證,則伺服器和用戶端都需要發送數位憑證給對方驗證;
- 三個隨機數:這三個隨機數構成了後續通訊過程中用來對資料進行對稱式加密解密的“對話密鑰”。
首先用戶端先發第一個隨機數N1,然後伺服器回了第二個隨機數N2(這個過程同時把之前提到的認證發給用戶端),這兩個隨機數都是明文的;而第三個隨機數N3(這個隨機數被稱為Premaster secret),用戶端用數位憑證的公開金鑰進行非對稱式加密,發給伺服器;而伺服器用只有自己知道的私密金鑰來解密,擷取第三個隨機數。這樣,服務端和用戶端都有了三個隨機數N1+N2+N3,然後兩端就使用這三個隨機數來產生“對話密鑰”,在此之後的通訊都是使用這個“對話密鑰”來進行對稱式加密解密。因為這個過程中,服務端的私密金鑰只用來解密第三個隨機數,從來沒有在網路中傳輸過,這樣的話,只要私密金鑰沒有被泄露,那麼資料就是安全的。
- 加密通訊協定:就是雙方商量使用哪一種加密方式,假如兩者支援的加密方式不匹配,則無法進行通訊;
有個常見的問題,關於隨機數為什麼要三個?只最後一個隨機數N3不可以嗎?
這是由於SSL/TLS設計,就假設伺服器不相信所有的用戶端都能夠提供完全隨機數,假如某個用戶端提供的隨機數不隨機的話,就大大增加了“對話密鑰”被破解的風險,所以由三組隨機數組成最後的隨機數,保證了隨機數的隨機性,以此來保證每次產生的“對話密鑰”安全性。
數位憑證
數位憑證是一個電子文檔,其中包含了持有人的資訊、公開金鑰以及證明該認證有效數位簽章。而數位憑證以及相關的公開金鑰管理和驗證等技術組成了PKI(公開金鑰基礎設施)規範體系。一般來說,數位憑證是由數位憑證認證機構(Certificate authority,即CA)來負責簽發和管理,並承擔PKI體系中公開金鑰合法性的檢驗責任;數位憑證的類型有很多,而HTTPS使用的是SSL認證。
怎麼來驗證數位憑證是由CA簽發的,而不是第三方偽造的呢? 在回答這個問題前,我們需要先瞭解CA的組織圖。首先,CA組織圖中,最頂層的就是根CA,根CA下可以授權給多個二級CA,而二級CA又可以授權多個三級CA,所以CA的組織圖是一個樹結構。對於SSL認證市場來說,主要被Symantec(旗下有VeriSign和GeoTrust)、Comodo SSL、Go Daddy 和 GlobalSign 瓜分。 瞭解了CA的組織圖後,來看看數位憑證的簽發流程:
數位憑證的簽發機構CA,在接收到申請者的資料後進行核對並確定資訊的真實有效,然後就會製作一份符合X.509標準的檔案。認證中的認證內容包含的持有人資訊和公開金鑰等都是由申請者提供的,而數位簽章則是CA機構對認證內容進行hash加密後得到的,而這個數位簽章就是我們驗證認證是否是有可信CA簽發的資料。
假設認證是由認證簽發機構CA1簽發的。
1)接收端接到一份數位憑證Cer1後,對認證的內容做Hash得到H1;
2)從簽發該認證的機構CA1的數位憑證中找到公開金鑰,對認證上數位簽章進行解密,得到認證Cer1簽名的Hash摘要H2;
3)對比H1和H2,如相等,則表示認證沒有被篡改。
4)但這個時候還是不知道CA是否是合法的,我們看到中有CA機構的數位憑證,這個認證是公開的,所有人都可以擷取到。而這個認證中的數位簽章是上一級產生的,所以可以這樣一直遞迴驗證下去,直到根CA。根CA是自驗證的,即他的數位簽章是由自己的私密金鑰來產生的。合法的根CA會被瀏覽器和作業系統加入到權威信任CA列表中,這樣就完成了最終的驗證。所以,一定要保護好自己環境(瀏覽器/作業系統)中根CA信任清單,信任了根CA就表示信任所有根CA下所有子級CA所簽發的認證,不要隨便添加根CA認證。
一般作業系統和瀏覽器只包含根CA機構的認證,而在配置Web伺服器的HTTPS時,也會將配置整個憑證鏈結,所以整個校正流程是從最後的葉子節點認證開始,用父節點校正子節點,一層層校正整個憑證鏈結的可信性。
打個比喻:父(根CA數位憑證)-子(CA數位憑證)-孫(數位憑證)三代人,假設父沒有其他兄弟(相當於根CA機構是唯一的),假如子與父進行DNA親子評鑑,檢測DNA位點(即認證簽名)相同,那就基本確定子是由父所生;孫與子一樣。這樣就能夠確定孫肯定是源於父一脈,是父(根CA數位憑證)的合法繼承人。數位憑證的驗證就是基於同樣的原理。
2. 實現支援HTTPS
首先,需要明確你使用HTTP/HTTPS的用途,因為OSX和iOS平台提供了多種API,來支援不同的用途,官方文檔《Making HTTP and HTTPS Requests》有詳細的說明,而文檔《HTTPS Server Trust Evaluation》則詳細講解了HTTPS驗證相關知識,這裡就不多說了。本文主要講解我們最常用的NSURLConnection支援HTTPS的實現(NSURLSession的實現方法類似,只是要求授權證明的回調不一樣而已),以及怎麼樣使用AFNetworking這個非常流行的第三方庫來支援HTTPS。本文假設你對HTTP以及NSURLConnection的介面有了足夠的瞭解。
驗證認證的API
相關的Api在Security Framework中,驗證流程如下:
1). 第一步,先擷取需要驗證的信任物件(Trust Object)。這個Trust Object在不同的應用情境下擷取的方式都不一樣,對於NSURLConnection來說,是從delegate方法-connection:willSendRequestForAuthenticationChallenge:回調回來的參數challenge中擷取([challenge.protectionSpace serverTrust])。
2). 使用系統預設驗證方式驗證Trust Object。SecTrustEvaluate會根據Trust Object的驗證策略,一級一級往上,驗證憑證鏈結上每一級數位簽章的有效性(上一部分有講解),從而評估認證的有效性。
3). 如第二步驗證通過了,一般的安全要求下,就可以直接驗證通過,進入到下一步:使用Trust Object產生一份憑證([NSURLCredential credentialForTrust:serverTrust]),傳入challenge的sender中([challenge.sender useCredential:cred forAuthenticationChallenge:challenge])處理,建立串連。
4). 假如有更強的安全要求,可以繼續對Trust Object進行更嚴格的驗證。常用的方式是在本地匯入認證,驗證Trust Object與匯入的認證是否匹配。更多的方法可以查看Enforcing Stricter Server Trust Evaluation,這一部分在講解AFNetworking源碼中會講解到。
5). 假如驗證失敗,取消此次Challenge-Response Authentication驗證流程,拒絕串連請求。
ps: 假如是自建認證的,則不使用第二步系統預設的驗證方式,因為自建認證的根CA的數位簽章未在作業系統的信任清單中。
iOS授權驗證的API和流程大概瞭解了,下面,我們看看在NSURLConnection中的代碼實現:
使用NSURLConnection支援HTTPS的實現
// Now start the connection
NSURL * httpsURL = [NSURL URLWithString:@"https://www.google.com"];
self.connection = [NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:httpsURL] delegate:self];
//回調
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
//1)擷取trust object
SecTrustRef trust = challenge.protectionSpace.serverTrust;
SecTrustResultType result;
//2)SecTrustEvaluate對trust進行驗證
OSStatus status = SecTrustEvaluate(trust, &result);
if (status == errSecSuccess &&
(result == kSecTrustResultProceed ||
result == kSecTrustResultUnspecified)) {
//3)驗證成功,產生NSURLCredential憑證cred,告知challenge的sender使用這個憑證來繼續串連
NSURLCredential *cred = [NSURLCredential credentialForTrust:trust];
[challenge.sender useCredential:cred forAuthenticationChallenge:challenge];
} else {
//5)驗證失敗,取消這次驗證流程
[challenge.sender cancelAuthenticationChallenge:challenge];
}
}
上面是代碼是通過系統預設驗證流程來驗證認證的。假如我們是自建認證的呢?這樣Trust Object裡面伺服器的認證因為不是可信任的CA簽發的,所以直接使用SecTrustEvaluate進行驗證是不會成功。又或者,即使伺服器返回的認證是信任CA簽發的,又如何確定這認證就是我們想要的特定認證?這就需要先在本地匯入認證,設定成需要參與驗證的Anchor Certificate(錨點認證,通過SecTrustSetAnchorCertificates設定了參與校正錨點認證之後,假如驗證的數位憑證是這個錨點認證的子節點,即驗證的數位憑證是由錨點認證對應CA或子CA簽發的,或是該認證本身,則信任該認證),再調用SecTrustEvaluate來驗證。代碼如下
//先匯入認證
NSString * cerPath = ...; //認證的路徑
NSData * cerData = [NSData dataWithContentsOfFile:cerPath];
SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)(cerData));
self.trustedCertificates = @[CFBridgingRelease(certificate)];
//回調
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
//1)擷取trust object
SecTrustRef trust = challenge.protectionSpace.serverTrust;
SecTrustResultType result;
//注意:這裡將之前置入的認證設定成下面驗證的Trust Object的anchor certificate
SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)self.trustedCertificates);
//2)SecTrustEvaluate會尋找前面SecTrustSetAnchorCertificates設定的認證或者系統預設提供的認證,對trust進行驗證
OSStatus status = SecTrustEvaluate(trust, &result);
if (status == errSecSuccess &&
(result == kSecTrustResultProceed ||
result == kSecTrustResultUnspecified)) {
//3)驗證成功,產生NSURLCredential憑證cred,告知challenge的sender使用這個憑證來繼續串連
NSURLCredential *cred = [NSURLCredential credentialForTrust:trust];
[challenge.sender useCredential:cred forAuthenticationChallenge:challenge];
} else {
//5)驗證失敗,取消這次驗證流程
[challenge.sender cancelAuthenticationChallenge:challenge];
}
}
建議採用本地匯入認證的方式驗證認證,來保證足夠的安全性。更多的驗證方法,請查看官方文檔《HTTPS Server Trust Evaluation》
使用AFNetworking來支援HTTPS
AFNetworking是iOS/OSX開發最流行的第三方開源庫之一,其作者是非常著名的iOS/OSX開發人員Mattt Thompson,其部落格NSHipster也是iOS/OSX開發人員學習和開闊技術視野的好地方。AFNetworking已經將上面的邏輯代碼封裝好,甚至更完善,在AFSecurityPolicy檔案中,有興趣可以閱讀這個模組的代碼;
AFNetworking上配置對HTTPS的支援非常簡單:
NSURL * url = [NSURL URLWithString:@"https://www.google.com"];
AFHTTPRequestOperationManager * requestOperationManager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:url];
dispatch_queue_t requestQueue = dispatch_create_serial_queue_for_name("kRequestCompletionQueue");
requestOperationManager.completionQueue = requestQueue;
AFSecurityPolicy * securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
//allowInvalidCertificates 是否允許無效認證(也就是自建的認證),預設為NO
//如果是需要驗證自建認證,需要設定為YES
securityPolicy.allowInvalidCertificates = YES;
//validatesDomainName 是否需要驗證網域名稱,預設為YES;
//假如認證的網域名稱與你請求的網域名稱不一致,需把該項設定為NO;如設成NO的話,即伺服器使用其他可信任機構頒發的認證,也可以建立串連,這個非常危險,建議開啟。
//置為NO,主要用於這種情況:用戶端請求的是子網域名稱,而認證上的是另外一個網域名稱。因為SSL認證上的網域名稱是獨立的,假如認證上註冊的網域名稱是www.google.com,那麼mail.google.com是無法驗證通過的;當然,有錢可以註冊萬用字元的網域名稱*.google.com,但這個還是比較貴的。
//如置為NO,建議自己添加對應網域名稱的校正邏輯。
securityPolicy.validatesDomainName = YES;
//validatesCertificateChain 是否驗證整個憑證鏈結,預設為YES
//設定為YES,會將伺服器返回的Trust Object上的憑證鏈結與本地匯入的認證進行對比,這就意味著,假如你的憑證鏈結是這樣的:
//GeoTrust Global CA
// Google Internet Authority G2
// *.google.com
//那麼,除了匯入*.google.com之外,還需要匯入憑證鏈結上所有的CA認證(GeoTrust Global CA, Google Internet Authority G2);
//如是自建認證的時候,可以設定為YES,增強安全性;假如是信任的CA所簽發的認證,則建議關閉該驗證,因為整個憑證鏈結一一比對是完全沒有必要(請查看原始碼);
securityPolicy.validatesCertificateChain = NO;
requestOperationManager.securityPolicy = securityPolicy;
這就是AFNetworking的支援HTTPS的主要配置說明,AFHTTPSessionManager與之基本一致,就不重複了。
使用ASIHttpRequest來支援HTTPS
一種方法
ASIHTTPRequest *request = [ASIHTTPRequestrequestWithURL:[NSURLURLWithString:bodyString]];
[request setDelegate:self];
request.timeOutSeconds = 20;
[request setRequestMethod:@"POST"];
[request addRequestHeader:@"Content-Type"value:@"application/xml;charset=UTF-8;"];
[request setValidatesSecureCertificate:NO];//請求https的時候,就要設定這個屬性
[request startAsynchronous];
二種方法
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:[NSURL URLWithString:bodyString]];
[request setDelegate:self];
request.timeOutSeconds = 20;
[request setValidatesSecureCertificate:NO];//請求https的時候,就要設定這個屬性
[request setRequestMethod:@"POST"]; [request addRequestHeader:@"Content-Type" value:@"application/xml;charset=UTF-8"]; request.defaultResponseEncoding = NSUTF8StringEncoding;
使用ASIHttpRequest來支援HTTPS
一種方法
ASIHTTPRequest *request = [ASIHTTPRequestrequestWithURL:[NSURLURLWithString:bodyString]];
[request setDelegate:self];
request.timeOutSeconds = 20;
[request setRequestMethod:@"POST"];
[request addRequestHeader:@"Content-Type"value:@"application/xml;charset=UTF-8;"];
[request setValidatesSecureCertificate:NO];//請求https的時候,就要設定這個屬性
[request startAsynchronous];
二種方法
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:[NSURL URLWithString:bodyString]];
[request setDelegate:self];
request.timeOutSeconds = 20;
[request setValidatesSecureCertificate:NO];//請求https的時候,就要設定這個屬性
[request setRequestMethod:@"POST"]; [request addRequestHeader:@"Content-Type" value:@"application/xml;charset=UTF-8"]; request.defaultResponseEncoding = NSUTF8StringEncoding;
iOS支援Https