引言
在移動應用安全領域,HTTPS/TLS 是數據傳輸的第一道防線,但僅依賴系統默認的證書驗證仍有被中間人(MITM)攻擊的風險。Certificate Pinning(證書固定)通過將客戶端信任“釘”在指定的服務器證書或公鑰上,徹底杜絕了偽造證書帶來的隱患,是面向金融、醫療、支付等高安全場景的必備防護手段。
為什么要做證書固定
- 防范中間人攻擊(MITM)
攻擊者可能在公共網絡或被劫持的路由中插入自簽或盜取的證書,繞過系統驗證,讓用戶數據泄露或被篡改。 - 抵御企業/校園 HTTPS 代理
有些網絡環境部署了 HTTPS 解密代理,雖然系統層面信任了其根證書,但應用層可通過固定真實服務器證書拒絕代理中轉。 - 滿足合規審計
金融、醫療等行業對通信安全有嚴格審計要求,證書固定可證明客戶端僅信任預定義的證書或公鑰。
核心原理與驗證流程
-
預嵌入證書或公鑰
- 將服務器證書(
.cer
)或對應公鑰哈希(Base64)打包進 App Bundle。
- 將服務器證書(
-
攔截 TLS 驗證回調
- 在
URLSessionDelegate
的didReceiveChallenge
中,先調用系統的SecTrustEvaluate
完成基礎驗證,再進行自定義校驗。
- 在
-
提取公鑰并計算哈希
- 從服務器返回的證書中提取公鑰數據,對其執行 SHA-256 運算,生成 Base64 編碼的哈希值。
-
哈希比對并決策
-
將計算所得哈希與預置的“釘扎”值對比:
- 匹配 → 繼續通信(
.useCredential(trust:)
) - 不匹配 → 拒絕連接(
.cancelAuthenticationChallenge
)
- 匹配 → 繼續通信(
-
實現示例
1. 原生 NSURLSession 公鑰固定
class PinnedDelegate: NSObject, URLSessionDelegate {// 本地存儲的公鑰哈希(Base64 編碼)private let pinnedHash = "Base64EncodedPublicKeyHashHere"func urlSession(_ session: URLSession,didReceive challenge: URLAuthenticationChallenge,completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {// 1. 檢查是否為 ServerTrust 驗證guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,let trust = challenge.protectionSpace.serverTrust else {completionHandler(.performDefaultHandling, nil)return}// 2. 系統默認驗證var result = SecTrustResultType.invalidguard SecTrustEvaluate(trust, &result) == errSecSuccess,(result == .unspecified || result == .proceed) else {completionHandler(.cancelAuthenticationChallenge, nil)return}// 3. 提取服務器證書并獲取公鑰數據guard let cert = SecTrustGetCertificateAtIndex(trust, 0),let pubKey = SecCertificateCopyKey(cert),let pubData = SecKeyCopyExternalRepresentation(pubKey, nil) as Data? else {completionHandler(.cancelAuthenticationChallenge, nil)return}// 4. 計算 SHA-256 哈希let hash = sha256(pubData).base64EncodedString()// 5. 與預置哈希對比if hash == pinnedHash {completionHandler(.useCredential, URLCredential(trust: trust))} else {completionHandler(.cancelAuthenticationChallenge, nil)print("🚨 公鑰哈希不匹配,證書固定校驗失敗")}}private func sha256(_ data: Data) -> Data {var buf = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))data.withUnsafeBytes { _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &buf) }return Data(buf)}
}// 使用方式
let session = URLSession(configuration: .default,delegate: PinnedDelegate(),delegateQueue: nil)
session.dataTask(with: URL(string: "https://api.yourdomain.com/data")!).resume()
2. Alamofire 整證書固定
import Alamofire// 1. 從 Bundle 加載本地 .cer
let url = Bundle.main.url(forResource: "server", withExtension: "cer")!
let cert = SecCertificateCreateWithData(nil, try! Data(contentsOf: url) as CFData)!// 2. 配置 ServerTrustManager
let evaluators: [String: ServerTrustEvaluating] = ["api.yourdomain.com":PinnedCertificatesTrustEvaluator(certificates: [cert],acceptSelfSignedCertificates: false,performDefaultValidation: true,validateHost: true)
]let manager = ServerTrustManager(evaluators: evaluators)
let session = Session(serverTrustManager: manager)// 3. 發起請求
session.request("https://api.yourdomain.com/data").validate().responseJSON { response inswitch response.result {case .success:print("?? 證書固定驗證通過")case .failure(let error):print("? 驗證失敗:\(error)")}}
3. AFNetworking 公鑰固定
#import "AFNetworking.h"// 1. 確保將 server.cer 放入 App Bundle
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.requestSerializer = [AFJSONRequestSerializer serializer];
manager.responseSerializer = [AFJSONResponseSerializer serializer];// 2. 配置公鑰固定(Public Key Pinning)
AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
// 不允許無效證書
policy.allowInvalidCertificates = NO;
// 必須校驗域名
policy.validatesDomainName = YES;
manager.securityPolicy = policy;// 3. 發起 GET 請求
[manager GET:@"https://api.yourdomain.com/data"parameters:nilheaders:nilprogress:nilsuccess:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {NSLog(@"?? 請求成功并通過公鑰固定校驗:%@", responseObject);} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {NSLog(@"? 請求失敗或證書固定校驗失敗:%@", error);}];
實踐建議
- 優先使用公鑰固定:對證書更新更具兼容性。
- 結合遠程配置動態更新:如 Firebase Remote Config,下發最新哈希,降低 App 發布頻率。
- 限定固定規則數量:僅對核心域名或關鍵 CA 進行固定,避免過度復雜。
- 完整測試與監控:在預發布環境模擬證書換新,確保校驗邏輯可用,并對失敗情況設置告警。
結語
本文從“為什么要做證書固定”到“核心驗證流程”,再到三種主流網絡框架(原生 NSURLSession
、Alamofire、AFNetworking)的實戰示例,幫助初學者系統掌握 iOS 證書固定的落地方案。做好 Certificate Pinning,為你的應用網絡通信再添一道牢不可破的安全防線。
擴展閱讀:
- 蘋果官方文檔:Networking and Security → Secure Connections
- OWASP Mobile Top 10 → M3: Insufficient Cryptography