Перейти к содержанию

Десктопный клиент для Windows — руководство разработчика

Десктопный клиент для Windows (C# / WinUI 3) платформы eID — приложение, которое выполняет вход по QR-коду или push по регистрационному номеру, управление USB-токенами (смарт-картами), предоставляет панель пользователя, представительство организаций и проверку сертификатов через мобильное приложение e-ID Mongolia. Исходники: 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-секрет, ни 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 готов (RpAuthServicePOST /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 3App.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/v1 gerege остались в исходниках, но не подключены к 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 (TargetPlatformMinVersion 10.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 либо 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 не задан → не упакованный 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-значения через наложение. Переопределение через переменные окружения:

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

Все значения привязываются к 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, Pkcs11Interop 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 использует SPM-пакет на Swift desktop/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=…>, имя MSIX mn.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 CertificateLocal MachineTrusted 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