Repository Wiki
BeyondDimension/SteamTools

证书管理与 HTTPS 拦截

Steam++(Watt Toolkit)加速器的反向代理需要以"中间人"方式接管 HTTPS 流量,这依赖一套自签名 CA 根证书的生成、加载、跨平台信任安装与按域名签发终端证书的完整机制。本页覆盖 Accelerator.ReverseProxy 插件中的证书子系统(CertificateManagerImpl、CertGenerator、ICertificateManager)及其与 IPC、平台层、系统证书存储的交互。

Purpose and Scope

本页覆盖以下内容:

  • CA 根证书的生成算法:CertGenerator 如何基于 CertificateRequest 构造自签名 CA(BasicConstraints、KeyUsage、EKU、SAN、SKI 等扩展),以及 RSA 2048 位密钥与有效期参数。
  • 根证书的持久化与加载:PFX/CER 双文件布局、X509CertificatePackable 打包封装、过期证书的检测与再生成。
  • 跨平台信任安装/卸载:Windows(X509Store 本机根存储)、macOS/Linux(通过 IPCPlatformService 委托给平台实现)的差异处理。
  • 启动期证书检查状态机:ICertificateManager.Constants.CheckRootCertificate 的完整判定链与 StartProxyResultCode 错误码语义。
  • HTTPS 拦截中 CA → 域名证书的签发链路:反向代理服务如何消费 RootCertificatePackable,以及 CreateEndCertificate 用根 CA 为目标域名签发终端证书的方式。

有意留给兄弟页面的内容:

  • 反向代理本身的请求转发、YARP 管道与脚本/host 映射规则 → 参见反向代理服务相关页面。
  • IPC 进程模型(子进程启动、IPCSubProcessService 通用机制)→ 参见 IPC 相关页面。
  • 平台证书实现的具体命令行调用细节(LinuxPlatformServiceImpl.Certificate.cs、MacCatalystPlatformServiceImpl.Certificate.cs)属于平台服务层,本页仅描述其调用契约。

Overview

加速功能本质上是把社区加速脚本的 host 映射转换为本地反向代理:客户端访问被加速域名时,实际连接的是本地代理服务器,再由代理与真实边缘节点通信。如果目标站点是 HTTPS,本地代理必须向客户端出示一个该域名的有效证书。为了不弹出浏览器告警,Steam++ 的做法是:

  1. 首次使用时在本地生成一个 自签名 CA 根证书(CertGenerator.GenerateBySelfPfx),导出为 PFX 文件保存在应用数据目录,同时导出 DER 编码的 CER 文件供系统导入。
  2. 引导用户把该 CA 安装到 操作系统受信任的根证书颁发机构(Windows 上直接写 LocalMachine\Root 存储;macOS/Linux 上经由 IPCPlatformService 调用平台实现并弹窗/引导浏览器导入)。
  3. 运行期间,反向代理(YarpReverseProxyServiceImpl)通过 CertificateManager.RootCertificatePackable 拿到根证书,并用 CertGenerator.CreateEndCertificate 以根 CA 为签发者为每个被拦截的域名签发终端证书,从而完成 TLS 终结与重建。

ICertificateManager 被 [IpcPublic] 标记为 IPC 公共接口,主进程与加速子进程之间通过序列化的 X509CertificatePackable 字节流传递证书数据,CheckRootCertificate 的注释明确说明其目标是"减少 Ipc 往返次数"——把生成、获取、安装、复查合并为一次子进程内调用链。

Architecture

Loading diagram...

