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.
217 lines
10 KiB
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);
|
|
});
|
|
});
|