TL;DR

前一陣子 AFNetworking 被爆出存在安全性漏洞,它們也針對這件事情發出聲明稿

簡單的說,就是建議開發者使用最新版的 AFNetworking,並且啟用安全連線。不過它們也承認這一部份的說明文件沒有寫得很齊全,所以困擾了不少開發者。

今天花了一點時間研究,順手把它記錄下來。安全相關的東西不是我的專長,所以如果有任何錯誤的地方,請留言告訴我。

取得安全憑證

1. 確認有使用安全連線

如果你跟遠端伺服器是透過 HTTP 連線,那就不是安全連線,如果是 HTTPS 那就是安全連線。

2. 準備好網站的安全憑證

接下來我們需要憑證檔(Certification file),它的副檔名是 .cer,你可以跟你們的網站管理員詢問,通常他們都知道怎麼拿到這個檔案。

如果你的網站管理員沒有 .cer 檔,只有 .crt 檔,那你可以透過以下這行指令轉檔,要注意的是它是採用 DER 編碼格式(請自行將 myWebsite 替換成你想要的名字):

openssl x509 -in myWebsite.crt -out myWebsite.cer -outform der

如果很不幸的,你的網站管理員連 .crt 檔都沒有,那你也可以使用下列這一整行指令從你們的網站取得憑證(請自行將 www.mywebsite.com 替換成你們的網址):

openssl s_client -connect www.mywebsite.com:443 </dev/null 2>/dev/null | openssl x509 -outform DER > myWebsite.cer

現在你有一個憑證檔了。

3. 將憑證加入你的專案

將你的憑證拖拉放到 Xcode 專案底下,記得要把 Copy items if neededAdd to targets 打勾。

好了,事前準備都做完,接著我們來設定 AFNetworking。

設定 AFNetworking

1. Pinning Mode

AFNetworking 的安全相關設定放在 AFSecurityPolicy,它定義了三種 SSL Pinning Mode:

/*
 `AFSSLPinningModeNone`
 Do not used pinned certificates to validate servers.

 `AFSSLPinningModePublicKey`
 Validate host certificates against public keys of pinned certificates.

 `AFSSLPinningModeCertificate`
 Validate host certificates against pinned certificates.
*/
typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
    AFSSLPinningModeNone,
    AFSSLPinningModePublicKey,
    AFSSLPinningModeCertificate,
};

關於 pinning mode 詳細的說明可以參考這篇文章,簡單的說就是你可以將憑證跟你的 APP 一起打包,藉由此機制來避免中間人偽造憑證的風險。

  • AFSSLPinningModeNone : 你不必將憑證跟你的 APP 一起打包,完全信任伺服器的憑證
  • AFSSLPinningModeCertificate : 比對伺服器憑證跟你的憑證是否完全匹配
  • AFSSLPinningModePublicKey : 只比對伺服器憑證的 public key 跟你的憑證的 public key 是否匹配

那要選用何種模式比較好呢?

AFSSLPinningModeCertificate 比較安全但也比較麻煩,它會比對你打包的憑證跟伺服器的憑證是否一致。因為你的憑證是跟 APP 一起打包的,這也就代表說如果你的憑證過期了或是變動了,你就得出一版新的 APP 而且舊版 APP 的憑證就失效了。你也可以在每次 APP 啟動時,就自動連到某個伺服器下載最新的憑證,不過此時這個下載連線就會是有風險的。

AFSSLPinningModePublicKey 則是只有比對憑證裡的 public key,所以即使伺服器憑證有所變動,只要 public key 不變,就能通過驗證。

所以如果你能確保每個使用者總是使用最新版本的 APP(例如是公司企業內部專用的),那就可以考慮 AFSSLPinningModeCertificate,否則的話選擇 AFSSLPinningModePublicKey 是比較實際的作法。

2. Certification Chain

/**
 Whether to evaluate an entire SSL certificate chain, or just the leaf certificate. Defaults to `YES`.
 */
@property (nonatomic, assign) BOOL validatesCertificateChain;

你的憑證是某家機構發出的,該機構的憑證是由更高一級的機構發出的,一路往上追,最後會到一個根機構,這樣一串由各機構發出的憑證稱為 certification chain。

如果你把 validatesCertificateChain 設為 YES,那就得把這一整串憑證都打包進你的 APP,必須每個驗證都通過才算通過。如果設為 NO,只需要打包你自己的憑證就夠了。

Update: validatesCertificateChain 這個選項已經在 AFNetworking v2.6.0 拿掉了。

3. 如何使用 AFSecurityPolicy

這裡以最新版的 AFNetworking 為例,假設你有一個 APIManager 處理所有的 API call,它繼承自 AFHTTPSessionManager,我們可以設定它的 security policy 如下:

@interface APIManager : AFHTTPSessionManager
+ (APIManager *)sharedInstance;
@end

@implementation APIManager
+ (APIManager *)sharedInstance {
  static APIManager *_sharedClient = nil;
  static dispatch_once_t onceToken;

  dispatch_once(&onceToken, ^{
    NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
    _sharedClient = [[APIManager alloc] initWithBaseURL:nil sessionConfiguration:sessionConfiguration];
    AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
     policy.validatesCertificateChain = NO; // v2.6.0 之後沒有這個選項了
    _sharedClient.securityPolicy = policy;
  });

  return _sharedClient;
}
@end

好了,到此大功告成,你已經正確設定好安全連線了!

錯誤排解

Q: 為什麼我可以連上其他的網址?我不是應該只能連上憑證綁定的網址嗎?

  1. 檢查 validatesDomainName 是否設為 NO 了,是的話就將它改成 YES。
  2. 檢查是否連到 http 開頭的網址,非安全連線是不受限制的。

Q: 為什麼有打包憑證了,還是會連線失敗?

  1. 確認你的憑證有加到你的 target 裡頭,拖拉到 Xcode 時要把 Copy items if neededAdd to targets 打勾。
  2. 如果 validatesCertificateChain 是 YES,記得把它改成 NO,或是把上級憑證也一同打包進 APP。(v2.6.0 之後沒有這個選項了)

參考文件

https://github.com/AFNetworking/AFNetworking/issues/2673
https://github.com/rnapier/RNPinnedCertValidator
http://stackoverflow.com/a/24625969
http://oncenote.com/2014/10/21/Security-1-HTTPS/