证书管理与 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++ 的做法是:
- 首次使用时在本地生成一个 自签名 CA 根证书(
CertGenerator.GenerateBySelfPfx),导出为 PFX 文件保存在应用数据目录,同时导出 DER 编码的 CER 文件供系统导入。 - 引导用户把该 CA 安装到 操作系统受信任的根证书颁发机构(Windows 上直接写
LocalMachine\Root存储;macOS/Linux 上经由IPCPlatformService调用平台实现并弹窗/引导浏览器导入)。 - 运行期间,反向代理(
YarpReverseProxyServiceImpl)通过CertificateManager.RootCertificatePackable拿到根证书,并用CertGenerator.CreateEndCertificate以根 CA 为签发者为每个被拦截的域名签发终端证书,从而完成 TLS 终结与重建。
ICertificateManager 被 [IpcPublic] 标记为 IPC 公共接口,主进程与加速子进程之间通过序列化的 X509CertificatePackable 字节流传递证书数据,CheckRootCertificate 的注释明确说明其目标是"减少 Ipc 往返次数"——把生成、获取、安装、复查合并为一次子进程内调用链。
Architecture
组件职责与连接关系说明:
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 扩展的选择理由:
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
逐项设计意图:
| 扩展 | 取值 | 为什么 |
|---|---|---|
X509BasicConstraintsExtension | CA=true, HasPathLengthConstraint=true, PathLength=1 | 声明这是 CA 证书,且路径长度限制为 1 —— 根 CA 只能直接签发终端证书,终端证书自身不得再作为 CA,降低滥用面 |
X509KeyUsageExtension | DigitalSignature | CrlSign | KeyCertSign | CA 只需要签名能力(签证书、签 CRL),不含加密用途 |
X509EnhancedKeyUsageExtension | 1.3.6.1.5.5.7.3.1(serverAuth)与 1.3.6.1.5.5.7.3.2(clientAuth) | 现代浏览器/系统对 SAN 与 EKU 校验严格,同时声明 TLS 服务端/客户端用途可避免部分平台告警 |
SubjectAlternativeNameBuilder | 加入 CertificateConstants.RootCertificateName | Chrome 等浏览器要求证书必须包含与主题匹配的 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 写入磁盘:
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 证书的差异集中在三处:
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 循环加入 SANSource: 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
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
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:
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)是整个子系统的编排入口,被反向代理启动流程调用,一次性完成"获取 → 生成 → 安装 → 复查":
源码中两个贯穿性的优化意图(均有注释佐证):
- 减少 IPC 往返(L122-L124 注释:"检查根证书,生成,信任,减少 Ipc 往返次数"):
platformService以参数形式直接传入静态方法,避免在Constants内再次通过 IPC 解析服务; - 双读取校验:
GetCerFilePath用本地闭包cerFilePathLazy缓存路径,GetRootCertificatePackable在生成前后各读一次 —— 第一次确认证书数据缺失,第二次确认生成真的生效,期间每一步失败都映射为独立的StartProxyResultCode,让上层(UI/日志)能精确提示用户失败原因。
StartProxyResultCode 中与证书相关的错误码
| 错误码 | 触发条件 | 用户侧含义 |
|---|---|---|
GenerateCerFilePathFail | CER 路径获取/生成失败(如磁盘不可写) | 无法生成证书文件 |
GetCertificatePackableFail | 生成后仍拿不到可序列化证书数据 | 证书生成异常 |
GetX509Certificate2Fail | X509CertificatePackable → X509Certificate2 隐式转换返回 null(损坏/平台不支持) | 证书不可用 |
TrustRootCertificateFail | 安装动作后复查仍未安装 | 信任安装失败(常见于权限不足或用户取消弹窗) |
IReverseProxyService.Constants.cs L322 定义了 GetX509Certificate2Fail 枚举成员,是上述链路在反向代理启动结果上的落点。
HTTPS 拦截时的证书消费时序
YarpReverseProxyServiceImpl 中的关键消费点:
X509Certificate2? cer = CertificateManager.RootCertificatePackable;
if (cer is not null &&Source: YarpReverseProxyServiceImpl.cs
这行代码利用了 X509CertificatePackable → X509Certificate2 的隐式转换(ICertificateManager L238 的 XML 注释明确说明该语义),ReverseProxyServiceState.RootCertificate 属性(ReverseProxyServiceImpl.cs L22)也是 X509Certificate2? 类型,供下游取用。
数据模型 — 文件与内存状态
API Reference
CertGenerator.GenerateBySelfPfx(x509Name, notBefore, notAfter, caPfxPath, password): X509Certificate2
生成自签名根证书并可选导出 PFX。
Parameters:
x509Name(string?):自定义 X500 主题 DN;传null/空串时使用默认X500DistinguishedNameValuenotBefore(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 属性成员
| 成员 | 类型 | 说明 |
|---|---|---|
PfxPassword | byte[]? | 证书密码的 UTF-8 字节(GetPfxPassword() 内部转回 string) |
PfxFilePath | string | 固定返回 CertificateConstants.DefaultPfxFilePath |
CerFilePath | string | 固定返回 CertificateConstants.DefaultCerFilePath |
RootCertificatePackable | byte[]? | Serializable.SMP2 序列化的证书数据(IPC 边界),getter 内 lock (lock_RootCertificatePackable) 懒加载 RootCertificate |
ICertificateManager.Constants 静态方法
| 方法 | 签名 | 职责 |
|---|---|---|
IsCertificateInstalled | (IPCPlatformService, X509CertificatePackable) → bool | 过期短路 + 平台分流(Unix 委托 IPC,Windows 查 Root/LocalMachine) |
IsRootCertificateInstalled | (ICertificateManager, IPCPlatformService, X509CertificatePackable) → bool | packable 为 default 时先取 CER 路径触发生成,再查安装态 |
TrustRootCertificate | (Func<string?>, IPCPlatformService, X509Certificate2) → void | Windows 写存储 / 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包装,RootCertificatePackablegetter 用懒加载 + 锁,避免每次 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所示。
Related Links
- ICertificateManager.cs — 接口契约 + 启动检查状态机
- CertificateManagerImpl.cs — 加载/生成/安装/卸载实现
- CertGenerator.cs — X.509 证书工厂
- YarpReverseProxyServiceImpl.cs — 消费
RootCertificatePackable的反向代理 - CertService.cs — HttpServer 侧证书服务
- CertificateConstants.cs — 主题名/有效期/路径常量
- CertificateUnitTest.cs — 签发链路单元测试
- IReverseProxyService.Constants.cs —
StartProxyResultCode错误码定义