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

385 lines
12 KiB

Stage 5b: the commitments, SECURITY_CBOR and the verdicts of alg 1 author.dart ports what an author signs and a seal seals from signature.go of datekeys-go: payload_commit, control_commit over CONTROL_SIG in each format, head_digest, signers_digest, AUTHOR_MESSAGE with its prefix and its 99 bytes, its code taken byte by byte as Go takes it, SIG_PART and SEAL_SUBJECT. security.dart ports SECURITY_CBOR and EvaluateSecurityIn: the outer map, author-signature and seal with the schema and the limits of Go, their encoders, and an evaluation that never throws. X for an outer map that fails its layer 2 or 3, version 2 among them; F0 to F4 for the signature, with verifyStrict for alg 1 and the key matched against the saved ones by its dkauthor1 string; S0 to S2 for the seal; and a failure inside one part fails that part only, as Go recovers a panic. Without a context it reads as a reader of v0.10. securityContext is newSecurityContext with the head digest, control_commit at zero when CONTROL_SIG cannot be encoded, and holderText the rule of a name of a certificate. The signature of alg 2 and the seal of seal_type 2 belong to the reader of CMS of stage 5c, behind the interface CmsEvaluator: without one their verdict is null, not evaluated, never guessed. verdicts.dart gains the texts of Go at the draft v0.12, Verdicts.lines and sealedAt, Detail, SignerLine and SignerResult, and verdicts that may be evaluated in part. The tests check every case of security_vectors.json, security.json in its context, the commitments, the signature and the seal of each fixture of format 3, and the boundary with a reader of CMS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
// 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<Json> _section(String name) => (_vectors[name]! as List).cast<Json>();
/// 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 = <String>[];
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<String>();
final bases = [
for (final b in (_vectors['bases']! as List).cast<String>()) 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('without one, alg 2 and seal_type 2 are not evaluated', () {
final v = evaluateSecurity(
encodeSecurityWith(signature: alg2, seal: seal2),
context: context,
);
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,
);
expect(
[s.signature, s.seal, s.lines],
[
null,
Verdict.sealUnsupported,
[Verdict.sealUnsupported.text],
],
);
final f = evaluateSecurity(
encodeSecurityWith(
signature: encodeAuthorSignature(9, [], []),
seal: seal2,
),
context: context,
);
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<Json>()
.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);
});
}

Powered by TurnKey Linux.