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.
270 lines
8.2 KiB
270 lines
8.2 KiB
// The shared vectors of testdata/ that the formats read, as vectors.test.ts
|
|
// of datekeys-ts: dk1.json, quicknet_rounds.json, profile_quicknet.json,
|
|
// padding.json and the schemas block of cbor.json, each object through the
|
|
// decoder of its schema and written back to the same bytes.
|
|
@TestOn('vm')
|
|
library;
|
|
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:datekeys/src/accesskey.dart';
|
|
import 'package:datekeys/src/bytes.dart';
|
|
import 'package:datekeys/src/control.dart';
|
|
import 'package:datekeys/src/datekey.dart';
|
|
import 'package:datekeys/src/errors.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:test/test.dart';
|
|
|
|
typedef Json = Map<String, Object?>;
|
|
|
|
Json readJson(String path) => jsonDecode(File(path).readAsStringSync()) as Json;
|
|
|
|
List<Json> section(Json file, String name) =>
|
|
(file[name]! as List).cast<Json>();
|
|
|
|
/// The code of the [DateKeysException] that [body] throws, or `ok`.
|
|
String resultOf(void Function() body) {
|
|
try {
|
|
body();
|
|
return 'ok';
|
|
} on DateKeysException catch (e) {
|
|
return e.code.code;
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
group('testdata/vectors/dk1.json', () {
|
|
final file = readJson('testdata/vectors/dk1.json');
|
|
final vectors = section(file, 'vectors');
|
|
|
|
test('is of this specification and holds valid and invalid strings', () {
|
|
expect(file['spec'], '0.11');
|
|
expect(vectors.where((v) => v.containsKey('dk1')), isNotEmpty);
|
|
expect(vectors.where((v) => v.containsKey('input')), isNotEmpty);
|
|
});
|
|
|
|
for (final v in vectors) {
|
|
test(v['name'], () {
|
|
if (v.containsKey('input')) {
|
|
expect(
|
|
resultOf(() => parseDateKey(v['input']! as String)),
|
|
v['error'],
|
|
);
|
|
return;
|
|
}
|
|
final d = parseDateKey(v['dk1']! as String);
|
|
expect([d.profileId, d.round], [v['network'], v['round']]);
|
|
expect(canonicalJson(d), v['canonical_json']);
|
|
expect(compactDateKey(d), v['dk1']);
|
|
expect(compactDateKey(d), 'dk1_${v['base64url']}');
|
|
});
|
|
}
|
|
});
|
|
|
|
group('testdata/vectors/quicknet_rounds.json', () {
|
|
final file = readJson('testdata/vectors/quicknet_rounds.json');
|
|
final p = quicknet();
|
|
|
|
test('is for the pinned Quicknet profile', () {
|
|
expect(file['profile'], quicknetId);
|
|
expect(section(file, 'vectors'), isNotEmpty);
|
|
});
|
|
|
|
for (final v in section(file, 'vectors')) {
|
|
test(v['name'], () {
|
|
final at = parseRfc3339(v['requested']! as String);
|
|
if (v.containsKey('error')) {
|
|
expect(resultOf(() => resolveDateKey(p, at)), v['error']);
|
|
return;
|
|
}
|
|
final d = resolveDateKey(p, at);
|
|
expect(d.round, v['round']);
|
|
expect(formatRfc3339Nano(unlockAt(d, p)!), v['effective']);
|
|
expect(resolveRfc3339(p, v['requested']! as String), d);
|
|
});
|
|
}
|
|
});
|
|
|
|
test('testdata/vectors/profile_quicknet.json', () {
|
|
final v = readJson('testdata/vectors/profile_quicknet.json');
|
|
final p = quicknet();
|
|
expect(
|
|
[
|
|
p.id,
|
|
p.provider,
|
|
p.network,
|
|
toHex(p.chainHash),
|
|
toHex(p.publicKey),
|
|
p.period,
|
|
p.genesisTime,
|
|
toHex(p.genesisSeed),
|
|
p.scheme,
|
|
],
|
|
[
|
|
v['profile_id'],
|
|
v['provider'],
|
|
v['network'],
|
|
v['chain_hash'],
|
|
v['public_key'],
|
|
v['period_seconds'],
|
|
v['genesis_time'],
|
|
v['genesis_seed'],
|
|
v['scheme'],
|
|
],
|
|
);
|
|
expect(toHex(canonicalCbor(p)), v['canonical_cbor']);
|
|
expect(toHex(profileHash(p)), v['profile_hash']);
|
|
expect(decodeProfile(fromHex(v['canonical_cbor']! as String)), p);
|
|
expect(
|
|
[quicknetCanonicalCbor, quicknetProfileHash],
|
|
[v['canonical_cbor'], v['profile_hash']],
|
|
);
|
|
});
|
|
|
|
group('testdata/vectors/padding.json', () {
|
|
final file = readJson('testdata/vectors/padding.json');
|
|
final vectors = section(file, 'vectors');
|
|
|
|
test('names L_MAX and covers the boundaries of spec §29.1', () {
|
|
expect(file['l_max'], maxPayloadLength);
|
|
final ls = vectors.map((v) => v['l']).toList();
|
|
for (final l in [
|
|
0,
|
|
256,
|
|
257,
|
|
2113929216,
|
|
2113929217,
|
|
4227858432,
|
|
4227858433,
|
|
4294967295,
|
|
4294967297,
|
|
562949953421311,
|
|
maxPayloadLength,
|
|
]) {
|
|
expect(ls, contains(l));
|
|
}
|
|
expect(file['rejected'], contains(maxPayloadLength + 1));
|
|
});
|
|
|
|
for (final v in vectors) {
|
|
test('L = ${v['l']}', () {
|
|
final l = v['l']! as int;
|
|
final b = paddedLength(l, PaddingRule.bloque256);
|
|
final r = paddedLength(l, PaddingRule.reforzado);
|
|
expect(
|
|
[b, r, payloadAgeLength(b), payloadAgeLength(r)],
|
|
[
|
|
v['bloque256'],
|
|
v['reforzado'],
|
|
v['payload_age_bloque256'],
|
|
v['payload_age_reforzado'],
|
|
],
|
|
);
|
|
if (l > 256) {
|
|
final (:e, :s, :lastBits) = padmeParameters(l);
|
|
expect([e, s, lastBits], [v['e'], v['s'], v['last_bits']]);
|
|
} else {
|
|
expect(v.containsKey('e'), isFalse);
|
|
}
|
|
});
|
|
}
|
|
|
|
test('rejects the lengths above L_MAX', () {
|
|
for (final l in (file['rejected']! as List).cast<int>()) {
|
|
for (final rule in PaddingRule.values) {
|
|
expect(
|
|
() => paddedLength(l, rule),
|
|
throwsA(
|
|
isA<ArgumentError>().having(
|
|
(e) => e.message,
|
|
'message',
|
|
'capsule: content of $l bytes exceeds L_MAX = '
|
|
'$maxPayloadLength',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
group('testdata/vectors/cbor.json: the schemas', () {
|
|
final file = readJson('testdata/vectors/cbor.json');
|
|
final schemas = section(file, 'schemas');
|
|
// The decoder of each schema, for the capsule format of the vector; it
|
|
// returns the writer of the object it decoded.
|
|
final decoders = <String, Object Function() Function(List<int>, int)>{
|
|
'provider_profile': (b, _) {
|
|
final p = decodeProfile(b);
|
|
return () => canonicalCbor(p);
|
|
},
|
|
'public_header': (b, _) {
|
|
final h = decodeHeader(b);
|
|
return () => encodeHeader(h);
|
|
},
|
|
'control_cbor': (b, format) {
|
|
final f = CapsuleFormat.fromVersion(format)!;
|
|
final c = decodeControl(b, f);
|
|
return () => encodeControl(c, f);
|
|
},
|
|
'dkk_body': (b, _) {
|
|
final k = decodeAccessKeyBody(b);
|
|
return () => marshalAccessKeyBody(k);
|
|
},
|
|
};
|
|
const blocks = {
|
|
'provider_profile': 'provider_profile',
|
|
'public_header': 'public_header',
|
|
'control_cbor': 'control_cbor',
|
|
'dkk_body': 'dkk_body',
|
|
'verification_metadata': 'dkk_body',
|
|
'extension': 'public_header',
|
|
};
|
|
|
|
test('has every block, with CONTROL_CBOR in the three formats', () {
|
|
for (final block in blocks.keys) {
|
|
expect(
|
|
schemas.where((v) => v['block'] == block).length,
|
|
greaterThanOrEqualTo(5),
|
|
reason: block,
|
|
);
|
|
}
|
|
for (final v in schemas) {
|
|
expect(blocks[v['block']], v['schema'], reason: '${v['name']}');
|
|
expect(
|
|
v.containsKey('format'),
|
|
v['schema'] == 'control_cbor' &&
|
|
(v['name']! as String).startsWith('format '),
|
|
reason: '${v['name']}',
|
|
);
|
|
}
|
|
final controls = schemas.where((v) => v['schema'] == 'control_cbor');
|
|
for (final f in [2, 3]) {
|
|
expect(
|
|
controls.where((v) => v['format'] == f).length,
|
|
greaterThanOrEqualTo(5),
|
|
);
|
|
}
|
|
});
|
|
|
|
for (final v in schemas) {
|
|
test('${v['block']} (${v['schema']}): ${v['name']}', () {
|
|
final input = fromHex(v['hex']! as String);
|
|
Object Function()? write;
|
|
final result = resultOf(() {
|
|
write = decoders[v['schema']]!(input, (v['format'] as int?) ?? 1);
|
|
});
|
|
expect(result, v['result']);
|
|
// An accepted object is written back to the same bytes.
|
|
if (write != null) {
|
|
expect(toHex(write!() as List<int>), v['hex']);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|