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.
319 lines
10 KiB
319 lines
10 KiB
// The tlock encryption and the tlock stanza (lib/src/ibe.dart,
|
|
// lib/src/tlock.dart) against the Go reference: test/vectors/
|
|
// tlock_vectors.json, written by tool/tlock_go_vectors.go, and the unwrap
|
|
// and recipient sections of test/vectors/release_vectors.json, written by
|
|
// tool/release_go_vectors.go. A port of tlock.test.ts of datekeys-ts.
|
|
@TestOn('vm')
|
|
library;
|
|
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:datekeys/datekeys.dart' show fromHex, toHex;
|
|
import 'package:datekeys/src/errors.dart';
|
|
import 'package:datekeys/src/ibe.dart';
|
|
import 'package:datekeys/src/release.dart';
|
|
import 'package:datekeys/src/tlock.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/tlock_vectors.json');
|
|
final Json g = readJson('test/vectors/release_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));
|
|
|
|
final Map<int, String> signatures = {
|
|
for (final e in section(v, 'encrypt')) e['round']! as int: s(e, 'signature'),
|
|
};
|
|
|
|
Release release(int round) => Release(round, fromHex(signatures[round]!));
|
|
|
|
// The profiles of release_vectors.json: Quicknet with another scheme or key.
|
|
TestProfile profileOf(String name) {
|
|
final c = section(g, 'profiles').firstWhere((p) => p['name'] == name);
|
|
return quicknet().copyWith(
|
|
scheme: s(c, 'scheme'),
|
|
publicKey: h(c, 'public_key'),
|
|
);
|
|
}
|
|
|
|
// Go's TimeIdentity.Unwrap as stage 4 will compose it: the stanza rules of
|
|
// agewrap that need the whole header (its count and the type of its
|
|
// stanza), which stage 2 brings, then unwrapTlockStanza. The texts are
|
|
// Go's.
|
|
Uint8List timeIdentityUnwrap(
|
|
PinnedProfile p,
|
|
int round,
|
|
Release r,
|
|
List<Stanza> stanzas,
|
|
) {
|
|
if (stanzas.length != 1) {
|
|
throw DateKeysException(
|
|
ErrorCode.policyStructureMismatch,
|
|
'agewrap: OUTER_TIME_AGE has ${stanzas.length} stanzas, want exactly '
|
|
'one tlock stanza',
|
|
);
|
|
}
|
|
final st = stanzas.single;
|
|
if (st.type != 'tlock') {
|
|
throw DateKeysException(
|
|
ErrorCode.policyStructureMismatch,
|
|
'agewrap: OUTER_TIME_AGE stanza type "${st.type}", want "tlock"',
|
|
);
|
|
}
|
|
return unwrapTlockStanza(p, round, r, st.args, st.body);
|
|
}
|
|
|
|
DateKeysException dateKeysError(void Function() body, String label) {
|
|
try {
|
|
body();
|
|
} on DateKeysException catch (e) {
|
|
return e;
|
|
}
|
|
fail('$label: no DateKeysException');
|
|
}
|
|
|
|
void main() {
|
|
test('reads vectors of the Quicknet scheme and key', () {
|
|
expect(v['generator'], 'tool/tlock_go_vectors.go');
|
|
expect([v['scheme'], v['public_key']], [quicknetScheme, quicknetPublicKey]);
|
|
expect(signatures.keys.toSet(), {1000, 1001});
|
|
expect(g['generator'], 'tool/release_go_vectors.go');
|
|
expect(g['max_round'], quicknet().maxRound);
|
|
});
|
|
|
|
test('encrypts as the reference, byte for byte, for a given sigma', () {
|
|
final p = quicknet();
|
|
for (final e in section(v, 'encrypt')) {
|
|
final name = s(e, 'name');
|
|
expect(toHex(roundIdentity(e['round']! as int)), s(e, 'id'));
|
|
final c = encryptOnG2WithSigma(
|
|
p.publicKey,
|
|
h(e, 'id'),
|
|
h(e, 'msg'),
|
|
h(e, 'sigma'),
|
|
);
|
|
expect(
|
|
[toHex(c.u), toHex(c.v), toHex(c.w)],
|
|
[s(e, 'u'), s(e, 'v'), s(e, 'w')],
|
|
reason: name,
|
|
);
|
|
expect(toHex(decryptOnG2(h(e, 'signature'), c)), s(e, 'msg'));
|
|
}
|
|
expect(
|
|
[for (final e in section(v, 'encrypt')) h(e, 'msg').length]..sort(),
|
|
[0, 1, 16, 16, 32],
|
|
);
|
|
});
|
|
|
|
test('opens what datekeys-ts encrypted and the reference opened: IBE bodies, '
|
|
'and age files whose header MAC the file key verifies', () {
|
|
final interop = v['interop']! as Json;
|
|
expect(interop['generator'], 'scripts/tlock-ts-samples.mjs');
|
|
final samples = section(interop, 'samples');
|
|
expect([for (final x in samples) '${x['kind']} ${x['round']}']..sort(), [
|
|
'age 1000',
|
|
'age 1001',
|
|
'ibe 1000',
|
|
'ibe 1001',
|
|
]);
|
|
for (final x in samples) {
|
|
final name = s(x, 'name');
|
|
final round = x['round']! as int;
|
|
expect(x['go'], 'ok', reason: name);
|
|
if (x['kind'] == 'ibe') {
|
|
expect(x['go_result'], x['file_key'], reason: name);
|
|
final key = decryptOnG2(
|
|
release(round).signature,
|
|
ciphertextFromBody(h(x, 'body')),
|
|
);
|
|
expect(toHex(key), s(x, 'file_key'), reason: name);
|
|
} else {
|
|
expect(x['go_result'], x['plaintext'], reason: name);
|
|
final header = readAgeHeader(h(x, 'file'));
|
|
final key = timeIdentityUnwrap(
|
|
quicknet(),
|
|
round,
|
|
release(round),
|
|
header.stanzas,
|
|
);
|
|
expect(
|
|
ageHeaderMacValid(key, header.macInput, header.mac),
|
|
isTrue,
|
|
reason: name,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
test(
|
|
'draws a new sigma every time, and the signature of the round opens every '
|
|
'ciphertext',
|
|
() {
|
|
final p = quicknet();
|
|
final msg = fromHex('00112233445566778899aabbccddeeff');
|
|
final a = encryptOnG2(p.publicKey, roundIdentity(1001), msg);
|
|
final b = encryptOnG2(p.publicKey, roundIdentity(1001), msg);
|
|
expect(toHex(a.u), isNot(toHex(b.u)));
|
|
for (final c in [a, b]) {
|
|
expect(decryptOnG2(release(1001).signature, c), msg);
|
|
}
|
|
expect(
|
|
() => decryptOnG2(release(1000).signature, a),
|
|
throwsA(isA<IbeException>()),
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'rejects a message longer than 32 bytes, a sigma of another length and a '
|
|
'key that is not a canonical point',
|
|
() {
|
|
final p = quicknet();
|
|
final id = roundIdentity(1000);
|
|
(IbeReason, String) failure(void Function() body) {
|
|
try {
|
|
body();
|
|
} on IbeException catch (e) {
|
|
return (e.reason, e.message);
|
|
}
|
|
fail('no IbeException');
|
|
}
|
|
|
|
final infinity = Uint8List(96)..[0] = 0xc0;
|
|
final offCurve = Uint8List.fromList(p.publicKey)..[95] ^= 1;
|
|
expect(failure(() => encryptOnG2(p.publicKey, id, Uint8List(33))), (
|
|
IbeReason.length,
|
|
'ibe: a message of 33 bytes, want at most 32',
|
|
));
|
|
expect(
|
|
failure(
|
|
() => encryptOnG2WithSigma(
|
|
p.publicKey,
|
|
id,
|
|
Uint8List(16),
|
|
Uint8List(15),
|
|
),
|
|
),
|
|
(IbeReason.length, 'ibe: sigma of 15 bytes for a message of 16'),
|
|
);
|
|
expect(
|
|
failure(() => encryptOnG2(p.publicKey.sublist(1), id, Uint8List(16))),
|
|
(IbeReason.length, 'ibe: the public key of 95 bytes, want 96'),
|
|
);
|
|
expect(failure(() => encryptOnG2(infinity, id, Uint8List(16))), (
|
|
IbeReason.identity,
|
|
'ibe: the public key is the point at infinity',
|
|
));
|
|
expect(
|
|
failure(() => encryptOnG2(offCurve, id, Uint8List(16))).$1,
|
|
IbeReason.encoding,
|
|
);
|
|
},
|
|
);
|
|
|
|
test('writes the stanza of tlock, which unwraps with the release', () {
|
|
final p = quicknet();
|
|
final fileKey = fromHex('0f' * 16);
|
|
final (args, body) = wrapTlockStanza(p, 1000, fileKey);
|
|
expect(args, ['1000', quicknetChainHash]);
|
|
expect(body, hasLength(tlockBodyLength));
|
|
expect(unwrapTlockStanza(p, 1000, release(1000), args, body), fileKey);
|
|
final e = dateKeysError(
|
|
() => unwrapTlockStanza(p, 1000, release(1001), args, body),
|
|
'another release',
|
|
);
|
|
expect(e.code, ErrorCode.roundMismatch);
|
|
});
|
|
|
|
test(
|
|
'checks the profile, then the round, as NewTimeRecipient, with its codes '
|
|
'and texts',
|
|
() {
|
|
// Only the scheme of Quicknet is supported, as in datekeys-ts: for
|
|
// another scheme the code is Go's, the text this library's.
|
|
const onlyQuicknet = {
|
|
'another scheme of drand':
|
|
'agewrap: profile datekeys:quicknet:v1 uses scheme '
|
|
'pedersen-bls-unchained; only bls-unchained-g1-rfc9380 is '
|
|
'supported here: ERR_UNKNOWN_PROFILE',
|
|
'a scheme that is not of drand':
|
|
'agewrap: profile datekeys:quicknet:v1 uses scheme datekeys-test; '
|
|
'only bls-unchained-g1-rfc9380 is supported here: '
|
|
'ERR_UNKNOWN_PROFILE',
|
|
};
|
|
for (final c in section(g, 'recipient')) {
|
|
final name = s(c, 'name');
|
|
final p = profileOf(s(c, 'profile'));
|
|
final round = c['round']! as int;
|
|
if (c['go'] == 'ok') {
|
|
final (args, body) = wrapTlockStanza(p, round, Uint8List(16));
|
|
expect(args.first, '$round', reason: name);
|
|
expect(body, hasLength(tlockBodyLength), reason: name);
|
|
continue;
|
|
}
|
|
final e = dateKeysError(
|
|
() => wrapTlockStanza(p, round, Uint8List(16)),
|
|
name,
|
|
);
|
|
expect(e.code.code, c['code'], reason: name);
|
|
expect(e.message, onlyQuicknet[name] ?? c['text'], reason: name);
|
|
}
|
|
},
|
|
);
|
|
|
|
test('unwraps the stanza of OUTER_TIME_AGE as TimeIdentity of Go, with its '
|
|
'codes and texts, in its order', () {
|
|
// As above, only the scheme of Quicknet; and the profile is checked
|
|
// as NewTimeIdentity does, before the stanza.
|
|
const onlyQuicknet = {
|
|
'another scheme of drand':
|
|
'agewrap: profile datekeys:quicknet:v1 uses scheme '
|
|
'pedersen-bls-unchained; only bls-unchained-g1-rfc9380 is '
|
|
'supported here: ERR_UNKNOWN_PROFILE',
|
|
};
|
|
var opened = 0;
|
|
for (final c in section(g, 'unwrap')) {
|
|
final name = s(c, 'name');
|
|
final p = profileOf(s(c, 'profile'));
|
|
final r = Release(c['release_round']! as int, h(c, 'signature'));
|
|
final stanzas = [
|
|
for (final st in (c['stanzas']! as List).cast<Json>())
|
|
(
|
|
type: s(st, 'type'),
|
|
args: (st['args']! as List).cast<String>(),
|
|
body: h(st, 'body'),
|
|
),
|
|
];
|
|
final round = c['round']! as int;
|
|
if (c['go'] == 'ok') {
|
|
expect(
|
|
toHex(timeIdentityUnwrap(p, round, r, stanzas)),
|
|
c['file_key'],
|
|
reason: name,
|
|
);
|
|
opened++;
|
|
continue;
|
|
}
|
|
final e = dateKeysError(
|
|
() => timeIdentityUnwrap(p, round, r, stanzas),
|
|
name,
|
|
);
|
|
expect(e.code.code, c['code'], reason: name);
|
|
expect(e.message, onlyQuicknet[name] ?? c['text'], reason: name);
|
|
}
|
|
expect(opened, 1);
|
|
});
|
|
}
|