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_support.dart

377 lines
13 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 need the reader of CMS
// of stage 5c, which this library does not have yet: such a part must be not
// evaluated, never guessed, and Go's verdict for it one that such a part can
// give. 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/author.dart';
import 'package:datekeys/src/bech32.dart' show bech32Encode;
import 'package:datekeys/src/bytes.dart' show utf8Bytes;
import 'package:datekeys/src/security.dart';
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,
);
}
/// The verdicts that a signature of alg 2 gives in Go, and those of a seal
/// of seal_type 2 in a context (spec §29.7, §29.10, §29.11).
const cmsSignatureVerdicts = {'F1', 'F2', 'F5', 'F6'};
const cmsSealVerdicts = {'S1', 'S2', 'S3', 'S4', 'S5'};
/// The number of lines that the seal of Go's verdicts adds: none for S0, and
/// one for any other, S4 with its authority.
int sealLines(String seal) => seal == 'S0' ? 0 : 1;
/// 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. A part that needs the reader of CMS, as [cms]
/// says, must be not evaluated, and its lines are those of Go without that
/// part; the other part must be Go's.
List<String> verdictDifferences(
Json want,
Verdicts v,
({bool signature, bool seal}) cms, {
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');
}
final signature = want['signature']! as String;
final seal = want['seal']! as String;
final lines = (want['lines']! as List).cast<String>();
if (v.error != null) out.add('the evaluator failed: ${v.error}');
if (cms.signature) {
same('signature of alg 2', v.signature?.code, null);
if (!cmsSignatureVerdicts.contains(signature)) {
out.add('Go gives $signature to a signature of alg 2');
}
} else {
same('signature', v.signature?.code, signature);
}
if (cms.seal) {
same('seal of seal_type 2', v.seal?.code, null);
if (!cmsSealVerdicts.contains(seal)) {
out.add('Go gives $seal to a seal of seal_type 2');
}
} else {
same('seal', v.seal?.code, seal);
}
if (!cms.signature && !cms.seal) {
same('lines', v.lines, lines);
final at = v.sealedAt;
if (keys) {
same(
'sealed_at',
at == null ? null : formatRfc3339Nano(at),
want['sealed_at'],
);
}
} else if (!cms.signature) {
// The seal of seal_type 2 is not evaluated: its line is not there.
same('lines', v.lines, lines.sublist(0, lines.length - sealLines(seal)));
} else if (!cms.seal) {
// The signature of alg 2 is not evaluated: only the line of the seal.
same('lines', v.lines, lines.sublist(lines.length - sealLines(seal)));
} else {
same('lines', v.lines, <String>[]);
}
if (keys) {
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;
}
/// 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 and lines, 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>();
final cms = (
signature: goCms.contains('signature'),
seal: goCms.contains('seal'),
);
// The parts that only the reader of CMS evaluates, as this library reads
// the area: those of Go, in a context.
final mine = cmsParts(security);
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, cms));
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;
}

Powered by TurnKey Linux.