feat(verify-gate): verify-receipt pure core sign/accept (G1 task2)

This commit is contained in:
Дмитрий
2026-06-09 10:13:57 +03:00
parent 030ceba1d0
commit 7dc97a3ea3
2 changed files with 76 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env node
/**
* verify-receipt (G1, Level A) — чистое ядро подписанной verify-расписки (домен VERIFY_PASS).
* Зеркало floor-signer.mjs. Подлинность — подпись подписанта; свежесть — совпадение
* code_fingerprint с ТЕКУЩИМ staged-diff (считает consumer/producer, инъекция currentFingerprint).
* occurrence — структурный positive-int (анти-двойной-зачёт = Level B). Δ4: ключ не защищён от
* мотивированного контроллера (см. план G1 §Forge); подпись даёт out-of-env + шов под HSM.
*/
import { signPayload, verifyReceipt, RECEIPT_DOMAINS } from './receipt-sign.mjs';
export function signVerifyReceipt({ code_fingerprint, occurrence }, key) {
if (!key) return null;
const body = { code_fingerprint, occurrence };
const sig = signPayload(body, key, RECEIPT_DOMAINS.VERIFY_PASS);
if (!sig) return null;
return { ...body, sig };
}
export function verifyReceiptSig(record, key) {
return verifyReceipt(record, key, RECEIPT_DOMAINS.VERIFY_PASS);
}
/**
* Принять расписку: подпись валидна И occurrence — целое >0 И (если задан currentFingerprint)
* code_fingerprint совпадает с ним. Любое нарушение → {accepted:false, reason}.
*/
export function acceptVerifyReceipt(record, key, { currentFingerprint = null } = {}) {
if (!verifyReceiptSig(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 (currentFingerprint !== null && record.code_fingerprint !== currentFingerprint) {
return { accepted: false, reason: 'stale-fingerprint' };
}
return { accepted: true, reason: 'ok' };
}
+41
View File
@@ -0,0 +1,41 @@
import { describe, it, expect } from 'vitest';
import { signVerifyReceipt, verifyReceiptSig, acceptVerifyReceipt } from './verify-receipt.mjs';
const KEY = 'signer-key';
const FP = 'a'.repeat(64);
describe('signVerifyReceipt', () => {
it('подписывает {code_fingerprint, occurrence}; без ключа → null', () => {
expect(signVerifyReceipt({ code_fingerprint: FP, occurrence: 1 }, '')).toBe(null);
const r = signVerifyReceipt({ code_fingerprint: FP, occurrence: 1 }, KEY);
expect(r.code_fingerprint).toBe(FP);
expect(r.occurrence).toBe(1);
expect(typeof r.sig).toBe('string');
expect(verifyReceiptSig(r, KEY)).toBe(true);
});
});
describe('acceptVerifyReceipt', () => {
const good = () => signVerifyReceipt({ code_fingerprint: FP, occurrence: 1 }, KEY);
it('валидная подпись + occurrence>0 + fingerprint совпал → accepted', () => {
expect(acceptVerifyReceipt(good(), KEY, { currentFingerprint: FP })).toEqual({ accepted: true, reason: 'ok' });
});
it('подмена fingerprint после подписи → bad-signature', () => {
const r = { ...good(), code_fingerprint: 'b'.repeat(64) };
expect(acceptVerifyReceipt(r, KEY, { currentFingerprint: 'b'.repeat(64) }).accepted).toBe(false);
});
it('staged-diff изменился (current ≠ receipt) → stale-fingerprint', () => {
expect(acceptVerifyReceipt(good(), KEY, { currentFingerprint: 'c'.repeat(64) }))
.toEqual({ accepted: false, reason: 'stale-fingerprint' });
});
it('occurrence не целое/≤0 → bad-occurrence', () => {
const r = signVerifyReceipt({ code_fingerprint: FP, occurrence: 0 }, KEY);
expect(acceptVerifyReceipt(r, KEY, { currentFingerprint: FP }).reason).toBe('bad-occurrence');
});
it('без ключа → bad-signature (fail-closed)', () => {
expect(acceptVerifyReceipt(good(), '', { currentFingerprint: FP }).accepted).toBe(false);
});
it('currentFingerprint не задан (null) → проверка свежести пропущена', () => {
expect(acceptVerifyReceipt(good(), KEY).accepted).toBe(true);
});
});