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

274 lines
8.9 KiB

Stage 7b: the author keys and Ed25519 signing ed25519_sign.dart signs as crypto_sign of TweetNaCl in its JavaScript port, with the SHA-512 of package:crypto: the field of curve25519.dart, whose arithmetic is private there, copied with the product as a loop, and modL over 64 limbs of 8 bits in a Float64List, with floor divisions in place of the shifts of TweetNaCl, exact on the VM and on the web. The secret scalar and the nonce never meet a BigInt or a branch; neither platform promises constant time, and the values are wiped as a best effort. authorkey.dart ports package authorkey of datekeys-go: AuthorKey with generate, fromSeed, publicKey, sign, clear and secret, and a toString that hides it; authorPublicString, parseAuthorPublic and parseAuthorSecret, also on the bytes of a Go string; marshalAuthorKey; encryptAuthorKey, scrypt with logN 16 through ScryptRecipient and ageEncrypt of stage 6a; and readAuthorKey, through the age reader with a maximum work factor of 16, whose lines are those of bufio.Scanner and strings.TrimSpace. Every error has the text of Go, with the sets of go_unicode.dart for the case of a string and the spaces of a line. A cleared key refuses every use, where Go would give the values of a key of zeros. tool/authorkey_go_vectors.go writes test/vectors/authorkey.json: the signatures of crypto/ed25519 over lines of sign.input (RFC 8032 tests 1, 2, 3 and 1024), TEST SHA(abc), seeded seeds and messages up to 1 MiB and other public keys; the scalars of math/big; and Generate, Encrypt, ParsePublic, ParseSecret and Read of authorkey with each text, while crypto/rand reads the keystream of SeededRandomSource. Dart writes the same bytes and gives the same texts in every case; authorkey.g.dart, a part of it, runs also in Node.js. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
// Helpers and tests of the author keys over test/vectors/authorkey.json, as
// tool/authorkey_go_vectors.go writes it: the signatures of Go's
// crypto/ed25519, the scalars modulo ℓ, and package authorkey of
// datekeys-go with the texts of its errors. authorkey_test.dart runs them
// over the part in authorkey.g.dart, also compiled to JavaScript, and
// authorkey_vm_test.dart over the whole file. They read no file.
import 'dart:typed_data';
import 'package:datekeys/src/age_writer.dart';
import 'package:datekeys/src/authorkey.dart';
import 'package:datekeys/src/bytes.dart';
import 'package:datekeys/src/curve25519.dart' show verifyStrict;
import 'package:datekeys/src/ed25519_sign.dart';
import 'package:datekeys/src/recipient.dart';
import 'package:datekeys/src/sha256.dart';
import 'package:test/test.dart';
import 'age_support.dart' show pattern;
import 'random_support.dart';
export 'random_support.dart';
/// The text of index [i] of the vectors, null for no error.
String? textOf(Json v, Object? i) {
final t = (v['texts']! as List)[i! as int] as String;
return t.isEmpty ? null : t;
}
/// The text of what [f] throws, null when it returns.
String? errorOf(void Function() f) {
try {
f();
return null;
} on AuthorKeyException catch (e) {
return e.message;
} on ArgumentError catch (e) {
return e.message as String;
}
}
/// The message of a signing case.
Uint8List messageOf(Json c) {
final n = c['message_pattern'] as int?;
return n != null ? pattern(n) : fromHex(c['message']! as String);
}
/// The bytes of a file of the vectors: parts {hex}, {byte, n} and {sealed:
/// {seed, passphrase, work_factor, plain}, length, sha256}, which is
/// written again here with SeededRandomSource and checked.
Uint8List fileOf(List<Object?> parts) {
final out = BytesBuilder(copy: false);
for (final p in parts.cast<Json>()) {
if (p['hex'] case final String h) {
out.add(fromHex(h));
} else if (p['sealed'] case final Json s) {
final f = ageEncrypt(fileOf(s['plain']! as List), [
ScryptRecipient(
s['passphrase']! as String,
workFactor: s['work_factor']! as int,
),
], random: seeded(s['seed']! as String));
expect(f.length, p['length']);
expect(toHex(sha256(f)), p['sha256']);
out.add(f);
} else {
out.add(
Uint8List(p['n']! as int)
..fillRange(0, p['n']! as int, p['byte']! as int),
);
}
}
return out.takeBytes();
}
/// The result of a parse as the vectors write it: the text, and the public
/// key when it passes.
Json parsed(Uint8List Function() f) {
try {
return {'public_key': toHex(f()), 'text': null};
} on AuthorKeyException catch (e) {
return {'text': e.message};
}
}
Json expectedParse(Json v, Json c) => {
if (c['public_key'] != null) 'public_key': c['public_key'],
'text': textOf(v, c['text']),
};
/// The tests of the signing cases [cases].
void signTests(List<Json> cases) {
test('Ed25519 signatures of crypto/ed25519 (${cases.length})', () {
for (final c in cases) {
final seed = fromHex(c['seed']! as String);
final pub = fromHex(c['public_key']! as String);
final msg = messageOf(c);
final sig = ed25519Sign(seed, pub, msg);
expect(toHex(sig), c['signature'], reason: c['name'] as String?);
if (c['valid'] == true) {
expect(toHex(ed25519PublicKey(seed)), c['public_key']);
expect(toHex(AuthorKey.fromSeed(seed).sign(msg)), c['signature']);
expect(
verifyStrict(pub, msg, sig),
isTrue,
reason: c['name'] as String?,
);
} else {
expect(verifyStrict(pub, msg, sig), isFalse);
}
}
});
}
/// The tests of the vectors [v] that authorkey.g.dart holds whole.
void authorKeyTests(Json v) {
test('the scalars modulo ℓ of math/big', () {
final s = v['scalars']! as Json;
for (final c in listOf(s['reduce'])) {
expect(
toHex(ed25519ReduceScalar(fromHex(c['in']! as String))),
c['out'],
reason: c['in'] as String?,
);
}
for (final c in listOf(s['muladd'])) {
final r = ed25519MulAdd(
fromHex(c['a']! as String),
fromHex(c['b']! as String),
fromHex(c['c']! as String),
);
expect(toHex(r), c['out']);
}
});
test('keys: the public key, its string, the secret, Marshal, String', () {
final k = v['keys']! as Json;
for (final c in listOf(k['keys'])) {
final key = AuthorKey.fromSeed(fromHex(c['seed']! as String));
expect(toHex(key.publicKey), c['public_key']);
expect(key.publicString, c['public']);
expect(authorPublicString(key.publicKey), c['public']);
expect(key.secret, c['secret']);
expect(decodeUtf8(marshalAuthorKey(key)), c['marshal']);
expect(key.toString(), c['string']);
expect('$key', c['gostring']);
expect(toHex(parseAuthorPublic(c['public']! as String)), c['public_key']);
expect(parseAuthorSecret(c['secret']! as String).secret, c['secret']);
}
for (final c in listOf(k['seed_errors'])) {
expect(
errorOf(() => AuthorKey.fromSeed(Uint8List(c['length']! as int))),
c['error'],
);
}
for (final c in listOf(k['public_errors'])) {
expect(
errorOf(() => authorPublicString(Uint8List(c['length']! as int))),
c['error'],
);
}
});
test('Generate draws its seed as Go does', () {
for (final c in listOf(v['generate'])) {
final r = RecordingSource(seeded(c['seed']! as String));
final key = AuthorKey.generate(r);
expect(r.json, c['draws']);
expect(key.secret, c['secret']);
expect(toHex(key.publicKey), c['public_key']);
}
});
for (final c in listOf(v['encrypt'])) {
if (c['error'] != null) {
test('Encrypt refuses an empty passphrase', () {
final key = AuthorKey.fromSeed(Uint8List(32));
expect(errorOf(() => encryptAuthorKey(key, '')), c['error']);
});
continue;
}
if (isWeb && c['node'] != true) continue;
test('Encrypt writes the bytes of Go: ${c['passphrase']}', () {
final key = AuthorKey.fromSeed(fromHex(c['key_seed']! as String));
final r = RecordingSource(seeded(c['seed']! as String));
final file = encryptAuthorKey(key, c['passphrase']! as String, random: r);
expect(r.json, c['draws']);
expect(toHex(file), c['file']);
final back = readAuthorKey(file, passphrase: c['passphrase']! as String);
expect(back.secret, key.secret);
});
}
test('ParsePublic, with the texts of Go', () {
for (final c in listOf(v['public'])) {
final b = fromHex(c['in']! as String);
final want = expectedParse(v, c);
expect(
parsed(() => parseAuthorPublicUtf8(b)),
want,
reason: c['in'] as String?,
);
final s = decodeUtf8(b);
if (s != null) expect(parsed(() => parseAuthorPublic(s)), want);
}
});
test('ParseSecret, with the texts of Go', () {
for (final c in listOf(v['secret'])) {
final b = fromHex(c['in']! as String);
final want = expectedParse(v, c);
expect(
parsed(() => parseAuthorSecretUtf8(b).publicKey),
want,
reason: c['in'] as String?,
);
final s = decodeUtf8(b);
if (s != null) {
expect(parsed(() => parseAuthorSecret(s).publicKey), want);
}
}
});
runeTests(v);
for (final c in listOf(v['read'])) {
if (isWeb && c['node'] != true) continue;
test('Read: ${c['name']}', () {
final file = fileOf(c['file']! as List);
final want = <String, Object?>{
'text': textOf(v, c['text']),
if (c['public_key'] != null) 'public_key': c['public_key'],
if (c['secret'] != null) 'secret': c['secret'],
};
Json got;
try {
final k = readAuthorKey(file, passphrase: c['passphrase']! as String);
got = {
'text': null,
'public_key': toHex(k.publicKey),
'secret': k.secret,
};
} on AuthorKeyException catch (e) {
got = {'text': e.message};
}
expect(got, want);
});
}
}
/// The rune cases of [v]: a valid string with one character replaced by a
/// rune of as many bytes, at each edge of the case and space sets of Go.
void runeTests(Json v) {
final r = v['runes']! as Json;
final cases = (r['cases']! as List).cast<List<Object?>>();
test('the runes at the edges of the sets of Go (${cases.length})', () {
final bases = [
utf8Bytes(r['public']! as String),
utf8Bytes(r['secret']! as String),
];
for (final c in cases) {
final kind = c[0]! as int;
final at = c[1]! as int;
final rune = c[2]! as int;
final e = utf8Bytes(String.fromCharCode(rune));
final b = Uint8List.fromList(bases[kind])..setRange(at, at + e.length, e);
final got = errorOf(
() => kind == 0 ? parseAuthorPublicUtf8(b) : parseAuthorSecretUtf8(b),
);
expect(got, textOf(v, c[3]), reason: 'U+${rune.toRadixString(16)}');
}
});
}

Powered by TurnKey Linux.