跳转至

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,因此并非浏览器打包产物。

源码:sdk/typescript/src/


1. 安装

npm install @eid-mongolia/sdk
  • Node.js ≥ 18(全局 fetchnode:crypto)。
  • 同时提供 ESM 与 CommonJS 两种格式(import / require)。
import { EidClient, sha256Base64 } from "@eid-mongolia/sdk";

包名与运行环境要求:sdk/typescript/package.json


2. 注册 RP

由运营方预先注册 RP(docs/RP_INTEGRATION.md §1)。通过管理控制台: admin.eidmongolia.mnRelying Party (RP)+ New RP。 注册完成后会生成 UUIDAPI secretrp_sk_…)—— secret 只显示一次

将它们传入 RpCredentials

字段 取值
rpUUID 已在 relying_parties 中注册的 RP UUID
rpName 展示给公民的 RP 名称(UTF-8 不超过 32 字节)
apiSecret API secret(rp_sk_…)—— 仅限后端

源码:types.tsRpCredentials


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(用于测试)。

若缺少 baseUrlcredentials.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 }

AuthOptionsauth.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} 内部先计算文本摘要,再签名

SignOptionscertificateLevelcallbackUrlhashType(默认 SHA256,也可为 SHA384/SHA512 —— 若摘要由您自行准备,请保持一致)。

注意: 在签名流程中 NotificationSession.rpChallenge 为空("")—— 签名是针对 摘要校验,而非 challenge (sign.ts:54)。

4.3 会话轮询(长轮询)

SessionApisession.ts) 会反复对 RP-API 的 GET /session/{id}?timeoutMs= 发起长轮询。

方法 说明
poll(sessionId, serverPollMs = 30000) 单次长轮询;最多等待 serverPollMs
waitForResult(sessionId, opts?) 反复轮询直到 COMPLETEopts = { maxWaitMs?, serverPollMs? }
  • 默认 serverPollMs = 30_000maxWaitMs = 150_000(公民无响应时会截断等待)。
  • HTTP 超时被设置为比服务端轮询多 10 秒
  • waitForResult 在截止前未达到 COMPLETE,则返回最后一次(RUNNING)结果 —— 校验器会将其视为未完成的会话并抛出 ValidationError

SessionResult 字段(types.ts): stateendResultdocumentNumbercertificateDerB64certificateLevelsignatureValueB64signatureAlgorithminteractionTypeUsed


5. 响应校验 —— 为什么 validator 不可省略

⚠️ 不要仅凭 endResult === "OK" 就予以信任。 若 RP-API 被攻陷或经过代理, 可能会收到伪造的 OK。

ResponseValidatorvalidator.ts) 执行以下步骤:

  1. state === COMPLETE && endResult === OK(否则抛出 SessionFailedError)。
  2. 将公民证书一路链接至信任锚(Gerege Root CA)进行校验 (颁发者必须是 CA,链深度 ≤ 8)。
  3. 证书处于有效期内(validFrom/validTo,允许 clockSkewMs 偏差), 且满足所需的 certificateLevel(默认 QUALIFIED)。
  4. 用公民公钥校验签名:
  5. auth → 针对 ACSP_V2 负载(LP("ACSP_V2") ‖ LP(rpChallenge) ‖ LP(SPKI)
  6. 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. 错误处理

所有错误均继承自 EidErrorerrors.ts):

错误 含义 字段
AuthenticationError HTTP 401 —— API secret 错误或缺失
ForbiddenError HTTP 403 —— IP 白名单 / mTLS 不允许
ApiError 其他 HTTP 错误(4xx/5xx) .status.body
NetworkError 超时 / 网络错误
SessionFailedError endResult ≠ OKTIMEOUTUSER_REFUSED*DOCUMENT_UNUSABLEWRONG_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 的 Agenthttp.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 契约 —— 接口、请求体字段 (relyingPartyUUIDrelyingPartyNamecertificateLevelsignatureProtocol: "ACSP_V2"interactions)以及 Bearer 认证方式完全一致。若希望不使用 SDK 而直接通过 HTTP 集成, 或想查看完整的接口列表与公民标识号码:


10. 开发

cd sdk/typescript
npm install
npm run lint   # tsc --noEmit
npm test       # build + node --test

包目录:sdk/typescript/。 TypeScript 是参考实现 —— Go/Python 版本遵循其 API 界面、安全模型(ResponseValidator) 与协议契约(sdk/README.md)。