/// 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 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 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 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 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: : ERR_RELEASE_UNAVAILABLE`, as /// sourceFailure of capsule.Open in Go. Future 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, ); } }