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.
301 lines
9.7 KiB
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;
|
|
}
|