Stage 7b: Seal and NewEnvelope of the locator, against Go both ways

locator_seal.dart ports the writing side of package locator of
datekeys-go: sealLocator, Seal of Go, marshals the locator, makes the
tlock recipient of the round and encrypts, in that order and with the
texts of Go, and wipes the plaintext; newEnvelope, NewEnvelope of Go,
draws I_SOBRE, encrypts the .dkc for it with ageEncrypt of stage 6a and
splits the file with splitEnvelope of stage 7a.

tool/locator_seal_go_vectors.go writes test/vectors/locator_seal.json:
Seal of locators of one to three blocks for rounds from 1 to the last of
Quicknet, its refusals, NewEnvelope of .dkc of 0 bytes to 1 MiB and a
whole flow, while crypto/rand reads the keystream of SeededRandomSource.
With the same seed, Dart draws the same values and writes the same bytes
in every case, and the sealed locators open with the release of their
round.

In the other direction, tool/seal_interop_dart_samples.dart writes sealed
locators with their envelopes, author key files and signatures from the
recipes of test/seal_interop_support.dart, and
tool/seal_interop_go_verdicts.go opens them with locator.Open,
OpenEnvelope, authorkey.Read and crypto/ed25519: Go reads all fifteen.
test/vectors/seal_interop.json keeps the verdicts and the digest of each
file, which the tests write again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.11
dev 1 day ago
parent 7c7e71599b
commit fe5b759e29

@ -0,0 +1,70 @@
/// The writing side of the locator of datekeys.capsule (spec §44.1), as
/// Seal and NewEnvelope of package locator of datekeys-go at the draft
/// v0.12: sealing a locator with tlock for the round of its DateKey, and
/// making the envelope of a .dkc, whose header the locator carries and
/// whose rest the person keeps outside. The rest of package locator is in
/// locator.dart.
///
/// Every random value comes from the [RandomSource] given, the CSPRNG of the
/// platform by default, in the order of Go: with the same values, these
/// write the bytes of Go.
library;
import 'dart:typed_data';
import 'age_writer.dart';
import 'locator.dart';
import 'random.dart';
import 'recipient.dart';
import 'release.dart' show PinnedProfile;
/// The locator [locator] as an age file with a single tlock stanza for
/// [round] of the pinned profile [p], the round of the DateKey (spec
/// §44.1): nobody reads it before the date, the holder of the key included.
/// As Seal of Go, it marshals the locator first, a [LocatorException] when
/// it breaks the rules of [Locator.marshal], then makes the tlock recipient
/// of [round], a DateKeysException as [TimeRecipient] (ERR_UNKNOWN_PROFILE
/// or ERR_DATEKEY_INVALID), and encrypts. The plaintext, which holds
/// I_SOBRE, is wiped.
///
/// The tlock encryption is not constant time (see ibe.dart).
Uint8List sealLocator(
PinnedProfile p,
int round,
Locator locator, {
RandomSource random = secureRandom,
}) {
final plain = locator.marshal();
try {
final r = TimeRecipient(p, round);
return ageEncrypt(plain, [r], random: random);
} finally {
plain.fillRange(0, plain.length, 0);
}
}
/// The envelope of the .dkc [dkc], as NewEnvelope of Go: [dkc] encrypted
/// with age for a new X25519 identity, I_SOBRE, and split
/// ([splitEnvelope]). The locator it returns has the key, the header, the
/// digests and the size of the rest, and no address yet; the rest, with no
/// mark, is what the person keeps outside. A caller adds the addresses where
/// it stored the rest ([Locator.withAddresses]), alone or inside another
/// file ([hideRest]), and then seals the locator ([sealLocator]).
///
/// The identity comes first from [random], then what age draws, as in Go.
/// The locator holds I_SOBRE: the caller wipes it ([Locator.wipe]) once
/// sealed.
({Locator locator, Uint8List rest}) newEnvelope(
List<int> dkc, {
RandomSource random = secureRandom,
}) {
final id = generateX25519Identity(random);
final raw = rawX25519Identity(id);
try {
final file = ageEncrypt(dkc, [X25519Recipient.of(id)], random: random);
return splitEnvelope(file, raw, dkc);
} finally {
raw.fillRange(0, raw.length, 0);
id.wipe();
}
}

@ -0,0 +1,149 @@
// The sealing of the locator and the envelope against Go, also compiled to
// JavaScript: test/vectors/locator_seal.json, from
// tool/locator_seal_go_vectors.go, holds what Seal and NewEnvelope of Go
// write while crypto/rand reads the keystream of a seed. With the same
// seed, sealLocator and newEnvelope draw the same values and write the same
// bytes; the sealed locators open with the release of their round, and the
// envelopes with their locator. Seal refuses what Go refuses, with its
// text, before drawing anything.
import 'dart:convert';
import 'dart:typed_data';
import 'package:datekeys/src/bytes.dart';
import 'package:datekeys/src/errors.dart';
import 'package:datekeys/src/locator.dart';
import 'package:datekeys/src/locator_seal.dart';
import 'package:datekeys/src/profile.dart';
import 'package:datekeys/src/release.dart' show Release;
import 'package:datekeys/src/sha256.dart';
import 'package:test/test.dart';
import 'age_support.dart' show pattern;
import 'random_support.dart';
import 'vectors/locator_seal.g.dart';
final Json vectors = jsonDecode(locatorSealJson) as Json;
Locator locatorOf(Json j) => Locator(
addresses: [
for (final a in listOf(j['addresses']))
LocatorAddress(a['uri']! as String, a['offset']! as int),
],
envelopeKey: fromHex(j['envelope_key']! as String),
envelopeHeader: fromHex(j['envelope_header']! as String),
restDigest: fromHex(j['rest_digest']! as String),
restSize: j['rest_size']! as int,
capsuleDigest: fromHex(j['capsule_digest']! as String),
);
Json jsonOf(Locator l) => {
'addresses': [
for (final a in l.addresses) {'uri': a.uri, 'offset': a.offset},
],
'envelope_key': toHex(l.envelopeKey),
'envelope_header': toHex(l.envelopeHeader),
'rest_digest': toHex(l.restDigest),
'rest_size': l.restSize,
'capsule_digest': toHex(l.capsuleDigest),
};
void expectFile(Uint8List b, Json want) {
expect(b.length, want['length']);
expect(toHex(sha256(b)), want['sha256']);
if (want['hex'] != null) expect(toHex(b), want['hex']);
}
Release releaseOf(int round) => Release(
round,
fromHex((vectors['releases']! as Json)['$round']! as String),
);
void main() {
final q = quicknet();
for (final c in listOf(vectors['seal'])) {
test('Seal writes the bytes of Go: ${c['name']}', () {
final round = c['round']! as int;
final loc = locatorOf(c['locator']! as Json);
expect(loc.marshal().length, c['plaintext_length']);
final r = RecordingSource(seeded(c['seed']! as String));
final sealed = sealLocator(q, round, loc, random: r);
expectFile(sealed, c['sealed']! as Json);
expect(r.draws, hasLength(c['draws']));
expect(r.json.first, c['first_draw']);
if (c['opens'] == true) {
final back = openLocator(q, round, releaseOf(round), sealed);
expect(jsonOf(back), c['locator']);
// Another round does not open it.
final other = round == 1000 ? 1001 : 1000;
expect(
() => openLocator(q, other, releaseOf(other), sealed),
throwsA(isA<LocatorException>()),
);
}
});
}
test('Seal refuses what Go refuses, before drawing anything', () {
for (final c in listOf(vectors['seal_errors'])) {
final r = RecordingSource(seeded('unused'));
String? got;
try {
sealLocator(
q,
c['round']! as int,
locatorOf(c['locator']! as Json),
random: r,
);
} on LocatorException catch (e) {
got = e.message;
} on DateKeysException catch (e) {
got = e.message;
}
expect(got, c['error'], reason: c['name'] as String?);
expect(r.draws, hasLength(c['draws']));
}
});
for (final c in listOf(vectors['envelope'])) {
if (isWeb && c['node'] != true) continue;
test('NewEnvelope writes the envelope of Go: ${c['dkc_length']} bytes', () {
final dkc = pattern(c['dkc_length']! as int);
final r = RecordingSource(seeded(c['seed']! as String));
final e = newEnvelope(dkc, random: r);
expect(jsonOf(e.locator), c['locator']);
expectFile(e.rest, c['rest']! as Json);
expect(r.json.first, (c['draws']! as List).first);
expect(e.locator.openEnvelope(e.rest), dkc);
});
}
test('a writer and a reader, as Go: envelope, addresses, seal, open', () {
final c = vectors['flow']! as Json;
final dkc = pattern(c['dkc_length']! as int);
final r = RecordingSource(seeded(c['seed']! as String));
final e = newEnvelope(dkc, random: r);
final hidden = hideRest(fromHex(c['host']! as String), e.rest);
final loc = e.locator.withAddresses([
LocatorAddress('https://example.com/capsule'),
LocatorAddress('https://example.org/host.gif', hidden.offset),
]);
final round = c['round']! as int;
final sealed = sealLocator(q, round, loc, random: r);
expect(r.draws, hasLength(c['draws']));
expect(jsonOf(loc), c['locator']);
expectFile(e.rest, c['rest']! as Json);
expectFile(sealed, c['sealed']! as Json);
final got = openLocator(q, round, releaseOf(round), sealed);
final a = got.addresses[1];
expect(got.openEnvelope(got.restIn(hidden.file, a.offset)), dkc);
});
test('sealing leaves the locator as it was', () {
final loc = locatorOf((listOf(vectors['seal']).first)['locator']! as Json);
final before = loc.marshal();
sealLocator(q, 1000, loc, random: seeded('as it was'));
expect(loc.marshal(), before);
});
}

