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

eID Mongolia — iOS SDK (GeregeSmartID)

GeregeSmartIDiOS SDK на Swift для платформы eID: он выполняет распределённую генерацию ключей, подписание пороговой ECDSA 2-of-2, App Attest и хранение в Keychain на телефоне гражданина. Его криптография побайтово совместима с internal/crypto Go-сервера (проверено golden-векторами), поэтому сценарии пороговой ECDSA / Paillier / Schnorr / PDL совпадают на обеих сторонах в точности.

Исходный код: ios/ios-sdk/Sources/GeregeSmartID/GeregeSmartIDClient.swift (оркестратор), SecureKeyStore.swift (Keychain + PIN), PhoneCrypto.swift, Paillier.swift, Proofs.swift, AppAttestManager.swift.

Терминология нумерации

У каждого гражданина один documentNumber (UUID устройства) и ДВА отдельных пороговых ключа: аутентификация (PIN1, вход, clientAuth) и подписание (PIN2, подпись, неотказуемость). В SDK это PINSlot.auth / PINSlot.sign (docs/IDENTIFIERS.md, docs/EID_2CERT_MILESTONE.md).


1. Обзор

GeregeSmartIDClient обращается к эндпойнтам регистрации и порогового подписания бэкенда (например, rp-api.eidmongolia.mn), оркестрируя сценарии App Attest и криптографии. Основные возможности:

Действие Описание
enroll Выполняет распределённую генерацию ключей по коду согласия DAN KYC и PIN, получает сертификат и сохраняет его. Если передан authPin, выполняются две генерации ключей (auth + sign) сразу (eID 2-cert).
approve Подтверждает QR/push-сессию RP с помощью PIN и собирает подпись пороговой ECDSA за 3 раунда.
changePIN / verifyPIN Локальная проверка и смена PIN (сервер НИКОГДА не видит PIN).
pending / activity / sessionInfo Опрос ожидающих сессий, история и детали сессии.

Закрытый ключ никогда не существует целиком в одном месте: одна половина на телефоне (зашифрована PIN-кодом в Keychain), другая — на сервере.


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

  • iOS 14.0+ (SDK), macOS 12.0+ — см. platforms в Package.swift. (Основное приложение eIDMongolia требует iOS 15.0+.)
  • App Attest не работает на симуляторе. У симуляторов/эмуляторов нет App Attest и Secure Enclave, поэтому запускайте Go-сервер с SMARTID_REQUIRE_ATTESTATION=false (значение по умолчанию для dev). В продакшене (=true) требуется реальное устройство с App Attest.
  • URL бэкенда конфигурируется (не зашит в код) — передайте его в GeregeSmartIDClient(baseURL:). Пример продуктивной базы: https://rp-api.eidmongolia.mn.
  • Зависимость BigInt (attaswift/BigInt) — нужна для арифметики сырых EC-скаляров/точек порогового протокола и Paillier (2048 бит) (CryptoKit их не предоставляет). Загружается автоматически при swift build.

TLS pinning

