// 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() .having((e) => e.code, 'code', code) .having((e) => e.message, 'message', message), ); Matcher ibeError(IbeReason reason, [String? message]) => throwsA( isA() .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().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().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().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 fetch(PinnedProfile p, int round) async => throw DateKeysException(ErrorCode.releaseInvalid, 'relay: bad signature'); }