# NSURLErrorDomain Code=-1202：iOS 提示“此服务器的证书无效”

> NSURLErrorDomain -1202 表示 iOS/macOS 应用不信任服务器证书：根证书不受信任、域名或 IP 不匹配、缺中间证书或不符合苹果证书规则。附排查命令。

Source: https://ssl.sh/zh/errors/ios-nsurlerrordomain-1202/
Last updated: 2026-11-10

**NSURLErrorDomain Code=-1202 的意思是：苹果的网络库不信任服务器出示的证书。** 对应常量是 `NSURLErrorServerCertificateUntrusted`（Swift 里是 `URLError.Code.serverCertificateUntrusted`）。苹果文档只提到“根证书不受信任”，但大多数信任失败都报这个码：内部 CA 或自签名证书、域名不匹配、证书链不完整、证书不符合苹果的规则。

## iOS 信任一台服务器之前查什么

苹果文档列出了默认服务器信任校验的内容：证书签名完好、没有过期、名称和服务器域名一致、能通过有效的证书一级级链到受信任的锚证书；锚证书要么随系统自带，要么由用户或管理员安装到设备上。好几种不同的失败最后都表现为同一个 -1202，所以光看这个码分不出是哪一项没过，需要按下面的步骤缩小范围。

## -1202 和其他 TLS 错误码的区别

-1202 专指证书信任校验失败；握手本身失败报 -1200，证书日期问题报 -1201。中文系统里，-1202 的日志通常以“此服务器的证书无效。您可能正在连接到一个伪装成……的服务器”开头。

| 错误码 | Swift 名称 | 含义 |
|---|---|---|
| -1200 | `secureConnectionFailed` | 握手失败，见 [NSURLErrorDomain -1200](https://ssl.sh/zh/errors/nsurlerrordomain-1200/) |
| -1201 | `serverCertificateHasBadDate` | 证书过期或尚未生效 |
| **-1202** | `serverCertificateUntrusted` | 信任校验失败 |
| -1203 | `serverCertificateHasUnknownRoot` | 没有由任何根证书签发 |
| -1204 | `serverCertificateNotYetValid` | 证书尚未生效 |
| -1206 | `clientCertificateRequired` | 服务器要求客户端证书 |

Safari 里同样的情况显示为 [此连接非私人连接](https://ssl.sh/zh/errors/safari-this-connection-is-not-private/)。

## 看日志里的底层 SSL 错误码

日志里的 `kCFStreamErrorDomainSSL` 错误码比 -1202 具体，能直接指出是缺中间证书、根证书不受信任还是主机名不匹配。URLSession 的日志常常还有一行 `NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813)`。后面这个数字来自苹果的 Security 框架，比 -1202 具体得多：

| 错误码 | 常量 | 含义 |
|---|---|---|
| -9807 | `errSSLXCertChainInvalid` | 证书链无效 |
| -9812 | `errSSLUnknownRootCert` | 链是完整的，但根证书不受信任 |
| -9813 | `errSSLNoRootCert` | 链没有被根证书验证，常见于缺中间证书或内部 CA |
| -9814 | `errSSLCertExpired` | 链里有证书过期 |
| -9843 | `errSSLHostNameMismatch` | 主机名和证书不匹配 |

## 常见原因（按出现频率排序）

最常见的原因是开发环境或内部服务器用了内部 CA，设备上没有它的根证书。

### 1. 开发环境或内部服务器用了内部 CA

测试环境 API、私有化部署的服务器、本机调试服务，经常用自签名证书或公司内部 CA，iOS 的信任库里都没有。看签发者：

```bash
openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer
```

### 2. 域名或 IP 不在证书里

应用请求的是 `https://203.0.113.10`，或者某个没写进 SAN 的别名。从 iOS 13 起，写在 CN 里的域名完全不被信任，只写在 CN 里的证书同样会失败。国内很多应用接入了 HTTPDNS，把域名换成 IP 直连，日志里就会出现 -9843 和“伪装成 IP 地址的服务器”这样的提示；阿里云的文档给出的处理办法是在证书校验时改用原始域名。查看证书里的域名：

```bash
openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -ext subjectAltName
```

### 3. 证书链不完整

服务器只发了自己的证书。有些客户端能自己补上，不要指望。用 `-showcerts` 数证书数量，或者用 [证书链工具](https://ssl.sh/zh/tools/chain-builder/) 检查，详见 [缺少中间证书](https://ssl.sh/zh/errors/missing-intermediate-certificate/)。从阿里云、腾讯云下载证书时，Nginx 格式的文件里通常已经含有中间证书，部署时不要只保留第一段。

### 4. 证书不符合苹果的规则

苹果要求 iOS 13、macOS 10.15 及以上系统中的所有 TLS 服务器证书：RSA 密钥不少于 2048 位，签名使用 SHA-2，域名必须写在 SAN 里。2019 年 7 月 1 日之后签发的证书还必须带 `id-kp-serverAuth` 扩展用途，有效期不超过 825 天。2020 年 9 月 1 日及以后由系统预装根签发的公共证书，有效期不得超过 398 天。很多内部 CA 习惯签五年期、不带扩展用途的证书，就会在这里栽跟头。用 [证书解析工具](https://ssl.sh/zh/tools/certificate-decoder/) 逐项检查。

### 5. 抓包代理或网络拦截

Charles、Proxyman、mitmproxy、公司代理、需要网页认证的公共 Wi-Fi，都会出示自己的证书。它们的根证书没有装到设备上并打开完全信任，应用就会报 -1202。

## 在 Mac 上排查

```bash
nscurl --ats-diagnostics --verbose https://api.example.com/
```

`nscurl` 用的是苹果自己的网络库，它报信任失败，基本就复现了应用遇到的情况。再用 [SSL 检测](https://ssl.sh/zh/tools/ssl-checker/) 从外部对比一下。

## 怎么修

公网服务器换成合规的公共证书并发送完整链；内部服务器按苹果规则签发证书，再把根证书下发到设备。应用代码保持默认的信任校验。

**公网服务器：** 用公共 CA 的证书，把应用会访问的每个域名都写进 SAN，并发送完整证书链（Nginx 用 `fullchain.pem`）。

**内部或开发服务器（测试设备上）：**
1. 按苹果规则签发证书：SAN 写域名、带 serverAuth 扩展用途、SHA-256、有效期不超过 825 天。
2. 通过描述文件或 MDM 把根证书装到设备上。
3. 手动安装的描述文件，到“设置 > 通用 > 关于本机 > 证书信任设置”里给这个根证书打开完全信任。通过 MDM 或 Apple Configurator 部署的会自动信任。

**应用代码：** 保持默认的信任校验。苹果关于手动校验服务器信任的文档演示了如何在 `urlSession(_:didReceive:completionHandler:)` 里检查服务器信任，但关于不安全连接的文档也说明，ATS 启用时只能收紧校验（比如锁定公钥），不能放松。生产环境要用内部 CA，就把根证书下发到受管设备，而不是在代码里降低校验。无论如何不要在发布版本里“接受所有证书”，临时的也不行。

## 怎么确认已经修好

修好的标准是 `openssl` 验证返回 0、`nscurl` 诊断全部 PASS，并且真机和模拟器都能正常请求。

1. 公共证书：`openssl s_client -connect api.example.com:443 -servername api.example.com -verify_return_error` 最后显示 `Verify return code: 0 (ok)`。
2. `nscurl --ats-diagnostics https://api.example.com/` 在默认配置下显示 PASS。
3. 真机和模拟器都跑一遍请求，它们的信任库不一样。
