Windows 桌面客户端 —— 开发者指南¶
eID 平台的 Windows(C# / WinUI 3)桌面客户端 —— 该应用通过 e-ID Mongolia 手机应用实现
二维码 / 登记号推送登录、USB 令牌(智能卡)管理、用户控制台、组织代表权
以及证书验证。源码:desktop/windows-app/(Clean Architecture,.NET 解决方案
eIDMongolia.Desktop.sln)。
First-party 客户端 —— 改造已完成。 该客户端从
geregev1 桌面端导入, 并已改品牌为 eID Mongolia(命名空间eIDMongolia.*,MSIX 标识mn.eidmongol.desktop)。 遵循与 macOS 客户端(DESKTOP_MACOS.md)和 iOS 应用完全相同的原则, 它并非 RP —— 客户端不持有 RP secret、RP UUID、设备 HMAC 密钥或长期 bearer 令牌。 所有调用都经由 web 后端的公开/api/*路由(与浏览器所用路径完全一致); Go RP-API(/v3/*)的RP_API_SECRET仅由 web 服务端持有 (web/src/lib/rpclient.ts)。从旧的 gerege 直连后端模型 (/web2app/v1、/rp/v1、device-HMAC、Bearer 会话)迁移到/api/*的工作已完成; 详细映射见:desktop/windows-app/BACKEND-INTEGRATION.md。
1. 功能¶
以下能力依据源码确认(Views/Pages/*、Infrastructure/*)。登录后侧边栏显示
Home · Dashboard · Organizations · Tokens · Verify 菜单(登录前仅有 Login)——
见 ShellViewModel.BuildMenu():
- 登录(
LoginPage/LoginViewModel、CitizenAuthService)—— 两种可用方式: - 按登记号 / civil-ID 号推送 —— 向手机发送通知,使用 PIN1 确认
(
POST /api/login-notify)。 - 二维码 —— 显示二维码(由
QRCoder渲染)供手机扫描(POST /api/start)。 - USB 令牌管理(
TokensPage/TokenDetailPage)—— 检测已连接的智能卡 / 令牌、 查看状态(Blank/Initialized/Provisioned/Enrolled)、生成密钥、管理 PIN。 令牌相关实现是自包含的(Windows CNG + PKCS#11,见 §6)。 - 控制台(
DashboardPage/DashboardService)—— 已登录公民的证书 / 设备 / 活动 计数与姓名(/api/dashboard、/api/devices、/api/activity)。 - 组织(
OrganizationsPage/OrgService)—— 已登录公民可代表的 ACTIVE 组织列表 (/api/representations)。 - 验证(
VerifyPage/VerifyViewModel)—— 在客户端本地解析粘贴的 PEM 或证书文件 (ICertificateService.ParsePem/ParseFile),展示 subject / 有效期 / 有效性。 不调用后端(与 macOS 一致)。
注意 —— 目前没有 PDF 签名界面。 First-party 的 PDF 签名服务已就绪 (
RpAuthService→POST /api/sign-pdf-start,自行计算 PDF 的 SHA-256 摘要), 但尚无 ViewModel / Page 调用它 ——IRpAuthService除在 DI 中注册外未被任何地方使用, 侧边栏也没有 Sign 菜单。此外,RpAuthService未调用用于下载加盖签章 PDF 的/api/sign-pdf-download(只获取原始签名)。因此在 Windows 侧目前尚无完整的 PDF 签名流程 —— 只有 macOS 客户端可端到端工作。USB 令牌登录同样未启用(§5,桩实现)。
2. 架构¶
2.1. Clean Architecture 分层¶
解决方案由四个源码层 + 一个 WinUI 入口 + 测试组成(eIDMongolia.Desktop.sln):
| 项目 | 职责 | 依赖 |
|---|---|---|
eIDMongolia.Domain |
实体、值对象、Result<T>、ApiError、令牌类型。无外部依赖。 |
— |
eIDMongolia.Application |
用例抽象(IBackendApi、ICitizenAuthService、ICryptoTokenProvider 等)、eIDMongoliaOptions。 |
Domain |
eIDMongolia.Infrastructure |
HTTP 客户端、证书固定、DPAPI 保险库、Windows Hello、PKCS#11 / CNG 令牌提供者。Windows TFM。 | Application、Domain |
eIDMongolia.Presentation |
ViewModel(CommunityToolkit.Mvvm)、导航、校验。与 UI 框架无关。 | Application、Domain |
eIDMongolia.Client |
WinUI 3 入口 —— App.xaml、MainWindow、Views/Pages/*、Generic Host 引导(Hosting/AppHost.cs)。 |
以上全部层 |
eIDMongolia.UnitTests |
xUnit 测试。 | 所有 src 项目 |
引用关系: Client → Presentation、Infrastructure、Application、Domain;
Presentation → Application、Domain;Infrastructure → Application、Domain;Application → Domain。
澄清 ——
Client并不是 HTTP 客户端。 尽管名称如此,eIDMongolia.Client是 WinUI 3 的 UI / 入口层。真正的 HTTP 客户端位于Infrastructure/Http/BackendApiClient.cs与Infrastructure/Auth/*Service.cs。
宿主:Microsoft.Extensions.Hosting 的 Generic Host —— DI、Configuration、Options、Serilog。
引导入口:Client/Hosting/AppHost.cs。
2.2. 连接后端 —— first-party /api/*¶
Infrastructure/DependencyInjection.cs 配置了命名 HttpClient(eIDMongolia.Backend):
BaseUrl(web 源)、TLS 1.3、带证书固定的主处理器,随后是
AddStandardResilienceHandler(Polly 重试)。设备 HMAC 签名器与 Bearer 附加处理器
未接入管线 —— 在 first-party 模型中并不需要。客户端像浏览器一样直接调用
web/src/app/api/*(Next.js)路由:
| 路由 | 方法 | 用途 | 服务 |
|---|---|---|---|
/api/health |
GET | 服务端健康状态(live=ready) | BackendApiClient |
/api/login-notify |
POST | 按登记号 / civil-ID 号推送登录 | CitizenAuthService |
/api/start |
POST | 启动二维码登录 | CitizenAuthService |
/api/status |
GET | 长轮询(?sessionId=&pollToken=) |
CitizenAuthService、RpAuthService |
/api/sign-pdf-start |
POST | 启动 PDF 摘要签名(服务已就绪,无界面) | RpAuthService |
/api/dashboard |
POST | {personId} → 证书 / 设备 / 活动计数 + 姓名 |
DashboardService |
/api/devices |
POST | {personId} → 已注册设备 |
DashboardService |
/api/activity |
POST | {personId} → 限定于 RP 的会话历史 |
DashboardService |
/api/representations |
POST | {personId} → 可代表的 ACTIVE 组织 |
OrgService |
这些 /api/* 路由在 web 服务端使用 first-party RP Bearer 代理至 Go RP-API 的 /v3/*
(personId = 已登录公民的 civil_id / etsi,取自身份缓存)。pollToken 只签发给会话发起方,
且是从 /api/status 获取个人信息(姓名 / civil-ID 号)的必要条件。
身份桥接(解决模型不匹配)。
/api/status不返回session_token/user_id; 登录成功时,name+idNumber直接来自证书 subject。因此CitizenAuthService.PollAsync会将该身份存入ISessionIdentityCache, 并为每个会话生成合成的SessionToken(=sessionId) 与UserId(GUID);UserProfileService、DashboardService与OrgService从该缓存读取 personId (不存在/me接口)。
由于尚无对应的 /api/* 路由,以下为诚实的桩实现(不会崩溃 —— 返回
ApiError.Internal("…not_available_on_first_party_backend"),界面会优雅降级):
- USB 令牌登录(
CitizenAuthService.InitiateTokenChallengeAsync/VerifyTokenAsync)—— web 后端没有token/challenge·token/verify。 - 证书签发(
CertEnrollmentService)。 - 组织写操作(
OrgService的 lookup / register / members / name-en / X-Road CSR)—— 只有ListAsync是真实实现。
旧处理器仍保留在代码树中。 gerege 的
/web2app/v1设备 HMAC(HmacSigningHandler) 与 Bearer 附加(BearerAuthHandler)类仍保留在源码中,但未接入 HTTP 管线 —— 作为未来直连后端构建的参考保留。没有任何代码依赖它们。
配置读取自 eIDMongoliaOptions(appsettings.json + appsettings.{ENV}.json 覆盖
+ 以 EIDMNG_ 为前缀的环境变量)。生产环境 BaseUrl=https://eidmongolia.mn;
dev 覆盖文件(appsettings.Development.json)中为 BaseUrl=http://localhost:3000、
RequireCertificatePinning=false、RequireWindowsHello=false。在 first-party 模型下,
CertificateSpkiPins 与 XRoadClient 为空 / null。
3. 前置条件¶
依据源码确认(Directory.Packages.props、*.csproj):
- .NET 8(LTS)、最新版 C#(12);
TreatWarningsAsErrors=true,启用 nullable 与 implicit usings。 eIDMongolia.Client的 TFM:net8.0-windows10.0.26100.0(TargetPlatformMinVersion10.0.17763.0)—— 即 Windows 10 1809+, 构建需要 Windows 11 SDK(26100)。- Windows App SDK 2.0.1 + WinUI 3(
UseWinUI=true)。 - Visual Studio 2022 17.10+,安装 Windows 应用开发 工作负载;或使用
dotnetCLI。 - 平台:
x86/x64/ARM64。 - 若要使用 USB 令牌,主机上需安装 PKCS#11 模块(OpenSC
opensc-pkcs11.dll或 Feitianeps2003csp11.dll)—— 非必需;缺少时仅令牌模块不可用。
4. 构建与运行¶
cd desktop/windows-app
dotnet restore eIDMongolia.Desktop.sln
dotnet build eIDMongolia.Desktop.sln -c Debug /p:Platform=x64
dotnet test tests/eIDMongolia.UnitTests/eIDMongolia.UnitTests.csproj
在 Visual Studio 中:打开 eIDMongolia.Desktop.sln,将 eIDMongolia.Client
设为启动项目,然后按 F5。
两种运行模式(eIDMongolia.Client.csproj):
- 未设置
EidMsix→ 未打包的单文件 exe(开发用,dotnet run)。 EidMsix=true→ 打包为 MSIX(publish /pack-msix.ps1,见 §8)。
注意 —— 只能在 Windows 上构建与运行。 Infrastructure 层使用 Windows TFM (WinUI、CNG、DPAPI、PKCS#11),因此无法在 macOS/Linux 上构建。CI 中会在 Windows runner 上完成完整构建 + 测试 + MSIX 打包(§7)。
配置¶
src/eIDMongolia.Client/appsettings.json 是规范配置文件;
appsettings.Development.json 以覆盖方式提供开发环境取值。可通过环境变量覆盖:
所有取值都会绑定到 eIDMongoliaOptions,并通过 DataAnnotations 提前完成校验。
5. 主要流程¶
登录(CitizenAuthService)¶
登记号推送: POST /api/login-notify {register} → {sessionId, vc, pollToken}。
register 可以是登记号 / civil-id / PNOMN-… —— 服务端均可解析。推送至手机
(每个目标限流约 60 秒 3 次)。随后进入轮询。
二维码: POST /api/start {} → {sessionId, qr, deviceLinkBase, vc, pollToken}。
将 qr(==sessionId)渲染为二维码(QRCoder)。
轮询(两种方式通用): GET /api/status?sessionId=&pollToken=。服务端会保持请求约 1 秒后返回,
ViewModel 随后再次轮询。本地会话状态机(server/internal/domain/enums.go):
state = RUNNING(继续)| COMPLETE(终态);endResult = OK | TIMEOUT |
USER_REFUSED* | WRONG_VC | FAILED | ……。返回 OK 时,身份信息
(name、idNumber、certificateLevel)随响应内联返回(不含 bearer 与证书 PEM)→
存入身份缓存。
USB 令牌登录(M9.B)—— 未启用(桩实现)。 InitiateTokenChallengeAsync /
VerifyTokenAsync 返回 ApiError.Internal("token_login_not_available_on_first_party_backend")
—— web 后端没有 token/challenge·token/verify 的对应实现。登录页的 “Token”
选项卡会优雅降级。
登出: 服务端不存在可吊销的会话 / bearer —— 只需清空身份缓存与本地会话存储。
PDF 签名(RpAuthService)—— 服务已就绪,无界面¶
InitiateAsync 在本地计算 PDF 字节的 SHA-256 摘要,并调用
POST /api/sign-pdf-start {etsi, digestB64, fileName, callbackUrl};
PollAsync 通过 /api/status 检查状态(sessionId → pollToken 的对应关系保存在
按会话划分的 ConcurrentDictionary 中)。返回 OK 时 signatureValueB64 会内联返回。
但目前没有任何 ViewModel / Page 调用该服务,且它也未调用用于下载加盖签章 PDF 的
/api/sign-pdf-download(见 §1 的说明)。
控制台 / 组织¶
DashboardService.LoadAsync → /api/dashboard + /api/devices + /api/activity
(personId 取自身份缓存)。OrgService.ListAsync → /api/representations
(已登录公民的 ACTIVE 代表权)。其他组织相关操作均为桩实现。
验证(VerifyPage)¶
在客户端本地解析粘贴的 PEM 或证书文件(ICertificateService),
展示 subject / 有效期 / 有效性 —— 无需后端。
6. USB 令牌 / 智能卡(自包含实现)¶
Windows 客户端通过自有的内部实现(Infrastructure/Tokens/)支持 USB 令牌 / 智能卡。
(USB 令牌登录未启用,因为 first-party 后端没有对应接口 —— 见 §5;
但令牌管理功能完全可用。)
- Windows CNG(
Tokens/Cng/WindowsCngTokenProvider.cs)—— 使用 Microsoft Base Smart Card Crypto Provider,由 Windows 自身弹出 PIN 对话框。 - PKCS#11(
Tokens/Pkcs11/Pkcs11TokenProvider.cs,Pkcs11Interop5.2.0)—— 为每个发现的模块注册一个提供者(OpenSCopensc-pkcs11.dll与 Feitianeps2003csp11.dll可并存)。模块路径依据TokenOptions.Pkcs11ModuleCandidates依次解析(字面量 → System32 → ProgramFiles);Pkcs11ModulePath覆盖项可直接指向某个 DLL。TokenRegistry会按公钥指纹对重复出现的同一张卡去重。
令牌状态机(Domain/Tokens/TokenTypes.cs):Blank → Initialized → Provisioned → Enrolled。
签发路径(初始化 SO PIN → 用户 PIN → 生成密钥对 → CSR)会走到 CertEnrollmentService,
但目前该服务仍是桩实现(§2.2)。
注意 —— 未使用
gerege-token-kit。 macOS 客户端使用 Swift SPM 包desktop/gerege-token-kit/。Windows 客户端并未使用它 —— 其自有的 C# CNG + Pkcs11Interop 技术栈是自包含的。
7. CI / CD¶
Windows 工作流:.github/workflows/windows-app.yml。
当 desktop/windows-app/** 或该工作流发生变更时触发。包含两个 job:
| Job | Runner | 触发条件 | 执行内容 |
|---|---|---|---|
build |
windows-latest(GitHub 托管) |
push main / PR / dispatch |
restore → build(Debug)→ 测试 → 开发证书自签名 → 打包 MSIX 并作为构建产物上传(eid-mongolia-windows-msix) |
selfhosted |
[self-hosted, windows-signing](win11-build,38.180.136.249) |
仅 workflow_dispatch |
build → 测试 → 开发证书自签名 → MSIX → 将已签名的 .msix 与公开 .cer 提交至 deploy/downloads/ → push |
selfhosted job 通过 git 将签名后的安装包发布到 deploy/downloads/
(由于使用 forced-command SSH,scp 不可行,因此 staging 的 ci-deploy 通过
git reset --hard 拉取,并由 nginx 在 /download/ 下提供)。目前使用开发证书自签名;
待取得正式 EV/OV 证书后,将替换 New-DevCert.ps1。
注意 —— 构建现已通过。 旧的“在 macOS/CI 上无法构建”的说法已过时: 在完成
/api/*改造与管线适配后,该解决方案已能在 CI 的 Windows runner 上 完整完成构建、测试与 MSIX 打包。
8. 打包(MSIX / .appinstaller)¶
完整流程:tools/README.md。
简要步骤(tools/*.ps1):
# 1) 每台机器执行一次(需管理员)—— 创建开发用代码签名证书
.\tools\dev-cert\New-DevCert.ps1
# 2) 构建 + 签名 MSIX → artifacts/
.\tools\pack-msix.ps1 -Configuration Release -Platform x64 -Version 0.1.0.0
# 3) 本地安装
Add-AppPackage -Path .\artifacts\eid-mongolia-setup.msix
- MSIX 打包使用 WinApp SDK 的单项目 MSIX MSBuild 工具链
(
dotnet publish -p:EidMsix=true);其内部从 NuGet 包Microsoft.Windows.SDK.BuildTools[.MSIX]获取makeappx/signtool, 因此无需在系统中安装 Windows SDK。pack-msix.ps1会按 PATH → Windows SDK → NuGet BuildTools 的顺序定位 signtool, 并使用signtool sign /tr <timestamp>进行签名。 - 输出:
artifacts/eid-mongolia-setup.msix(固定文件名 —— 供网站下载链接与 CI 使用) artifacts/<Name>_<Version>_<Platform>.msix(带版本号的归档)。publish-appinstaller.ps1—— 渲染.appinstaller模板并配置 Windows 原生自动更新 (启动时检查 URL,退出时静默更新)。- Publisher DN:
CN=Gerege Systems LLC, O=Gerege Systems LLC, C=MN(Package.appxmanifest的<Identity Publisher=…>,MSIX 名称mn.eidmongol.desktop)。 代码签名证书的 subject DN 必须与之完全一致 ——New-DevCert.ps1使用相同的 subject。 生产环境需使用已纳入 Microsoft Trusted Root Program 的 EV/OV 证书 (DigiCert / GlobalSign / Sectigo)。切勿将 PFX 提交到仓库。
9. 下载与安装(终端用户)¶
已签名的 MSIX 与信任证书均可通过 /download/ 获取(nginx
deploy/nginx/staging.conf 的 location ^~ /download/ → deploy/downloads/;
网站下载按钮见 web/src/components/landing/DesktopAppSection.tsx、
web/src/app/solutions/page.tsx):
| 文件 | 链接 |
|---|---|
| Windows 安装包 | https://eidmongolia.mn/download/eid-mongolia-setup.msix |
| 信任用公开证书 | https://eidmongolia.mn/download/eid-mongolia.cer |
当前
.msix使用开发证书自签名。 安装前请将eid-mongolia.cer添加到 Trusted People(或 Trusted Root):下载.cer→ 双击 → Install Certificate → Local Machine → Trusted People。随后双击.msix进行安装。 生产环境取得正式 EV/OV 证书后,无需此手动信任步骤。
10. 安全默认设置¶
来自 SecurityOptions 与 Infrastructure/Security/*:
| 事项 | 机制 |
|---|---|
| TLS | TLS 1.3(启用固定时仅 1.3;否则 1.2+1.3) |
| 证书固定 | SPKI-SHA256(SpkiPinValidator、CertificateSpkiPins);RequireCertificatePinning(dev=false) |
| 密钥材料 | Windows DPAPI 保险库(DataProtectionSecretVault) |
| 生物识别 | Windows Hello(WindowsHelloService)、RequireWindowsHello(prod=true) |
| 空闲锁定 | IdleLockService(IdleTimeoutSeconds,默认 900 秒) |
| 剪贴板 | ClipboardService 自动清空(ClipboardClearSeconds) |
| RASP | RaspService —— 调试 / 篡改探测快照 |
| 日志 | Serilog → Console + Debug + 滚动文件(%LOCALAPPDATA%/eIDMongolia/logs/) |
注意 —— 设备 HMAC 未接入管线。 旧的 gerege
/mobile/*设备 HMAC 签名 (HmacSigner及其测试)仍保留在代码中,但在 first-party 模型下未挂载到 HTTP 管线(§2.2)。
11. 测试¶
tests/eIDMongolia.UnitTests —— xUnit + FluentAssertions + NSubstitute。
目前唯一的真实测试是 Security/HmacSignerTests.cs(外加一个占位测试)——
测试覆盖有限,这与该应用尚属新建相符。CI(windows-app.yml)中通过
dotnet test 运行。
12. 源码(链接)¶
- Windows 应用:
https://github.com/gerege-systems/eid-platform-mn/tree/main/desktop/windows-app - 后端集成映射:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/BACKEND-INTEGRATION.md - HTTP 客户端:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/src/eIDMongolia.Infrastructure/Http/BackendApiClient.cs - 公民登录:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/src/eIDMongolia.Infrastructure/Auth/CitizenAuthService.cs - PDF 签名(服务):
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/src/eIDMongolia.Infrastructure/Auth/RpAuthService.cs - 控制台:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/src/eIDMongolia.Infrastructure/Dashboard/DashboardService.cs - 组织:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/src/eIDMongolia.Infrastructure/Org/OrgService.cs - DI / HttpClient 配置:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/src/eIDMongolia.Infrastructure/DependencyInjection.cs - MSIX 清单:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/src/eIDMongolia.Client/Package.appxmanifest - PKCS#11 令牌:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/src/eIDMongolia.Infrastructure/Tokens/Pkcs11/Pkcs11TokenProvider.cs - 打包:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/tools/README.md - CI 工作流:
https://github.com/gerege-systems/eid-platform-mn/blob/main/.github/workflows/windows-app.yml - macOS 客户端(对比参考):DESKTOP_MACOS.md