kestrel 的 ssl 行为分析

📅 2026/6/30 5:54:00
kestrel 的 ssl 行为分析
找来一台老的 centos 做客户端使用 curl 来请求到 kestrel 的接口也得到了服务器证书验证不通过的问题然后使用 openssl 客户端来获取 kestrel 和 nginx 返回的证书链做对比Copyopenssl s_client -connect xxx.com:443 -showcerts发现 nginx 返回的证书链和证书文件提供的证书链是吻合的但 kestrel 返回的证书链短了一节这个发现惊呆了我因为我潜意识里都认为在 linux 上 kestrel 会完使用用户自定义指定的证书链。老的 ssl 证书还有20天的时长这个给我有足够长的时间来分析 kestrel 的 ssl 行为了。分析 ServerCertificateChain#先提供结论kestrel 目前只能读取到 Endpoint 下配置的 pem 格式证书文件的证书链其它三种情况都有BUG。证书配置位置证书文件类型证书链读取EndpointPEM√EndpointPfx×DefaultPEM×DefaultPfx×遇到问题不要慌先 AI 分析一波AI 告诉要设置 ServerCertificateChainCopybuilder.WebHost.ConfigureKestrel(kestrel { kestrel.ConfigureHttpsDefaults(https { https.ServerCertificateChain 自己实现从证书文件读取; }); });我有点想怼 AIkestrel 不可能不读取证书文件的证书链于是我加了行 ServerCertificateChain 不为 null 的断言同时在配置文件的默认证书了放了 ssl 证书Copybuilder.WebHost.ConfigureKestrel(kestrel { kestrel.ConfigureHttpsDefaults(https { https.OnAuthenticate (context, options) { Debug.Assert(https.ServerCertificateChain is not null); }; }); });Copy{ Kestrel: { Endpoints: { Https: { Url: https://*:443 }, Certificates: { Default: { Path: certs/test_bundle.crt, KeyPath: certs/test.key } } } }结果还真炸起来了Debug.Assert 断言不通过这泥马 https.ServerCertificateChain 为 null证书链短一节我一点都不觉得荒唐了但为什么为 null 呢我翻了 ASP.NET core 的 issues找到25年3月报告的一个问题Kestrel inconsistent certificate chain handling between endpoint and default configuration描述的是证书放到 Default 节点后表现为和放到 Endpoint(Https) 节点不一样我们现在把配置文件改成如下Copy{ Kestrel: { Endpoints: { Https: { Url: https://*:443, Certificate: { Path: certs/test_bundle.crt, KeyPath: certs/test.key } } } } }现在能读取到 ServerCertificateChain看来AI说得没错https.ServerCertificateChain 自己实现从证书文件读取可以弥补 Default 证书不读取证书链的问题假设我们手动设置了 ServerCertificateChain证书也是配置在Endpoint下最终保留的来源于哪里的证书链呢做了以下实验后发现是证书文件的证书链优先。Copybuilder.WebHost.ConfigureKestrel(kestrel { kestrel.ConfigureHttpsDefaults(https { // 手动设置 ServerCertificateChain https.ServerCertificateChain []; https.OnAuthenticate (context, options) { // 断言成立如果能读取到证书文件的证书链手动设置的 ServerCertificateChain 会被覆盖 Debug.Assert(https.ServerCertificateChain is not null https.ServerCertificateChain.Count 0); }; }); });我把证书格式改成带证书链的 pfx配置在 Endpoint 节下发现 kestrel 读取到的证书链不为 null 但总是 0 个元素翻看最新的源码看到 kestrel 目前只简单粗暴的通过X509Certificate2Collection.ImportFromPemFile())来读取证书链对于 pfx 格式这肯定 import 不到内容哈。使用 ServerCertificateChain#经过上述深入的分析只要我们使用 PEM 格式的证书文件并且配置在 Endpoint 节下那么 kestrel 就能使用上证书文件里提供的中间证书最后生成和 nginx 一样的证书链。我迫不及待地搭建了一个测试服务器用上和 nginx 一样的 PEM 证书满怀信心地使用 openssl 客户端来验证可我发现还是验证不通过!我确定 kestrel 已经从 PEM 证书文件里原封地读取到了证书链且设置了 https.ServerCertificateChain但在 tls 握手时服务器响应的证书链变短了。翻看了 HttpsConnectionMiddleware 源码发现 serverCertificateContext 的构建方式是依赖于系统证书 storehttps.ServerCertificateChain 只不过是其构建过程中可能用到的辅料罢了或者说几乎所有服务器操作系统环境https.ServerCertificateChain 有值或为 null 生成的 serverCertificateContext 一般都不会有差异。我们通过自定义生成 ServerCertificateContext 并应用到 kestrel最终发现生成的证书链和 nginx 的完全一样在客户端设备上进行 https 连接成功。Copybuilder.WebHost.ConfigureKestrel(kestrel { kestrel.ConfigureHttpsDefaults(https { https.OnAuthenticate (context, options) { var chain https.ServerCertificateChain; Debug.Assert(chain is not null chain.Count 0); // ServerCertificateContext 要缓存不能每次 OnAuthenticate 都创建新的 ServerCertificateContext否则性能会非常差。 var serverCertificate options.ServerCertificate as X509Certificate2; var trust SslCertificateTrust.CreateForX509Collection(chain); options.ServerCertificateContext SslStreamCertificateContext.Create(serverCertificate!, chain, false, trust); }; }); });通用避坑方案#我想实现一个避坑库让使用者加一行代码就能避开 kestrel 的上述行为而开发者的项目的证书文件类型能继续保留为 pfx 格式也允许只配置默认证书为所有 https Endpoint 共享同时要求这个库不能影响到开发者既有的 https 配置委托代码或 Endpoint 配置委托代码的执行也不能让 https tls握手时的性能不可接受的下降。