You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 04:44:53