Verifier
Приём готового утверждения от бумажника
Бумажник присылает уже выпущенные claim-конверты. Сервис проверяет подпись эмитента через DNS-якорь (с DNSSEC, если включён strict), срок действия, отзыв и заставляет пользователя доказать владение subject-ключом по challenge/response.
import express from 'express';
import { Verifier, type Session } from '@persona-claims/verifier';
import { ClaimSchema, ConfirmationSchema, dns } from '@persona-claims/core';
// 1. Один Verifier на процесс. publicKey — публичный ключ нашего верификатора,
// с которым у выпущенных нам claim'ов связано поле `ref` (опционально).
// dnsPolicy: 'strict' требует DNSSEC-доказательства для TXT-записей эмитентов.
const verifier = new Verifier({
publicKey: process.env.VERIFIER_PUBLIC_KEY!,
resolver: new dns.NodeResolver(),
dnsPolicy: 'strict'
});
// Сессии бумажника хранятся между двумя запросами (start → verify).
// В проде — Redis/БД; здесь — Map для краткости.
const sessions = new Map<string, Session>();
const app = express();
app.use(express.json());
// 2. Бумажник кладёт claim'ы. Verifier по спеке возвращает ОДИН nonce и
// список уникальных subject-ключей, для которых нужны подтверждения.
app.post('/api/persona/start', (req, res) => {
const claims = req.body.claims.map((c: unknown) => ClaimSchema.parse(c));
const session = verifier.start(claims);
sessions.set(req.body.sessionId, session);
res.json({
challenge: session.challenge, // одно значение-задача на сессию
subjectKeys: session.subjectKeys // одно подтверждение на каждый ключ, не на claim
});
});
// 3. Бумажник возвращает confirmations[] — по одной на уникальный subject-ключ.
// Каждая confirmation — это подпись общего challenge соответствующим ключом.
app.post('/api/persona/verify/:sessionId', async (req, res) => {
const session = sessions.get(req.params.sessionId);
if (!session) return res.status(400).json({ error: 'нет активной сессии' });
sessions.delete(req.params.sessionId);
const confirmations = req.body.confirmations.map((c: unknown) => ConfirmationSchema.parse(c));
const status = await session.verify(confirmations);
if (!status.ok) {
// status.results[].reason — причина отказа: 'claim expired', 'claim revoked',
// 'invalid issuer signature', 'proof of possession failed', 'unknown challenge' и т.п.
return res.status(400).json({ error: 'не прошло проверку', results: status.results });
}
await allowUserAction(req.params.sessionId, status.results);
res.json({ ok: true });
});