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.
236 lines
8.1 KiB
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 24 capsules and the 6 access keys of spec §67, §68', () {
|
|
expect(names, hasLength(24));
|
|
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);
|
|
});
|
|
}
|
|
}
|