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

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

Десктопный клиент для macOS (SwiftUI) платформы eID — приложение, которое выполняет вход по QR-коду или push по регистрационному номеру через мобильное приложение e-ID Mongolia и накладывает квалифицированную электронную подпись на PDF. Исходники: desktop/macos-app/ (проект XcodeGen eIDMongolia).

First-party клиент. По тому же принципу, что и приложение для iOS, этот клиент не является RP — в нём нет ни RP-секрета, ни RP UUID, ни регистрации. Все вызовы идут через публичные маршруты /api/* собственного веб-бэкенда (ровно тот же путь, что использует браузер). RP_API_SECRET для Go RP-API (/v3/*) хранится только на веб-сервере (web/src/lib/rpclient.ts). Поэтому это НЕ интеграция RP — интеграцию RP выполняет сам веб-сервер, а десктоп лишь использует его публичный фронтенд.

1. Что он делает

  • Вход — отправляет push по QR-коду или по регистрационному / civil-ID номеру, а мобильное приложение подтверждает его с помощью PIN1. Веб-сервер криптографически проверяет подпись, извлекает из subject сертификата имя, civil-ID номер и documentNumber и возвращает их — десктоп сам сертификат не разбирает.
  • Подписание PDF — выберите PDF, его дайджест SHA-256 вычисляется локально, а подпись выполняется с PIN2 в мобильном приложении. Проштампованный PDF (PAdES/PKCS#7 + страница проверки) сохраняется в ~/Downloads.
  • Handle идентичности — Bearer-сессии нет; полученный при входе documentNumber становится handle идентичности и сохраняется в Keychain (при восстановлении — проверка Touch ID).

2. Архитектура

Клиент обращается к публичным маршрутам /api/* веб-приложения (web/src/app/api/*). Используемые маршруты (по исходникам Core/Network/Endpoints.swift, Core/Network/APIClient.swift):

Маршрут Метод Назначение
/api/start POST Старт QR-сессии → {sessionId, qr, deviceLinkBase, vc, pollToken}
/api/login-notify POST Push по регистрационному / civil-ID номеру → {sessionId, vc, pollToken} (rate limit 3 за 60 с)
/api/status GET ?sessionId=&pollToken= long-poll — сервер держит ~1 с, клиент повторяет
/api/sign-pdf-start POST {etsi, digestB64, fileName, callbackUrl} → push с PIN2 → {sessionId, vc, pollToken} (rate limit 3 за 60 с на etsi)
/api/sign-pdf-download POST multipart file + sessionId + pollToken → байты проштампованного PDF
/api/health GET Проверка состояния сервера

Примечание. pollToken возвращается в ответах start / login-notify / sign-pdf-start и обязателен для /api/status и /api/sign-pdf-download. sessionId раскрывается в QR-коде, поэтому сам по себе он не даёт права читать персональные данные — pollToken выдаётся только инициатору сессии (Endpoints.swift, строки 18-23).

Секрета, RP UUID и имени RP в клиенте нет (Core/Network/AppConfig.swift). Приоритет источников базового URL сервера (побеждает первый непустой):

Источник Описание
UserDefaults["API_BASE_URL_OVERRIDE"] Задаётся в UI настроек
env API_BASE_URL Переменная окружения
По умолчанию DEBUG: http://localhost:3000, Release: https://eidmongolia.mn

3. Требования

  • macOS 14+ (deployment target 14.0)
  • Xcode 16+, Swift 5.10
  • brew install xcodegen — генерирует .xcodeproj из project.yml
  • Зависимости SPM: Sparkle 2 (автообновление), ../gerege-token-kit (пакет по локальному пути)

4. Сборка и запуск

cd desktop/macos-app
xcodegen generate
xcodebuild -project eIDMongolia.xcodeproj -scheme eIDMongolia \
           -configuration Debug -destination 'platform=macOS,arch=arm64' build
open eIDMongolia.xcodeproj    # ⌘R

Важно. Используйте -scheme eIDMongolia, а не -target — схема необходима для разрешения локального SPM-пакета GeregeTokenKit.

Локальное тестирование

Сервер по умолчанию для DEBUG-сборки — web (:3000). Запустите и Go API, и web:

cd ../../server && SMARTID_RP_API_SECRET= go run ./cmd/smartid   # Go API :8080
cd ../../web && npm run dev                                       # web :3000

Чтобы указать другой адрес, используйте Настройки → Сервер (или env API_BASE_URL). Мобильное приложение должно указывать на тот же Go-сервер.

5. Основные сценарии

Вход (Features/Login/LoginView.swift)

QR (initQR): 1. POST /api/start{sessionId, qr, vc, pollToken}. 2. Отрисовать значение qr (= sessionId) в виде QR через CoreImage; показать на экране код подтверждения vc. 3. Отсканировать QR телефоном → подтвердить с PIN1.

Push по регистрационному номеру (initiateLogin): 1. Пользователь вводит регистрационный / civil-ID номер (register, в верхнем регистре). 2. POST /api/login-notify {register}{sessionId, vc, pollToken}; на телефон уходит push.

Общее завершение (APIClient.waitForAuth): - Повторяет GET /api/status?sessionId=&pollToken= с интервалом ~400 мс до COMPLETE (на стороне сервера удержание 1 с). - При COMPLETE + OK веб-сервер извлекает из subject сертификата name / idNumber (serialNumber → civil-ID номер). - Результат сохраняется в Keychain как StoredIdentity (documentNumber, fullName, civilID, certificateLevel).

Подписание PDF (Features/Sign/SignView.swift)

Ровно тот же сценарий, что и на веб-демо-странице (web/src/app/demo/page.tsx):

  1. Выберите PDF (до 25 МБ). Дайджест SHA-256 исходного PDF вычисляется локально на клиенте (CryptoKit.SHA256) — это ТЕ ЖЕ байты, которые будут проштампованы.
  2. POST /api/sign-pdf-start {etsi, digestB64, fileName, callbackUrl:""}{sessionId, vc, pollToken}. etsi = civil-ID номер, полученный при входе (civilID, fallback nationalID, в верхнем регистре). На телефон уходит push с PIN2. callbackUrl пуст — телефон является отдельным устройством, поэтому возврата Web2App нет.
  3. Опрос GET /api/status?sessionId=&pollToken= (тот же путь, что и для auth) → COMPLETE/OK.
  4. POST /api/sign-pdf-download (multipart file + sessionId + pollToken) → байты проштампованного PDF → ~/Downloads/<имя>_signed.pdf (при совпадении имени добавляется числовой суффикс).

SEC-3: перед стартом сценария подписания выполняется SecurityGuard.enforce() — проверки на вмешательство/отладку (активны в Release).

6. USB token kit

desktop/gerege-token-kit/локальный SPM-пакет без зависимостей для USB-токенов FEITIAN (локальный PKCS#11/APDU, без подключения к серверу). Приложение для macOS автоматически подключает ../gerege-token-kit как path-пакет и использует его в Core/Token/ (TokenManager, TokenProvisioner) и в разделе Tokens. Подробнее: USB token kit.

7. Исходники (ссылки)

  • Приложение macOS: https://github.com/gerege-systems/eid-platform-mn/tree/main/desktop/macos-app
  • Сетевой слой: https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/macos-app/Core/Network/Endpoints.swift
  • HTTP-клиент: https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/macos-app/Core/Network/APIClient.swift
  • Вход: https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/macos-app/Features/Login/LoginView.swift
  • Подписание: https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/macos-app/Features/Sign/SignView.swift
  • Детали по компонентам: desktop/macos-app/CLAUDE.md, усиление безопасности: desktop/macos-app/SECURITY-HARDENING.md