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.
774 lines
24 KiB
774 lines
24 KiB
// The differential of the formats against the Go reference: every case of
|
|
// test/vectors/formats_*.json, which tool/formats_go_vectors.go writes with
|
|
// the packages capsule, accesskey, profile, extension, datekey and codec of
|
|
// datekeys-go and Go's time, run through the Dart code, with the same
|
|
// result, normative code and text. It reads no file itself, so that the
|
|
// tests compiled to JavaScript run it too, on the part of the cases that
|
|
// formats_vectors.g.dart holds; the cases that need a fixture run only when
|
|
// a reader of fixtures is given.
|
|
library;
|
|
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:datekeys/src/accesskey.dart';
|
|
import 'package:datekeys/src/body.dart';
|
|
import 'package:datekeys/src/bytes.dart';
|
|
import 'package:datekeys/src/cbor.dart';
|
|
import 'package:datekeys/src/control.dart';
|
|
import 'package:datekeys/src/datekey.dart';
|
|
import 'package:datekeys/src/errors.dart';
|
|
import 'package:datekeys/src/extension.dart';
|
|
import 'package:datekeys/src/framing.dart';
|
|
import 'package:datekeys/src/header.dart';
|
|
import 'package:datekeys/src/padding.dart';
|
|
import 'package:datekeys/src/profile.dart';
|
|
import 'package:datekeys/src/sha256.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
typedef Json = Map<String, Object?>;
|
|
|
|
/// The cases of [section] of [file].
|
|
List<Json> cases(Json file, String section) =>
|
|
(file[section]! as List).cast<Json>();
|
|
|
|
String str(Json c, String key) => c[key]! as String;
|
|
|
|
int number(Json c, String key) => c[key]! as int;
|
|
|
|
/// What [body] gives, as the vectors write it: `ok`, the normative code of a
|
|
/// [DateKeysException] with its text, or `error` and the text of an error
|
|
/// without a normative code, which this library throws as an
|
|
/// [ArgumentError], as Go returns it without a code.
|
|
(String, String?) outcomeOf(void Function() body) {
|
|
try {
|
|
body();
|
|
return ('ok', null);
|
|
} on DateKeysException catch (e) {
|
|
return (e.code.code, e.message);
|
|
} on ArgumentError catch (e) {
|
|
return ('error', '${e.message}');
|
|
}
|
|
}
|
|
|
|
/// Expects the outcome of [body] to be the one of the case [c]: its
|
|
/// `result` and its `text`, or those of [resultKey] and [textKey].
|
|
void expectOutcome(
|
|
Json c,
|
|
void Function() body, {
|
|
String resultKey = 'result',
|
|
String textKey = 'text',
|
|
}) {
|
|
final (result, text) = outcomeOf(body);
|
|
expect((result, text), (c[resultKey], c[textKey]), reason: '$c');
|
|
}
|
|
|
|
/// An extension of the vectors: [id, version, data in hexadecimal or null].
|
|
Extension extensionOf(Object? v) {
|
|
final l = v! as List<Object?>;
|
|
final data = l[2] as String?;
|
|
return Extension(
|
|
l[0]! as String,
|
|
l[1]! as int,
|
|
data == null ? null : fromHex(data),
|
|
);
|
|
}
|
|
|
|
List<Extension> extensionsOf(Object? v) => [
|
|
for (final e in v! as List<Object?>) extensionOf(e),
|
|
];
|
|
|
|
/// [exts] as the vectors write them.
|
|
List<Object?> extensionsJson(List<Extension> exts) => [
|
|
for (final e in exts)
|
|
[e.id, e.version, e.data == null ? null : toHex(e.data!)],
|
|
];
|
|
|
|
/// The objects of the extension registries, by their names in the
|
|
/// specification.
|
|
final objectsByName = {for (final o in ExtensionObject.values) o.specName: o};
|
|
|
|
final arraysByName = {for (final a in ExtensionArray.values) a.specName: a};
|
|
|
|
/// The registry placed of the generator: org.a v1 and org.b v1 known
|
|
/// everywhere; org.ctl v1 registered only in the critical array of
|
|
/// CONTROL_CBOR and org.note v1 only in the noncritical array of
|
|
/// PUBLIC_HEADER. Data is valid when it is "ok"; org.b with "ko!" is
|
|
/// rejected with an error of code ERR_EXTENSION_DATA_INVALID, as a note that
|
|
/// breaks its rules is, and any other data with a text without a code.
|
|
final class PlacedRegistry extends ExtensionRegistry {
|
|
const PlacedRegistry();
|
|
|
|
@override
|
|
bool known(String id, int version) =>
|
|
version == 1 &&
|
|
(id == 'org.a' || id == 'org.b' || id == 'org.ctl' || id == 'org.note');
|
|
|
|
@override
|
|
Object? validateData(Extension e) {
|
|
final data = e.data;
|
|
if (data != null && equalBytes(data, 'ok'.codeUnits)) return null;
|
|
if (e.id == 'org.b' && data != null && equalBytes(data, 'ko!'.codeUnits)) {
|
|
return DateKeysException(
|
|
ErrorCode.extensionDataInvalid,
|
|
'a data of ${data.length} bytes',
|
|
);
|
|
}
|
|
return 'want "ok"';
|
|
}
|
|
|
|
@override
|
|
bool registeredIn(
|
|
String id,
|
|
int version,
|
|
ExtensionObject obj,
|
|
ExtensionArray arr,
|
|
) => switch (id) {
|
|
'org.ctl' =>
|
|
obj == ExtensionObject.control && arr == ExtensionArray.critical,
|
|
'org.note' =>
|
|
obj == ExtensionObject.publicHeader && arr == ExtensionArray.noncritical,
|
|
_ => true,
|
|
};
|
|
}
|
|
|
|
/// The profile of the fields [f] of a vector.
|
|
Profile profileFrom(Json f) => Profile(
|
|
id: str(f, 'id'),
|
|
provider: str(f, 'provider'),
|
|
network: str(f, 'network'),
|
|
chainHash: fromHex(str(f, 'chain_hash')),
|
|
publicKey: fromHex(str(f, 'public_key')),
|
|
period: number(f, 'period'),
|
|
genesisTime: number(f, 'genesis_time'),
|
|
scheme: str(f, 'scheme'),
|
|
genesisSeed: fromHex(str(f, 'genesis_seed')),
|
|
);
|
|
|
|
/// The profile of the cases of times: Quicknet with [c]'s genesis time and
|
|
/// period.
|
|
Profile profileOf(Json c) => quicknet().copyWith(
|
|
genesisTime: number(c, 'genesis_time'),
|
|
period: number(c, 'period'),
|
|
);
|
|
|
|
/// Defines the tests of every section of [files], formats_<name>.json by
|
|
/// <name>. [testdata] reads a file of testdata/, such as
|
|
/// `fixtures/time_only.dkc`; without it, the cases that need a fixture are
|
|
/// left out.
|
|
void formatsDifferential(
|
|
Map<String, Json> files, {
|
|
Uint8List Function(String path)? testdata,
|
|
}) {
|
|
final framing = files['framing']!;
|
|
group('framing', () {
|
|
test('prelude', () {
|
|
for (final c in cases(framing, 'prelude')) {
|
|
expectOutcome(c, () {
|
|
final p = parsePrelude(fromHex(str(c, 'hex')));
|
|
expect(
|
|
[p.format.version, p.publicHeaderLen, p.sealedControlLen],
|
|
[c['format'], c['public_header_len'], c['sealed_control_len']],
|
|
);
|
|
expect(toHex(preludeBytes(p)), str(c, 'hex'));
|
|
});
|
|
}
|
|
});
|
|
|
|
if (testdata != null) {
|
|
test('split: steps 1 to 3 of the inspection', () {
|
|
for (final c in cases(framing, 'split')) {
|
|
var dkc = testdata('fixtures/${str(c, 'fixture')}.dkc')
|
|
.sublist(0, number(c, 'cut'));
|
|
for (final e in (c['edits'] as List<Object?>?) ?? const []) {
|
|
final edit = e! as List<Object?>;
|
|
final at = edit[0]! as int;
|
|
dkc = Uint8List.fromList(dkc)
|
|
..setAll(at, fromHex(edit[2]! as String));
|
|
}
|
|
final step = number(c, 'step');
|
|
try {
|
|
final s = splitCapsule(dkc);
|
|
expect(step == 0 || step > 3, isTrue, reason: '$c');
|
|
// Go finds a short SEALED_CONTROL at step 5; anything else
|
|
// that fails later has it whole.
|
|
final short =
|
|
step == 5 &&
|
|
c['text'] ==
|
|
'capsule: truncated SEALED_CONTROL: '
|
|
'ERR_INTEGRITY';
|
|
expect(s.sealedControl == null, short, reason: '$c');
|
|
if (short) {
|
|
expect(truncatedSealedControl().message, c['text']);
|
|
} else {
|
|
expect(s.sealedControl!.length, s.prelude.sealedControlLen);
|
|
}
|
|
expect(s.publicHeader.length, s.prelude.publicHeaderLen);
|
|
} on FramingException catch (e) {
|
|
expect(
|
|
(e.step, e.error.code.code, e.error.message),
|
|
(step, c['result'], c['text']),
|
|
reason: '$c',
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
test('dkk', () {
|
|
for (final c in cases(framing, 'dkk')) {
|
|
final input = fromHex(str(c, 'hex'));
|
|
expectOutcome(c, () {
|
|
final k = decodeAccessKey(input);
|
|
expect(
|
|
[
|
|
toHex(k.credentialId),
|
|
toHex(k.capsuleId),
|
|
k.type,
|
|
toHex(k.material),
|
|
k.verification == null
|
|
? null
|
|
: toHex(k.verification!.capsuleDigest),
|
|
extensionsJson(k.critical),
|
|
extensionsJson(k.noncritical),
|
|
],
|
|
[
|
|
c['credential_id'],
|
|
c['capsule_id'],
|
|
c['type'],
|
|
c['material'],
|
|
c['capsule_digest'],
|
|
c['critical'],
|
|
c['noncritical'],
|
|
],
|
|
);
|
|
if (c.containsKey('encode_result')) {
|
|
expectOutcome(
|
|
c,
|
|
() => encodeAccessKey(k),
|
|
resultKey: 'encode_result',
|
|
textKey: 'encode_text',
|
|
);
|
|
} else {
|
|
expect(toHex(encodeAccessKey(k)), str(c, 'hex'));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// Every fault of a list, alone and with every other, on a valid object:
|
|
// the code of the first layer of spec §69.1 that fails.
|
|
test('precedence of the layers of spec §69.1', () {
|
|
for (final c in cases(files['header']!, 'precedence')) {
|
|
expectOutcome(c, () => decodeHeader(fromHex(str(c, 'hex'))));
|
|
}
|
|
for (final c in cases(files['control']!, 'precedence')) {
|
|
final format = CapsuleFormat.fromVersion(number(c, 'format'))!;
|
|
expectOutcome(c, () => decodeControl(fromHex(str(c, 'hex')), format));
|
|
}
|
|
for (final c in cases(files['profile']!, 'precedence')) {
|
|
expectOutcome(c, () => decodeProfile(fromHex(str(c, 'hex'))));
|
|
}
|
|
for (final c in cases(framing, 'dkk_precedence')) {
|
|
expectOutcome(c, () => decodeAccessKey(fromHex(str(c, 'hex'))));
|
|
}
|
|
});
|
|
|
|
test('header', () {
|
|
for (final c in cases(files['header']!, 'header')) {
|
|
expectOutcome(c, () {
|
|
final h = decodeHeader(fromHex(str(c, 'hex')));
|
|
expect(
|
|
[
|
|
h.capsuleIdHex,
|
|
compactDateKey(h.dateKey),
|
|
h.policy.label,
|
|
extensionsJson(h.critical),
|
|
extensionsJson(h.noncritical),
|
|
],
|
|
[
|
|
c['capsule_id'],
|
|
c['datekey'],
|
|
c['policy'],
|
|
c['critical'],
|
|
c['noncritical'],
|
|
],
|
|
);
|
|
expect(toHex(encodeHeader(h)), str(c, 'hex'));
|
|
});
|
|
}
|
|
});
|
|
|
|
test('control', () {
|
|
for (final c in cases(files['control']!, 'control')) {
|
|
final format = CapsuleFormat.fromVersion(number(c, 'format'))!;
|
|
expectOutcome(c, () {
|
|
final ctl = decodeControl(fromHex(str(c, 'hex')), format);
|
|
expect(
|
|
[
|
|
toHex(ctl.headerBinding),
|
|
toHex(ctl.payloadIdentity),
|
|
ctl.payloadLength,
|
|
ctl.padding?.code,
|
|
extensionsJson(ctl.critical),
|
|
extensionsJson(ctl.noncritical),
|
|
],
|
|
[
|
|
c['header_binding'],
|
|
c['payload_identity'],
|
|
c['payload_length'],
|
|
c['padding'],
|
|
c['critical'],
|
|
c['noncritical'],
|
|
],
|
|
);
|
|
expect(toHex(encodeControl(ctl, format)), str(c, 'hex'));
|
|
});
|
|
}
|
|
});
|
|
|
|
final profiles = files['profile']!;
|
|
group('profile', () {
|
|
Json fields(Profile p) => {
|
|
'id': p.id,
|
|
'provider': p.provider,
|
|
'network': p.network,
|
|
'chain_hash': toHex(p.chainHash),
|
|
'public_key': toHex(p.publicKey),
|
|
'period': p.period,
|
|
'genesis_time': p.genesisTime,
|
|
'scheme': p.scheme,
|
|
'genesis_seed': toHex(p.genesisSeed),
|
|
'max_round': p.maxRound,
|
|
};
|
|
|
|
test('decode', () {
|
|
for (final c in cases(profiles, 'decode')) {
|
|
expectOutcome(c, () {
|
|
final p = decodeProfile(fromHex(str(c, 'hex')));
|
|
expect(fields(p), c['profile']);
|
|
expect(toHex(canonicalCbor(p)), str(c, 'hex'));
|
|
});
|
|
}
|
|
});
|
|
|
|
test('validate', () {
|
|
for (final c in cases(profiles, 'validate')) {
|
|
final f = c['profile']! as Json;
|
|
final p = profileFrom(f);
|
|
expect(p.maxRound, f['max_round'], reason: '$c');
|
|
expectOutcome(c, () => validateProfile(p));
|
|
}
|
|
});
|
|
});
|
|
|
|
final extensions = files['extension']!;
|
|
group('extension', () {
|
|
test('array', () {
|
|
for (final c in cases(extensions, 'array')) {
|
|
expectOutcome(c, () {
|
|
late List<Extension> exts;
|
|
unmarshalCbor(
|
|
fromHex(str(c, 'hex')),
|
|
(d) => exts = decodeExtensionArray(d),
|
|
(e) => encodeExtensionArray(e, exts),
|
|
);
|
|
expect(extensionsJson(exts), c['extensions']);
|
|
});
|
|
}
|
|
});
|
|
|
|
test('canonical', () {
|
|
for (final c in cases(extensions, 'canonical')) {
|
|
expectOutcome(c, () {
|
|
final sorted = canonicalExtensions(extensionsOf(c['extensions']));
|
|
expect(extensionsJson(sorted), c['sorted']);
|
|
});
|
|
}
|
|
});
|
|
|
|
test('disjoint', () {
|
|
for (final c in cases(extensions, 'disjoint')) {
|
|
expectOutcome(
|
|
c,
|
|
() => checkDisjoint(
|
|
extensionsOf(c['critical']),
|
|
extensionsOf(c['noncritical']),
|
|
),
|
|
);
|
|
}
|
|
});
|
|
|
|
test('registry', () {
|
|
for (final c in cases(extensions, 'registry')) {
|
|
final reg = c['registry'] == 'none' ? null : const PlacedRegistry();
|
|
final obj = objectsByName[c['object']];
|
|
expectOutcome(
|
|
c,
|
|
() => checkCritical(extensionsOf(c['critical']), reg, obj),
|
|
);
|
|
final unusable = checkNoncritical(
|
|
extensionsOf(c['noncritical']),
|
|
reg,
|
|
obj,
|
|
);
|
|
expect(
|
|
[
|
|
for (final u in unusable)
|
|
[u.id, u.version, u.error.message, u.error.code],
|
|
],
|
|
[
|
|
for (final u in c['unusable']! as List<Object?>)
|
|
[...(u! as List<Object?>), ErrorCode.extensionDataInvalid],
|
|
],
|
|
reason: '$c',
|
|
);
|
|
}
|
|
});
|
|
|
|
test('write', () {
|
|
for (final c in cases(extensions, 'write')) {
|
|
expectOutcome(
|
|
c,
|
|
() => checkWrite(
|
|
// Go's extension.Standard, without ValidateCapsule.
|
|
const StandardExtensions(validateCapsule: null),
|
|
objectsByName[c['object']]!,
|
|
arraysByName[c['array']]!,
|
|
extensionsOf(c['extensions']),
|
|
),
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
test('datekey', () {
|
|
for (final c in cases(files['datekey']!, 'parse')) {
|
|
expectOutcome(c, () {
|
|
final d = parseDateKey(str(c, 'input'));
|
|
expect([d.profileId, d.round], [c['network'], c['round']]);
|
|
});
|
|
}
|
|
});
|
|
|
|
final times = files['time']!;
|
|
group('time', () {
|
|
test('parse', () {
|
|
for (final c in cases(times, 'parse')) {
|
|
Instant? t;
|
|
try {
|
|
t = parseRfc3339(str(c, 'input'));
|
|
} on FormatException {
|
|
t = null;
|
|
}
|
|
expect(
|
|
t == null ? ['error'] : ['ok', t.seconds, t.nanos],
|
|
c['result'] == 'ok'
|
|
? ['ok', c['seconds'], c['nanos']]
|
|
: [c['result']],
|
|
reason: '$c',
|
|
);
|
|
}
|
|
});
|
|
|
|
test('format', () {
|
|
for (final c in cases(times, 'format')) {
|
|
final t = Instant(number(c, 'seconds'), number(c, 'nanos'));
|
|
expect(
|
|
[formatRfc3339(t), formatRfc3339Nano(t)],
|
|
[c['rfc3339'], c['rfc3339nano']],
|
|
);
|
|
}
|
|
});
|
|
|
|
test('resolve', () {
|
|
for (final c in cases(times, 'resolve')) {
|
|
final p = profileOf(c);
|
|
expectOutcome(c, () {
|
|
final d = resolveDateKey(
|
|
p,
|
|
Instant(number(c, 'seconds'), number(c, 'nanos')),
|
|
);
|
|
expect(d.profileId, p.id);
|
|
expect(
|
|
[d.round, roundTime(p, d.round).seconds],
|
|
[c['round'], c['round_time']],
|
|
);
|
|
});
|
|
}
|
|
});
|
|
|
|
test('round_time', () {
|
|
for (final c in cases(times, 'round_time')) {
|
|
expectOutcome(c, () {
|
|
final t = roundTime(profileOf(c), number(c, 'round'));
|
|
expect([t.seconds, t.nanos], [c['seconds'], 0]);
|
|
});
|
|
}
|
|
});
|
|
|
|
test('validate', () {
|
|
for (final c in cases(times, 'validate')) {
|
|
expectOutcome(
|
|
c,
|
|
() => validateDateKey(
|
|
DateKey(str(c, 'profile_id'), number(c, 'round')),
|
|
profileOf(c),
|
|
),
|
|
);
|
|
}
|
|
});
|
|
|
|
test('max_round', () {
|
|
for (final c in cases(times, 'max_round')) {
|
|
expect(profileOf(c).maxRound, c['max_round']);
|
|
}
|
|
});
|
|
});
|
|
|
|
test('padding', () {
|
|
for (final c in cases(files['padding']!, 'padded')) {
|
|
final l = number(c, 'l');
|
|
for (final rule in PaddingRule.values) {
|
|
final want = c[rule.name] as int?;
|
|
if (want == null) {
|
|
expect(outcomeOf(() => paddedLength(l, rule)), (
|
|
'error',
|
|
c['text'],
|
|
), reason: '$c');
|
|
continue;
|
|
}
|
|
final p = paddedLength(l, rule);
|
|
expect(
|
|
[p, payloadAgeLength(p)],
|
|
[want, c['payload_age_${rule.name}']],
|
|
reason: '$c',
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
{
|
|
// The cases of a fixture whose content is empty need no file.
|
|
test('padding: the check of the plaintext against L and P', () {
|
|
for (final c in cases(files['padding']!, 'check')) {
|
|
final l = number(c, 'l');
|
|
if (l > 0 && testdata == null) continue;
|
|
final p = number(c, 'p');
|
|
final length = number(c, 'length');
|
|
final content = l == 0
|
|
? Uint8List(0)
|
|
: testdata!('fixtures/${str(c, 'fixture')}.plaintext');
|
|
final plain = Uint8List(length)
|
|
..setRange(0, length < l ? length : l, content);
|
|
if (c['at'] != null) plain[number(c, 'at')] = number(c, 'value');
|
|
// Format 3 has read BODY, L bytes, before the padding.
|
|
final start = number(c, 'format') == 3 ? l : 0;
|
|
final delivered = BytesBuilder();
|
|
expectOutcome(c, () {
|
|
final check = PaddingCheck(l, p, start: start);
|
|
// Pieces of every size, as age delivers its chunks.
|
|
var at = start;
|
|
for (var size = 1; at < length; size = size * 3 + 1) {
|
|
final end = at + size < length ? at + size : length;
|
|
delivered.add(check.add(Uint8List.sublistView(plain, at, end)));
|
|
at = end;
|
|
}
|
|
expect(check.received, length);
|
|
check.close();
|
|
});
|
|
// In format 2 the content is delivered, never the padding: all of
|
|
// it when the plaintext is valid, and a part of it otherwise.
|
|
final got = delivered.takeBytes();
|
|
final offered = Uint8List.sublistView(
|
|
plain,
|
|
0,
|
|
start > 0 ? 0 : (length < l ? length : l),
|
|
);
|
|
if (c['result'] == 'ok') {
|
|
expect(got, offered, reason: '$c');
|
|
} else {
|
|
expect(got.length <= offered.length, isTrue, reason: '$c');
|
|
expect(got, offered.sublist(0, got.length), reason: '$c');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
final encode = files['encode']!;
|
|
group('encode', () {
|
|
// The encoding of an encoder that succeeded, as the vectors write it.
|
|
void written(Json c, Uint8List b) {
|
|
if (c.containsKey('hex')) {
|
|
expect(toHex(b), c['hex']);
|
|
} else {
|
|
expect([b.length, toHex(sha256(b))], [c['length'], c['sha256']]);
|
|
}
|
|
}
|
|
|
|
List<Extension> withBig(Json c, List<Extension> exts) {
|
|
final n = c['big'] as int?;
|
|
return [
|
|
...exts,
|
|
if (n != null) Extension('a', 1, Uint8List(n)..fillRange(0, n, 1)),
|
|
];
|
|
}
|
|
|
|
test('header', () {
|
|
for (final c in cases(encode, 'header')) {
|
|
final h = Header(
|
|
capsuleId: Uint8List(16)..fillRange(0, 16, 7),
|
|
dateKey: DateKey(str(c, 'profile_id'), number(c, 'round')),
|
|
policy: AccessPolicy.fromCode(number(c, 'policy'))!,
|
|
critical: extensionsOf(c['critical']),
|
|
noncritical: withBig(c, extensionsOf(c['noncritical'])),
|
|
);
|
|
expectOutcome(c, () => written(c, encodeHeader(h)));
|
|
}
|
|
});
|
|
|
|
test('control', () {
|
|
for (final c in cases(encode, 'control')) {
|
|
final format = CapsuleFormat.fromVersion(number(c, 'format'))!;
|
|
final l = number(c, 'payload_length');
|
|
// Go's Control holds 0 for an L and a code that are not set.
|
|
final ctl = Control(
|
|
headerBinding: Uint8List(32)..fillRange(0, 32, 1),
|
|
payloadIdentity: Uint8List(32)..fillRange(0, 32, 5),
|
|
critical: extensionsOf(c['critical']),
|
|
noncritical: extensionsOf(c['noncritical']),
|
|
payloadLength: format.isPadded || l != 0 ? l : null,
|
|
padding: PaddingRule.fromCode(number(c, 'padding')),
|
|
);
|
|
expectOutcome(c, () => written(c, encodeControl(ctl, format)));
|
|
}
|
|
});
|
|
|
|
test('dkk', () {
|
|
for (final c in cases(encode, 'dkk')) {
|
|
final digest = c['capsule_digest'] as String?;
|
|
final k = AccessKey(
|
|
credentialId: Uint8List(16)..fillRange(0, 16, 1),
|
|
capsuleId: Uint8List(16)..fillRange(0, 16, 2),
|
|
type: str(c, 'type'),
|
|
material: fromHex(str(c, 'material')),
|
|
verification: digest == null ? null : Verification(fromHex(digest)),
|
|
critical: extensionsOf(c['critical']),
|
|
noncritical: withBig(c, extensionsOf(c['noncritical'])),
|
|
);
|
|
expectOutcome(c, () => written(c, encodeAccessKey(k)));
|
|
}
|
|
});
|
|
|
|
test('registry', () {
|
|
for (final c in cases(encode, 'registry')) {
|
|
final pins = [
|
|
for (final pin
|
|
in ((c['pins'] as List<Object?>?) ?? const []).cast<Json>())
|
|
Pin(
|
|
profileFrom(pin['profile']! as Json),
|
|
fromHex(str(pin, 'hash')),
|
|
),
|
|
];
|
|
expectOutcome(c, () {
|
|
final reg = newRegistry(pins);
|
|
for (final pin in pins) {
|
|
final p = reg.lookup(pin.profile.id)!;
|
|
expect(p, pin.profile);
|
|
// A copy: the registry keeps its own.
|
|
p.chainHash[0] ^= 1;
|
|
expect(reg.lookup(pin.profile.id), pin.profile);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
test('compact', () {
|
|
for (final c in cases(encode, 'compact')) {
|
|
final d = DateKey(str(c, 'profile_id'), number(c, 'round'));
|
|
expect(
|
|
[canonicalJson(d), compactDateKey(d), '$d'],
|
|
[c['json'], c['compact'], c['compact']],
|
|
);
|
|
}
|
|
});
|
|
|
|
if (testdata != null) {
|
|
test('limits of spec §57', () {
|
|
for (final c in cases(encode, 'limits')) {
|
|
switch (c['what']) {
|
|
case 'header':
|
|
expectOutcome(
|
|
c,
|
|
() => decodeHeader(Uint8List(number(c, 'zeros'))),
|
|
);
|
|
case 'body':
|
|
expectOutcome(
|
|
c,
|
|
() => decodeAccessKeyBody(Uint8List(number(c, 'zeros'))),
|
|
);
|
|
case 'dkk':
|
|
final n = number(c, 'zeros');
|
|
final dkk = Uint8List(12 + n)..setAll(0, dkkPreludeBytes(n));
|
|
expectOutcome(c, () => decodeAccessKey(dkk));
|
|
case 'extension_data':
|
|
final n = number(c, 'len');
|
|
final b = Uint8List(13 + n)
|
|
..setAll(0, fromHex('81a30061610101025a'))
|
|
..fillRange(13, 13 + n, 7);
|
|
ByteData.sublistView(b).setUint32(9, n);
|
|
expectOutcome(c, () {
|
|
late List<Extension> exts;
|
|
unmarshalCbor(
|
|
b,
|
|
(d) => exts = decodeExtensionArray(d),
|
|
(e) => encodeExtensionArray(e, exts),
|
|
);
|
|
expect(exts.single.data!.length, n);
|
|
});
|
|
case 'extension_canonical':
|
|
final n = number(c, 'len');
|
|
expectOutcome(
|
|
c,
|
|
() => canonicalExtensions([Extension('a', 1, Uint8List(n))]),
|
|
);
|
|
default:
|
|
fail('unknown limit ${c['what']}');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
final body = files['body']!;
|
|
group('body', () {
|
|
test('frame', () {
|
|
for (final c in cases(body, 'frame')) {
|
|
final l = number(c, 'l');
|
|
expectOutcome(c, () {
|
|
final f = parseBodyFrame(fromHex(str(c, 'hex')), l);
|
|
expect(
|
|
[f.areaLen, f.securityLen, f.headLen, contentLength(f, l)],
|
|
[
|
|
c['area_len'],
|
|
c['security_len'],
|
|
c['head_len'],
|
|
c['content_length'],
|
|
],
|
|
);
|
|
expect(toHex(bodyFrameBytes(f)), str(c, 'hex'));
|
|
});
|
|
}
|
|
});
|
|
|
|
test('area', () {
|
|
for (final c in cases(body, 'area')) {
|
|
expectOutcome(
|
|
c,
|
|
() => checkArea(fromHex(str(c, 'hex')), number(c, 'security_len')),
|
|
);
|
|
}
|
|
});
|
|
});
|
|
}
|