@ -0,0 +1,25 @@
// The vectors of stage 7b against their files: the Dart constants that the
// tests compiled to JavaScript read are test/vectors/locator_seal.json and
// seal_interop.json byte for byte, as tool/locator_seal_go_vectors.go and
// tool/seal_interop_go_verdicts.go write them.
@TestOn('vm')
library;
import 'dart:io';
import 'package:test/test.dart';
import 'vectors/locator_seal.g.dart';
import 'vectors/seal_interop.g.dart';
void main() {
test('locator_seal.g.dart holds locator_seal.json', () {
final file = File('test/vectors/locator_seal.json').readAsStringSync();
expect(locatorSealJson, file);
});
test('seal_interop.g.dart holds seal_interop.json', () {
final file = File('test/vectors/seal_interop.json').readAsStringSync();
expect(sealInteropJson, file);
});
}

@ -0,0 +1,106 @@
// The recipes of the interoperability of stage 7b, Dart to Go: sealed
// locators with their envelopes, author key files and Ed25519 signatures,
// which this library writes with SeededRandomSource. tool/
// seal_interop_dart_samples.dart writes their files, tool/
// seal_interop_go_verdicts.go opens them with Go and writes test/vectors/
// seal_interop.json, and seal_interop_test.dart writes them again and
// compares them with what Go read. It reads no file.
import 'dart:typed_data';
import 'package:datekeys/src/authorkey.dart';
import 'package:datekeys/src/bytes.dart';
import 'package:datekeys/src/locator.dart';
import 'package:datekeys/src/locator_seal.dart';
import 'package:datekeys/src/profile.dart';
import 'package:datekeys/src/sha256.dart';
import 'age_support.dart' show pattern;
import 'random_support.dart';
export 'random_support.dart';
/// The recipes, in their order.
List<Json> interopRecipes() => [
for (final (round, n, addresses) in [
(1000, 0, 1),
(1001, 100, 2),
(1004, 70000, 8),
(2000, 5000, 3),
])
{
'kind': 'locator',
'name': 'a locator of round $round, a .dkc of $n bytes',
'seed': 'interop locator $round',
'round': round,
'dkc_length': n,
'addresses': [
for (var i = 0; i < addresses; i++)
{'uri': 'https://example.com/$round/$i', 'offset': i * 1000},
],
},
for (final (i, pass) in [(0, ''), (1, 'una contraseña larga'), (2, 'p')])
{
'kind': 'key',
'name': pass.isEmpty ? 'a plain key file' : 'a key file for "$pass"',
'seed': 'interop key $i',
'passphrase': pass,
},
for (final n in [0, 1, 32, 64, 99, 100, 1000, 70000])
{
'kind': 'signature',
'name': 'a signature of $n bytes',
'seed': 'interop signature $n',
'message_length': n,
},
];
/// The files of a recipe, by the suffix of their name, and what Go must
/// find in them.
Map<String, Uint8List> interopFiles(Json r) {
final random = seeded(r['seed']! as String);
switch (r['kind']) {
case 'locator':
final dkc = pattern(r['dkc_length']! as int);
final e = newEnvelope(dkc, random: random);
final loc = e.locator.withAddresses([
for (final a in listOf(r['addresses']))
LocatorAddress(a['uri']! as String, a['offset']! as int),
]);
final sealed = sealLocator(
quicknet(),
r['round']! as int,
loc,
random: random,
);
return {
'sealed': sealed,
'rest': e.rest,
'plaintext': loc.marshal(),
'dkc': dkc,
};
case 'key':
final key = AuthorKey.generate(random);
final pass = r['passphrase']! as String;
return {
'file': pass.isEmpty
? marshalAuthorKey(key)
: encryptAuthorKey(key, pass, random: random),
'secret': utf8Bytes(key.secret),
'public': utf8Bytes(key.publicString),
};
default:
final key = AuthorKey.generate(random);
final msg = pattern(r['message_length']! as int);
return {
'signature': key.sign(msg),
'public_key': key.publicKey,
'seed': utf8Bytes(key.secret),
};
}
}
/// The SHA-256 of each file, as the vectors write them.
Json interopDigests(Map<String, Uint8List> files) => {
for (final e in files.entries) e.key: toHex(sha256(e.value)),
};

