// The BLS12-381 code against the Go reference: test/vectors/ // bls12381_vectors.json, written by tool/bls12381_go_vectors.go with // kilic/bls12-381 and drand/kyber-bls12381, the libraries of drand and // tlock. Decoding verdicts, sums, multiples, pairings, hashes to G1, the map // of one element and BLS signatures on G1. @TestOn('vm') library; import 'dart:convert'; import 'dart:io'; import 'package:datekeys/datekeys.dart' show fromHex, toHex; import 'package:datekeys/src/bls12381_curve.dart'; import 'package:datekeys/src/bls12381_fp.dart'; import 'package:datekeys/src/bls12381_hash.dart'; import 'package:datekeys/src/bls12381_pairing.dart'; import 'package:datekeys/src/bls12381_tower.dart'; import 'package:test/test.dart'; typedef Json = Map; final Json vectors = jsonDecode( File('test/vectors/bls12381_vectors.json').readAsStringSync(), ) as Json; List section(String name) => (vectors[name]! as List).cast(); String s(Json v, String key) => v[key]! as String; BlsGroup group(Json v) => s(v, 'group') == 'G1' ? BlsGroup.g1 : BlsGroup.g2; PointVerdict verdictOf(String go) => PointVerdict.values.byName(go); G1Point g1(String hex) => G1Point.decode(fromHex(hex))!; G2Point g2(String hex) => G2Point.decode(fromHex(hex))!; void main() { test('the vectors come from the Go libraries of the reference', () { expect(vectors['generator'], 'tool/bls12381_go_vectors.go'); expect( vectors['libraries'], allOf( contains('github.com/kilic/bls12-381 v0.1.0'), contains('github.com/drand/kyber-bls12381 v0.3.4'), ), ); expect(section('points'), hasLength(157)); }); test('decodes every frozen edge case of datekeys-ts as Go does', () { final seen = {}; for (final v in section('points')) { final got = checkCompressedPoint(group(v), fromHex(s(v, 'hex'))); expect(got, verdictOf(s(v, 'go')), reason: s(v, 'label')); seen.add('${s(v, 'group')} ${s(v, 'go')} ${v['class'] ?? ''}'); } // Every class of verdict and of failure, in both groups. expect( seen, containsAll([ 'G1 point ', 'G1 identity ', 'G1 invalid format', 'G1 invalid curve', 'G1 invalid subgroup', 'G2 point ', 'G2 identity ', 'G2 invalid format', 'G2 invalid curve', 'G2 invalid subgroup', ]), ); }); test('decodes the encodings drawn from the seed as Go does, failing where Go ' 'fails', () { final classes = {}; for (final v in section('decode')) { final b = fromHex(s(v, 'hex')); final label = s(v, 'label'); expect( checkCompressedPoint(group(v), b), verdictOf(s(v, 'go')), reason: label, ); final cls = v['class'] as String?; final key = '${s(v, 'group')} ${cls ?? s(v, 'go')}'; classes[key] = (classes[key] ?? 0) + 1; // Where Go finds a point of the curve outside the subgroup, so does // this code: the subgroup check is what rejects it. if (cls == 'subgroup' || cls == 'curve') { expect( onCurveOutsideSubgroup(group(v), b), cls == 'subgroup', reason: label, ); } // A point re-encodes to its bytes. if (s(v, 'go') == 'point') { final encoded = group(v) == BlsGroup.g1 ? g1(s(v, 'hex')).toBytes() : g2(s(v, 'hex')).toBytes(); expect(toHex(encoded), s(v, 'hex'), reason: label); } } expect(classes['G1 subgroup'], greaterThanOrEqualTo(16)); expect(classes['G2 subgroup'], greaterThanOrEqualTo(8)); expect(classes['G1 curve'], greaterThanOrEqualTo(8)); expect(classes['G2 curve'], greaterThanOrEqualTo(4)); }); test('adds as kilic', () { for (final v in section('add')) { final label = s(v, 'label'); if (group(v) == BlsGroup.g1) { final sum = g1(s(v, 'a')) + g1(s(v, 'b')); expect(toHex(sum.toBytes()), s(v, 'sum'), reason: label); // The mixed addition agrees. final b = g1(s(v, 'b')).toAffine(); if (b != null) { expect( toHex(g1(s(v, 'a')).addAffine(b.$1, b.$2).toBytes()), s(v, 'sum'), reason: label, ); } } else { final sum = g2(s(v, 'a')) + g2(s(v, 'b')); expect(toHex(sum.toBytes()), s(v, 'sum'), reason: label); final b = g2(s(v, 'b')).toAffine(); if (b != null) { expect( toHex(g2(s(v, 'a')).addAffine(b.$1, b.$2).toBytes()), s(v, 'sum'), reason: label, ); } } } }); test('multiplies as kilic, also by 0, r and scalars above r', () { for (final v in section('multiply')) { final k = BigInt.parse(s(v, 'scalar'), radix: 16); final product = group(v) == BlsGroup.g1 ? g1(s(v, 'point')).multiply(k).toBytes() : g2(s(v, 'point')).multiply(k).toBytes(); expect(toHex(product), s(v, 'product'), reason: s(v, 'label')); } }); test('pairs as kilic, serialized as kyber-bls12381 marshals GT', () { for (final v in section('pairing')) { final gt = pairing(g1(s(v, 'g1')), g2(s(v, 'g2'))); expect(toHex(gt.toBytes()), s(v, 'gt'), reason: s(v, 'label')); } }); test('hashes to G1 as kilic, for the DST of Quicknet and of RFC 9380', () { final dsts = {}; for (final v in section('hash_to_g1')) { final p = hashToG1(fromHex(s(v, 'msg')), s(v, 'dst')); final label = s(v, 'label'); expect(toHex(p.toBytes()), s(v, 'point'), reason: label); final (x, y) = p.toAffine()!; expect( [toHex(x.toBytes()), toHex(y.toBytes())], [s(v, 'x'), s(v, 'y')], reason: label, ); dsts.add(s(v, 'dst')); } expect(dsts, { quicknetDst, 'QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_', }); }); test('maps one element to G1 as kilic, the exceptional ones included', () { for (final v in section('map_to_g1')) { final u = Fp.fromBytes(fromHex(s(v, 'u')))!; expect(toHex(mapToG1(u).toBytes()), s(v, 'point'), reason: s(v, 'label')); } }); test('verifies BLS signatures on G1 as kyber sign/bls', () { for (final v in section('signatures')) { final key = g2(s(v, 'public_key')); final sig = G1Point.decode(fromHex(s(v, 'signature'))); final ok = sig != null && !sig.isInfinity && pairingCheck([ (hashToG1(fromHex(s(v, 'msg')), quicknetDst), key), (-sig, G2Point.generator), ]); expect(ok, v['go'], reason: s(v, 'label')); } }); } // Whether the compressed [b] has an x of a point of the curve, which then // lies outside the subgroup: what kilic reports as "not on correct // subgroup" rather than "not on curve". bool onCurveOutsideSubgroup(BlsGroup group, List b) { final raw = [b[0] & 0x1f, ...b.sublist(1)]; if (group == BlsGroup.g1) { final x = Fp.fromBytes(raw)!; final y = (x.square() * x + G1Point.b).sqrt(); return y != null && !G1Point.affine(x, y).isInSubgroup; } final x = Fp2( Fp.fromBytes(raw.sublist(48))!, Fp.fromBytes(raw.sublist(0, 48))!, ); final y = (x.square() * x + G2Point.b).sqrt(); return y != null && !G2Point.affine(x, y).isInSubgroup; }