跳转至

Windows 桌面客户端 —— 开发者指南

eID 平台的 Windows(C# / WinUI 3)桌面客户端 —— 该应用通过 e-ID Mongolia 手机应用实现 二维码 / 登记号推送登录USB 令牌(智能卡)管理、用户控制台、组织代表权 以及证书验证。源码:desktop/windows-app/(Clean Architecture,.NET 解决方案 eIDMongolia.Desktop.sln)。

First-party 客户端 —— 改造已完成。 该客户端从 gerege v1 桌面端导入, 并已改品牌为 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 / LoginViewModelCitizenAuthService)—— 两种可用方式:
  • 按登记号 / 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 签名服务已就绪 (RpAuthServicePOST /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 用例抽象(IBackendApiICitizenAuthServiceICryptoTokenProvider 等)、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.xamlMainWindowViews/Pages/*、Generic Host 引导(Hosting/AppHost.cs)。 以上全部层
eIDMongolia.UnitTests xUnit 测试。 所有 src 项目

引用关系: Client → Presentation、Infrastructure、Application、DomainPresentation → Application、DomainInfrastructure → Application、DomainApplication → Domain

澄清 —— Client 并不是 HTTP 客户端。 尽管名称如此,eIDMongolia.Client 是 WinUI 3 的 UI / 入口层。真正的 HTTP 客户端位于 Infrastructure/Http/BackendApiClient.csInfrastructure/Auth/*Service.cs

宿主:Microsoft.Extensions.Hosting 的 Generic Host —— DI、Configuration、Options、Serilog。 引导入口:Client/Hosting/AppHost.cs

2.2. 连接后端 —— first-party /api/*

Infrastructure/DependencyInjection.cs 配置了命名 HttpClienteIDMongolia.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= CitizenAuthServiceRpAuthService
/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); UserProfileServiceDashboardServiceOrgService 从该缓存读取 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 管线 —— 作为未来直连后端构建的参考保留。没有任何代码依赖它们。

配置读取自 eIDMongoliaOptionsappsettings.json + appsettings.{ENV}.json 覆盖 + 以 EIDMNG_ 为前缀的环境变量)。生产环境 BaseUrl=https://eidmongolia.mn; dev 覆盖文件(appsettings.Development.json)中为 BaseUrl=http://localhost:3000RequireCertificatePinning=falseRequireWindowsHello=false。在 first-party 模型下, CertificateSpkiPinsXRoadClient 为空 / null。

3. 前置条件

依据源码确认(Directory.Packages.props*.csproj):

  • .NET 8(LTS)、最新版 C#(12);TreatWarningsAsErrors=true,启用 nullable 与 implicit usings。
  • eIDMongolia.Client 的 TFM:net8.0-windows10.0.26100.0TargetPlatformMinVersion 10.0.17763.0)—— 即 Windows 10 1809+, 构建需要 Windows 11 SDK(26100)。
  • Windows App SDK 2.0.1 + WinUI 3UseWinUI=true)。
  • Visual Studio 2022 17.10+,安装 Windows 应用开发 工作负载;或使用 dotnet CLI。
  • 平台:x86 / x64 / ARM64
  • 若要使用 USB 令牌,主机上需安装 PKCS#11 模块(OpenSC opensc-pkcs11.dll 或 Feitian eps2003csp11.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 以覆盖方式提供开发环境取值。可通过环境变量覆盖:

$env:EIDMNG_eIDMongolia__Backend__BaseUrl = "https://<staging>"

所有取值都会绑定到 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 时,身份信息 (nameidNumbercertificateLevel)随响应内联返回(不含 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 中)。返回 OKsignatureValueB64 会内联返回。 但目前没有任何 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 CNGTokens/Cng/WindowsCngTokenProvider.cs)—— 使用 Microsoft Base Smart Card Crypto Provider,由 Windows 自身弹出 PIN 对话框。
  • PKCS#11Tokens/Pkcs11/Pkcs11TokenProvider.csPkcs11Interop 5.2.0)—— 为每个发现的模块注册一个提供者(OpenSC opensc-pkcs11.dll 与 Feitian eps2003csp11.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 SDKpack-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=MNPackage.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.conflocation ^~ /download/deploy/downloads/; 网站下载按钮见 web/src/components/landing/DesktopAppSection.tsxweb/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 CertificateLocal MachineTrusted People。随后双击 .msix 进行安装。 生产环境取得正式 EV/OV 证书后,无需此手动信任步骤。

10. 安全默认设置

来自 SecurityOptionsInfrastructure/Security/*

事项 机制
TLS TLS 1.3(启用固定时仅 1.3;否则 1.2+1.3)
证书固定 SPKI-SHA256(SpkiPinValidatorCertificateSpkiPins);RequireCertificatePinning(dev=false)
密钥材料 Windows DPAPI 保险库(DataProtectionSecretVault
生物识别 Windows Hello(WindowsHelloService)、RequireWindowsHello(prod=true)
空闲锁定 IdleLockServiceIdleTimeoutSeconds,默认 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