Web RP demo — руководство разработчика¶
Браузерное RP-демо платформы eID — пример доверяющей стороны на Next.js
(App Router, TypeScript). Оно демонстрирует в браузере сценарий входа через
QR / push по РД с помощью мобильного приложения eID Mongolia и наложения
квалифицированной электронной подписи на PDF. Исходники: web/ (точный порт
Java-демо smartid-demo: WebDemo.java + index.html — те же экраны, тот же
сценарий, те же монгольские подписи).
Это настоящий RP. В отличие от клиентов для macOS/iOS, само веб-приложение хранит RP-секрет:
RP_API_SECRETдля Go RP-API (/v3/*) используется только в серверных route handlers (web/src/lib/rpclient.ts). Браузер никогда не видит этот секрет — все вызовы проксируются через публичные маршруты/api/*веб-приложения. Подробнее об интеграции RP: Интеграция RP; о десктопном клиенте, использующем те же/api/*: macOS desktop.
1. Что демонстрируется¶
- Аутентификация — QR-код (
/api/start) либо push по РД / civil ID (/api/login-notify). Подтверждение — PIN1 в приложении на телефоне. Серверный маршрут криптографически проверяет подпись над rpChallenge, затем извлекает из subject сертификата имя, civil ID иdocumentNumberи возвращает их (браузер сертификат не разбирает). - Подпись PDF — выберите PDF, его дайджест SHA-256 вычисляется локально на клиенте,
и подпись выполняется с PIN2 (ключ подписи, неотказуемость) в приложении на телефоне.
Бэкенд возвращает проштампованный PDF (PAdES / PKCS#7 + страница проверки)
(
/api/sign-pdf-download). - Представительство организации — после входа физического лица выбор
«от имени какой организации продолжить» подтягивается из реестра в реальном времени
(
/api/representations, аналог эстонского äriregister).
Симуляция «виртуального телефона» не работает. Java-демо выполняло настоящую пороговую ECDSA через JVM SDK
VirtualPhone/PhoneCrypto. Эта криптография существует только на стороне JVM, поэтому/api/simulate-phoneвозвращает501(dev) /404(prod) (web/src/app/api/simulate-phone/route.ts). Реальный сценарий: подтвердить QR/push в приложении eID Mongolia.
2. Архитектура¶
Браузер (page.tsx, demo/page.tsx)
│ fetch /api/* (same-origin, без секрета)
▼
Route handler Next.js (web/src/app/api/**, на сервере)
│ rpclient.ts: Authorization: Bearer <RP_API_SECRET>
▼
Go RP-API (/v3/*, RP_API_BASE)
Java-демо обращалось к RP-API из встроенного лёгкого HTTP-сервера. В этом порте каждый
вспомогательный бэкенд-эндпойнт стал route handler Next.js, который вызывает
Go RP-API (/v3/...) со стороны сервера (без CORS, логика та же, что в Java).
Контракт протокола 1:1 соответствует Go-DTO (server/internal/dto/*.go): ответы содержат
sessionID, vc.value, result.endResult, result.documentNumber,
cert.value (base64 DER), cert.certificateLevel.
Где хранятся секрет и идентификаторы RP¶
| Значение | Где | Описание |
|---|---|---|
RP_API_SECRET |
env, только на сервере | Отправляется функцией authHeaders() из rpclient.ts как Authorization: Bearer. Не NEXT_PUBLIC_, поэтому не попадает в клиентский бандл. Если не задан, заголовок не добавляется (dev / RP-auth отключён). |
RP_UUID / RP_NAME |
константы в rpclient.ts |
Не env — константы времени компиляции (RP_UUID = "2d87bd3a-…", RP_NAME = "Demo Bank"). Заранее зарегистрированы в relying_parties, стабильны между перезапусками. |
rpChallenge |
на сервере, случайное | 64 случайных байта (base64) на каждый запрос. Сохраняется в challengeStore по ключу sessionId и используется для проверки подписи AUTH по ACSP_V2 в /status. |
pollToken(защита PII). ПосколькуsessionIdраскрывается в QR-коде, сам по себе он не даёт права читать имя/регистрационный номер/подпись. Каждый ответstart/login-notify/sign-pdf-startвозвращаетpollToken, и маршруты/api/status,/api/sign-pdf-downloadтребуют этот токен (web/src/lib/pollTokenStore.ts).
3. Маршруты /api/*¶
Публичный демо-сценарий RP (page.tsx, demo/page.tsx, клиент macOS)¶
| Маршрут | Метод | Что делает | Эндпойнт Go RP-API |
|---|---|---|---|
/api/start |
POST | Запускает анонимную QR-сессию → {sessionId, qr, deviceLinkBase, vc, pollToken} (qr = sessionId) |
POST /v3/authentication/device-link/anonymous |
/api/login-notify |
POST | {register, callbackUrl} — push по РД / civil ID → {sessionId, vc, pollToken}. Rate limit 3 за 60 с на цель |
POST /v3/authentication/notification/etsi/{etsi} |
/api/status |
GET | ?sessionId=&pollToken= long-poll (сервер держит ~1 с). При COMPLETE/OK проверяет подпись AUTH над rpChallenge и извлекает из сертификата name/idNumber/documentNumber |
GET /v3/session/{id}?timeoutMs=1000 |
/api/sign-start |
POST | {etsi, doc} — push для подписи текстового документа (PIN2) → {sessionId, vc, doc, pollToken}. Rate limit 3 за 60 с |
POST /v3/signature/notification/etsi/{etsi} |
/api/sign-pdf-start |
POST | {etsi, digestB64, fileName, onBehalfOf?} — сессия для подписи дайджеста SHA-256 PDF с PIN2 → {sessionId, vc, pollToken}. Дайджест должен быть 32 байта; rate limit 3 за 60 с |
POST /v3/signature/notification/etsi/{etsi} |
/api/sign-pdf-download |
POST | multipart file + sessionId + pollToken → байты проштампованного PDF (вложение application/pdf) |
POST /v3/signature/stamp/{sessionId} |
/api/representations |
POST | {personId} — организации в статусе ACTIVE, которые лицо может представлять → {personEtsi, representations}. Rate limit 10 за 60 с |
GET /v3/organization/representations/etsi/{personEtsi} |
/api/simulate-phone |
POST | Заглушка — 501 (dev) / 404 (prod). Требует настоящий JVM SDK пороговой ECDSA |
— |
/api/health |
GET | {status:"ok", service:"eidmongolia-web"} |
— |
Прокси-сценарий /demo/live (web/src/app/api/demo/*)¶
Тонкий прокси для страницы живого демо на публичном сайте — он возвращает другой формат
с ответами в snake_case, но по-прежнему проходит через rpclient.ts (RP-API /v3).
| Маршрут | Метод | Что делает |
|---|---|---|
/api/demo/auth/init |
POST | Анонимная device-link аутентификация → {session_id, device_link_url, control_code, poll_token, expires_at} (device_link_url = сырой sessionId) |
/api/demo/auth/poll |
GET | ?id=&poll_token= — опрос сессии, проверка подписи AUTH и возврат идентичности |
/api/demo/sign/init |
POST | multipart file + заголовок x-eid-token (documentNumber, полученный при аутентификации) — вычисляет SHA-256 PDF и открывает церемонию PIN2 → {session_id, document_hash, verification_code} |
/api/demo/sign/poll |
GET | ?id= + x-eid-token — при COMPLETE/OK возвращает отделённую подпись ECDSA signature_hex; страница сама встраивает её в PDF через pdf-lib (здесь нет /download) |
4. Запуск¶
cd web
npm install
npm run dev # http://localhost:3000
npm run build # проверка продакшн-сборки
npm run lint
npm run dev запускает web на :3000 и консоль администратора на :3001 (CLAUDE.md).
Переменные окружения¶
| Переменная | По умолчанию | Описание |
|---|---|---|
RP_API_BASE |
— | Базовый URL Go RP-API (только на сервере). Route handler сам добавляет /v3. Если не задан в продакшене, rpclient.ts выбрасывает ошибку |
NEXT_PUBLIC_API_BASE |
— | Легаси-fallback (совместимость). Читается, если RP_API_BASE не задан |
RP_API_SECRET |
— | Общий секрет RP. Должен совпадать с SMARTID_RP_API_SECRET на стороне Go. Если пуст, Bearer не добавляется (dev) |
NODE_ENV |
— | При production simulate-phone возвращает 404, а RP_API_BASE обязателен |
Dev-fallback. Если и
RP_API_BASE, иNEXT_PUBLIC_API_BASEпусты, происходит откат наhttp://localhost:8080/v3только в dev (rpclient.tsapiBase()). Поэтому для локального тестирования достаточно запустить Go-сервер на:8080.
Полный локальный стек (Go API + web):
cd server && SMARTID_RP_API_SECRET= go run ./cmd/smartid # Go API :8080 (RP-auth отключён)
cd web && npm run dev # web :3000
Приложение на телефоне также должно указывать на тот же Go-сервер (на симуляторе —
SMARTID_REQUIRE_ATTESTATION=false).
5. rpclient.ts — как он общается с RP-API¶
web/src/lib/rpclient.ts — копия Java-классов GeregeSmartIdRpClient + WebDemo.
Он выполняется только на сервере (из route handlers).
- Базовый URL —
apiBase()=RP_API_BASE(илиNEXT_PUBLIC_API_BASE) +/v3, с удалением завершающего/. - Аутентификация —
authHeaders()возвращаетAuthorization: Bearer <secret>, если заданRP_API_SECRET; иначе пустой заголовок (dev). Каждыйpost()/get()добавляет этот заголовок. - Тело запроса —
relyingPartyUUID,relyingPartyName,certificateLevel(QUALIFIED),signatureProtocol(ACSP_V2),interactions, а при необходимостиrpChallenge/digest+hashType/initialCallbackUrl/onBehalfOf. - Таймаут — 15 с для обычных вызовов, 140 с для long-poll (
AbortSignal.timeout). - Штамп PDF —
stampSignedPdf()отправляет исходный PDF вPOST /v3/signature/stamp/{sessionId}какapplication/pdfдля завершённой SIGN-сессии и получает обратно проштампованный PDF.
web/src/lib/x509subject.ts — минимальный парсер ASN.1 DER, который извлекает имя и
регистрационный номер (serialNumber) из subject сертификата (замена BouncyCastle из Java).
6. Ссылки¶
- Общее руководство по интеграции RP: Интеграция RP
- Десктопный клиент, использующий те же
/api/*: macOS desktop - Идентификаторы гражданина: IDENTIFIERS.md
Исходники (GitHub)¶
- Веб-демо:
https://github.com/gerege-systems/eid-platform-mn/tree/main/web - Клиент RP-API:
https://github.com/gerege-systems/eid-platform-mn/blob/main/web/src/lib/rpclient.ts - Route handlers:
https://github.com/gerege-systems/eid-platform-mn/tree/main/web/src/app/api /api/start:https://github.com/gerege-systems/eid-platform-mn/blob/main/web/src/app/api/start/route.ts/api/status:https://github.com/gerege-systems/eid-platform-mn/blob/main/web/src/app/api/status/route.ts/api/sign-pdf-download:https://github.com/gerege-systems/eid-platform-mn/blob/main/web/src/app/api/sign-pdf-download/route.ts
Next.js 16. Репозиторий использует Next.js 16 (с ломающими изменениями). Прежде чем писать код на Next.js, следуйте указаниям в
web/AGENTS.mdи сверяйтесь сnode_modules/next/dist/docs/, а не с данными обучения.