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/tlock_vectors_test.dart

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);
});
}

Powered by TurnKey Linux.