Windows Desktop client — Developer guide¶
The eID platform's Windows (C# / WinUI 3) desktop client — a desktop
application that performs QR / registration-number push login, USB token
(smart card) management, a user dashboard, organization representation, and
certificate verification via the e-ID Mongolia mobile app. Source:
desktop/windows-app/ (Clean Architecture, .NET solution
eIDMongolia.Desktop.sln).
First-party client — rewire complete. This client was imported from
geregev1 desktop and rebranded to eID Mongolia (eIDMongolia.*namespace, MSIX identitymn.eidmongol.desktop). Following exactly the same principle as the macOS client (DESKTOP_MACOS.md) and the iOS app, this is not an RP — the client holds no RP secret, RP UUID, device HMAC secret, or long-lived bearer token. Every call goes through the web backend's public/api/*routes (the exact same path a browser uses); the Go RP-API (/v3/*)'sRP_API_SECRETis held only by the web server (web/src/lib/rpclient.ts). The work of migrating from the old gerege direct-backend (/web2app/v1,/rp/v1, device-HMAC, Bearer session) model to/api/*is complete; detailed mapping:desktop/windows-app/BACKEND-INTEGRATION.md.
1. What it does¶
Capabilities confirmed from the code (Views/Pages/*, Infrastructure/*).
After login, the sidebar shows Home · Dashboard · Organizations · Tokens ·
Verify menus (before login, only Login) — ShellViewModel.BuildMenu():
- Login (
LoginPage/LoginViewModel,CitizenAuthService) — two active modes: - Push by registration / civil-ID number — sends a notification to the
phone, confirmed with PIN1 (
POST /api/login-notify). - QR — displays a QR code (rendered with
QRCoder) to be scanned by the phone (POST /api/start). - USB token management (
TokensPage/TokenDetailPage) — detect connected smart cards / tokens, view state (Blank/Initialized/Provisioned/Enrolled), generate keys, PIN management. The token stack is self-contained (Windows CNG + PKCS#11, see §6). - Dashboard (
DashboardPage/DashboardService) — the logged-in citizen's certificate / device / activity counts and name (/api/dashboard,/api/devices,/api/activity). - Organizations (
OrganizationsPage/OrgService) — the list of ACTIVE organizations the logged-in citizen can represent (/api/representations). - Verify (
VerifyPage/VerifyViewModel) — parse a certificate from a PEM paste or file on the client side (ICertificateService.ParsePem/ParseFile) and display subject / validity period / validity. No backend call (same as macOS).
Note — no PDF-signing UI at present. The first-party PDF-signing service is ready (
RpAuthService→POST /api/sign-pdf-start, computing the PDF's SHA-256 digest itself), but there is no ViewModel / Page that calls it —IRpAuthServiceis not used anywhere beyond being registered in DI, and there is no Sign menu in the sidebar. Also,RpAuthServicedoes not call/api/sign-pdf-download, which downloads the stamped PDF (it only gets the raw signature). Thus, on the Windows side there is no complete flow at present for signing a PDF — only the macOS client works end to end. USB-token login is also inactive (§5, stub).
2. Architecture¶
2.1. Clean Architecture layers¶
The solution consists of four source layers + a WinUI entry point + tests
(eIDMongolia.Desktop.sln):
| Project | Role | Dependencies |
|---|---|---|
eIDMongolia.Domain |
Entities, value objects, Result<T>, ApiError, token types. No external dependencies. |
— |
eIDMongolia.Application |
Use-case abstractions (IBackendApi, ICitizenAuthService, ICryptoTokenProvider, …), eIDMongoliaOptions. |
Domain |
eIDMongolia.Infrastructure |
HTTP client, cert pinning, DPAPI vault, Windows Hello, PKCS#11 / CNG token provider. Windows TFM. | Application, Domain |
eIDMongolia.Presentation |
ViewModels (CommunityToolkit.Mvvm), navigation, validation. Independent of the UI framework. | Application, Domain |
eIDMongolia.Client |
WinUI 3 entry point — App.xaml, MainWindow, Views/Pages/*, Generic Host bootstrap (Hosting/AppHost.cs). |
all layers above |
eIDMongolia.UnitTests |
xUnit tests. | all src projects |
Reference graph: Client → Presentation, Infrastructure, Application,
Domain; Presentation → Application, Domain; Infrastructure →
Application, Domain; Application → Domain.
Clarification —
Clientis NOT the HTTP client. Despite the name,eIDMongolia.Clientis the WinUI 3 UI/entry point layer. The actual HTTP client lives inInfrastructure/Http/BackendApiClient.csandInfrastructure/Auth/*Service.cs.
Host: Microsoft.Extensions.Hosting Generic Host — DI, Configuration, Options,
Serilog. Bootstrap Client/Hosting/AppHost.cs.
2.2. Connecting to the backend — first-party /api/*¶
Infrastructure/DependencyInjection.cs configures a named HttpClient
(eIDMongolia.Backend): BaseUrl (web origin), TLS 1.3, a cert-pinning primary
handler, then AddStandardResilienceHandler (Polly retry). The device-HMAC
signer and Bearer-attach handler are not wired into the pipeline — not needed
in the first-party model. The client calls the web/src/app/api/* (Next.js)
routes directly like a browser:
| Route | Method | Purpose | Service |
|---|---|---|---|
/api/health |
GET | Server health (live=ready) | BackendApiClient |
/api/login-notify |
POST | Push login by registration/civil-ID number | CitizenAuthService |
/api/start |
POST | Start QR login | CitizenAuthService |
/api/status |
GET | Long-poll (?sessionId=&pollToken=) |
CitizenAuthService, RpAuthService |
/api/sign-pdf-start |
POST | Start signing a PDF digest (service ready, no UI) | RpAuthService |
/api/dashboard |
POST | {personId} → cert/device/activity counts + name |
DashboardService |
/api/devices |
POST | {personId} → registered devices |
DashboardService |
/api/activity |
POST | {personId} → RP-scoped session history |
DashboardService |
/api/representations |
POST | {personId} → ACTIVE organizations that can be represented |
OrgService |
These /api/* routes are proxied on the web server to the Go RP-API's /v3/*
with the first-party RP Bearer (personId = the logged-in citizen's
civil_id / etsi, from the identity cache). pollToken is issued only to the
session initiator and is required to obtain PII (name / civil-ID number) from
/api/status.
Identity bridge (solving the impedance mismatch).
/api/statusdoes not return asession_token/user_id; on successful login,name+idNumbercome directly from the cert subject. SoCitizenAuthService.PollAsyncstores this identity inISessionIdentityCacheand generates a syntheticSessionToken(=sessionId) +UserId(GUID) per session;UserProfileService,DashboardService, andOrgServiceread the personId from this cache (there is no/meendpoint).
Since there is not yet an equivalent /api/* route, an honest stub (not a
crash — returns ApiError.Internal("…not_available_on_first_party_backend") and
the UI degrades cleanly):
- USB-token login (
CitizenAuthService.InitiateTokenChallengeAsync/VerifyTokenAsync) — there is notoken/challenge·token/verifyon the web backend. - Certificate enroll (
CertEnrollmentService). - Organization WRITE operations (
OrgService's lookup / register / members / name-en / X-Road CSR) — onlyListAsyncis real.
Old handlers left in the tree. gerege's
/web2app/v1device-HMAC (HmacSigningHandler) and Bearer-attach (BearerAuthHandler) classes remain in the source tree but are not wired into the HTTP pipeline — kept as a reference for a future direct-backend build. Nothing depends on them.
Configuration is read from eIDMongoliaOptions (appsettings.json +
appsettings.{ENV}.json overlay + EIDMNG_-prefixed env). In prod
BaseUrl=https://eidmongolia.mn; in the dev overlay
(appsettings.Development.json), BaseUrl=http://localhost:3000,
RequireCertificatePinning=false, RequireWindowsHello=false.
CertificateSpkiPins and XRoadClient are empty / null in the first-party
model.
3. Prerequisites¶
Confirmed from the code (Directory.Packages.props, *.csproj):
- .NET 8 (LTS), C# latest (12);
TreatWarningsAsErrors=true, nullable + implicit usings enabled. eIDMongolia.ClientTFM:net8.0-windows10.0.26100.0(TargetPlatformMinVersion10.0.17763.0) — in other words Windows 10 1809+, with the Windows 11 SDK (26100) required to build.- Windows App SDK 2.0.1 + WinUI 3 (
UseWinUI=true). - Visual Studio 2022 17.10+, the Windows application development workload;
or the
dotnetCLI. - Platform:
x86/x64/ARM64. - To use USB tokens, a PKCS#11 module must be installed on the host (OpenSC
opensc-pkcs11.dllor Feitianeps2003csp11.dll) — not required; without it only the token section becomes inactive.
4. Build and run¶
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
In Visual Studio: open eIDMongolia.Desktop.sln, set eIDMongolia.Client as
the startup project, and press F5.
Two execution modes (eIDMongolia.Client.csproj):
EidMsixnot set → unpackaged single-file exe (dev,dotnet run).EidMsix=true→ packaged MSIX (publish /pack-msix.ps1, §8).
Note — build/run only on Windows. The Infrastructure layer uses a Windows TFM (WinUI, CNG, DPAPI, PKCS#11), so it does not build on macOS/Linux. In CI, a full build + test + MSIX packaging is done on a Windows runner (§7).
Configuration¶
src/eIDMongolia.Client/appsettings.json is the canonical configuration;
appsettings.Development.json provides dev values via overlay. Override via env:
All values are bound to eIDMongoliaOptions and validated up front via
DataAnnotations.
5. Main flows¶
Login (CitizenAuthService)¶
Registration-number push: POST /api/login-notify {register} →
{sessionId, vc, pollToken}. register may be a registration number /
civil-id / PNOMN-… — the server resolves any of them. Push to the phone
(rate limit ~3/60s per target). Then poll.
QR: POST /api/start {} → {sessionId, qr, deviceLinkBase, vc, pollToken}.
Display qr (==sessionId) as a QR (QRCoder).
Poll (common to both): GET /api/status?sessionId=&pollToken=. The server
holds and returns after ~1s, and the ViewModel polls again. Local session state
machine (server/internal/domain/enums.go): state = RUNNING (continue) |
COMPLETE (terminal); endResult = OK | TIMEOUT | USER_REFUSED* |
WRONG_VC | FAILED | …. On OK, the identity (name, idNumber,
certificateLevel) comes inline (no bearer / cert PEM) → stored in the identity
cache.
USB-token login (M9.B) — inactive (stub). InitiateTokenChallengeAsync /
VerifyTokenAsync return
ApiError.Internal("token_login_not_available_on_first_party_backend") — there
is no token/challenge·token/verify equivalent on the web backend. The
"Token" login tab degrades cleanly.
Logout: there is no session/bearer to revoke on the server side — only the identity cache + local session store are cleared.
PDF signing (RpAuthService) — service ready, no UI¶
InitiateAsync computes the SHA-256 digest of the PDF bytes locally and
calls POST /api/sign-pdf-start {etsi, digestB64, fileName, callbackUrl};
PollAsync checks the state via /api/status (threading the sessionId →
pollToken through a per-session ConcurrentDictionary). On OK,
signatureValueB64 comes inline. But there is no ViewModel / Page that calls
this service at present, and it does not call /api/sign-pdf-download, which
downloads the stamped PDF (see the §1 note).
Dashboard / Organizations¶
DashboardService.LoadAsync → /api/dashboard + /api/devices +
/api/activity (personId from the identity cache). OrgService.ListAsync →
/api/representations (the logged-in citizen's ACTIVE representations). Other
org operations are stubs.
Verify (VerifyPage)¶
Parse a certificate from a PEM paste or file on the client side
(ICertificateService) and display subject / validity period / validity — no
backend required.
6. USB token / smart card (self-contained)¶
The Windows client supports USB tokens / smart cards through its own internal
implementation (Infrastructure/Tokens/). (USB-token login is inactive
because it has no equivalent on the first-party backend — §5; but token
management works fully.)
- Windows CNG (
Tokens/Cng/WindowsCngTokenProvider.cs) — uses the Microsoft Base Smart Card Crypto Provider, with Windows itself showing the PIN dialog. - PKCS#11 (
Tokens/Pkcs11/Pkcs11TokenProvider.cs,Pkcs11Interop5.2.0) — registers one provider per discovered module (OpenSCopensc-pkcs11.dll+ Feitianeps2003csp11.dllside by side). The module path is resolved fromTokenOptions.Pkcs11ModuleCandidates(in order: literal → System32 → ProgramFiles); thePkcs11ModulePathoverride can point to a single DLL.TokenRegistrydedupes the same card seen twice by public key fingerprint.
Token state machine (Domain/Tokens/TokenTypes.cs): Blank → Initialized →
Provisioned → Enrolled. The enroll path (SO PIN init → user PIN → keypair →
CSR) reaches CertEnrollmentService, but that is currently a stub (§2.2).
Note — not connected to
gerege-token-kit. The macOS client uses the Swift SPM packagedesktop/gerege-token-kit/. The Windows client does not use it — its own C# CNG + Pkcs11Interop stack is self-contained.
7. CI / CD¶
Windows workflow: .github/workflows/windows-app.yml.
Runs when desktop/windows-app/** or the workflow changes. Two jobs:
| Job | Runner | Trigger | What it does |
|---|---|---|---|
build |
windows-latest (GitHub-hosted) |
push main / PR / dispatch |
restore → build (Debug) → test → dev-cert self-sign → package MSIX and upload as artifact (eid-mongolia-windows-msix) |
selfhosted |
[self-hosted, windows-signing] (win11-build, 38.180.136.249) |
only workflow_dispatch |
build → test → dev-cert self-sign → MSIX → commit signed .msix + public .cer to deploy/downloads/ → push |
The selfhosted job publishes the signed package to deploy/downloads/ via
git (scp is impossible due to the forced-command SSH, so staging ci-deploy
pulls it via git reset --hard and nginx serves it under /download/). It is
currently self-signed with a dev-cert; when a real EV/OV cert arrives,
New-DevCert.ps1 is replaced.
Note — build is now green. The old "does not build on macOS/CI" state is outdated: after the
/api/*rewire + pipeline adaptation was complete, the solution builds + tests + packages MSIX fully on the CI Windows runner.
8. Packaging (MSIX / .appinstaller)¶
Full pipeline: tools/README.md.
In brief (tools/*.ps1):
# 1) Once per machine (admin) — create the dev code-signing cert
.\tools\dev-cert\New-DevCert.ps1
# 2) Build + sign MSIX → artifacts/
.\tools\pack-msix.ps1 -Configuration Release -Platform x64 -Version 0.1.0.0
# 3) Install locally
Add-AppPackage -Path .\artifacts\eid-mongolia-setup.msix
- MSIX packaging is done with the WinApp SDK single-project MSIX MSBuild
tooling (
dotnet publish -p:EidMsix=true); internally it getsmakeappx/signtoolfrom theMicrosoft.Windows.SDK.BuildTools[.MSIX]NuGet package, so no system-wide Windows SDK installation is required.pack-msix.ps1resolves signtool in the order PATH → Windows SDK → NuGet BuildTools and signs withsigntool sign /tr <timestamp>. - Output:
artifacts/eid-mongolia-setup.msix(fixed name — used by the web download link and in CI) +artifacts/<Name>_<Version>_<Platform>.msix(versioned archive). publish-appinstaller.ps1— renders the.appinstallertemplate and configures Windows-native auto-update (checks the URL at launch and updates silently on exit).- Publisher DN:
CN=Gerege Systems LLC, O=Gerege Systems LLC, C=MN(Package.appxmanifest<Identity Publisher=…>, MSIX Namemn.eidmongol.desktop). The code-signing cert's subject DN must match this exactly —New-DevCert.ps1uses the same subject. In production an EV/OV cert included in the Microsoft Trusted Root Program (DigiCert / GlobalSign / Sectigo) is used. Never commit the PFX.
9. Download and install (end user)¶
The signed MSIX + trust cert are live under /download/ (nginx
deploy/nginx/staging.conf location ^~ /download/ → deploy/downloads/; the
web download button web/src/components/landing/DesktopAppSection.tsx,
web/src/app/solutions/page.tsx):
| File | Web link |
|---|---|
| Windows installer | https://eidmongolia.mn/download/eid-mongolia-setup.msix |
| Trust public cert | https://eidmongolia.mn/download/eid-mongolia.cer |
The current
.msixis self-signed with a dev-cert. Before installing, trusteid-mongolia.cerin Trusted People (or Trusted Root): download the.cer→ double-click → Install Certificate → Local Machine → Trusted People. Then double-click the.msixto install. In production, when a real EV/OV cert arrives, this manual trust step is not required.
10. Security defaults¶
From SecurityOptions + Infrastructure/Security/*:
| Concern | Mechanism |
|---|---|
| TLS | TLS 1.3 (only 1.3 when pinning is active; otherwise 1.2+1.3) |
| Cert pinning | SPKI-SHA256 (SpkiPinValidator, CertificateSpkiPins); RequireCertificatePinning (dev=false) |
| Secrets | Windows DPAPI vault (DataProtectionSecretVault) |
| Biometric | Windows Hello (WindowsHelloService), RequireWindowsHello (prod=true) |
| Idle lock | IdleLockService (IdleTimeoutSeconds, default 900s) |
| Clipboard | ClipboardService auto-clear (ClipboardClearSeconds) |
| RASP | RaspService — debug/tamper probe snapshot |
| Logging | Serilog → Console + Debug + rolling file (%LOCALAPPDATA%/eIDMongolia/logs/) |
Note — device-HMAC not wired into the pipeline. The old gerege
/mobile/*device-HMAC signing (HmacSigner+ tests) remains in the code but is not attached to the HTTP pipeline in the first-party model (§2.2).
11. Tests¶
tests/eIDMongolia.UnitTests — xUnit + FluentAssertions + NSubstitute. Currently
the only real test is Security/HmacSignerTests.cs (+ a placeholder) — test
coverage is limited, reflecting that the app is new. Run in CI
(windows-app.yml) via dotnet test.
12. Source (links)¶
- Windows app:
https://github.com/gerege-systems/eid-platform-mn/tree/main/desktop/windows-app - Backend integration mapping:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/BACKEND-INTEGRATION.md - HTTP client:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/src/eIDMongolia.Infrastructure/Http/BackendApiClient.cs - Citizen login:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/src/eIDMongolia.Infrastructure/Auth/CitizenAuthService.cs - PDF signing (service):
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/src/eIDMongolia.Infrastructure/Auth/RpAuthService.cs - Dashboard:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/src/eIDMongolia.Infrastructure/Dashboard/DashboardService.cs - Organizations:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/src/eIDMongolia.Infrastructure/Org/OrgService.cs - DI / HttpClient configuration:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/src/eIDMongolia.Infrastructure/DependencyInjection.cs - MSIX manifest:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/src/eIDMongolia.Client/Package.appxmanifest - PKCS#11 token:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/src/eIDMongolia.Infrastructure/Tokens/Pkcs11/Pkcs11TokenProvider.cs - Packaging:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/windows-app/tools/README.md - CI workflow:
https://github.com/gerege-systems/eid-platform-mn/blob/main/.github/workflows/windows-app.yml - macOS client (comparison): DESKTOP_MACOS.md