Stage 4b: the differential of the formats against Go
tool/formats_go_vectors.go runs in the module of datekeys-go at c531e93,
without changing anything there, and writes test/vectors/formats_*.json:
the result, the normative code and the text of Go on inputs of a fixed
seed, valid and broken in every layer of spec §69.1, and on the fixtures
of testdata/, edited:
- the PRELUDE (229 cases), the steps 1 to 3 of capsule.Inspect on cut and
edited fixtures (492) and whole .dkk files (268);
- PUBLIC_HEADER (660) and CONTROL_CBOR of the three formats (618);
- Provider Profiles decoded (163) and validated as values (60);
- extension arrays (260), Canonical (80), CheckDisjoint (50), the
registries with places (120) and CheckWrite with Standard (60);
- dk1_ strings (466);
- RFC 3339 parsed (434) and formatted (80), Resolve (320), RoundTime (64),
Validate (128) and MaxRound (8), on profiles of other genesis times and
periods;
- PaddedLength and PayloadAgeLength at the boundaries up to L_MAX (474),
and the check of the padding of capsule.Open at step 17 on fixtures whose
PAYLOAD_AGE is encrypted again with an edited plaintext (56);
- the encoders on values and the decoders at the limits of spec §57 (90);
- the frame of BODY (260) and the zeros of the area (60).
The output is the same on every run. formats_vectors.g.dart holds every
eighth case as Dart constants, so that the differential runs compiled to
JavaScript too, on Node.js; a test on the VM checks that they are those of
the files. Every file is under 310 KB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
|
|
|
// 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'));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
Stage 4b: every pair of faults across the layers of spec §69.1
A fault injected in the order of the layers of PUBLIC_HEADER, parsing the
DateKey before the rule across the extension arrays, passed the tests: no
random case had a DateKey of layer 4 and a fault of layer 3 together. The
generator now builds, for PUBLIC_HEADER, CONTROL_CBOR of the three
formats, the Provider Profile and the .dkk, each fault of a list alone and
each pair of them on a valid object, with Go's code and text: 153, 360,
153 and 181 cases, the .dkk sometimes with FLAGS 1 too. The cases use no
random value, so the other sections are the same as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
|
|
|
// 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'))));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
Stage 4b: the differential of the formats against Go
tool/formats_go_vectors.go runs in the module of datekeys-go at c531e93,
without changing anything there, and writes test/vectors/formats_*.json:
the result, the normative code and the text of Go on inputs of a fixed
seed, valid and broken in every layer of spec §69.1, and on the fixtures
of testdata/, edited:
- the PRELUDE (229 cases), the steps 1 to 3 of capsule.Inspect on cut and
edited fixtures (492) and whole .dkk files (268);
- PUBLIC_HEADER (660) and CONTROL_CBOR of the three formats (618);
- Provider Profiles decoded (163) and validated as values (60);
- extension arrays (260), Canonical (80), CheckDisjoint (50), the
registries with places (120) and CheckWrite with Standard (60);
- dk1_ strings (466);
- RFC 3339 parsed (434) and formatted (80), Resolve (320), RoundTime (64),
Validate (128) and MaxRound (8), on profiles of other genesis times and
periods;
- PaddedLength and PayloadAgeLength at the boundaries up to L_MAX (474),
and the check of the padding of capsule.Open at step 17 on fixtures whose
PAYLOAD_AGE is encrypted again with an edited plaintext (56);
- the encoders on values and the decoders at the limits of spec §57 (90);
- the frame of BODY (260) and the zeros of the area (60).
The output is the same on every run. formats_vectors.g.dart holds every
eighth case as Dart constants, so that the differential runs compiled to
JavaScript too, on Node.js; a test on the VM checks that they are those of
the files. Every file is under 310 KB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
|
|
|
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(
|
Stage 7a: the default registry checks datekeys.capsule; README, changelog
StandardExtensions checks by default the data of datekeys.capsule with
checkCapsuleData, as locator.Standard of Go: when a .dkk is opened with
it, an extension whose data does not read is unusable, with the text of
Go. With validateCapsule: null it checks only that there is data, as
Go's extension.Standard without ValidateCapsule: the writer of a .dkk
keeps that one, as Go's writer does, and so do the tests of the formats
that compare with extension.Standard. Inspection and opening give Go's
results on every fixture and vector, and the tests of the locator use the
default registry.
lib/datekeys.dart exports locator.dart. The README has the section of
part 7a, its vectors and how the generator makes them; the changelog has
its entry, with its tests and its injected faults.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
|
|
|
// Go's extension.Standard, without ValidateCapsule.
|
|
|
|
|
const StandardExtensions(validateCapsule: null),
|
Stage 4b: the differential of the formats against Go
tool/formats_go_vectors.go runs in the module of datekeys-go at c531e93,
without changing anything there, and writes test/vectors/formats_*.json:
the result, the normative code and the text of Go on inputs of a fixed
seed, valid and broken in every layer of spec §69.1, and on the fixtures
of testdata/, edited:
- the PRELUDE (229 cases), the steps 1 to 3 of capsule.Inspect on cut and
edited fixtures (492) and whole .dkk files (268);
- PUBLIC_HEADER (660) and CONTROL_CBOR of the three formats (618);
- Provider Profiles decoded (163) and validated as values (60);
- extension arrays (260), Canonical (80), CheckDisjoint (50), the
registries with places (120) and CheckWrite with Standard (60);
- dk1_ strings (466);
- RFC 3339 parsed (434) and formatted (80), Resolve (320), RoundTime (64),
Validate (128) and MaxRound (8), on profiles of other genesis times and
periods;
- PaddedLength and PayloadAgeLength at the boundaries up to L_MAX (474),
and the check of the padding of capsule.Open at step 17 on fixtures whose
PAYLOAD_AGE is encrypted again with an edited plaintext (56);
- the encoders on values and the decoders at the limits of spec §57 (90);
- the frame of BODY (260) and the zeros of the area (60).
The output is the same on every run. formats_vectors.g.dart holds every
eighth case as Dart constants, so that the differential runs compiled to
JavaScript too, on Node.js; a test on the VM checks that they are those of
the files. Every file is under 310 KB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
|
|
|
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')),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|