SDK проверяет каждое соединение с бэкендом через CertPinner (pinning по публичному ключу). Если сервер предъявит сертификат от CA, отличного от ожидаемого (Let's Encrypt), соединение прерывается с NSURLErrorCancelled (-999).


3. Установка (Swift Package Manager)

Имя пакета: GeregeSmartID, продукт: GeregeSmartID (ios/ios-sdk/Package.swift).

Приложение внутри монорепозитория (XcodeGen project.yml) подключает SDK по локальному пути:

packages:
  GeregeSmartID:
    path: ../ios-sdk             # локальный Swift-пакет (GeregeSmartID)

Чтобы подключить его как SPM-зависимость из внешнего проекта, в Package.swift:

dependencies: [
    .package(url: "https://github.com/gerege-systems/eid-platform-mn.git", branch: "main"),
    // либо из репозитория, где SDK размещён отдельно
],
targets: [
    .target(name: "MyApp", dependencies: [
        .product(name: "GeregeSmartID", package: "eid-platform-mn")
    ])
]

4. Базовое использование

4.1 Создание клиента

import GeregeSmartID

let client = GeregeSmartIDClient(
    baseURL: URL(string: "https://rp-api.eidmongolia.mn")!,
    account: "default")   // имя записи внутри Keychain (по умолчанию)

account — имя слота в Keychain. SDK хранит ключ подписания под account, а ключ аутентификации — под account + ".auth" (init(baseURL:account:)).

4.2 Регистрация (enroll)

enroll выполняет распределённую генерацию ключей по коду согласия DAN KYC и PIN, получает сертификат, сохраняет его в Keychain и возвращает documentNumber (UUID). Если передан authPin, выполняются две генерации ключей (auth + sign): ключ подписания сохраняется под pin, ключ аутентификации — под authPin, каждый в своём слоте; если authPin == nil, создаётся только сертификат подписания (легаси-поведение).

@discardableResult
public func enroll(danAuthCode: String, pin: String, authPin: String? = nil,
                   pushToken: String,
                   reviewLatin: LatinNameReview? = nil,
                   progress: ProgressHandler? = nil) async throws -> String

Пример (eID 2-cert — два PIN-кода):

let documentNumber = try await client.enroll(
    danAuthCode: danState,   // код согласия, подтверждённый DAN (state)
    pin: "1234",             // PIN2 → подписание (подпись)
    authPin: "5678",         // PIN1 → аутентификация (вход); если nil — только подписание
    pushToken: apnsToken)

Колбэк reviewLatin может показать гражданину латинскую транслитерацию для исправления (LatinNameProposal → исправленные (givenNameLatin, surnameLatin)). progress отображает каждый раунд в UI с монгольской подписью (ProgressHandler = @MainActor (String) -> Void).

Сценарий DAN KYC

danAuthCode, передаваемый в enroll, — это state верификации DAN. Предварительно: danInit(registrationNumber:) → открыть URL верификации → опрос через danStatus(state:) → после подтверждения enroll(danAuthCode: state, …). (kycMethods() возвращает доступные методы: DAN / G-Sign / паспорт / удостоверение личности.)

4.3 Подтверждение сессии (approve)

Подтвердите sessionId, полученный из QR/push от RP, с помощью PIN. При authentication: true (сценарий входа) используется ключ аутентификации (PIN1, слот .auth); иначе — ключ подписания (PIN2), в соответствии с ключом, который выбрал сервер. flowType известен заранее через sessionInfo(sessionId:).

@discardableResult
public func approve(sessionId: String, pin: String,
                    authentication: Bool = false,
                    confirmVc: String? = nil,
                    progress: ProgressHandler? = nil) async throws -> String

Пример:

// Сессия входа (аутентификация)
let status = try await client.approve(
    sessionId: sessionId, pin: "5678", authentication: true)  // "OK"

// Сессия подписания
let status = try await client.approve(
    sessionId: sessionId, pin: "1234")  // authentication: false (по умолчанию)

Внутри approve выполняет пороговую ECDSA за 3 раунда: commit → prove → finish (/v3/mobile/session/{id}/sign/{commit,prove,finish}). ПЕРЕД ОТПРАВКОЙ подписи на сервер телефон самостоятельно вычисляет сообщение сессии (SIGN: дайджест; AUTH: ACSP_V2(rpChallenge, собственный SPKI)) и проверяет финальную пару (r,s) по ECDSA публичным ключом своего сертификата (привязка транскрипта — WYSIWYS). Для ускорения можно заранее подготовить раунды с nonce, не требующие PIN, вызовом presign(sessionId:documentNumber:).

4.4 PINSlot и управление PIN

public enum PINSlot { case auth, sign }

public func verifyPIN(slot: PINSlot, pin: String) -> Bool          // локальная проверка
public func changePIN(slot: PINSlot, oldPIN: String, newPIN: String) throws
public func deleteRegistration()                                    // локальное удаление
  • verifyPIN — пытается разблокировать сохранённую идентичность PIN-кодом (без обращения к серверу).
  • changePIN — разблокирует старым PIN и перешифровывает новым; сертификат и публичный ключ не меняются. Неверный старый PIN → SDKError.wrongPIN.
  • deleteRegistration — удаляет ТОЛЬКО ЛОКАЛЬНО (оба слота — sign и auth); серверный сертификат при этом не отзывается (это действие администратора).

4.5 Ошибки (SDKError)

public enum SDKError: Error {
    case crypto(String)   // криптографическая ошибка
    case http(String)     // сетевая / серверная ошибка
    case wrongPIN         // неверный PIN (ключ не расшифровывается)
    case locked           // слишком много неверных PIN → регистрация удалена (нужен повторный enroll)
}

SDKError реализует LocalizedError, поэтому errorDescription возвращает реальную причину на монгольском языке.


5. Keychain / Secure Enclave / биометрия

Секретами на телефоне управляет SecureKeyStore (SecureKeyStore.swift):

  • PIN → PBKDF2 (HMAC-SHA256, 210 000 итераций) → шифрование AES-GCM, затем ECIES-обёртка ключом P-256 из Secure Enclave (аппаратная привязка). PIN НИКОГДА не покидает устройство.
  • Формат хранения: [salt(16) | iv(12) | ciphertext | tag(16)] → обёртка SE → [1 | wrapped]. Если на реальном устройстве SE недоступен, это жёсткая ошибка (SDKError.crypto); только на симуляторе допускается [0 | blob] (без аппаратной привязки, только для dev).
  • Биометрия (H8): ключ обёртки SE имеет контроль доступа .userPresence (Face ID / Touch ID или код-пароль). При разворачивании ключа подписания (= разблокировке идентичности PIN-кодом) появляется биометрический запрос; если пользователь отменит его, разворачивание не удастся → подписание прекращается.
  • Защита от перебора: при превышении 10 подряд неверных PIN зашифрованная регистрация УДАЛЯЕТСЯ (SDKError.locked). Счётчик сохраняется в Keychain даже после удаления блоба.
  • Доступность: всё помечено kSecAttrAccessibleWhenUnlockedThisDeviceOnly — привязано к этому устройству и не синхронизируется с iCloud.
  • Токен привязки устройства: bearer-учётные данные без PIN, отправляемые как X-Device-Token в каждом запросе /v3/mobile/* (опрос pending/activity нужен ещё до ввода PIN) — тоже ThisDeviceOnly.

Пространство имён Keychain

GeregeSmartIDKeychain.service — брендовое пространство имён (eIDMongolia"mn.eidmongolia.smartid"). Задайте его ОДИН раз при старте приложения. НЕ МЕНЯЙТЕ его в приложении, у которого уже есть пользователи — регистрации под прежним именем станут нечитаемыми.


6. Сборка основного приложения (eIDMongolia)

eIDMongolia (ios/eIDMongolia/) — единственное основное / эталонное приложение этого репозитория (XcodeGen project.yml, bundle mn.eidmongolia.dan). Подробнее: ios/README.md.

brew install xcodegen
cd ios/eIDMongolia
export GEREGE_BACKEND_URL=https://rp-api.eidmongolia.mn   # либо через значение по умолчанию в AppConfig
xcodegen                                                  # генерирует eIDMongolia.xcodeproj
open eIDMongolia.xcodeproj                                # Run из Xcode

Сборка и установка на подключённое устройство из CLI (автоматическая подпись, используйте -scheme, а не -target):

xcodebuild -project eIDMongolia.xcodeproj -scheme eIDMongolia \
  -configuration Debug -destination 'id=<DEVICE_ID>' \
  -allowProvisioningUpdates build
xcrun devicectl device install app --device <DEVICE_ID> <path>/eIDMongolia.app

Приоритет URL бэкенда (eIDMongolia/App/AppConfig.swift): env схемы Xcode (GEREGE_BACKEND_URL) → Info.plist (build setting из project.yml) → значение по умолчанию в коде (AppConfig.defaultBackendURL).

Проверка SDK через CLI (цель — симулятор iOS):

cd ios/ios-sdk
swift build --sdk "$(xcrun --sdk iphonesimulator --show-sdk-path)" \
            --triple arm64-apple-ios16.0-simulator

(Простой swift build собирает под macOS и останавливается на API App Attest — это ожидаемо, SDK предназначен только для iOS.)


7. Сценарий App Attest (кратко)

AppAttestManager аттестует устройство при первом запросе к бэкенду:

  1. Первый запрос: generateKeyattestKey → заголовок Type: attest (сервер записывает keyId / pubkey / counter).
  2. Каждый последующий запрос: generateAssertion → заголовок Type: assert + KeyId (сервер проверяет строгое возрастание счётчика).

В ответе сервер возвращает заголовок X-Next-Attestation-Nonce, поэтому приложение может пропустить отдельный GET за challenge (GET /v3/attestation/challenge) и работать быстрее.


8. Эндпойнты бэкенда (используемые SDK)

Все соответствуют internal/httpapi Go-сервера:

Эндпойнт Назначение
GET /v3/attestation/challenge Nonce для App Attest
POST /v3/enrollment/init · /complete Распределённая генерация ключей + сертификат
GET /v3/mobile/session/{id} · /v3/mobile/pending/{doc} Детали сессии / опрос ожидающих
POST /v3/mobile/session/{id}/sign/{commit,prove,finish} Пороговая ECDSA 2-of-2 (3 раунда)

9. Ссылки

  • Исходный код SDK: ios/ios-sdk/Sources/GeregeSmartID/
  • README SDK: ios/ios-sdk/README.md
  • Приложение и инструкции по сборке: ios/README.md
  • Идентификаторы: docs/IDENTIFIERS.md · 2 сертификата: docs/EID_2CERT_MILESTONE.md
  • Интеграция RP (серверная сторона): docs/RP_INTEGRATION.md