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/release_vectors_test.dart

301 lines
9.7 KiB

// 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<String, Object?>;
Json readJson(String path) => jsonDecode(File(path).readAsStringSync()) as Json;
final Json g = readJson('test/vectors/release_vectors.json');
List<Json> section(Json file, String name) =>
(file[name]! as List).cast<Json>();
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',
// Spec v0.15: two release objects of another chain.
'release object of another chain',
'release object of another chain and another round, with a clock '
'behind',
]..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<Object?>,
);
final rel = c['release']! as Json;
final e = dateKeysError(
() => verifyRelease(
quicknet(),
dateKeyRound(bytes),
Release(
rel['round']! as int,
fromHex(s(rel, 'signature')),
chainHash: rel['chain_hash'] == null
? null
: fromHex(s(rel, 'chain_hash')),
),
),
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<DateKeysException>().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<DateKeysException> 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<Release> fetch(PinnedProfile p, int round) async => throw error;
}

Powered by TurnKey Linux.