SSL Pinning:加強 App 和 Server 通訊安全的方法
什麼是 SSL Pinning?
一種加強 App 和 Server 間通訊安全性的方法。主要目標是確保 App 僅與預先驗證的 Server 建立安全連接,防止中間人攻擊(Man-in-the-Middle,MitM)等安全風險。一般會有兩種方式進行驗證,Certificate Pinning 和 Public Key Pinning
Certificate Pinning
Certificate Pinning 通常需要驗證整張 SSL 證書,因此安全性相對較高。然而,此方法較為固定,如果 Server 更新證書,App 需要定期更新並重新上架。
首先,我們可以建立一個 Certificates
結構,用於從 App 內取得憑證檔案。
struct Certificates {
static let testSSLPinning = Certificates.certificate(filename: "testSSLPinning")
private static func certificate(filename: String, type: String = "cer") -> SecCertificate {
let filePath = Bundle.main.path(forResource: filename, ofType: type)!
let data = try! Data(contentsOf: URL(fileURLWithPath: filePath))
let certificate = SecCertificateCreateWithData(nil, data as CFData)!
return certificate
}
}
建立自定義的 Session
,以下使用 Alamofire 的 ServerTrustManager 進行 SSL Pinning
allHostsMustBeEvaluated
設置為 false,表示不需要對所有 domain 進行 SSLPinningevaluators
指定對特定 domain 進行 SSLPinning
let CertificateSessionManager: Session = {
let configuration = URLSessionConfiguration.af.default
configuration.urlCache = nil
let interceptor = SPNetworkInterceptor()
// domain 移除 https://
let testSSLPinningDomain = "www.github.com"
// key 是要進行 SSLPinning 的 domain
// value 則是一個 PinnedCertificatesTrustEvaluator,將前面所定義的憑證檔進行驗證
let evaluators: [String: ServerTrustEvaluating] = [
testSSLPinningDomain: PinnedCertificatesTrustEvaluator(certificates: [Certificates.testSSLPinning]))
]
let serverTrustManager = ServerTrustManager(allHostsMustBeEvaluated: false, evaluators: evaluators)
let session = Session(configuration: configuration,
interceptor: interceptor,
serverTrustManager: serverTrustManager,
cachedResponseHandler: ResponseCacher(behavior: .doNotCache))
return session
}()
Public key Pinning
Public Key Pinning 則僅驗證公開金鑰,不需驗證整張憑證。這使得當憑證需要更新時,只需確保公鑰保持不變,無需經常更新 App。
可以利用 檢測網站 SSL 憑證安全等級 這個網站取得公鑰 SHA256

導入 TrustKit
pod 'TrustKit'
建立自定義的 SessionDelegate
,用於處理 SSL Pinning 驗證:
- 如果驗證失敗則返回 false,可能是因為不需要進行伺服器信任,或者該 domain 未進行 Pinning。在這種情況下,將執行默認的處理方式。
class CustomSessionDelegate: SessionDelegate {
override func urlSession(_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
let pinning = TrustKit.sharedInstance().pinningValidator
if pinning.handle(challenge, completionHandler: completionHandler) == false {
completionHandler(.performDefaultHandling, nil)
}
}
}
接著,建立自定義的 Session
,以下使用 TrustKit 進行 SSL Pinning
設定 Config:
kTSKSwizzleNetworkDelegates
這個參數設置是否採用替換網絡代理的方式來進行 SSL Pinning。當設置為 false 時,不會替換網絡代理,而是在 CustomSessionDelegate 中使用 TrustKit 來處理驗證。kTSKEnforcePinning
是否強制執行 SSL PinningkTSKIncludeSubdomains
是否包含子域名。當設置為 true 時,也會對子域名進行 SSL Pinning 驗證kTSKDisableDefaultReportUri
是否禁用默認的報告 URI。當設置為 true 時,如果 SSL Pinning 驗證失敗,不會自動向默認的報告 URI 發送報告kTSKPublicKeyHashes
設定公鑰 SHA256
let PublicKeySessionManager: Session = {
let interceptor = SPNetworkInterceptor()
let configuration = URLSessionConfiguration.af.default
configuration.urlCache = nil
let sessionDelegate = CustomSessionDelegate()
// domain 移除 https://
let testSSLPinningDomain = "www.github.com"
// 設定 TrustKit 的配置
let trustKitConfig = [
kTSKSwizzleNetworkDelegates: false,
kTSKPinnedDomains: [
testSSLPinningDomain: [
kTSKEnforcePinning: true,
kTSKIncludeSubdomains: true,
kTSKDisableDefaultReportUri: true,
kTSKPublicKeyHashes: [
"jSd+RbSAB3215SSioJKeyfdEFELVT/xz+Fwod2ypqtE=",
],
]
]
] as [String : Any]
TrustKit.initSharedInstance(withConfiguration: trustKitConfig)
let afSession = Session(configuration: configuration,
delegate: sessionDelegate,
interceptor: interceptor,
cachedResponseHandler: ResponseCacher(behavior: .doNotCache))
return afSession
}()
以上是 SSL Pinning 的基本原理和實現方式。選擇憑證驗證或公鑰驗證取決於需求和安全性考量。無論哪種方法,都能有效提升 App 和 Server 間的通訊安全性。