服务器信任质询

NSURLSession 与 NSURLAuthenticationMethodServerTrust ------ 从零开始的"服务器信任质询"全流程

目标读者:刚接触 iOS 网络开发、准备理解 HTTPS 与证书校验细节的同学

出发点:搞清楚为什么会有"质询"、质询的触发时机、以及在 delegate 里怎么正确地处理它


1. 质询到底是什么?

URLSession 发现需要某种额外凭据(credential)才能继续网络交互时,会暂停请求并向你抛出 authentication challenge 。对 HTTPS 来说,最常见的触发类型就是 NSURLAuthenticationMethodServerTrust

  1. 服务器把 X.509 证书链塞进 TLS 握手。

  2. 客户端(iOS TLS 实现 + ATS 默认策略)检查:

    • 证书是否在有效期、是否被吊销;
    • 证书链是否能追溯到系统或配置的受信根 CA;
    • 证书的 CN/SAN 是否与请求的 host 完全匹配。
  3. 如果 全部 检查都能自动通过,URLSession 不会打扰你------直接走默认证书校验并继续请求。

  4. 只要 你实现了 session-level delegate 方法
    urlSession(_:didReceive:completionHandler:),系统就会把步骤 2 的工作"交卷"给你------即使校验本来能自动通过。

🚩 所以:不实现该 delegate == 自动信任系统 CA + ATS 默认策略;实现 delegate == 你必须亲自裁定是否信任。


2. 质询出现的典型场景

场景 为什么会收到质询? 你通常怎么做?
生产环境,使用合法证书(Let's Encrypt、GlobalSign...) 你自己实现了 delegate,但只是想保留系统默认验证 再次调用 SecTrustEvaluateWithError,通过则 .useCredential
内网/测试环境 使用自签名证书 系统根证书链里找不到颁发者 把自签根证书预装到 App Bundle 并做自定义信任
SSL Pinning(证书/公钥固定) 你想缩短信任链,拒绝被"合法"但非预期的 CA 篡改 手动比对二进制证书或公钥哈希,然后再决定是否信任
使用 HTTP 抓包工具 (Charles、mitmproxy) 代理伪造服务器证书,除非你安装其证书为根 CA 开发调试时允许 Charles 证书;上线包一定要拒绝

3. 基础实现(Swift 5+)

swift 复制代码
/// 在创建 URLSession 时指定 delegate,而不是用 URLSession.shared
let session = URLSession(configuration: .default,
                         delegate: self,
                         delegateQueue: nil)

extension YourNetworkManager: URLSessionDelegate {

    /// 系统对"服务器信任"发起的质询都会走到这里
    func urlSession(_ session: URLSession,
                    didReceive challenge: URLAuthenticationChallenge,
                    completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

        guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
              let serverTrust = challenge.protectionSpace.serverTrust else {
            // 交给系统默认处理(例如 HTTP Basic、客户端证书等)
            completionHandler(.performDefaultHandling, nil)
            return
        }

        // 1️⃣ 让系统再跑一次标准评估
        if SecTrustEvaluateWithError(serverTrust, nil) {
            let credential = URLCredential(trust: serverTrust)
            completionHandler(.useCredential, credential)   // 继续请求
        } else {
            completionHandler(.cancelAuthenticationChallenge, nil) // 终止
        }
    }
}

iOS 13- 及更早版本用 SecTrustEvaluate;iOS 13+ 强烈建议改用 SecTrustEvaluateWithError 以拿到 CFError 信息并避免阻塞 main thread。

Objective-C 版本(简化)

objc 复制代码
- (void)URLSession:(NSURLSession *)session
        didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
          completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {

    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        SecTrustRef trust = challenge.protectionSpace.serverTrust;
        if (SecTrustEvaluateWithError(trust, NULL)) {
            NSURLCredential *cred = [NSURLCredential credentialForTrust:trust];
            completionHandler(NSURLSessionAuthChallengeUseCredential, cred);
        } else {
            completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
        }
        return;
    }
    completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}

4. 深入:自签名证书与 SSL Pinning

4.1 只信任 Bundle 中的根证书

