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.
446 lines
15 KiB
446 lines
15 KiB
// Helpers of the tests of the security area against the vectors of Go: the
|
|
// verdicts of an area as the vectors record them, compared part by part,
|
|
// the signature of alg 2 and the seal of seal_type 2 with the reader of CMS
|
|
// of securitycms.dart, as evaluateSecurity does by default. They read no
|
|
// file, so that the tests that run on Node.js can use them.
|
|
library;
|
|
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:datekeys/datekeys.dart';
|
|
import 'package:datekeys/src/bech32.dart' show bech32Encode;
|
|
import 'package:datekeys/src/bytes.dart' show utf8Bytes;
|
|
import 'package:datekeys/src/sha256.dart' show sha256;
|
|
|
|
import 'open_vectors_support.dart';
|
|
import 'tlock_support.dart' show applyEdits;
|
|
|
|
/// Which parts of [security] the reader of CMS evaluates in a context: a
|
|
/// signature of alg 2, and a seal of seal_type 2.
|
|
({bool signature, bool seal}) cmsParts(List<int> security) {
|
|
final w = decodeSecurity(security);
|
|
if (w == null) return (signature: false, seal: false);
|
|
final sig = w.signature;
|
|
final seal = w.seal;
|
|
return (
|
|
signature: sig != null && decodeAuthorSignature(sig)?.alg == algCms,
|
|
seal: seal != null && decodeSeal(seal)?.sealType == sealTypeRfc3161,
|
|
);
|
|
}
|
|
|
|
/// Whether [t] is Go's zero time, which Go writes as no time.
|
|
bool isGoZero(Instant t) => t.seconds == -62135596800 && t.nanos == 0;
|
|
|
|
/// [t] as the vectors of Go write a time: RFC 3339 with its fraction, and
|
|
/// null for none or for Go's zero time.
|
|
String? timeText(Instant? t) =>
|
|
t == null || isGoZero(t) ? null : formatRfc3339Nano(t);
|
|
|
|
/// The differences between [v] and [want], the verdicts of Go as the vectors
|
|
/// record them: signature, seal, lines, and with [keys] author_key,
|
|
/// author_label and sealed_at.
|
|
List<String> verdictDifferences(Json want, Verdicts v, {bool keys = true}) {
|
|
final out = <String>[];
|
|
void same(String what, Object? got, Object? expected) {
|
|
final g = canonical(got);
|
|
final w = canonical(expected);
|
|
if (g != w) out.add('$what: got $g, want $w');
|
|
}
|
|
|
|
if (v.error != null) out.add('the evaluator failed: ${v.error}');
|
|
same('signature', v.signature?.code, want['signature']);
|
|
same('seal', v.seal?.code, want['seal']);
|
|
same('lines', v.lines, want['lines']);
|
|
if (keys) {
|
|
same('sealed_at', timeText(v.sealedAt), want['sealed_at']);
|
|
final key = v.authorKey;
|
|
same(
|
|
'author_key',
|
|
key == null ? null : bech32Encode('dkauthor', key),
|
|
want['author_key'],
|
|
);
|
|
same('author_label', v.authorLabel, want['author_label']);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/// A signer as the lines section of security_vectors.json and the cases of
|
|
/// securitycms_vectors.json write it, as signerJSON of
|
|
/// tool/security_go_vectors.go.
|
|
Json signerJson(SignerLine s) => {
|
|
'holder': s.holder,
|
|
'issuer': s.issuer,
|
|
'result': s.result.code,
|
|
'seal_holder': s.sealHolder,
|
|
'before': s.before,
|
|
if (timeText(s.sealTime) != null) 'seal_time': timeText(s.sealTime),
|
|
};
|
|
|
|
/// The detail of [v] as tool/security_go_vectors.go writes Go's, null
|
|
/// without one.
|
|
Json? detailJson(Verdicts v) {
|
|
final d = v.detail;
|
|
if (d == null) return null;
|
|
return {
|
|
'signers': [for (final s in d.signers) signerJson(s)],
|
|
'foreign': [for (final s in d.foreign) signerJson(s)],
|
|
'seal_holder': d.sealHolder,
|
|
if (timeText(d.sealTime) != null) 'seal_time': timeText(d.sealTime),
|
|
};
|
|
}
|
|
|
|
/// The bytes of [hex] or null.
|
|
Uint8List? hexOrNull(Object? hex) =>
|
|
hex == null ? null : fromHex(hex as String);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The sections of test/vectors/security_vectors.json
|
|
|
|
/// The contexts of the vectors, by index.
|
|
List<SecurityContext> contextsOf(Json f) => [
|
|
for (final c in (f['contexts']! as List).cast<Json>())
|
|
SecurityContext(
|
|
controlCommit: fromHex(str(c, 'control_commit')),
|
|
headDigest: fromHex(str(c, 'head_digest')),
|
|
roundTime: c['round_time'] == null
|
|
? null
|
|
: parseRfc3339(str(c, 'round_time')),
|
|
authorKeys:
|
|
(c['author_keys'] as Map?)?.cast<String, String>() ?? const {},
|
|
),
|
|
];
|
|
|
|
/// The bytes of [parts], [hex, repeat] runs.
|
|
Uint8List fromParts(List<Object?> parts) => concatBytes([
|
|
for (final p in parts.cast<List<Object?>>())
|
|
for (var i = 0; i < (p[1]! as int); i++) fromHex(p[0]! as String),
|
|
]);
|
|
|
|
/// The bytes of the field [name] of [c], given as hex or as name_parts.
|
|
Uint8List? blobOf(Json c, String name) {
|
|
final parts = c['${name}_parts'] as List?;
|
|
if (parts != null) return fromParts(parts.cast<Object?>());
|
|
return hexOrNull(c[name]);
|
|
}
|
|
|
|
/// SECURITY_CBOR of an evaluation: its hex or parts, or a base of [bases]
|
|
/// with its edits, of the area or of the content of its key 2 or 3, which
|
|
/// is then written again and checked against the first 8 bytes of the
|
|
/// SHA-256 of Go's.
|
|
Uint8List securityOf(Json c, List<Uint8List> bases) {
|
|
final hex = c['hex'] as String?;
|
|
if (hex != null) return fromHex(hex);
|
|
final parts = c['parts'] as List?;
|
|
if (parts != null) return fromParts(parts.cast<Object?>());
|
|
final base = bases[c['base']! as int];
|
|
final edits = (c['edits']! as List).cast<Object?>();
|
|
final key = c['key'] as int?;
|
|
if (key == null) return applyEdits(base, edits);
|
|
final w = decodeSecurity(base)!;
|
|
final out = encodeSecurityWith(
|
|
signature: key == 2 ? applyEdits(w.signature!, edits) : w.signature,
|
|
seal: key == 3 ? applyEdits(w.seal!, edits) : w.seal,
|
|
);
|
|
if (toHex(sha256(out).sublist(0, 8)) != c['sha256']) {
|
|
throw StateError('the area of ${canonical(c)} is not the one of Go');
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/// The differences between the evaluation of the case [c] and Go's: its
|
|
/// verdicts, lines and detail, the parts of CMS, and alg and seal_type as
|
|
/// read.
|
|
List<String> evaluateDifferences(
|
|
Json c,
|
|
Uint8List security,
|
|
List<SecurityContext> contexts,
|
|
List<String> texts,
|
|
) {
|
|
final out = <String>[];
|
|
final at = c['context'] as int?;
|
|
final context = at == null ? null : contexts[at];
|
|
final v = evaluateSecurity(security, context: context);
|
|
final goCms = ((c['cms'] as List?) ?? const []).cast<String>();
|
|
// The parts that the reader of CMS evaluates, as this library reads the
|
|
// area: those of Go, in a context.
|
|
final mine = cmsParts(security);
|
|
final cms = (
|
|
signature: goCms.contains('signature'),
|
|
seal: goCms.contains('seal'),
|
|
);
|
|
if (context != null && mine != cms || context == null && goCms.isNotEmpty) {
|
|
out.add('cms: $mine, Go $goCms');
|
|
}
|
|
final w = decodeSecurity(security);
|
|
final sig = w?.signature;
|
|
final seal = w?.seal;
|
|
final alg = sig == null ? null : decodeAuthorSignature(sig)?.alg;
|
|
final sealType = seal == null ? null : decodeSeal(seal)?.sealType;
|
|
if (alg != c['alg']) out.add('alg $alg, Go ${c['alg']}');
|
|
if (sealType != c['seal_type']) {
|
|
out.add('seal_type $sealType, Go ${c['seal_type']}');
|
|
}
|
|
if ((w == null) != (c['signature'] == 'X')) {
|
|
out.add('decodeSecurity gives ${w == null ? 'X' : 'an area'}');
|
|
}
|
|
final want = {
|
|
...c,
|
|
'lines': [for (final i in (c['lines']! as List).cast<int>()) texts[i]],
|
|
};
|
|
out.addAll(verdictDifferences(want, v));
|
|
// Go's detail, the signers of alg 2 and the authority of a valid seal,
|
|
// which the generator writes whenever Go has one.
|
|
if (canonical(detailJson(v)) != canonical(c['detail'])) {
|
|
out.add('detail ${canonical(detailJson(v))}');
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/// The verdicts of a case of lines, as Go built them.
|
|
Verdicts verdictsOf(Json c) {
|
|
final d = c['detail'] as Json?;
|
|
SignerLine signer(Json s) => SignerLine(
|
|
holder: str(s, 'holder'),
|
|
issuer: str(s, 'issuer'),
|
|
result: SignerResult.fromCode(str(s, 'result'))!,
|
|
sealHolder: str(s, 'seal_holder'),
|
|
sealTime: s['seal_time'] == null ? null : parseRfc3339(str(s, 'seal_time')),
|
|
before: s['before']! as bool,
|
|
);
|
|
return Verdicts(
|
|
signature: Verdict.fromCode(str(c, 'signature')),
|
|
seal: Verdict.fromCode(str(c, 'seal')),
|
|
authorKey: fromHex(str(c, 'author_key')),
|
|
authorLabel: str(c, 'author_label'),
|
|
detail: d == null
|
|
? null
|
|
: Detail(
|
|
signers: [
|
|
for (final s in (d['signers']! as List).cast<Json>()) signer(s),
|
|
],
|
|
foreign: [
|
|
for (final s in (d['foreign']! as List).cast<Json>()) signer(s),
|
|
],
|
|
sealHolder: str(d, 'seal_holder'),
|
|
sealTime: d['seal_time'] == null
|
|
? null
|
|
: parseRfc3339(str(d, 'seal_time')),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// The differences of the lines and the earliest seal of a case of lines.
|
|
List<String> lineDifferences(Json c) {
|
|
final v = verdictsOf(c);
|
|
final out = <String>[];
|
|
if (canonical(v.lines) != canonical(c['lines'])) {
|
|
out.add('lines ${canonical(v.lines)}');
|
|
}
|
|
final at = v.sealedAt;
|
|
final got = at == null ? null : formatRfc3339Nano(at);
|
|
if (got != c['sealed_at']) out.add('sealed_at $got');
|
|
return out;
|
|
}
|
|
|
|
/// The name of a case of holder: its text, or its UTF-16 code units.
|
|
String nameOf(Json c) => c['units'] == null
|
|
? str(c, 'name')
|
|
: String.fromCharCodes((c['units']! as List).cast<int>());
|
|
|
|
/// The differences of the commitments of the vectors.
|
|
List<String> commitmentDifferences(Json f) {
|
|
final out = <String>[];
|
|
void same(String what, Object? got, Object? want) {
|
|
if (got != want) out.add('$what: got $got, want $want');
|
|
}
|
|
|
|
List<Json> section(String name) => (f[name]! as List).cast<Json>();
|
|
for (final c in section('payload_commit')) {
|
|
same(
|
|
'payload_commit',
|
|
toHex(payloadCommit(fromHex(str(c, 'identity')))),
|
|
c['commit'],
|
|
);
|
|
}
|
|
for (final c in section('control_commit')) {
|
|
final decoded = decodeControl(
|
|
fromHex(str(c, 'control')),
|
|
CapsuleFormat.fromVersion(c['decode_format']! as int)!,
|
|
);
|
|
final format = CapsuleFormat.fromVersion(c['format']! as int)!;
|
|
String got;
|
|
try {
|
|
got = toHex(controlCommit(decoded, format));
|
|
} on ArgumentError catch (e) {
|
|
got = 'error: ${e.message}';
|
|
} on DateKeysException catch (e) {
|
|
got = 'error: ${e.message}';
|
|
}
|
|
same(
|
|
'control_commit of ${c['name']} in format ${format.version}',
|
|
got,
|
|
c['commit'] ?? 'error: ${c['error']}',
|
|
);
|
|
}
|
|
for (final c in section('head_digest')) {
|
|
same(
|
|
'head_digest',
|
|
toHex(headDigest(fromHex(str(c, 'head')))),
|
|
c['digest'],
|
|
);
|
|
}
|
|
for (final c in section('signers_digest')) {
|
|
same(
|
|
'signers_digest',
|
|
toHex(signersDigest(c['alg']! as int, hexOrNull(c['signers']))),
|
|
c['digest'],
|
|
);
|
|
}
|
|
for (final c in section('author_message')) {
|
|
final m = authorMessage(
|
|
fromHex(str(c, 'control_commit')),
|
|
fromHex(str(c, 'head_digest')),
|
|
fromHex(str(c, 'signers_digest')),
|
|
);
|
|
same('author_message', String.fromCharCodes(m), c['message']);
|
|
same('author_code', authorCode(m), c['code']);
|
|
}
|
|
for (final c in section('author_code')) {
|
|
final code = authorCode(fromHex(str(c, 'message')));
|
|
same('author_code of ${c['message']}', code, c['code']);
|
|
// When Go's code is UTF-8, these are its bytes.
|
|
final bytes = fromHex(str(c, 'code_hex'));
|
|
if (decodeUtf8(bytes) != null) {
|
|
same('the bytes of author_code', toHex(utf8Bytes(code)), c['code_hex']);
|
|
}
|
|
}
|
|
for (final c in section('sig_part')) {
|
|
same('sig_part', toHex(sigPart(hexOrNull(c['signature']))), c['sig_part']);
|
|
}
|
|
for (final c in section('seal_subject')) {
|
|
same(
|
|
'seal_subject',
|
|
toHex(
|
|
sealSubject(
|
|
fromHex(str(c, 'control_commit')),
|
|
fromHex(str(c, 'head_digest')),
|
|
fromHex(str(c, 'sig_part')),
|
|
),
|
|
),
|
|
c['seal_subject'],
|
|
);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/// The differences of the encoders.
|
|
List<String> encodeDifferences(List<Json> cases) {
|
|
final out = <String>[];
|
|
for (final c in cases) {
|
|
final Uint8List got;
|
|
switch (c['what']) {
|
|
case 'security':
|
|
got = encodeSecurity();
|
|
case 'security_with':
|
|
got = encodeSecurityWith(
|
|
signature: blobOf(c, 'signature'),
|
|
seal: blobOf(c, 'seal'),
|
|
);
|
|
case 'author_signature':
|
|
got = encodeAuthorSignature(
|
|
c['alg']! as int,
|
|
fromHex(str(c, 'key')),
|
|
fromHex(str(c, 'value')),
|
|
);
|
|
default:
|
|
got = encodeSeal(c['seal_type']! as int, fromHex(str(c, 'token')));
|
|
}
|
|
if (!equalBytes(got, blobOf(c, 'hex')!)) out.add('encode ${canonical(c)}');
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// testdata/vectors/security_cms.json
|
|
|
|
/// The results of the signers [lines] as security_cms.json and the records
|
|
/// of the fixtures write them, as vectorSignerResults of Go's
|
|
/// cmsvectors_test.go: t with its fraction, only for a seal that verifies.
|
|
List<Json> signerResults(List<SignerLine> lines) => [
|
|
for (final l in lines)
|
|
{
|
|
'holder': l.holder,
|
|
'issuer': l.issuer,
|
|
'result': l.result.code,
|
|
if (timeText(l.sealTime) != null) 'seal_time': timeText(l.sealTime),
|
|
'before_round_time': l.before,
|
|
},
|
|
];
|
|
|
|
/// What a case of security_cms.json records of the verdicts [v], as
|
|
/// TestCMSVectors of Go reads them: the verdicts, the results of the
|
|
/// required signers and of the foreign ones, each list only when it is not
|
|
/// empty, the authority and t of a valid seal of key 3, and the lines.
|
|
Json cmsVectorOf(Verdicts v) {
|
|
final d = v.detail;
|
|
final when = d == null ? null : timeText(d.sealTime);
|
|
return {
|
|
'signature': v.signature?.code,
|
|
'seal': v.seal?.code,
|
|
if (d != null && d.signers.isNotEmpty) 'signers': signerResults(d.signers),
|
|
if (d != null && d.foreign.isNotEmpty)
|
|
'foreign_signers': signerResults(d.foreign),
|
|
if (when != null) 'seal_holder': d!.sealHolder,
|
|
'seal_time': ?when,
|
|
'lines': v.lines,
|
|
};
|
|
}
|
|
|
|
/// The context of a case of security_cms.json.
|
|
SecurityContext cmsVectorContext(Json c) {
|
|
final ctx = c['context']! as Json;
|
|
return SecurityContext(
|
|
controlCommit: fromHex(str(ctx, 'control_commit')),
|
|
headDigest: fromHex(str(ctx, 'head_digest')),
|
|
roundTime: parseRfc3339(str(ctx, 'round_time')),
|
|
);
|
|
}
|
|
|
|
/// The differences between the case [c] of security_cms.json and its
|
|
/// evaluation in its context, as TestCMSVectors of Go checks them: the
|
|
/// verdicts, the results, the seal and the lines, byte for byte, and no line
|
|
/// with a control or a bidirectional character.
|
|
List<String> cmsVectorDifferences(Json c) {
|
|
final v = evaluateSecurity(
|
|
fromHex(str(c, 'security_cbor')),
|
|
context: cmsVectorContext(c),
|
|
);
|
|
final out = <String>[];
|
|
final want = {
|
|
for (final k in const [
|
|
'signature',
|
|
'seal',
|
|
'signers',
|
|
'foreign_signers',
|
|
'seal_holder',
|
|
'seal_time',
|
|
'lines',
|
|
])
|
|
if (c.containsKey(k)) k: c[k],
|
|
};
|
|
final got = cmsVectorOf(v);
|
|
if (canonical(got) != canonical(want)) out.add('got ${canonical(got)}');
|
|
for (final line in v.lines) {
|
|
for (final r in line.runes) {
|
|
if (r < 0x20 ||
|
|
r >= 0x7f && r <= 0x9f ||
|
|
r >= 0x202a && r <= 0x202e ||
|
|
r >= 0x2066 && r <= 0x2069 ||
|
|
r == 0xfeff) {
|
|
out.add('a line with U+${r.toRadixString(16)}: $line');
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|