3795fcfe0a
- receipt-sign.mjs: +домен M5_GREEN (отдельный, не default; cross-domain тест) - floor-signer.mjs (новый): signGreen/verifyGreen/acceptGreen — подпись по-критерийного GREEN в домене M5_GREEN, occurrence монотонный (анти-пере-зачёт одного green на 2 критерия), подмена criterion_id/occurrence ломает HMAC → bad-signature. - Δ4 честно: подписант = анти-казуальная утечка ключа + шов под HSM, НЕ защита ключа от мотивированного контроллера (node tools/X.mjs до Пакета 4); см. spec §7/§10. - Чистые функции (ключ-аргумент), как receipt-sign.mjs; процесс-обёртка/резолв ключа — Пакет 4+; потребитель держит lastOccurrence — Пакет 5. - TDD RED-GREEN; tools-only регрессия 2666 passed + 2 skip (+17). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
54 lines
2.7 KiB
JavaScript
54 lines
2.7 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',
|
|
});
|
|
|
|
/** Стабильная сериализация: ключи объектов сортируются рекурсивно, без пробелов. */
|
|
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) + ' |