/// 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 signature, {List? 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 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); } /// 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 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, ) => _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 _step9(Future 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 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 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 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 _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 = []; 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 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 bytes) : _bytes = Uint8List.fromList(bytes); final Uint8List _bytes; @override Future 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 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 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)); } }