這篇文章是我一邊學習證書驗證一邊記錄的內容,
稍微整理了下,共扯了三部份內容:html
HTTPS 是運行在 TLS/SSL 之上的 HTTP,與普通的 HTTP 相比,在數據傳輸的安全性上有很大的提高。
要了解它安全性的巧妙之處,須要先簡單地瞭解對稱加密和非對稱加密的區別:ios
爲了提升安全性,咱們經常使用的作法是使用對稱加密的手段加密數據。但是隻使用對稱加密的話,雙方通訊的開始總會以明文的方式傳輸密鑰。那麼從一開始這個密鑰就泄露了,談不上什麼安全。因此 TLS/SSL 在握手的階段,結合非對稱加密的手段,保證只有通訊雙方纔知道對稱加密的密鑰。大概的流程以下:web
因此,HTTPS 實現傳輸安全的關鍵是:在 TLS/SSL 握手階段保證僅有通訊雙方獲得 Session Key!算法
X.509 應該是比較流行的 SSL 數字證書標準,包含(但不限於)如下的字段:後端
字段 | 值說明 |
---|---|
對象名稱(Subject Name) | 用於識別該數字證書的信息 |
共有名稱(Common Name) | 對於客戶證書,一般是相應的域名 |
證書頒發者(Issuer Name) | 發佈並簽署該證書的實體的信息 |
簽名算法(Signature Algorithm) | 簽名所使用的算法 |
序列號(Serial Number) | 數字證書機構(Certificate Authority, CA)給證書的惟一整數,一個數字證書一個序列號 |
生效期(Not Valid Before) | (`・ω・´) |
失效期(Not Valid After) | (╯°口°)╯(┴—┴ |
公鑰(Public Key) | 可公開的密鑰 |
簽名(Signature) | 經過簽名算法計算證書內容後獲得的數據,用於驗證證書是否被篡改 |
除了上述所列的字段,還有不少拓展字段,在此不一一詳述。瀏覽器
下圖爲 Wikipedia 的公鑰證書:安全
數字證書的生成是分層級的,下一級的證書須要其上一級證書的私鑰簽名。
因此後者是前者的證書頒發者,也就是說上一級證書的 Subject Name 是其下一級證書的 Issuer Name。服務器
在獲得證書申請者的一些必要信息(對象名稱,公鑰私鑰)以後,證書頒發者經過 SHA-256 哈希獲得證書內容的摘要,再用本身的私鑰給這份摘要加密,獲得數字簽名。綜合已有的信息,生成分別包含公鑰和私鑰的兩個證書。session
扯到這裏,就有幾個問題:數據結構
問:若是說發佈一個數字證書必需要有上一級證書的私鑰加密,那麼最頂端的證書——根證書怎麼來的?
根證書是自簽名的,即用本身的私鑰簽名,不須要其餘證書的私鑰來生成簽名。
問:怎麼驗證證書是有沒被篡改?
當客戶端走 HTTPS 訪問站點時,服務器會返回整個證書鏈。如下圖的證書鏈爲例:
要驗證
*.wikipedia.org
這個證書有沒被篡改,就要用到GlobalSign Organization Validation CA - SHA256 - G2
提供的公鑰解密前者的簽名獲得摘要 Digest1,咱們的客戶端也計算前者證書的內容獲得摘要 Digest2。對比這兩個摘要就能知道前者是否被篡改。後者同理,使用GlobalSign Root CA
提供的公鑰驗證。當驗證到到受信任的根證書時,就能肯定*.wikipedia.org
這個證書是可信的。
問:爲何上面那個根證書 GlobalSign Root CA
是受信任的?
數字證書認證機構(Certificate Authority, CA)簽署和管理的 CA 根證書,會被歸入到你的瀏覽器和操做系統的可信證書列表中,並由這個列表判斷根證書是否可信。因此不要隨便導入奇奇怪怪的根證書到你的操做系統中。
問:生成的數字證書(如 *.wikipedia.org
)均可用來簽署新的證書嗎?
不必定。以下圖,拓展字段裏面有個叫 Basic Constraints 的數據結構,裏面有個字段叫路徑長度約束(Path Length Constraint),代表了該證書能繼續簽署 CA 子證書的深度,這裏爲0,說明這個
GlobalSign Organization Validation CA - SHA256 - G2
只能簽署客戶端證書,而客戶端證書不能用於簽署新的證書,CA 子證書才能這麼作。
在 Overriding TLS Chain Validation Correctly 中提到:
When a TLS certificate is verified, the operating system verifies its chain of trust. If that chain of trust contains only valid certificates and ends at a known (trusted) anchor certificate, then the certificate is considered valid.
因此在 iOS 中,證書是否有效的標準是:
信任鏈中若是隻含有有效證書而且以可信錨點(trusted anchor)結尾,那麼這個證書就被認爲是有效的。
其中可信錨點指的是系統隱式信任的證書,一般是包括在系統中的 CA 根證書。不過你也能夠在驗證證書鏈時,設置自定義的證書做爲可信的錨點。
具體到使用 NSURLSession 走 HTTPS 訪問網站,-URLSession:didReceiveChallenge:completionHandler:
回調中會收到一個 challenge,也就是質詢,須要你提供認證信息才能完成鏈接。這時候能夠經過 challenge.protectionSpace.authenticationMethod
取得保護空間要求咱們認證的方式,若是這個值是 NSURLAuthenticationMethodServerTrust
的話,咱們就能夠插手 TLS 握手中「驗證數字證書有效性」這一步。
系統的默認實現(也即代理不實現這個方法)是驗證這個信任鏈,結果是有效的話則根據 serverTrust 建立 credential 用於同服務端確立 SSL 鏈接。不然會獲得 「The certificate for this server is invalid...」 這樣的錯誤而沒法訪問。
好比在訪問 https://www.google.com 的時候咧,咱們不實現這個方法也能訪問成功的。系統對 Google 服務器返回來的證書鏈,從葉節點證書往根證書層層驗證(有效期、簽名等等),遇到根證書時,發現做爲可信錨點的它存在與可信證書列表中,那麼驗證就經過,容許與服務端創建鏈接。
而當咱們訪問 https://www.12306.cn 時,就會出現 "The certificate for this server is invalid. You might be connecting to a server that is pretending to be 「www.12306.cn」 which could put your confidential information at risk." 的錯誤。緣由就是系統在驗證到根證書時,發現它是自簽名、不可信的。
若是咱們要實現這個代理方法的話,須要提供 NSURLSessionAuthChallengeDisposition(處置方式)和 NSURLCredential(資格認證)這兩個參數給 completionHandler 這個 block:
1 -(void)URLSession:(NSURLSession *)session 2 didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge 3 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, 4 NSURLCredential * _Nullable))completionHandler { 5 6 // 若是使用默認的處置方式,那麼 credential 就會被忽略 7 NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling; 8 NSURLCredential *credential = nil; 9 10 if ([challenge.protectionSpace.authenticationMethod 11 isEqualToString: 12 NSURLAuthenticationMethodServerTrust]) { 13 14 /* 調用自定義的驗證過程 */ 15 if ([self myCustomValidation:challenge]) { 16 credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; 17 if (credential) { 18 disposition = NSURLSessionAuthChallengeUseCredential; 19 } 20 } else { 21 /* 無效的話,取消 */ 22 disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge 23 } 24 } 25 if (completionHandler) { 26 completionHandler(disposition, credential); 27 } 28 }
在 [self myCustomValidation:challenge]
調用自定義驗證過程,結果是有效的話才建立 credential 確立鏈接。
自定義的驗證過程,須要先拿出一個 SecTrustRef 對象,它是一種執行信任鏈驗證的抽象實體,包含着驗證策略(SecPolicyRef)以及一系列受信任的錨點證書,而咱們能作的也是修改這兩樣東西而已。
1 SecTrustRef trust = challenge.protectionSpace.serverTrust;
拿到 trust 對象以後,能夠用下面這個函數對它進行驗證。
1 static BOOL serverTrustIsVaild(SecTrustRef trust) { 2 BOOL allowConnection = NO; 3 4 // 假設驗證結果是無效的 5 SecTrustResultType trustResult = kSecTrustResultInvalid; 6 7 // 函數的內部遞歸地從葉節點證書到根證書的驗證 8 OSStatus statue = SecTrustEvaluate(trust, &trustResult); 9 10 if (statue == noErr) { 11 // kSecTrustResultUnspecified: 系統隱式地信任這個證書 12 // kSecTrustResultProceed: 用戶加入本身的信任錨點,顯式地告訴系統這個證書是值得信任的 13 14 allowConnection = (trustResult == kSecTrustResultProceed 15 || trustResult == kSecTrustResultUnspecified); 16 } 17 return allowConnection; 18 }
這個函數何時調用徹底取決於你的需求,若是你不想對驗證策略作修改而直接調用的話,那你竟然還看到這裏!?(╯‵□′)╯︵┻━┻
能夠經過如下的代碼得到當前的驗證策略:
1 CFArrayRef policiesRef;
2 SecTrustCopyPolicies(trust, &policiesRef);
打印 policiesRef 後,你會發現默認的驗證策略就包含了域名驗證,即「服務器證書上的域名和請求域名是否匹配」。若是你的一個證書須要用來鏈接不一樣域名的主機,或者你直接用 IP 地址去鏈接,那麼你能夠重設驗證策略以忽略域名驗證:
1 NSMutableArray *policies = [NSMutableArray array]; 2 3 // BasicX509 不驗證域名是否相同 4 SecPolicyRef policy = SecPolicyCreateBasicX509(); 5 [policies addObject:(__bridge_transfer id)policy]; 6 SecTrustSetPolicies(trust, (__bridge CFArrayRef)policies); 7 8
後再調用 serverTrustIsVaild()
驗證。
可是若是不驗證域名的話,安全性就會大打折扣。拿瀏覽器舉🌰:
試想你要傳輸報文到 https://www.real-website.com ,然而因爲域名劫持,把你帶到了 https://www.real-website.cn 這個🎣網站,大概有如下兩種結果:
你可能會問:公鑰證書是每一個人都能獲得的,釣魚網站能不能返回真正的公鑰證書給咱們呢?
我以爲是能夠的,然而這並無什麼卵用。沒有私鑰的釣魚服務器沒法得到第三個隨機數,沒法生成 Session Key,也就不能對咱們傳給它的數據進行解密了。
在 App 中想要防止上面提到的中間人公雞攻擊,比較好的作法是將公鑰證書打包進 App 中,而後在收到服務端證書鏈的時候,可以有效地驗證服務端是否可信,這也是驗證自簽名的證書鏈所必須作的。
假設你的服務器返回:[你的自簽名的根證書] -- [你的二級證書] -- [你的客戶端證書],系統是不信任這個三個證書的。
因此你在驗證的時候須要將這三個的其中一個設置爲錨點證書,固然,多個也行。
好比將 [你的二級證書] 做爲錨點後,SecTrustEvaluate()
函數只要驗證到 [你的客戶端證書] 確實是由 [你的二級證書] 簽署的,那麼驗證結果爲 kSecTrustResultUnspecified
,代表了 [你的客戶端證書] 是可信的。下面是設置錨點證書的作法:
1 NSMutableArray *certificates = [NSMutableArray array]; 2 3 NSDate *cerData = /* 在 App Bundle 中你用來作錨點的證書數據,證書是 CER 編碼的,常見擴展名有:cer, crt...*/ 4 5 SecCertificateRef cerRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)cerData); 6 7 [certificates addObject:(__bridge_transfer id)cerRef]; 8 9 // 設置錨點證書。 10 SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)certificates);
只調用 SecTrustSetAnchorCertificates ()
這個函數的話,那麼就只有做爲參數被傳入的證書做爲錨點證書,連繫統自己信任的 CA 證書不能做爲錨點驗證證書鏈。要想恢復系統中 CA 證書做爲錨點的功能,還要再調用下面這個函數:
1 // true 表明僅被傳入的證書做爲錨點,false 容許系統 CA 證書也做爲錨點
2 SecTrustSetAnchorCertificatesOnly(trust, false);
這樣,再調用 serverTrustIsVaild()
驗證證書有效性就能成功了。
上面說的是沒通過 CA 認證的自簽證書的驗證,而 CA 的證書鏈的驗證方式也是同樣,不一樣點在不可信錨點的證書類型不同而已:前者的錨點是自籤的須要被打包進 App 用於驗證,後者的錨點可能原本就存在系統之中了。不過我腦補了這麼的一個坑:
假如咱們使用的是 CA 根證書籤署的數字證書,並且只用這個 CA 根證書做爲錨點,在不驗證域名的狀況下,是否是就會在握手階段信任被同一個 CA 根證書籤名的僞造證書呢?
Overriding TLS Chain Validation Correctly
上文有什麼我理解得不正確、或表達不許確的地方,煩請指教。🌝
公司的接口通常會兩種協議的,一種HTTP,一種HTTPS的,HTTP 只要請求,服務器就會響應,若是咱們不對請求和響應作出加密處理,全部信息都是會被檢測劫持到的,是很不安全的,客戶端加密可使用我這套工具類進行處理:文章地址
可是不論在任什麼時候候,都應該將服務置於HTTPS上,由於它能夠避免中間人攻擊的問題,還自帶了基於非對稱密鑰的加密通道!現實是這些年涌現了大量速成的移動端開發人員,這些人每每基礎不好,徹底不瞭解加解密爲什麼物,使用HTTPS後,能夠省去教育他們各類加解密技術,生活輕鬆多了。
簡答說,HTTPS 就是 HTTP協議加了一層SSL協議的加密處理,SSL 證書就是遵照 SSL協議,由受信任的數字證書頒發機構CA(如GlobalSign,wosign),在驗證服務器身份後頒發,這是須要花錢滴,簽發後的證書做爲公鑰通常放在服務器的根目錄下,便於客戶端請求返回給客戶端,私鑰在服務器的內部中心保存,用於解密公鑰。
一、客戶端發送請求,服務器返回公鑰給客戶端;
二、客戶端生成對稱加密祕鑰,用公鑰對其進行加密後,返回給服務器;
三、服務器收到後,利用私鑰解開獲得對稱加密祕鑰,保存;
四、以後的交互都使用對稱加密後的數據進行交互。
談下證書
簡單說,證書有兩種,一種是正經的:
一種是不正經的:
若是遇到正經的證書,咱們直接用AFNetworking 直接請求就行了,AFNetworking 內部幫咱們封裝了HTTPS的請求方式,可是大部分公司接口都是不正經的證書,這時須要咱們作如下幾步:
一、將服務器的公鑰證書拖到Xcode中
二、修改驗證模式
manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
原理:
簡單來講,就是你本能夠修改AFN這個設置來容許客戶端接收服務器的任何證書,可是這麼作有個問題,就是你沒法驗證證書是不是你的服務器後端的證書,給中間人攻擊,即經過重定向路由來分析僞造你的服務器端打開了大門。
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy defaultPolicy];
securityPolicy.allowInvalidCertificates = YES;
解決方法:AFNetworking是容許內嵌證書的,經過內嵌證書,AFNetworking就經過比對服務器端證書、內嵌的證書、站點域名是否一致來驗證鏈接的服務器是否正確。因爲CA證書驗證是經過站點域名進行驗證的,若是你的服務器後端有綁定的域名,這是最方便的。將你的服務器端證書,若是是pem格式的,用下面的命令轉成cer格式
openssl x509 -in <你的服務器證書>.pem -outform der -out server.cer
而後將生成的server.cer文件,若是有自建ca,再加上ca的cer格式證書,引入到app的bundle裏,AFNetworking在
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy AFSSLPinningModeCertificate];
或者
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy AFSSLPinningModePublicKey];
狀況下,會自動掃描bundle中.cer的文件,並引入,這樣就能夠經過自簽證書來驗證服務器惟一性了。
AFSecurityPolicy分三種驗證模式:
AFSSLPinningModeNone
這個模式表示不作SSL pinning,
只跟瀏覽器同樣在系統的信任機構列表裏驗證服務端返回的證書。若證書是信任機構簽發的就會經過,如果本身服務器生成的證書就不會經過。AFSSLPinningModeCertificate
這個模式表示用證書綁定方式驗證證書,須要客戶端保存有服務端的證書拷貝,這裏驗證分兩步,第一步驗證證書的域名有效期等信息,第二步是對比服務端返回的證書跟客戶端返回的是否一致。
AFSSLPinningModePublicKey
這個模式一樣是用證書綁定方式驗證,客戶端要有服務端的證書拷貝,
只是驗證時只驗證證書裏的公鑰,不驗證證書的有效期等信息。只要公鑰是正確的,就能保證通訊不會被竊聽,由於中間人沒有私鑰,沒法解開經過公鑰加密的數據。