Skip to content

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 gerege v1 desktop and rebranded to eID Mongolia (eIDMongolia.* namespace, MSIX identity mn.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/*)'s RP_API_SECRET is 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 (RpAuthServicePOST /api/sign-pdf-start, computing the PDF's SHA-256 digest itself), but there is no ViewModel / Page that calls it — IRpAuthService is not used anywhere beyond being registered in DI, and there is no Sign menu in the sidebar. Also, RpAuthService does 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 pointApp.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 — Client is NOT the HTTP client. Despite the name, eIDMongolia.Client is the WinUI 3 UI/entry point layer. The actual HTTP client lives in Infrastructure/Http/BackendApiClient.cs and Infrastructure/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/status does not return a session_token / user_id; on successful login, name + idNumber come directly from the cert subject. So CitizenAuthService.PollAsync stores this identity in ISessionIdentityCache and generates a synthetic SessionToken(=sessionId) + UserId(GUID) per session; UserProfileService, DashboardService, and OrgService read the personId from this cache (there is no /me endpoint).

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 no token/challenge·token/verify on the web backend.
  • Certificate enroll (CertEnrollmentService).
  • Organization WRITE operations (OrgService's lookup / register / members / name-en / X-Road CSR) — only ListAsync is real.

Old handlers left in the tree. gerege's /web2app/v1 device-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.Client TFM: net8.0-windows10.0.26100.0 (TargetPlatformMinVersion 10.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 dotnet CLI.
  • Platform: x86 / x64 / ARM64.
  • To use USB tokens, a PKCS#11 module must be installed on the host (OpenSC opensc-pkcs11.dll or Feitian eps2003csp11.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):

  • EidMsix not set → unpackaged single-file exe (dev, dotnet run).
  • EidMsix=truepackaged 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:

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

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, Pkcs11Interop 5.2.0) — registers one provider per discovered module (OpenSC opensc-pkcs11.dll + Feitian eps2003csp11.dll side by side). The module path is resolved from TokenOptions.Pkcs11ModuleCandidates (in order: literal → System32 → ProgramFiles); the Pkcs11ModulePath override can point to a single DLL. TokenRegistry dedupes 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 package desktop/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 gets makeappx/signtool from the Microsoft.Windows.SDK.BuildTools[.MSIX] NuGet package, so no system-wide Windows SDK installation is required. pack-msix.ps1 resolves signtool in the order PATH → Windows SDK → NuGet BuildTools and signs with signtool 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 .appinstaller template 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 Name mn.eidmongol.desktop). The code-signing cert's subject DN must match this exactlyNew-DevCert.ps1 uses 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 .msix is self-signed with a dev-cert. Before installing, trust eid-mongolia.cer in Trusted People (or Trusted Root): download the .cer → double-click → Install CertificateLocal MachineTrusted People. Then double-click the .msix to 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.

  • 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