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.
247 lines
8.9 KiB
247 lines
8.9 KiB
|
3 days ago
|
/// Releases (spec §45 to §52): their local verification, provider.Verify of
|
||
|
|
/// the Go reference (spec §17, §51, §63 step 10), and the sources that
|
||
|
|
/// deliver them (provider.ReleaseSource), as release.ts of datekeys-ts.
|
||
|
|
///
|
||
|
|
/// [verifyRelease] checks, in the order of the reference and with its
|
||
|
|
/// texts: the round against the range of the profile (ERR_DATEKEY_INVALID);
|
||
|
|
/// the round of the release against the expected one (ERR_ROUND_MISMATCH),
|
||
|
|
/// before the signature; the length of the signature; the pinned public key
|
||
|
|
/// (ERR_UNKNOWN_PROFILE if it is not the canonical encoding of a point); and
|
||
|
|
/// the signature (ERR_RELEASE_INVALID): the canonical encoding of a point of
|
||
|
|
/// G1 other than the point at infinity (spec §12.2) that verifies as the BLS
|
||
|
|
/// signature of the round under the pinned key.
|
||
|
|
///
|
||
|
|
/// Only the scheme of Quicknet, bls-unchained-g1-rfc9380, is verified, as in
|
||
|
|
/// datekeys-ts: a profile of another scheme fails with ERR_UNKNOWN_PROFILE
|
||
|
|
/// after the round checks, where the reference would verify it.
|
||
|
|
///
|
||
|
|
/// The HTTP client is not part of the library: the application supplies a
|
||
|
|
/// [ReleaseSource], as OpenOptions.Source in Go.
|
||
|
|
///
|
||
|
|
/// Not constant time (see bls12381_fp.dart): everything it handles is
|
||
|
|
/// public, the signature of a round once drand publishes it.
|
||
|
|
library;
|
||
|
|
|
||
|
|
import 'dart:typed_data';
|
||
|
|
|
||
|
|
import 'bls12381_curve.dart';
|
||
|
|
import 'bls12381_hash.dart';
|
||
|
|
import 'bls12381_pairing.dart';
|
||
|
|
import 'bytes.dart';
|
||
|
|
import 'errors.dart';
|
||
|
|
import 'ibe.dart';
|
||
|
|
|
||
|
|
/// The drand scheme of Quicknet, the only one this library verifies.
|
||
|
|
const quicknetScheme = 'bls-unchained-g1-rfc9380';
|
||
|
|
|
||
|
|
/// What the verification of a release and the tlock stanza read of a
|
||
|
|
/// locally pinned Provider Profile (spec §10, §13): its id, its drand
|
||
|
|
/// scheme, its public key, its chain hash and the last round of its range
|
||
|
|
/// (spec §15). The Provider Profile of the library implements it.
|
||
|
|
abstract interface class PinnedProfile {
|
||
|
|
/// profile_id, such as `datekeys:quicknet:v1`.
|
||
|
|
String get id;
|
||
|
|
|
||
|
|
/// The drand scheme, such as `bls-unchained-g1-rfc9380`.
|
||
|
|
String get scheme;
|
||
|
|
|
||
|
|
/// The compressed public key of the drand network.
|
||
|
|
Uint8List get publicKey;
|
||
|
|
|
||
|
|
/// The 32 bytes of the chain hash.
|
||
|
|
Uint8List get chainHash;
|
||
|
|
|
||
|
|
/// The last round whose round time is not after 9999-12-31T23:59:59Z
|
||
|
|
/// (spec §15), 0 when there is none.
|
||
|
|
int get maxRound;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The material that satisfies a round: for drand, the BLS signature of the
|
||
|
|
/// round.
|
||
|
|
final class Release {
|
||
|
|
/// The release of [round] with [signature], which it copies.
|
||
|
|
Release(this.round, List<int> signature)
|
||
|
|
: signature = Uint8List.fromList(signature);
|
||
|
|
|
||
|
|
/// The round.
|
||
|
|
final int round;
|
||
|
|
|
||
|
|
/// The compressed signature of the round.
|
||
|
|
final Uint8List signature;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Verifies a release of [round] locally against the pinned profile [p], as
|
||
|
|
/// provider.Verify does. Throws a [DateKeysException]; returns normally when
|
||
|
|
/// the release is valid.
|
||
|
|
void verifyRelease(PinnedProfile p, int round, Release r) {
|
||
|
|
verifiedSignature(p, round, r);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// [verifyRelease], returning the point of the verified signature, for the
|
||
|
|
/// decryption of the tlock stanza that follows.
|
||
|
|
G1Point verifiedSignature(PinnedProfile p, int round, Release r) {
|
||
|
|
if (round < 1 || round > p.maxRound) {
|
||
|
|
throw DateKeysException(
|
||
|
|
ErrorCode.dateKeyInvalid,
|
||
|
|
'provider: round $round outside the range of ${p.id}',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
if (r.round != round) {
|
||
|
|
throw DateKeysException(
|
||
|
|
ErrorCode.roundMismatch,
|
||
|
|
'provider: release for round ${r.round}, expected $round',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
if (p.scheme != quicknetScheme) {
|
||
|
|
throw DateKeysException(
|
||
|
|
ErrorCode.unknownProfile,
|
||
|
|
'provider: profile ${p.id} uses scheme ${p.scheme}; only '
|
||
|
|
'$quicknetScheme releases are verified here',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
if (r.signature.length != signatureLength) {
|
||
|
|
throw DateKeysException(
|
||
|
|
ErrorCode.releaseInvalid,
|
||
|
|
'provider: signature is ${r.signature.length} bytes, $quicknetScheme '
|
||
|
|
'uses $signatureLength',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
final key = pinnedKey(p.publicKey);
|
||
|
|
if (key == null) {
|
||
|
|
throw DateKeysException(
|
||
|
|
ErrorCode.unknownProfile,
|
||
|
|
'provider: pinned public key of ${p.id} is not the canonical encoding '
|
||
|
|
'of a point of the key group',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
final last = _lastVerified;
|
||
|
|
if (last != null &&
|
||
|
|
last.round == r.round &&
|
||
|
|
equalBytes(last.signature, r.signature) &&
|
||
|
|
equalBytes(last.publicKey, p.publicKey)) {
|
||
|
|
return last.point;
|
||
|
|
}
|
||
|
|
// kyber decodes the point at infinity as a key, and with it, the point at
|
||
|
|
// infinity as a signature verifies (both pairs drop out of kilic's
|
||
|
|
// check). Spec §63 step 10 rejects such a signature, and so does this
|
||
|
|
// code, with any key: a profile with that key is never pinned (spec
|
||
|
|
// §12.1).
|
||
|
|
final signature = G1Point.decode(r.signature);
|
||
|
|
if (key.isInfinity ||
|
||
|
|
signature == null ||
|
||
|
|
signature.isInfinity ||
|
||
|
|
!_verifies(signature, r.round, key)) {
|
||
|
|
throw DateKeysException(
|
||
|
|
ErrorCode.releaseInvalid,
|
||
|
|
'provider: the signature is not a canonical point encoding, or does not '
|
||
|
|
'verify as the BLS signature of round ${r.round} under ${p.id}',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
_lastVerified = (
|
||
|
|
publicKey: Uint8List.fromList(p.publicKey),
|
||
|
|
round: r.round,
|
||
|
|
signature: Uint8List.fromList(r.signature),
|
||
|
|
point: signature,
|
||
|
|
);
|
||
|
|
return signature;
|
||
|
|
}
|
||
|
|
|
||
|
|
// The last release that verified, under its key: the opening verifies the
|
||
|
|
// same release at step 10 and again in the tlock identity of step 11, as Go
|
||
|
|
// does, and the second time costs no pairing. Only the BLS check is skipped:
|
||
|
|
// the checks before it run every time.
|
||
|
|
({Uint8List publicKey, int round, Uint8List signature, G1Point point})?
|
||
|
|
_lastVerified;
|
||
|
|
|
||
|
|
// BLS on G1, as Verify of kyber's sign/bls with NewSchemeOnG1: e(H(m), key)
|
||
|
|
// = e(signature, G2), with m = SHA-256(uint64be(round)), the message drand
|
||
|
|
// signs for an unchained scheme, hashed to G1 with the DST of RFC 9380.
|
||
|
|
bool _verifies(G1Point signature, int round, G2Point key) => pairingCheck([
|
||
|
|
(hashToG1(roundIdentity(round), quicknetDst), key),
|
||
|
|
(-signature, G2Point.generator),
|
||
|
|
]);
|
||
|
|
|
||
|
|
// The last public key decoded: the pinned key of Quicknet, every time.
|
||
|
|
Uint8List? _lastKeyBytes;
|
||
|
|
G2Point? _lastKey;
|
||
|
|
|
||
|
|
/// The point of the pinned public key [bytes], or null when it is not the
|
||
|
|
/// canonical encoding of a point of G2 (the point at infinity is returned
|
||
|
|
/// as such). The last key decoded is kept, so that the pinned key of a
|
||
|
|
/// profile is decoded once.
|
||
|
|
G2Point? pinnedKey(List<int> bytes) {
|
||
|
|
final last = _lastKeyBytes;
|
||
|
|
if (last != null && equalBytes(last, bytes)) return _lastKey;
|
||
|
|
final key = G2Point.decode(bytes);
|
||
|
|
_lastKeyBytes = Uint8List.fromList(bytes);
|
||
|
|
_lastKey = key;
|
||
|
|
return key;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// A source of releases (spec §45 to §50), as provider.ReleaseSource:
|
||
|
|
/// [fetch] returns the release of a round of the profile, or throws.
|
||
|
|
///
|
||
|
|
/// A source that fetches releases over a network (a relay, the Release API
|
||
|
|
/// or a cache) verifies each response with [verifyRelease] and discards the
|
||
|
|
/// one that fails; when none passes, it throws ERR_RELEASE_UNAVAILABLE,
|
||
|
|
/// which the opening reports at step 9. Only a release that the caller
|
||
|
|
/// supplies directly gets the codes of step 10 (spec §63 steps 9 and 10).
|
||
|
|
/// Whatever a source throws, step 9 reports it with ERR_RELEASE_UNAVAILABLE
|
||
|
|
/// and no other code, keeping only its text (spec §76, correction 6): see
|
||
|
|
/// [fetchRelease].
|
||
|
|
abstract interface class ReleaseSource {
|
||
|
|
/// The release of [round] of the profile [p].
|
||
|
|
Future<Release> fetch(PinnedProfile p, int round);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The source of a release that the caller supplies directly, as the
|
||
|
|
/// official vectors do: it hands [release] over for any round, unverified,
|
||
|
|
/// so that step 10 checks it; without a release, it has none to give
|
||
|
|
/// (ERR_RELEASE_UNAVAILABLE).
|
||
|
|
ReleaseSource suppliedRelease([Release? release]) => _Supplied(release);
|
||
|
|
|
||
|
|
final class _Supplied implements ReleaseSource {
|
||
|
|
_Supplied(this.release);
|
||
|
|
|
||
|
|
final Release? release;
|
||
|
|
|
||
|
|
@override
|
||
|
|
Future<Release> fetch(PinnedProfile p, int round) async {
|
||
|
|
final r = release;
|
||
|
|
if (r == null) {
|
||
|
|
throw DateKeysException(
|
||
|
|
ErrorCode.releaseUnavailable,
|
||
|
|
'release: no release supplied for round $round',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return r;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The release of [round] from [source], as step 9 of the opening obtains
|
||
|
|
/// it (spec §63): whatever the source throws becomes ERR_RELEASE_UNAVAILABLE,
|
||
|
|
/// the one code of that step. A [DateKeysException] of that code is kept as
|
||
|
|
/// it is; anything else, of another code or none, is kept as text only:
|
||
|
|
/// `capsule: release source: <text>: ERR_RELEASE_UNAVAILABLE`, as
|
||
|
|
/// sourceFailure of capsule.Open in Go.
|
||
|
|
Future<Release> fetchRelease(
|
||
|
|
ReleaseSource source,
|
||
|
|
PinnedProfile p,
|
||
|
|
int round,
|
||
|
|
) async {
|
||
|
|
try {
|
||
|
|
return await source.fetch(p, round);
|
||
|
|
} on Object catch (e, stack) {
|
||
|
|
if (e is DateKeysException && e.code == ErrorCode.releaseUnavailable) {
|
||
|
|
rethrow;
|
||
|
|
}
|
||
|
|
Error.throwWithStackTrace(
|
||
|
|
DateKeysException(
|
||
|
|
ErrorCode.releaseUnavailable,
|
||
|
|
'capsule: release source: $e',
|
||
|
|
),
|
||
|
|
stack,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|