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.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 双证书)。 |
| 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,产品: 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") // 钥匙串内的注册名称(默认值)
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 流程
传入 enroll 的 danAuthCode 即 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 / 生物识别¶
手机端的密钥材料由 SecureKeyStore(SecureKeyStore.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)¶
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 # 在 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 会在首个发往后端的请求上对设备进行认证:
- 首个请求:
generateKey→attestKey→ 请求头Type: attest(服务端记录 keyId / pubkey / counter)。 - 之后的每个请求:
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. 链接¶
- SDK 源码:
ios/ios-sdk/Sources/GeregeSmartID/ - SDK README:
ios/ios-sdk/README.md - 应用与构建说明:
ios/README.md - 标识符:
docs/IDENTIFIERS.md· 双证书:docs/EID_2CERT_MILESTONE.md - RP 集成(服务端):
docs/RP_INTEGRATION.md