@ -0,0 +1,40 @@
// Stage 7b against Go, in the other direction: what this library writes,
// opened by Go. test/vectors/seal_interop.json holds the recipes of
// test/seal_interop_support.dart, the SHA-256 of each file that this
// library wrote from them with SeededRandomSource, and the verdict of Go
// (tool/seal_interop_go_verdicts.go): the sealed locators open with the
// release of their round to the plaintext that was sealed and their
// envelopes to the .dkc, the key files read to the key, and the signatures
// are those of crypto/ed25519. Here each file is written again and must be
// the one that Go read. The vectors come from a Dart constant, so that
// this also runs compiled to JavaScript.
import 'dart:convert';
import 'package:test/test.dart';
import 'seal_interop_support.dart';
import 'vectors/seal_interop.g.dart';
final Json vectors = jsonDecode(sealInteropJson) as Json;
void main() {
final samples = listOf(vectors['samples']);
test('the recipes are those that the tool writes', () {
expect([
for (final s in samples)
{
for (final e in s.entries)
if (e.key != 'files' && e.key != 'verdict') e.key: e.value,
},
], jsonDecode(jsonEncode(interopRecipes())));
});
for (final s in samples) {
test('Go reads it: ${s['name']}', () {
expect(s['verdict'], 'ok');
expect(interopDigests(interopFiles(s)), s['files']);
});
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,266 @@
// Generated by tool/seal_interop_go_verdicts.go from seal_interop.json, for
// the tests that also run compiled to JavaScript, where no file can be
// read. Do not edit.
/// The text of test/vectors/seal_interop.json.
const sealInteropJson = r'''
{
"description": "What Go makes of the files that datekeys-dart writes in stage 7b with SeededRandomSource, from the recipes of test/seal_interop_support.dart, by tool/seal_interop_go_verdicts.go: the SHA-256 of each file and the verdict of Go, ok or the text of what failed.",
"go": "go1.26.8",
"samples": [
{
"addresses": [
{
"offset": 0,
"uri": "https://example.com/1000/0"
}
],
"dkc_length": 0,
"files": {
"dkc": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"plaintext": "40b0d932687eedbcd3d74ce5c2f78333ec6793dd84584ea8095ac36e4ac2306a",
"rest": "f934c42de6a3d9977f743f7f5372a433cd6f82360bd89b99dd08566133bb6e9c",
"sealed": "fe6a39326639e1396ac342f4f0ac0267b9830061c3ed4f4c33761ce78dcab679"
},
"kind": "locator",
"name": "a locator of round 1000, a .dkc of 0 bytes",
"round": 1000,
"seed": "interop locator 1000",
"verdict": "ok"
},
{
"addresses": [
{
"offset": 0,
"uri": "https://example.com/1001/0"
},
{
"offset": 1000,
"uri": "https://example.com/1001/1"
}
],
"dkc_length": 100,
"files": {
"dkc": "c22e490daa445fb2fba44278c022df135310fd278cabca4ad7919eddcccd1dce",
"plaintext": "b01f2323ec7f8bbfcca5062c0f5b64bc35a629dcf839cbfa31964aab0e78ddb2",
"rest": "f6fc60769fba93535b502e7e5590cfa7c17ea9721138b474c48911a0fb2fd72b",
"sealed": "07c736c85a761011fa564f3e16c549d6ec4c5d0ab1cb23884f288ef2714f8aab"
},
"kind": "locator",
"name": "a locator of round 1001, a .dkc of 100 bytes",
"round": 1001,
"seed": "interop locator 1001",
"verdict": "ok"
},
{
"addresses": [
{
"offset": 0,
"uri": "https://example.com/1004/0"
},
{
"offset": 1000,
"uri": "https://example.com/1004/1"
},
{
"offset": 2000,
"uri": "https://example.com/1004/2"
},
{
"offset": 3000,
"uri": "https://example.com/1004/3"
},
{
"offset": 4000,
"uri": "https://example.com/1004/4"
},
{
"offset": 5000,
"uri": "https://example.com/1004/5"
},
{
"offset": 6000,
"uri": "https://example.com/1004/6"
},
{
"offset": 7000,
"uri": "https://example.com/1004/7"
}
],
"dkc_length": 70000,
"files": {
"dkc": "3500f58cfd1bd88e231edf56dca995542a702bd54525804e5a8604c8aa5cb52e",
"plaintext": "d79c80c116b31a31391070b573dfa5063d056c11f71c1d508318b8cc15545cf3",
"rest": "ed0d32149875c0e314ae3840561385ee2dbdc472c76383cf4e3f3b4d9f143971",
"sealed": "95658ed8c30cd6c7dfec5a523dd73891652437727950bd300b93dc38bf7c7bff"
},
"kind": "locator",
"name": "a locator of round 1004, a .dkc of 70000 bytes",
"round": 1004,
"seed": "interop locator 1004",
"verdict": "ok"
},
{
"addresses": [
{
"offset": 0,
"uri": "https://example.com/2000/0"
},
{
"offset": 1000,
"uri": "https://example.com/2000/1"
},
{
"offset": 2000,
"uri": "https://example.com/2000/2"
}
],
"dkc_length": 5000,
"files": {
"dkc": "1e92fd98f113aba0a78e0830ca06e2775912370feab112dfc57bf3258b810595",
"plaintext": "38d67b3d030237b57113a328f2b5f17d5de2de74086bf19458a59d502c86cd78",
"rest": "5f971c4e472ba62a97f373095bfc05e2df73c79fd2b8f15941c3480254be5a69",
"sealed": "6e90df800d2e87e824260be86214e7c78dd1bdf927afb461122475cc32d814e2"
},
"kind": "locator",
"name": "a locator of round 2000, a .dkc of 5000 bytes",
"round": 2000,
"seed": "interop locator 2000",
"verdict": "ok"
},
{
"files": {
"file": "6846a021b88a119849758a19bded28365178e7e454a1fc2b4126d53e6bd11dc8",
"public": "5c1ca6047ddd9b46b2f0b19178e8c823bd950314a46d3a3f4178ac770e4de282",
"secret": "9f7e372f8f01b0f11e596b86cc4bcd5f1504abb114f0606ad552292e909753fa"
},
"kind": "key",
"name": "a plain key file",
"passphrase": "",
"seed": "interop key 0",
"verdict": "ok"
},
{
"files": {
"file": "be15e0c2e993c420a142bcecb2f12e7f1b2f7ba342e6820aa73efe303c545b45",
"public": "0a4069069fc25231fdf99a58a018a6943b02feca3398234daaf72b468cda324e",
"secret": "8f93539516fee1db475a6140aa2998f55e9c8a3b092b7d36f3beff68ec7f0216"
},
"kind": "key",
"name": "a key file for \"una contraseña larga\"",
"passphrase": "una contraseña larga",
"seed": "interop key 1",
"verdict": "ok"
},
{
"files": {
"file": "6269546815904443defaf2318708a3bf3610096d974796ae3bad7b3a6a777b5c",
"public": "b100984ead5ef7f61d7fdea87aa023979cb70616b45c16ef209c8ccb5718f439",
"secret": "3c413240b32f18a3b19cd459dbf5817fa25f1b480c9c3024f4a915ce865095d0"
},
"kind": "key",
"name": "a key file for \"p\"",
"passphrase": "p",
"seed": "interop key 2",
"verdict": "ok"
},
{
"files": {
"public_key": "3358aa7e36687481caca1c7dba748e500027270dff4673d9bfd81c91c5e843ba",
"seed": "491bcbb989c198abd10c75735b79713dc24349e45ba4f0a38e61806658705d57",
"signature": "e48d354d1e7f700adf23309cd8362e6ba196d140e451c06b20d5716c77332c01"
},
"kind": "signature",
"message_length": 0,
"name": "a signature of 0 bytes",
"seed": "interop signature 0",
"verdict": "ok"
},
{
"files": {
"public_key": "9c89f174d6f8f51b9dab9c704dec08a8c8089e258b200c7414308987dc685120",
"seed": "e8d49bc3d8cc5fda1832038af5bf3e68342896b1eca47c4081050fff660cf3d3",
"signature": "a1d37f0a1c7226f4b9478cabf514c8259227e5a1463e071cb76186fdb46d7d53"
},
"kind": "signature",
"message_length": 1,
"name": "a signature of 1 bytes",
"seed": "interop signature 1",
"verdict": "ok"
},
{
"files": {
"public_key": "3cbb549de3322c657abf75294defa6066ca441c32f78ac9f572a0db9d9a43696",
"seed": "53f5981c9f6d59b59429e8514ab550ea8397f82337d9f636c6586e6c2b863a7b",
"signature": "be29ef4550df9f670600b67b2dddeee6d304880bb69f06d04a1a646d53622682"
},
"kind": "signature",
"message_length": 32,
"name": "a signature of 32 bytes",
"seed": "interop signature 32",
"verdict": "ok"
},
{
"files": {
"public_key": "c3a4aee3feb8b65efd92161742d5354b3dbe6f3650917805698cdb3465841063",
"seed": "ba581f8ed5f49daacc9380f581f81bdbf495e2c252d1aa717916a076ebb64a92",
"signature": "f795ea47916aa143e1125326c7b0ce1b9f5da3abd1cd94fb874c2348fe6eb749"
},
"kind": "signature",
"message_length": 64,
"name": "a signature of 64 bytes",
"seed": "interop signature 64",
"verdict": "ok"
},
{
"files": {
"public_key": "34050271dc91d14530f673194b3b73736404cdb0ef7373c479b18f6ea6ccc456",
"seed": "cf28e6c8097d2ec9a5088a64db27d76b046de4bdb4ca63c38ad07c956f7db754",
"signature": "28b48542cad3cde82f0628563ffa48a621cd30ca14b5e8129b28f709b1a215ad"
},
"kind": "signature",
"message_length": 99,
"name": "a signature of 99 bytes",
"seed": "interop signature 99",
"verdict": "ok"
},
{
"files": {
"public_key": "7156c4e431d88b8cd54046688402e958f050a9aa2b26fb3ccb7fcb2e30f84489",
"seed": "0c4a7e1f88ff11eed4484a86e669be7b67ccaed7a9c7d86a45f477d4e73fa950",
"signature": "0a975de1dad918529a515897093b2a797d9e33b9365ed69b1f008b7187607703"
},
"kind": "signature",
"message_length": 100,
"name": "a signature of 100 bytes",
"seed": "interop signature 100",
"verdict": "ok"
},
{
"files": {
"public_key": "7ddc85da4213d558f9aab9e9b525a7efc40a697ab9c510eb437ba31d984d89e9",
"seed": "ede75719e8b826c28d1086cfcddd85baeaed8ee804729744612649ad54a9adbb",
"signature": "cc628c04e47f0886670b5fbaeadc714dbd45aa5a16bea5d2e6905349fdc6938b"
},
"kind": "signature",
"message_length": 1000,
"name": "a signature of 1000 bytes",
"seed": "interop signature 1000",
"verdict": "ok"
},
{
"files": {
"public_key": "a60d104db6d021adf6562c4f748d63bf810a6f94cef6356422ccfa0df0666600",
"seed": "babbb81065bb332113a35e2f1fe48e8e1dd5ab15e125613f4348a244c33f07cf",
"signature": "d1980f717fb5c211d74ffdb16f590259db6302b59e814eed148b8752abcbed5d"
},
"kind": "signature",
"message_length": 70000,
"name": "a signature of 70000 bytes",
"seed": "interop signature 70000",
"verdict": "ok"
}
],
"source": "c531e936fa8188a4aab01575607cc85d4f5d4fa4"
}
''';

@ -0,0 +1,259 @@
{
"description": "What Go makes of the files that datekeys-dart writes in stage 7b with SeededRandomSource, from the recipes of test/seal_interop_support.dart, by tool/seal_interop_go_verdicts.go: the SHA-256 of each file and the verdict of Go, ok or the text of what failed.",
"go": "go1.26.8",
"samples": [
{
"addresses": [
{
"offset": 0,
"uri": "https://example.com/1000/0"
}
],
"dkc_length": 0,
"files": {
"dkc": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"plaintext": "40b0d932687eedbcd3d74ce5c2f78333ec6793dd84584ea8095ac36e4ac2306a",
"rest": "f934c42de6a3d9977f743f7f5372a433cd6f82360bd89b99dd08566133bb6e9c",
"sealed": "fe6a39326639e1396ac342f4f0ac0267b9830061c3ed4f4c33761ce78dcab679"
},
"kind": "locator",
"name": "a locator of round 1000, a .dkc of 0 bytes",
"round": 1000,
"seed": "interop locator 1000",
"verdict": "ok"
},
{
"addresses": [
{
"offset": 0,
"uri": "https://example.com/1001/0"
},
{
"offset": 1000,
"uri": "https://example.com/1001/1"
}
],
"dkc_length": 100,
"files": {
"dkc": "c22e490daa445fb2fba44278c022df135310fd278cabca4ad7919eddcccd1dce",
"plaintext": "b01f2323ec7f8bbfcca5062c0f5b64bc35a629dcf839cbfa31964aab0e78ddb2",
"rest": "f6fc60769fba93535b502e7e5590cfa7c17ea9721138b474c48911a0fb2fd72b",
"sealed": "07c736c85a761011fa564f3e16c549d6ec4c5d0ab1cb23884f288ef2714f8aab"
},
"kind": "locator",
"name": "a locator of round 1001, a .dkc of 100 bytes",
"round": 1001,
"seed": "interop locator 1001",
"verdict": "ok"
},
{
"addresses": [
{
"offset": 0,
"uri": "https://example.com/1004/0"
},
{
"offset": 1000,
"uri": "https://example.com/1004/1"
},
{
"offset": 2000,
"uri": "https://example.com/1004/2"
},
{
"offset": 3000,
"uri": "https://example.com/1004/3"
},
{
"offset": 4000,
"uri": "https://example.com/1004/4"
},
{
"offset": 5000,
"uri": "https://example.com/1004/5"
},
{
"offset": 6000,
"uri": "https://example.com/1004/6"
},
{
"offset": 7000,
"uri": "https://example.com/1004/7"
}
],
"dkc_length": 70000,
"files": {
"dkc": "3500f58cfd1bd88e231edf56dca995542a702bd54525804e5a8604c8aa5cb52e",
"plaintext": "d79c80c116b31a31391070b573dfa5063d056c11f71c1d508318b8cc15545cf3",
"rest": "ed0d32149875c0e314ae3840561385ee2dbdc472c76383cf4e3f3b4d9f143971",
"sealed": "95658ed8c30cd6c7dfec5a523dd73891652437727950bd300b93dc38bf7c7bff"
},
"kind": "locator",
"name": "a locator of round 1004, a .dkc of 70000 bytes",
"round": 1004,
"seed": "interop locator 1004",
"verdict": "ok"
},
{
"addresses": [
{
"offset": 0,
"uri": "https://example.com/2000/0"
},
{
"offset": 1000,
"uri": "https://example.com/2000/1"
},
{
"offset": 2000,
"uri": "https://example.com/2000/2"
}
],
"dkc_length": 5000,
"files": {
"dkc": "1e92fd98f113aba0a78e0830ca06e2775912370feab112dfc57bf3258b810595",
"plaintext": "38d67b3d030237b57113a328f2b5f17d5de2de74086bf19458a59d502c86cd78",
"rest": "5f971c4e472ba62a97f373095bfc05e2df73c79fd2b8f15941c3480254be5a69",
"sealed": "6e90df800d2e87e824260be86214e7c78dd1bdf927afb461122475cc32d814e2"
},
"kind": "locator",
"name": "a locator of round 2000, a .dkc of 5000 bytes",
"round": 2000,
"seed": "interop locator 2000",
"verdict": "ok"
},
{
"files": {
"file": "6846a021b88a119849758a19bded28365178e7e454a1fc2b4126d53e6bd11dc8",
"public": "5c1ca6047ddd9b46b2f0b19178e8c823bd950314a46d3a3f4178ac770e4de282",
"secret": "9f7e372f8f01b0f11e596b86cc4bcd5f1504abb114f0606ad552292e909753fa"
},
"kind": "key",
"name": "a plain key file",
"passphrase": "",
"seed": "interop key 0",
"verdict": "ok"
},
{
"files": {
"file": "be15e0c2e993c420a142bcecb2f12e7f1b2f7ba342e6820aa73efe303c545b45",
"public": "0a4069069fc25231fdf99a58a018a6943b02feca3398234daaf72b468cda324e",
"secret": "8f93539516fee1db475a6140aa2998f55e9c8a3b092b7d36f3beff68ec7f0216"
},
"kind": "key",
"name": "a key file for \"una contraseña larga\"",
"passphrase": "una contraseña larga",
"seed": "interop key 1",
"verdict": "ok"
},
{
"files": {
"file": "6269546815904443defaf2318708a3bf3610096d974796ae3bad7b3a6a777b5c",
"public": "b100984ead5ef7f61d7fdea87aa023979cb70616b45c16ef209c8ccb5718f439",
"secret": "3c413240b32f18a3b19cd459dbf5817fa25f1b480c9c3024f4a915ce865095d0"
},
"kind": "key",
"name": "a key file for \"p\"",
"passphrase": "p",
"seed": "interop key 2",
"verdict": "ok"
},
{
"files": {
"public_key": "3358aa7e36687481caca1c7dba748e500027270dff4673d9bfd81c91c5e843ba",
"seed": "491bcbb989c198abd10c75735b79713dc24349e45ba4f0a38e61806658705d57",
"signature": "e48d354d1e7f700adf23309cd8362e6ba196d140e451c06b20d5716c77332c01"
},
"kind": "signature",
"message_length": 0,
"name": "a signature of 0 bytes",
"seed": "interop signature 0",
"verdict": "ok"
},
{
"files": {
"public_key": "9c89f174d6f8f51b9dab9c704dec08a8c8089e258b200c7414308987dc685120",
"seed": "e8d49bc3d8cc5fda1832038af5bf3e68342896b1eca47c4081050fff660cf3d3",
"signature": "a1d37f0a1c7226f4b9478cabf514c8259227e5a1463e071cb76186fdb46d7d53"
},
"kind": "signature",
"message_length": 1,
"name": "a signature of 1 bytes",
"seed": "interop signature 1",
"verdict": "ok"
},
{
"files": {
"public_key": "3cbb549de3322c657abf75294defa6066ca441c32f78ac9f572a0db9d9a43696",
"seed": "53f5981c9f6d59b59429e8514ab550ea8397f82337d9f636c6586e6c2b863a7b",
"signature": "be29ef4550df9f670600b67b2dddeee6d304880bb69f06d04a1a646d53622682"
},
"kind": "signature",
"message_length": 32,
"name": "a signature of 32 bytes",
"seed": "interop signature 32",
"verdict": "ok"
},
{
"files": {
"public_key": "c3a4aee3feb8b65efd92161742d5354b3dbe6f3650917805698cdb3465841063",
"seed": "ba581f8ed5f49daacc9380f581f81bdbf495e2c252d1aa717916a076ebb64a92",
"signature": "f795ea47916aa143e1125326c7b0ce1b9f5da3abd1cd94fb874c2348fe6eb749"
},
"kind": "signature",
"message_length": 64,
"name": "a signature of 64 bytes",
"seed": "interop signature 64",
"verdict": "ok"
},
{
"files": {
"public_key": "34050271dc91d14530f673194b3b73736404cdb0ef7373c479b18f6ea6ccc456",
"seed": "cf28e6c8097d2ec9a5088a64db27d76b046de4bdb4ca63c38ad07c956f7db754",
"signature": "28b48542cad3cde82f0628563ffa48a621cd30ca14b5e8129b28f709b1a215ad"
},
"kind": "signature",
"message_length": 99,
"name": "a signature of 99 bytes",
"seed": "interop signature 99",
"verdict": "ok"
},
{
"files": {
"public_key": "7156c4e431d88b8cd54046688402e958f050a9aa2b26fb3ccb7fcb2e30f84489",
"seed": "0c4a7e1f88ff11eed4484a86e669be7b67ccaed7a9c7d86a45f477d4e73fa950",
"signature": "0a975de1dad918529a515897093b2a797d9e33b9365ed69b1f008b7187607703"
},
"kind": "signature",
"message_length": 100,
"name": "a signature of 100 bytes",
"seed": "interop signature 100",
"verdict": "ok"
},
{
"files": {
"public_key": "7ddc85da4213d558f9aab9e9b525a7efc40a697ab9c510eb437ba31d984d89e9",
"seed": "ede75719e8b826c28d1086cfcddd85baeaed8ee804729744612649ad54a9adbb",
"signature": "cc628c04e47f0886670b5fbaeadc714dbd45aa5a16bea5d2e6905349fdc6938b"
},
"kind": "signature",
"message_length": 1000,
"name": "a signature of 1000 bytes",
"seed": "interop signature 1000",
"verdict": "ok"
},
{
"files": {
"public_key": "a60d104db6d021adf6562c4f748d63bf810a6f94cef6356422ccfa0df0666600",
"seed": "babbb81065bb332113a35e2f1fe48e8e1dd5ab15e125613f4348a244c33f07cf",
"signature": "d1980f717fb5c211d74ffdb16f590259db6302b59e814eed148b8752abcbed5d"
},
"kind": "signature",
"message_length": 70000,
"name": "a signature of 70000 bytes",
"seed": "interop signature 70000",
"verdict": "ok"
}
],
"source": "c531e936fa8188a4aab01575607cc85d4f5d4fa4"
}

@ -0,0 +1,400 @@
//go:build ignore
// Writes test/vectors/locator_seal.json and locator_seal.g.dart, the
// vectors of the sealing of the locator and of the envelope of
// datekeys-dart, stage 7b of docs/PLAN_dart.md: Seal and NewEnvelope of the
// package locator of datekeys-go, while crypto/rand reads the keystream of
// SeededRandomSource of lib/src/random.dart (ChaCha20 under SHA-256(seed),
// zero nonce), with each draw, so that the writer of datekeys-dart, with
// the same seed, must write the same bytes:
//
// - seal: Seal of locators of 1 to 8 addresses, with offsets and headers
// of 1 to 1024 bytes, whose plaintext takes one, two or three blocks of
// 4096 bytes, for rounds from 1 to the last one of Quicknet. Go opens
// each with Open and the published release of its round when the
// fixtures have it, 1000, 1001, 1004 or 2000, and gets the locator
// back. A small file is stored whole; every file with its length and
// SHA-256;
// - seal_errors: what Seal refuses, with its text: locators that Marshal
// refuses, rounds out of the range of Quicknet, and both at once,
// where Marshal goes first;
// - envelope: NewEnvelope of .dkc of 0 bytes to 1 MiB: the
// addresses, and the rest, whole when
// small, with its length and SHA-256. Go opens each with OpenEnvelope;
// - flow: NewEnvelope, the addresses, Seal, Open and OpenEnvelope in one
// seed, as a writer and a reader do.
//
// The plaintext of a .dkc of n bytes has (31·i + 7) mod 256 as byte i.
//
// It imports only public packages, so it runs in the module of the
// reference implementation, without changing anything there, from the
// datekeys-go next to this repository, on the branch v0.12 at c531e93:
//
// cd ../datekeys-go && go run ../datekeys-dart/tool/locator_seal_go_vectors.go \
// -source $(git rev-parse v0.12) -testdata ../datekeys-dart/testdata \
// -out ../datekeys-dart/test/vectors
//
// The output is the same on every run.
package main
import (
"bytes"
cryptorand "crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"slices"
"strings"
"golang.org/x/crypto/chacha20"
"g.activething.com/go/DateKeys/locator"
"g.activething.com/go/DateKeys/profile"
"g.activething.com/go/DateKeys/provider"
)
type obj = map[string]any
func h(b []byte) string { return hex.EncodeToString(b) }
func sum(b []byte) string {
s := sha256.Sum256(b)
return h(s[:])
}
func check(err error) {
if err != nil {
_, file, line, _ := runtime.Caller(1)
log.Fatalf("%s:%d: %v", filepath.Base(file), line, err)
}
}
func mustHex(s string) []byte {
b, err := hex.DecodeString(s)
check(err)
return b
}
func label(s string) []byte {
b := sha256.Sum256([]byte("datekeys-dart stage 7b locator: " + s))
return b[:]
}
func pattern(n int) []byte {
b := make([]byte, n)
for i := range b {
b[i] = byte(31*i + 7)
}
return b
}
// ---------------------------------------------------------------------------
// crypto/rand from a seed, as in tool/age_writer_go_vectors.go
type seeded struct {
c *chacha20.Cipher
draws [][]byte
}
func (s *seeded) Read(p []byte) (int, error) {
clear(p)
s.c.XORKeyStream(p, p)
s.draws = append(s.draws, bytes.Clone(p))
return len(p), nil
}
func with(seed string, f func()) [][]byte {
key := sha256.Sum256([]byte(seed))
c, err := chacha20.NewUnauthenticatedCipher(key[:], make([]byte, chacha20.NonceSize))
check(err)
s := &seeded{c: c}
old := cryptorand.Reader
cryptorand.Reader = s
defer func() { cryptorand.Reader = old }()
f()
return s.draws
}
func drawsOf(d [][]byte) []obj {
out := []obj{}
for _, b := range d {
out = append(out, obj{"n": len(b), "hex": h(b)})
}
return out
}
// small is the largest file stored whole.
const small = 16 << 10
func fileObj(b []byte) obj {
o := obj{"length": len(b), "sha256": sum(b)}
if len(b) <= small {
o["hex"] = h(b)
}
return o
}
func locObj(l *locator.Locator) obj {
addrs := []obj{}
for _, a := range l.Addresses {
addrs = append(addrs, obj{"uri": a.URI, "offset": a.Offset})
}
return obj{"addresses": addrs, "envelope_key": h(l.EnvelopeKey[:]), "envelope_header": h(l.EnvelopeHeader), "rest_digest": h(l.RestDigest[:]), "rest_size": l.RestSize, "capsule_digest": h(l.CapsuleDigest[:])}
}
// newLoc is a locator of n addresses, the i-th of uri length about
// uriLen, with offsets, and a header of headerLen bytes.
func newLoc(name string, n, uriLen, headerLen int, offsets bool) *locator.Locator {
l := &locator.Locator{EnvelopeHeader: label(name + " header")}
for len(l.EnvelopeHeader) < headerLen {
l.EnvelopeHeader = append(l.EnvelopeHeader, label(fmt.Sprintf("%s header %d", name, len(l.EnvelopeHeader)))...)
}
l.EnvelopeHeader = l.EnvelopeHeader[:headerLen]
copy(l.EnvelopeKey[:], label(name+" key"))
copy(l.RestDigest[:], label(name+" rest"))
copy(l.CapsuleDigest[:], label(name+" capsule"))
l.RestSize = uint64(len(name)) * 1000003
for i := 0; i < n; i++ {
uri := fmt.Sprintf("https://example.com/%s/%d/", strings.ReplaceAll(name, " ", "-"), i)
for len(uri) < uriLen {
uri += "a"
}
var off uint64
if offsets && i%2 == 1 {
off = uint64(i) * 4099
}
l.Addresses = append(l.Addresses, locator.Address{URI: uri, Offset: off})
}
return l
}
// ---------------------------------------------------------------------------
var releases map[uint64]provider.Release
// readReleases reads the releases of the fixtures, public data of drand.
func readReleases(testdata string) map[uint64]provider.Release {
out := map[uint64]provider.Release{}
files, err := filepath.Glob(filepath.Join(testdata, "fixtures", "*.json"))
check(err)
slices.Sort(files)
for _, f := range files {
raw, err := os.ReadFile(f)
check(err)
var v struct {
Release *struct {
Round uint64 `json:"round"`
Signature string `json:"signature"`
} `json:"release"`
}
if err := json.Unmarshal(raw, &v); err != nil || v.Release == nil {
continue
}
out[v.Release.Round] = provider.Release{Round: v.Release.Round, Signature: mustHex(v.Release.Signature)}
}
return out
}
// opens reports whether Go opens sealed for round and gets l back; null
// when no release of the round is known.
func opens(p *profile.Profile, round uint64, l *locator.Locator, sealed []byte) any {
rel, ok := releases[round]
if !ok {
return nil
}
got, err := locator.Open(p, round, rel, sealed)
check(err)
a, err := l.Marshal()
check(err)
b, err := got.Marshal()
check(err)
if !bytes.Equal(a, b) {
log.Fatalf("round %d: Open gives another locator", round)
}
return true
}
func sealSection() []obj {
p := profile.Quicknet()
type c struct {
name string
round uint64
loc *locator.Locator
}
cases := []c{
{"one address", 1000, newLoc("one address", 1, 30, 200, false)},
{"two addresses with offsets", 1001, newLoc("two addresses", 2, 60, 300, true)},
{"eight addresses, two blocks", 2000, newLoc("eight addresses", 8, 500, 1024, true)},
{"eight long addresses, three blocks", 1004, newLoc("eight long", 8, 1024, 1024, true)},
{"a header of one byte", 1000, newLoc("one byte", 1, 20, 1, false)},
{"round 1", 1, newLoc("round 1", 3, 100, 500, true)},
{"the last round of Quicknet", p.MaxRound(), newLoc("last round", 1, 40, 900, false)},
{"round 12345678901", 12345678901, newLoc("eleven digits", 4, 400, 700, true)},
}
// The plaintext of one block exactly, and of one byte more before key 6.
for _, cut := range []int{1, 0} {
l := newLoc("edge", 4, 750, 100, false)
for {
pt, err := l.Marshal()
check(err)
if len(pt) > 4096 {
break
}
l.EnvelopeHeader = append(l.EnvelopeHeader, 'h')
}
l.EnvelopeHeader = l.EnvelopeHeader[:len(l.EnvelopeHeader)-cut]
cases = append(cases, c{[]string{"a block and one byte", "one block exactly"}[cut], 1000, l})
}
out := []obj{}
for _, k := range cases {
seed := "locator seal " + k.name
var sealed []byte
d := with(seed, func() {
var err error
sealed, err = locator.Seal(p, k.round, k.loc)
check(err)
})
pt, err := k.loc.Marshal()
check(err)
o := obj{"name": k.name, "seed": seed, "round": k.round, "locator": locObj(k.loc), "plaintext_length": len(pt), "draws": len(d), "sealed": fileObj(sealed), "opens": opens(p, k.round, k.loc, sealed)}
if len(d) > 0 {
o["first_draw"] = drawsOf(d[:1])[0]
}
out = append(out, o)
}
return out
}
func sealErrorsSection() []obj {
p := profile.Quicknet()
out := []obj{}
add := func(name string, round uint64, l *locator.Locator) {
var err error
d := with("locator seal error "+name, func() { _, err = locator.Seal(p, round, l) })
if err == nil {
log.Fatalf("%s: no error", name)
}
out = append(out, obj{"name": name, "round": round, "locator": locObj(l), "error": err.Error(), "draws": len(d)})
}
good := newLoc("good", 1, 30, 100, false)
add("round 0", 0, good)
add("a round after the last one", p.MaxRound()+1, good)
none := newLoc("none", 0, 0, 100, false)
add("no address", 1000, none)
add("no address and round 0", 0, none)
add("nine addresses", 1000, newLoc("nine", 9, 30, 100, false))
add("an address of 1025 bytes", 1000, newLoc("long uri", 1, 1025, 100, false))
add("an empty header", 1000, newLoc("empty header", 1, 30, 0, false))
add("a header of 1025 bytes", 1000, newLoc("long header", 1, 30, 1025, false))
bad := newLoc("bad", 2, 30, 100, false)
bad.Addresses[1].URI = "http://example.com/"
add("an address of http", 1000, bad)
bad2 := newLoc("bad2", 1, 30, 100, false)
bad2.Addresses[0].URI = "https://192.168.1.1/x"
add("a private address", 1000, bad2)
add("a private address and round 0", 0, bad2)
big := newLoc("big", 1, 30, 100, false)
big.RestSize = 1 << 53
add("a rest of 2^53 bytes", 1000, big)
return out
}
func envelopeSection() []obj {
out := []obj{}
for _, n := range []int{0, 1, 1000, 65535, 65536, 65537, 200000, 1 << 20} {
seed := fmt.Sprintf("locator envelope %d", n)
dkc := pattern(n)
var loc *locator.Locator
var rest []byte
d := with(seed, func() {
var err error
loc, rest, err = locator.NewEnvelope(dkc)
check(err)
})
back, err := loc.OpenEnvelope(rest)
check(err)
if !bytes.Equal(back, dkc) {
log.Fatal("OpenEnvelope")
}
out = append(out, obj{"seed": seed, "dkc_length": n, "locator": locObj(loc), "rest": fileObj(rest), "draws": drawsOf(d[:1]), "node": n <= 65537})
}
return out
}
func flowSection() obj {
p := profile.Quicknet()
seed := "locator flow"
dkc := pattern(5000)
var sealed, rest, file []byte
var offset uint64
var loc *locator.Locator
d := with(seed, func() {
var err error
loc, rest, err = locator.NewEnvelope(dkc)
check(err)
file, offset = locator.Hide([]byte("GIF89a, a host of some kind"), rest)
loc.Addresses = []locator.Address{{URI: "https://example.com/capsule"}, {URI: "https://example.org/host.gif", Offset: offset}}
sealed, err = locator.Seal(p, 1000, loc)
check(err)
})
got, err := locator.Open(p, 1000, releases[1000], sealed)
check(err)
r, err := got.RestIn(file, got.Addresses[1].Offset)
check(err)
back, err := got.OpenEnvelope(r)
check(err)
if !bytes.Equal(back, dkc) {
log.Fatal("the flow")
}
return obj{"seed": seed, "dkc_length": len(dkc), "host": h([]byte("GIF89a, a host of some kind")), "round": 1000, "draws": len(d), "locator": locObj(loc), "rest": fileObj(rest), "sealed": fileObj(sealed)}
}
func main() {
out := flag.String("out", "", "where the vectors go")
src := flag.String("source", "", "the commit of datekeys-go")
testdata := flag.String("testdata", "", "the testdata of this repository")
flag.Parse()
if *out == "" || *src == "" || *testdata == "" {
log.Fatal("usage: -source <commit> -testdata <dir> -out <dir>")
}
releases = readReleases(*testdata)
rel := obj{}
for r, v := range releases {
rel[fmt.Sprint(r)] = h(v.Signature)
}
doc := obj{
"source": *src,
"go": runtime.Version(),
"description": "Seal and NewEnvelope of package locator while crypto/rand reads the keystream of SeededRandomSource (ChaCha20 under SHA-256(seed), zero nonce), by tool/locator_seal_go_vectors.go. A file is {length, sha256} and its hex when it is small. The .dkc of n bytes has (31·i + 7) mod 256 as byte i. opens is true when Go opened the sealed locator with the release of its round, which releases holds, and null when no release is known. A case marked node false is left out compiled to JavaScript.",
"releases": rel,
"seal": sealSection(),
"seal_errors": sealErrorsSection(),
"envelope": envelopeSection(),
"flow": flowSection(),
}
var buf bytes.Buffer
e := json.NewEncoder(&buf)
e.SetEscapeHTML(false)
e.SetIndent("", " ")
check(e.Encode(doc))
if bytes.Contains(buf.Bytes(), []byte("'''")) {
log.Fatal("the JSON holds three quotes")
}
path := filepath.Join(*out, "locator_seal.json")
check(os.WriteFile(path, buf.Bytes(), 0o644))
fmt.Printf("wrote %s, %d bytes\n", path, buf.Len())
dart := "// Generated by tool/locator_seal_go_vectors.go from locator_seal.json, for\n" +
"// the tests that also run compiled to JavaScript, where no file can be\n" +
"// read. Do not edit.\n\n" +
"/// The text of test/vectors/locator_seal.json.\n" +
"const locatorSealJson = r'''\n" + buf.String() + "''';\n"
dpath := filepath.Join(*out, "locator_seal.g.dart")
check(os.WriteFile(dpath, []byte(dart), 0o644))
fmt.Printf("wrote %s, %d bytes\n", dpath, len(dart))
}

@ -0,0 +1,35 @@
// Writes the samples of the interoperability of stage 7b, Dart to Go: the
// files of the recipes of test/seal_interop_support.dart, sealed locators
// with their envelopes, author key files and Ed25519 signatures, which this
// library writes with SeededRandomSource, and samples.json, the recipes,
// into a directory. tool/seal_interop_go_verdicts.go then opens them with
// Go and writes test/vectors/seal_interop.json; the tests write the files
// again from the recipes and compare them with what Go read.
//
// dart run tool/seal_interop_dart_samples.dart <directory>
//
// ignore_for_file: avoid_print
import 'dart:convert';
import 'dart:io';
import '../test/seal_interop_support.dart';
void main(List<String> args) {
if (args.length != 1) {
stderr.writeln('usage: dart run tool/seal_interop_dart_samples.dart <dir>');
exit(2);
}
final dir = Directory(args.single)..createSync(recursive: true);
final recipes = interopRecipes();
for (var i = 0; i < recipes.length; i++) {
final files = interopFiles(recipes[i]);
for (final e in files.entries) {
File('${dir.path}/$i.${e.key}').writeAsBytesSync(e.value);
}
print('${recipes[i]['name']}: ${files.keys.join(', ')}');
}
File('${dir.path}/samples.json').writeAsStringSync(
const JsonEncoder.withIndent(' ').convert({'samples': recipes}),
);
print('wrote ${recipes.length} samples to ${dir.path}');
}

@ -0,0 +1,215 @@
//go:build ignore
// Opens with Go what datekeys-dart writes in stage 7b of docs/PLAN_dart.md,
// and writes test/vectors/seal_interop.json and seal_interop.g.dart: each
// recipe of tool/seal_interop_dart_samples.dart with the length and the
// SHA-256 of each of its files and the verdict of Go:
//
// - locator: locator.Open of the sealed locator with Quicknet, its round
// and the published release of the round, from the fixtures; Marshal
// of the locator that Open gives, which must be the plaintext that
// Dart sealed; and OpenEnvelope of the rest, which must give the .dkc;
// - key: authorkey.Read of the key file with its passphrase, which must
// give the secret key that Dart wrote, and ParsePublic of its public
// key;
// - signature: authorkey.ParseSecret of the key, ed25519.Sign of the
// message with it, which must be the signature that Dart wrote, and
// ed25519.Verify.
//
// A verdict is "ok" or the text of the first thing that failed.
//
// It imports only public packages, so it runs in the module of the
// reference implementation, without changing anything there, after the
// Dart tool wrote the samples:
//
// tmp=$(mktemp -d)
// dart run tool/seal_interop_dart_samples.dart "$tmp"
// (cd ../datekeys-go && go run ../datekeys-dart/tool/seal_interop_go_verdicts.go \
// -source $(git rev-parse v0.12) -testdata ../datekeys-dart/testdata \
// -samples "$tmp" -out ../datekeys-dart/test/vectors)
// rm -rf "$tmp"
package main
import (
"bytes"
"crypto/ed25519"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"g.activething.com/go/DateKeys/authorkey"
"g.activething.com/go/DateKeys/locator"
"g.activething.com/go/DateKeys/profile"
"g.activething.com/go/DateKeys/provider"
)
type obj = map[string]any
func h(b []byte) string { return hex.EncodeToString(b) }
func check(err error) {
if err != nil {
_, file, line, _ := runtime.Caller(1)
log.Fatalf("%s:%d: %v", filepath.Base(file), line, err)
}
}
func pattern(n int) []byte {
b := make([]byte, n)
for i := range b {
b[i] = byte(31*i + 7)
}
return b
}
func releases(testdata string) map[uint64]provider.Release {
out := map[uint64]provider.Release{}
files, err := filepath.Glob(filepath.Join(testdata, "fixtures", "*.json"))
check(err)
sort.Strings(files)
for _, f := range files {
raw, err := os.ReadFile(f)
check(err)
var v struct {
Release *struct {
Round uint64 `json:"round"`
Signature string `json:"signature"`
} `json:"release"`
}
if err := json.Unmarshal(raw, &v); err != nil || v.Release == nil {
continue
}
sig, err := hex.DecodeString(v.Release.Signature)
check(err)
out[v.Release.Round] = provider.Release{Round: v.Release.Round, Signature: sig}
}
return out
}
func verdict(r obj, files map[string][]byte, rel map[uint64]provider.Release) error {
switch r["kind"] {
case "locator":
round := uint64(r["round"].(float64))
release, ok := rel[round]
if !ok {
return fmt.Errorf("no release of round %d", round)
}
loc, err := locator.Open(profile.Quicknet(), round, release, files["sealed"])
if err != nil {
return err
}
pt, err := loc.Marshal()
if err != nil {
return err
}
if !bytes.Equal(pt, files["plaintext"]) {
return errors.New("another plaintext")
}
dkc, err := loc.OpenEnvelope(files["rest"])
if err != nil {
return err
}
if !bytes.Equal(dkc, files["dkc"]) || !bytes.Equal(dkc, pattern(int(r["dkc_length"].(float64)))) {
return errors.New("another .dkc")
}
return nil
case "key":
k, err := authorkey.Read(bytes.NewReader(files["file"]), r["passphrase"].(string))
if err != nil {
return err
}
if k.Secret() != string(files["secret"]) {
return errors.New("another secret key")
}
pub, err := authorkey.ParsePublic(string(files["public"]))
if err != nil {
return err
}
if !bytes.Equal(pub, k.Public()) {
return errors.New("another public key")
}
return nil
default:
k, err := authorkey.ParseSecret(string(files["seed"]))
if err != nil {
return err
}
msg := pattern(int(r["message_length"].(float64)))
if !bytes.Equal(k.Public(), files["public_key"]) {
return errors.New("another public key")
}
if !bytes.Equal(k.Sign(msg), files["signature"]) {
return errors.New("another signature")
}
if !ed25519.Verify(files["public_key"], msg, files["signature"]) {
return errors.New("the signature does not verify")
}
return nil
}
}
func main() {
samples := flag.String("samples", "", "the directory of the samples")
out := flag.String("out", "", "where the vectors go")
src := flag.String("source", "", "the commit of datekeys-go")
testdata := flag.String("testdata", "", "the testdata of this repository")
flag.Parse()
if *samples == "" || *out == "" || *src == "" || *testdata == "" {
log.Fatal("usage: -source <commit> -testdata <dir> -samples <dir> -out <dir>")
}
rel := releases(*testdata)
raw, err := os.ReadFile(filepath.Join(*samples, "samples.json"))
check(err)
var doc struct {
Samples []obj `json:"samples"`
}
check(json.Unmarshal(raw, &doc))
result := []obj{}
for i, r := range doc.Samples {
names, err := filepath.Glob(filepath.Join(*samples, fmt.Sprintf("%d.*", i)))
check(err)
files := map[string][]byte{}
digests := obj{}
for _, n := range names {
b, err := os.ReadFile(n)
check(err)
suffix := strings.TrimPrefix(filepath.Ext(n), ".")
files[suffix] = b
s := sha256.Sum256(b)
digests[suffix] = h(s[:])
}
v := "ok"
if err := verdict(r, files, rel); err != nil {
v = err.Error()
log.Printf("%s: %s", r["name"], v)
}
r["files"] = digests
r["verdict"] = v
result = append(result, r)
}
var buf bytes.Buffer
e := json.NewEncoder(&buf)
e.SetEscapeHTML(false)
e.SetIndent("", " ")
check(e.Encode(obj{"source": *src, "go": runtime.Version(), "description": "What Go makes of the files that datekeys-dart writes in stage 7b with SeededRandomSource, from the recipes of test/seal_interop_support.dart, by tool/seal_interop_go_verdicts.go: the SHA-256 of each file and the verdict of Go, ok or the text of what failed.", "samples": result}))
path := filepath.Join(*out, "seal_interop.json")
check(os.WriteFile(path, buf.Bytes(), 0o644))
fmt.Printf("wrote %s, %d bytes\n", path, buf.Len())
dart := "// Generated by tool/seal_interop_go_verdicts.go from seal_interop.json, for\n" +
"// the tests that also run compiled to JavaScript, where no file can be\n" +
"// read. Do not edit.\n\n" +
"/// The text of test/vectors/seal_interop.json.\n" +
"const sealInteropJson = r'''\n" + buf.String() + "''';\n"
dpath := filepath.Join(*out, "seal_interop.g.dart")
check(os.WriteFile(dpath, []byte(dart), 0o644))
fmt.Printf("wrote %s, %d bytes\n", dpath, len(dart))
}
Loading…
Cancel
Save

Powered by TurnKey Linux.