Десктопный клиент для Windows — руководство разработчика¶
Десктопный клиент для Windows (C# / WinUI 3) платформы eID — приложение, которое выполняет
вход по QR-коду или push по регистрационному номеру, управление USB-токенами (смарт-картами),
предоставляет панель пользователя, представительство организаций и проверку сертификатов через
мобильное приложение e-ID Mongolia. Исходники: 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-секрет, ни RP UUID, ни HMAC-секрет устройства, ни долгоживущий bearer-токен. Все вызовы идут через публичные маршруты/api/*веб-бэкенда (ровно тот же путь, что использует браузер);RP_API_SECRETдля Go RP-API (/v3/*) хранится только на веб-сервере (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) — два активных режима: - Push по регистрационному / civil-ID номеру — на телефон отправляется уведомление,
подтверждаемое PIN1 (
POST /api/login-notify). - QR — отображается QR-код (рендерится через
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).
Примечание — UI подписания PDF пока отсутствует. First-party сервис подписания PDF готов (
RpAuthService→POST /api/sign-pdf-start, дайджест SHA-256 вычисляется им самим), но нет ViewModel / Page, который бы его вызывал —IRpAuthServiceнигде не используется, кроме регистрации в DI, и в боковом меню нет пункта Sign. Кроме того,RpAuthServiceне вызывает/api/sign-pdf-download, который скачивает проштампованный PDF (он получает только сырую подпись). Таким образом, на стороне Windows полного сценария подписания PDF пока нет — сквозной сценарий работает только в клиенте macOS. Вход по USB-токену также неактивен (§5, заглушка).
2. Архитектура¶
2.1. Слои Clean Architecture¶
Решение состоит из четырёх слоёв исходников + точка входа WinUI + тесты (eIDMongolia.Desktop.sln):
| Проект | Роль | Зависимости |
|---|---|---|
eIDMongolia.Domain |
Сущности, value-объекты, Result<T>, ApiError, типы токенов. Без внешних зависимостей. |
— |
eIDMongolia.Application |
Абстракции use-case (IBackendApi, ICitizenAuthService, ICryptoTokenProvider, …), eIDMongoliaOptions. |
Domain |
eIDMongolia.Infrastructure |
HTTP-клиент, pinning сертификатов, хранилище 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/*, bootstrap 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— это слой UI/точки входа WinUI 3. Собственно HTTP-клиент находится вInfrastructure/Http/BackendApiClient.csиInfrastructure/Auth/*Service.cs.
Хост: Generic Host из Microsoft.Extensions.Hosting — DI, Configuration, Options, Serilog.
Bootstrap: Client/Hosting/AppHost.cs.
2.2. Подключение к бэкенду — first-party /api/*¶
Infrastructure/DependencyInjection.cs настраивает именованный HttpClient
(eIDMongolia.Backend): BaseUrl (origin веб-приложения), TLS 1.3, первичный обработчик с pinning
сертификата, затем AddStandardResilienceHandler (retry на Polly). Подписант device-HMAC и
обработчик подстановки Bearer в конвейер не подключены — в first-party модели они не нужны.
Клиент вызывает маршруты web/src/app/api/* (Next.js) напрямую, как браузер:
| Маршрут | Метод | Назначение | Сервис |
|---|---|---|---|
/api/health |
GET | Состояние сервера (live=ready) | BackendApiClient |
/api/login-notify |
POST | Push-вход по регистрационному / civil-ID номеру | CitizenAuthService |
/api/start |
POST | Старт входа по QR | CitizenAuthService |
/api/status |
GET | Long-poll (?sessionId=&pollToken=) |
CitizenAuthService, RpAuthService |
/api/sign-pdf-start |
POST | Старт подписания дайджеста PDF (сервис готов, UI нет) | RpAuthService |
/api/dashboard |
POST | {personId} → счётчики сертификатов/устройств/активности + имя |
DashboardService |
/api/devices |
POST | {personId} → зарегистрированные устройства |
DashboardService |
/api/activity |
POST | {personId} → история сессий в рамках RP |
DashboardService |
/api/representations |
POST | {personId} → организации в статусе ACTIVE, которые можно представлять |
OrgService |
Эти маршруты /api/* проксируются на веб-сервере к /v3/* Go RP-API с first-party RP Bearer
(personId = civil_id / etsi вошедшего гражданина, из кеша идентичности). pollToken выдаётся
только инициатору сессии и необходим, чтобы получить персональные данные (имя / civil-ID номер)
из /api/status.
Мост идентичности (решение проблемы несоответствия моделей).
/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"), и UI деградирует корректно):
- Вход по USB-токену (
CitizenAuthService.InitiateTokenChallengeAsync/VerifyTokenAsync) — на веб-бэкенде нетtoken/challenge·token/verify. - Выпуск сертификата (
CertEnrollmentService). - Операции ЗАПИСИ по организациям (lookup / register / members / name-en / X-Road CSR в
OrgService) — реальный толькоListAsync.
Старые обработчики оставлены в дереве. Классы device-HMAC (
HmacSigningHandler) и подстановки Bearer (BearerAuthHandler) из/web2app/v1gerege остались в исходниках, но не подключены к HTTP-конвейеру — сохранены как ориентир для возможной будущей сборки с прямым бэкендом. Ничто от них не зависит.
Конфигурация читается из eIDMongoliaOptions (appsettings.json + наложение
appsettings.{ENV}.json + переменные окружения с префиксом EIDMNG_). В продакшене
BaseUrl=https://eidmongolia.mn; в dev-наложении (appsettings.Development.json) —
BaseUrl=http://localhost:3000, RequireCertificatePinning=false, RequireWindowsHello=false.
CertificateSpkiPins и XRoadClient в first-party модели пусты / null.
3. Требования¶
Подтверждено по коду (Directory.Packages.props, *.csproj):
- .NET 8 (LTS), последняя версия C# (12);
TreatWarningsAsErrors=true, включены nullable и implicit usings. - TFM проекта
eIDMongolia.Client: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 application development;
либо CLI
dotnet. - Платформы:
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не задан → не упакованный single-file exe (dev,dotnet run).EidMsix=true→ упакованный MSIX (publish /pack-msix.ps1, §8).
Примечание — сборка и запуск только на Windows. Слой Infrastructure использует Windows TFM (WinUI, CNG, DPAPI, PKCS#11), поэтому на macOS/Linux он не собирается. В CI полная сборка + тесты + упаковка MSIX выполняются на Windows-раннере (§7).
Конфигурация¶
src/eIDMongolia.Client/appsettings.json — канонический файл конфигурации;
appsettings.Development.json задаёт dev-значения через наложение. Переопределение через
переменные окружения:
Все значения привязываются к eIDMongoliaOptions и проверяются заранее через DataAnnotations.
5. Основные сценарии¶
Вход (CitizenAuthService)¶
Push по регистрационному номеру: POST /api/login-notify {register} →
{sessionId, vc, pollToken}. register может быть регистрационным номером, civil-id либо
PNOMN-… — сервер распознаёт любой из них. Push на телефон (rate limit ~3 за 60 с на цель).
Затем опрос.
QR: POST /api/start {} → {sessionId, qr, deviceLinkBase, vc, pollToken}.
Отобразите qr (==sessionId) в виде QR-кода (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) приходит inline (без bearer и PEM
сертификата) → сохраняется в кеш идентичности.
Вход по USB-токену (M9.B) — неактивен (заглушка). InitiateTokenChallengeAsync /
VerifyTokenAsync возвращают
ApiError.Internal("token_login_not_available_on_first_party_backend") — на веб-бэкенде нет
эквивалента token/challenge·token/verify. Вкладка входа «Token» деградирует корректно.
Выход: на стороне сервера нет сессии/bearer, которые можно отозвать — очищаются только кеш идентичности и локальное хранилище сессии.
Подписание PDF (RpAuthService) — сервис готов, UI нет¶
InitiateAsync вычисляет дайджест SHA-256 байтов PDF локально и вызывает
POST /api/sign-pdf-start {etsi, digestB64, fileName, callbackUrl}; PollAsync проверяет
состояние через /api/status (пара sessionId → pollToken хранится в ConcurrentDictionary
на каждую сессию). При OK signatureValueB64 приходит inline. Однако сейчас нет ни ViewModel,
ни Page, вызывающих этот сервис, и он не вызывает /api/sign-pdf-download, который скачивает
проштампованный PDF (см. примечание в §1).
Панель / Организации¶
DashboardService.LoadAsync → /api/dashboard + /api/devices + /api/activity
(personId из кеша идентичности). OrgService.ListAsync → /api/representations
(активные представительства вошедшего гражданина). Остальные операции по организациям — заглушки.
Проверка (VerifyPage)¶
Разбор сертификата из вставленного PEM или файла на стороне клиента (ICertificateService)
и отображение subject / срока действия / валидности — бэкенд не требуется.
6. USB-токен / смарт-карта (самодостаточный стек)¶
Клиент для Windows поддерживает USB-токены и смарт-карты через собственную внутреннюю реализацию
(Infrastructure/Tokens/). (Вход по USB-токену неактивен, поскольку на first-party бэкенде нет
эквивалента — §5; но управление токенами работает полностью.)
- Windows CNG (
Tokens/Cng/WindowsCngTokenProvider.cs) — использует Microsoft Base Smart Card Crypto Provider, диалог PIN показывает сама Windows. - 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 использует SPM-пакет на Swiftdesktop/gerege-token-kit/. Клиент для Windows его не использует — его собственный стек на C# (CNG + Pkcs11Interop) самодостаточен.
7. CI / CD¶
Workflow для Windows: .github/workflows/windows-app.yml.
Запускается при изменениях в desktop/windows-app/** либо самого workflow. Две задачи:
| Задача | Раннер | Триггер | Что делает |
|---|---|---|---|
build |
windows-latest (GitHub-hosted) |
push в main / PR / dispatch |
restore → build (Debug) → тесты → самоподпись dev-сертификатом → упаковка MSIX и выгрузка как артефакт (eid-mongolia-windows-msix) |
selfhosted |
[self-hosted, windows-signing] (win11-build, 38.180.136.249) |
только workflow_dispatch |
build → тесты → самоподпись dev-сертификатом → MSIX → коммит подписанного .msix и публичного .cer в deploy/downloads/ → push |
Задача selfhosted публикует подписанный пакет в deploy/downloads/ через git (scp невозможен
из-за SSH с forced-command, поэтому staging ci-deploy забирает его через git reset --hard,
а nginx отдаёт по /download/). Сейчас используется самоподпись dev-сертификатом; когда появится
настоящий EV/OV-сертификат, New-DevCert.ps1 будет заменён.
Примечание — сборка теперь зелёная. Старое состояние «не собирается на macOS/CI» устарело: после завершения перехода на
/api/*и адаптации конвейера решение полностью собирается, проходит тесты и упаковывается в MSIX на Windows-раннере CI.
8. Упаковка (MSIX / .appinstaller)¶
Полный конвейер: tools/README.md.
Кратко (tools/*.ps1):
# 1) Один раз на машину (от админа) — создать dev-сертификат подписи кода
.\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 выполняется средствами single-project MSIX MSBuild из WinApp SDK
(
dotnet publish -p:EidMsix=true); внутриmakeappx/signtoolберутся из NuGet-пакетаMicrosoft.Windows.SDK.BuildTools[.MSIX], поэтому системная установка Windows SDK не требуется.pack-msix.ps1ищет signtool в порядке PATH → Windows SDK → NuGet BuildTools и подписывает командой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=…>, имя MSIXmn.eidmongol.desktop). Subject DN сертификата подписи кода должен в точности совпадать с этим —New-DevCert.ps1использует тот же subject. В продакшене применяется сертификат EV/OV, включённый в Microsoft Trusted Root Program (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самоподписан dev-сертификатом. Перед установкой добавьте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 при активном pinning; иначе 1.2+1.3) |
| Pinning сертификата | 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 + rolling-файл (%LOCALAPPDATA%/eIDMongolia/logs/) |
Примечание — device-HMAC не подключён к конвейеру. Старая подпись device-HMAC для
/mobile/*из gerege (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 - Workflow CI:
https://github.com/gerege-systems/eid-platform-mn/blob/main/.github/workflows/windows-app.yml - Клиент macOS (для сравнения): DESKTOP_MACOS.md