// The arithmetic of BLS12-381 by its properties, on the VM and compiled to // JavaScript: the field layer against plain BigInt arithmetic, the tower, // the constants against their definitions, the group laws and the encodings, // the subgroup checks, the pairing and the hash to G1. They read no file: // the few Go values they use are in bls12381_constants.dart. The vectors of Go // are in bls12381_vectors_test.dart. // // On Node.js the BigInt of dart2js is about 20 times slower than on the VM, // so the loops draw fewer cases there ([web]). library; import 'dart:math'; import 'dart:typed_data'; import 'package:crypto/crypto.dart'; 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'; import 'bls12381_constants.dart'; /// Whether the tests run compiled to JavaScript. final bool web = identical(0, 0.0); /// [vm] cases on the VM, [js] on the web. int cases(int vm, int js) => web ? js : vm; final BigInt p = fpModulus; final BigInt r = groupOrder; // A fixed-seed source of field elements, scalars and bytes. final class Draw { Draw(int seed) : _r = Random(seed); final Random _r; Uint8List bytes(int n) => Uint8List.fromList([for (var i = 0; i < n; i++) _r.nextInt(256)]); BigInt big(int n) => BigInt.parse(toHex(bytes(n)), radix: 16); Fp fp() => Fp.fromBytesReduced(bytes(64)); Fp2 fp2() => Fp2(fp(), fp()); Fp6 fp6() => Fp6(fp2(), fp2(), fp2()); Fp12 fp12() => Fp12(fp6(), fp6()); /// A scalar in 1..r-1. BigInt scalar() => big(40) % (r - BigInt.one) + BigInt.one; /// A point of E(Fp) from a random x: almost never in G1. G1Point g1OnCurve() { for (;;) { final x = fp(); final y = (x.square() * x + G1Point.b).sqrt(); if (y != null) return G1Point.affine(x, y); } } /// A point of E'(Fp2) from a random x: almost never in G2. G2Point g2OnCurve() { for (;;) { final x = fp2(); final y = (x.square() * x + G2Point.b).sqrt(); if (y != null) return G2Point.affine(x, y); } } } BigInt big(Fp a) => a.toBigInt(); Fp fp(int v) => Fp(BigInt.from(v)); // The product in Fp2 by the definition u² = −1. Fp2 mulFp2(Fp2 a, Fp2 b) => Fp2(a.c0 * b.c0 - a.c1 * b.c1, a.c0 * b.c1 + a.c1 * b.c0); final Fp2 xi = Fp2(Fp.one, Fp.one); // The product in Fp6 by the definition v³ = ξ, schoolbook. Fp6 mulFp6(Fp6 a, Fp6 b) { final c = List.filled(5, Fp2.zero); final x = [a.c0, a.c1, a.c2]; final y = [b.c0, b.c1, b.c2]; for (var i = 0; i < 3; i++) { for (var j = 0; j < 3; j++) { c[i + j] = c[i + j] + mulFp2(x[i], y[j]); } } return Fp6(c[0] + mulFp2(c[3], xi), c[1] + mulFp2(c[4], xi), c[2]); } final Fp6 v6 = Fp6(Fp2.zero, Fp2.one, Fp2.zero); // The product in Fp12 by the definition w² = v, schoolbook. Fp12 mulFp12(Fp12 a, Fp12 b) => Fp12( mulFp6(a.c0, b.c0) + mulFp6(mulFp6(a.c1, b.c1), v6), mulFp6(a.c0, b.c1) + mulFp6(a.c1, b.c0), ); void main() { group('Fp, the field layer', () { test('p and r are those of spec §12.2, and p = 3 mod 4', () { expect( p.toRadixString(16), '1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241ea' 'bfffeb153ffffb9feffffffffaaab', ); expect( r.toRadixString(16), '73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001', ); expect(p % BigInt.from(4), BigInt.from(3)); }); test('computes as BigInt arithmetic modulo p', () { final d = Draw(1); for (var i = 0; i < cases(200, 20); i++) { final a = d.fp(); final b = d.fp(); final c = d.fp(); final e = d.fp(); expect(big(a + b), (big(a) + big(b)) % p); expect(big(a - b), (big(a) - big(b)) % p); expect(big(-a), (-big(a)) % p); expect(big(a * b), (big(a) * big(b)) % p); expect(big(a.square()), (big(a) * big(a)) % p); expect(big(a.double()), (big(a) * BigInt.two) % p); expect( big(Fp.mulSub(a, b, c, e)), (big(a) * big(b) - big(c) * big(e)) % p, ); expect( big(Fp.mulAdd(a, b, c, e)), (big(a) * big(b) + big(c) * big(e)) % p, ); expect((a * a.inverse()).isOne, isTrue); expect(a.pow(BigInt.from(5)).equals(a * a * a * a * a), isTrue); } expect(Fp.zero.inverse().isZero, isTrue); expect((-Fp.zero).isZero, isTrue); expect((Fp.half + Fp.half).isOne, isTrue); }); test('takes square roots exactly of the squares', () { final d = Draw(2); var squares = 0; for (var i = 0; i < cases(60, 8); i++) { final a = d.fp(); final legendre = big(a).modPow((p - BigInt.one) >> 1, p); final s = a.sqrt(); expect(s != null, legendre == BigInt.one, reason: '$i'); if (s != null) { expect(s.square().equals(a), isTrue); squares++; } else { // The power is then a root of −a. final (t, isSquare) = a.sqrtOrNegatedRoot(); expect(isSquare, isFalse); expect(t.square().equals(-a), isTrue); } } expect(squares, inInclusiveRange(1, cases(59, 7))); expect(Fp.zero.sqrt()!.isZero, isTrue); }); test('reads and writes 48 big-endian bytes, never reducing', () { final d = Draw(3); for (var i = 0; i < 20; i++) { final a = d.fp(); expect(Fp.fromBytes(a.toBytes())!.equals(a), isTrue); } Uint8List be(BigInt v) => fromHex(v.toRadixString(16).padLeft(96, '0')); expect(Fp.fromBytes(be(p - BigInt.one))!.toBigInt(), p - BigInt.one); expect(Fp.fromBytes(be(p)), isNull); expect(Fp.fromBytes(be(p + BigInt.one)), isNull); expect(Fp.fromBytes(Uint8List(48)..fillRange(0, 48, 0xff)), isNull); expect(() => Fp.fromBytes(Uint8List(47)), throwsArgumentError); expect(Fp.fromBytesReduced(be(p + BigInt.two)).toBigInt(), BigInt.two); expect(() => Fp(p), throwsArgumentError); expect(() => Fp(-BigInt.one), throwsArgumentError); }); test('the sign of spec §12.2 turns at (p − 1)/2', () { final half = (p - BigInt.one) >> 1; expect(Fp(half).isLexicographicallyLargest, isFalse); expect(Fp(half + BigInt.one).isLexicographicallyLargest, isTrue); expect(Fp.zero.isLexicographicallyLargest, isFalse); expect(fp(3).isOdd, isTrue); expect(fp(4).isOdd, isFalse); }); }); group('the tower', () { test('Fp2, Fp6 and Fp12 multiply as their definitions', () { final d = Draw(4); for (var i = 0; i < cases(20, 3); i++) { final a2 = d.fp2(); final b2 = d.fp2(); expect((a2 * b2).equals(mulFp2(a2, b2)), isTrue); expect(a2.square().equals(a2 * a2), isTrue); expect((a2 * a2.inverse()).isOne, isTrue); expect(a2.mulByNonResidue().equals(a2 * xi), isTrue); final a6 = d.fp6(); final b6 = d.fp6(); expect((a6 * b6).equals(mulFp6(a6, b6)), isTrue); expect(a6.square().equals(a6 * a6), isTrue); expect((a6 * a6.inverse()).isOne, isTrue); expect(a6.mulByNonResidue().equals(a6 * v6), isTrue); final x0 = d.fp2(); final x1 = d.fp2(); expect(a6.mul01(x0, x1).equals(a6 * Fp6(x0, x1, Fp2.zero)), isTrue); expect(a6.mul1(x1).equals(a6 * Fp6(Fp2.zero, x1, Fp2.zero)), isTrue); final a12 = d.fp12(); final b12 = d.fp12(); expect((a12 * b12).equals(mulFp12(a12, b12)), isTrue); expect(a12.square().equals(a12 * a12), isTrue); expect((a12 * a12.inverse()).isOne, isTrue); final x4 = d.fp2(); final sparse = Fp12(Fp6(x0, x1, Fp2.zero), Fp6(Fp2.zero, x4, Fp2.zero)); expect(a12.mul014(x0, x1, x4).equals(a12 * sparse), isTrue); } }); test('takes square roots in Fp2 exactly of the squares', () { final d = Draw(5); for (var i = 0; i < cases(30, 4); i++) { final a = d.fp2(); final sq = a.square(); final s = sq.sqrt()!; expect(s.square().equals(sq), isTrue); // ξ = 1 + u is not a square: neither is ξ·a². expect((sq * xi).sqrt(), isNull); } // Elements of Fp: a square, and a non-square whose root is in u·Fp. final four = Fp2(fp(4), Fp.zero); expect(four.sqrt()!.square().equals(four), isTrue); final minusFour = Fp2(-fp(4), Fp.zero); final root = minusFour.sqrt()!; expect(root.c0.isZero, isTrue); expect(root.square().equals(minusFour), isTrue); expect(Fp2.zero.sqrt()!.isZero, isTrue); }); test('the Frobenius coefficients are ξ^((p^k − 1)/6)', () { for (var k = 1; k <= 3; k++) { final e = (p.pow(k) - BigInt.one) ~/ BigInt.from(6); expect(xi.pow(e).equals(frobeniusGammas[k - 1]), isTrue, reason: '$k'); } }); test('the Frobenius map of Fp12 is a ring morphism of order 12', () { final d = Draw(6); final a = d.fp12(); final b = d.fp12(); for (var k = 1; k <= 3; k++) { expect( (a * b).frobenius(k).equals(a.frobenius(k) * b.frobenius(k)), isTrue, reason: '$k', ); } expect(a.frobenius(1).frobenius(1).equals(a.frobenius(2)), isTrue); expect(a.frobenius(2).frobenius(1).equals(a.frobenius(3)), isTrue); var x = a; for (var i = 0; i < 12; i++) { x = x.frobenius(1); } expect(x.equals(a), isTrue); // On an element of Fp2, it is the conjugation. final c = d.fp2(); final f = Fp12(Fp6(c, Fp2.zero, Fp2.zero), Fp6.zero); expect(f.frobenius(1).c0.c0.equals(c.conjugate()), isTrue); // And it is the p-th power: (a^p)·a^-p = 1 needs a^p, which the // pairing vectors of Go check through the final exponentiation. }); test('squares in the cyclotomic subgroup as in the field', () { final d = Draw(7); for (var i = 0; i < cases(5, 2); i++) { // The easy part of the final exponentiation lands in the subgroup. final f = d.fp12(); var g = f.conjugate() * f.inverse(); g = g.frobenius(2) * g; expect(g.cyclotomicSquare().equals(g.square()), isTrue); expect((g * g.conjugate()).isOne, isTrue); } }); test('GT is written c1 before c0 at every level', () { final e = [for (var i = 1; i <= 12; i++) fp(i)]; final f = Fp12( Fp6(Fp2(e[0], e[1]), Fp2(e[2], e[3]), Fp2(e[4], e[5])), Fp6(Fp2(e[6], e[7]), Fp2(e[8], e[9]), Fp2(e[10], e[11])), ); final b = f.toBytes(); expect(b, hasLength(576)); // c1 of Fp12: c2, c1, c0 of Fp6, each c1 then c0 of Fp2. expect( [for (var i = 0; i < 12; i++) b[48 * i + 47]], [ 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, // ], ); }); }); group('the curves', () { test('the generators are those of Go, on their curves and in their ' 'subgroups', () { expect(toHex(G1Point.generator.toBytes()), generatorG1); expect(toHex(G2Point.generator.toBytes()), generatorG2); expect( G1Point.decode(fromHex(generatorG1))!.equals(G1Point.generator), isTrue, ); expect( G2Point.decode(fromHex(generatorG2))!.equals(G2Point.generator), isTrue, ); final (x1, y1) = G1Point.generator.toAffine()!; final (x2, y2) = G2Point.generator.toAffine()!; expect(G1Point.isOnCurve(x1, y1), isTrue); expect(G2Point.isOnCurve(x2, y2), isTrue); expect(G1Point.generator.multiply(r).isInfinity, isTrue); expect(G2Point.generator.multiply(r).isInfinity, isTrue); }); test('add, double and multiply as a group of order r', () { final d = Draw(8); for (var i = 0; i < cases(4, 1); i++) { final a = d.scalar(); final b = d.scalar(); final g1 = G1Point.generator; final g2 = G2Point.generator; expect( (g1.multiply(a) + g1.multiply(b)).equals(g1.multiply((a + b) % r)), isTrue, ); expect( (g2.multiply(a) + g2.multiply(b)).equals(g2.multiply((a + b) % r)), isTrue, ); final p1 = g1.multiply(a); final p2 = g2.multiply(b); expect((p1 + p1).equals(p1.double()), isTrue); expect((p2 + p2).equals(p2.double()), isTrue); expect((p1 + -p1).isInfinity, isTrue); expect((p2 + -p2).isInfinity, isTrue); expect(p1.multiply(r + BigInt.one).equals(p1), isTrue); expect(p2.multiply(BigInt.zero).isInfinity, isTrue); final (ax, ay) = p1.toAffine()!; expect(g1.addAffine(ax, ay).equals(g1 + p1), isTrue); final (bx, by) = p2.toAffine()!; expect(g2.addAffine(bx, by).equals(g2 + p2), isTrue); } expect( (G1Point.infinity + G1Point.generator).equals(G1Point.generator), isTrue, ); expect(G1Point.infinity.double().isInfinity, isTrue); expect( () => G1Point.generator.multiply(-BigInt.one), throwsArgumentError, ); }); test('encode and decode the canonical encodings only', () { final d = Draw(9); final p1 = G1Point.generator.multiply(d.scalar()); final p2 = G2Point.generator.multiply(d.scalar()); for (final (group, b, point) in [ (BlsGroup.g1, p1.toBytes(), p1 as Object), (BlsGroup.g2, p2.toBytes(), p2 as Object), ]) { expect(checkCompressedPoint(group, b), PointVerdict.point); final decoded = group == BlsGroup.g1 ? G1Point.decode(b) : G2Point.decode(b); expect( decoded is G1Point ? decoded.equals(point as G1Point) : (decoded! as G2Point).equals(point as G2Point), isTrue, ); // The negation flips the sign bit only. final neg = Uint8List.fromList(b)..[0] ^= 0x20; final negated = group == BlsGroup.g1 ? (-(point as G1Point)).toBytes() : (-(point as G2Point)).toBytes(); expect(negated, neg); // Not canonical: the compression flag cleared, the infinity flag // set, another length, x + p in the first coordinate. expect( checkCompressedPoint(group, Uint8List.fromList(b)..[0] ^= 0x80), PointVerdict.invalid, ); expect( checkCompressedPoint(group, Uint8List.fromList(b)..[0] |= 0x40), PointVerdict.invalid, ); expect(checkCompressedPoint(group, b.sublist(1)), PointVerdict.invalid); expect(checkCompressedPoint(group, [...b, 0]), PointVerdict.invalid); final raw = Uint8List.fromList(b)..[0] &= 0x1f; final x = BigInt.parse(toHex(raw.sublist(0, 48)), radix: 16) + p; if (x.bitLength <= 381) { final plusP = fromHex(x.toRadixString(16).padLeft(96, '0')); plusP[0] |= b[0] & 0xe0; expect( checkCompressedPoint(group, [...plusP, ...b.sublist(48)]), PointVerdict.invalid, ); } } for (final group in BlsGroup.values) { final n = group.byteLength; final infinity = Uint8List(n)..[0] = 0xc0; expect(checkCompressedPoint(group, infinity), PointVerdict.identity); expect( checkCompressedPoint(group, Uint8List.fromList(infinity)..[0] = 0xe0), PointVerdict.invalid, ); expect( checkCompressedPoint( group, Uint8List.fromList(infinity)..[n - 1] = 1, ), PointVerdict.invalid, ); expect(checkCompressedPoint(group, Uint8List(n)), PointVerdict.invalid); } expect(toHex(G1Point.infinity.toBytes()), 'c0${'00' * 47}'); expect(toHex(G2Point.infinity.toBytes()), 'c0${'00' * 95}'); }); test('reject the points of the curve outside the subgroup, torsion ' 'added to a point of the subgroup included', () { final d = Draw(10); for (var i = 0; i < cases(3, 1); i++) { final q1 = d.g1OnCurve(); expect(q1.isInSubgroup, isFalse); expect( checkCompressedPoint(BlsGroup.g1, q1.toBytes()), PointVerdict.invalid, ); // [r]·Q is a point of order dividing the cofactor. final t1 = q1.multiply(r); expect(t1.isInfinity, isFalse); final s1 = G1Point.generator.multiply(d.scalar()) + t1; expect(s1.isInSubgroup, isFalse); expect( checkCompressedPoint(BlsGroup.g1, s1.toBytes()), PointVerdict.invalid, ); final q2 = d.g2OnCurve(); expect(q2.isInSubgroup, isFalse); expect( checkCompressedPoint(BlsGroup.g2, q2.toBytes()), PointVerdict.invalid, ); final t2 = q2.multiply(r); final s2 = G2Point.generator.multiply(d.scalar()) + t2; expect(s2.isInSubgroup, isFalse); expect( checkCompressedPoint(BlsGroup.g2, s2.toBytes()), PointVerdict.invalid, ); } }); test('the subgroup check of G2 by ψ decides as [r]·P = O, torsion of ' 'every small order of the cofactor included', () { final xi = Fp2(Fp.one, Fp.one); expect( xi.pow((p - BigInt.one) ~/ BigInt.from(3)).inverse().equals(psiX), isTrue, ); expect( xi.pow((p - BigInt.one) ~/ BigInt.two).inverse().equals(psiY), isTrue, ); final g = G2Point.generator; final absX = BigInt.parse('d201000000010000', radix: 16); expect(g.psi().equals(-g.multiply(absX)), isTrue); // The cofactor of G2 (cofactorG2 of kilic), and its prime factors // below 2^21: 13², 23², 2713, 11953 and 262069. final h2 = BigInt.parse( '5d543a95414e7f1091d50792876a202cd91de4547085abaa68a205b2e5a7ddfa628' 'f1cb4d9e82ef21537e293a6691ae1616ec6e786f0c70cf1c38e31c7238e5', radix: 16, ); final small = [13, 23, 2713, 11953, 262069]; for (final f in small) { expect(h2 % BigInt.from(f), BigInt.zero); } final d = Draw(13); final points = []; for (var i = 0; i < cases(3, 1); i++) { final q = d.g2OnCurve(); // r·h2 is the order of E'(Fp2). expect(q.multiply(r * h2).isInfinity, isTrue); final s = g.multiply(d.scalar()); points.addAll([q, s, s + q.multiply(r)]); // A point of each small order of the cofactor, and the point of the // subgroup plus it. for (final f in web ? small.take(2) : small) { final t = q.multiply(r * h2 ~/ BigInt.from(f)); if (!t.isInfinity) points.addAll([t, s + t]); } } var outside = 0; for (final q in points) { final naive = q.multiply(r).isInfinity; expect(q.isInSubgroup, naive); if (!naive) outside++; } expect(outside, greaterThanOrEqualTo(points.length * 2 ~/ 3)); }); }); group('the pairing', () { test('e(G1, G2) gives the H2 of tlock_ibe.json', () { // H2 of the IBE of tlock: SHA-256("IBE-H2" || GT), truncated. final gt = pairing(G1Point.generator, G2Point.generator); final h2 = sha256.convert([...'IBE-H2'.codeUnits, ...gt.toBytes()]); expect(toHex(h2.bytes.sublist(0, 16)), h2OfGenerators); }); test('is bilinear and non-degenerate', () { final d = Draw(11); final p1 = G1Point.generator.multiply(d.scalar()); final q2 = G2Point.generator.multiply(d.scalar()); final e = pairing(p1, q2); expect(e.isOne, isFalse); expect(pairing(p1.double(), q2).equals(e.square()), isTrue); expect(pairing(p1, q2.double() + q2).equals(e.square() * e), isTrue); expect(pairing(-p1, q2).equals(e.conjugate()), isTrue); expect(pairing(G1Point.infinity, q2).isOne, isTrue); expect(pairing(p1, G2Point.infinity).isOne, isTrue); expect(pairingCheck([(p1, q2), (-p1, q2)]), isTrue); expect(pairingCheck([(p1.double(), q2), (-p1, q2.double())]), isTrue); expect(pairingCheck([(p1, q2), (p1, q2)]), isFalse); expect(pairingCheck([(G1Point.infinity, q2)]), isTrue); }); test('lands in the subgroup of order r of the cyclotomic subgroup', () { final e = pairing(G1Point.generator, G2Point.generator); var x = Fp12.one; final bits = r.toRadixString(2); for (var i = 0; i < bits.length; i++) { x = x.cyclotomicSquare(); if (bits[i] == '1') x = x * e; } expect(x.isOne, isTrue); }); test('runs the Miller loop over |x| = 0xd201000000010000', () { expect( BigInt.parse('1$millerLoopBits', radix: 2).toRadixString(16), 'd201000000010000', ); }); }); group('the hash to G1', () { test('maps to E′, which the isogeny maps to E, and lands in G1', () { final d = Draw(12); final a = Fp.hex( '00144698a3b8e9433d693a02c96d4982b0ea985383ee66a8d8e8981aefd881ac9893' '6f8da0e0f97f5cf428082d584c1d', ); final b = Fp.hex( '12e2908d11688030018b12e8753eee3b2016c1f0f24f4070a0b9c14fcef35ef55a23' '215a316ceaa5d1cc48e98e172be0', ); for (var i = 0; i < cases(10, 2); i++) { final u = d.fp(); final (x, y) = mapToIsogenousCurve(u); expect(y.square().equals((x.square() + a) * x + b), isTrue); expect(y.isOdd, u.isOdd); final (ix, iy) = isogenyMap(x, y); expect(G1Point.isOnCurve(ix, iy), isTrue); } // The exceptional u = 0: x1 = B/(Z·A). final (x0, _) = mapToIsogenousCurve(Fp.zero); expect(x0.equals(b * (fp(11) * a).inverse()), isTrue); expect(isogenyConstants.map((k) => k.length), [12, 11, 16, 16]); expect(isogenyConstants[1].last.isOne, isTrue); expect(isogenyConstants[3].last.isOne, isTrue); }); test('hashes as Go, for the DST of Quicknet and of RFC 9380', () { // The identity of round 1000: SHA-256 of its 8 big-endian bytes. final id = sha256.convert([0, 0, 0, 0, 0, 0, 0x03, 0xe8]).bytes; final p1 = hashToG1(id, quicknetDst); expect(toHex(p1.toBytes()), hashOfRound1000); expect(p1.isInSubgroup, isTrue); final (x, y) = hashToG1( 'abc'.codeUnits, 'QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_', ).toAffine()!; expect( [toHex(x.toBytes()), toHex(y.toBytes())], [hashOfAbcX, hashOfAbcY], ); }); test('expand_message_xmd checks its lengths', () { expect(expandMessageXmd([], 'DST'.codeUnits, 0), isEmpty); expect(expandMessageXmd([], 'DST'.codeUnits, 8160), hasLength(8160)); expect( () => expandMessageXmd([], 'DST'.codeUnits, 8161), throwsArgumentError, ); expect( () => expandMessageXmd([], List.filled(256, 0x41), 32), throwsArgumentError, ); // The length is an input of b_0: no output is a prefix of a longer // one. final a = expandMessageXmd([1, 2], 'DST'.codeUnits, 32); final b = expandMessageXmd([1, 2], 'DST'.codeUnits, 64); expect(a, isNot(b.sublist(0, 32))); }); }); }