eID Mongolia — iOS SDK (GeregeSmartID)¶
GeregeSmartID — iOS 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 по локальному пути:
Чтобы подключить его как 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 аттестует устройство при первом запросе к бэкенду:
- Первый запрос:
generateKey→attestKey→ заголовокType: attest(сервер записывает keyId / pubkey / counter). - Каждый последующий запрос:
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