Stage 4b: PUBLIC_HEADER, CONTROL_CBOR of the three formats and the .dkk
lib/src/header.dart, control.dart and accesskey.dart port DecodeHeader,
EncodeHeader, DecodeControl and EncodeControl of package capsule and
package accesskey of datekeys-go at c531e93, in the layers of spec §69.1
and with the texts of Go: the limit of the frame, the type tag and the
schema version, the CBOR profile with the re-encoding and the CDDL, keys 6
and 7 of CONTROL_CBOR versions 2 and 3, and only then the DateKey of the
header and access_type and access_material of the .dkk. The .dkk is
written with the rule of spec §72 for the extensions of the specification
and read back before it is returned, as MarshalBody. I_PAYLOAD and
access_material are copied once and wiped on every path; their objects
print without them. The access policy is the enum AccessPolicy.
The tests read every fixture of testdata/: the PRELUDE, the sections and
header_binding of the 24 capsules, their header and control decoded and
written back, the round time of their DateKey, P, and in format 3 the frame
of BODY, the area, the head and the files; the six .dkk with their
capsule_digest. And the shared vectors dk1.json, quicknet_rounds.json,
profile_quicknet.json, padding.json and the 172 schemas of cbor.json,
which stage 1 left aside, each decoded by its schema and written back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
|
|
|
// 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 data of the branch v0.12 of datekeys-go at c531e93
testdata/ is synced with datekeys-go at c531e93, the head of the branch
v0.12, whose testdata is that of 601e6d2, the copy of datekeys-ts. Every
file keeps "spec": "0.11": the draft v0.12 is not approved yet. From the
tag spec-v0.11 it brings the fixtures format3_unsigned and format3_note,
format3_seal_unsupported with seal_type 4294967295, the records of
format3_sealed and format3_signed_cms, the mutation corpus of 218 cases,
note.json, security.json with a context and lines, security_cms.json of
135 cases and locator.json.
The vectors that the generators of tool/ make from the testdata are
written again by Go at c531e93: mutation_texts.json, open_cases.json,
formats_*.json with formats_vectors.g.dart, ibe_vectors.json and
age_fixtures.json; release_vectors.json, primitives.json and the views
of open_vectors.g.dart come out the same. age.json does not read the
testdata, and stays frozen: age draws its keys from crypto/rand. The
tests count 26 fixtures and 218 cases, and the inspection of
format3_note gives the note of its record. No difference with Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
|
|
|
test('there are the 26 capsules and the 6 access keys of spec §67, §68', () {
|
|
|
|
|
expect(names, hasLength(26));
|
Stage 4b: PUBLIC_HEADER, CONTROL_CBOR of the three formats and the .dkk
lib/src/header.dart, control.dart and accesskey.dart port DecodeHeader,
EncodeHeader, DecodeControl and EncodeControl of package capsule and
package accesskey of datekeys-go at c531e93, in the layers of spec §69.1
and with the texts of Go: the limit of the frame, the type tag and the
schema version, the CBOR profile with the re-encoding and the CDDL, keys 6
and 7 of CONTROL_CBOR versions 2 and 3, and only then the DateKey of the
header and access_type and access_material of the .dkk. The .dkk is
written with the rule of spec §72 for the extensions of the specification
and read back before it is returned, as MarshalBody. I_PAYLOAD and
access_material are copied once and wiped on every path; their objects
print without them. The access policy is the enum AccessPolicy.
The tests read every fixture of testdata/: the PRELUDE, the sections and
header_binding of the 24 capsules, their header and control decoded and
written back, the round time of their DateKey, P, and in format 3 the frame
of BODY, the area, the head and the files; the six .dkk with their
capsule_digest. And the shared vectors dk1.json, quicknet_rounds.json,
profile_quicknet.json, padding.json and the 172 schemas of cbor.json,
which stage 1 left aside, each decoded by its schema and written back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|