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,
|
Stage 5b: the opening evaluates the security area as Go
OpenOptions.evaluator is now evaluateSecurityInput, so that opening a
capsule of format 3 gives the verdicts of Go at step 17: the signature of
alg 1 and every verdict of the form, with the author keys of the options,
and the signature of alg 2 and the seal of seal_type 2 not evaluated
until a reader of CMS is given. notEvaluated stays for a caller that
shows no verdict.
tool/open_go_vectors.go records the verdicts of each capsule of format 3
that opens, with their lines, the key and the label of alg 1 and the
earliest valid seal, and opens the fixtures signed with alg 1 also with
their author key saved, F3, and with another, F4. Every case of
open_cases.json and every case of the mutation corpus that opens gives
those verdicts and lines: format3_signed, format3_unsigned,
format3_signature_unsupported, format3_seal_unsupported,
format3_security_v2 and format3_note all of them, and format3_signed_cms
and format3_sealed the part that needs no reader of CMS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
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,
|
Stage 5b: the opening evaluates the security area as Go
OpenOptions.evaluator is now evaluateSecurityInput, so that opening a
capsule of format 3 gives the verdicts of Go at step 17: the signature of
alg 1 and every verdict of the form, with the author keys of the options,
and the signature of alg 2 and the seal of seal_type 2 not evaluated
until a reader of CMS is given. notEvaluated stays for a caller that
shows no verdict.
tool/open_go_vectors.go records the verdicts of each capsule of format 3
that opens, with their lines, the key and the label of alg 1 and the
earliest valid seal, and opens the fixtures signed with alg 1 also with
their author key saved, F3, and with another, F4. Every case of
open_cases.json and every case of the mutation corpus that opens gives
those verdicts and lines: format3_signed, format3_unsigned,
format3_signature_unsupported, format3_seal_unsupported,
format3_security_v2 and format3_note all of them, and format3_signed_cms
and format3_sealed the part that needs no reader of CMS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
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(
|
Stage 5b: the opening evaluates the security area as Go
OpenOptions.evaluator is now evaluateSecurityInput, so that opening a
capsule of format 3 gives the verdicts of Go at step 17: the signature of
alg 1 and every verdict of the form, with the author keys of the options,
and the signature of alg 2 and the seal of seal_type 2 not evaluated
until a reader of CMS is given. notEvaluated stays for a caller that
shows no verdict.
tool/open_go_vectors.go records the verdicts of each capsule of format 3
that opens, with their lines, the key and the label of alg 1 and the
earliest valid seal, and opens the fixtures signed with alg 1 also with
their author key saved, F3, and with another, F4. Every case of
open_cases.json and every case of the mutation corpus that opens gives
those verdicts and lines: format3_signed, format3_unsigned,
format3_signature_unsupported, format3_seal_unsupported,
format3_security_v2 and format3_note all of them, and format3_signed_cms
and format3_sealed the part that needs no reader of CMS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
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],
|
|
|
|
|
);
|
Stage 5b: the opening evaluates the security area as Go
OpenOptions.evaluator is now evaluateSecurityInput, so that opening a
capsule of format 3 gives the verdicts of Go at step 17: the signature of
alg 1 and every verdict of the form, with the author keys of the options,
and the signature of alg 2 and the seal of seal_type 2 not evaluated
until a reader of CMS is given. notEvaluated stays for a caller that
shows no verdict.
tool/open_go_vectors.go records the verdicts of each capsule of format 3
that opens, with their lines, the key and the label of alg 1 and the
earliest valid seal, and opens the fixtures signed with alg 1 also with
their author key saved, F3, and with another, F4. Every case of
open_cases.json and every case of the mutation corpus that opens gives
those verdicts and lines: format3_signed, format3_unsigned,
format3_signature_unsupported, format3_seal_unsupported,
format3_security_v2 and format3_note all of them, and format3_signed_cms
and format3_sealed the part that needs no reader of CMS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
// 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()),
|
|
|
|
|
);
|
Stage 5b: the opening evaluates the security area as Go
OpenOptions.evaluator is now evaluateSecurityInput, so that opening a
capsule of format 3 gives the verdicts of Go at step 17: the signature of
alg 1 and every verdict of the form, with the author keys of the options,
and the signature of alg 2 and the seal of seal_type 2 not evaluated
until a reader of CMS is given. notEvaluated stays for a caller that
shows no verdict.
tool/open_go_vectors.go records the verdicts of each capsule of format 3
that opens, with their lines, the key and the label of alg 1 and the
earliest valid seal, and opens the fixtures signed with alg 1 also with
their author key saved, F3, and with another, F4. Every case of
open_cases.json and every case of the mutation corpus that opens gives
those verdicts and lines: format3_signed, format3_unsigned,
format3_signature_unsupported, format3_seal_unsupported,
format3_security_v2 and format3_note all of them, and format3_signed_cms
and format3_sealed the part that needs no reader of CMS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
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(
|
Stage 5b: the opening evaluates the security area as Go
OpenOptions.evaluator is now evaluateSecurityInput, so that opening a
capsule of format 3 gives the verdicts of Go at step 17: the signature of
alg 1 and every verdict of the form, with the author keys of the options,
and the signature of alg 2 and the seal of seal_type 2 not evaluated
until a reader of CMS is given. notEvaluated stays for a caller that
shows no verdict.
tool/open_go_vectors.go records the verdicts of each capsule of format 3
that opens, with their lines, the key and the label of alg 1 and the
earliest valid seal, and opens the fixtures signed with alg 1 also with
their author key saved, F3, and with another, F4. Every case of
open_cases.json and every case of the mutation corpus that opens gives
those verdicts and lines: format3_signed, format3_unsigned,
format3_signature_unsupported, format3_seal_unsupported,
format3_security_v2 and format3_note all of them, and format3_signed_cms
and format3_sealed the part that needs no reader of CMS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
[
|
|
|
|
|
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
|
|
|
);
|
Stage 5b: the opening evaluates the security area as Go
OpenOptions.evaluator is now evaluateSecurityInput, so that opening a
capsule of format 3 gives the verdicts of Go at step 17: the signature of
alg 1 and every verdict of the form, with the author keys of the options,
and the signature of alg 2 and the seal of seal_type 2 not evaluated
until a reader of CMS is given. notEvaluated stays for a caller that
shows no verdict.
tool/open_go_vectors.go records the verdicts of each capsule of format 3
that opens, with their lines, the key and the label of alg 1 and the
earliest valid seal, and opens the fixtures signed with alg 1 also with
their author key saved, F3, and with another, F4. Every case of
open_cases.json and every case of the mutation corpus that opens gives
those verdicts and lines: format3_signed, format3_unsigned,
format3_signature_unsupported, format3_seal_unsupported,
format3_security_v2 and format3_note all of them, and format3_signed_cms
and format3_sealed the part that needs no reader of CMS.
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_());
|
|
|
|
|
}
|
|
|
|
|
}
|