iOS 防 DNS 汙染方案調研--- HTTPS(非SNI) 業務場景
1. 背景說明
本文主要介紹 HTTPS(含SNI) 業務場景下在 iOS 端實現 “IP直連” 的通用解決方案。
1.1 HTTPS
發送 HTTPS 請求首先要進行 SSL/TLS 握手,握手過程大致如下:
- 客戶端發起握手請求,攜帶隨機數、支持算法列表等參數。
- 服務端收到請求,選擇合適的算法,下發公鑰證書和隨機數。
- 客戶端對服務端證書進行校驗,並發送隨機數信息,該信息使用公鑰加密。
- 服務端通過私鑰獲取隨機數信息。
- 雙方根據以上交互的信息生成session ticket,用作該連接後續數據傳輸的加密密鑰。
上述過程中,和“IP直連”有關的是第3步,客戶端需要驗證服務端下發的證書,驗證過程有以下兩個要點:
- 客戶端用本地保存的根證書解開證書鏈,確認服務端下發的證書是由可信任的機構頒發的。
- 客戶端需要檢查證書的 domain 域和擴展域,看是否包含本次請求的 host。
如果上述兩點都校驗通過,就證明當前的服務端是可信任的,否則就是不可信任,應當中斷當前連接。
當客戶端使用“IP直連”解析域名時,請求URL中的host會被替換成解析出來的IP,所以在證書驗證的第2步,會出現domain不匹配的情況,導致SSL/TLS握手不成功。
1.2 SNI
SNI(Server Name Indication)是為了解決一個服務器使用多個域名和證書的SSL/TLS擴展。它的工作原理如下:
- 在連接到服務器建立SSL鏈接之前先發送要訪問站點的域名(Hostname)。
- 服務器根據這個域名返回一個合適的證書。
目前,大多數操作係統和瀏覽器都已經很好地支持SNI擴展,OpenSSL 0.9.8也已經內置這一功能。
上述過程中,當客戶端使用“IP直連”時,請求URL中的host會被替換成解析出來的IP,導致服務器獲取到的域名為解析後的IP,無法找到匹配的證書,隻能返回默認的證書或者不返回,所以會出現SSL/TLS握手不成功的錯誤。
比如當你需要通過 HTTPS 訪問 CDN 資源時,CDN 的站點往往服務了很多的域名,所以需要通過SNI指定具體的域名證書進行通信。
2. HTTPS場景(非SNI)解決方案
針對“domain不匹配”問題,可以采用如下方案解決:hook 證書校驗過程中第2步,將IP直接替換成原來的域名,再執行證書驗證。該方案與使用“自定義證書”進行 HTTPS 請求的校驗方案一樣。
【注意】基於該方案發起網絡請求,若報出SSL校驗錯誤
,比如 iOS 係統報錯kCFStreamErrorDomainSSL, -9813; The certificate for this server is invalid
,Android係統報錯System.err: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
,請檢查應用場景是否為SNI(單IP多HTTPS域名)。
下麵分別列出 iOS 平台的示例代碼。
iOS示例
此示例針對NSURLSession/NSURLConnection接口。
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain
{
/*
* 創建證書校驗策略
*/
NSMutableArray *policies = [NSMutableArray array];
if (domain) {
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
/*
* 綁定校驗策略到服務端的證書上
*/
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
/*
* 評估當前serverTrust是否可信任,
* 官方建議在result = kSecTrustResultUnspecified 或 kSecTrustResultProceed
* 的情況下serverTrust可以被驗證通過,https://developer.apple.com/library/ios/technotes/tn2232/_index.html
* 關於SecTrustResultType的詳細信息請參考SecTrust.h
*/
SecTrustResultType result;
SecTrustEvaluate(serverTrust, &result);
return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
}
/*
* NSURLConnection
*/
- (void)connection:(NSURLConnection *)connection
willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
if (!challenge) {
return;
}
/*
* URL裏麵的host在使用“IP直連”的情況下被設置成了IP,此處從HTTP Header中獲取真實域名
*/
NSString* host = [[self.request allHTTPHeaderFields] objectForKey:@"host"];
if (!host) {
host = self.request.URL.host;
}
/*
* 判斷challenge的身份驗證方法是否是NSURLAuthenticationMethodServerTrust(HTTPS模式下會進行該身份驗證流程),
* 在沒有配置身份驗證方法的情況下進行默認的網絡請求流程。
*/
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
{
if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
/*
* 驗證完以後,需要構造一個NSURLCredential發送給發起方
*/
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
} else {
/*
* 驗證失敗,進入默認處理流程
*/
[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
}
} else {
/*
* 對於其他驗證方法直接進行處理流程
*/
[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
}
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
/*
* NSURLSession
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler
{
if (!challenge) {
return;
}
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
NSURLCredential *credential = nil;
/*
* 獲取原始域名信息。
*/
NSString* host = [[self.request allHTTPHeaderFields] objectForKey:@"host"];
if (!host) {
host = self.request.URL.host;
}
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
// 對於其他的challenges直接使用默認的驗證方案
completionHandler(disposition,credential);
}
需要修改HOST的場景總結
那麼什麼時候需要修改Host?
答案是所有情況都需要設置 HOST:做網絡請求時,采用 IP 直連的方案會遇到 HOST 字段被改為 IP 的問題,所以都需要手動地配置 HOST 字段。
場景 | HTTP | HTTPS(非SNI) | HTTPS(SNI) |
---|---|---|---|
如何設置 | 改Host | 改Host,在移動端我們自己校驗,直接返回YES | 改HOST,而且需要做SNI適配。 |
雖然 IP 直接連的方案,導致的結果是 HOST 字段被改為了IP,所以需要手動修改HOST。但是服務端唯一的根據是SNI字段。下麵就介紹下針對 SNI 場景的方案:
3. HTTPS(SNI)場景方案
3.1 iOS SNI場景
SNI(單IP多HTTPS證書)場景下,iOS上層網絡庫NSURLConnection/NSURLSession
沒有提供接口進行SNI字段
的配置,因此需要Socket層級的底層網絡庫例如CFNetwork
,來實現IP直連網絡請求
適配方案。而基於CFNetwork的解決方案需要開發者考慮數據的收發、重定向、解碼、緩存等問題(CFNetwork是非常底層的網絡實現),希望開發者合理評估該場景的使用風險。
可參考:
具體的實現方案可以參考: 《防 DNS 汙染方案調研----- SNI 場景》
最後更新:2017-09-18 21:33:48