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

985 lines
32 KiB

/// Releases (spec §45 to §52): their local verification, provider.Verify of
/// the Go reference (spec §17, §51, §63 step 10), the sources that deliver
/// them (provider.ReleaseSource), as release.ts of datekeys-ts, and, since
/// spec v0.15, the release object (§47.1), drand's JSON as an input of the
/// caller, a release in the caller's hand (provider.Supplier) and the lookup
/// of a local release archive (§50).
///
/// [verifyRelease] checks, in the order of the reference and with its
/// texts: the round against the range of the profile (ERR_DATEKEY_INVALID);
/// the chain hash that the release names, if any, against the pinned profile
/// (ERR_PROFILE_MISMATCH, spec v0.15);
/// 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:convert';
import 'dart:typed_data';
import 'bls12381_curve.dart';
import 'bls12381_hash.dart';
import 'bls12381_pairing.dart';
import 'bytes.dart';
import 'cbor.dart';
import 'errors.dart';
import 'ibe.dart';
import 'schema.dart';
import 'sha256.dart';
import 'source.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, and the
/// chain it names, [chainHash], which it copies too.
Release(this.round, List<int> signature, {List<int>? chainHash})
: signature = Uint8List.fromList(signature),
chainHash = chainHash == null ? null : Uint8List.fromList(chainHash);
/// The round.
final int round;
/// The compressed signature of the round.
final Uint8List signature;
/// The chain the release names, key 2 of a release object (spec v0.15,
/// §47.1), or null when it names none, as the answer of a relay and
/// drand's JSON. [verifyRelease] compares it with the pinned profile.
final Uint8List? chainHash;
}
/// 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}',
);
}
final chain = r.chainHash;
if (chain != null && !equalBytes(chain, p.chainHash)) {
throw DateKeysException(
ErrorCode.profileMismatch,
'provider: release of chain ${toHex(chain)}, the pinned profile '
'${p.id} is chain ${toHex(p.chainHash)}',
);
}
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);
}
/// A source that hands [release] over for any round, unverified, so that
/// step 10 checks it; without a release, it has none to give
/// (ERR_RELEASE_UNAVAILABLE). It is a [ReleaseSource], so the opening does
/// not ask it before the round time; a release in the caller's hand, which
/// is not compared with the clock (spec v0.15, §63 step 9.c), is a
/// [ReleaseSupplier] such as [EncodedRelease], given as OpenOptions.release.
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,
) => _step9(() => source.fetch(p, round));
// Whatever [body] throws, as sourceFailure of capsule.Open: a
// DateKeysException of ERR_RELEASE_UNAVAILABLE as it is, anything else as
// text only.
Future<T> _step9<T>(Future<T> Function() body) async {
try {
return await body();
} on Object catch (e, stack) {
if (e is DateKeysException && e.code == ErrorCode.releaseUnavailable) {
rethrow;
}
Error.throwWithStackTrace(
DateKeysException(
ErrorCode.releaseUnavailable,
'capsule: release source: $e',
),
stack,
);
}
}
// ---------------------------------------------------------------------------
// The release object (spec v0.15, §47.1)
/// The type tag of the release object.
const releaseTypeTag = 'datekeys-release';
/// The schema version of the release object.
const releaseSchemaVersion = 1;
/// The largest release object a reader decodes, in bytes (spec v0.15,
/// §47.1): the object has no frame, so a larger input is rejected before it
/// is decoded, with ERR_NON_CANONICAL_CBOR. No valid encoding comes close.
const maxReleaseObjectSize = 1024;
/// The longest signature of a release object: a compressed point of G2.
/// Quicknet signs with 48 bytes, a point of G1.
const maxReleaseSignatureLength = 96;
/// The largest drand JSON that [parseRelease] reads, in bytes, the bound of
/// a relay response in provider/drand of Go.
const maxReleaseJsonSize = 8 << 10;
// The keys of the release object, all required.
const _releaseKeys = 5;
void _encodeReleaseWire(CborEncoder e, Release r) {
e
..map(_releaseKeys)
..uint(0)
..text(releaseTypeTag)
..uint(1)
..uint(releaseSchemaVersion)
..uint(2)
..bstr(r.chainHash!)
..uint(3)
..uint(r.round)
..uint(4)
..bstr(r.signature);
}
DateKeysException _nonCanonical(String context) =>
DateKeysException(ErrorCode.nonCanonicalCbor, context);
// Reads the map with every CDDL rule of the release object, all of them
// ERR_NON_CANONICAL_CBOR: what each field means against the pinned profile
// and the DateKey is checked by verifyRelease, at step 10.
Release _decodeReleaseWire(CborDecoder d) {
final pairs = d.map(_releaseKeys);
if (pairs != _releaseKeys) {
throw _nonCanonical('$pairs keys, want all $_releaseKeys');
}
late Uint8List chainHash;
late int round;
late Uint8List signature;
for (var want = 0; want < _releaseKeys; want++) {
final k = d.key();
if (k != want) throw _nonCanonical('key $k where key $want was expected');
inKey(k, () {
switch (want) {
case 0:
d.text(releaseTypeTag.length);
case 1:
d.uint(releaseSchemaVersion);
case 2:
chainHash = d.bstr(32, 32);
case 3:
round = d.uint();
if (round == 0) throw _nonCanonical('round 0');
default:
signature = d.bstr(1, maxReleaseSignatureLength);
}
});
}
d.endMap();
return Release(round, signature, chainHash: chainHash);
}
/// The release object of [r], its chain hash, its round and its signature
/// (spec v0.15, §47.1), as EncodeRelease of Go. It does not verify the
/// release: [verifyRelease] does, against the pinned profile. A chain hash
/// other than 32 bytes, a round outside 1..2^53-1 or a signature outside
/// 1..96 bytes is ERR_NON_CANONICAL_CBOR.
Uint8List encodeRelease(Release r) {
final chain = r.chainHash;
if (chain == null || chain.length != 32) {
throw _nonCanonical(
'provider: release object: chain hash of ${chain?.length ?? 0} bytes, '
'want 32',
);
}
if (r.round < 1 || r.round > maxSafeUint) {
throw _nonCanonical(
'provider: release object: round ${r.round} outside 1..$maxSafeUint',
);
}
if (r.signature.isEmpty || r.signature.length > maxReleaseSignatureLength) {
throw _nonCanonical(
'provider: release object: signature of ${r.signature.length} bytes '
'outside 1..$maxReleaseSignatureLength',
);
}
final e = CborEncoder();
_encodeReleaseWire(e, r);
return e.out();
}
/// Decodes a release object (spec v0.15, §47.1) with the layers of spec
/// §69.1 that it has, as DecodeRelease of Go: its size, 1 to
/// [maxReleaseObjectSize] bytes; its type and schema version
/// (ERR_NON_CANONICAL_CBOR, then ERR_UNSUPPORTED_VERSION); its encoding and
/// schema (ERR_NON_CANONICAL_CBOR). The release names its chain, which
/// [verifyRelease] checks against the pinned profile at step 10, before the
/// round and the signature.
Release decodeRelease(List<int> b) {
if (b.isEmpty || b.length > maxReleaseObjectSize) {
throw _nonCanonical(
'provider: release object of ${b.length} bytes, outside '
'1..$maxReleaseObjectSize',
);
}
return withContext('provider: release object', () {
checkSchema(b, releaseTypeTag, releaseSchemaVersion);
late Release r;
unmarshalCbor(
b,
(d) => r = _decodeReleaseWire(d),
(e) => _encodeReleaseWire(e, r),
);
return r;
});
}
/// Reads a release that the caller supplies, as ParseRelease of Go: drand's
/// JSON when its first byte other than a JSON space is `{`, or else a
/// release object, with [decodeRelease].
///
/// drand's JSON is the answer of a relay, `{"round": …, "signature": "…"}`,
/// with an optional `randomness` that must be the SHA-256 of the signature,
/// in hexadecimal; other fields are ignored. It does not name its chain, so
/// the release has no chain hash. Any failure to read it, and an input of
/// more than [maxReleaseJsonSize] bytes, is ERR_RELEASE_INVALID. It is an
/// input, never written.
Release parseRelease(List<int> b) {
for (final c in b) {
if (c == 0x20 || c == 0x09 || c == 0x0d || c == 0x0a) continue;
if (c == 0x7b) return _parseDrandJson(b);
break;
}
return decodeRelease(b);
}
DateKeysException _invalidJson(String context) =>
DateKeysException(ErrorCode.releaseInvalid, 'provider: drand JSON$context');
// The name of a JSON key as encoding/json of Go matches it with the name of
// a field: without case, with the two letters of Unicode that fold to an
// ASCII letter of these names, the long s and the Kelvin sign.
String _foldKey(String k) => k
.replaceAll(String.fromCharCode(0x017f), 's')
.replaceAll(String.fromCharCode(0x212a), 'k')
.toLowerCase();
// Reads the JSON of a drand relay, as parseDrandJSON of Go, with the rules
// of encoding/json for its three fields: keys without case, the last one
// wins, null unsets round and signature and leaves randomness as it is, a
// value of another type is an error, and round is an unsigned integer
// written as such, without a sign, a fraction or an exponent. Its own reader
// ([_GoJson]), because jsonDecode of Dart does not keep how a number is
// written, and on the web reads 1000.0 as the int 1000.
Release _parseDrandJson(List<int> b) {
if (b.length > maxReleaseJsonSize) {
throw _invalidJson(
' of ${b.length} bytes, larger than $maxReleaseJsonSize',
);
}
final malformed = _invalidJson(': malformed, or without round or signature');
final List<(String, _JsonValue)> fields;
try {
fields = _GoJson(b).topObject();
} on FormatException {
throw malformed;
}
String? round;
String? signature;
var randomness = '';
var typeError = false;
for (final (key, value) in fields) {
switch (key) {
case 'round':
if (value.isNull) {
round = null;
} else if (value.number != null &&
RegExp(r'^(0|[1-9][0-9]*)$').hasMatch(value.number!)) {
round = value.number;
} else {
typeError = true;
}
case 'signature':
if (value.isNull) {
signature = null;
} else if (value.string != null) {
signature = value.string;
} else {
typeError = true;
}
case 'randomness':
if (value.string != null) {
randomness = value.string!;
} else if (!value.isNull) {
typeError = true;
}
}
}
if (typeError || round == null || signature == null) throw malformed;
// A round above 2^53-1 does not fit an int on every platform: it is an
// unsigned integer of Go up to 2^64-1, and no DateKey has it (spec §58).
final wide = BigInt.parse(round);
if (wide > maxUint64) throw malformed;
if (wide > BigInt.from(maxSafeUint)) {
throw _invalidJson(
': round $round above $maxSafeUint, the largest round of a DateKey',
);
}
final Uint8List sig;
try {
sig = fromHex(signature);
} on FormatException {
throw _invalidJson(': signature is not hex');
}
if (randomness.isNotEmpty && randomness.toLowerCase() != toHex(sha256(sig))) {
throw _invalidJson(': randomness does not match the signature');
}
return Release(wide.toInt(), sig);
}
// A value of the drand JSON that [_parseDrandJson] reads: null, a string, the
// literal of a number, or anything else (a boolean, an array or an object).
final class _JsonValue {
const _JsonValue({this.isNull = false, this.string, this.number});
final bool isNull;
final String? string;
final String? number;
}
// A reader of JSON with the syntax that encoding/json of Go accepts (RFC
// 8259, with invalid UTF-8 in strings read as U+FFFD): the members of the top
// object whose names fold to round, signature or randomness, in order, each
// one that appears, as Go sets them one after another. It
// throws a FormatException for anything else than one object between JSON
// spaces.
final class _GoJson {
_GoJson(this._b);
final List<int> _b;
int _i = 0;
Never _bad() => throw const FormatException('malformed JSON');
void _space() {
while (_i < _b.length) {
final c = _b[_i];
if (c != 0x20 && c != 0x09 && c != 0x0a && c != 0x0d) return;
_i++;
}
}
int _peek() => _i < _b.length ? _b[_i] : -1;
void _expect(int c) {
if (_peek() != c) _bad();
_i++;
}
List<(String, _JsonValue)> topObject() {
_space();
final out = <(String, _JsonValue)>[];
_object(out);
_space();
if (_i != _b.length) _bad();
return out;
}
// Reads an object; with [out], keeps its members of the three names.
void _object([List<(String, _JsonValue)>? out]) {
_expect(0x7b);
_space();
if (_peek() == 0x7d) {
_i++;
return;
}
for (;;) {
_space();
final name = _string();
_space();
_expect(0x3a);
_space();
final v = _value();
if (out != null) {
final k = _foldKey(name);
if (k == 'round' || k == 'signature' || k == 'randomness') {
// In the order of the input: Go sets each in turn.
out.add((k, v));
}
}
_space();
if (_peek() == 0x2c) {
_i++;
continue;
}
_expect(0x7d);
return;
}
}
void _array() {
_expect(0x5b);
_space();
if (_peek() == 0x5d) {
_i++;
return;
}
for (;;) {
_space();
_value();
_space();
if (_peek() == 0x2c) {
_i++;
continue;
}
_expect(0x5d);
return;
}
}
_JsonValue _value() {
final c = _peek();
switch (c) {
case 0x7b:
_object();
return const _JsonValue();
case 0x5b:
_array();
return const _JsonValue();
case 0x22:
return _JsonValue(string: _string());
case 0x74:
_literal('true');
return const _JsonValue();
case 0x66:
_literal('false');
return const _JsonValue();
case 0x6e:
_literal('null');
return const _JsonValue(isNull: true);
}
if (c == 0x2d || (c >= 0x30 && c <= 0x39)) {
return _JsonValue(number: _number());
}
_bad();
}
void _literal(String word) {
for (final c in word.codeUnits) {
_expect(c);
}
}
bool _digit() {
final c = _peek();
return c >= 0x30 && c <= 0x39;
}
String _number() {
final start = _i;
if (_peek() == 0x2d) _i++;
if (_peek() == 0x30) {
_i++;
} else if (_digit()) {
while (_digit()) {
_i++;
}
} else {
_bad();
}
if (_peek() == 0x2e) {
_i++;
if (!_digit()) _bad();
while (_digit()) {
_i++;
}
}
if (_peek() == 0x65 || _peek() == 0x45) {
_i++;
if (_peek() == 0x2b || _peek() == 0x2d) _i++;
if (!_digit()) _bad();
while (_digit()) {
_i++;
}
}
return String.fromCharCodes(_b.sublist(start, _i));
}
int _hex4() {
if (_i + 4 > _b.length) _bad();
var v = 0;
for (var k = 0; k < 4; k++) {
final c = _b[_i++];
final d = c >= 0x30 && c <= 0x39
? c - 0x30
: (c | 0x20) >= 0x61 && (c | 0x20) <= 0x66
? (c | 0x20) - 0x61 + 10
: -1;
if (d < 0) _bad();
v = v << 4 | d;
}
return v;
}
String _string() {
_expect(0x22);
final out = StringBuffer();
final raw = <int>[];
void flush() {
if (raw.isEmpty) return;
out.write(utf8.decode(raw, allowMalformed: true));
raw.clear();
}
for (;;) {
if (_i >= _b.length) _bad();
final c = _b[_i++];
if (c == 0x22) break;
if (c < 0x20) _bad();
if (c != 0x5c) {
raw.add(c);
continue;
}
flush();
if (_i >= _b.length) _bad();
final e = _b[_i++];
switch (e) {
case 0x22 || 0x5c || 0x2f:
out.writeCharCode(e);
case 0x62:
out.writeCharCode(0x08);
case 0x66:
out.writeCharCode(0x0c);
case 0x6e:
out.writeCharCode(0x0a);
case 0x72:
out.writeCharCode(0x0d);
case 0x74:
out.writeCharCode(0x09);
case 0x75:
var u = _hex4();
if (u >= 0xd800 && u < 0xdc00) {
// A pair of surrogates is one character; a lone one is U+FFFD.
final save = _i;
if (_i + 6 <= _b.length && _b[_i] == 0x5c && _b[_i + 1] == 0x75) {
_i += 2;
final low = _hex4();
if (low >= 0xdc00 && low < 0xe000) {
u = 0x10000 + ((u - 0xd800) << 10) + (low - 0xdc00);
} else {
_i = save;
u = 0xfffd;
}
} else {
u = 0xfffd;
}
} else if (u >= 0xdc00 && u < 0xe000) {
u = 0xfffd;
}
out.writeCharCode(u);
default:
_bad();
}
}
flush();
return out.toString();
}
}
// ---------------------------------------------------------------------------
// A release in the caller's hand (spec v0.15, §49, §63 step 9.c)
/// Hands over a release that the caller has in hand, as provider.Supplier of
/// Go: a release object, drand's JSON that the person saved, or an entry of
/// a local release archive. It makes no network request, so the opening asks
/// it for the release without comparing its clock with the round time: a
/// valid signature proves that the round was published.
///
/// [supply] returns the encoding of the release of [round], as it is: a
/// release object or drand's JSON, which the opening reads with
/// [parseRelease] and verifies at step 10, with the codes of that step.
/// Without a release for the round it throws ERR_RELEASE_UNAVAILABLE, the
/// code of step 9; whatever else it throws is reported at step 9 with that
/// code alone, keeping only its text ([supplyRelease]).
abstract interface class ReleaseSupplier {
/// The encoding of the release of [round] of the profile [p].
Future<Uint8List> supply(PinnedProfile p, int round);
}
/// A release in hand, already read: the bytes of a release object or of
/// drand's JSON, as provider.Encoded of Go. It supplies itself whatever the
/// round; step 10 compares its round with the DateKey.
final class EncodedRelease implements ReleaseSupplier {
/// The release encoded in [bytes], which it copies.
EncodedRelease(List<int> bytes) : _bytes = Uint8List.fromList(bytes);
final Uint8List _bytes;
@override
Future<Uint8List> supply(PinnedProfile p, int round) async =>
Uint8List.fromList(_bytes);
}
/// The encoding of the release of [round] from [supplier], as step 9 of the
/// opening obtains it: whatever the supplier throws becomes
/// ERR_RELEASE_UNAVAILABLE, as [fetchRelease] does for a source.
Future<Uint8List> supplyRelease(
ReleaseSupplier supplier,
PinnedProfile p,
int round,
) => _step9(() => supplier.supply(p, round));
// ---------------------------------------------------------------------------
// The local release archive (spec v0.15, §50, informative)
/// The type tag of the header of a release archive.
const releaseArchiveTypeTag = 'datekeys-release-archive';
/// The schema version of the header of a release archive.
const releaseArchiveSchemaVersion = 1;
// The bound of the header of an archive: its five keys take at most
// 1 + 26 + 2 + 35 + 9 + 9 bytes.
const _maxArchiveHeader = 128;
const _archiveKeys = 5;
// The header of an archive, as archiveHeader of Go.
final class _ArchiveHeader {
Uint8List chainHash = Uint8List(0);
int first = 0;
int count = 0;
void encode(CborEncoder e) {
e
..map(_archiveKeys)
..uint(0)
..text(releaseArchiveTypeTag)
..uint(1)
..uint(releaseArchiveSchemaVersion)
..uint(2)
..bstr(chainHash)
..uint(3)
..uint(first)
..uint(4)
..uint(count);
}
// Throws a DateKeysException of the codec, whose text Go prints with its
// code, or an _ArchiveError, a text of Go without a code.
void decode(CborDecoder d) {
final pairs = d.map(_archiveKeys);
if (pairs != _archiveKeys) {
throw _ArchiveError('$pairs keys, want all $_archiveKeys');
}
for (var want = 0; want < _archiveKeys; want++) {
final k = d.key();
if (k != want) {
throw _ArchiveError('key $k where key $want was expected');
}
inKey(k, () {
switch (want) {
case 0:
d.text(releaseArchiveTypeTag.length);
case 1:
d.uint(releaseArchiveSchemaVersion);
case 2:
chainHash = d.bstr(32, 32);
case 3:
first = d.uint();
default:
count = d.uint();
}
});
}
d.endMap();
}
}
final class _ArchiveError implements Exception {
_ArchiveError(this.text);
final String text;
}
/// A local release archive, the informative format of spec v0.15, §50, as
/// provider.Archive of Go: a header in deterministic CBOR,
/// `{0: "datekeys-release-archive", 1: 1, 2: chain_hash, 3: first round,
/// 4: number of rounds}`, followed by the signatures, so that the one of
/// round r starts at the end of the header plus (r - first)·n, with n the
/// length of a signature of the chain, 48 bytes in Quicknet. A round written
/// as zeros is missing.
///
/// Read locally, it is a release in hand: its entry is the release object of
/// the round, with the chain hash of the header, decoded and verified at
/// step 10 like any other. A round it lacks, a header it cannot read, an
/// archive of another chain or of another length, and a failure to read the
/// [ByteSource], are failures to supply a release: ERR_RELEASE_UNAVAILABLE at
/// step 9, with the texts of Go. The format has no codes of its own.
final class ReleaseArchive implements ReleaseSupplier {
/// The archive that [source] reads by ranges. Nothing is read until
/// [supply].
ReleaseArchive(this.source);
/// The bytes of the archive.
final ByteSource source;
@override
Future<Uint8List> supply(PinnedProfile p, int round) async {
DateKeysException unavailable(String text) => DateKeysException(
ErrorCode.releaseUnavailable,
'provider: release archive: $text',
);
final size = source.length;
final Uint8List head;
try {
head = await readRange(
source,
0,
size < _maxArchiveHeader ? size : _maxArchiveHeader,
);
} on Object catch (e) {
throw unavailable('$e');
}
try {
checkSchema(head, releaseArchiveTypeTag, releaseArchiveSchemaVersion);
} on DateKeysException {
throw unavailable(
'not an archive of version $releaseArchiveSchemaVersion',
);
}
final h = _ArchiveHeader();
try {
h.decode(CborDecoder(head));
} on DateKeysException catch (e) {
throw unavailable('its header does not decode: ${e.message}');
} on _ArchiveError catch (e) {
throw unavailable('its header does not decode: ${e.text}');
}
// The header is the deterministic encoding of what it says: its length
// is that of the encoding, and the signatures follow it.
final e = CborEncoder();
h.encode(e);
final enc = e.out();
final shown = enc.length < head.length ? enc.length : head.length;
if (!equalBytes(enc, Uint8List.sublistView(head, 0, shown))) {
throw unavailable(
'its header is not the deterministic encoding of its value',
);
}
if (!equalBytes(h.chainHash, p.chainHash)) {
throw unavailable(
'archive of chain ${toHex(h.chainHash)}, the pinned profile ${p.id} '
'is chain ${toHex(p.chainHash)}',
);
}
if (h.first == 0 ||
h.count == 0 ||
round < h.first ||
round - h.first >= h.count) {
throw unavailable(
'round $round is not in the archive, which holds ${h.count} rounds '
'from ${h.first}',
);
}
if (p.scheme != quicknetScheme) {
throw unavailable(
'profile ${p.id} uses scheme ${p.scheme}; only $quicknetScheme '
'archives are read here',
);
}
const n = signatureLength;
// count is at most 2^53-1: the product is exact on the VM, and on the
// web it is inexact only above 2^53, which no source measures.
if (size != enc.length + h.count * n) {
throw unavailable(
'$size bytes, its header announces ${h.count} rounds of $n bytes',
);
}
final Uint8List sig;
try {
sig = await readRange(source, enc.length + (round - h.first) * n, n);
} on Object catch (e) {
throw unavailable('$e');
}
if (sig.every((b) => b == 0)) {
throw unavailable('round $round is missing: its entry is zeros');
}
return encodeRelease(Release(round, sig, chainHash: h.chainHash));
}
}

Powered by TurnKey Linux.