组件职责与连接关系说明:

  • ICertificateManager(Services/Certificate/ICertificateManager.cs):证书安装/卸载的对外契约,标注 [IpcPublic(Timeout = AssemblyInfo.IpcTimeout, IgnoresIpcException = false)],可被跨进程调用。其嵌套 static class Constants 承载了与 IPC 无关的纯静态逻辑(IsCertificateInstalled、TrustRootCertificate、CheckRootCertificate),这样这些逻辑既能在子进程内直接执行,也能被复用。
  • CertificateManagerImpl:默认实现,持有 RootCertificate / RootCertificatePackable / PfxPassword 状态,并通过构造函数注入的 IPCSubProcessService 获取 IPCPlatformService 与 IPCToastService。
  • CertGenerator:纯静态的证书工厂,源自 FastGithub 项目的 CertGenerator(文件头有来源注释),负责 CA 证书与终端证书的 X.509 构造,不接触任何存储。
  • CertificateConstants:集中定义根证书主题名(RootCertificateName)、有效期天数(CertificateValidDays)与默认 PFX/CER 文件路径。
  • X509CertificatePackable:可跨进程序列化的证书包装类型,支持到 X509Certificate2 的隐式转换,是 IPC 边界上传输证书的唯一载体。
  • YarpReverseProxyServiceImpl / CertService:消费方。前者在代理启动时读取 RootCertificatePackable(见源码 L52-L55);后者位于 Services.Implementation/HttpServer/Certificates/,服务于 HTTPS 拦截时的证书上下文(本页源码预算内未展开读取该文件,细节以源码为准)。

Main Content — 实现深度解析

1. 根证书生成:CertGenerator.CreateCACertificate

根 CA 的构造完全基于 .NET 的 CertificateRequest API,关键点在于每个 X.509 扩展的选择理由:

csharp
1public static X509Certificate2 CreateCACertificate( 2 X500DistinguishedName subjectName, 3 DateTimeOffset notBefore, 4 DateTimeOffset notAfter, 5 int rsaKeySizeInBits = 2048, 6 int pathLengthConstraint = 1) 7{ 8 using var rsa = RSA.Create(rsaKeySizeInBits); 9 var request = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 10 11 var basicConstraints = new X509BasicConstraintsExtension(true, pathLengthConstraint > 0, pathLengthConstraint, true); 12 request.CertificateExtensions.Add(basicConstraints); 13 14 var keyUsage = new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.CrlSign | X509KeyUsageFlags.KeyCertSign, true); 15 request.CertificateExtensions.Add(keyUsage); 16 17 var oids = new OidCollection { tlsServerOid, tlsClientOid }; 18 var enhancedKeyUsage = new X509EnhancedKeyUsageExtension(oids, true); 19 request.CertificateExtensions.Add(enhancedKeyUsage); 20 21 var dnsBuilder = new SubjectAlternativeNameBuilder(); 22 dnsBuilder.Add(CertificateConstants.RootCertificateName); 23 request.CertificateExtensions.Add(dnsBuilder.Build()); 24 25 var subjectKeyId = new X509SubjectKeyIdentifierExtension(request.PublicKey, false); 26 request.CertificateExtensions.Add(subjectKeyId); 27 28 return request.CreateSelfSigned(notBefore, notAfter); 29}

Source: CertGenerator.cs

逐项设计意图:

扩展取值为什么
X509BasicConstraintsExtensionCA=true, HasPathLengthConstraint=true, PathLength=1声明这是 CA 证书,且路径长度限制为 1 —— 根 CA 只能直接签发终端证书,终端证书自身不得再作为 CA,降低滥用面
X509KeyUsageExtensionDigitalSignature | CrlSign | KeyCertSignCA 只需要签名能力(签证书、签 CRL),不含加密用途
X509EnhancedKeyUsageExtension1.3.6.1.5.5.7.3.1(serverAuth)与 1.3.6.1.5.5.7.3.2(clientAuth)现代浏览器/系统对 SAN 与 EKU 校验严格,同时声明 TLS 服务端/客户端用途可避免部分平台告警
SubjectAlternativeNameBuilder加入 CertificateConstants.RootCertificateNameChrome 等浏览器要求证书必须包含与主题匹配的 SAN
X509SubjectKeyIdentifierExtension基于公钥派生终端证书的 AKI 扩展会引用它建立签名链
密钥与签名RSA 2048 位 + SHA256 + PKCS#1兼容性与安全性的折中(KEY_SIZE_BITS = 2048 常量同时暴露供终端证书复用)

默认主题 DN 定义在 X500DistinguishedNameValue:C=CN, O=BeyondDimension, OU=Technical Department, CN={CertificateConstants.RootCertificateName}(CertGenerator.cs L13),所有由本工具签出的证书都带有该可识别的组织信息,便于用户在系统证书管理器中辨认与卸载。

