# The remote certificate is invalid（.NET）原因与修复

> .NET 的 SslStream 拒绝服务器证书时抛出这个异常。先看 SslPolicyErrors 是哪一项，再修证书链、域名或信任库，不要在回调里直接返回 true。

Source: https://ssl.sh/zh/errors/dotnet-remote-certificate-is-invalid/
Last updated: 2026-11-10

**`The remote certificate is invalid according to the validation procedure` 的意思是：.NET 的 `SslStream` 在 TLS 握手时拒绝了服务器证书。** `HttpClient` 通常把它包在 `HttpRequestException: The SSL connection could not be established, see inner exception` 里面。具体原因在 `SslPolicyErrors` 标志里：证书链不受信任或不完整、域名不匹配，或者没拿到证书。

较新的 .NET 版本常常会把原因直接附在消息后面，比如 `...: RemoteCertificateNameMismatch, RemoteCertificateChainErrors`，或 `The remote certificate is invalid because of errors in the certificate chain: UntrustedRoot`。

## 快速修复

修法取决于 `SslPolicyErrors` 里是哪个标志。先从一台干净的机器检查服务器：

```bash
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null
```

再按下表找到对应修法：

| SslPolicyErrors / 链状态 | 含义 | 修法 |
|---|---|---|
| `RemoteCertificateChainErrors` + `PartialChain` | 服务器没发中间证书 | 服务器发送完整证书链 |
| `RemoteCertificateChainErrors` + `UntrustedRoot` | 本机不信任根证书 | 安装私有根或代理根证书 |
| `RemoteCertificateChainErrors` + `NotTimeValid` | 证书过期或尚未生效 | 续期；检查客户端时间 |
| `RemoteCertificateNameMismatch` | 访问的主机不在证书 SAN 里 | 换成正确的域名或重签证书 |
| `RemoteCertificateNotAvailable` | 服务器没出示证书 | 检查服务器 TLS 配置 |

公网域名可以直接用 [SSL 检测工具](https://ssl.sh/zh/tools/ssl-checker/)一次看清证书链、域名和有效期。

## 原因

这个异常有 4 类原因，最常见的是根证书不受信任，私有 CA、自签名证书和企业代理都属于这一类。

### 1. 根证书不受信任（私有 CA、自签名、企业代理）

内部接口、解密 HTTPS 的企业代理、本地开发服务器用的根证书都不在公共证书库里。

- **确认：** 链状态是 `UntrustedRoot`，最上层签发者是公司名或代理设备。
- **修复：** 把根证书装进系统信任库（见下文）。如果只想让某一个应用信任它，就显式按这张根证书校验：

```csharp
var companyRoot = X509Certificate2.CreateFromPemFile("company-root.pem");
var handler = new SocketsHttpHandler();
handler.SslOptions.RemoteCertificateValidationCallback = (sender, cert, chain, errors) =>
{
    if (errors == SslPolicyErrors.None) return true;
    if (errors != SslPolicyErrors.RemoteCertificateChainErrors || cert is not X509Certificate2 leaf)
        return false;                       // 域名不匹配绝不放行
    using var custom = new X509Chain();
    custom.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
    custom.ChainPolicy.CustomTrustStore.Add(companyRoot);
    return custom.Build(leaf);
};
var client = new HttpClient(handler);
```

这样仍然校验域名，只额外信任一张指定的根证书。

### 2. 服务器证书链不完整

- **确认：** 链状态是 `PartialChain`；`openssl s_client` 只显示一张证书。
- **修复：** 在服务器或负载均衡上配置站点证书加中间证书。IIS 要把中间证书导入计算机账户的“中级证书颁发机构”。详见[缺少中间证书](https://ssl.sh/zh/errors/missing-intermediate-certificate/)。

### 3. 域名不匹配

- **确认：** `RemoteCertificateNameMismatch`；你用 IP、内部别名或证书没覆盖的主机名访问。
- **修复：** 改用证书 [SAN](https://ssl.sh/zh/glossary/san/) 里列出的名字，或重新签发。

### 4. 证书过期或系统时间不对

- **确认：** `NotTimeValid`。同时检查服务器证书到期日和客户端时间，从快照恢复的虚拟机时间经常不准。

## 各环境修法

.NET 在 Windows 上读 Windows 证书库，在 Linux 上读 OpenSSL 的 CA 证书包，在 macOS 上读系统钥匙串。私有根证书要装进当前平台对应的位置。

**Windows（.NET Framework 和 .NET）：** 把根证书导入“本地计算机”的“受信任的根证书颁发机构”（`certlm.msc`），或用 PowerShell：

```powershell
Import-Certificate -FilePath .\company-root.cer -CertStoreLocation Cert:\LocalMachine\Root
```

**Linux 和容器：** .NET 读 OpenSSL 的信任库。

```dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:8.0
COPY company-root.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates
```

Alpine 版镜像先用 `apk` 安装 `ca-certificates`。国内拉取 mcr.microsoft.com 镜像较慢时，可以在公司的镜像仓库里做一份缓存，但同样要在构建时加入根证书。

**macOS：** 把根证书加入“系统”钥匙串并设为“始终信任”。

**ASP.NET Core 本地开发：** 在 Windows 和 macOS 上执行 `dotnet dev-certs https --trust` 信任开发证书。

**.NET Framework 4.x：** 如果内层消息说连接被意外关闭，可能是程序只提供了老旧的 TLS 版本，那是协议问题，不是证书校验失败，见 [The SSL connection could not be established](https://ssl.sh/zh/errors/dotnet-ssl-connection-could-not-be-established/)。

## 不要这样做

下面 3 种写法都能让异常消失，但会让任何人都能冒充你的服务器。

- **在 .NET Framework 里写 `ServicePointManager.ServerCertificateValidationCallback = (s, c, ch, e) => true;`。** 这会关闭整个进程所有连接的证书校验。
- **在隔离测试之外使用 `HttpClientHandler.DangerousAcceptAnyServerCertificateValidator`。**
- **在回调里忽略 `RemoteCertificateNameMismatch`。** 信任一张根证书是合理的，接受任意域名则不然：那张根签发的任何证书都能冒充你的服务器。
