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.
265 lines
8.4 KiB
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'),
|
|
);
|
|
});
|
|
}
|