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

Stage 5c: the verdicts of alg 2 and seal_type 2, and the opening gives them securitycms.dart ports evaluateCMS, signerLine and evaluateSeal of signature2.go of Go at the draft v0.12, on the reader of CMS of stage 5a, with the same order of checks: SIGNERS with its profile and at most 16 entries, then the SignedData; each required signer in the order of SIGNERS and each foreign one in the order of the encoding, valid, invalid, absent, not verifiable, without seal, with an invalid seal or out of validity at the time of its seal; F2, F5 and F6 with their detail; and the seal over SEAL_SUBJECT, S1 to S5 with the authority and t. A round time at Go's zero time is no round time, as IsZero, and Verdicts.sealedAt skips a seal at that time, as SealedAt. cmsReader is the default CmsEvaluator of evaluateSecurity, and so of evaluateSecurityInput and the opening: nothing that Go evaluates is left not evaluated; a caller that passes cms: null still gets the parts without CMS alone. encodeSigners and maxSigners are exported, as EncodeSigners and MaxSigners of Go. The tests compare every part with Go: the 135 cases of security_cms.json with the result of each signer, the 24 of security.json, the 56 signatures of alg 2 and 105 seals of seal_type 2 of security_vectors.json, the 755 cases of securitycms_vectors.json with their detail and earliest seal, the fixtures format3_signed_cms and format3_sealed, and their openings in open_cases.json. On Node.js, a part of the vectors and the two fixtures opened in full. 1572 tests on the VM and 332 on Node.js. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
// 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.