397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
58 lines
2.8 KiB
JavaScript
58 lines
2.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* receipt-sign — подпись/проверка расписок и хеш-первооснова Журнала.
|
|
* canonicalJson: стабильная сериализация (сорт ключей, без пробелов) — иначе
|
|
* HMAC/хеш зависят от порядка ключей. signPayload/verifyReceipt — HMAC-SHA256.
|
|
*/
|
|
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
|
|
/**
|
|
* R-31 — домены подписи: каждый тип записи подписывается со своей меткой, чтобы
|
|
* подпись одного типа (напр. голова журнала) НЕ принималась за подпись другого
|
|
* (напр. замороженный план). Метка примешивается в HMAC до содержания.
|
|
*/
|
|
export const RECEIPT_DOMAINS = Object.freeze({
|
|
JOURNAL_HEAD: 'journal-head',
|
|
FROZEN_PLAN: 'frozen-plan',
|
|
FROZEN_ARTIFACT: 'frozen-artifact',
|
|
APPROVAL: 'approval',
|
|
STEP_PTR: 'step-ptr',
|
|
M5_GREEN: 'm5-green',
|
|
VERIFY_PASS: 'verify-pass',
|
|
FLOOR_ESCAPE: 'floor-escape',
|
|
JUDGE_GO: 'judge-go',
|
|
MENTOR_GO: 'mentor-go',
|
|
});
|
|
|
|
/** Стабильная сериализация: ключи объектов сортируются рекурсивно, без пробелов. */
|
|
export function canonicalJson(value) {
|
|
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
if (Array.isArray(value)) return '[' + value.map(canonicalJson).join(',') + ']';
|
|
const keys = Object.keys(value).sort();
|
|
return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonicalJson(value[k])).join(',') + '}';
|
|
}
|
|
|
|
/** HMAC-SHA256(hex) над `domain\0 + canonicalJson(payload)`. Без ключа → null.
|
|
* domain='' по умолчанию — обратная совместимость. */
|
|
export function signPayload(payload, key, domain = '') {
|
|
if (!key) return null;
|
|
return createHmac('sha256', String(key)).update(String(domain) + ' |