// 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 get _cases => (_part['cases']! as List).cast(); /// The context of the vectors named [name]. SecurityContext _context(String name) { final all = (_part['contexts']! as List).cast(); return contextsOf(_part)[all.indexWhere((c) => c['name'] == name)]; } /// A sink of files that keeps them. final class _Files implements FileSink { final files = []; 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(); 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(); 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 [ (>[], '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().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'), ); }); }