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.
401 lines
13 KiB
401 lines
13 KiB
//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))
|
|
}
|