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.
278 lines
8.7 KiB
278 lines
8.7 KiB
// The IBE, the tlock stanza and the verification of releases on the VM and
|
|
// compiled to JavaScript, with the Go values of ibe_constants.dart: the
|
|
// tests that read the vectors of Go run on the VM only (ibe_vectors_test,
|
|
// tlock_vectors_test and release_vectors_test). Each case here costs a
|
|
// pairing or two, about half a second on Node.js: they are few.
|
|
library;
|
|
|
|
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 'ibe_constants.dart';
|
|
import 'tlock_support.dart';
|
|
|
|
Matcher dateKeysError(ErrorCode code, String message) => throwsA(
|
|
isA<DateKeysException>()
|
|
.having((e) => e.code, 'code', code)
|
|
.having((e) => e.message, 'message', message),
|
|
);
|
|
|
|
Matcher ibeError(IbeReason reason, [String? message]) => throwsA(
|
|
isA<IbeException>()
|
|
.having((e) => e.reason, 'reason', reason)
|
|
.having((e) => e.message, 'message', message ?? anything),
|
|
);
|
|
|
|
void main() {
|
|
final p = quicknet();
|
|
final sig1000 = fromHex(signature1000);
|
|
final sig1001 = fromHex(signature1001);
|
|
|
|
test('H3, H4 and the identity of a round are those of Go', () {
|
|
final sigma = fromHex(h3Sigma);
|
|
final msg = fromHex(h3Msg);
|
|
expect(h3(sigma, msg).toRadixString(16).padLeft(64, '0'), h3R);
|
|
expect(h3(sigma, msg, iterations: h3Iterations3), h3(sigma, msg));
|
|
expect(
|
|
() => h3(sigma, msg, iterations: h3Iterations3 - 1),
|
|
ibeError(
|
|
IbeReason.proof,
|
|
'ibe: no scalar r below the order of the group (rejection sampling '
|
|
'failed)',
|
|
),
|
|
);
|
|
expect(toHex(h4(fromHex(h4Sigma), 16)), h4Of16);
|
|
expect(toHex(roundIdentity(1000)), encrypt1000Id);
|
|
expect(roundIdentity(0), hasLength(32));
|
|
expect(roundIdentity(maxSafeRound), hasLength(32));
|
|
expect(() => roundIdentity(-1), throwsRangeError);
|
|
});
|
|
|
|
test('encrypts as Go for a given sigma, and decrypts', () {
|
|
final ct = encryptOnG2WithSigma(
|
|
p.publicKey,
|
|
fromHex(encrypt1000Id),
|
|
fromHex(encrypt1000Msg),
|
|
fromHex(encrypt1000Sigma),
|
|
);
|
|
expect(
|
|
[toHex(ct.u), toHex(ct.v), toHex(ct.w)],
|
|
[encrypt1000U, encrypt1000V, encrypt1000W],
|
|
);
|
|
expect(toHex(decryptOnG2(sig1000, ct)), encrypt1000Msg);
|
|
expect(
|
|
() => decryptOnG2(sig1001, ct),
|
|
ibeError(
|
|
IbeReason.proof,
|
|
'ibe: U is not r·G2: the ciphertext does not decrypt under this '
|
|
'signature',
|
|
),
|
|
);
|
|
});
|
|
|
|
// Random.secure of dart2js fails under dart test -p node, where
|
|
// crypto.getRandomValues is called with another this; it works in a
|
|
// browser. The two tests that draw sigma run on the VM only.
|
|
test('draws sigma at random, and the round opens what it seals', () {
|
|
final msg = fromHex('00112233445566778899aabbccddeeff');
|
|
final ct = encryptOnG2(p.publicKey, roundIdentity(1001), msg);
|
|
expect(ct.u, hasLength(uLength));
|
|
expect(decryptOnG2(sig1001, ct), msg);
|
|
}, testOn: 'vm');
|
|
|
|
test('rejects lengths and encodings before any pairing', () {
|
|
final ct = TlockCiphertext(
|
|
fromHex(encrypt1000U),
|
|
fromHex(encrypt1000V),
|
|
fromHex(encrypt1000W),
|
|
);
|
|
expect(
|
|
() => decryptOnG2(sig1000.sublist(1), ct),
|
|
ibeError(IbeReason.length, 'ibe: the signature of 47 bytes, want 48'),
|
|
);
|
|
expect(
|
|
() => decryptOnG2(Uint8List(48)..[0] = 0xc0, ct),
|
|
ibeError(
|
|
IbeReason.identity,
|
|
'ibe: the signature is the point at infinity',
|
|
),
|
|
);
|
|
expect(
|
|
() => decryptOnG2(Uint8List.fromList(sig1000)..[0] ^= 0x80, ct),
|
|
ibeError(IbeReason.encoding),
|
|
);
|
|
expect(
|
|
() => decryptOnG2(
|
|
sig1000,
|
|
TlockCiphertext(Uint8List(96)..[0] = 0xc0, ct.v, ct.w),
|
|
),
|
|
ibeError(IbeReason.identity, 'ibe: U is the point at infinity'),
|
|
);
|
|
expect(
|
|
() => decryptOnG2(sig1000, TlockCiphertext(ct.u, ct.v, Uint8List(15))),
|
|
ibeError(IbeReason.length),
|
|
);
|
|
expect(
|
|
() => encryptOnG2(p.publicKey, roundIdentity(1000), Uint8List(33)),
|
|
ibeError(IbeReason.length, 'ibe: a message of 33 bytes, want at most 32'),
|
|
);
|
|
});
|
|
|
|
test('verifies a release of Quicknet, in the order of provider.Verify', () {
|
|
verifyRelease(p, 1000, Release(1000, sig1000));
|
|
expect(
|
|
() => verifyRelease(p, 1000, Release(1001, sig1001)),
|
|
dateKeysError(
|
|
ErrorCode.roundMismatch,
|
|
'provider: release for round 1001, expected 1000: ERR_ROUND_MISMATCH',
|
|
),
|
|
);
|
|
expect(
|
|
() => verifyRelease(p, 1000, Release(1000, sig1001)),
|
|
dateKeysError(
|
|
ErrorCode.releaseInvalid,
|
|
'provider: the signature is not a canonical point encoding, or does '
|
|
'not verify as the BLS signature of round 1000 under '
|
|
'datekeys:quicknet:v1: ERR_RELEASE_INVALID',
|
|
),
|
|
);
|
|
expect(
|
|
() => verifyRelease(p, 1000, Release(1000, sig1000.sublist(1))),
|
|
dateKeysError(
|
|
ErrorCode.releaseInvalid,
|
|
'provider: signature is 47 bytes, bls-unchained-g1-rfc9380 uses 48: '
|
|
'ERR_RELEASE_INVALID',
|
|
),
|
|
);
|
|
expect(
|
|
() => verifyRelease(p, 0, Release(0, sig1000)),
|
|
dateKeysError(
|
|
ErrorCode.dateKeyInvalid,
|
|
'provider: round 0 outside the range of datekeys:quicknet:v1: '
|
|
'ERR_DATEKEY_INVALID',
|
|
),
|
|
);
|
|
expect(
|
|
() => verifyRelease(
|
|
p.copyWith(scheme: 'pedersen-bls-unchained'),
|
|
1000,
|
|
Release(1000, sig1000),
|
|
),
|
|
throwsA(
|
|
isA<DateKeysException>().having(
|
|
(e) => e.code,
|
|
'code',
|
|
ErrorCode.unknownProfile,
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
test('wraps a file key in a tlock stanza that the release unwraps', () {
|
|
final fileKey = fromHex('0f' * 16);
|
|
final (args, body) = wrapTlockStanza(p, 1000, fileKey);
|
|
expect(args, ['1000', quicknetChainHash]);
|
|
expect(
|
|
unwrapTlockStanza(p, 1000, Release(1000, sig1000), args, body),
|
|
fileKey,
|
|
);
|
|
expect(
|
|
() => unwrapTlockStanza(
|
|
p,
|
|
1000,
|
|
Release(1000, sig1000),
|
|
args,
|
|
body.sublist(1),
|
|
),
|
|
dateKeysError(
|
|
ErrorCode.integrity,
|
|
'agewrap: tlock stanza body of 127 bytes, want 128: ERR_INTEGRITY',
|
|
),
|
|
);
|
|
expect(
|
|
() => wrapTlockStanza(p, p.maxRound + 1, fileKey),
|
|
throwsA(
|
|
isA<DateKeysException>().having(
|
|
(e) => e.code,
|
|
'code',
|
|
ErrorCode.dateKeyInvalid,
|
|
),
|
|
),
|
|
);
|
|
}, testOn: 'vm');
|
|
|
|
test('unwraps the tlock stanza of the encryption of Go', () {
|
|
final args = ['1000', quicknetChainHash];
|
|
final body = fromHex(encrypt1000U + encrypt1000V + encrypt1000W);
|
|
final r = Release(1000, sig1000);
|
|
expect(toHex(unwrapTlockStanza(p, 1000, r, args, body)), encrypt1000Msg);
|
|
expect(
|
|
() => unwrapTlockStanza(p, 1000, r, args, body.sublist(1)),
|
|
dateKeysError(
|
|
ErrorCode.integrity,
|
|
'agewrap: tlock stanza body of 127 bytes, want 128: ERR_INTEGRITY',
|
|
),
|
|
);
|
|
expect(
|
|
() => unwrapTlockStanza(p, 1000, r, ['1001', quicknetChainHash], body),
|
|
dateKeysError(
|
|
ErrorCode.roundMismatch,
|
|
'agewrap: tlock stanza round "1001", DateKey round 1000: '
|
|
'ERR_ROUND_MISMATCH',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('never takes the point at infinity for a signature, whatever the key '
|
|
'(Go does, with the point at infinity for key)', () {
|
|
final infinity = Uint8List(96)..[0] = 0xc0;
|
|
expect(
|
|
() => verifyRelease(
|
|
p.copyWith(publicKey: infinity),
|
|
1000,
|
|
Release(1000, Uint8List(48)..[0] = 0xc0),
|
|
),
|
|
dateKeysError(
|
|
ErrorCode.releaseInvalid,
|
|
'provider: the signature is not a canonical point encoding, or does '
|
|
'not verify as the BLS signature of round 1000 under '
|
|
'datekeys:quicknet:v1: ERR_RELEASE_INVALID',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('reports whatever a source throws as ERR_RELEASE_UNAVAILABLE', () async {
|
|
await expectLater(
|
|
fetchRelease(suppliedRelease(), p, 1000),
|
|
throwsA(
|
|
isA<DateKeysException>().having(
|
|
(e) => e.message,
|
|
'message',
|
|
'release: no release supplied for round 1000: '
|
|
'ERR_RELEASE_UNAVAILABLE',
|
|
),
|
|
),
|
|
);
|
|
await expectLater(
|
|
fetchRelease(_Failing(), p, 1000),
|
|
dateKeysError(
|
|
ErrorCode.releaseUnavailable,
|
|
'capsule: release source: relay: bad signature: ERR_RELEASE_INVALID: '
|
|
'ERR_RELEASE_UNAVAILABLE',
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
final class _Failing implements ReleaseSource {
|
|
@override
|
|
Future<Release> fetch(PinnedProfile p, int round) async =>
|
|
throw DateKeysException(ErrorCode.releaseInvalid, 'relay: bad signature');
|
|
}
|