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/lib/src/release.dart

247 lines
8.9 KiB

/// 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,
);
}
}

Powered by TurnKey Linux.