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.
dateKeys-dart/test/ibe_vectors_test.dart

382 lines
13 KiB

// The IBE of lib/src/ibe.dart against the Go reference: test/vectors/
// ibe_vectors.json, written by tool/ibe_go_vectors.go with drand/kyber
// encrypt/ibe, tlock and age, the libraries of the reference implementation,
// on the fixtures of testdata/. A port of ibe.test.ts of datekeys-ts.
@TestOn('vm')
library;
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:datekeys/datekeys.dart' show concatBytes, fromHex, toHex;
import 'package:datekeys/src/bls12381_curve.dart';
import 'package:datekeys/src/bls12381_pairing.dart';
import 'package:datekeys/src/bls12381_tower.dart';
import 'package:datekeys/src/ibe.dart';
import 'package:test/test.dart';
import 'tlock_support.dart';
typedef Json = Map<String, Object?>;
Json readJson(String path) => jsonDecode(File(path).readAsStringSync()) as Json;
final Json v = readJson('test/vectors/ibe_vectors.json');
List<Json> section(Json file, String name) =>
(file[name]! as List).cast<Json>();
String s(Json v, String key) => v[key]! as String;
Uint8List h(Json v, String key) => fromHex(s(v, key));
TlockCiphertext ct(Json v) => TlockCiphertext(h(v, 'u'), h(v, 'v'), h(v, 'w'));
String scalarHex(BigInt r) => r.toRadixString(16).padLeft(64, '0');
Uint8List xor(List<int> a, List<int> b) =>
Uint8List.fromList([for (var i = 0; i < a.length; i++) a[i] ^ b[i]]);
final Json timeOnly = section(
v,
'fixtures',
).firstWhere((f) => f['name'] == 'time_only');
const proofMessage =
'ibe: U is not r·G2: the ciphertext does not decrypt under this signature';
// Runs [body] and returns the IbeException it throws.
IbeException ibeError(void Function() body, String label) {
try {
body();
} on IbeException catch (e) {
return e;
}
fail('$label: no IbeException');
}
// GT serialized c0 first at every level of the tower: the order of noble's
// Fp12.toBytes, which H2 must not hash.
Uint8List c0First(Fp12 gt) {
List<int> fp2(Fp2 a) => [...a.c0.toBytes(), ...a.c1.toBytes()];
List<int> fp6(Fp6 a) => [...fp2(a.c0), ...fp2(a.c1), ...fp2(a.c2)];
return Uint8List.fromList([...fp6(gt.c0), ...fp6(gt.c1)]);
}
void main() {
test('reads the vectors of the Quicknet scheme and key', () {
expect(v['generator'], 'tool/ibe_go_vectors.go');
expect(v['scheme'], 'bls-unchained-g1-rfc9380');
expect(
v['public_key'],
readJson('testdata/vectors/profile_quicknet.json')['public_key'],
);
expect(v['public_key'], quicknetPublicKey);
});
test(
'serializes GT in the order of kilic, which H2 hashes, and never c0 first',
() {
for (final g in section(v, 'gt')) {
final gt = pairing(
G1Point.decode(h(g, 'g1'))!,
G2Point.decode(h(g, 'g2'))!,
);
final name = s(g, 'name');
expect(toHex(gtBytes(gt)), s(g, 'gt'), reason: name);
expect(toHex(h2(gt, 16)), s(g, 'h2_16'), reason: name);
expect(toHex(h2(gt, 32)), s(g, 'h2_32'), reason: name);
expect(toHex(c0First(gt)), isNot(s(g, 'gt')), reason: name);
}
// The first vector is the one every implementation shares.
final shared = section(
readJson('testdata/vectors/tlock_ibe.json'),
'vectors',
).first;
final first = section(v, 'gt').first;
expect(
[s(first, 'gt'), s(first, 'h2_16')],
[s(shared, 'gt'), s(shared, 'h2')],
);
},
);
test('computes H3 through its rejection sampling, and H4', () {
for (final c in section(v, 'h3')) {
final sigma = h(c, 'sigma');
final msg = h(c, 'msg');
final iterations = c['iterations']! as int;
final name = s(c, 'name');
expect(scalarHex(h3(sigma, msg)), s(c, 'r'), reason: name);
expect(
scalarHex(h3(sigma, msg, iterations: iterations)),
s(c, 'r'),
reason: name,
);
if (iterations > 1) {
final e = ibeError(
() => h3(sigma, msg, iterations: iterations - 1),
name,
);
expect(
[e.reason, e.message],
[
IbeReason.proof,
'ibe: no scalar r below the order of the group (rejection sampling '
'failed)',
],
);
}
}
expect([
for (final c in section(v, 'h3')) c['iterations'],
], containsAll([1, 2, 3]));
for (final c in section(v, 'h4')) {
expect(toHex(h4(h(c, 'sigma'), 16)), s(c, 'h4_16'));
expect(toHex(h4(h(c, 'sigma'), 32)), s(c, 'h4_32'));
}
});
test('derives the identity of a round as drand does', () {
for (final c in section(v, 'round_identities')) {
final round = c['round']! as int;
expect(toHex(roundIdentity(round)), s(c, 'id'), reason: '$round');
}
for (final bad in [-1, maxSafeRound + 1]) {
expect(() => roundIdentity(bad), throwsRangeError, reason: '$bad');
}
});
test('opens the tlock stanza of every official fixture with the file key of '
'the reference, which authenticates the age header', () {
final names = [
for (final f in Directory('testdata/fixtures').listSync())
if (f.path.endsWith('.dkc'))
f.uri.pathSegments.last.replaceAll('.dkc', ''),
]..sort();
final fixtures = section(v, 'fixtures');
expect([for (final f in fixtures) s(f, 'name')]..sort(), names);
for (final f in fixtures) {
final name = s(f, 'name');
final dkc = File('testdata/fixtures/$name.dkc').readAsBytesSync();
final record = readJson('testdata/fixtures/$name.json');
expect(record['release'], {
'round': f['round'],
'signature': f['signature'],
});
final header = readAgeHeader(sealedControl(dkc));
expect(header.stanzas, hasLength(1));
final stanza = header.stanzas.single;
expect(
[stanza.type, stanza.args.first, toHex(stanza.body)],
['tlock', '${f['round']}', s(f, 'body')],
reason: name,
);
final sig = h(f, 'signature');
final c = ciphertextFromBody(stanza.body);
final fileKey = decryptOnG2(sig, c);
expect(toHex(fileKey), s(f, 'file_key'), reason: name);
expect(
ageHeaderMacValid(fileKey, header.macInput, header.mac),
isTrue,
reason: name,
);
// The values in between, as the reference computes them.
final gt = pairing(G1Point.decode(sig)!, G2Point.decode(c.u)!);
expect(toHex(gtBytes(gt)), s(f, 'gt'), reason: name);
final sigma = xor(c.v, h2(gt, 16));
expect(toHex(sigma), s(f, 'sigma'), reason: name);
expect(xor(c.w, h4(sigma, 16)), h(f, 'file_key'), reason: name);
expect(scalarHex(h3(sigma, h(f, 'file_key'))), s(f, 'r'), reason: name);
expect(toHex(ciphertextToBody(c)), s(f, 'body'), reason: name);
// Another file key fails the MAC.
expect(
ageHeaderMacValid(
xor(fileKey, List.filled(16, 1)),
header.macInput,
header.mac,
),
isFalse,
);
}
});
test('decrypts what kyber encrypts, for messages of 0 to 32 bytes', () {
final kyber = section(v, 'kyber');
expect([for (final c in kyber) h(c, 'v').length], [0, 1, 16, 32]);
for (final c in kyber) {
expect(
toHex(decryptOnG2(h(c, 'signature'), ct(c))),
c['msg'] ?? '',
reason: s(c, 'name'),
);
}
});
test('rejects what the reference rejects, with the reason of the first '
'failing check', () {
const want = {
'the time_only stanza': null,
'U with p added to c0': IbeReason.encoding,
'U is the point at infinity': IbeReason.identity,
'U negated': IbeReason.proof,
'V with its first bit flipped': IbeReason.proof,
'W with its last bit flipped': IbeReason.proof,
'the signature of round 1001': IbeReason.proof,
'the signature negated': IbeReason.proof,
'the signature is the point at infinity': IbeReason.identity,
'W one byte shorter than V': IbeReason.length,
'V and W of 33 bytes': IbeReason.length,
'V and W empty': IbeReason.proof,
'U is the generator of G2': IbeReason.proof,
};
final cases = section(v, 'decrypt');
expect({for (final c in cases) s(c, 'name')}, want.keys.toSet());
for (final c in cases) {
final name = s(c, 'name');
expect(c['go'] == 'ok', want[name] == null, reason: name);
if (c['go'] == 'ok') {
expect(
toHex(decryptOnG2(h(c, 'signature'), ct(c))),
c['msg'],
reason: name,
);
continue;
}
final e = ibeError(() => decryptOnG2(h(c, 'signature'), ct(c)), name);
expect(e.reason, want[name], reason: name);
if (e.reason == IbeReason.proof) {
expect(e.message, proofMessage, reason: name);
}
}
});
test('gates every encoding of the signature and of U as Go decodes it', () {
final points = section(
readJson('test/vectors/bls12381_vectors.json'),
'points',
);
final c = ciphertextFromBody(h(timeOnly, 'body'));
final sig = h(timeOnly, 'signature');
const reason = {
'invalid': IbeReason.encoding,
'identity': IbeReason.identity,
'point': IbeReason.proof,
};
final seen = <IbeReason>{};
for (final p in points) {
final bytes = h(p, 'hex');
final g1 = p['group'] == 'G1';
final label = s(p, 'label');
final e = ibeError(
() => g1
? decryptOnG2(bytes, c)
: decryptOnG2(sig, TlockCiphertext(bytes, c.v, c.w)),
label,
);
// An encoding of another length is invalid for Go too; the IBE says
// so first.
final want = bytes.length == (g1 ? 48 : 96)
? reason[s(p, 'go')]
: IbeReason.length;
expect(e.reason, want, reason: label);
seen.add(e.reason);
}
expect(seen, IbeReason.values.toSet());
});
test('checks the lengths first, with fixed texts', () {
final c = ciphertextFromBody(h(timeOnly, 'body'));
final sig = h(timeOnly, 'signature');
final cases = <(String, void Function(), String)>[
(
'a signature of 47 bytes',
() => decryptOnG2(sig.sublist(1), c),
'ibe: the signature of 47 bytes, want 48',
),
(
'a signature of 49 bytes',
() => decryptOnG2([...sig, 0], c),
'ibe: the signature of 49 bytes, want 48',
),
(
'U of 95 bytes',
() => decryptOnG2(sig, TlockCiphertext(c.u.sublist(1), c.v, c.w)),
'ibe: U of 95 bytes, want 96',
),
(
'V longer than W',
() => decryptOnG2(sig, TlockCiphertext(c.u, Uint8List(17), c.w)),
'ibe: V of 17 bytes and W of 16, want equal lengths of at most 32',
),
(
'a body of 127 bytes',
() => ciphertextFromBody(Uint8List(tlockBodyLength - 1)),
'ibe: tlock stanza body of 127 bytes, want 128',
),
(
'a body of 129 bytes',
() => ciphertextFromBody(Uint8List(tlockBodyLength + 1)),
'ibe: tlock stanza body of 129 bytes, want 128',
),
(
'a body with V of 15 bytes',
() => ciphertextToBody(TlockCiphertext(c.u, c.v.sublist(1), c.w)),
'ibe: a tlock stanza body holds U of 96 bytes and V and W of 16',
),
(
'a body with W of 17 bytes',
() => ciphertextToBody(TlockCiphertext(c.u, c.v, Uint8List(17))),
'ibe: a tlock stanza body holds U of 96 bytes and V and W of 16',
),
(
'a body with U of 95 bytes',
() => ciphertextToBody(TlockCiphertext(c.u.sublist(1), c.v, c.w)),
'ibe: a tlock stanza body holds U of 96 bytes and V and W of 16',
),
];
for (final (label, body, message) in cases) {
final e = ibeError(body, label);
expect([e.reason, e.message], [IbeReason.length, message], reason: label);
}
});
test('never proves with r = 0', () {
final u = G2Point.decode(h(timeOnly, 'body').sublist(0, 96))!;
expect(proofHolds(BigInt.zero, u), isFalse);
expect(proofHolds(BigInt.parse(s(timeOnly, 'r'), radix: 16), u), isTrue);
});
test('never puts a value of the computation in an error', () {
// The messages are fixed by the reason and the lengths: the file key,
// sigma and r of the stanzas the edits start from appear in none.
final secrets = [
s(timeOnly, 'file_key'),
s(timeOnly, 'sigma'),
s(timeOnly, 'r'),
s(timeOnly, 'signature'),
s(timeOnly, 'body').substring(0, 32),
];
for (final c in section(v, 'decrypt').where((c) => c['go'] == 'reject')) {
final name = s(c, 'name');
final e = ibeError(() => decryptOnG2(h(c, 'signature'), ct(c)), name);
for (final secret in secrets) {
expect(
e.message.toLowerCase(),
isNot(contains(secret.substring(0, 16))),
reason: name,
);
}
expect(e.message, isNot(matches(RegExp('[0-9a-f]{16}'))), reason: name);
}
});
test('a body splits and joins back', () {
final body = h(timeOnly, 'body');
final c = ciphertextFromBody(body);
expect(concatBytes([c.u, c.v, c.w]), body);
expect(ciphertextToBody(c), body);
});
}

Powered by TurnKey Linux.