eID Gerege —— TypeScript RP SDK¶
@eid-mongolia/sdk 是一个运行在依赖方(RP)后端的 Node.js/TypeScript 库。
它将对 RP-API(/v3)的所有调用(身份认证、签名、会话轮询)汇聚到单一的 EidClient 中,
并在其之上增加了对响应的密码学校验(证书链 + 签名)。
它是 docs/RP_INTEGRATION.md 中所描述原始 HTTP 契约的类型化封装 ——
接口相同、安全模型相同,但由 SDK 代为生成 challenge、反复轮询会话并校验证书。
⚠️ 仅限服务端使用。 API secret(
rp_sk_…)绝不能暴露给浏览器 / 手机。 SDK 依赖node:crypto,因此并非浏览器打包产物。
1. 安装¶
- Node.js ≥ 18(全局
fetch、node:crypto)。 - 同时提供 ESM 与 CommonJS 两种格式(
import/require)。
包名与运行环境要求:sdk/typescript/package.json。
2. 注册 RP¶
由运营方预先注册 RP(docs/RP_INTEGRATION.md §1)。通过管理控制台:
admin.eidmongolia.mn → Relying Party (RP) → + New RP。
注册完成后会生成 UUID 与 API secret(rp_sk_…)—— secret 只显示一次。
将它们传入 RpCredentials:
| 字段 | 取值 |
|---|---|
rpUUID |
已在 relying_parties 中注册的 RP UUID |
rpName |
展示给公民的 RP 名称(UTF-8 不超过 32 字节) |
apiSecret |
API secret(rp_sk_…)—— 仅限后端 |
3. 配置客户端¶
EidClient 是所有流程的入口:
import { EidClient } from "@eid-mongolia/sdk";
const eid = new EidClient({
baseUrl: "https://rp-api.eidmongolia.mn", // 不带 /v3 —— SDK 会自行追加
credentials: {
rpUUID: process.env.EID_RP_UUID!,
rpName: "Khan Bank",
apiSecret: process.env.EID_SECRET!, // rp_sk_…
},
trust: { trustAnchorsPem: [process.env.EID_ROOT_CA_PEM!] }, // 生产环境**必填**
});
ClientConfig 的全部字段(client.ts):
| 字段 | 必填 | 取值 |
|---|---|---|
baseUrl |
✅ | RP-API 基础 URL。SDK 会追加 /v3。 |
credentials |
✅ | RpCredentials(见上)。若 apiSecret 为空,构造函数会抛出异常。 |
trust |
生产环境 ✅ | TrustConfig —— 信任锚(Root CA)+ 校验器设置。 |
defaultCertificateLevel |
QUALIFIED(默认)或 ADVANCED。 |
|
timeoutMs |
HTTP 超时(毫秒)。默认 15000。 | |
dispatcher |
undici 的 Dispatcher —— mTLS 客户端证书(§8)。 |
|
fetchImpl |
替换 fetch(用于测试)。 |
若缺少 baseUrl 或 credentials.apiSecret,构造函数会立即抛出 Error。
EidClient 提供以下子 API:
| 字段 | 类 | 职责 |
|---|---|---|
eid.auth |
AuthApi |
发起身份认证(推送 / 二维码) |
eid.sign |
SignApi |
发起签名(摘要) |
eid.session |
SessionApi |
轮询结果(长轮询) |
eid.validator |
ResponseValidator |
对响应做密码学校验 |
4. 核心流程¶
4.1 身份认证(推送)¶
eid.auth 的每个方法都会在内部生成随机 rpChallenge 并附加到返回的会话上
(校验器会将其与签名比对)。源码:
auth.ts。
// РД / civil ID / ETSI —— 任意一种均可(服务端会自动识别类型)
const s = await eid.auth.notificationByEtsi("PNOMN-111949212017", [
{ type: "displayTextAndPIN", displayText60: "Sign in to Khan Bank" },
]);
showToUser(s.vc); // ← 显示在公民手机上的验证码(VC)
const result = await eid.session.waitForResult(s.sessionId); // 长轮询(§4.3)
const who = eid.validator.validateAuth(result, s.rpChallenge); // ← 进行密码学校验
console.log(who.documentNumber, who.subject); // 已验证的公民
AuthApi 方法:
| 方法 | 接口 | 响应 |
|---|---|---|
notificationByEtsi(id, interactions, opts?) |
POST /authentication/notification/etsi/{id} |
NotificationSession |
notificationByDocument(documentNumber, interactions, opts?) |
POST /authentication/notification/document/{id} |
NotificationSession |
deviceLinkAnonymous(interactions, opts?) |
POST /authentication/device-link/anonymous |
DeviceLinkSession |
deviceLinkByEtsi(id, interactions, opts?) |
POST /authentication/device-link/etsi/{id} |
DeviceLinkSession |
- notification(推送) —— 直接向公民手机发送通知。
NotificationSession返回{ sessionId, vc, rpChallenge }。 - device-link(二维码 / App2App) —— 用于生成二维码 / deeplink。
DeviceLinkSession返回{ sessionId, sessionToken, sessionSecret, deviceLinkBase, rpChallenge }。
AuthOptions(auth.ts):
certificateLevel(针对本次会话覆盖默认值)、callbackUrl(同设备 App2App 的返回 URL ——
若提供,将以 initialCallbackUrl 发送)。
4.2 签名(合格电子签名)¶
RP 发送其文档的 SHA-256 摘要,并依据该摘要与公民证书校验返回的签名。源码:
sign.ts。
import { sha256Base64 } from "@eid-mongolia/sdk";
const digest = sha256Base64(pdfBytes); // 文档的 SHA-256(base64)
const s = await eid.sign.digestByEtsi("PNOMN-111949212017", digest, [
{ type: "displayTextAndPIN", displayText60: "Sign the loan agreement" },
]);
showToUser(s.vc);
const result = await eid.session.waitForResult(s.sessionId);
const sig = eid.validator.validateSign(result, digest); // 依据摘要校验
console.log(sig.signatureValueB64, sig.subject);
SignApi 方法:
| 方法 | 接口 | 说明 |
|---|---|---|
digestByEtsi(id, digestB64, interactions, opts?) |
POST /signature/notification/etsi/{id} |
使用现成摘要(二进制文档) |
digestByDocument(documentNumber, digestB64, interactions, opts?) |
POST /signature/notification/document/{id} |
使用现成摘要(指定设备) |
textByEtsi(id, text, interactions, opts?) |
POST /signature/notification/etsi/{id} |
内部先计算文本摘要,再签名 |
SignOptions:certificateLevel、callbackUrl、hashType(默认 SHA256,也可为
SHA384/SHA512 —— 若摘要由您自行准备,请保持一致)。
注意: 在签名流程中
NotificationSession.rpChallenge为空("")—— 签名是针对 摘要校验,而非 challenge (sign.ts:54)。
4.3 会话轮询(长轮询)¶
SessionApi(session.ts)
会反复对 RP-API 的 GET /session/{id}?timeoutMs= 发起长轮询。
| 方法 | 说明 |
|---|---|
poll(sessionId, serverPollMs = 30000) |
单次长轮询;最多等待 serverPollMs。 |
waitForResult(sessionId, opts?) |
反复轮询直到 COMPLETE。opts = { maxWaitMs?, serverPollMs? }。 |
- 默认
serverPollMs = 30_000、maxWaitMs = 150_000(公民无响应时会截断等待)。 - HTTP 超时被设置为比服务端轮询多 10 秒。
- 若
waitForResult在截止前未达到COMPLETE,则返回最后一次(RUNNING)结果 —— 校验器会将其视为未完成的会话并抛出ValidationError。
SessionResult 字段(types.ts):
state、endResult、documentNumber、certificateDerB64、certificateLevel、
signatureValueB64、signatureAlgorithm、interactionTypeUsed。
5. 响应校验 —— 为什么 validator 不可省略¶
⚠️ 不要仅凭
endResult === "OK"就予以信任。 若 RP-API 被攻陷或经过代理, 可能会收到伪造的 OK。
ResponseValidator(validator.ts)
执行以下步骤:
state === COMPLETE && endResult === OK(否则抛出SessionFailedError)。- 将公民证书一路链接至信任锚(Gerege Root CA)进行校验 (颁发者必须是 CA,链深度 ≤ 8)。
- 证书处于有效期内(
validFrom/validTo,允许clockSkewMs偏差), 且满足所需的certificateLevel(默认QUALIFIED)。 - 用公民公钥校验签名:
- auth → 针对 ACSP_V2 负载(
LP("ACSP_V2") ‖ LP(rpChallenge) ‖ LP(SPKI)) - sign → 针对 RP 提供的摘要
任一步骤失败都会抛出 ValidationError —— 此时请勿信任该响应。
公开方法:
| 方法 | 返回值 | 说明 |
|---|---|---|
validateAuth(result, rpChallengeB64) |
VerifiedIdentity |
{ documentNumber, certificate, subject, certificateLevel } |
validateSign(result, digestB64) |
VerifiedSignature |
{ documentNumber, signatureValueB64, signatureAlgorithm, certificate, subject } |
checkRevocation(cert) |
Promise<void> |
OCSP/CRL 钩子(见下)。 |
5.1 TrustConfig 设置¶
通过 client.trust 传入(validator.ts):
| 字段 | 取值 |
|---|---|
trustAnchorsPem |
根 CA 的 PEM。生产环境必填。 若为空则抛出 ValidationError(仅可通过 allowUntrusted 绕过)。 |
intermediatesPem |
中间 CA 的 PEM(连接 leaf ↔ root)。 |
requiredLevel |
所需的 certLevel(默认 QUALIFIED)。 |
clockSkewMs |
校验有效期时允许的时钟偏差(默认 0)。 |
allowUntrusted |
为 true 时不校验证书链 —— 仅限开发 / 测试。 |
revocation |
OCSP/CRL 检查钩子(RevocationChecker)。 |
revocationMode |
"hard-fail"(默认;拒绝 unknown)或 "soft-fail"。 |
吊销检查。 若未配置
revocation钩子,则不会检查证书吊销状态 —— 这由 RP 自行负责。若已配置,请在validateAuth/validateSign之后调用await eid.validator.checkRevocation(who.certificate)(吊销检查涉及网络 I/O, 因此为异步方法,而 validate 是同步的)。
6. 错误处理¶
所有错误均继承自 EidError(errors.ts):
| 错误 | 含义 | 字段 |
|---|---|---|
AuthenticationError |
HTTP 401 —— API secret 错误或缺失 | — |
ForbiddenError |
HTTP 403 —— IP 白名单 / mTLS 不允许 | — |
ApiError |
其他 HTTP 错误(4xx/5xx) | .status、.body |
NetworkError |
超时 / 网络错误 | — |
SessionFailedError |
endResult ≠ OK(TIMEOUT、USER_REFUSED*、DOCUMENT_UNUSABLE、WRONG_VC 等) |
.endResult |
ValidationError |
证书链 / 签名 / 级别校验未通过 | — |
import { SessionFailedError, ValidationError } from "@eid-mongolia/sdk";
try {
const result = await eid.session.waitForResult(s.sessionId);
const who = eid.validator.validateAuth(result, s.rpChallenge);
} catch (e) {
if (e instanceof SessionFailedError) {
// e.endResult: "USER_REFUSED" | "TIMEOUT" | … → 界面提示、重试
} else if (e instanceof ValidationError) {
// 可能被伪造 / 代理 —— 请勿信任该响应
}
throw e;
}
7. 密码学辅助函数¶
基于 node:crypto 的导出(crypto.ts):
| 函数 | 说明 |
|---|---|
sha256Base64(data) |
对文本 / 字节计算 SHA-256 摘要(base64)—— 用于准备签名流程所需摘要。 |
randomChallenge(bytes = 64) |
随机 RP challenge(base64)。由 auth 方法内部调用。 |
buildAcspV2Payload(rpChallengeB64, spkiDER) |
组装 ACSP_V2 负载 —— 供校验器使用。 |
⚠️ ACSP_V2 负载在服务端 (
server/internal/crypto/acsp.go)、 手机端与 RP SDK 之间必须逐字节一致。LP(x) = 4 字节大端长度 ‖ x。
8. mTLS(eIDAS 合格环境)¶
若生产环境的 RP-API 要求客户端证书,请通过 dispatcher 提供 undici 的 Agent
(http.ts):
import { Agent } from "undici";
const eid = new EidClient({
baseUrl: "https://rp-api.eidmongolia.mn",
credentials: { /* … */ },
trust: { trustAnchorsPem: [ROOT_CA_PEM] },
dispatcher: new Agent({ connect: { cert: clientCertPem, key: clientKeyPem } }),
});
一旦提供 dispatcher,所有 fetch 调用都会使用它(仅限 Node 环境)。
9. 与原始 HTTP 版本的关系¶
SDK 完全遵循 docs/RP_INTEGRATION.md 的原始 HTTP 契约 —— 接口、请求体字段
(relyingPartyUUID、relyingPartyName、certificateLevel、signatureProtocol: "ACSP_V2"、
interactions)以及 Bearer 认证方式完全一致。若希望不使用 SDK 而直接通过 HTTP 集成,
或想查看完整的接口列表与公民标识号码:
- RP 集成指南(原始 HTTP)
- 可运行的 RP 客户端示例:
web/src/lib/rpclient.ts
10. 开发¶
包目录:sdk/typescript/。
TypeScript 是参考实现 —— Go/Python 版本遵循其 API 界面、安全模型(ResponseValidator)
与协议契约(sdk/README.md)。