|
|
// 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 = <G2Point>[];
|
|
|
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)));
|
|
|
});
|
|
|
});
|
|
|
}
|