// 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 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 verdictDifferences(Json want, Verdicts v, {bool keys = true}) { final out = []; 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 contextsOf(Json f) => [ for (final c in (f['contexts']! as List).cast()) 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() ?? const {}, ), ]; /// The bytes of [parts], [hex, repeat] runs. Uint8List fromParts(List parts) => concatBytes([ for (final p in parts.cast>()) 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()); 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 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()); final base = bases[c['base']! as int]; final edits = (c['edits']! as List).cast(); 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 evaluateDifferences( Json c, Uint8List security, List contexts, List texts, ) { final out = []; 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(); // 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()) 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()) signer(s), ], foreign: [ for (final s in (d['foreign']! as List).cast()) 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 lineDifferences(Json c) { final v = verdictsOf(c); final out = []; 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()); /// The differences of the commitments of the vectors. List commitmentDifferences(Json f) { final out = []; void same(String what, Object? got, Object? want) { if (got != want) out.add('$what: got $got, want $want'); } List section(String name) => (f[name]! as List).cast(); 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 encodeDifferences(List cases) { final out = []; 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 signerResults(List 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 cmsVectorDifferences(Json c) { final v = evaluateSecurity( fromHex(str(c, 'security_cbor')), context: cmsVectorContext(c), ); final out = []; 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; }