iOS Swift中无第三方库实现动态SSL Pinning的方法咨询
动态SSL Pinning实现方案(iOS Swift)
核心疑问解答:证书续期后公钥是否变化?
- 正常续期场景下,如果复用原有私钥向CA申请新证书,公钥不会改变——因为公钥与私钥是成对生成的,私钥不变则公钥必然一致。
- 只有当续期时重新生成了新的密钥对(比如私钥丢失或主动更换),公钥才会发生变化。正规的证书续期流程大多会复用私钥,因此公钥通常保持稳定。
无需第三方库实现动态SSL Pinning的方法
iOS中可通过自定义URLSessionDelegate的证书验证逻辑实现,核心思路是动态维护可信公钥列表(比如从后端接口拉取最新公钥),在握手阶段对比服务器证书的公钥是否在可信列表内。
步骤1:封装公钥提取与格式转换工具
先实现两个工具方法,用于从证书中提取公钥,并转为可存储/对比的字符串格式:
import Foundation import Security // 从SecCertificate提取公钥 func publicKey(from certificate: SecCertificate) -> SecKey? { let policy = SecPolicyCreateBasicX509() var trust: SecTrust? let status = SecTrustCreateWithCertificates(certificate, policy, &trust) guard status == errSecSuccess, let trust = trust else { return nil } return SecTrustCopyPublicKey(trust) } // 将SecKey转为Base64字符串(用于对比和存储) func publicKeyString(from key: SecKey) -> String? { guard let data = SecKeyCopyExternalRepresentation(key, nil) as Data? else { return nil } return data.base64EncodedString() }
步骤2:动态拉取可信公钥列表
在APP启动时、定时或按需从后端接口拉取最新的可信公钥列表,存储在内存或本地(比如UserDefaults):
// 示例:从后端接口拉取可信公钥列表 func fetchTrustedPublicKeys(completion: @escaping ([String]?) -> Void) { guard let url = URL(string: "https://your-api-domain.com/trusted-keys") else { completion(nil) return } let task = URLSession.shared.dataTask(with: url) { data, _, error in guard let data = data, error == nil else { completion(nil) return } // 假设后端返回JSON数组格式:["key1-base64", "key2-base64"] let keys = try? JSONDecoder().decode([String].self, from: data) completion(keys) } task.resume() }
步骤3:自定义Delegate实现证书校验
在URLSessionDelegate的didReceive challenge方法中,提取服务器证书的公钥并与可信列表对比:
class CustomSessionDelegate: NSObject, URLSessionDelegate { var trustedPublicKeys: [String] = [] func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { // 仅处理服务器证书校验逻辑 guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust else { completionHandler(.performDefaultHandling, nil) return } guard let serverTrust = challenge.protectionSpace.serverTrust, let certificates = SecTrustCopyCertificateChain(serverTrust) as? [SecCertificate], let serverCert = certificates.first else { completionHandler(.cancelAuthenticationChallenge, nil) return } // 提取服务器证书的公钥字符串 guard let serverPublicKey = publicKey(from: serverCert), let serverPublicKeyStr = publicKeyString(from: serverPublicKey) else { completionHandler(.cancelAuthenticationChallenge, nil) return } // 对比是否在可信列表内 if trustedPublicKeys.contains(serverPublicKeyStr) { let credential = URLCredential(trust: serverTrust) completionHandler(.useCredential, credential) } else { completionHandler(.cancelAuthenticationChallenge, nil) } } }
步骤4:使用自定义Delegate创建URLSession
// 初始化时先拉取可信公钥 let delegate = CustomSessionDelegate() fetchTrustedPublicKeys { keys in if let keys = keys { delegate.trustedPublicKeys = keys } } // 创建使用自定义Delegate的URLSession let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) // 发起请求示例 let task = session.dataTask(with: URL(string: "https://your-target-domain.com")!) { data, response, error in // 处理请求结果 } task.resume()
补充说明
- 动态更新策略:可在APP后台唤醒时重新拉取公钥,或在校验失败时触发一次更新重试,避免因公钥更新导致APP无法联网。
- 兜底方案:如果拉取公钥失败,可保留一份本地预置的备用公钥,作为临时兜底。
内容的提问来源于stack exchange,提问作者Ankit Kumar Gupta
相关产品推荐
相关产品推荐

