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.
389 lines
13 KiB
389 lines
13 KiB
//go:build ignore
|
|
|
|
// Prints src/lib/dkc/testing/locator-seal.json, the vectors of the sealing
|
|
// of the locator and of the envelope of datekeys-ts (envelope.ts): Seal and
|
|
// NewEnvelope of package locator of datekeys-go at the tag spec-v0.12, while
|
|
// crypto/rand reads the keystream of a seed (ChaCha20 under SHA-256(seed),
|
|
// zero nonce, as seededFill of src/lib/dkc/testing/seeded.ts), with each
|
|
// draw, so that seal and newEnvelope, with the same seed, must draw the same
|
|
// values in the same order and 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 is
|
|
// the generator of the stage 7b of datekeys-dart, with the seeds of this
|
|
// repository.
|
|
//
|
|
// It imports only public packages of the reference implementation, and runs
|
|
// in a module of it without changing anything there: an export of the tag
|
|
// spec-v0.12, so that no change in progress in datekeys-go is read.
|
|
//
|
|
// git -C ../datekeys-go archive spec-v0.12 | tar -x -C /tmp/dkgo
|
|
// (cd /tmp/dkgo && go run .../datekeys-ts/scripts/locator-seal-go-vectors.go \
|
|
// -source spec-v0.12 -testdata .../datekeys-ts/testdata -out .../datekeys-ts/src/lib/dkc/testing)
|
|
//
|
|
// 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-ts locator seal: " + 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
|
|
|
|
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 := "datekeys-ts 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": drawsOf(d), "sealed": fileObj(sealed), "opens": opens(p, k.round, k.loc, sealed)}
|
|
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("datekeys-ts 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("datekeys-ts 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)})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func flowSection() obj {
|
|
p := profile.Quicknet()
|
|
seed := "datekeys-ts 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": drawsOf(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), as seededFill of src/lib/dkc/testing/seeded.ts, by scripts/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. draws lists every value that crypto/rand gave, in order.",
|
|
"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))
|
|
path := filepath.Join(*out, "locator-seal.json")
|
|
check(os.WriteFile(path, buf.Bytes(), 0o644))
|
|
fmt.Printf("wrote %s, %d bytes\n", path, buf.Len())
|
|
}
|