跳转至

eID Mongolia —— iOS SDK(GeregeSmartID)

GeregeSmartID 是 eID 平台的 iOS Swift SDK —— 它在公民手机上执行分布式密钥生成2-of-2 门限 ECDSA 签名、App Attest 以及钥匙串存储。其密码学实现与 Go 服务端的 internal/crypto 逐字节兼容(通过 golden 向量验证),因此门限 ECDSA / Paillier / Schnorr / PDL 流程在两端完全一致。

源码: ios/ios-sdk/Sources/GeregeSmartID/ —— GeregeSmartIDClient.swift(编排器)、SecureKeyStore.swift(钥匙串 + PIN)、 PhoneCrypto.swiftPaillier.swiftProofs.swiftAppAttestManager.swift

编号术语说明

每位公民拥有一个 documentNumber(设备 UUID)与两把相互独立的门限密钥: 身份认证(PIN1、登录、clientAuth)与签名(PIN2、签名、不可否认)。 在 SDK 中对应 PINSlot.auth / PINSlot.signdocs/IDENTIFIERS.mddocs/EID_2CERT_MILESTONE.md)。


1. 概览

GeregeSmartIDClient 与后端(例如 rp-api.eidmongolia.mn)的注册与门限签名接口通信, 并编排 App Attest 与密码学流程。核心能力:

操作 说明
enroll 使用 DAN KYC 授权码 + PIN 执行分布式密钥生成,获取证书并保存。若提供 authPin,则一次性执行两次密钥生成(auth + sign)(eID 双证书)。
approve 使用 PIN 批准 RP 的二维码 / 推送会话,并完成 3 轮门限 ECDSA 签名的组装。
changePIN / verifyPIN 在本地校验 / 修改 PIN(服务端绝不会看到 PIN)。
pending / activity / sessionInfo 轮询待处理会话、历史记录与会话详情。

私钥永远不会在任何一处完整存在:一半保存在手机上(用 PIN 加密后存于钥匙串),另一半在服务器上。


