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.
199 lines
7.3 KiB
199 lines
7.3 KiB
// 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<String, Object?>;
|
|
|
|
// 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<int> a, List<int> b) {
|
|
expect(a, hasLength(b.length));
|
|
return Uint8List.fromList([for (var i = 0; i < a.length; i++) a[i] ^ b[i]]);
|
|
}
|
|
|
|
BigInt _big(List<int> 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<Json>();
|
|
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<Json>();
|
|
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<IbeException>()),
|
|
);
|
|
}
|
|
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));
|
|
});
|
|
}
|
|
}
|