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

265 lines
8.4 KiB

// The verdicts of a signature of alg 2 and of a seal of seal_type 2 on the
// VM and compiled to JavaScript: the part of the vectors of Go that
// test/vectors/securitycms_vectors.g.dart holds, which
// tool/security_go_vectors.go makes and evaluates with package capsule of
// the reference; the fixtures format3_signed_cms and format3_sealed opened
// to the verdicts and lines of their records; and what the API does:
// SIGNERS, the reader of CMS as the default of evaluateSecurity and of the
// opening, and Go's zero time as no time.
library;
import 'dart:convert';
import 'dart:typed_data';
import 'package:datekeys/datekeys.dart';
import 'package:datekeys/src/bech32.dart' show bech32Encode;
import 'package:test/test.dart';
import 'open_support.dart';
import 'securitycms_support.dart';
import 'vectors/securitycms_vectors.g.dart';
final Json _part = jsonDecode(securityCmsVectorsJson) as Json;
List<Json> get _cases => (_part['cases']! as List).cast<Json>();
/// The context of the vectors named [name].
SecurityContext _context(String name) {
final all = (_part['contexts']! as List).cast<Json>();
return contextsOf(_part)[all.indexWhere((c) => c['name'] == name)];
}
/// A sink of files that keeps them.
final class _Files implements FileSink {
final files = <BytesBuilder>[];
bool committed = false;
@override
void begin(Head head) =>
files.addAll([for (final _ in head.files) BytesBuilder()]);
@override
ByteSink create(int i) => _File(files[i]);
@override
void commit() => committed = true;
@override
void abort(Object reason) {}
}
final class _File implements ByteSink {
_File(this.b);
final BytesBuilder b;
@override
void add(Uint8List bytes) => b.add(bytes);
@override
void close() {}
@override
void abort(Object reason) {}
}
void main() {
test('the part of the vectors is of this spec, from its generator', () {
expect(_part['spec'], specVersion);
expect(_part['generator'], 'tool/security_go_vectors.go');
expect(_cases, hasLength(greaterThan(40)));
});
test('each case of the part, with the verdicts, the lines, the detail '
'and the earliest seal of Go', () {
final bases = basesOf(_part, const []);
final contexts = contextsOf(_part);
final texts = (_part['texts']! as List).cast<String>();
for (final c in _cases) {
expect(
evaluateDifferences(c, cmsAreaOf(c, const [], bases), contexts, texts),
isEmpty,
reason: c['name'] as String? ?? canonical(c),
);
}
});
group('the fixtures', () {
final fixtures =
((jsonDecode(securityCmsFixturesJson) as Json)['fixtures']! as List)
.cast<Json>();
for (final x in fixtures) {
test(
'${x['name']} opens to the verdicts and lines of its record',
() async {
final rel = x['release']! as Json;
final files = _Files();
final o = await openCapsule(
fromHex(str(x, 'dkc')),
OpenOptions(
source: suppliedRelease(
Release(rel['round']! as int, fromHex(str(rel, 'signature'))),
),
now: () => parseRfc3339(str(x, 'unlock_at')),
sink: files,
),
);
expect([o.ok, files.committed], [true, true], reason: '${o.error}');
final v = o.verdicts!;
expect(
verdictDifferences(x['verdicts']! as Json, v, keys: false),
isEmpty,
);
final key = v.authorKey;
expect(
key == null ? null : bech32Encode('dkauthor', key),
(x['verdicts']! as Json)['author_key'],
);
final d = v.detail!;
expect(
canonical(d.signers.isEmpty ? null : signerResults(d.signers)),
canonical(x['signer_results']),
);
final seal = x['seal'] as Json?;
expect(
canonical(
seal == null
? null
: {'holder': d.sealHolder, 'time': timeText(d.sealTime)},
),
canonical(seal),
);
},
);
}
});
group('SIGNERS', () {
final a = Uint8List(32)..[0] = 0x61;
final b = Uint8List(32)..[0] = 0x62;
test('encodeSigners sorts the hashes, as EncodeSigners of Go', () {
final x = encodeSigners([a, b]);
expect(encodeSigners([b, a]), x);
expect(x, hasLength(1 + 2 * 34));
expect(x.sublist(0, 3), [0x82, 0x58, 0x20]);
expect(maxSigners, 16);
expect(
encodeSigners(List.generate(16, (i) => Uint8List(32)..[31] = i)),
hasLength(1 + 16 * 34),
);
});
test('encodeSigners refuses what Go refuses, with its texts', () {
for (final (hashes, text) in [
(<List<int>>[], 'capsule: SIGNERS holds from 1 to 16 certificates'),
([a, a], 'capsule: SIGNERS names a certificate twice'),
(
List.generate(17, (i) => Uint8List(32)..[31] = i),
'capsule: SIGNERS holds from 1 to 16 certificates',
),
([Uint8List(31)], 'capsule: a SHA-256 of 31 bytes, want 32'),
]) {
expect(
() => encodeSigners(hashes),
throwsA(
isA<ArgumentError>().having((e) => e.message, 'message', text),
),
);
}
});
});
group('the reader of CMS', () {
test('is the default of evaluateSecurity and of the opening', () {
for (final c in _cases.take(12)) {
final area = cmsAreaOf(c, const [], basesOf(_part, const []));
final context = contextsOf(_part)[c['context']! as int];
final byDefault = evaluateSecurity(area, context: context);
final given = evaluateSecurity(area, context: context, cms: cmsReader);
expect(byDefault.lines, given.lines);
expect(
[byDefault.signature, byDefault.seal],
[given.signature, given.seal],
);
}
final options = OpenOptions(
source: suppliedRelease(Release(1, Uint8List(48))),
now: () => Instant(0),
);
expect(options.evaluator, same(evaluateSecurityInput));
});
test('reads a round time at Go\'s zero time as no round time, as '
'IsZero', () {
// A seal of key 3 before the round time: S4; without a round time, or
// at 0001-01-01T00:00:00Z, S5.
final c = _cases.firstWhere(
(c) =>
c['seal'] == 'S4' &&
((c['cms'] as List?) ?? const []).contains('seal'),
);
final area = cmsAreaOf(c, const [], basesOf(_part, const []));
final context = contextsOf(_part)[c['context']! as int];
SecurityContext at(Instant? round) => SecurityContext(
controlCommit: context.controlCommit,
headDigest: context.headDigest,
roundTime: round,
);
expect(evaluateSecurity(area, context: context).seal, Verdict.sealed);
for (final round in [null, Instant(-62135596800)]) {
final v = evaluateSecurity(area, context: at(round));
expect(v.seal, Verdict.sealedLate, reason: '$round');
expect(v.detail!.sealTime, isNotNull);
}
// A nanosecond after Go's zero time is a time: the seal of 2026 is not
// before it either.
expect(
evaluateSecurity(area, context: at(Instant(-62135596800, 1))).seal,
Verdict.sealedLate,
);
expect(_context('the same, without a round time').roundTime, isNull);
});
});
test('the earliest seal ignores Go\'s zero time, as SealedAt of Go', () {
final zero = Instant(-62135596800);
final later = parseRfc3339('2026-09-30T12:00:00Z');
SignerLine signer(Instant t) => SignerLine(
holder: 'Ana',
issuer: 'CA',
result: SignerResult.valid,
sealHolder: 'TSA',
sealTime: t,
);
expect(
Verdicts(
signature: Verdict.noSignature,
seal: Verdict.sealed,
detail: Detail(sealHolder: 'TSA', sealTime: zero),
).sealedAt,
isNull,
);
expect(
Verdicts(
signature: Verdict.signedComplete,
seal: Verdict.sealed,
detail: Detail(
signers: [signer(zero), signer(later)],
sealHolder: 'TSA',
sealTime: zero,
),
).sealedAt,
later,
);
// The line shows it all the same, as Go writes it.
expect(
Verdicts(
signature: Verdict.noSignature,
seal: Verdict.sealed,
detail: Detail(sealHolder: 'TSA', sealTime: zero),
).lines.last,
contains('existía el 0001-01-01T00:00:00Z'),
);
});
}

Powered by TurnKey Linux.