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/open_test.dart

741 lines
23 KiB

Stage 4c: steps 9 to 18, the opening of the three formats lib/src/open.dart and open3.dart port Open and openBody of package capsule of datekeys-go at c531e93, with the checks, codes, steps and texts of the reference and the detail of each check: the .dkk of step 9.a, decoded or still encoded, with its material, its critical extensions, its capsule_id and its capsule_digest; at least one credential; the clock and the release, with the rule of step 9 for the failures of its source (sourceFailure); its verification at step 10; OUTER_TIME_AGE with the tlock stanza; INNER_ACCESS_AGE with the X25519 identities, those of the .dkk and the key of words, and the rules of the slots; CONTROL_CBOR, header_binding, I_PAYLOAD and P; PAYLOAD_AGE streamed, with the padding of format 2 checked and never delivered, and in format 3 the frame of BODY, the area, the head, each file to the sink with its SHA-256 and the padding, with the precedence of spec §63; and the commit. A failure of age keeps the code of the identity that reports it, or is ERR_INTEGRITY with the reason of its phase, as classify of Go. openCapsule opens a capsule in memory and openCapsuleSource one that a ByteSource reads: the prefix of the inspection and the nonce of PAYLOAD_AGE before the release, then pieces of 1 MiB. lib/src/sink.dart has ByteSink, for the content of formats 1 and 2 and for each file, and FileSink, for the files of format 3, as the dst and the Sink of Go, with MemoryByteSink and MemoryFileSink. Nothing is presented as valid before step 17 ends: the output is closed only then and aborted after any failure, and the sink aborted after any failure that follows its begin (spec §56). The signature and the seal are stage 5: lib/src/verdicts.dart has the verdicts and the SecurityEvaluator, given what newSecurityContext and EvaluateSecurityIn of Go take, which never fails the opening; the default evaluates nothing. OpenOptions.accept is Accept of Go. And AgePayloadDecryptor.wipe clears the key of a STREAM left unread. The tests run open_cases.json and the mutation corpus with the texts and the checks of Go, in memory and from a source read in pieces; capsules of several MiB made from the fixtures, for the streaming; a capsule with a stanza for a key of words; and the caller, the sinks and the evaluator. open_test.dart runs on Node.js too, with open_vectors.g.dart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
// The head, the note, the inspection and the opening, on the VM and
// compiled to JavaScript: the parts of the vectors of Go that
// test/vectors/open_vectors.g.dart holds, with the small fixtures they edit,
// and what the API does with the caller: its errors, the output and the
// sinks, the evaluator of the security area and accept.
library;
import 'dart:convert';
import 'dart:typed_data';
import 'package:datekeys/datekeys.dart';
import 'package:test/test.dart';
import 'open_support.dart';
import 'tlock_support.dart' show applyEdits;
import 'vectors/open_vectors.g.dart';
final Map<String, Uint8List> _fixtures = {
for (final e in (jsonDecode(openFixturesJson) as Json).entries)
e.key: fromHex(e.value! as String),
};
Uint8List fixture(String file) => _fixtures[file]!;
final Json _cases = jsonDecode(openCasesJson) as Json;
/// The case of the part of open_cases.json named [name].
Json caseNamed(String name) => [
...(_cases['fixtures']! as List).cast<Json>(),
...(_cases['steps']! as List).cast<Json>(),
].firstWhere((c) => c['name'] == name);
/// The options of the opening of the case [name], with [output] and [sink].
OpenOptions optionsOf(
Json c, {
ByteSink? output,
FileSink? sink,
SecurityEvaluator? evaluator,
Stage 4c: steps 9 to 18, the opening of the three formats lib/src/open.dart and open3.dart port Open and openBody of package capsule of datekeys-go at c531e93, with the checks, codes, steps and texts of the reference and the detail of each check: the .dkk of step 9.a, decoded or still encoded, with its material, its critical extensions, its capsule_id and its capsule_digest; at least one credential; the clock and the release, with the rule of step 9 for the failures of its source (sourceFailure); its verification at step 10; OUTER_TIME_AGE with the tlock stanza; INNER_ACCESS_AGE with the X25519 identities, those of the .dkk and the key of words, and the rules of the slots; CONTROL_CBOR, header_binding, I_PAYLOAD and P; PAYLOAD_AGE streamed, with the padding of format 2 checked and never delivered, and in format 3 the frame of BODY, the area, the head, each file to the sink with its SHA-256 and the padding, with the precedence of spec §63; and the commit. A failure of age keeps the code of the identity that reports it, or is ERR_INTEGRITY with the reason of its phase, as classify of Go. openCapsule opens a capsule in memory and openCapsuleSource one that a ByteSource reads: the prefix of the inspection and the nonce of PAYLOAD_AGE before the release, then pieces of 1 MiB. lib/src/sink.dart has ByteSink, for the content of formats 1 and 2 and for each file, and FileSink, for the files of format 3, as the dst and the Sink of Go, with MemoryByteSink and MemoryFileSink. Nothing is presented as valid before step 17 ends: the output is closed only then and aborted after any failure, and the sink aborted after any failure that follows its begin (spec §56). The signature and the seal are stage 5: lib/src/verdicts.dart has the verdicts and the SecurityEvaluator, given what newSecurityContext and EvaluateSecurityIn of Go take, which never fails the opening; the default evaluates nothing. OpenOptions.accept is Accept of Go. And AgePayloadDecryptor.wipe clears the key of a STREAM left unread. The tests run open_cases.json and the mutation corpus with the texts and the checks of Go, in memory and from a source read in pieces; capsules of several MiB made from the fixtures, for the streaming; a capsule with a stanza for a key of words; and the caller, the sinks and the evaluator. open_test.dart runs on Node.js too, with open_vectors.g.dart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
Map<String, String> authorKeys = const {},
void Function(Verdicts v)? accept,
ReleaseSource? source,
}) {
final rel = c['release']! as Json;
final file = c['dkk_file'] as String?;
return OpenOptions(
source:
source ??
suppliedRelease(
Release(rel['round']! as int, fromHex(str(rel, 'signature'))),
),
now: () => parseRfc3339(str(c, 'now')),
accessKeyFile: file == null ? null : fromHex(file),
output: output,
sink: sink,
evaluator: evaluator ?? evaluateSecurityInput,
Stage 4c: steps 9 to 18, the opening of the three formats lib/src/open.dart and open3.dart port Open and openBody of package capsule of datekeys-go at c531e93, with the checks, codes, steps and texts of the reference and the detail of each check: the .dkk of step 9.a, decoded or still encoded, with its material, its critical extensions, its capsule_id and its capsule_digest; at least one credential; the clock and the release, with the rule of step 9 for the failures of its source (sourceFailure); its verification at step 10; OUTER_TIME_AGE with the tlock stanza; INNER_ACCESS_AGE with the X25519 identities, those of the .dkk and the key of words, and the rules of the slots; CONTROL_CBOR, header_binding, I_PAYLOAD and P; PAYLOAD_AGE streamed, with the padding of format 2 checked and never delivered, and in format 3 the frame of BODY, the area, the head, each file to the sink with its SHA-256 and the padding, with the precedence of spec §63; and the commit. A failure of age keeps the code of the identity that reports it, or is ERR_INTEGRITY with the reason of its phase, as classify of Go. openCapsule opens a capsule in memory and openCapsuleSource one that a ByteSource reads: the prefix of the inspection and the nonce of PAYLOAD_AGE before the release, then pieces of 1 MiB. lib/src/sink.dart has ByteSink, for the content of formats 1 and 2 and for each file, and FileSink, for the files of format 3, as the dst and the Sink of Go, with MemoryByteSink and MemoryFileSink. Nothing is presented as valid before step 17 ends: the output is closed only then and aborted after any failure, and the sink aborted after any failure that follows its begin (spec §56). The signature and the seal are stage 5: lib/src/verdicts.dart has the verdicts and the SecurityEvaluator, given what newSecurityContext and EvaluateSecurityIn of Go take, which never fails the opening; the default evaluates nothing. OpenOptions.accept is Accept of Go. And AgePayloadDecryptor.wipe clears the key of a STREAM left unread. The tests run open_cases.json and the mutation corpus with the texts and the checks of Go, in memory and from a source read in pieces; capsules of several MiB made from the fixtures, for the streaming; a capsule with a stanza for a key of words; and the caller, the sinks and the evaluator. open_test.dart runs on Node.js too, with open_vectors.g.dart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
authorKeys: authorKeys,
accept: accept,
);
}
void main() {
group('the vectors of Go', () {
for (final section in ['fixtures', 'steps']) {
final cases = (_cases[section]! as List).cast<Json>();
for (var i = 0; i < cases.length; i++) {
final c = cases[i];
test('$section: ${c['name']}', () async {
final dkc = capsuleOf(c, fixture);
expect(differences(c, await openCase(c, dkc)), isEmpty);
// From a source, every fourth: the others run on the VM.
if (i % 4 == 0) {
expect(
differences(c, await openCase(c, dkc, fromSource: true)),
isEmpty,
);
}
});
}
}
test('the heads', () {
final h = jsonDecode(openHeadsJson) as Json;
checkDecodeCases((h['decode']! as List).cast<Json>());
checkEncodeCases((h['encode']! as List).cast<Json>());
});
test('the notes', () {
final n = jsonDecode(openNotesJson) as Json;
checkNoteCases(
(n['check']! as List).cast<Json>(),
(n['note']! as List).cast<Json>(),
(n['standard']! as List).cast<Json>(),
);
});
test('the inspection: the views and the mutations', () {
final v = jsonDecode(openInspectJson) as Json;
for (final c in (v['views']! as List).cast<Json>()) {
final r = inspectCapsule(
capsuleOf(c, fixture),
extensions: switch (c['registry']) {
'standard' => const StandardExtensions(),
'reject_all' => const RejectAll('not today'),
_ => null,
},
);
expect(
inspectJson(inspectView(r, file: 'capsule.dkc')),
c['view'],
reason: str(c, 'name'),
);
}
final mutations = (v['mutations']! as List).cast<Json>();
expect(mutations, hasLength(greaterThan(300)));
for (final m in mutations) {
final dkc = applyEdits(fixture(str(m, 'base')), m['edits']! as List);
final r = inspectCapsule(dkc);
final c = r.checks.last;
expect(
[c.step, c.ok, c.error ?? 'ok', if (!c.ok) c.detail],
[
m['step'] ?? 8,
m['result'] == 'ok',
m['result'],
if (m['result'] != 'ok') m['text'],
],
reason: 'mutation ${m['index']}',
);
}
});
});
group('the caller', () {
final single = caseNamed('format3_single');
final keyed = caseNamed('time_and_key_recipients: the .dkk, decoded');
final plain = caseNamed('time_only_extensions');
test('gets an ArgumentError for options that do not go together, with '
'the output aborted', () async {
final dkc = capsuleOf(keyed, fixture);
final key = decodeAccessKey(fromHex(str(keyed, 'dkk')));
final out = RecordingOutput();
await expectLater(
openCapsule(
dkc,
OpenOptions(
source: suppliedRelease(),
now: () => Instant(0),
accessKey: key,
accessKeyFile: fromHex(str(keyed, 'dkk')),
output: out,
),
),
throwsArgumentError,
);
expect([out.aborted, out.closed], [isArgumentError, false]);
await expectLater(
openCapsule(
dkc,
OpenOptions(
source: suppliedRelease(),
now: () => Instant(0),
identities: [Uint8List(31)],
output: RecordingOutput(),
),
),
throwsArgumentError,
);
});
test('needs the sink for format 3 and the output for formats 1 and 2, '
'right after step 2, before any request', () async {
final calls = CaseSource(null, null);
final dkc = capsuleOf(single, fixture);
final out = RecordingOutput();
await expectLater(
openCapsule(dkc, optionsOf(single, source: calls, output: out)),
throwsA(
isArgumentError.having(
(e) => '${e.message}',
'message',
'open: a format 3 capsule holds files: OpenOptions.sink is '
'required',
),
),
);
expect(
[calls.calls, out.aborted, out.log],
[
0,
isArgumentError,
['abort'],
],
);
// Steps 3 to 8 do not matter: a truncated PUBLIC_HEADER after a valid
// prelude.
await expectLater(
openCapsule(dkc.sublist(0, 20), optionsOf(single, source: calls)),
throwsArgumentError,
);
await expectLater(
openCapsule(
capsuleOf(plain, fixture),
optionsOf(plain, source: calls, sink: MemoryFileSink()),
),
throwsA(
isArgumentError.having(
(e) => '${e.message}',
'message',
'open: a capsule of format 1 or 2 holds one content: '
'OpenOptions.output is required',
),
),
);
expect(calls.calls, 0);
// Steps 1 and 2 fail first: no format, nothing needed.
final r = await openCapsule(dkc.sublist(0, 10), optionsOf(single));
expect([r.error?.code, r.checks.last.step], [ErrorCode.integrity, 1]);
});
test('leaves the sink untouched in formats 1 and 2, and the output in '
'format 3', () async {
final sink = RecordingSink();
final out = RecordingOutput();
final r = await openCapsule(
capsuleOf(plain, fixture),
optionsOf(plain, output: out, sink: sink),
);
expect([r.ok, sink.log, out.closed], [true, isEmpty, true]);
expect(out.log.last, 'close');
final out3 = RecordingOutput();
final s = await openCapsule(
capsuleOf(single, fixture),
optionsOf(single, output: out3, sink: MemoryFileSink()),
);
expect([s.ok, out3.log], [true, isEmpty]);
});
test('hands the sink the files in the order of the head, each closed '
'before the next, and commits last', () async {
final sink = RecordingSink();
final r = await openCapsule(
capsuleOf(single, fixture),
optionsOf(single, sink: sink),
);
expect(r.ok, isTrue);
expect(sink.log, [
'begin',
'create 0',
for (final l in sink.log.where((l) => l.startsWith('write 0 '))) l,
'close 0',
'commit',
]);
final written = sink.log
.where((l) => l.startsWith('write 0 '))
.map((l) => int.parse(l.split(' ')[2]))
.fold(0, (a, b) => a + b);
expect(written, r.head!.files.single.size);
});
test('keeps the files in a MemoryFileSink, given once committed', () async {
final sink = MemoryFileSink();
final r = await openCapsule(
capsuleOf(single, fixture),
optionsOf(single, sink: sink),
);
expect(sink.head, same(r.head));
expect(sink.files!.single, hasLength(r.head!.files.single.size));
expect(
canonical(
filesOf(Outcome(r, RecordingOutput(), _asRecording(sink), 0, [])),
Stage 4c: steps 9 to 18, the opening of the three formats lib/src/open.dart and open3.dart port Open and openBody of package capsule of datekeys-go at c531e93, with the checks, codes, steps and texts of the reference and the detail of each check: the .dkk of step 9.a, decoded or still encoded, with its material, its critical extensions, its capsule_id and its capsule_digest; at least one credential; the clock and the release, with the rule of step 9 for the failures of its source (sourceFailure); its verification at step 10; OUTER_TIME_AGE with the tlock stanza; INNER_ACCESS_AGE with the X25519 identities, those of the .dkk and the key of words, and the rules of the slots; CONTROL_CBOR, header_binding, I_PAYLOAD and P; PAYLOAD_AGE streamed, with the padding of format 2 checked and never delivered, and in format 3 the frame of BODY, the area, the head, each file to the sink with its SHA-256 and the padding, with the precedence of spec §63; and the commit. A failure of age keeps the code of the identity that reports it, or is ERR_INTEGRITY with the reason of its phase, as classify of Go. openCapsule opens a capsule in memory and openCapsuleSource one that a ByteSource reads: the prefix of the inspection and the nonce of PAYLOAD_AGE before the release, then pieces of 1 MiB. lib/src/sink.dart has ByteSink, for the content of formats 1 and 2 and for each file, and FileSink, for the files of format 3, as the dst and the Sink of Go, with MemoryByteSink and MemoryFileSink. Nothing is presented as valid before step 17 ends: the output is closed only then and aborted after any failure, and the sink aborted after any failure that follows its begin (spec §56). The signature and the seal are stage 5: lib/src/verdicts.dart has the verdicts and the SecurityEvaluator, given what newSecurityContext and EvaluateSecurityIn of Go take, which never fails the opening; the default evaluates nothing. OpenOptions.accept is Accept of Go. And AgePayloadDecryptor.wipe clears the key of a STREAM left unread. The tests run open_cases.json and the mutation corpus with the texts and the checks of Go, in memory and from a source read in pieces; capsules of several MiB made from the fixtures, for the streaming; a capsule with a stanza for a key of words; and the caller, the sinks and the evaluator. open_test.dart runs on Node.js too, with open_vectors.g.dart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
),
canonical(single['files']),
);
expect(sink.aborted, isFalse);
});
test('gives the evaluator what Go\'s newSecurityContext takes, once the '
'head is read, and never fails for it', () async {
final seen = <SecurityInput>[];
final dkc = capsuleOf(single, fixture);
final keys = {'dkauthor1x': 'Ana'};
final r = await openCapsule(
dkc,
optionsOf(
single,
sink: MemoryFileSink(),
authorKeys: keys,
evaluator: (input) {
seen.add(input);
// The control is whole while the evaluator runs.
expect(input.control.payloadIdentity.any((b) => b != 0), isTrue);
return Verdicts(
signature: Verdict.noSignature,
seal: Verdict.noSeal,
);
},
),
);
expect(r.ok, isTrue);
final input = seen.single;
final s = splitCapsule(dkc);
expect(
[
input.format,
input.roundTime,
input.authorKeys,
toHex(input.control.headerBinding),
decodeHead(input.head).files.single.path,
input.security.isNotEmpty,
input.security.length <= r.areaLen!,
],
[
CapsuleFormat.format3,
r.inspection.unlockAt,
keys,
toHex(headerBinding(s.preludeBytes, s.publicHeader)),
r.head!.files.single.path,
true,
true,
],
);
expect(
[r.verdicts!.signature, r.verdicts!.seal],
[Verdict.noSignature, Verdict.noSeal],
);
// I_PAYLOAD is wiped once the opening ends.
expect(input.control.payloadIdentity.every((b) => b == 0), isTrue);
// An evaluator that throws: the capsule opens, not evaluated.
final t = await openCapsule(
dkc,
optionsOf(
single,
sink: MemoryFileSink(),
evaluator: (_) => throw StateError('broken'),
),
);
expect(
[t.ok, t.verdicts!.evaluated, t.verdicts!.error],
[true, false, isStateError],
);
// The default evaluates as Go: an area without a signature or a seal
// is F0 and S0.
expect(
OpenOptions(
source: suppliedRelease(r.release!),
now: () => r.inspection.unlockAt!,
).evaluator,
same(evaluateSecurityInput),
);
Stage 4c: steps 9 to 18, the opening of the three formats lib/src/open.dart and open3.dart port Open and openBody of package capsule of datekeys-go at c531e93, with the checks, codes, steps and texts of the reference and the detail of each check: the .dkk of step 9.a, decoded or still encoded, with its material, its critical extensions, its capsule_id and its capsule_digest; at least one credential; the clock and the release, with the rule of step 9 for the failures of its source (sourceFailure); its verification at step 10; OUTER_TIME_AGE with the tlock stanza; INNER_ACCESS_AGE with the X25519 identities, those of the .dkk and the key of words, and the rules of the slots; CONTROL_CBOR, header_binding, I_PAYLOAD and P; PAYLOAD_AGE streamed, with the padding of format 2 checked and never delivered, and in format 3 the frame of BODY, the area, the head, each file to the sink with its SHA-256 and the padding, with the precedence of spec §63; and the commit. A failure of age keeps the code of the identity that reports it, or is ERR_INTEGRITY with the reason of its phase, as classify of Go. openCapsule opens a capsule in memory and openCapsuleSource one that a ByteSource reads: the prefix of the inspection and the nonce of PAYLOAD_AGE before the release, then pieces of 1 MiB. lib/src/sink.dart has ByteSink, for the content of formats 1 and 2 and for each file, and FileSink, for the files of format 3, as the dst and the Sink of Go, with MemoryByteSink and MemoryFileSink. Nothing is presented as valid before step 17 ends: the output is closed only then and aborted after any failure, and the sink aborted after any failure that follows its begin (spec §56). The signature and the seal are stage 5: lib/src/verdicts.dart has the verdicts and the SecurityEvaluator, given what newSecurityContext and EvaluateSecurityIn of Go take, which never fails the opening; the default evaluates nothing. OpenOptions.accept is Accept of Go. And AgePayloadDecryptor.wipe clears the key of a STREAM left unread. The tests run open_cases.json and the mutation corpus with the texts and the checks of Go, in memory and from a source read in pieces; capsules of several MiB made from the fixtures, for the streaming; a capsule with a stanza for a key of words; and the caller, the sinks and the evaluator. open_test.dart runs on Node.js too, with open_vectors.g.dart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
final d = await openCapsule(
dkc,
optionsOf(single, sink: MemoryFileSink()),
);
final want = single['verdicts']! as Json;
Stage 4c: steps 9 to 18, the opening of the three formats lib/src/open.dart and open3.dart port Open and openBody of package capsule of datekeys-go at c531e93, with the checks, codes, steps and texts of the reference and the detail of each check: the .dkk of step 9.a, decoded or still encoded, with its material, its critical extensions, its capsule_id and its capsule_digest; at least one credential; the clock and the release, with the rule of step 9 for the failures of its source (sourceFailure); its verification at step 10; OUTER_TIME_AGE with the tlock stanza; INNER_ACCESS_AGE with the X25519 identities, those of the .dkk and the key of words, and the rules of the slots; CONTROL_CBOR, header_binding, I_PAYLOAD and P; PAYLOAD_AGE streamed, with the padding of format 2 checked and never delivered, and in format 3 the frame of BODY, the area, the head, each file to the sink with its SHA-256 and the padding, with the precedence of spec §63; and the commit. A failure of age keeps the code of the identity that reports it, or is ERR_INTEGRITY with the reason of its phase, as classify of Go. openCapsule opens a capsule in memory and openCapsuleSource one that a ByteSource reads: the prefix of the inspection and the nonce of PAYLOAD_AGE before the release, then pieces of 1 MiB. lib/src/sink.dart has ByteSink, for the content of formats 1 and 2 and for each file, and FileSink, for the files of format 3, as the dst and the Sink of Go, with MemoryByteSink and MemoryFileSink. Nothing is presented as valid before step 17 ends: the output is closed only then and aborted after any failure, and the sink aborted after any failure that follows its begin (spec §56). The signature and the seal are stage 5: lib/src/verdicts.dart has the verdicts and the SecurityEvaluator, given what newSecurityContext and EvaluateSecurityIn of Go take, which never fails the opening; the default evaluates nothing. OpenOptions.accept is Accept of Go. And AgePayloadDecryptor.wipe clears the key of a STREAM left unread. The tests run open_cases.json and the mutation corpus with the texts and the checks of Go, in memory and from a source read in pieces; capsules of several MiB made from the fixtures, for the streaming; a capsule with a stanza for a key of words; and the caller, the sinks and the evaluator. open_test.dart runs on Node.js too, with open_vectors.g.dart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
expect(
[
d.verdicts!.signature?.code,
d.verdicts!.seal?.code,
d.verdicts!.lines,
],
[want['signature'], want['seal'], want['lines']],
Stage 4c: steps 9 to 18, the opening of the three formats lib/src/open.dart and open3.dart port Open and openBody of package capsule of datekeys-go at c531e93, with the checks, codes, steps and texts of the reference and the detail of each check: the .dkk of step 9.a, decoded or still encoded, with its material, its critical extensions, its capsule_id and its capsule_digest; at least one credential; the clock and the release, with the rule of step 9 for the failures of its source (sourceFailure); its verification at step 10; OUTER_TIME_AGE with the tlock stanza; INNER_ACCESS_AGE with the X25519 identities, those of the .dkk and the key of words, and the rules of the slots; CONTROL_CBOR, header_binding, I_PAYLOAD and P; PAYLOAD_AGE streamed, with the padding of format 2 checked and never delivered, and in format 3 the frame of BODY, the area, the head, each file to the sink with its SHA-256 and the padding, with the precedence of spec §63; and the commit. A failure of age keeps the code of the identity that reports it, or is ERR_INTEGRITY with the reason of its phase, as classify of Go. openCapsule opens a capsule in memory and openCapsuleSource one that a ByteSource reads: the prefix of the inspection and the nonce of PAYLOAD_AGE before the release, then pieces of 1 MiB. lib/src/sink.dart has ByteSink, for the content of formats 1 and 2 and for each file, and FileSink, for the files of format 3, as the dst and the Sink of Go, with MemoryByteSink and MemoryFileSink. Nothing is presented as valid before step 17 ends: the output is closed only then and aborted after any failure, and the sink aborted after any failure that follows its begin (spec §56). The signature and the seal are stage 5: lib/src/verdicts.dart has the verdicts and the SecurityEvaluator, given what newSecurityContext and EvaluateSecurityIn of Go take, which never fails the opening; the default evaluates nothing. OpenOptions.accept is Accept of Go. And AgePayloadDecryptor.wipe clears the key of a STREAM left unread. The tests run open_cases.json and the mutation corpus with the texts and the checks of Go, in memory and from a source read in pieces; capsules of several MiB made from the fixtures, for the streaming; a capsule with a stanza for a key of words; and the caller, the sinks and the evaluator. open_test.dart runs on Node.js too, with open_vectors.g.dart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
);
expect(want['signature'], 'F0');
Stage 4c: steps 9 to 18, the opening of the three formats lib/src/open.dart and open3.dart port Open and openBody of package capsule of datekeys-go at c531e93, with the checks, codes, steps and texts of the reference and the detail of each check: the .dkk of step 9.a, decoded or still encoded, with its material, its critical extensions, its capsule_id and its capsule_digest; at least one credential; the clock and the release, with the rule of step 9 for the failures of its source (sourceFailure); its verification at step 10; OUTER_TIME_AGE with the tlock stanza; INNER_ACCESS_AGE with the X25519 identities, those of the .dkk and the key of words, and the rules of the slots; CONTROL_CBOR, header_binding, I_PAYLOAD and P; PAYLOAD_AGE streamed, with the padding of format 2 checked and never delivered, and in format 3 the frame of BODY, the area, the head, each file to the sink with its SHA-256 and the padding, with the precedence of spec §63; and the commit. A failure of age keeps the code of the identity that reports it, or is ERR_INTEGRITY with the reason of its phase, as classify of Go. openCapsule opens a capsule in memory and openCapsuleSource one that a ByteSource reads: the prefix of the inspection and the nonce of PAYLOAD_AGE before the release, then pieces of 1 MiB. lib/src/sink.dart has ByteSink, for the content of formats 1 and 2 and for each file, and FileSink, for the files of format 3, as the dst and the Sink of Go, with MemoryByteSink and MemoryFileSink. Nothing is presented as valid before step 17 ends: the output is closed only then and aborted after any failure, and the sink aborted after any failure that follows its begin (spec §56). The signature and the seal are stage 5: lib/src/verdicts.dart has the verdicts and the SecurityEvaluator, given what newSecurityContext and EvaluateSecurityIn of Go take, which never fails the opening; the default evaluates nothing. OpenOptions.accept is Accept of Go. And AgePayloadDecryptor.wipe clears the key of a STREAM left unread. The tests run open_cases.json and the mutation corpus with the texts and the checks of Go, in memory and from a source read in pieces; capsules of several MiB made from the fixtures, for the streaming; a capsule with a stanza for a key of words; and the caller, the sinks and the evaluator. open_test.dart runs on Node.js too, with open_vectors.g.dart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
});
test('shows the verdicts to accept before step 18, which may refuse '
'them', () async {
final dkc = capsuleOf(single, fixture);
final verdicts = Verdicts(
signature: Verdict.signedOther,
seal: Verdict.noSeal,
authorKey: Uint8List(32),
);
Verdicts? shown;
final sink = RecordingSink();
final out = RecordingOutput();
final r = await openCapsule(
dkc,
optionsOf(
single,
sink: sink,
output: out,
evaluator: (_) => verdicts,
accept: (v) {
shown = v;
expect(sink.log, isNot(contains('commit')));
throw 'not signed by Ana';
},
),
);
expect(shown, same(verdicts));
expect(
[r.ok, r.error, r.refusal, r.head, r.verdicts],
[false, null, 'not signed by Ana', null, null],
);
expect(sink.state, 'aborted');
expect(out.aborted, 'not signed by Ana');
expect(
[r.checks.last.step, r.checks.last.ok, r.checks.last.detail],
[
17,
true,
'payload authenticated; the caller refused its verdicts and nothing '
'was published',
],
);
// Accepted, they are those of the result.
final a = await openCapsule(
dkc,
optionsOf(
single,
sink: MemoryFileSink(),
evaluator: (_) => verdicts,
accept: (v) {},
),
);
expect([a.ok, a.verdicts], [true, same(verdicts)]);
});
test('does not wipe the credentials of the caller, and wipes the .dkk it '
'decodes', () async {
final dkc = capsuleOf(keyed, fixture);
final key = decodeAccessKey(fromHex(str(keyed, 'dkk')));
final material = Uint8List.fromList(key.material);
final identity = Uint8List.fromList(material);
final out = MemoryByteSink();
final rel = keyed['release']! as Json;
final r = await openCapsule(
dkc,
OpenOptions(
source: suppliedRelease(
Release(rel['round']! as int, fromHex(str(rel, 'signature'))),
),
now: () => parseRfc3339(str(keyed, 'now')),
accessKey: key,
identities: [identity],
output: out,
),
);
expect(r.ok, isTrue);
expect([key.material, identity], [material, material]);
expect(out.bytes, isNotNull);
});
});
group('the sinks of memory', () {
test('MemoryByteSink gives its bytes once closed, and wipes them on '
'abort', () {
final s = MemoryByteSink();
final a = Uint8List.fromList([1, 2, 3]);
s.add(a);
s.add(Uint8List.fromList([4]));
expect(s.bytes, isNull);
s.close();
expect(s.bytes, [1, 2, 3, 4]);
expect(() => s.add(Uint8List(1)), throwsStateError);
final t = MemoryByteSink();
final b = Uint8List.fromList([9, 9]);
t.add(b);
t.abort('failed');
expect(
[t.bytes, t.abortReason, b],
[
null,
'failed',
[0, 0],
],
);
expect(t.close, throwsStateError);
});
test('MemoryFileSink gives its files only once committed', () {
final s = MemoryFileSink();
final h = Head(
salt: Uint8List(32),
files: [
HeadFile(path: 'a', size: 1, start: 0, end: 1, sha256: Uint8List(32)),
HeadFile(path: 'b', size: 0, start: 1, end: 1, sha256: Uint8List(32)),
],
);
s.begin(h);
s.create(0)
..add(Uint8List.fromList([7]))
..close();
s.create(1).close();
expect([s.head, s.files], [null, null]);
s.commit();
expect(s.head, same(h));
expect(s.files, [
[7],
isEmpty,
]);
final t = MemoryFileSink()..begin(h);
final f = t.create(0)..add(Uint8List.fromList([5]));
t.abort('no');
expect([t.aborted, t.files, f.bytes], [true, null, null]);
});
});
Specification 0.15: the release object, a release in hand and step 9.c specVersion is 0.15 and testdata is synced with datekeys-go at 3c3e737 (branch v0.15): every file changes its spec field, mutations.json gains the field source and four cases, and vectors/release.json and releases/ are new (142 files). release.dart, as provider of Go at 2eeca40, with its texts: encodeRelease and decodeRelease of the release object (spec v0.15, 47.1) with the CBOR profile of the library, its size first, then its type and version, then its schema; parseRelease, which also reads drand's JSON with the rules of encoding/json of Go for its three fields; verifyRelease checks the chain hash a release names, ERR_PROFILE_MISMATCH, before its round and its signature; ReleaseSupplier, EncodedRelease and supplyRelease, a release in the caller's hand; and ReleaseArchive, the lookup of a local release archive (50), whose failures are ERR_RELEASE_UNAVAILABLE at step 9. open: OpenOptions.release takes a release in hand, exclusive with source. It is not compared with the clock (step 9.c); Opened.clockBehind reports a clock behind it, and the step 9 detail is Go's. Step 10 starts with the layers of the object. A network source keeps its behaviour. Opened.release carries the chain hash of the pinned profile. Vectors: mutation_texts.json is regenerated with tool/mutation_go_texts.go, which now replays each case with its source as testkit does: 149 cases change only the detail of step 9 ("release supplied by the caller"), the case "round not reached yet" now opens, and the four new cases are added. release_vectors.json, the open vectors, and the formats, locator, security and seal vectors come out the same at 3c3e737 but for their spec field. tool/release_archive_go_texts.go writes the texts of provider.Archive on edited archives; tool/release_copy.dart copies release.json, releases/ and those texts to release.g.dart for the tests compiled to JavaScript. errors_spec_test reads the spec at the commit of testdata/SOURCE.json rather than at its tag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
group('a release in hand (spec v0.15, §63 step 9.c)', () {
final c = caseNamed('time_and_key_portable: the .dkk, still encoded');
final rel = c['release']! as Json;
final round = rel['round']! as int;
final signature = fromHex(str(rel, 'signature'));
final chain = quicknet().chainHash;
final object = encodeRelease(Release(round, signature, chainHash: chain));
final unlock = parseRfc3339(str(c, 'now'));
Future<Opened> open(
ReleaseSupplier supplier,
Instant now, {
bool withKey = true,
}) => openCapsule(
capsuleOf(c, fixture),
OpenOptions(
release: supplier,
now: () => now,
accessKeyFile: withKey ? fromHex(str(c, 'dkk_file')) : null,
output: RecordingOutput(),
),
);
test('is not compared with the clock, which the opening says is '
'behind', () async {
for (final (name, now, behind) in [
('after the round time', unlock, false),
(
'a clock one nanosecond behind',
Instant(unlock.seconds - 1, 999999999),
true,
),
('a clock years behind', Instant(0), true),
]) {
final o = await open(EncodedRelease(object), now);
expect(o.error, isNull, reason: name);
expect(o.clockBehind, behind, reason: name);
// The release that opened it, with the chain of the pinned
// profile, encodes to the same object.
expect(encodeRelease(o.release!), object, reason: name);
final step9 = o.checks.firstWhere(
(k) => k.step == 9 && k.name == 'release',
);
expect(
step9.detail,
behind
? 'release supplied by the caller; round $round is published at '
'${formatRfc3339(unlock)} and the clock says '
'${formatRfc3339(now)}: it may be behind'
: 'release supplied by the caller',
reason: name,
);
}
// drand's JSON, which names no chain, opens it too.
final json = '{"round":$round,"signature":"${toHex(signature)}"}';
final o = await open(EncodedRelease(utf8.encode(json)), Instant(0));
expect([o.error, o.clockBehind], [null, true]);
expect(o.release!.chainHash, chain);
});
test('keeps the order of steps 9 and 10, with their codes', () async {
final other = Uint8List.fromList(chain)..[5] ^= 1;
for (final (name, supply, withKey, code, step, calls) in [
(
'no credential, before any supply',
() async => object,
false,
'ERR_ACCESS_REQUIRED',
9,
0,
),
(
'nothing supplied',
() async =>
throw DateKeysException(ErrorCode.releaseUnavailable, 'none'),
true,
'ERR_RELEASE_UNAVAILABLE',
9,
1,
),
(
'a supplier failing with another code',
() async => throw DateKeysException(ErrorCode.integrity, 'odd'),
true,
'ERR_RELEASE_UNAVAILABLE',
9,
1,
),
(
'an empty object',
() async => Uint8List(0),
true,
'ERR_NON_CANONICAL_CBOR',
10,
1,
),
(
'another chain and another round',
() async =>
encodeRelease(Release(round + 1, signature, chainHash: other)),
true,
'ERR_PROFILE_MISMATCH',
10,
1,
),
(
'another round',
() async =>
encodeRelease(Release(round + 1, signature, chainHash: chain)),
true,
'ERR_ROUND_MISMATCH',
10,
1,
),
(
"drand's JSON of another round",
() async => utf8.encode('{"round":${round + 1},"signature":"00"}'),
true,
'ERR_ROUND_MISMATCH',
10,
1,
),
]) {
final s = _CountingSupplier(supply);
final o = await open(s, Instant(0), withKey: withKey);
expect(
[o.error?.code.code, o.checks.last.step, s.calls],
[code, step, calls],
reason: name,
);
// As Go, the clock behind is reported once the release is in
// hand, even when step 10 then fails.
expect(o.clockBehind, step == 10, reason: name);
expect(o.release, isNull, reason: name);
}
});
test('is exclusive with a source, and one of them is required', () async {
for (final o in [
OpenOptions(
source: suppliedRelease(),
release: EncodedRelease(object),
now: () => unlock,
output: RecordingOutput(),
),
OpenOptions(now: () => unlock, output: RecordingOutput()),
]) {
await expectLater(
openCapsule(capsuleOf(c, fixture), o),
throwsA(
isArgumentError.having(
(e) => '${e.message}',
'message',
'open: set exactly one of OpenOptions.source and '
'OpenOptions.release',
),
),
);
expect((o.output! as RecordingOutput).aborted, isArgumentError);
}
});
});
Stage 4c: steps 9 to 18, the opening of the three formats lib/src/open.dart and open3.dart port Open and openBody of package capsule of datekeys-go at c531e93, with the checks, codes, steps and texts of the reference and the detail of each check: the .dkk of step 9.a, decoded or still encoded, with its material, its critical extensions, its capsule_id and its capsule_digest; at least one credential; the clock and the release, with the rule of step 9 for the failures of its source (sourceFailure); its verification at step 10; OUTER_TIME_AGE with the tlock stanza; INNER_ACCESS_AGE with the X25519 identities, those of the .dkk and the key of words, and the rules of the slots; CONTROL_CBOR, header_binding, I_PAYLOAD and P; PAYLOAD_AGE streamed, with the padding of format 2 checked and never delivered, and in format 3 the frame of BODY, the area, the head, each file to the sink with its SHA-256 and the padding, with the precedence of spec §63; and the commit. A failure of age keeps the code of the identity that reports it, or is ERR_INTEGRITY with the reason of its phase, as classify of Go. openCapsule opens a capsule in memory and openCapsuleSource one that a ByteSource reads: the prefix of the inspection and the nonce of PAYLOAD_AGE before the release, then pieces of 1 MiB. lib/src/sink.dart has ByteSink, for the content of formats 1 and 2 and for each file, and FileSink, for the files of format 3, as the dst and the Sink of Go, with MemoryByteSink and MemoryFileSink. Nothing is presented as valid before step 17 ends: the output is closed only then and aborted after any failure, and the sink aborted after any failure that follows its begin (spec §56). The signature and the seal are stage 5: lib/src/verdicts.dart has the verdicts and the SecurityEvaluator, given what newSecurityContext and EvaluateSecurityIn of Go take, which never fails the opening; the default evaluates nothing. OpenOptions.accept is Accept of Go. And AgePayloadDecryptor.wipe clears the key of a STREAM left unread. The tests run open_cases.json and the mutation corpus with the texts and the checks of Go, in memory and from a source read in pieces; capsules of several MiB made from the fixtures, for the streaming; a capsule with a stanza for a key of words; and the caller, the sinks and the evaluator. open_test.dart runs on Node.js too, with open_vectors.g.dart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
group('the sources', () {
test('BytesSource reads views of its bytes, and readRange keeps '
'reading', () async {
final b = Uint8List.fromList(List.generate(100, (i) => i));
final s = BytesSource(b);
expect(s.length, 100);
expect(await s.read(98, 10), [98, 99]);
expect(await s.read(100, 10), isEmpty);
final c = ChunkySource(b, 7);
final r = await readSource(c, 3, 50);
expect(r, List.generate(50, (i) => i + 3));
expect(c.reads, 8);
expect(await readSource(c, 90, 50), List.generate(10, (i) => 90 + i));
});
test('a source that gives nothing before its end is an error of the '
'caller', () async {
final r = await openCapsuleSource(
_Short(capsuleOf(singleCase(), fixture)),
optionsOf(singleCase(), sink: MemoryFileSink()),
).then<Object?>((o) => o, onError: (Object e) => e);
expect(r, isStateError);
});
});
}
Json singleCase() => caseNamed('format3_single');
/// readRange of source.dart, which lib/datekeys.dart does not export.
Future<Uint8List> readSource(ByteSource s, int offset, int n) async {
final out = BytesBuilder();
for (var at = offset; at < offset + n && at < s.length;) {
final piece = await s.read(at, offset + n - at);
if (piece.isEmpty) break;
out.add(piece);
at += piece.length;
}
return out.takeBytes();
}
// A source whose reads past its first 100 bytes give nothing.
final class _Short implements ByteSource {
_Short(this._b);
final Uint8List _b;
@override
int get length => _b.length;
@override
Future<Uint8List> read(int offset, int n) async {
if (offset >= 100) return Uint8List(0);
final end = offset + n < 100 ? offset + n : 100;
return Uint8List.sublistView(_b, offset, end);
}
}
// A RecordingSink with the files of [m], for filesOf.
RecordingSink _asRecording(MemoryFileSink m) {
final r = RecordingSink();
r.files = [for (final f in m.files!) BytesBuilder()..add(f)];
return r;
}
Specification 0.15: the release object, a release in hand and step 9.c specVersion is 0.15 and testdata is synced with datekeys-go at 3c3e737 (branch v0.15): every file changes its spec field, mutations.json gains the field source and four cases, and vectors/release.json and releases/ are new (142 files). release.dart, as provider of Go at 2eeca40, with its texts: encodeRelease and decodeRelease of the release object (spec v0.15, 47.1) with the CBOR profile of the library, its size first, then its type and version, then its schema; parseRelease, which also reads drand's JSON with the rules of encoding/json of Go for its three fields; verifyRelease checks the chain hash a release names, ERR_PROFILE_MISMATCH, before its round and its signature; ReleaseSupplier, EncodedRelease and supplyRelease, a release in the caller's hand; and ReleaseArchive, the lookup of a local release archive (50), whose failures are ERR_RELEASE_UNAVAILABLE at step 9. open: OpenOptions.release takes a release in hand, exclusive with source. It is not compared with the clock (step 9.c); Opened.clockBehind reports a clock behind it, and the step 9 detail is Go's. Step 10 starts with the layers of the object. A network source keeps its behaviour. Opened.release carries the chain hash of the pinned profile. Vectors: mutation_texts.json is regenerated with tool/mutation_go_texts.go, which now replays each case with its source as testkit does: 149 cases change only the detail of step 9 ("release supplied by the caller"), the case "round not reached yet" now opens, and the four new cases are added. release_vectors.json, the open vectors, and the formats, locator, security and seal vectors come out the same at 3c3e737 but for their spec field. tool/release_archive_go_texts.go writes the texts of provider.Archive on edited archives; tool/release_copy.dart copies release.json, releases/ and those texts to release.g.dart for the tests compiled to JavaScript. errors_spec_test reads the spec at the commit of testdata/SOURCE.json rather than at its tag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
// A supplier that counts the requests and answers with [supply].
final class _CountingSupplier implements ReleaseSupplier {
_CountingSupplier(this.supply_);
final Future<List<int>> Function() supply_;
int calls = 0;
@override
Future<Uint8List> supply(PinnedProfile p, int round) async {
calls++;
return Uint8List.fromList(await supply_());
}
}

Powered by TurnKey Linux.