2. 前置条件

  • iOS 14.0+(SDK)、macOS 12.0+ —— 见 Package.swift 中的 platforms。 (主应用 eIDMongolia 要求 iOS 15.0+。)
  • App Attest 在模拟器上不可用。 模拟器 / 仿真器没有 App Attest 与 Secure Enclave, 因此请以 SMARTID_REQUIRE_ATTESTATION=false 运行 Go 服务端(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(公钥固定)校验每一次与后端的连接。若服务器出示的证书来自 非预期 CA(Let's Encrypt 之外),连接将以 NSURLErrorCancelled(-999)中止。


3. 安装(Swift Package Manager)

包名: GeregeSmartID产品: GeregeSmartIDios/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")   // 钥匙串内的注册名称(默认值)

account 是钥匙串槽位的名称。SDK 将签名密钥保存在 account 下, 将身份认证密钥保存在 account + ".auth" 下(init(baseURL:account:))。

4.2 注册(enroll)

enroll 使用 DAN KYC 授权码 + PIN 执行分布式密钥生成,获取证书并保存到钥匙串, 返回 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 双证书 —— 两个 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 会以蒙古语 文案在界面上显示每一轮进度(ProgressHandler = @MainActor (String) -> Void)。

DAN KYC 流程

传入 enrolldanAuthCode 即 DAN 核验的 state。此前需要: danInit(registrationNumber:) → 打开核验 URL → 使用 danStatus(state:) 轮询 → 核验通过后调用 enroll(danAuthCode: state, …)。(kycMethods() 返回可用的方式: DAN / G-Sign / 护照 / 身份证。)

4.3 批准会话(approve)

使用 PIN 批准从 RP 的二维码 / 推送中获得的 sessionId。当 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 内部执行 3 轮门限 ECDSA:commit → prove → finish/v3/mobile/session/{id}/sign/{commit,prove,finish})。在把签名发送到服务器之前, 手机会独立计算该会话的消息(SIGN:摘要;AUTH:ACSP_V2(rpChallenge, 自身 SPKI)), 并用自身证书的公钥对最终的 (r,s) 做 ECDSA 校验(transcript 绑定 —— WYSIWYS)。 为提升速度,可事先通过 presign(sessionId:documentNumber:) 预备无需 PIN 的 nonce 轮次。

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 重新加密;证书与公钥保持不变。 旧 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. 钥匙串 / Secure Enclave / 生物识别

手机端的密钥材料由 SecureKeyStoreSecureKeyStore.swift)管理:

  • PIN → PBKDF2(HMAC-SHA256,210,000 次迭代)→ AES-GCM 加密,随后用 Secure Enclave 的 P-256 密钥做 ECIES 封装(硬件绑定)。PIN 绝不离开设备。
  • 存储格式:[salt(16) | iv(12) | ciphertext | tag(16)] → SE 封装 → [1 | wrapped]。 若在真机上无法使用 SE,则视为硬错误SDKError.crypto);只有在模拟器上才允许 [0 | blob](无硬件绑定,仅供开发)。
  • 生物识别(H8): SE 封装密钥带有 .userPresence 访问控制(Face ID / Touch ID 或密码)。 在解封签名密钥(即用 PIN 解锁身份)时会弹出生物识别提示;若用户取消,解封失败 → 签名中止。
  • 暴力破解防护: 连续输错 PIN 超过 10 次后,加密的注册数据会被删除SDKError.locked)。即便数据块被删除,计数器仍持久保存在钥匙串中。
  • 可访问性: 所有条目均为 kSecAttrAccessibleWhenUnlockedThisDeviceOnly —— 绑定本机,不会同步到 iCloud。
  • 设备绑定令牌: 一种无需 PIN 的 bearer 凭据,在每个 /v3/mobile/* 请求中以 X-Device-Token 发送(在输入 PIN 之前就需要轮询 pending/activity)—— 同样为 ThisDeviceOnly

钥匙串命名空间

GeregeSmartIDKeychain.service 是品牌命名空间(eIDMongolia"mn.eidmongolia.smartid")。 请在应用启动时设置一次。对于已有用户的应用请勿修改 —— 否则以旧名称保存的注册数据将无法读取。


6. 构建主应用(eIDMongolia)

eIDMongoliaios/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                                # 在 Xcode 中运行

通过命令行构建并安装到已连接的设备(自动签名,请使用 -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):Xcode scheme 环境变量 (GEREGE_BACKEND_URL)→ Info.plist(project.yml 中的 build setting)→ 代码默认值 (AppConfig.defaultBackendURL)。

通过命令行检查 SDK(目标为 iOS 模拟器):

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

(单独执行 swift build 会针对 macOS 构建并在 App Attest API 处失败 —— 这是预期行为, 本 SDK 仅面向 iOS。)


7. App Attest 流程(简述)

AppAttestManager 会在首个发往后端的请求上对设备进行认证:

  1. 首个请求:generateKeyattestKey → 请求头 Type: attest (服务端记录 keyId / pubkey / counter)。
  2. 之后的每个请求:generateAssertion → 请求头 Type: assert + KeyId (服务端校验计数器严格递增)。

服务端会在响应中返回 X-Next-Attestation-Nonce 头,使应用可以跳过单独的 challenge GET (GET /v3/attestation/challenge),从而更快完成流程。


8. 后端接口(SDK 所使用)

均对应 Go 服务端的 internal/httpapi

接口 用途
GET /v3/attestation/challenge App Attest nonce
POST /v3/enrollment/init · /complete 分布式密钥生成 + 证书
GET /v3/mobile/session/{id} · /v3/mobile/pending/{doc} 会话详情 / 待处理轮询
POST /v3/mobile/session/{id}/sign/{commit,prove,finish} 2-of-2 门限 ECDSA(3 轮)

9. 链接