// The security area on the VM and compiled to JavaScript: the part of the // vectors of Go that test/vectors/security_vectors.g.dart holds, which // tool/security_go_vectors.go writes with package capsule of the reference, // and what the API does: the boundary with the reader of CMS of alg 2 and // seal_type 2, an evaluation that never throws, and the context that the // opening builds. library; import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; import 'package:datekeys/datekeys.dart'; import 'package:test/test.dart'; import 'open_vectors_support.dart'; import 'security_support.dart'; import 'vectors/security_vectors.g.dart'; final Json _vectors = jsonDecode(securityVectorsJson) as Json; List _section(String name) => (_vectors[name]! as List).cast(); /// A reader of CMS that answers what it is given, and records its calls. final class FakeCms implements CmsEvaluator { FakeCms({this.signature, this.seal, this.throws = false}); final CmsSignatureVerdict? Function()? signature; final CmsSealVerdict Function()? seal; final bool throws; final calls = []; Uint8List? signedWith; bool? hasSeal; @override CmsSignatureVerdict? evaluateSignature( Uint8List signers, Uint8List value, bool hasSeal, SecurityContext context, ) { calls.add('signature'); this.hasSeal = hasSeal; if (throws) throw StateError('broken'); return signature!(); } @override CmsSealVerdict evaluateSeal( Uint8List token, Uint8List? signature, SecurityContext context, ) { calls.add('seal'); signedWith = signature; if (throws) throw StateError('broken'); return seal!(); } } void main() { test('the part of the vectors is of this spec, from its generator', () { expect(_vectors['spec'], specVersion); expect(_vectors['generator'], 'tool/security_go_vectors.go'); expect(_section('evaluate'), hasLength(greaterThan(150))); }); test('the commitments, as Go builds them', () { expect(commitmentDifferences(_vectors['commitments']! as Json), isEmpty); }); test('the encoders, as Go writes them', () { expect(encodeDifferences(_section('encode')), isEmpty); expect(encodeSecurity(), hasLength(22)); }); test('every eighth evaluation, with the verdicts and lines of Go', () { final contexts = contextsOf(_vectors); final texts = (_vectors['texts']! as List).cast(); final bases = [ for (final b in (_vectors['bases']! as List).cast()) fromHex(b), ]; for (final c in _section('evaluate')) { final security = securityOf(c, bases); expect( evaluateDifferences(c, security, contexts, texts), isEmpty, reason: canonical(c), ); } }); test('the lines and the earliest seal of every verdict and detail', () { for (final c in _section('lines')) { expect(lineDifferences(c), isEmpty, reason: str(c, 'name')); } }); test('the name of a certificate, as holderText of Go', () { final cases = _section('holder'); expect(cases, hasLength(greaterThan(200))); for (final c in cases) { expect( holderText(nameOf(c), fromHex(str(c, 'hash'))), c['result'], reason: canonical(c), ); } // A hash that is not a SHA-256 is an error of the caller. expect(() => holderText('Ana', Uint8List(31)), throwsArgumentError); }); group('the reader of CMS', () { final context = SecurityContext( controlCommit: Uint8List(32)..[0] = 1, headDigest: Uint8List(32)..[0] = 2, roundTime: parseRfc3339('2030-01-01T00:00:00Z'), ); final alg2 = encodeAuthorSignature(algCms, [0x80], [1, 2, 3]); final seal2 = encodeSeal(sealTypeRfc3161, [4, 5, 6]); final ana = SignerLine( holder: 'Ana', issuer: 'CA', result: SignerResult.valid, sealHolder: 'TSA', sealTime: parseRfc3339('2026-09-30T12:00:00.5Z'), before: true, ); final tsaTime = parseRfc3339('2026-09-29T10:00:00Z'); test('is cmsReader by default, and the opening evaluates with it', () { // SIGNERS empty is F1, and a token that is not DER, S2. final v = evaluateSecurity( encodeSecurityWith(signature: alg2, seal: seal2), context: context, ); expect( [v.signature, v.seal, v.evaluated], [Verdict.signatureUnchecked, Verdict.sealUnreadable, true], ); expect(v.lines, [ Verdict.signatureUnchecked.text, Verdict.sealUnreadable.text, ]); }); test('without one, alg 2 and seal_type 2 are not evaluated', () { final v = evaluateSecurity( encodeSecurityWith(signature: alg2, seal: seal2), context: context, cms: null, ); expect( [v.signature, v.seal, v.evaluated, v.lines], [null, null, false, isEmpty], ); final s = evaluateSecurity( encodeSecurityWith(signature: alg2, seal: encodeSeal(1, [1])), context: context, cms: null, ); expect( [s.signature, s.seal, s.lines], [ null, Verdict.sealUnsupported, [Verdict.sealUnsupported.text], ], ); final f = evaluateSecurity( encodeSecurityWith( signature: encodeAuthorSignature(9, [], []), seal: seal2, ), context: context, cms: null, ); expect( [f.signature, f.seal, f.lines], [ Verdict.signatureUnchecked, null, [Verdict.signatureUnchecked.text], ], ); expect('$f', 'Verdicts(F1, not evaluated)'); }); test('without a context it is never asked, as a reader of v0.10', () { final cms = FakeCms(); final v = evaluateSecurity( encodeSecurityWith(signature: alg2, seal: seal2), cms: cms, ); expect( [v.signature, v.seal], [Verdict.signatureUnchecked, Verdict.sealUnsupported], ); expect(cms.calls, isEmpty); }); test('gives F2, F5 or F6 with the signers, and the seal with its ' 'authority, in one detail', () { final cms = FakeCms( signature: () => CmsSignatureVerdict(Verdict.signedComplete, Detail(signers: [ana])), seal: () => CmsSealVerdict(Verdict.sealed, holder: 'TSA 2', time: tsaTime), ); final security = encodeSecurityWith(signature: alg2, seal: seal2); final v = evaluateSecurity(security, context: context, cms: cms); expect([v.signature, v.seal], [Verdict.signedComplete, Verdict.sealed]); expect(cms.calls, ['signature', 'seal']); expect([cms.hasSeal, cms.signedWith], [true, alg2]); expect(v.detail!.signers, [ana]); expect([v.detail!.sealHolder, v.detail!.sealTime], ['TSA 2', tsaTime]); // The lines are those of these verdicts, which the vectors of Go // check: those of F6 with its signer, and S4 with its authority. final merged = Verdicts( signature: Verdict.signedComplete, seal: Verdict.sealed, detail: Detail(signers: [ana], sealHolder: 'TSA 2', sealTime: tsaTime), ); expect(v.lines, merged.lines); expect(v.lines, hasLength(4)); expect(v.sealedAt, tsaTime); // A seal without a signature seals SIG_PART 0x00. final alone = evaluateSecurity( encodeSecurityWith(seal: seal2), context: context, cms: cms, ); expect( [alone.signature, alone.seal, cms.signedWith], [Verdict.noSignature, Verdict.sealed, null], ); }); test('null is F1, and S1 to S3 name no authority', () { final cms = FakeCms( signature: () => null, seal: () => CmsSealVerdict(Verdict.sealInvalid, holder: 'x', time: tsaTime), ); final v = evaluateSecurity( encodeSecurityWith(signature: alg2, seal: seal2), context: context, cms: cms, ); expect( [v.signature, v.seal, v.detail], [Verdict.signatureUnchecked, Verdict.sealInvalid, null], ); }); test('what it throws, or a verdict out of its range, fails its own part ' 'only, as a panic in Go', () { final security = encodeSecurityWith(signature: alg2, seal: seal2); final broken = evaluateSecurity( security, context: context, cms: FakeCms(throws: true), ); expect( [broken.signature, broken.seal, broken.detail], [Verdict.signatureUnchecked, Verdict.sealUnreadable, null], ); for (final (sig, seal) in [ ( () => CmsSignatureVerdict(Verdict.signedSaved, Detail()), () => const CmsSealVerdict(Verdict.noSeal), ), ( () => CmsSignatureVerdict(Verdict.unreadable, Detail()), () => const CmsSealVerdict(Verdict.sealed, holder: 'no time'), ), ]) { final v = evaluateSecurity( security, context: context, cms: FakeCms(signature: sig, seal: seal), ); expect( [v.signature, v.seal, v.detail], [Verdict.signatureUnchecked, Verdict.sealUnreadable, null], ); } // The seal fails, and the signers of the signature stay. final half = evaluateSecurity( security, context: context, cms: FakeCms( signature: () => CmsSignatureVerdict( Verdict.signedIncomplete, Detail(signers: [ana]), ), seal: () => throw StateError('broken'), ), ); expect( [half.signature, half.seal], [Verdict.signedIncomplete, Verdict.sealUnreadable], ); expect( [half.detail!.signers, half.detail!.sealTime], [ [ana], null, ], ); }); }); test('never throws, whatever the area holds', () { final r = Random(20261005); final valid = encodeSecurityWith( signature: encodeAuthorSignature(1, Uint8List(32), Uint8List(64)), seal: encodeSeal(1, [1]), ); final context = SecurityContext( controlCommit: Uint8List(32), headDigest: Uint8List(32), ); for (var i = 0; i < 300; i++) { final b = Uint8List.fromList(valid); for (var j = 0; j < 1 + r.nextInt(3); j++) { b[r.nextInt(b.length)] = r.nextInt(256); } final cut = Uint8List.sublistView(b, 0, r.nextInt(b.length + 1)); for (final area in [ b, cut, Uint8List.fromList(List.generate(r.nextInt(40), (_) => r.nextInt(256))), ]) { for (final ctx in [null, context]) { final v = evaluateSecurity(area, context: ctx); expect(v.evaluated, isTrue); expect(v.lines, isNotEmpty); } } } }); test('the context of the opening: control_commit, head_digest, the round ' 'time and the keys', () { final built = ((_vectors['commitments']! as Json)['control_commit']! as List) .cast() .firstWhere((c) => c['decode_format'] == 3 && c['format'] == 3); final control = decodeControl( fromHex(str(built, 'control')), CapsuleFormat.format3, ); final head = Uint8List.fromList(utf8.encode('a head')); final keys = {'dkauthor1x': 'Ana'}; final input = SecurityInput( security: encodeSecurity(), head: head, control: control, format: CapsuleFormat.format3, roundTime: parseRfc3339('2030-01-01T00:00:00Z'), authorKeys: keys, ); final c = securityContext(input); expect(toHex(c.controlCommit), built['commit']); expect(c.headDigest, headDigest(head)); expect([c.roundTime, c.authorKeys], [input.roundTime, keys]); expect( [ evaluateSecurityInput(input).signature, evaluateSecurityInput(input).seal, ], [Verdict.noSignature, Verdict.noSeal], ); // A control that CONTROL_SIG cannot hold leaves control_commit at zero, // as in Go. final odd = securityContext( SecurityInput( security: input.security, head: head, control: control, format: CapsuleFormat.format1, roundTime: input.roundTime, authorKeys: const {}, ), ); expect(odd.controlCommit, Uint8List(32)); }); test('the texts of the verdicts and of the results', () { // S0 shows nothing, and the verdicts that name a key, a holder or a // time have their text in the lines. for (final v in Verdict.values) { expect(v.text.isEmpty, {'S0', 'F3', 'F4', 'F6', 'S4'}.contains(v.code)); } for (final r in SignerResult.values) { expect(SignerResult.fromCode(r.code), r); } expect(SignerResult.fromCode('valida'), isNull); expect(Verdicts.notEvaluated.lines, isEmpty); expect(Verdicts.failed(StateError('x')).lines, isEmpty); }); }