// The verification of releases (lib/src/release.dart) against the Go // reference: the verify section of test/vectors/release_vectors.json, // written by tool/release_go_vectors.go with provider.Verify, and every // release of the mutation corpus that fails at step 10. A port of // release.test.ts of datekeys-ts. @TestOn('vm') library; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:datekeys/datekeys.dart' show fromHex; import 'package:datekeys/src/bls12381_curve.dart'; import 'package:datekeys/src/bls12381_hash.dart'; import 'package:datekeys/src/bls12381_pairing.dart'; import 'package:datekeys/src/errors.dart'; import 'package:datekeys/src/ibe.dart'; import 'package:datekeys/src/release.dart'; import 'package:test/test.dart'; import 'tlock_support.dart'; typedef Json = Map; Json readJson(String path) => jsonDecode(File(path).readAsStringSync()) as Json; final Json g = readJson('test/vectors/release_vectors.json'); List section(Json file, String name) => (file[name]! as List).cast(); String s(Json v, String key) => v[key]! as String; TestProfile profileOf(String name) { final c = section(g, 'profiles').firstWhere((p) => p['name'] == name); return quicknet().copyWith( scheme: s(c, 'scheme'), publicKey: fromHex(s(c, 'public_key')), ); } DateKeysException dateKeysError(void Function() body, String label) { try { body(); } on DateKeysException catch (e) { return e; } fail('$label: no DateKeysException'); } const invalid = 'provider: the signature is not a canonical point encoding, or does not ' 'verify as the BLS signature of round 1000 under datekeys:quicknet:v1: ' 'ERR_RELEASE_INVALID'; String onlyQuicknet(String scheme) => 'provider: profile datekeys:quicknet:v1 uses scheme $scheme; only ' 'bls-unchained-g1-rfc9380 releases are verified here: ERR_UNKNOWN_PROFILE'; void main() { test('the profile of the tests is the pinned Quicknet of testdata/', () { final q = readJson('testdata/vectors/profile_quicknet.json'); expect( [ q['profile_id'], q['scheme'], q['public_key'], q['chain_hash'], q['genesis_time'], q['period_seconds'], ], [ quicknetId, quicknetScheme, quicknetPublicKey, quicknetChainHash, quicknetGenesisTime, quicknetPeriod, ], ); // The last round, as the round resolution of spec §15 gives it. final last = section( readJson('testdata/vectors/quicknet_rounds.json'), 'vectors', ).firstWhere((v) => v['name'] == 'last representable round time'); expect(quicknet().maxRound, last['round']); expect(quicknet().maxRound, g['max_round']); // The published signatures of the tests are those of the fixtures. expect( (readJson('testdata/fixtures/time_only.json')['release']! as Json)['signature'], signature1000, ); expect( (readJson('testdata/fixtures/empty_payload.json')['release']! as Json)['signature'], signature1001, ); }); test( 'verifies as provider.Verify, in its order and with its codes and texts', () { // Where this library departs from Go, on purpose. Only the scheme of // Quicknet is verified, as in datekeys-ts: Go verifies the other // schemes of drand, and names an unknown one in its own text. And the // point at infinity is never a valid signature (spec §63 step 10): // Go accepts it when the key is the point at infinity too, as kilic // drops both pairs of its check, a key that no pinned profile has // (spec §12.1). final departs = { 'another scheme of drand': onlyQuicknet('pedersen-bls-unchained'), 'another scheme of drand on G1': onlyQuicknet('bls-unchained-on-g1'), 'a scheme that is not of drand': onlyQuicknet('datekeys-test'), 'the key and the signature are the point at infinity': invalid, }; var accepted = 0; for (final c in section(g, 'verify')) { final name = s(c, 'name'); final p = profileOf(s(c, 'profile')); final round = c['round']! as int; final r = Release( c['release_round']! as int, fromHex(s(c, 'signature')), ); final want = departs[name]; if (want != null) { expect( dateKeysError(() => verifyRelease(p, round, r), name).message, want, reason: name, ); continue; } if (c['go'] == 'ok') { verifyRelease(p, round, r); accepted++; continue; } final e = dateKeysError(() => verifyRelease(p, round, r), name); expect([e.code.code, e.message], [c['code'], c['text']], reason: name); } expect(accepted, 4); expect( departs.keys, everyElement(isIn(section(g, 'verify').map((c) => c['name']))), ); }, ); test('gives the code of every release of the mutation corpus that fails at ' 'step 10', () { final corpus = section( readJson('testdata/vectors/mutations.json'), 'cases', ).where((c) => c['step'] == 10).toList(); const names = [ 'DateKey A + release of round B', 'release of another round', 'release signature is the point at infinity', 'release signature negated', 'release signature re-encoded with x + p', 'release signature with the infinity flag and a payload', 'negated release signature and U re-encoded with c0 + p', ]; expect( [for (final c in corpus) s(c, 'name')]..sort(), [ ...names, for (final n in names) 'format 2: $n', for (final n in names) 'format 3: $n', ]..sort(), ); for (final c in corpus) { final name = s(c, 'name'); final dkc = c['dkc']! as Json; final base = dkc['base'] as String?; final bytes = applyEdits( base == null ? Uint8List(0) : File('testdata/fixtures/$base').readAsBytesSync(), dkc['edits']! as List, ); final rel = c['release']! as Json; final e = dateKeysError( () => verifyRelease( quicknet(), dateKeyRound(bytes), Release(rel['round']! as int, fromHex(s(rel, 'signature'))), ), name, ); expect(e.code.code, c['error'], reason: name); } }); test( 'hashes the round with the DST of RFC 9380 for G1, not the one of G2 that ' 'bls-unchained-on-g1 uses', () { final key = G2Point.decode(fromHex(quicknetPublicKey))!; final sig = G1Point.decode(fromHex(signature1000))!; bool verifies(String dst) => pairingCheck([ (hashToG1(roundIdentity(1000), dst), key), (-sig, G2Point.generator), ]); expect(quicknetDst, 'BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_'); expect(verifies(quicknetDst), isTrue); expect(verifies('BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_'), isFalse); }, ); test('the supplied release is handed over for any round, unverified, and ' 'there is none without one', () async { final r = Release(1001, fromHex(signature1001)); expect(await suppliedRelease(r).fetch(quicknet(), 1000), same(r)); await expectLater( suppliedRelease().fetch(quicknet(), 1000), throwsA( isA().having( (e) => e.message, 'message', 'release: no release supplied for round 1000: ' 'ERR_RELEASE_UNAVAILABLE', ), ), ); }); test('whatever a source throws is ERR_RELEASE_UNAVAILABLE at step 9, keeping ' 'only its text', () async { final p = quicknet(); final r = Release(1000, fromHex(signature1000)); expect(await fetchRelease(suppliedRelease(r), p, 1000), same(r)); Future failure(Object thrown) async { try { await fetchRelease(_Throwing(thrown), p, 1000); } on DateKeysException catch (e) { return e; } fail('no failure'); } // A release that is not available keeps its text. final none = await failure( DateKeysException(ErrorCode.releaseUnavailable, 'relay: not yet'), ); expect(none.message, 'relay: not yet: ERR_RELEASE_UNAVAILABLE'); // Another code is kept as text only. final other = await failure( DateKeysException(ErrorCode.releaseInvalid, 'relay: bad signature'), ); expect( [other.code, other.message], [ ErrorCode.releaseUnavailable, 'capsule: release source: relay: bad signature: ERR_RELEASE_INVALID: ' 'ERR_RELEASE_UNAVAILABLE', ], ); // And any other failure too. final io = await failure(const FormatException('no JSON')); expect( [io.code, io.message], [ ErrorCode.releaseUnavailable, 'capsule: release source: FormatException: no JSON: ' 'ERR_RELEASE_UNAVAILABLE', ], ); // The verification of step 10 comes after, on what the source gave. expect( dateKeysError( () => verifyRelease(p, 1000, Release(1001, fromHex(signature1001))), 'another round', ).code, ErrorCode.roundMismatch, ); }); } final class _Throwing implements ReleaseSource { _Throwing(this.error); final Object error; @override Future fetch(PinnedProfile p, int round) async => throw error; }