Files
brain/tools/floor-signer.mjs
T

66 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* floor-signer (Машина 5, Блок 2) — подписант по-критерийного GREEN.
*
* ЧЕСТНАЯ ЦЕННОСТЬ (Δ4, факт-аудит кода spec §10):
* Подписант НЕ делает HMAC-ключ нечитаемым для мотивированного контроллера.
* Факт: ключ в OS-keychain читается любым процессом того же пользователя
* (receipt-key-config.mjs), а `node <файл>` в whitelist гейта (до сужения в
* Пакете 4) → контроллер может написать свой `tools/X.mjs` и прочесть ключ.
* Поэтому подписант даёт ДВЕ реальные вещи, не больше:
* (а) убирает ключ из логов/env (анти-казуальная утечка);
* (б) ШОВ ПОД HSM/YubiKey (v4.2) — туда встанет железо, и подделка станет
* реально невозможной (см. spec §7, ~0.5% неустранимо без железа).
* Блок 2 НЕ заявляет «защиту ключа от контроллера».
*
* occurrence-счётчик — анти-пере-зачёт: один зелёный прогон нельзя пере-зачесть
* на два критерия. Монотонность проверяется acceptGreen относительно
* lastOccurrence (чистая функция; состояние держит потребитель). Подделка
* criterion_id/occurrence/fingerprint после подписи аннулирует sig (HMAC привязан
* к полному payload в домене M5_GREEN).
*
* Чистые функции (ключ — аргумент), как receipt-sign.mjs. «Только подписант
* держит ключ» — операционное свойство процесса-обёртки (Пакет 4+), не этого
* модуля.
*/
import { signPayload, verifyReceipt, RECEIPT_DOMAINS } from './receipt-sign.mjs';
/**
* Подписать по-критерийный GREEN в домене M5_GREEN.
* @param {{criterion_id:string, code_fingerprint:string, occurrence:number}} payload
* @param {string} key — ключ подписанта; без ключа → null (fail-closed).
* @returns {{criterion_id, code_fingerprint, occurrence, sig}|null}
*/
export function signGreen(payload, key) {
if (!key) return null;
const body = {
criterion_id: payload.criterion_id,
code_fingerprint: payload.code_fingerprint,
occurrence: payload.occurrence,
};
const sig = signPayload(body, key, RECEIPT_DOMAINS.M5_GREEN);
if (!sig) return null;
return { ...body, sig };
}
/** Проверить подпись GREEN в домене M5_GREEN. Без sig/ключа → false (fail-closed). */
export function verifyGreen(record, key) {
return verifyReceipt(record, key, RECEIPT_DOMAINS.M5_GREEN);
}
/**
* Принять GREEN: подпись валидна И occurrence строго больше последнего принятого.
* lastOccurrence — состояние потребителя (по умолчанию 0). Защищает от:
* - подделки (подмена criterion_id/occurrence ломает sig) → 'bad-signature';
* - нецелого/неположительного occurrence → 'bad-occurrence';
* - пере-зачёта одного green (occurrence <= lastOccurrence) → 'stale-occurrence'.
* @returns {{accepted:boolean, reason:string}}
*/
export function acceptGreen(record, key, { lastOccurrence = 0 } = {}) {
if (!verifyGreen(record, key)) return { accepted: false, reason: 'bad-signature' };
const occ = record.occurrence;
if (!Number.isInteger(occ) || occ <= 0) return { accepted: false, reason: 'bad-occurrence' };
if (occ <= lastOccurrence) return { accepted: false, reason: 'stale-occurrence' };
return { accepted: true, reason: 'ok' };
}