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-dart/test/formats_fixtures_test.dart

236 lines
8.1 KiB

// Every official fixture of testdata/ through the formats: the PRELUDE, the
// sections and header_binding of each .dkc, its PUBLIC_HEADER and its
// CONTROL_CBOR, decoded and written back to the same bytes, the profile and
// the round time of its DateKey, P = rule(L), and in format 3 the frame of
// BODY, the security area and the place of the head and of the files; and
// each .dkk, whose capsule_digest is the SHA-256 of its .dkc. The values are
// those of the records <name>.json and <name>.dkk.json, which Go wrote.
@TestOn('vm')
library;
import 'dart:convert';
import 'dart:io';
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/control.dart';
import 'package:datekeys/src/datekey.dart';
import 'package:datekeys/src/digest.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:test/test.dart';
typedef Json = Map<String, Object?>;
Json readJson(String path) => jsonDecode(File(path).readAsStringSync()) as Json;
Uint8List readBytes(String path) => File(path).readAsBytesSync();
/// The extensions of a record, as `{critical, id, version, data}`.
List<Object?> recordExtensions(Object? v) => [
for (final e in ((v as List<Object?>?) ?? const []).cast<Json>())
[e['critical'], e['id'], e['version'], e['data']],
];
List<Object?> extensionsOf(List<Extension> critical, List<Extension> non) => [
for (final (c, list) in [(true, critical), (false, non)])
for (final e in list)
[c, e.id, e.version, e.data == null ? null : toHex(e.data!)],
];
String codeOf(void Function() body) {
try {
body();
return 'ok';
} on DateKeysException catch (e) {
return e.code.code;
}
}
void main() {
final names =
Directory('testdata/fixtures')
.listSync()
.map((f) => f.uri.pathSegments.last)
.where((n) => n.endsWith('.dkc'))
.map((n) => n.substring(0, n.length - 4))
.toList()
..sort();
final keys =
Directory('testdata/fixtures')
.listSync()
.map((f) => f.uri.pathSegments.last)
.where((n) => n.endsWith('.dkk'))
.toList()
..sort();
test('there are the 26 capsules and the 6 access keys of spec §67, §68', () {
expect(names, hasLength(26));
expect(keys, hasLength(6));
});
for (final name in names) {
test(name, () {
final r = readJson('testdata/fixtures/$name.json');
final dkc = readBytes('testdata/fixtures/$name.dkc');
expect(toHex(capsuleDigest(dkc)), r['sha256']);
// The frame.
final format = CapsuleFormat.fromVersion(r['format']! as int)!;
final prelude = parsePrelude(dkc.sublist(0, dkcPreludeSize));
expect(prelude.format, format);
expect(toHex(preludeBytes(prelude)), r['prelude']);
final s = splitCapsule(dkc);
expect(s.prelude, prelude);
expect(toHex(s.preludeBytes), r['prelude']);
expect(toHex(s.publicHeader), r['public_header']);
expect(s.sealedControl, hasLength(prelude.sealedControlLen));
expect(s.payload.length, dkc.length - payloadOffset(prelude));
expect(
toHex(headerBinding(s.preludeBytes, s.publicHeader)),
r['header_binding'],
);
// PUBLIC_HEADER, its profile and its round.
final h = decodeHeader(s.publicHeader);
expect(
[h.capsuleIdHex, compactDateKey(h.dateKey), h.policy.label],
[r['capsule_id'], r['datekey'], r['access_policy']],
);
expect(
extensionsOf(h.critical, h.noncritical),
recordExtensions(r['header_extensions']),
);
expect(toHex(encodeHeader(h)), r['public_header']);
final p = defaultRegistry().lookup(h.dateKey.profileId)!;
validateDateKey(h.dateKey, p);
expect(formatRfc3339(unlockAt(h.dateKey, p)!), r['unlock_at']);
// CONTROL_CBOR, in its format and in no other.
final control = decodeControl(
fromHex(r['control_cbor']! as String),
format,
);
expect(
[toHex(control.headerBinding), toHex(control.payloadIdentity)],
[r['header_binding'], r['payload_identity']],
);
expect(
extensionsOf(control.critical, control.noncritical),
recordExtensions(r['control_extensions']),
);
expect(toHex(encodeControl(control, format)), r['control_cbor']);
for (final other in CapsuleFormat.values.where((f) => f != format)) {
expect(
codeOf(
() => decodeControl(fromHex(r['control_cbor']! as String), other),
),
'ERR_UNSUPPORTED_VERSION',
);
}
final plaintext = readBytes('testdata/fixtures/${r['plaintext_file']}');
if (!format.isPadded) {
expect([control.payloadLength, control.padding], [null, null]);
expect(r.containsKey('padding'), isFalse);
expect(plaintext.length, r['payload_length']);
return;
}
final l = control.payloadLength!;
final rule = control.padding!;
expect([l, rule.code], [r['payload_length'], r['padding']]);
final padded = paddedLength(l, rule);
expect(padded, r['padded_length']);
expect(plaintext.length, l);
// The plaintext of PAYLOAD_AGE is the content and zeros up to P.
final check = PaddingCheck(l, padded);
final content = BytesBuilder()
..add(check.add(plaintext))
..add(check.add(Uint8List(padded - l)));
check.close();
expect(content.takeBytes(), plaintext);
if (format != CapsuleFormat.format3) return;
// BODY: its frame, the security area, the head and the files.
final frame = parseBodyFrame(plaintext.sublist(0, bodyFrameSize), l);
expect(frame.areaLen, r['area_len']);
final area = plaintext.sublist(
bodyFrameSize,
bodyFrameSize + frame.areaLen,
);
checkArea(area, frame.securityLen);
expect(toHex(area.sublist(0, frame.securityLen)), r['security_cbor']);
final headStart = bodyFrameSize + frame.areaLen;
expect(
toHex(plaintext.sublist(headStart, headStart + frame.headLen)),
r['head_cbor'],
);
expect(headStart + frame.headLen, r['content_offset']);
final files = (r['files'] as List<Object?>?) ?? const [];
expect(
contentLength(frame, l),
[
0,
for (final f in files) (f! as Json)['size']! as int,
].reduce((a, b) => a + b),
);
});
}
for (final key in keys) {
test(key, () {
final r = readJson('testdata/fixtures/$key.json');
final raw = readBytes('testdata/fixtures/$key');
expect(toHex(capsuleDigest(raw)), r['sha256']);
final k = decodeAccessKey(raw);
expect(
[
toHex(k.credentialId),
toHex(k.capsuleId),
k.type,
toHex(k.material),
k.verification == null ? null : toHex(k.verification!.capsuleDigest),
],
[
r['credential_id'],
r['capsule_id'],
r['access_type'],
r['access_material'],
r['capsule_digest'],
],
);
expect(
extensionsOf(k.critical, k.noncritical),
recordExtensions(r['extensions']),
);
checkAccessKeyMaterial(k);
// capsule_digest is the SHA-256 of the exact bytes of its .dkc, and its
// capsule_id that of the header of that capsule.
final dkc = readBytes('testdata/fixtures/${r['capsule']}');
checkCapsuleDigest(capsuleDigest(dkc), k.verification!.capsuleDigest);
expect(
decodeHeader(splitCapsule(dkc).publicHeader).capsuleId,
k.capsuleId,
);
expect(
codeOf(
() => checkCapsuleDigest(
capsuleDigest(raw),
k.verification!.capsuleDigest,
),
),
'ERR_ACCESS_INVALID',
);
expect(toHex(encodeAccessKey(k)), toHex(raw));
k.wipe();
expect(k.material.every((b) => b == 0), isTrue);
});
}
}

Powered by TurnKey Linux.