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>
3 days ago
|
|
|
|
// The author keys and the Ed25519 signing against Go, also compiled to
|
|
|
|
|
|
// JavaScript: the part of test/vectors/authorkey.json in authorkey.g.dart
|
|
|
|
|
|
// (see authorkey_support.dart), and the behaviour of a key that the vectors
|
|
|
|
|
|
// do not show: a cleared key, the hidden secret and keys of random seeds.
|
|
|
|
|
|
|
|
|
|
|
|
import 'dart:convert';
|
|
|
|
|
|
import 'dart:typed_data';
|
|
|
|
|
|
|
|
|
|
|
|
import 'package:datekeys/src/authorkey.dart';
|
|
|
|
|
|
import 'package:datekeys/src/bytes.dart';
|
|
|
|
|
|
import 'package:datekeys/src/curve25519.dart' show ed25519Order, verifyStrict;
|
|
|
|
|
|
import 'package:datekeys/src/ed25519_sign.dart';
|
|
|
|
|
|
import 'package:datekeys/src/random.dart' show randomBytes;
|
|
|
|
|
|
import 'package:test/test.dart';
|
|
|
|
|
|
|
|
|
|
|
|
import 'authorkey_support.dart';
|
|
|
|
|
|
import 'vectors/authorkey.g.dart';
|
|
|
|
|
|
|
|
|
|
|
|
final Json vectors = jsonDecode(authorKeyJson) as Json;
|
|
|
|
|
|
|
|
|
|
|
|
BigInt le(List<int> b) {
|
|
|
|
|
|
var n = BigInt.zero;
|
|
|
|
|
|
for (var i = b.length - 1; i >= 0; i--) {
|
|
|
|
|
|
n = (n << 8) | BigInt.from(b[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
return n;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
|
signTests(listOf(vectors['sign']));
|
|
|
|
|
|
authorKeyTests(vectors);
|
|
|
|
|
|
|
|
|
|
|
|
test('the scalars modulo ℓ against BigInt, at random', () {
|
|
|
|
|
|
final r = seeded('scalars');
|
|
|
|
|
|
for (var i = 0; i < 200; i++) {
|
|
|
|
|
|
final x = randomBytes(r, 64);
|
|
|
|
|
|
// Some with long runs of 0xff or of zeros.
|
|
|
|
|
|
if (i % 3 == 1) x.fillRange(0, i % 64, 0xff);
|
|
|
|
|
|
if (i % 3 == 2) x.fillRange(i % 64, 64, 0);
|
|
|
|
|
|
expect(le(ed25519ReduceScalar(x)), le(x) % ed25519Order);
|
|
|
|
|
|
final a = randomBytes(r, 32), b = randomBytes(r, 32);
|
|
|
|
|
|
final c = randomBytes(r, 32);
|
|
|
|
|
|
expect(
|
|
|
|
|
|
le(ed25519MulAdd(a, b, c)),
|
|
|
|
|
|
(le(a) * le(b) + le(c)) % ed25519Order,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('a key signs what verifyStrict accepts, and nothing else', () {
|
|
|
|
|
|
final r = seeded('sign and verify');
|
|
|
|
|
|
for (var i = 0; i < 16; i++) {
|
|
|
|
|
|
final key = AuthorKey.generate(r);
|
|
|
|
|
|
final msg = randomBytes(r, i * 7);
|
|
|
|
|
|
final sig = key.sign(msg);
|
|
|
|
|
|
expect(verifyStrict(key.publicKey, msg, sig), isTrue);
|
|
|
|
|
|
final other = [...msg, 0];
|
|
|
|
|
|
expect(verifyStrict(key.publicKey, other, sig), isFalse);
|
|
|
|
|
|
// The strings and the file give the same key back.
|
|
|
|
|
|
expect(parseAuthorPublic(key.publicString), key.publicKey);
|
|
|
|
|
|
expect(parseAuthorSecret(key.secret).publicKey, key.publicKey);
|
|
|
|
|
|
expect(readAuthorKey(marshalAuthorKey(key)).secret, key.secret);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('a cleared key refuses every use', () {
|
|
|
|
|
|
final key = AuthorKey.fromSeed(List.filled(32, 7));
|
|
|
|
|
|
expect(key.isCleared, isFalse);
|
|
|
|
|
|
key.clear();
|
|
|
|
|
|
expect(key.isCleared, isTrue);
|
|
|
|
|
|
expect(() => key.publicKey, throwsStateError);
|
|
|
|
|
|
expect(() => key.sign(const [1]), throwsStateError);
|
|
|
|
|
|
expect(() => key.secret, throwsStateError);
|
|
|
|
|
|
expect(() => key.publicString, throwsStateError);
|
|
|
|
|
|
expect(() => marshalAuthorKey(key), throwsStateError);
|
|
|
|
|
|
expect(() => encryptAuthorKey(key, 'p'), throwsStateError);
|
|
|
|
|
|
expect('$key', 'DKAUTHOR-SECRET-KEY-1… (hidden)');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('the secret never shows in an interpolation', () {
|
|
|
|
|
|
final key = AuthorKey.fromSeed(List.filled(32, 9));
|
|
|
|
|
|
expect('$key'.contains(key.secret.substring(21)), isFalse);
|
|
|
|
|
|
expect('${[key]}', '[DKAUTHOR-SECRET-KEY-1… (hidden)]');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('a seed is copied, and wiping it leaves the key', () {
|
|
|
|
|
|
final seed = Uint8List.fromList(List.filled(32, 3));
|
|
|
|
|
|
final key = AuthorKey.fromSeed(seed);
|
|
|
|
|
|
final pub = key.publicKey;
|
|
|
|
|
|
seed.fillRange(0, 32, 0);
|
|
|
|
|
|
expect(key.publicKey, pub);
|
|
|
|
|
|
expect(key.publicKey, isNot(same(key.publicKey)));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('the arguments of the scalar functions', () {
|
|
|
|
|
|
expect(() => ed25519ReduceScalar(Uint8List(63)), throwsArgumentError);
|
|
|
|
|
|
expect(
|
|
|
|
|
|
() => ed25519MulAdd(Uint8List(32), Uint8List(31), Uint8List(32)),
|
|
|
|
|
|
throwsArgumentError,
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(() => ed25519PublicKey(Uint8List(31)), throwsArgumentError);
|
|
|
|
|
|
expect(
|
|
|
|
|
|
() => ed25519Sign(Uint8List(32), Uint8List(33), const []),
|
|
|
|
|
|
throwsArgumentError,
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('ℓ is the order of Go', () {
|
|
|
|
|
|
expect(
|
|
|
|
|
|
le(fromHex((vectors['scalars']! as Json)['order']! as String)),
|
|
|
|
|
|
ed25519Order,
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|