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. Установка¶
- 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-секрет (rp_sk_…) — секрет показывается только один раз.
Передайте их в RpCredentials:
| Поле | Значение |
|---|---|
rpUUID |
UUID RP, зарегистрированный в relying_parties |
rpName |
Имя RP, показываемое гражданину (≤ 32 байт UTF-8) |
apiSecret |
API-секрет (rp_sk_…) — только на бэкенде |
Исходник: types.ts — RpCredentials.
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)
выполняет следующие шаги:
state === COMPLETE && endResult === OK(иначеSessionFailedError).- Проверяет сертификат гражданина, выстраивая цепочку до trust anchor (Gerege Root CA) (издатель должен быть CA, глубина ≤ 8).
- Сертификат находится в пределах срока действия (
validFrom/validTo, с допускомclockSkewMs) и удовлетворяет требуемомуcertificateLevel(по умолчаниюQUALIFIED). - Проверяет подпись публичным ключом гражданина:
- auth → по payload 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 |
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 либо посмотреть полный список эндпойнтов и идентификаторов гражданина:
- Руководство по интеграции RP (raw HTTP)
- Рабочий пример RP-клиента:
web/src/lib/rpclient.ts
10. Разработка¶
Пакет: sdk/typescript/.
TypeScript — эталонная реализация: версии на Go/Python следуют её API-поверхности, модели
безопасности (ResponseValidator) и контракту протокола
(sdk/README.md).