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/tool/locator_seal_go_vectors.go

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))
}

Powered by TurnKey Linux.