You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys-App/src/lib/inspector/report.test.ts

217 lines
10 KiB

import { describe, expect, it } from 'vitest';
import { defaultRegistry, type ExtensionRegistry, ExtensionSet, inspectView, inspectWith } from '../dkc/index.ts';
import { frame, replaceText, split } from '../dkc/testing/capsule.ts';
import { arr, b, dkRound, ext, map, t, u, bn } from '../dkc/testing/cborhex.ts';
import { h, listTestdata, readBytes, readJSON } from '../dkc/testing/testdata.ts';
import { cliJSON } from './format.ts';
import { buildReport, type Report } from './report.ts';
const registry = await defaultRegistry();
const timeOnly = readBytes('fixtures/time_only_extensions.dkc');
const parts = split(timeOnly);
function report(dkc: Uint8Array, extensions?: ExtensionRegistry, fileName = 'x.dkc'): Report {
const inspection = inspectWith(dkc, registry, extensions);
return buildReport(extensions === undefined ? { fileName, bytes: dkc, size: dkc.length, inspection } : { fileName, bytes: dkc, size: dkc.length, inspection, extensions });
}
// A PUBLIC_HEADER like the one of time_only_extensions, with other extensions.
function header(o: { crit?: string; non?: string; dk?: string }): Uint8Array {
const entries: [number, string][] = [
[0, t('datekeycap')],
[1, u(1)],
[2, bn(16, 7)],
[3, o.dk ?? t(dkRound(2000))],
[4, u(0)],
];
if (o.crit) entries.push([5, o.crit]);
if (o.non) entries.push([6, o.non]);
return h(map(...entries));
}
interface FixtureRecord {
format: number;
capsule_id: string;
datekey: string;
access_policy: string;
unlock_at: string;
prelude: string;
outer_stanzas: { type: string; args: string[] }[];
payload_stanzas: { type: string; args: string[] }[];
header_extensions?: { critical: boolean; id: string; version: number; data?: string }[];
}
describe('buildReport', () => {
it('carries the state of the profile that the caller gives, from the registry of §71', () => {
const dkc = readBytes('fixtures/time_only.dkc');
const inspection = inspectWith(dkc, registry);
expect(buildReport({ fileName: 'x.dkc', bytes: dkc, size: dkc.length, inspection }).profileStatus).toBeUndefined();
expect(buildReport({ fileName: 'x.dkc', bytes: dkc, size: dkc.length, inspection, profileStatus: 'compromised' }).profileStatus).toBe('compromised');
});
it('shows every official fixture as its JSON record', () => {
for (const f of listTestdata('fixtures', '.dkc')) {
const dkc = readBytes(f);
const want = readJSON<FixtureRecord>(f.replace(/\.dkc$/, '.json'));
const name = f.slice('fixtures/'.length);
const r = report(dkc, undefined, name);
expect(r.valid, f).toBe(true);
expect(r.failure).toBeUndefined();
expect(r.steps.map((s) => [s.step, s.state])).toEqual([1, 2, 3, 4, 5, 6, 7, 8].map((s) => [s, 'ok']));
expect(r.capsule).toMatchObject({ capsuleId: want.capsule_id, dateKey: want.datekey, accessPolicy: want.access_policy, network: 'datekeys:quicknet:v1' });
expect(JSON.parse(r.capsule!.dateKeyJSON)).toEqual({ version: 1, network: 'datekeys:quicknet:v1', round: r.capsule!.round });
expect(r.unlock?.rfc3339).toBe(want.unlock_at);
expect(r.unlock?.epochMs).toBe(Date.parse(want.unlock_at));
expect(r.prelude?.hex).toBe(want.prelude);
expect(r.prelude).toMatchObject({ magic: 'DKC1', format: want.format, flags: '0x00', reserved: '0x0000' });
expect(r.prelude!.payloadOffset + r.prelude!.payloadLength!).toBe(dkc.length);
expect(r.outerStanzas).toEqual(want.outer_stanzas);
expect(r.payloadStanzas).toEqual(want.payload_stanzas);
expect(r.tlock?.every((c) => c.match)).toBe(true);
expect(r.profile).toMatchObject({ id: 'datekeys:quicknet:v1', network: 'quicknet', provider: 'drand', period: 3, genesis: '2023-08-23T15:09:27Z' });
expect(r.extensions?.map((e) => ({ critical: e.critical, id: e.id, version: e.version, data: e.hex }))).toEqual(
(want.header_extensions ?? []).map((e) => ({ critical: e.critical, id: e.id, version: e.version, data: e.data ?? '' })),
);
expect(r.view).toEqual(inspectView(inspectWith(dkc, registry), name));
expect(r.json).toBe(cliJSON(r.view));
expect(r.view.file).toBe(name);
expect(r.readLength).toBe(dkc.length);
}
});
it('carries the public note, or that a note is not shown', () => {
const note = readBytes('fixtures/format3_note.dkc');
const base = inspectWith(note, registry);
expect(report(note).note).toBeUndefined(); // inspectWith does not read the note: inspect does
const r = (inspection: typeof base): Report => buildReport({ fileName: 'n.dkc', bytes: note, size: note.length, inspection });
expect(r({ ...base, publicNote: 'Cartas del viaje', unusableNote: false }).note).toEqual({ text: 'Cartas del viaje' });
expect(r({ ...base, unusableNote: true }).note).toEqual({ unusable: true });
expect(r(base).note).toBeUndefined();
});
it('shows header extension data as text, never as CBOR when it is not', () => {
const r = report(timeOnly);
expect(r.extensions).toEqual([
{
critical: false,
id: 'org.example.label',
idEscaped: false,
version: 1,
known: false,
length: 12,
hex: '7075626c6963206c6162656c',
text: 'public label',
},
]);
});
it('decodes CBOR data informatively and escapes identifiers', () => {
const hb = header({ non: arr(ext('a\u202eb', 3, b('a2000701667365616c6564')), ext('z', 1)) });
const r = report(frame({ ...parts, header: hb }));
expect(r.valid).toBe(true);
expect(r.extensions).toEqual([
{
critical: false,
id: '"a\\u202eb"',
idEscaped: true,
version: 3,
known: false,
length: 11,
hex: 'a2000701667365616c6564',
cbor: { text: '{\n 0: 7,\n 1: "sealed"\n}', truncated: false },
},
{ critical: false, id: 'z', idEscaped: false, version: 1, known: false, length: 0, hex: '' },
]);
});
it('reports an unknown critical extension at step 4 and still lists it', () => {
const hb = header({ crit: arr(ext('org.example.must-understand', 1, b('01'))) });
const r = report(frame({ ...parts, header: hb }));
expect(r.valid).toBe(false);
expect(r.failure).toEqual({
step: 4,
code: 'ERR_EXTENSION_CRITICAL_UNKNOWN',
message: 'capsule: PUBLIC_HEADER: extension org.example.must-understand v1: ERR_EXTENSION_CRITICAL_UNKNOWN',
});
expect(r.steps.map((s) => s.state)).toEqual(['ok', 'ok', 'ok', 'failed', 'not-run', 'not-run', 'not-run', 'not-run']);
expect(r.steps[3]).toMatchObject({ code: 'ERR_EXTENSION_CRITICAL_UNKNOWN', name: 'header validation' });
expect(r.steps[4]!.detail).toBeUndefined();
expect(r.extensions).toEqual([
{ critical: true, id: 'org.example.must-understand', idEscaped: false, version: 1, known: false, length: 1, hex: '01', cbor: { text: '1', truncated: false } },
]);
expect(r.capsule?.capsuleId).toBe('07'.repeat(16));
expect(r.unlock).toBeUndefined();
expect(r.outerStanzas).toBeUndefined();
expect(r.tlock).toBeUndefined();
expect(JSON.parse(r.json)).toMatchObject({ valid: false, error: 'ERR_EXTENSION_CRITICAL_UNKNOWN' });
});
it('marks known extensions and the unusable ones', () => {
const reg: ExtensionRegistry = {
known: (id) => id === 'a' || id === 'b',
validateData: (e) => (e.id === 'b' ? new Error('bad') : undefined),
};
const r = report(frame({ ...parts, header: header({ crit: arr(ext('a', 1, b('01'))), non: arr(ext('b', 1, b('02')), ext('c', 1)) }) }), reg);
expect(r.valid).toBe(true);
expect(r.extensions?.map((e) => [e.id, e.critical, e.known, e.unusable])).toEqual([
['a', true, true, undefined],
['b', false, true, 'extension b v1: data: bad: ERR_EXTENSION_DATA_INVALID'],
['c', false, false, undefined],
]);
const known = report(frame({ ...parts, header: header({ crit: arr(ext('a', 1)) }) }), new ExtensionSet([['a', [1]]]));
expect(known.extensions?.[0]?.known).toBe(true);
});
it('compares the tlock stanza with the pinned profile', () => {
const round = report(replaceText(timeOnly, '-> tlock 2000 ', '-> tlock 2001 '));
expect(round.failure?.code).toBe('ERR_ROUND_MISMATCH');
expect(round.unlock?.rfc3339).toBe('2023-08-23T16:49:24Z');
expect(round.tlock).toEqual([
{ label: 'Argumentos', found: '2', expected: '2', match: true },
{ label: 'Ronda', found: '2001', expected: '2000', match: false },
{
label: 'Cadena (chain hash)',
found: '52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971',
expected: '52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971',
match: true,
},
]);
const noChain = report(frame({ ...parts, sealed: replaceText(parts.sealed, ' 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971', '') }));
expect(noChain.tlock?.map((c) => [c.found, c.match])).toEqual([
['1', false],
['2000', true],
[undefined, false],
]);
const noTlock = report(frame({ ...parts, sealed: replaceText(parts.sealed, '-> tlock', '-> TLOCK') }));
expect(noTlock.failure?.step).toBe(5);
expect(noTlock.outerStanzas?.map((s) => s.type)).toEqual(['TLOCK']);
expect(noTlock.tlock).toBeUndefined();
});
it('keeps what the framing steps found', () => {
const r = report(timeOnly.subarray(0, 100));
expect(r.failure).toMatchObject({ step: 3, code: 'ERR_INTEGRITY' });
expect(r.prelude).toMatchObject({ publicHeaderLen: 159, sealedControlLen: 482, payloadOffset: 657 });
expect(r.prelude?.payloadLength).toBeUndefined();
expect(r.capsule).toBeUndefined();
expect(r.extensions).toBeUndefined();
const empty = report(new Uint8Array(0));
expect(empty.failure).toMatchObject({ step: 1, code: 'ERR_INVALID_MAGIC' });
expect(empty.prelude).toBeUndefined();
expect(empty.steps.filter((s) => s.state === 'not-run')).toHaveLength(7);
// An unknown profile: the header decoded, no profile, no comparison.
const other = report(frame({ ...parts, header: header({ dk: t(dkRound(2000, 'datekeys:evmnet:v1')) }) }));
expect(other.failure?.code).toBe('ERR_UNKNOWN_PROFILE');
expect(other.capsule?.network).toBe('datekeys:evmnet:v1');
expect(other.profile).toBeUndefined();
});
it('records the prefix that was read', () => {
const inspection = inspectWith(timeOnly, registry);
const r = buildReport({ fileName: 'big.dkc', bytes: timeOnly, size: timeOnly.length + 5_000_000, inspection });
expect(r.readLength).toBe(timeOnly.length);
expect(r.size).toBe(timeOnly.length + 5_000_000);
expect(r.prelude!.payloadLength).toBe(timeOnly.length + 5_000_000 - 657);
});
});

Powered by TurnKey Linux.