GenerateBySelfPfx(L29-L43)在生成后立即将证书导出为 PKCS#12 写入磁盘:

csharp
1var r = CreateCACertificate(new X500DistinguishedName(string.IsNullOrEmpty(x509Name) ? X500DistinguishedNameValue : x509Name), notBefore, notAfter); 2if (!string.IsNullOrEmpty(caPfxPath)) 3{ 4 byte[] exported = r.Export(X509ContentType.Pkcs12, password); 5 File.WriteAllBytes(caPfxPath, exported); 6} 7return r;

Source: CertGenerator.cs

x509Name 传 null 时使用默认 DN —— CertificateManagerImpl.SharedCreateRootCertificate 正是这样调用的,并指定有效期 validFrom = DateTime.Today.AddDays(-1)(回拨一天以容忍时钟偏差)和 validTo = DateTime.Today.AddDays(CertificateConstants.CertificateValidDays)。

2. 终端(域名)证书签发:CreateEndCertificate

HTTPS 拦截时为每个被代理域名动态生成叶子证书,与 CA 证书的差异集中在三处:

csharp
1public static X509Certificate2 CreateEndCertificate( 2 X509Certificate2 issuerCertificate, 3 X500DistinguishedName subjectName, 4 IEnumerable<string>? extraDnsNames = default, 5 DateTimeOffset? notBefore = default, 6 DateTimeOffset? notAfter = default, 7 int rsaKeySizeInBits = 2048) 8{ 9 using var rsa = RSA.Create(rsaKeySizeInBits); 10 var request = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 11 12 var basicConstraints = new X509BasicConstraintsExtension(false, false, 0, true); 13 request.CertificateExtensions.Add(basicConstraints); 14 15 var keyUsage = new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, true); 16 request.CertificateExtensions.Add(keyUsage); 17 18 var oids = new OidCollection { tlsServerOid, tlsClientOid }; 19 var enhancedKeyUsage = new X509EnhancedKeyUsageExtension(oids, true); 20 request.CertificateExtensions.Add(enhancedKeyUsage); 21 22 var authorityKeyId = GetAuthorityKeyIdentifierExtension(issuerCertificate); 23 request.CertificateExtensions.Add(authorityKeyId); 24 25 var subjectKeyId = new X509SubjectKeyIdentifierExtension(request.PublicKey, false); 26 request.CertificateExtensions.Add(subjectKeyId); 27 28 var dnsBuilder = new SubjectAlternativeNameBuilder(); 29 dnsBuilder.Add(subjectName.Name[3..]); 30 // ... extraDnsNames 循环加入 SAN

Source: CertGenerator.cs

  • BasicConstraints 为 CA=false:叶子证书不能签发其它证书;
  • KeyUsage 换成 DigitalSignature | KeyEncipherment:TLS 握手需要密钥交换;
  • 新增 GetAuthorityKeyIdentifierExtension(issuerCertificate)(AKI):把叶子证书与根 CA 的 SKI 关联,构成验证链;
  • SAN 从 subjectName.Name[3..] 去掉前导 "CN=" 后取主体名,再叠加 extraDnsNames(如同一 CDN 上的多个域名/IP),这是浏览器匹配域名的唯一依据。

单元测试 CertificateUnitTest 展示了典型用法:new X509Certificate2("ca.pfx") 加载根证书后调用 CreateEndCertificate(x509Certificate2, subjectName, domains, default, DateTimeOffset.UtcNow.AddYears(1))(CertificateUnitTest.cs L18-L21)。

3. 加载与容错:LoadRootCertificate