swift 复制代码
if let certPath = Bundle.main.path(forResource: "myRootCA", ofType: "cer"),
   let certData = try? Data(contentsOf: URL(fileURLWithPath: certPath)),
   let rootCert = SecCertificateCreateWithData(nil, certData as CFData) {

    SecTrustSetAnchorCertificates(serverTrust, [rootCert] as CFArray)
    SecTrustSetAnchorCertificatesOnly(serverTrust, true)

    if SecTrustEvaluateWithError(serverTrust, nil) {
        completionHandler(.useCredential, URLCredential(trust: serverTrust))
    } else {
        completionHandler(.cancelAuthenticationChallenge, nil)
    }
}

SecTrustSetAnchorCertificatesOnly=true 的效果是"把系统根 CA 全部踢出,仅信任我给定的这一束证书"。

4.2 公钥 Pinning(效率更高,证书续期更灵活)

swift 复制代码
guard let serverTrust = challenge.protectionSpace.serverTrust else { ... }
guard SecTrustEvaluateWithError(serverTrust, nil) else { ... }

let serverPublicKey = SecTrustCopyKey(serverTrust)!
let serverKeyData   = SecKeyCopyExternalRepresentation(serverPublicKey, nil)! as Data
let serverKeyHash   = SHA256(serverKeyData) // 自己写或 CryptoKit

if pinnedHashes.contains(serverKeyHash) {
    completionHandler(.useCredential, URLCredential(trust: serverTrust))
} else {
    completionHandler(.cancelAuthenticationChallenge, nil)
}

5. 典型错误排查清单

现象 根因 快速定位
Code -999 "已取消" 你在 delegate 里返回了 .cancelAuthenticationChallenge.rejectProtectionSpace 打断点检查 challenge.protectionSpace
Code -1200 "SSL error" 证书链无效 / ATS 阻止弱加密 观察 Console,中会打印 ATS policy requires...
Charles 无法抓包 ATS 拒绝了 Charles 证书;或你启用了 Pinning 临时将 NSExceptionDomains 加入 Info.plist,或关闭 Pinning
偶发 ServerTrust 失败 服务器有多个证书链、SNI/host 不一致 手动访问 https://hostopenssl s_client -servername host -connect ip:443

6. 最佳实践速览

  • 不做弱校验 。切勿直接 .useCredential 而不跑 SecTrustEvaluateWithError,那等同于"信任一切",上线会被审核拒绝。
  • Pin 公钥而非整张证书,减少因证书续期频繁发版。
  • 按需配置 ATS。绝大多数生产 HTTPS 服务都可以满足 ATS 默认要求:TLS 1.2+、至少 RSA 2048 或 ECC 256 、SHA-256 签名。
  • 调试与上线严格隔离 。把抓包例外、测试根证书全部写在 #if DEBUG ... #endif 分支中。

结语

NSURLAuthenticationMethodServerTrust 看似只是"系统多问一句,你到底信不信任这台服务器?",但背后承载的是 PKITLS 乃至你 App 用户的数据安全。真正的安全措施都在"默认正确"与"最小权限"

默认让系统校验一切,只有当你非常确定要改时才介入,并且介入后要保证比系统 更严格 而不是更松。

掌握这些基础,你就能轻松向自签环境、抓包调试甚至 SSL Pinning 过渡,也能对任何"为什么连接被取消?"作出快速诊断。愿你写出的每一行网络代码都能经得起安全审计与真实攻击的考验。

相关推荐
AI逐月20 分钟前
tmux 常用命令总结:从入门到稳定使用的一篇实战博客
linux·服务器·ssh·php
想逃离铁厂的老铁22 分钟前
Day55 >> 并查集理论基础 + 107、寻找存在的路线
java·服务器
小白跃升坊1 小时前
基于1Panel的AI运维
linux·运维·人工智能·ai大模型·教学·ai agent
杨江1 小时前
seafile docker安装说明
运维
舰长1151 小时前
linux 实现文件共享的实现方式比较
linux·服务器·网络
好好沉淀1 小时前
Docker开发笔记(详解)
运维·docker·容器
zmjjdank1ng1 小时前
Linux 输出重定向
linux·运维
路由侠内网穿透.1 小时前
本地部署智能家居集成解决方案 ESPHome 并实现外部访问( Linux 版本)
linux·运维·服务器·网络协议·智能家居
树℡独1 小时前
ns-3仿真之应用层(三)
运维·服务器·ns3
VekiSon2 小时前
Linux内核驱动——基础概念与开发环境搭建
linux·运维·服务器·c语言·arm开发