// Steps 10 and 11 of spec §63 for Quicknet, value by value, against // testdata/vectors/tlock_steps.json (spec v0.14, the paragraphs "Mensaje de // ronda y hash a G1" and "H3 y H4"): every intermediate value of the file is // computed again with the code of the library, release.dart, ibe.dart, // tlock.dart and bls12381_*.dart, and so are the negative checks that the // generator of Go runs on each vector. tlock_steps_vm_test.dart walks the // file; tlock_steps_test.dart walks its copy compiled to JavaScript. import 'dart:convert'; import 'dart:typed_data'; import 'package:datekeys/src/bls12381_curve.dart'; import 'package:datekeys/src/bls12381_fp.dart' show groupOrder; import 'package:datekeys/src/bls12381_hash.dart'; import 'package:datekeys/src/bls12381_pairing.dart'; import 'package:datekeys/src/bytes.dart'; import 'package:datekeys/src/ibe.dart'; import 'package:datekeys/src/profile.dart'; import 'package:datekeys/src/release.dart'; import 'package:datekeys/src/sha256.dart'; import 'package:datekeys/src/tlock.dart'; import 'package:datekeys/src/version.dart'; import 'package:test/test.dart'; typedef Json = Map; // The DST of RFC 9380 for G2, which bls-unchained-on-g1 uses to hash to G1: // a reader that takes it, or the round itself as the message, fails step 10. const _g2Dst = 'BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_'; String _s(Json v, String key) => v[key]! as String; Uint8List _h(Json v, String key) => fromHex(_s(v, key)); Uint8List _xor(List a, List b) { expect(a, hasLength(b.length)); return Uint8List.fromList([for (var i = 0; i < a.length; i++) a[i] ^ b[i]]); } BigInt _big(List b) => b.isEmpty ? BigInt.zero : BigInt.parse(toHex(b), radix: 16); /// The tests of [f], the parsed tlock_steps.json. void tlockStepsTests(Json f) { final vectors = (f['vectors']! as List).cast(); final tags = f['tags']! as Json; test('names this specification and the pinned profile of Quicknet', () { expect(f['spec'], specVersion); final p = quicknet(); validateProfile(p); expect(f['profile'], p.id); expect(f['scheme'], quicknetScheme); expect(f['scheme'], p.scheme); expect(_s(f, 'chain_hash'), toHex(p.chainHash)); expect(_s(f, 'chain_hash'), toHex(chainInfoHash(p))); expect(_s(f, 'public_key'), toHex(p.publicKey)); expect(f['dst'], quicknetDst); expect(tags, { 'h2': toHex(ascii.encode('IBE-H2')), 'h3': toHex(ascii.encode('IBE-H3')), 'h4': toHex(ascii.encode('IBE-H4')), }); expect(vectors, hasLength(5)); // The last vector is the one whose H3 needs more than one try. expect((vectors.last['h3_tries']! as List).length, greaterThan(2)); }); final key = G2Point.decode(fromHex(_s(f, 'public_key')))!; final chainHash = _s(f, 'chain_hash'); final h3Tag = fromHex(tags['h3']! as String); for (final v in vectors) { final name = _s(v, 'name'); final round = v['round']! as int; final signature = _h(v, 'signature'); test('$name: step 10, M, H(M) and the pairing equation', () { final m = roundIdentity(round); expect(toHex(m), _s(v, 'message')); final round8 = Uint8List(8) ..buffer.asByteData().setUint32(0, round ~/ 0x100000000) ..buffer.asByteData().setUint32(4, round % 0x100000000); expect(m, sha256(round8)); final hm = hashToG1(m, quicknetDst); expect(toHex(hm.toBytes()), _s(v, 'hash_to_g1')); final sig = signaturePoint(signature); // e(H(M), public_key) = e(signature, G2). expect(pairingCheck([(hm, key), (-sig, G2Point.generator)]), isTrue); verifyRelease(quicknet(), round, Release(round, signature)); // The usual misreadings do not verify: the DST of G2, and the round // without SHA-256 as the message. expect( pairingCheck([(hashToG1(m, _g2Dst), key), (-sig, G2Point.generator)]), isFalse, ); expect( pairingCheck([ (hashToG1(round8, quicknetDst), key), (-sig, G2Point.generator), ]), isFalse, ); }); test('$name: step 11, H2, sigma, H4, the file key, H3 and r·G2 = U', () { final body = _h(v, 'body'); final u = _h(v, 'u'); final vv = _h(v, 'v'); final w = _h(v, 'w'); expect(body, hasLength(tlockBodyLength)); expect(body, concatBytes([u, vv, w])); final ct = ciphertextFromBody(body); expect([ct.u, ct.v, ct.w], [u, vv, w]); expect(ciphertextToBody(ct), body); final sig = signaturePoint(signature); final uPoint = G2Point.decode(u)!; expect(uPoint.isInfinity, isFalse); final gt = pairing(sig, uPoint); expect(toHex(gtBytes(gt)), _s(v, 'pairing')); final mask2 = h2(gt, tlockBlockLength); expect(toHex(mask2), _s(v, 'h2')); final sigma = _xor(vv, mask2); expect(toHex(sigma), _s(v, 'sigma')); final mask4 = h4(sigma, tlockBlockLength); expect(toHex(mask4), _s(v, 'h4')); final fileKey = _xor(w, mask4); expect(toHex(fileKey), _s(v, 'file_key')); // H3, try by try. final base = sha256(concatBytes([h3Tag, sigma, fileKey])); expect(toHex(base), _s(v, 'h3_base')); final tries = (v['h3_tries']! as List).cast(); expect(tries, isNotEmpty); Uint8List? cleared; for (var k = 0; k < tries.length; k++) { final t = tries[k]; final i = k + 1; expect(t['i'], i); final d = sha256( concatBytes([ [i & 0xff, i >> 8], base, ]), ); expect(toHex(d), _s(t, 'digest')); final shifted = Uint8List.fromList(d)..[0] = d[0] >> 1; expect(toHex(shifted), _s(t, 'shifted')); final accepted = _big(shifted) < groupOrder; expect(t['accepted'], accepted); expect(accepted, k == tries.length - 1, reason: 'only the last try'); // Clearing the top bit instead accepts another value. final c = Uint8List.fromList(d)..[0] = d[0] & 0x7f; if (cleared == null && _big(c) < groupOrder) cleared = c; } final r = _h(v, 'r'); expect(toHex(r), _s(tries.last, 'shifted')); expect(h3(sigma, fileKey), _big(r)); if (tries.length > 1) { // The library rejects every try before the last one. expect( () => h3(sigma, fileKey, iterations: tries.length - 1), throwsA(isA()), ); } expect(cleared, isNotNull); expect(toHex(cleared!), isNot(toHex(r))); // r·G2 = U. expect(proofHolds(_big(r), uPoint), isTrue); expect(toHex(G2Point.generator.multiply(_big(r)).toBytes()), toHex(u)); expect(proofHolds(_big(cleared), uPoint), isFalse); }); test('$name: the library opens the stanza and writes it again', () { final body = _h(v, 'body'); final fileKey = _s(v, 'file_key'); expect(toHex(decryptOnG2(signature, ciphertextFromBody(body))), fileKey); expect( toHex( unwrapTlockStanza(quicknet(), round, Release(round, signature), [ '$round', chainHash, ], body), ), fileKey, ); final ct = encryptOnG2WithSigma( fromHex(_s(f, 'public_key')), fromHex(_s(v, 'message')), fromHex(fileKey), _h(v, 'sigma'), ); expect(toHex(ciphertextToBody(ct)), toHex(body)); }); } }