iOS開發中的HTTPS
前言:自娛自樂的iOS開發學習實踐。為實現一個簡單社交應用做功課,簡要理解基於SSL/TLS協議的HTTPS通信運行機制,實踐iOS端到伺服器端的安全通信。
提出問題
蘋果公司在2016的開發者大會上宣布:到2017年,所有的iOS應用都必須使用HTTPS與伺服器進行通信。iOS開發者應該都不會對這個決定感到驚訝,因為自iOS9就已經引入了ATS(應用傳輸安全App Transport Security)特性,該特性對應用的安全傳輸做出了以下要求:
The protocol Transport Security Layer (TLS) must be at least version 1.2. Connection ciphers are limited to those that provide forward secrecy (see the list of ciphers below.) Certificates must use at least an SHA256 fingerprint with either a 2048 bit or greater RSA key, or a 256 bit or greater Elliptic-Curve (ECC) key.
不符合以上條件的任意一項,網路請求將會被中斷並返回空值。
既然蘋果公司划出了最後期限,HTTPS將成為必然,那麼一個iOS開發自學者自然會提出以下問題:
- HTTPS的原理和運行機制是什麼?
- 為實現HTTPS通信,伺服器端需要做什麼?
- 為實現HTTPS通信,iOS客戶端需要做什麼?
筆者將以上述問題和學習中遇到的新問題為牽引,逐個進行學習與實踐。
HTTPS的原理及運行機制
網路上有很多對於SSL/TLS協議進行講解的文章,比如SSL/TLS協議運行機制的概述和圖解SSL/TLS協議兩篇就很不錯,推薦閱讀。閱讀之後能基本理解HTTPS的原理和運行機制,本小節簡要歸納自己的理解。
首先,不使用SSL/TLS的HTTP通信,是不加密的通信,這顯然不能保證客戶的信息安全。在上一篇功課iOS開發中使用keyChain保存用戶密碼中,辛辛苦苦地將用戶名密碼能夠安全地存儲在keyChain中(不越獄的前提下),但是如果用戶名密碼就這樣明文地發送出去,一旦被截獲,後果可想而知。當然,我們很自然地就會想到將用戶密碼字元串進行MD5加密後,再通過網路發送給伺服器,比如:
NSString *sourceStr = [NSString stringWithFormat:@"attach=iOS&chartset=utf-8&format=json&partner=google&userid=%@&password=%@」,userid,password];NSString *signStr = [NSString md5String:sourceStr];
但即便如此也不能保證安全,因為存在碰撞攻擊。關於碰撞攻擊,可以閱讀安全科普:密碼學之碰撞攻擊一文。
於是,為了實現通信的安全,SSL/TLS協議採用公鑰加密法,其運行的基本流程是:
- 客戶端向伺服器端索要並驗證公鑰;
- 雙方協商生成"對話密鑰";
- 雙方採用"對話密鑰"進行加密通信。
其中,第1和2步被稱為握手階段。握手階段的細節這裡就不贅述,我們只需要知道,通過握手階段,客戶端和伺服器端主要交換了3個信息:
- 數字證書。該信息是我們進行開發需要關注的!數字證書包含了公鑰等信息,一般由伺服器發給客戶端,接收方通過驗證這個證書是不是由信賴的CA簽發,或者與本地的證書相對比,來判斷證書是否可信;假如需要雙向驗證,則伺服器和客戶端都需要發送數字證書給對方驗證;
- 3個隨機數。3個隨機數是用於生成對話密鑰的,我們不需要關心這細節;
- 加密通信協議。客戶端和伺服器端通信需要採取同樣的加密通信協議,我們也不需要太關注。
數字證書與公鑰基礎設施
對於不大接觸網路安全的同學,看了上面的原理,可能和我一樣也是充滿問題,這是因為我們是半路出家,既不知道頂層構架,又不接地氣。但是如果對於公鑰基礎設施(PKI)和數字證書的關係有了一個直觀的理解,疑惑就會迎刃而解。
剛才已經了解到HTTPS是基於公鑰加密法,而公鑰基礎設施(PKI)就是一種遵循標準的利用公鑰加密技術為電子商務的開展提供一套安全基礎平台的技術和規範。完整的PKI系統擁有權威認證機構(CA)、數字證書庫、密鑰備份及恢復系統、證書作廢系統、應用介面(API)等基本構成部分,其中權威認證機構CA將是我們需要打交道的部門。
該系統的邏輯關係可以這樣理解:
- 申請人向CA提交申請材料;
- 數字證書是由證書認證機構(CA)對證書申請者真實身份驗證之後,用CA的根證書對申請人的一些基本信息以及申請人的公鑰進行簽名(相當於加蓋發證書機構的公章)後形成的一個數字文件。CA完成簽發證書後,會將證書發布在CA的證書庫(目錄伺服器)中,任何人都可以查詢和下載,因此數字證書和公鑰一樣是公開的。
- 每個證書持有人都有一對公鑰和私鑰,這兩把密鑰可以互為加解密。公鑰是公開的,不需要保密,而私鑰是由證書持有人自己持有,並且必須妥善保管和注意保密。
- 簡單地說,數字證書就是經過CA認證過的公鑰,而私鑰一般情況都是由證書持有者在自己本地生成的,由證書持有者自己負責保管。
至此,邏輯鏈條就和SSL/TLS協議運行機制銜接上了:申請人事先通過向CA申請,已經有了公鑰和私鑰。客戶端向伺服器請求,能夠得到包含公鑰的數字證書。得到公鑰後,雙方就按照上節中運行的基本流程生成對話密鑰,而後開始加密通信。如果對於數字證書和CA之間的關係還不是很清楚,建議閱讀數字證書及CA的掃盲介紹一文。
思維比較敏銳的同學也許會問:
我怎麼知道數字證書是由CA簽發的,而不是第三方偽造的呢?
問得好!簡單地說,就是用CA的組織結構和數字證書的簽發流程來保證,具體細節在這裡不討論,感興趣的同學可以閱讀iOS安全系列之一:HTTPS一文。
小結:客戶端向伺服器發出請求得到包含公鑰的數字證書,數字證書的真實性是由CA來進行保證。基於公鑰,客戶端和伺服器端能夠通過「握手」建立加密的通信。
伺服器端如何實現HTTPS
如何實現,肯定得靠自己搭建一個HTTPS伺服器啊。根據網路上的資料,可知有兩種方式來搭建HTTPS伺服器:
- 一種是創建證書請求,然後到權威機構認證,隨之配置到伺服器;
- 一種是自建證書,然後配置給伺服器。
第一種方式搭建的HTTPS伺服器是最優的。建立網站的話,直接就會被信任。而伺服器作為移動端app的伺服器時,也不需要為ATS做過多的適配(正是我所需要積累知識的方向)。雖然說權威的機構認證都是需要錢的,但是如今也不乏存在免費的第三方認證機構;
第二種方式搭建的HTTPS伺服器,對於網站來說完全不可行,用戶打開時直接彈出一個警告提醒,說這是一個不受信任的網站,讓用戶是否繼續,體驗很差,而且讓用戶感覺網站不安全。對於移動端來說,在iOS9出現之前,這個沒什麼問題,但是在iOS9出來之後,第二種方式是通不過ATS特性,需要在info.plist文件中將App Transport Security Settings中的Allow Arbitrary Loads設置為YES才行。
在本文中,為了快速地驗證iOS端與HTTPS伺服器能夠不需要為ATS做過多的適配,採取了選擇一個現成的HTTPS伺服器來做驗證的方式,學習如何自己搭建的工作,放到下一篇功課中。那麼如何才能知道一個HTTPS伺服器是符合ATS特性中的要求的呢?使用nscurl命令如下:
nscurl --ats-diagnostics --verbose https://example.com
命令後接的URL可以使你想要檢測的HTTPS伺服器地址。接下來,讓我們看看知乎能不能通過ATS檢測,在終端中輸入命令:
nscurl --ats-diagnostics --verbose https://www.zhihu.com
輸出如下:
Starting ATS DiagnosticsConfiguring ATS Info.plist keys and displaying the result of HTTPS loads to https://www.zhihu.com.A test will "PASS" if URLSession:task:didCompleteWithError: returns a nil error.=======================================================================Default ATS Secure Connection---ATS Default ConnectionATS Dictionary:{}Result : PASS---=======================================================================Allowing Arbitrary Loads---Allow All LoadsATS Dictionary:{ NSAllowsArbitraryLoads = true;}Result : PASS---=======================================================================Configuring TLS exceptions for www.zhihu.com---TLSv1.2ATS Dictionary:{ NSExceptionDomains = { "www.zhihu.com" = { NSExceptionMinimumTLSVersion = "TLSv1.2"; }; };}Result : PASS------TLSv1.1ATS Dictionary:{ NSExceptionDomains = { "www.zhihu.com" = { NSExceptionMinimumTLSVersion = "TLSv1.1"; }; };}Result : PASS------TLSv1.0ATS Dictionary:{ NSExceptionDomains = { "www.zhihu.com" = { NSExceptionMinimumTLSVersion = "TLSv1.0"; }; };}Result : PASS---=======================================================================Configuring PFS exceptions for www.zhihu.com---Disabling Perfect Forward SecrecyATS Dictionary:{ NSExceptionDomains = { "www.zhihu.com" = { NSExceptionRequiresForwardSecrecy = false; }; };}Result : PASS---=======================================================================Configuring PFS exceptions and allowing insecure HTTP for www.zhihu.com---Disabling Perfect Forward Secrecy and Allowing Insecure HTTPATS Dictionary:{ NSExceptionDomains = { "www.zhihu.com" = { NSExceptionAllowsInsecureHTTPLoads = true; NSExceptionRequiresForwardSecrecy = false; }; };}Result : PASS---=======================================================================Configuring TLS exceptions with PFS disabled for www.zhihu.com---TLSv1.2 with PFS disabledATS Dictionary:{ NSExceptionDomains = { "www.zhihu.com" = { NSExceptionMinimumTLSVersion = "TLSv1.2"; NSExceptionRequiresForwardSecrecy = false; }; };}Result : PASS------TLSv1.1 with PFS disabledATS Dictionary:{ NSExceptionDomains = { "www.zhihu.com" = { NSExceptionMinimumTLSVersion = "TLSv1.1"; NSExceptionRequiresForwardSecrecy = false; }; };}Result : PASS------TLSv1.0 with PFS disabledATS Dictionary:{ NSExceptionDomains = { "www.zhihu.com" = { NSExceptionMinimumTLSVersion = "TLSv1.0"; NSExceptionRequiresForwardSecrecy = false; }; };}Result : PASS---=======================================================================Configuring TLS exceptions with PFS disabled and insecure HTTP allowed for www.zhihu.com---TLSv1.2 with PFS disabled and insecure HTTP allowedATS Dictionary:{ NSExceptionDomains = { "www.zhihu.com" = { NSExceptionAllowsInsecureHTTPLoads = true; NSExceptionMinimumTLSVersion = "TLSv1.2"; NSExceptionRequiresForwardSecrecy = false; }; };}Result : PASS------TLSv1.1 with PFS disabled and insecure HTTP allowedATS Dictionary:{ NSExceptionDomains = { "www.zhihu.com" = { NSExceptionAllowsInsecureHTTPLoads = true; NSExceptionMinimumTLSVersion = "TLSv1.1"; NSExceptionRequiresForwardSecrecy = false; }; };}Result : PASS------TLSv1.0 with PFS disabled and insecure HTTP allowedATS Dictionary:{ NSExceptionDomains = { "www.zhihu.com" = { NSExceptionAllowsInsecureHTTPLoads = true; NSExceptionMinimumTLSVersion = "TLSv1.0"; NSExceptionRequiresForwardSecrecy = false; }; };}Result : PASS---=====================================================================
可以看到知乎在所有的測試案例中都是PASS,由此證明知乎通過了ATS檢測,給知乎對於用戶信息安全的保護點個贊。
然後,進入喜聞樂見的黑百度時間:
nscurl --ats-diagnostics --verbose https://www.baidu.com
然而結果顯示,百度同樣通過了ATS測試,所以說百度做得也不錯。最後被黑到的是新浪微博,感興趣的知友可以試試,這裡就不贅述了。現在我們已經有了百度和知乎這兩個HTTPS伺服器,接下來就看客戶端是否能夠成功訪問了。
iOS端如何實現HTTPS
基於對HTTPS運行機制的理解,我們知道,在iOS客戶端實現與伺服器的HTTPS通信,前提條件是你伺服器是一個提供了HTTPS的伺服器。如果前提得以滿足,那麼iOS客戶端就需要向伺服器發出請求索要公鑰,而後驗證公鑰,然後進行握手,左後開始加密通信。那麼,具體怎麼做呢?難倒這些都需要我自己實現嗎?肯定不是的,這種基礎性工作,蘋果早就做好了,著名的第三方庫AFNetworking也早就做好了。本文中主要學習和實踐基於AFNetworking的通信。
在上文中提到過,經過CA認證的HTTPS伺服器是最好的,在iOS客戶端這裡基本上不需要做太多ATS適配,現在我們就嘗試一下。在上篇功課使用CocoaPods安裝AFNetworking並測試中,已經通過設置ATS能夠實現HTTP的GET網路請求,這裡我們就基於該項目進行修改如下:
- 首先,刪除掉info.plist文件中App Transport Security Settings及其子項Allow Arbitrary Loads,讓ATS恢復到默認狀態。
- 其次,修改viewDidLoad方法中的代碼,主要將url修改為HTTPS伺服器的url:
- (void)viewDidLoad { [super viewDidLoad]; // 將上次實驗的URL注釋掉 // NSString *urlString = @"http://api.jirengu.com/weather.php"; // NSString *urlString = @"http://itunes.apple.com/search?term=metallica"; // 使用baidu的HTTPS鏈接 NSString *urlString = @"https://www.baidu.com"; NSURL *url = [NSURL URLWithString:urlString]; AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; [manager GET:url.absoluteString parameters:nil progress:nil success:^(NSURLSessionDataTask *task, id responseObject) { NSLog(@"results: %@", responseObject); } failure:^(NSURLSessionDataTask *task, NSError *error) { NSLog(@"results: %@", error); }];}
運行項目,得到報錯如下:
...JSON text did not start with array or object and option to allow fragments not set
通過查詢,發現問題是因為AFNetworking默認把請求的相應結果認為是JSON數據,然而實際上我們輸入百度的地址,得到的應該是html數據。但是AFNetworking並不知道,它堅信請求的結果就是一個json文本,然後固執地以json的形式去解析,這樣顯然沒辦法把一個網頁解析成一個字典或者數組,所以產生了上述錯誤。
解決辦法很簡答,在代碼中添加一句:
manager.responseSerializer = [AFHTTPResponseSerializer serializer]
告訴AFNetworking別把這個網頁當成JSON數據來解析!於是修改代碼如下:
- (void)viewDidLoad { [super viewDidLoad]; // 將上次實驗的URL注釋掉 // NSString *urlString = @"http://api.jirengu.com/weather.php"; // NSString *urlString = @"http://itunes.apple.com/search?term=metallica"; // 使用baidu的HTTPS鏈接 NSString *urlString = @"https://www.baidu.com"; NSURL *url = [NSURL URLWithString:urlString]; AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer serializer] [manager GET:url.absoluteString parameters:nil progress:nil success:^(NSURLSessionDataTask *task, id responseObject) { NSLog(@"results: %@", responseObject); } failure:^(NSURLSessionDataTask *task, NSError *error) { NSLog(@"results: %@", error); }];}
運行項目,得到輸出如下:
2016-10-08 20:00:18.052 HTTPS[1916:359510] results: <3c21444f 43545950 45206874 6d6c3e0a 3c68746d 6c3e0a20 2020203c 212d2d53 54415455 53204f4b 2d2d3e0a 20202020 3c686561 643e0a20 20202020 2020203c ...... 6c3e0a>Message from debugger: Terminated due to signal 15
顯然,這次成功地獲取到了數據。將HTTPS鏈接換為知乎的URL,得到的結果類似。
由此可知,對於符合ATS要求的HTTPS伺服器,在iOS端不需要對ATS做特殊的適配就能和HTTPS伺服器進行通信。而要符合ATS要求,則需要老老實實地創建證書請求,然後到權威機構認證,隨之配置到伺服器。
小結:在本篇功課中,對HTTPS通信的原理和機制有了一定了解,實現了利用AFNetworking庫向符合ATS要求的HTTPS伺服器的通信。
下一步
學習自己搭建伺服器,創建證書請求,然後到權威機構認證,然後配置到伺服器使之成為符合ATS要求的HTTPS伺服器,最後實現iOS端與伺服器端的安全通信。
作者反饋
- 自娛自樂的iOS開發學習,歡迎專業開發者批評指教;
- 對於學習自建證書,然後配置給伺服器的方式,沒有計划去學習;
- 注意:知友@蘭卿私信指正:
文章里的HTTPS請求通過Charles是可以抓到包且查看每一個欄位的,Charles是通過代理,AFN沒有在有代理的情況下對HTTPS進行限制,對用戶來說是不安全的。ASI可以做限制,有代理則請求不成功,保證即使被抓包,也無法得到欄位。
個人對於@蘭卿指出的問題不是很清楚,還需要查閱資料進行學習驗證,後續搞明白之後在修正到文中。本著對用戶安全負責的原則,將這個指正放在這裡,請各位知友注意。最後感謝@蘭卿的批評指正。
推薦閱讀:
※?? 也談 HTTPS - 如何內測
※android開發如何保障本地加密密鑰的安全?
※HTTPS 為什麼更安全,先看這些
※大致介紹下SSL?
※HTTP Status Codes