csharp
1X509Certificate2? LoadRootCertificate() 2{ 3 try 4 { 5 ICertificateManager thiz = Interface; 6 if (!File.Exists(thiz.PfxFilePath)) 7 return null; 8 X509Certificate2 rootCert; 9 try 10 { 11 RootCertificatePackable = X509CertificatePackable.CreateX509Certificate2( 12 thiz.PfxFilePath, GetPfxPassword(), X509KeyStorageFlags.Exportable); 13 rootCert = RootCertificatePackable!; 14 rootCert.ThrowIsNull(); 15 } 16 catch (PlatformNotSupportedException) 17 { 18 // https://github.com/dotnet/runtime/issues/71603 19 return null; 20 } 21 catch (CryptographicException e) 22 { 23 if (e.InnerException is PlatformNotSupportedException) return null; 24 throw; 25 } 26 if (rootCert.NotAfter <= DateTime.Now) 27 { 28 Log.Error(TAG, "Loaded root certificate has expired."); 29 return null; 30 } 31 return rootCert; 32 } 33 catch (Exception ex) 34 { 35 Log.Error(TAG, ex, nameof(LoadRootCertificate)); 36 return null; 37 } 38}

Source: CertificateManagerImpl.cs

值得注意的边界处理:

  • 文件不存在:返回 null(而非抛异常),调用方 SharedCreateRootCertificate 据此决定走"新生成"分支;
  • PlatformNotSupportedException 双重捕获:专门为 dotnet/runtime#71603(部分平台/运行时下 PFX 加载抛 PlatformNotSupportedException,且可能被包装在 CryptographicException 里)兜底,返回 null 触发重新生成而不是让代理启动崩溃;
  • 过期证书视同不存在:NotAfter <= DateTime.Now 时返回 null,下一次 GenerateCertificate 会生成新的根证书(但注意:旧证书若仍在系统存储中,会形成"同名双证书"状态,卸载逻辑按主题名清除可兜底);
  • X509KeyStorageFlags.Exportable:保证后续可以通过 IPC 以 X509CertificatePackable 序列化导出;
  • 整体吞异常 + 日志:加载失败绝不让上层崩溃,因为证书可以随时重建。

4. 生成与再生成:GetCerFilePathGeneratedWhenNoFileExists 与 GenerateCertificate

csharp
1public string? GetCerFilePathGeneratedWhenNoFileExists() 2{ 3 var filePath = Interface.CerFilePath; 4 lock (lockGenerateCertificate) 5 { 6 if (!File.Exists(filePath)) 7 { 8 if (!GenerateCertificateUnlock(filePath)) 9 return null; 10 } 11 else if (RootCertificate == null) 12 { 13 RootCertificate = LoadRootCertificate(); 14 } 15 return filePath; 16 } 17} 18 19bool GenerateCertificateUnlock(string filePath) 20{ 21 var result = SharedCreateRootCertificate(); 22 if (!result || RootCertificate == null) 23 { 24 Log.Error(TAG, "Failed to create certificate"); 25 toast.Show( 26 IPCToastService.ToastIcon.Error, 27 IPCToastService.ToastText.CreateCertificateFaild); 28 return false; 29 } 30 RootCertificate.SaveCerCertificateFile(filePath); 31 return true; 32}

Source: CertificateManagerImpl.cs

  • 文件存在但内存态 RootCertificate == null 时只做加载,不做生成 —— 幂等且避免覆盖用户已有证书;
  • 生成失败会通过 IPC Toast(ToastText.CreateCertificateFaild)直接通知子进程 UI 层,同时返回 null 让 CheckRootCertificate 报 GenerateCerFilePathFail;
  • SharedCreateRootCertificate(L130-L153)内部:先 LoadRootCertificate(),命中则直接返回 true;否则计算有效期并 CertGenerator.GenerateBySelfPfx(...) 落盘 PFX,再重新 CreateX509Certificate2 填充 RootCertificatePackable。

5. 跨平台信任安装:ICertificateManager.Constants.TrustRootCertificate

安装逻辑按操作系统分流,核心差异在于 Windows 可以进程内直写证书存储,而 macOS/Linux 必须经由 IPCPlatformService:

csharp
1internal static void TrustRootCertificate( 2 Func<string?> getCerFilePath, 3 IPCPlatformService platformService, 4 X509Certificate2 certificate2) 5{ 6 if (OperatingSystem.IsWindows()) 7 { 8 using var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine); 9 try 10 { 11 store.Open(OpenFlags.ReadWrite); 12 var findCerts = store.Certificates.Find(X509FindType.FindByThumbprint, certificate2.Thumbprint, true); 13 if (!findCerts.Any()) 14 { 15 store.Add(certificate2); 16 } 17 } 18 catch (Exception e) 19 { 20 Log.Error(nameof(ICertificateManager), e, 21 "Please manually install the CA certificate to a trusted root certificate authority."); 22 } 23 } 24 else if (OperatingSystem.IsMacOS()) 25 { 26 var cerFilePath = getCerFilePath(); 27 if (cerFilePath == null) return; 28 void TrustRootCertificateMacOS() 29 { 30 var result = platformService.TrustRootCertificateAsync(cerFilePath); 31 if (result.HasValue && !result.Value) 32 { 33 TrustRootCertificateMacOS(); 34 } 35 } 36 TrustRootCertificateMacOS(); 37 } 38 else if (OperatingSystem.IsLinux()) 39 { 40 var cerFilePath = getCerFilePath(); 41 if (cerFilePath == null) return; 42 void TrustRootCertificateLinux() 43 { 44 var result = platformService.TrustRootCertificateAsync(cerFilePath); 45 try 46 { 47 // 部分系统还是只能手动导入浏览器 48 Browser2.Open(Const.Urls.OfficialWebsite_LiunxSetupCer); 49 } 50 catch { } 51 if (result.HasValue && !result.Value) 52 getCerFilePath(); 53 } 54 TrustRootCertificateLinux(); 55 } 56}

Source: ICertificateManager.cs

设计要点:

  • 按 Thumbprint 去重(Windows):FindByThumbprint(..., validOnly: true) 找不到时才 store.Add,重复点击"安装证书"不会产生重复条目;
  • LocalMachine 而非 CurrentUser:写入机器级根存储需要管理员权限,失败时只记录错误并提示"请手动安装",不阻断流程;卸载同理,SharedRemoveTrustedRootCertificate 的异常消息明确写着 "You may need admin rights."(CertificateManagerImpl.cs L179-L181);
  • macOS 递归重试:TrustRootCertificateAsync(命名上带 Async 但此处为同步调用语义)返回 false(用户在 Keychain 弹窗点了取消)时递归重试,直到用户确认或成功;
  • Linux 打开官方引导页:许多发行版/浏览器(尤其 Firefox 使用独立 NSS 存储)无法通过系统命令统一导入,代码会调用 Browser2.Open(Const.Urls.OfficialWebsite_LiunxSetupCer) 打开官网的 Linux 安装证书教程页。

安装状态检查 IsCertificateInstalled(L17-L39)同样分流:Unix 系(Android/Linux/macOS)委托 platformService.IsCertificateInstalled(packable),Windows 打开只读的 Root/LocalMachine 存储并用 store.Certificates.Contains(certificate2) 判断;两者之前都先做 NotAfter <= DateTime.Now 的过期短路。

Core Flow — 代理启动时的证书检查状态机

ICertificateManager.Constants.CheckRootCertificate(L127-L215)是整个子系统的编排入口,被反向代理启动流程调用,一次性完成"获取 → 生成 → 安装 → 复查":

Loading diagram...

源码中两个贯穿性的优化意图(均有注释佐证):

  1. 减少 IPC 往返(L122-L124 注释:"检查根证书,生成,信任,减少 Ipc 往返次数"):platformService 以参数形式直接传入静态方法,避免在 Constants 内再次通过 IPC 解析服务;
  2. 双读取校验:GetCerFilePath 用本地闭包 cerFilePathLazy 缓存路径,GetRootCertificatePackable 在生成前后各读一次 —— 第一次确认证书数据缺失,第二次确认生成真的生效,期间每一步失败都映射为独立的 StartProxyResultCode,让上层(UI/日志)能精确提示用户失败原因。

StartProxyResultCode 中与证书相关的错误码

错误码触发条件用户侧含义
GenerateCerFilePathFailCER 路径获取/生成失败(如磁盘不可写)无法生成证书文件
GetCertificatePackableFail生成后仍拿不到可序列化证书数据证书生成异常
GetX509Certificate2FailX509CertificatePackable → X509Certificate2 隐式转换返回 null(损坏/平台不支持)证书不可用
TrustRootCertificateFail安装动作后复查仍未安装信任安装失败(常见于权限不足或用户取消弹窗)

IReverseProxyService.Constants.cs L322 定义了 GetX509Certificate2Fail 枚举成员,是上述链路在反向代理启动结果上的落点。

HTTPS 拦截时的证书消费时序

Loading diagram...

YarpReverseProxyServiceImpl 中的关键消费点:

csharp
X509Certificate2? cer = CertificateManager.RootCertificatePackable; if (cer is not null &&

Source: YarpReverseProxyServiceImpl.cs

这行代码利用了 X509CertificatePackable → X509Certificate2 的隐式转换(ICertificateManager L238 的 XML 注释明确说明该语义),ReverseProxyServiceState.RootCertificate 属性(ReverseProxyServiceImpl.cs L22)也是 X509Certificate2? 类型,供下游取用。

数据模型 — 文件与内存状态

Loading diagram...

API Reference

CertGenerator.GenerateBySelfPfx(x509Name, notBefore, notAfter, caPfxPath, password): X509Certificate2

生成自签名根证书并可选导出 PFX。

Parameters:

  • x509Name (string?):自定义 X500 主题 DN;传 null/空串时使用默认 X500DistinguishedNameValue
  • notBefore (DateTimeOffset):生效起始时间(调用方回拨 1 天)
  • notAfter (DateTimeOffset):失效时间(Today + CertificateValidDays)
  • caPfxPath (string?):PFX 输出路径,非空时 Export(X509ContentType.Pkcs12, password) 落盘
  • password (string?, 默认 null):PFX 加密密码

Returns: 生成的 X509Certificate2(自签名)。

CertGenerator.CreateEndCertificate(issuerCertificate, subjectName, extraDnsNames, notBefore, notAfter, rsaKeySizeInBits): X509Certificate2

以根 CA 为签发者生成域名终端证书,用于 HTTPS 拦截时向客户端出示。

Parameters:

  • issuerCertificate (X509Certificate2):签发者(根 CA)证书
  • subjectName (X500DistinguishedName):终端主体,SAN 取 Name[3..]
  • extraDnsNames (IEnumerable<string>?):附加到 SAN 的域名集合
  • notBefore / notAfter (DateTimeOffset?):有效期,默认值由实现决定
  • rsaKeySizeInBits (int, 默认 2048):终端证书 RSA 密钥长度

Returns: 可用于 TLS 的叶子证书(BasicConstraints CA=false,含 AKI/SKI/SAN)。

CertificateManagerImpl.GetCerFilePathGeneratedWhenNoFileExists(): string?

确保 CER 文件存在(不存在则生成根证书并导出),返回路径。

Returns: CER 文件绝对路径;生成失败(Toast 提示 CreateCertificateFaild)时返回 null。

Throws: 无(内部捕获异常并日志 + Toast)。

ICertificateManager 属性成员

成员类型说明
PfxPasswordbyte[]?证书密码的 UTF-8 字节(GetPfxPassword() 内部转回 string)
PfxFilePathstring固定返回 CertificateConstants.DefaultPfxFilePath
CerFilePathstring固定返回 CertificateConstants.DefaultCerFilePath
RootCertificatePackablebyte[]?Serializable.SMP2 序列化的证书数据(IPC 边界),getter 内 lock (lock_RootCertificatePackable) 懒加载 RootCertificate

ICertificateManager.Constants 静态方法

方法签名职责
IsCertificateInstalled(IPCPlatformService, X509CertificatePackable) → bool过期短路 + 平台分流(Unix 委托 IPC,Windows 查 Root/LocalMachine)
IsRootCertificateInstalled(ICertificateManager, IPCPlatformService, X509CertificatePackable) → boolpackable 为 default 时先取 CER 路径触发生成,再查安装态
TrustRootCertificate(Func<string?>, IPCPlatformService, X509Certificate2) → voidWindows 写存储 / macOS 递归重试 / Linux 调平台 + 打开教程页
CheckRootCertificate(IPCPlatformService, ICertificateManager) → StartProxyResultCode启动期编排(见 Core Flow)

Failure Modes, Edge Cases & Concurrency

  • 平台不支持加载 PFX:dotnet/runtime#71603 导致的 PlatformNotSupportedException(含包装在 CryptographicException.InnerException 中的情况)被静默转成 null,走重新生成分支而非崩溃。
  • 证书过期:加载与安装检查两处都做 NotAfter <= DateTime.Now 判断;过期根证书会被静默替换,但系统存储中的旧条目需依赖按主题名卸载的 SharedRemoveTrustedRootCertificate 清理。
  • 权限不足:Windows 写 LocalMachine\Root 需要管理员;失败仅记录日志并提示手动安装,代理启动会以 TrustRootCertificateFail 结束(用户仍可手动导入 CER 后重试)。
  • macOS 用户取消弹窗:TrustRootCertificateMacOS 本地函数递归重试,保证用户必须做出选择;Linux 失败时则依赖官网教程页引导。
  • 并发控制:两把独立锁 —— lock_RootCertificatePackable(CertificateManagerImpl.cs L26-L38)保护 RootCertificate 懒加载与序列化;lockGenerateCertificate(L188)串行化 GetCerFilePathGeneratedWhenNoFileExists / GenerateCertificate,防止并发生成产生两份根证书(第二个进入者会发现文件已存在只做加载)。
  • IPC 数据缺失:CheckRootCertificate 对 RootCertificatePackable 做 default 相等判断,GetRootCertificatePackable 在非 APP_REVERSE_PROXY 编译路径下用 try/catch 包裹反序列化,异常静默返回 default,由后续错误码暴露。
  • 过期/同名证书清理:SharedTrustRootCertificate(旧版,已标 [Obsolete],L91-L128)中按 Thumbprint 查找去重的逻辑保留为参考;现行 Constants.TrustRootCertificate 沿用相同策略。卸载按 FindBySubjectName(RootCertificateName) 移除全部匹配项(注释掉了 Thumbprint 过滤),确保清理彻底。

Performance & Operational Notes

  • 一次生成,长期复用:RSA 2048 自签名生成只发生在 PFX 缺失或过期时;运行期加载走文件 + X509CertificatePackable 包装,RootCertificatePackable getter 用懒加载 + 锁,避免每次 IPC 调用重复 IO。
  • IPC 序列化开销控制:Serializable.SMP2(RootCertificatePackable) 只在 getter 返回时执行一次打包;CheckRootCertificate 注释强调把 platformService 作为参数传递"避免 IPC 调用开销"。
  • 终端证书按需签发:CreateEndCertificate 每次创建新 RSA 密钥并自签,属于 CPU 敏感操作;使用方(CertService / YARP 证书选择器)应对域名证书做缓存(本页源码预算内未展开该缓存实现,细节参见 HttpServer/Certificates/CertService.cs)。
  • 运维排障入口:Startup.Commands.cs 提供 CLI 查看证书(new X509Certificate2(CertificateConstants.DefaultPfxFilePath, (string?)null, X509KeyStorageFlags.Exportable) 打印 Subject 等,L292)与移除证书的命令入口;IGenerateCaCertCommand 支持在发布流程中生成 CA。
  • 测试:CertificateUnitTest(L18-L21)验证"加载 ca.pfx → 为多域名签发一年期终端证书"的链路,可作为签名行为的回归样例。

Extension Points

  • 新增平台:实现 IPCPlatformService 的证书相关成员(IsCertificateInstalled、TrustRootCertificateAsync 等),Constants.IsCertificateInstalled / TrustRootCertificate 的 OS 分流中补一个 OperatingSystem.IsXxx() 分支即可。
  • 更换证书参数:RSA 密钥长度、SHA256、pathLengthConstraint=1、默认 DN 均在 CertGenerator / CertificateConstants 中集中定义,改一处即可全局生效(注意:更换主题名会让系统中的旧证书成为孤儿)。
  • 接入其它代理引擎:任何需要根证书的组件只需依赖 ICertificateManager.RootCertificatePackable 并利用其到 X509Certificate2 的隐式转换,如 YarpReverseProxyServiceImpl 所示。

Sources

(3 files)
src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/Services.Implementation/Certificate
src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/Services/Certificate