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.
387 lines
12 KiB
387 lines
12 KiB
// 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:datekeys/src/author.dart';
|
|
import 'package:datekeys/src/security.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);
|
|
});
|
|
}
|