Перейти к содержанию

eID Gerege — TypeScript RP SDK

@eid-mongolia/sdk — библиотека для Node.js/TypeScript, работающая на бэкенде доверяющей стороны (RP). Она собирает все вызовы RP-API (/v3) (аутентификация, подписание, опрос сессии) в единый EidClient и добавляет сверху криптографическую проверку ответов (цепочка сертификатов + подпись).

Это типизированная обёртка над «сырым» HTTP-контрактом, описанным в docs/RP_INTEGRATION.md — те же эндпойнты, та же модель безопасности, но SDK сам генерирует challenge, многократно опрашивает сессии и валидирует сертификаты.

⚠️ Только на стороне сервера. API-секрет (rp_sk_…) никогда не должен попадать в браузер или на телефон. SDK опирается на node:crypto, поэтому это не браузерный бандл.

Исходники: sdk/typescript/src/


1. Установка

npm install @eid-mongolia/sdk
  • Node.js ≥ 18 (глобальный fetch, node: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. После регистрации выдаются UUID и API-секрет (rp_sk_…) — секрет показывается только один раз.

Передайте их в RpCredentials:

Поле Значение
rpUUID UUID RP, зарегистрированный в relying_parties
rpName Имя RP, показываемое гражданину (≤ 32 байт UTF-8)
apiSecret API-секрет (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 Базовый URL RP-API. SDK добавляет /v3.
credentials RpCredentials (см. выше). Если apiSecret пуст, конструктор выбрасывает исключение.
trust ✅ в продакшене TrustConfig — trust anchor (Root CA) + настройки валидатора.
defaultCertificateLevel QUALIFIED (по умолчанию) или ADVANCED.
timeoutMs HTTP-таймаут (мс). По умолчанию 15000.
dispatcher Dispatcher из undici — клиентский сертификат mTLS (§8).
fetchImpl Замена fetch (для тестирования).

Конструктор немедленно выбрасывает Error, если отсутствует baseUrl или credentials.apiSecret.

EidClient предоставляет следующие под-API:

Поле Класс Роль
eid.auth AuthApi Запуск аутентификации (push / QR)
eid.sign SignApi Запуск подписания (дайджест)
eid.session SessionApi Опрос результата (long-poll)
eid.validator ResponseValidator Криптографическая проверка ответа

4. Основные сценарии

4.1 Аутентификация (push)

Каждый метод 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);  // long-poll (§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 (push) — уведомление отправляется прямо на телефон гражданина. NotificationSession возвращает { sessionId, vc, rpChallenge }.
  • device-link (QR/App2App) — для генерации QR-кода / диплинка. DeviceLinkSession возвращает { sessionId, sessionToken, sessionSecret, deviceLinkBase, rpChallenge }.

AuthOptions (auth.ts): certificateLevel (переопределяет значение по умолчанию для этой сессии), callbackUrl (URL возврата App2App на том же устройстве — если задан, отправляется как 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 Опрос сессии (long-poll)

SessionApi (session.ts) многократно выполняет long-poll к GET /session/{id}?timeoutMs= RP-API.

Метод Описание
poll(sessionId, serverPollMs = 30000) Один long-poll; ждёт до serverPollMs.
waitForResult(sessionId, opts?) Опрашивает повторно до COMPLETE. opts = { maxWaitMs?, serverPollMs? }.
  • По умолчанию serverPollMs = 30_000, maxWaitMs = 150_000 (обрывает ожидание, если гражданин не отвечает).
  • HTTP-таймаут устанавливается на 10 секунд длиннее серверного poll.
  • Если 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) выполняет следующие шаги:

  1. state === COMPLETE && endResult === OK (иначе SessionFailedError).
  2. Проверяет сертификат гражданина, выстраивая цепочку до trust anchor (Gerege Root CA) (издатель должен быть CA, глубина ≤ 8).
  3. Сертификат находится в пределах срока действия (validFrom/validTo, с допуском clockSkewMs) и удовлетворяет требуемому certificateLevel (по умолчанию QUALIFIED).
  4. Проверяет подпись публичным ключом гражданина:
  5. auth → по payload 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 PEM корневых CA. ОБЯЗАТЕЛЬНО в продакшене. Если пусто — ValidationError (обходится только через allowUntrusted).
intermediatesPem PEM промежуточных CA (leaf ↔ root).
requiredLevel Требуемый certLevel (по умолчанию QUALIFIED).
clockSkewMs Допустимое расхождение часов при проверке срока действия (по умолчанию 0).
allowUntrusted Если true, цепочка не проверяется — только для dev/test.
revocation Хук проверки OCSP/CRL (RevocationChecker).
revocationMode "hard-fail" (по умолчанию; отклоняет unknown) или "soft-fail".

Отзыв. Если хук revocation не настроен, статус отзыва сертификата не проверяется — это ответственность RP. Если настроен, после validateAuth/validateSign вызывайте await eid.validator.checkRevocation(who.certificate) (проверка отзыва — сетевой ввод-вывод, поэтому она асинхронная, тогда как validate — синхронный).


6. Обработка ошибок

Все ошибки наследуются от EidError (errors.ts):

Ошибка Значение Поля
AuthenticationError HTTP 401 — API-секрет неверен или отсутствует
ForbiddenError HTTP 403 — не разрешено по IP-allowlist / 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" | …  → сообщение в UI, повтор
  } 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) Собирает payload ACSP_V2 — используется валидатором.

⚠️ Payload ACSP_V2 должен быть побайтово идентичен на сервере (server/internal/crypto/acsp.go), на телефоне и в RP SDK. LP(x) = 4-байтовая длина big-endian ‖ x.


8. mTLS (квалифицированная среда eIDAS)

Если продуктивный RP-API требует клиентский сертификат, передайте Agent из undici через dispatcher (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 в точности следует HTTP-контракту из docs/RP_INTEGRATION.md — эндпойнты, поля тела (relyingPartyUUID, relyingPartyName, certificateLevel, signatureProtocol: "ACSP_V2", interactions) и Bearer-аутентификация полностью совпадают. Чтобы интегрироваться напрямую по HTTP без SDK либо посмотреть полный список эндпойнтов и идентификаторов гражданина:


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).