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.
2529 lines
87 KiB
2529 lines
87 KiB
//go:build ignore
|
|
|
|
// Writes the vectors of the locator of datekeys-dart, stage 7a of
|
|
// docs/PLAN_dart.md: the results, the normative codes and the texts of
|
|
// package locator of datekeys-go at the draft v0.12 (spec §43 to §44.1), and
|
|
// of the parser of IP addresses of Go that it calls, on
|
|
//
|
|
// - locator_uris.json: CheckURI and Address.Host on the 247 addresses of
|
|
// testdata/vectors/locator.json, and on addresses built at every edge of
|
|
// §44.1 and drawn from a fixed seed: IPv4 and IPv6 in every notation
|
|
// that netip.ParseAddr accepts or rejects, with zones, mapped,
|
|
// compatible, of NAT64, 6to4 and Teredo, the first and the last address
|
|
// of every block of IANA and their neighbours, names with long and
|
|
// punycode labels, local names in any case, ports, percent-encodings,
|
|
// dot segments and CIDs, canonical or not; CheckURI on bytes with the
|
|
// three bytes of a surrogate, those of a String of Dart with a lone
|
|
// surrogate; netip.ParseAddr itself, with the address, its zone and its
|
|
// String; and locator.publicIP on the bytes of an address, as a reader
|
|
// checks the IP that a name resolves to on every connection. publicIP
|
|
// is not exported: this program reaches it with go:linkname, which Go
|
|
// allows for a package outside the standard library.
|
|
// - locator_vectors.json: the texts of the other cases of
|
|
// testdata/vectors/locator.json; PlaintextLength on every base from
|
|
// -4100 to 16484, in runs; Unmarshal on plaintexts of three bases, valid
|
|
// and broken byte by byte and field by field; Marshal on locators at
|
|
// every limit and at every boundary of the padding; Open on sealed
|
|
// locators of four rounds, with the releases of the others, broken
|
|
// profiles and edited files; Open on files whose plaintext passes 1 MiB,
|
|
// which Go reads through io.LimitReader, written here with a file key
|
|
// that this program knows; OpenEnvelope, RestIn and Hide on envelopes
|
|
// and rests, valid and edited; the split of an age file into the header
|
|
// and the rest of an envelope, as NewEnvelope does it (headerEnd,
|
|
// reached with go:linkname too); Info.Extension, Info.OpenLocator with
|
|
// the default registry, and ParseInfo; the registry of locator.Standard
|
|
// through CheckNoncriticalIn, CheckCriticalIn and CheckWrite; and
|
|
// capsule.Open of a fixture with its .dkk carrying datekeys.capsule,
|
|
// with locator.Standard.
|
|
//
|
|
// It also writes the Dart constants of locator_uris.g.dart, the whole of
|
|
// locator_uris.json, and of locator_vectors.g.dart, the part of
|
|
// locator_vectors.json that the tests compiled to JavaScript need, where no
|
|
// file can be read. A test on the VM checks that they are those of the
|
|
// files.
|
|
//
|
|
// Every expected value is what Go gives; none is written by hand. The
|
|
// randomness of age, tlock and NewEnvelope comes from crypto/rand.Reader,
|
|
// which this program replaces with a ChaCha8 of a fixed seed, and every
|
|
// other choice from another: every run writes the same bytes with Go 1.26.8.
|
|
// Binary values are lower-case hexadecimal, and a text is an index into
|
|
// texts, whose first entry, "", stands for no error. An edited value is a
|
|
// list of edits of a base, as in "Edited files" of testdata/README.md, with
|
|
// one more form: [at, 0, byte, count] inserts count copies of the byte. An
|
|
// address with a run of 32 or more equal bytes is written [before, byte,
|
|
// count, after]. The JSON is ASCII.
|
|
//
|
|
// Run it in the module of the reference implementation, which it imports,
|
|
// 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_go_vectors.go \
|
|
// -testdata ../datekeys-dart/testdata -out ../datekeys-dart/test/vectors
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/hkdf"
|
|
cryptorand "crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base32"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"math/rand/v2"
|
|
"net/netip"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf16"
|
|
"unicode/utf8"
|
|
_ "unsafe"
|
|
|
|
"filippo.io/age"
|
|
"golang.org/x/crypto/chacha20poly1305"
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
"g.activething.com/go/DateKeys/accesskey"
|
|
"g.activething.com/go/DateKeys/agewrap"
|
|
"g.activething.com/go/DateKeys/capsule"
|
|
"g.activething.com/go/DateKeys/datekey"
|
|
"g.activething.com/go/DateKeys/extension"
|
|
"g.activething.com/go/DateKeys/locator"
|
|
"g.activething.com/go/DateKeys/profile"
|
|
"g.activething.com/go/DateKeys/provider"
|
|
)
|
|
|
|
//go:linkname publicIP g.activething.com/go/DateKeys/locator.publicIP
|
|
func publicIP(a netip.Addr) bool
|
|
|
|
//go:linkname headerEnd g.activething.com/go/DateKeys/locator.headerEnd
|
|
func headerEnd(file []byte) (int, error)
|
|
|
|
// locatorCID is the CID of locator.json: dag-pb, SHA-256.
|
|
const locatorCID = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
|
|
|
|
// seed derives the seed of one part of the output, so that a change in one
|
|
// part leaves the others as they were.
|
|
func seed(label string) [32]byte {
|
|
return sha256.Sum256([]byte("datekeys-dart stage 7a: " + label))
|
|
}
|
|
|
|
// newRand returns the generator of the choices of one part.
|
|
func newRand(label string) *rand.Rand { return rand.New(rand.NewChaCha8(seed(label))) }
|
|
|
|
// setCryptoRand makes crypto/rand.Reader, which age, tlock and NewEnvelope
|
|
// read, a ChaCha8 of a fixed seed: crypto/rand.Read reads the Reader that
|
|
// replaces the default one.
|
|
func setCryptoRand(label string) { cryptorand.Reader = rand.NewChaCha8(seed("crypto/rand " + label)) }
|
|
|
|
// recorder keeps every byte read through it.
|
|
type recorder struct {
|
|
r io.Reader
|
|
got []byte
|
|
}
|
|
|
|
func (r *recorder) Read(p []byte) (int, error) {
|
|
n, err := r.r.Read(p)
|
|
r.got = append(r.got, p[:n]...)
|
|
return n, err
|
|
}
|
|
|
|
func hx(b []byte) string { return hex.EncodeToString(b) }
|
|
|
|
func sum(b []byte) string {
|
|
s := sha256.Sum256(b)
|
|
return hx(s[:])
|
|
}
|
|
|
|
func must[T any](v T, err error) T {
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
return v
|
|
}
|
|
|
|
// patterned returns n bytes that depend only on label.
|
|
func patterned(label string, n int) []byte {
|
|
out := make([]byte, 0, n+32)
|
|
for i := uint32(0); len(out) < n; i++ {
|
|
var c [4]byte
|
|
binary.BigEndian.PutUint32(c[:], i)
|
|
h := sha256.Sum256(append([]byte(label), c[:]...))
|
|
out = append(out, h[:]...)
|
|
}
|
|
return out[:n]
|
|
}
|
|
|
|
// edits writes b as edits of base: the bytes between their common prefix
|
|
// and their common suffix replace those of base, in pieces, each run of 32
|
|
// or more equal bytes as an edit [at, 0, byte, count] that inserts count
|
|
// copies of the byte.
|
|
func edits(base, b []byte) [][]any {
|
|
if bytes.Equal(base, b) {
|
|
return [][]any{}
|
|
}
|
|
p := 0
|
|
for p < len(base) && p < len(b) && base[p] == b[p] {
|
|
p++
|
|
}
|
|
s := 0
|
|
for s < len(base)-p && s < len(b)-p && base[len(base)-1-s] == b[len(b)-1-s] {
|
|
s++
|
|
}
|
|
insert := b[p : len(b)-s]
|
|
out := [][]any{}
|
|
del := len(base) - p - s
|
|
at := p
|
|
flush := func(piece []byte, repeat int) {
|
|
e := []any{at, del, hx(piece)}
|
|
if repeat > 0 {
|
|
e = append(e, repeat)
|
|
}
|
|
out = append(out, e)
|
|
at += del
|
|
del = 0
|
|
}
|
|
start := 0
|
|
for i := 0; i < len(insert); {
|
|
j := i
|
|
for j < len(insert) && insert[j] == insert[i] {
|
|
j++
|
|
}
|
|
if j-i >= 32 {
|
|
if start < i {
|
|
flush(insert[start:i], 0)
|
|
}
|
|
flush(insert[i:i+1], j-i)
|
|
start = j
|
|
}
|
|
i = j
|
|
}
|
|
if start < len(insert) || len(out) == 0 {
|
|
flush(insert[start:], 0)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// compactURI writes an address with a run of 32 or more equal bytes as
|
|
// [before, byte, count, after].
|
|
func compactURI(u string) any {
|
|
for i := 0; i < len(u); {
|
|
j := i
|
|
for j < len(u) && u[j] == u[i] {
|
|
j++
|
|
}
|
|
if j-i >= 32 {
|
|
return []any{u[:i], u[i : i+1], j - i, u[j:]}
|
|
}
|
|
i = j
|
|
}
|
|
return u
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The texts
|
|
|
|
// textTable numbers the texts of the errors: 0 is no error.
|
|
type textTable struct {
|
|
list []string
|
|
index map[string]int
|
|
}
|
|
|
|
func newTexts() *textTable { return &textTable{list: []string{""}, index: map[string]int{"": 0}} }
|
|
|
|
func (t *textTable) of(err error) int {
|
|
s := ""
|
|
if err != nil {
|
|
s = err.Error()
|
|
if s == "" {
|
|
log.Fatal("an error without text")
|
|
}
|
|
}
|
|
if i, ok := t.index[s]; ok {
|
|
return i
|
|
}
|
|
t.index[s] = len(t.list)
|
|
t.list = append(t.list, s)
|
|
return len(t.list) - 1
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The JSON
|
|
|
|
// asciiJSON is the JSON of v in one line, ASCII: every other character and
|
|
// the apostrophe, which three times in a row would end the raw string of the
|
|
// Dart constant, escaped.
|
|
func asciiJSON(v any) string {
|
|
var b bytes.Buffer
|
|
enc := json.NewEncoder(&b)
|
|
enc.SetEscapeHTML(false)
|
|
if err := enc.Encode(v); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
s := strings.TrimSuffix(b.String(), "\n")
|
|
var out strings.Builder
|
|
for _, r := range s {
|
|
switch {
|
|
case r == '\'':
|
|
fmt.Fprintf(&out, "\\u%04x", r)
|
|
case r < 0x80:
|
|
out.WriteRune(r)
|
|
case r < 0x10000:
|
|
fmt.Fprintf(&out, "\\u%04x", r)
|
|
default:
|
|
r1, r2 := utf16.EncodeRune(r)
|
|
fmt.Fprintf(&out, "\\u%04x\\u%04x", r1, r2)
|
|
}
|
|
}
|
|
return out.String()
|
|
}
|
|
|
|
// document writes a JSON object whose members are in the order given, each
|
|
// list one item per line.
|
|
type document struct {
|
|
b bytes.Buffer
|
|
first bool
|
|
}
|
|
|
|
func newDocument() *document {
|
|
d := &document{first: true}
|
|
d.b.WriteString("{\n")
|
|
return d
|
|
}
|
|
|
|
func (d *document) sep() {
|
|
if !d.first {
|
|
d.b.WriteString(",\n")
|
|
}
|
|
d.first = false
|
|
}
|
|
|
|
func (d *document) value(key string, v any) {
|
|
d.sep()
|
|
fmt.Fprintf(&d.b, " %s: %s", asciiJSON(key), asciiJSON(v))
|
|
}
|
|
|
|
func (d *document) list(key string, items []any) {
|
|
d.sep()
|
|
fmt.Fprintf(&d.b, " %s: [", asciiJSON(key))
|
|
for i, it := range items {
|
|
if i > 0 {
|
|
d.b.WriteByte(',')
|
|
}
|
|
d.b.WriteString("\n ")
|
|
d.b.WriteString(asciiJSON(it))
|
|
}
|
|
d.b.WriteString("\n ]")
|
|
}
|
|
|
|
func (d *document) bytes() []byte {
|
|
out := slices.Clone(d.b.Bytes())
|
|
out = append(out, "\n}\n"...)
|
|
var parsed any
|
|
if err := json.Unmarshal(out, &parsed); err != nil {
|
|
log.Fatalf("the JSON does not parse: %v", err)
|
|
}
|
|
for _, c := range out {
|
|
if c >= 0x80 {
|
|
log.Fatal("the JSON is not ASCII")
|
|
}
|
|
}
|
|
if bytes.Contains(out, []byte("'''")) {
|
|
log.Fatal("the JSON holds three apostrophes")
|
|
}
|
|
return out
|
|
}
|
|
|
|
func anyList[T any](xs []T) []any {
|
|
out := make([]any, len(xs))
|
|
for i, x := range xs {
|
|
out[i] = x
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Addresses
|
|
|
|
// uriCase is CheckURI and Host of an address: [uri, text, host].
|
|
func uriCase(t *textTable, uri string) []any {
|
|
if !utf8.ValidString(uri) {
|
|
log.Fatalf("an address that is not UTF-8: %q", uri)
|
|
}
|
|
err := locator.CheckURI(uri)
|
|
host := locator.Address{URI: uri}.Host()
|
|
if (err == nil) != (host != "") {
|
|
log.Fatalf("%q: %v and host %q", uri, err, host)
|
|
}
|
|
return []any{uri, t.of(err), host}
|
|
}
|
|
|
|
// prefixInfo is a block of addresses, its first and last address and their
|
|
// neighbours.
|
|
type prefixInfo struct {
|
|
first, last netip.Addr
|
|
}
|
|
|
|
func blockOf(s string) prefixInfo {
|
|
p := netip.MustParsePrefix(s).Masked()
|
|
first := p.Addr()
|
|
b := first.AsSlice()
|
|
bits := p.Bits()
|
|
for i := bits; i < len(b)*8; i++ {
|
|
b[i/8] |= 0x80 >> (i % 8)
|
|
}
|
|
last, _ := netip.AddrFromSlice(b)
|
|
return prefixInfo{first, last}
|
|
}
|
|
|
|
// The blocks of §44.1, those that hold an IPv4 address, and others outside
|
|
// 2000::/3, whose edges the cases visit.
|
|
var blocks4 = []string{"0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.2.0/24",
|
|
"192.88.99.0/24", "192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4",
|
|
// Neighbours of interest that are public.
|
|
"1.0.0.0/24", "8.8.8.0/24", "192.0.1.0/24", "192.31.196.0/24", "192.52.193.0/24", "192.175.48.0/24"}
|
|
|
|
var blocks6 = []string{"2000::/3", "2001::/23", "2001:db8::/32", "2002::/16", "3fff::/20",
|
|
"::/96", "::ffff:0:0/96", "64:ff9b::/96", "64:ff9b:1::/48", "2001::/32", "fc00::/7", "fe80::/10", "ff00::/8", "100::/64", "::1/128",
|
|
"2001:20::/28", "2001:2::/48", "2620:4f:8000::/48", "2001:4860::/32"}
|
|
|
|
// edges gives the first and the last address of each block, and the
|
|
// addresses before and after them; inner reports the first and the last.
|
|
func edges(list []string) (out []netip.Addr, inner []bool) {
|
|
for _, s := range list {
|
|
b := blockOf(s)
|
|
for i, a := range []netip.Addr{b.first.Prev(), b.first, b.first.Next(), b.last.Prev(), b.last, b.last.Next()} {
|
|
if a.IsValid() {
|
|
out = append(out, a)
|
|
inner = append(inner, i == 1 || i == 4)
|
|
}
|
|
}
|
|
}
|
|
return out, inner
|
|
}
|
|
|
|
func randomAddr4(r *rand.Rand) netip.Addr {
|
|
var b [4]byte
|
|
switch r.IntN(4) {
|
|
case 0: // inside a block of §44.1
|
|
bl := blockOf(blocks4[r.IntN(15)])
|
|
f, l := bl.first.As4(), bl.last.As4()
|
|
for i := range b {
|
|
b[i] = f[i] + byte(r.IntN(int(l[i]-f[i])+1))
|
|
}
|
|
default:
|
|
for i := range b {
|
|
b[i] = byte(r.IntN(256))
|
|
}
|
|
}
|
|
return netip.AddrFrom4(b)
|
|
}
|
|
|
|
func randomAddr6(r *rand.Rand) netip.Addr {
|
|
var b [16]byte
|
|
for i := range b {
|
|
b[i] = byte(r.IntN(256))
|
|
}
|
|
switch r.IntN(6) {
|
|
case 0: // inside a block
|
|
bl := blockOf(blocks6[r.IntN(len(blocks6))])
|
|
f, l := bl.first.As16(), bl.last.As16()
|
|
for i := range b {
|
|
if f[i] == l[i] {
|
|
b[i] = f[i]
|
|
} else {
|
|
b[i] = f[i] | b[i]&(l[i]^f[i])
|
|
}
|
|
}
|
|
case 1: // zeros, to compress
|
|
for i := range b {
|
|
if r.IntN(3) > 0 {
|
|
b[i] = 0
|
|
}
|
|
}
|
|
case 2: // 2000::/3
|
|
b[0] = 0x20 | b[0]&0x1f
|
|
}
|
|
return netip.AddrFrom16(b)
|
|
}
|
|
|
|
func randomCase(r *rand.Rand, s string) string {
|
|
var out strings.Builder
|
|
for _, c := range s {
|
|
switch {
|
|
case c >= 'a' && c <= 'z' && r.IntN(3) == 0:
|
|
out.WriteRune(c - 32)
|
|
case c >= 'A' && c <= 'Z' && r.IntN(3) == 0:
|
|
out.WriteRune(c + 32)
|
|
default:
|
|
out.WriteRune(c)
|
|
}
|
|
}
|
|
return out.String()
|
|
}
|
|
|
|
// notations4 writes an IPv4 address in the notations that netip and some
|
|
// clients read.
|
|
func notations4(r *rand.Rand, a netip.Addr) []string {
|
|
b := a.As4()
|
|
v := binary.BigEndian.Uint32(b[:])
|
|
octet := func(x byte, form int) string {
|
|
switch form {
|
|
case 1:
|
|
return "0" + strconv.Itoa(int(x))
|
|
case 2:
|
|
return "0x" + strconv.FormatUint(uint64(x), 16)
|
|
case 3:
|
|
return "0" + strconv.FormatUint(uint64(x), 8)
|
|
case 4:
|
|
return "00" + strconv.Itoa(int(x))
|
|
}
|
|
return strconv.Itoa(int(x))
|
|
}
|
|
dotted := func(forms [4]int) string {
|
|
parts := make([]string, 4)
|
|
for i := range parts {
|
|
parts[i] = octet(b[i], forms[i])
|
|
}
|
|
return strings.Join(parts, ".")
|
|
}
|
|
out := []string{a.String(), dotted([4]int{r.IntN(5) * r.IntN(2), 0, 0, 0}), dotted([4]int{0, 0, 0, r.IntN(5)}),
|
|
fmt.Sprintf("%d.%d.%d", b[0], b[1], uint32(b[2])<<8|uint32(b[3])), fmt.Sprintf("%d.%d", b[0], v&0xffffff), strconv.FormatUint(uint64(v), 10),
|
|
"0x" + strconv.FormatUint(uint64(v), 16), "0" + strconv.FormatUint(uint64(v), 8), a.String() + ".", a.String() + ".0",
|
|
randomCase(r, "0X"+strconv.FormatUint(uint64(v), 16))}
|
|
return out
|
|
}
|
|
|
|
// notations6 writes an IPv6 address in the notations that netip accepts and
|
|
// in some that it rejects.
|
|
func notations6(r *rand.Rand, a netip.Addr) []string {
|
|
b := a.As16()
|
|
groups := make([]string, 8)
|
|
for i := range groups {
|
|
groups[i] = strconv.FormatUint(uint64(binary.BigEndian.Uint16(b[2*i:])), 16)
|
|
}
|
|
full := strings.Join(groups, ":")
|
|
padded := make([]string, 8)
|
|
for i, g := range groups {
|
|
padded[i] = strings.Repeat("0", 4-len(g)) + g
|
|
}
|
|
out := []string{a.String(), full, strings.Join(padded, ":"), randomCase(r, a.String())}
|
|
// :: in place of each run of zeros, the longest or not.
|
|
for i := 0; i < 8; i++ {
|
|
if groups[i] != "0" {
|
|
continue
|
|
}
|
|
j := i
|
|
for j < 8 && groups[j] == "0" {
|
|
j++
|
|
}
|
|
out = append(out, strings.Join(groups[:i], ":")+"::"+strings.Join(groups[j:], ":"))
|
|
i = j
|
|
}
|
|
// The last 32 bits as an IPv4 address.
|
|
v4 := fmt.Sprintf("%d.%d.%d.%d", b[12], b[13], b[14], b[15])
|
|
out = append(out, strings.Join(groups[:6], ":")+":"+v4)
|
|
if a.Is4In6() || b[0] == 0 && b[1] == 0 {
|
|
out = append(out, "::"+strings.Join(groups[5:6], ":")+":"+v4)
|
|
}
|
|
// Rejected: a group of five digits, nine groups, two ::, a short form.
|
|
k := r.IntN(8)
|
|
five := slices.Clone(padded)
|
|
five[k] = "0" + five[k]
|
|
nine := append(slices.Clone(groups), "1")
|
|
out = append(out, strings.Join(five, ":"), strings.Join(nine, ":"), strings.Join(groups[:7], ":"),
|
|
strings.Join(groups[:3], ":")+"::"+strings.Join(groups[4:5], ":")+"::1", full+":", ":"+full, full+"::")
|
|
return out
|
|
}
|
|
|
|
func randomLabel(r *rand.Rand) string {
|
|
const alnum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
n := 1 + r.IntN(12)
|
|
switch r.IntN(10) {
|
|
case 0:
|
|
n = 61 + r.IntN(5)
|
|
case 1:
|
|
n = 1
|
|
}
|
|
var s strings.Builder
|
|
for i := 0; i < n; i++ {
|
|
if i > 0 && i < n-1 && r.IntN(8) == 0 {
|
|
s.WriteByte('-')
|
|
} else {
|
|
s.WriteByte(alnum[r.IntN(len(alnum))])
|
|
}
|
|
}
|
|
l := s.String()
|
|
switch r.IntN(30) {
|
|
case 0:
|
|
l = "-" + l
|
|
case 1:
|
|
l += "-"
|
|
case 2:
|
|
l = "xn--" + strings.ToLower(l)
|
|
case 3:
|
|
l = strconv.Itoa(r.IntN(1000))
|
|
case 4:
|
|
l = randomCase(r, "0x") + strconv.FormatUint(r.Uint64N(1<<32), 16)
|
|
case 5:
|
|
l = strings.Replace(l, string(l[0]), "_", 1)
|
|
case 6:
|
|
l = ""
|
|
case 7:
|
|
l = randomCase(r, []string{"localhost", "local", "internal", "invalid", "test", "example", "onion", "arpa", "home"}[r.IntN(9)])
|
|
}
|
|
return l
|
|
}
|
|
|
|
var nonASCII = []string{"\xc3\xa9", "\xc3\xb1", "\xe2\x80\xae", "\xef\xbc\x8e", "\xe3\x80\x82", "\xf0\x9f\x8c\x8d", "\xc2\xa0", "\xd0\xb0", "\xe2\x80\x8b"}
|
|
|
|
func randomName(r *rand.Rand) string {
|
|
n := 1 + r.IntN(4)
|
|
labels := make([]string, n)
|
|
for i := range labels {
|
|
labels[i] = randomLabel(r)
|
|
}
|
|
if n > 1 && r.IntN(3) == 0 {
|
|
labels[n-1] = []string{"org", "com", "es", "io", "net", "arpa", "local", "test", "0", "123", "0x1f", "0Xff", "1e3"}[r.IntN(13)]
|
|
}
|
|
if r.IntN(4) == 0 {
|
|
suffix := []string{"localhost", "local", "home.arpa", "internal", "invalid", "test", "example", "onion"}[r.IntN(8)]
|
|
labels = append(labels, strings.Split(randomCase(r, suffix), ".")...)
|
|
}
|
|
name := strings.Join(labels, ".")
|
|
switch r.IntN(25) {
|
|
case 0:
|
|
name += "."
|
|
case 1:
|
|
name = "." + name
|
|
case 2:
|
|
i := r.IntN(len(name) + 1)
|
|
name = name[:i] + nonASCII[r.IntN(len(nonASCII))] + name[i:]
|
|
case 3:
|
|
i := r.IntN(len(name) + 1)
|
|
name = name[:i] + fmt.Sprintf("%%%02X", r.IntN(256)) + name[i:]
|
|
}
|
|
return name
|
|
}
|
|
|
|
func randomPort(r *rand.Rand) string {
|
|
switch r.IntN(12) {
|
|
case 0:
|
|
return ":"
|
|
case 1:
|
|
return ":0"
|
|
case 2:
|
|
return ":0" + strconv.Itoa(1+r.IntN(65535))
|
|
case 3:
|
|
return ":" + strconv.Itoa(65536+r.IntN(40000))
|
|
case 4:
|
|
return ":" + strings.Repeat("0", 1+r.IntN(3)) + strconv.Itoa(r.IntN(1000))
|
|
case 5:
|
|
return []string{":65535", ":65536", ":1", ":99999", ":100000", ":443", ":8443", ":-1", ":+1", ":4a", ": 1", "::1"}[r.IntN(12)]
|
|
}
|
|
return ":" + strconv.Itoa(1+r.IntN(65535))
|
|
}
|
|
|
|
// The characters of a path: allowed, percent-encoded and not allowed.
|
|
func randomSegment(r *rand.Rand) string {
|
|
const allowed = "abcxyzABZ019-._~!$&'()*+,;=:@"
|
|
n := r.IntN(8)
|
|
var s strings.Builder
|
|
for i := 0; i < n; i++ {
|
|
switch r.IntN(20) {
|
|
case 0:
|
|
s.WriteString([]string{"%2e", "%2E", "%2f", "%41", "%00", "%ff", "%25", "%7e"}[r.IntN(8)])
|
|
case 1:
|
|
s.WriteString([]string{"%", "%4", "%zz", "%g0", "%0g"}[r.IntN(5)])
|
|
case 2:
|
|
s.WriteString([]string{" ", "\"", "<", ">", "\\", "^", "`", "{", "|", "}", "\x00", "\t", "\n", "\x7f", "\x1f"}[r.IntN(15)])
|
|
case 3:
|
|
s.WriteString(nonASCII[r.IntN(len(nonASCII))])
|
|
default:
|
|
s.WriteByte(allowed[r.IntN(len(allowed))])
|
|
}
|
|
}
|
|
seg := s.String()
|
|
switch r.IntN(10) {
|
|
case 0:
|
|
seg = []string{".", "..", "%2e", "%2E%2e", ".%2E", "%2e.", "...", "..a"}[r.IntN(8)]
|
|
}
|
|
return seg
|
|
}
|
|
|
|
func randomPath(r *rand.Rand) string {
|
|
var s strings.Builder
|
|
for n := r.IntN(4); n > 0; n-- {
|
|
s.WriteByte('/')
|
|
s.WriteString(randomSegment(r))
|
|
}
|
|
if r.IntN(5) == 0 {
|
|
s.WriteString("?" + randomSegment(r) + "/" + randomSegment(r))
|
|
}
|
|
if r.IntN(6) == 0 {
|
|
s.WriteString("#" + randomSegment(r) + "/" + randomSegment(r))
|
|
}
|
|
return s.String()
|
|
}
|
|
|
|
func cidV1(b []byte) string {
|
|
return "b" + strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b))
|
|
}
|
|
|
|
func uvarint(v uint64) []byte { return binary.AppendUvarint(nil, v) }
|
|
|
|
// randomCID writes a CID v1 in base32, valid or with one defect of its bytes
|
|
// or of its text.
|
|
func randomCID(r *rand.Rand) string {
|
|
version := uint64(1)
|
|
if r.IntN(12) == 0 {
|
|
version = []uint64{0, 2, 3, 0x81}[r.IntN(4)]
|
|
}
|
|
codec := []uint64{0x55, 0x70, 0x71, 0x0129, 0x0200, r.Uint64N(1 << 14)}[r.IntN(6)]
|
|
hash := []uint64{0x12, 0x13, 0x00, 0xb220, r.Uint64N(1 << 21)}[r.IntN(5)]
|
|
n := []int{32, 64, 20, 1 + r.IntN(80)}[r.IntN(4)]
|
|
digest := make([]byte, n)
|
|
for i := range digest {
|
|
digest[i] = byte(r.IntN(256))
|
|
}
|
|
parts := [][]byte{uvarint(version), uvarint(codec), uvarint(hash), uvarint(uint64(n)), digest}
|
|
switch r.IntN(10) {
|
|
case 0: // a varint that is not minimal
|
|
i := r.IntN(4)
|
|
v := parts[i]
|
|
parts[i] = append(append(slices.Clone(v[:len(v)-1]), v[len(v)-1]|0x80), 0)
|
|
case 1: // a varint of ten bytes, or of nine with the last continued
|
|
i := r.IntN(4)
|
|
parts[i] = append(bytes.Repeat([]byte{0xff}, 8+r.IntN(2)), 0x01)
|
|
case 2: // a length that is not that of the digest
|
|
parts[3] = uvarint(uint64(n + 1 - 2*r.IntN(2)))
|
|
case 3: // a byte after the digest
|
|
parts = append(parts, []byte{byte(r.IntN(256))})
|
|
case 4: // an empty digest
|
|
parts[3], parts[4] = []byte{0}, nil
|
|
case 5: // cut
|
|
all := slices.Concat(parts...)
|
|
parts = [][]byte{all[:r.IntN(len(all))]}
|
|
}
|
|
s := cidV1(slices.Concat(parts...))
|
|
switch r.IntN(14) {
|
|
case 0: // the last character with bits beyond a byte
|
|
s = s[:len(s)-1] + string("abcdefghijklmnopqrstuvwxyz234567"[r.IntN(32)])
|
|
case 1:
|
|
s = "b" + strings.ToUpper(s[1:])
|
|
case 2:
|
|
s += "="
|
|
case 3:
|
|
s += "a"
|
|
case 4:
|
|
s = s[:len(s)-1]
|
|
case 5:
|
|
i := 1 + r.IntN(len(s)-1)
|
|
s = s[:i] + string("0189"[r.IntN(4)]) + s[i:]
|
|
case 6:
|
|
s = "B" + s[1:]
|
|
case 7:
|
|
s = "Qm" + s[1:]
|
|
}
|
|
return s
|
|
}
|
|
|
|
func randomURI(r *rand.Rand) string {
|
|
scheme := "https"
|
|
switch x := r.IntN(20); {
|
|
case x < 4:
|
|
scheme = "ipfs"
|
|
case x < 6:
|
|
scheme = []string{"http", "HTTPS", "Https", "IPFS", "ipfS", "", "ftp", "file", "h", "https+x", "1https", "ht-tp", "https.", "data"}[r.IntN(14)]
|
|
}
|
|
sep := "://"
|
|
if r.IntN(25) == 0 {
|
|
sep = []string{":/", ":", "//", ":///", ":\\\\", "::/"}[r.IntN(6)]
|
|
}
|
|
var host string
|
|
if strings.EqualFold(scheme, "ipfs") && r.IntN(5) > 0 {
|
|
host = randomCID(r)
|
|
} else {
|
|
switch r.IntN(10) {
|
|
case 0, 1:
|
|
ns := notations4(r, randomAddr4(r))
|
|
host = ns[r.IntN(len(ns))]
|
|
case 2, 3:
|
|
ns := notations6(r, randomAddr6(r))
|
|
host = ns[r.IntN(len(ns))]
|
|
switch r.IntN(12) {
|
|
case 0:
|
|
host = "[[" + host + "]"
|
|
case 1:
|
|
host = "[" + host
|
|
case 2:
|
|
host += "]"
|
|
case 3:
|
|
host = "[" + host + "%25" + []string{"eth0", "1", "lo"}[r.IntN(3)] + "]"
|
|
case 4:
|
|
host = "[" + host + "]]"
|
|
case 5:
|
|
default:
|
|
host = "[" + host + "]"
|
|
}
|
|
default:
|
|
host = randomName(r)
|
|
}
|
|
}
|
|
authority := host
|
|
if r.IntN(25) == 0 {
|
|
authority = []string{"user@", "@", "a:b@", "u%40v@"}[r.IntN(4)] + authority
|
|
}
|
|
if r.IntN(5) == 0 {
|
|
authority += randomPort(r)
|
|
}
|
|
uri := scheme + sep + authority + randomPath(r)
|
|
if r.IntN(40) == 0 {
|
|
// At the limit of the length.
|
|
want := 1023 + r.IntN(3)
|
|
if len(uri) < want {
|
|
if !strings.Contains(uri[len(scheme)+len(sep):], "/") {
|
|
uri += "/"
|
|
}
|
|
uri += strings.Repeat("a", want-len(uri))
|
|
}
|
|
}
|
|
return uri
|
|
}
|
|
|
|
// fixedURIs are the addresses built at the edges of §44.1.
|
|
func fixedURIs(r *rand.Rand) []string {
|
|
var out []string
|
|
add := func(s ...string) { out = append(out, s...) }
|
|
// Every block of IPv4 at its edges; its first and last address in every
|
|
// notation, and inside IPv6.
|
|
as, inner := edges(blocks4)
|
|
for i, a := range as {
|
|
add("https://" + a.String() + "/")
|
|
if !inner[i] {
|
|
continue
|
|
}
|
|
for _, n := range notations4(r, a)[1:] {
|
|
add("https://" + n + "/")
|
|
}
|
|
add("https://"+a.String()+":443/x", "https://["+a.String()+"]/", "https://[::ffff:"+a.String()+"]/", "https://[::"+a.String()+"]/",
|
|
"https://[64:ff9b::"+a.String()+"]/", "https://[64:ff9b:1::"+a.String()+"]/")
|
|
}
|
|
// Every block of IPv6 at its edges; its first and last address in every
|
|
// notation, with and without brackets, with a zone and a port.
|
|
as, inner = edges(blocks6)
|
|
for i, a := range as {
|
|
s := a.String()
|
|
add("https://[" + s + "]/")
|
|
if !inner[i] {
|
|
continue
|
|
}
|
|
for _, n := range notations6(r, a)[1:] {
|
|
add("https://[" + n + "]/")
|
|
}
|
|
add("https://"+s+"/", "https://["+s+"]:8443/a", "https://["+s+"]:0443/", "https://["+s+"%25eth0]/", "https://["+s+"%eth0]/",
|
|
"https://[["+s+"]/", "https://[["+s+"]]/", "https://["+s+"]]/", "https://["+s+"/", "https://["+s+"]x/", "https://["+s+"]:/")
|
|
}
|
|
// Names.
|
|
for _, n := range []string{"a", "a.b", "ejemplo.org", "EJEMPLO.ORG", "xn--pple-43d.com", "xn--a.b", "a-b.c-d.e", "a--b.org", "1.org", "org.1",
|
|
"123.456", "0x.org", "org.0x", "org.0X", "org.0x1", "a.0b1", "a.1b", "a.b1", "a.b-1", "a.0", "a.00", "a.08", "a.1e1",
|
|
strings.Repeat("a", 63) + ".org", strings.Repeat("a", 64) + ".org", "org." + strings.Repeat("b", 63), "org." + strings.Repeat("b", 64),
|
|
strings.Repeat("a.", 120) + "org", "localhost", "LOCALHOST", "LocalHost", "a.localhost", "localhost.org", "a.local", "A.LOCAL", "local",
|
|
"home.arpa", "a.home.arpa", "a.HOME.ARPA", "home.arpa.org", "arpa", "a.arpa", "x.internal", "x.invalid", "x.test", "x.example",
|
|
"x.onion", "x.Onion", "onion.x", "example", "example.com", "test.org", "a.localhost.", "a.local-", "a._b.org", "a.b_.org"} {
|
|
add("https://"+n+"/", "https://"+n+":8443/x")
|
|
}
|
|
// Ports.
|
|
for _, p := range []string{"", ":", ":0", ":00", ":1", ":01", ":9", ":09", ":443", ":0443", ":00443", ":000443", ":65535", ":065535",
|
|
":65536", ":99999", ":100000", ":4294967739", ":-443", ":+443", ":44 3", ":4a", ":443:80", "::443"} {
|
|
add("https://ejemplo.org"+p+"/x", "https://[2606:4700::1111]"+p+"/x", "ipfs://"+locatorCID+p+"/x")
|
|
}
|
|
// Schemes, separators and userinfo.
|
|
for _, u := range []string{"https://ejemplo.org/", "HTTPS://ejemplo.org/", "hTTps://ejemplo.org/", "https:/ejemplo.org/", "https:ejemplo.org/",
|
|
"https:///ejemplo.org/", "https:////ejemplo.org/", "://ejemplo.org/", "https//ejemplo.org/", "ipfs:/" + locatorCID, "ipfs:" + locatorCID,
|
|
"ipfs:///" + locatorCID, "https://user@ejemplo.org/", "https://@ejemplo.org/", "https://ejemplo.org@x/", "https://ejemplo.org/a@b",
|
|
"https://ejemplo.org?a@b", "https://ejemplo.org#a@b", "https://ejemplo.org/?", "https://ejemplo.org#", "https://ejemplo.org?",
|
|
"https://ejemplo.org/%", "https://ejemplo.org/%2", "https://ejemplo.org/%2G", "https://ejemplo.org/%2f%2F", "https://ejemplo.org%2f/",
|
|
"https://ejemplo.org/a%", "https://ejemplo.org/a%%41", "https://", "ipfs://", "https://:443/", "https://.:443/", "https://./"} {
|
|
add(u)
|
|
}
|
|
// Dot segments.
|
|
for _, p := range []string{"/.", "/..", "/./", "/../", "/a/.", "/a/..", "/a/./b", "/a/../b", "/%2e", "/%2E", "/%2e%2e", "/%2E%2E", "/.%2e",
|
|
"/%2e.", "/%2e%2E/", "/...", "/.a", "/a.", "/.%2", "/%2e%2e%2f", "/a?/../", "/a#/../", "/a?x=/..", "?/..", "#/..", "?a/../b", "//..", "/..//"} {
|
|
add("https://ejemplo.org"+p, "ipfs://"+locatorCID+p)
|
|
}
|
|
// Each byte that is not a character of RFC 3986, and the first byte of
|
|
// a character that is not ASCII.
|
|
for c := 0; c < 0x80; c++ {
|
|
add(fmt.Sprintf("https://ejemplo.org/a%cb", c))
|
|
}
|
|
for _, n := range nonASCII {
|
|
add("https://ejemplo.org/"+n, "https://ejempl"+n+".org/")
|
|
}
|
|
// Lengths.
|
|
for _, n := range []int{1, 2, 1023, 1024, 1025, 2000} {
|
|
u := "https://ejemplo.org/"
|
|
if n > len(u) {
|
|
add(u + strings.Repeat("a", n-len(u)))
|
|
} else {
|
|
add(strings.Repeat("h", n))
|
|
}
|
|
}
|
|
// CIDs at their limits.
|
|
digest := sha256.Sum256([]byte("locator cid"))
|
|
for _, b := range [][]byte{
|
|
slices.Concat([]byte{0x01, 0x55, 0x12, 0x20}, digest[:]), slices.Concat([]byte{0x01, 0x70, 0x12, 0x20}, digest[:]),
|
|
slices.Concat([]byte{0x01, 0x55, 0x00, 0x01}, []byte{0}), slices.Concat([]byte{0x01, 0x55, 0x00, 0x00}),
|
|
slices.Concat([]byte{0x01, 0x55, 0x00, 0x4b}, patterned("cid 75", 75)), slices.Concat([]byte{0x01, 0x55, 0x00, 0x4c}, patterned("cid 76", 76)),
|
|
slices.Concat([]byte{0x01, 0x55, 0x00, 0x4d}, patterned("cid 77", 77)), slices.Concat([]byte{0x01, 0x80, 0x01, 0x12, 0x20}, digest[:]),
|
|
slices.Concat([]byte{0x01, 0x80, 0x00, 0x12, 0x20}, digest[:]), slices.Concat([]byte{0x81, 0x00, 0x55, 0x12, 0x20}, digest[:]),
|
|
slices.Concat([]byte{0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x12, 0x20}, digest[:]),
|
|
slices.Concat([]byte{0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0x12, 0x20}, digest[:]),
|
|
slices.Concat([]byte{0x01, 0x55, 0x12, 0x80, 0x00}), slices.Concat([]byte{0x01, 0x55, 0x12, 0xa0, 0x00}, digest[:]),
|
|
slices.Concat([]byte{0x01, 0x55, 0x12, 0x21}, digest[:]), slices.Concat([]byte{0x01, 0x55, 0x12, 0x1f}, digest[:]),
|
|
slices.Concat([]byte{0x01, 0x55, 0x12, 0x20}, digest[:], []byte{0}), {0x01}, {0x01, 0x55}, {0x01, 0x55, 0x12}, {},
|
|
} {
|
|
s := cidV1(b)
|
|
add("ipfs://"+s, "ipfs://"+s+"/x", "ipfs://"+s+"a", "ipfs://"+s+"aa", "ipfs://"+s+"q", "ipfs://"+s[:max(1, len(s)-1)])
|
|
}
|
|
add("ipfs://"+locatorCID+"a", "ipfs://"+locatorCID+"b", "ipfs://"+locatorCID+"aa", "ipfs://"+locatorCID+"7", "ipfs://"+strings.ToUpper(locatorCID),
|
|
"ipfs://b", "ipfs://", "ipfs://bb", "ipfs://"+locatorCID+":443", "ipfs://"+locatorCID+"@x", "ipfs://x@"+locatorCID, "ipfs://["+locatorCID+"]")
|
|
return out
|
|
}
|
|
|
|
func uriCases(t *textTable, r *rand.Rand) []any {
|
|
seen := map[string]bool{}
|
|
var out []any
|
|
add := func(u string) {
|
|
if seen[u] || !utf8.ValidString(u) {
|
|
return
|
|
}
|
|
seen[u] = true
|
|
out = append(out, uriCase(t, u))
|
|
}
|
|
for _, u := range fixedURIs(r) {
|
|
add(u)
|
|
}
|
|
for len(out) < 2800 {
|
|
add(randomURI(r))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// wtf8Cases gives CheckURI on addresses with the three bytes of a
|
|
// surrogate, which are not UTF-8: the bytes of a String of Dart with a lone
|
|
// surrogate, [bytes, text].
|
|
func wtf8Cases(t *textTable) []any {
|
|
var out []any
|
|
for _, s := range []string{"https://a.org/\xed\xa0\x80", "https://a.org/\xed\xbf\xbf", "\xed\xa0\x80", "https://ejempl\xed\xb0\x80.org/"} {
|
|
out = append(out, []any{hx([]byte(s)), t.of(locator.CheckURI(s))})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ipCase is netip.ParseAddr of s: [s, false], or [s, true, the bytes of the
|
|
// address, its zone, its String, publicIP].
|
|
func ipCase(s string) []any {
|
|
a, err := netip.ParseAddr(s)
|
|
if err != nil {
|
|
return []any{s, false}
|
|
}
|
|
return []any{s, true, hx(a.AsSlice()), a.Zone(), a.String(), publicIP(a)}
|
|
}
|
|
|
|
func ipCases(r *rand.Rand) []any {
|
|
seen := map[string]bool{}
|
|
var out []any
|
|
add := func(s string) {
|
|
if seen[s] || !utf8.ValidString(s) {
|
|
return
|
|
}
|
|
seen[s] = true
|
|
out = append(out, ipCase(s))
|
|
}
|
|
as, inner := edges(blocks4)
|
|
for i, a := range as {
|
|
add(a.String())
|
|
if inner[i] {
|
|
for _, n := range notations4(r, a)[1:] {
|
|
add(n)
|
|
}
|
|
}
|
|
}
|
|
as, inner = edges(blocks6)
|
|
for i, a := range as {
|
|
add(a.String())
|
|
if inner[i] {
|
|
for _, n := range notations6(r, a)[1:] {
|
|
add(n)
|
|
add(n + "%" + []string{"eth0", "1", "%", "\xc3\xa9", "a b", "]", ""}[r.IntN(7)])
|
|
}
|
|
}
|
|
}
|
|
for _, s := range []string{"", ".", ":", "::", ":::", "%", "%eth0", "::%", "::%eth0", "::1%", "::1%%", "1.2.3.4%eth0", "::1.2.3.4%x",
|
|
"::ffff:1.2.3.4", "::ffff:01.2.3.4", "::ffff:1.2.3", "::ffff:1.2.3.4.5", "::1.2.3.4:5", "1.2.3.4::", "1::2.3.4.5", "1:2:3:4:5:6:1.2.3.4",
|
|
"1:2:3:4:5:6:7:1.2.3.4", "1:2:3:4:5:1.2.3.4", "1:2:3:4:5::1.2.3.4", "::1:2:3:4:5:6:1.2.3.4", "0:0:0:0:0:0:0:0", "0::0", "0:0::0:0:0:0:0:0",
|
|
"0:0:0:0:0:0:0::0", "1:2:3:4:5:6:7::", "::2:3:4:5:6:7:8", "1::3:4:5:6:7:8", "1:2:3:4:5:6:7:8::", "1:2:3:4:5:6:7:8:9", "fffff::",
|
|
"FFFF::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "g::", "::g", "1.2.3.4", "01.2.3.4", "1.2.3.04", "1.2.3.00", "0.0.0.0", "00.0.0.0",
|
|
"255.255.255.255", "256.255.255.255", "1.2.3", "1.2.3.4.5", "1..2.3", ".1.2.3", "1.2.3.", "1.2.3.4 ", " 1.2.3.4", "1.2.3.4\x00",
|
|
"\xef\xbc\x91.2.3.4", "1\xe3\x80\x822.3.4", "0x1.2.3.4", "1e1.2.3.4", "2130706433", "0x7f000001", "[::1]", "::1]", "[::1"} {
|
|
add(s)
|
|
}
|
|
const alphabet = "0123456789abcdefABCDEFgx.:%"
|
|
for len(out) < 1700 {
|
|
n := 1 + r.IntN(24)
|
|
var s strings.Builder
|
|
for i := 0; i < n; i++ {
|
|
s.WriteByte(alphabet[r.IntN(len(alphabet))])
|
|
}
|
|
add(s.String())
|
|
a := randomAddr6(r)
|
|
ns := notations6(r, a)
|
|
add(ns[r.IntN(len(ns))])
|
|
a4 := randomAddr4(r)
|
|
ns4 := notations4(r, a4)
|
|
add(ns4[r.IntN(len(ns4))])
|
|
}
|
|
return out
|
|
}
|
|
|
|
// publicCases is publicIP of the address of each byte string, as a reader
|
|
// checks the IP that a name resolves to: [hex, public, String], or [hex,
|
|
// null] for a length that is no address.
|
|
func publicCases(r *rand.Rand) []any {
|
|
var out []any
|
|
add := func(b []byte) {
|
|
a, ok := netip.AddrFromSlice(b)
|
|
if !ok {
|
|
out = append(out, []any{hx(b), nil})
|
|
return
|
|
}
|
|
out = append(out, []any{hx(b), publicIP(a), a.String()})
|
|
}
|
|
as, _ := edges(blocks4)
|
|
for _, a := range as {
|
|
add(a.AsSlice())
|
|
m := netip.AddrFrom16(a.As16())
|
|
add(m.AsSlice())
|
|
}
|
|
as, _ = edges(blocks6)
|
|
for _, a := range as {
|
|
add(a.AsSlice())
|
|
}
|
|
for i := 0; i < 250; i++ {
|
|
add(randomAddr4(r).AsSlice())
|
|
add(randomAddr6(r).AsSlice())
|
|
}
|
|
for _, n := range []int{0, 1, 3, 5, 8, 15, 17, 32} {
|
|
add(patterned("length", n))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Locators
|
|
|
|
// enc writes CBOR by hand, so that a plaintext may break any rule.
|
|
type enc struct{ b []byte }
|
|
|
|
// head writes a head of major type m and argument n in size bytes: 0 for the
|
|
// shortest.
|
|
func (e *enc) head(m byte, n uint64, size int) {
|
|
if size == 0 {
|
|
switch {
|
|
case n < 24:
|
|
size = 1
|
|
case n < 1<<8:
|
|
size = 2
|
|
case n < 1<<16:
|
|
size = 3
|
|
case n < 1<<32:
|
|
size = 5
|
|
default:
|
|
size = 9
|
|
}
|
|
}
|
|
switch size {
|
|
case 1:
|
|
e.b = append(e.b, m<<5|byte(n))
|
|
case 2:
|
|
e.b = append(e.b, m<<5|24, byte(n))
|
|
case 3:
|
|
e.b = binary.BigEndian.AppendUint16(append(e.b, m<<5|25), uint16(n))
|
|
case 5:
|
|
e.b = binary.BigEndian.AppendUint32(append(e.b, m<<5|26), uint32(n))
|
|
default:
|
|
e.b = binary.BigEndian.AppendUint64(append(e.b, m<<5|27), n)
|
|
}
|
|
}
|
|
|
|
func (e *enc) uint(n uint64) { e.head(0, n, 0) }
|
|
func (e *enc) bstr(b []byte) { e.head(2, uint64(len(b)), 0); e.b = append(e.b, b...) }
|
|
func (e *enc) text(s string) { e.head(3, uint64(len(s)), 0); e.b = append(e.b, s...) }
|
|
func (e *enc) raw(b []byte) { e.b = append(e.b, b...) }
|
|
func (e *enc) array(n uint64) { e.head(4, n, 0) }
|
|
func (e *enc) mapOf(n uint64) { e.head(5, n, 0) }
|
|
|
|
// rawAddr is an address of a plaintext: its entries in order.
|
|
type rawAddr struct {
|
|
pairs []rawPair
|
|
}
|
|
|
|
type rawPair struct {
|
|
key uint64
|
|
val func(*enc)
|
|
}
|
|
|
|
// rawLoc is a plaintext of a locator: the entries of its map in order, the
|
|
// count its head declares (-1 for the number of entries), and the padding.
|
|
type rawLoc struct {
|
|
pairs []rawPair
|
|
count int
|
|
}
|
|
|
|
func addrPairs(uri string, offset uint64) rawAddr {
|
|
a := rawAddr{pairs: []rawPair{{0, func(e *enc) { e.text(uri) }}}}
|
|
if offset != 0 {
|
|
a.pairs = append(a.pairs, rawPair{1, func(e *enc) { e.uint(offset) }})
|
|
}
|
|
return a
|
|
}
|
|
|
|
func addrsValue(addrs []rawAddr) func(*enc) {
|
|
return func(e *enc) {
|
|
e.array(uint64(len(addrs)))
|
|
for _, a := range addrs {
|
|
e.mapOf(uint64(len(a.pairs)))
|
|
for _, p := range a.pairs {
|
|
e.uint(p.key)
|
|
p.val(e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func rawOf(loc *locator.Locator, addrs []rawAddr) rawLoc {
|
|
return rawLoc{count: -1, pairs: []rawPair{
|
|
{0, addrsValue(addrs)},
|
|
{1, func(e *enc) { e.bstr(loc.EnvelopeKey[:]) }},
|
|
{2, func(e *enc) { e.bstr(loc.EnvelopeHeader) }},
|
|
{3, func(e *enc) { e.bstr(loc.RestDigest[:]) }},
|
|
{4, func(e *enc) { e.uint(loc.RestSize) }},
|
|
{5, func(e *enc) { e.bstr(loc.CapsuleDigest[:]) }},
|
|
}}
|
|
}
|
|
|
|
func (l rawLoc) encode(extra ...rawPair) []byte {
|
|
var e enc
|
|
pairs := append(slices.Clone(l.pairs), extra...)
|
|
n := uint64(len(pairs))
|
|
if l.count >= 0 {
|
|
n = uint64(l.count)
|
|
}
|
|
e.mapOf(n)
|
|
for _, p := range pairs {
|
|
e.uint(p.key)
|
|
p.val(&e)
|
|
}
|
|
return e.b
|
|
}
|
|
|
|
func bstrHeadLen(n int) int {
|
|
switch {
|
|
case n < 24:
|
|
return 1
|
|
case n < 256:
|
|
return 2
|
|
case n < 65536:
|
|
return 3
|
|
}
|
|
return 5
|
|
}
|
|
|
|
// padded is the plaintext with the key 6 that makes it the least multiple of
|
|
// 4096 that holds it, as Marshal pads it, or none when it already is one.
|
|
func (l rawLoc) padded() []byte {
|
|
base := l.encode()
|
|
total := locator.PlaintextLength(len(base))
|
|
if total == len(base) {
|
|
return base
|
|
}
|
|
for pad := 1; ; pad++ {
|
|
if len(base)+1+bstrHeadLen(pad)+pad == total {
|
|
return l.encode(rawPair{6, func(e *enc) { e.bstr(make([]byte, pad)) }})
|
|
}
|
|
}
|
|
}
|
|
|
|
// summary is what a reader reads of a locator: [[uri, offset, host,
|
|
// usable]...], the key, the SHA-256 of the header, the digest of the rest,
|
|
// its size and capsule_digest.
|
|
func summary(l *locator.Locator) []any {
|
|
addrs := []any{}
|
|
for _, a := range l.Addresses {
|
|
addrs = append(addrs, []any{compactURI(a.URI), a.Offset, compactURI(a.Host()), locator.CheckURI(a.URI) == nil})
|
|
}
|
|
usable := l.Usable()
|
|
n := 0
|
|
for _, a := range l.Addresses {
|
|
if locator.CheckURI(a.URI) == nil {
|
|
if usable[n] != a {
|
|
log.Fatal("Usable is not the addresses that CheckURI accepts")
|
|
}
|
|
n++
|
|
}
|
|
}
|
|
return []any{addrs, hx(l.EnvelopeKey[:]), sum(l.EnvelopeHeader), hx(l.RestDigest[:]), l.RestSize, hx(l.CapsuleDigest[:])}
|
|
}
|
|
|
|
// readCase is Unmarshal of b: [base, edits, text, summary or null].
|
|
func readCase(t *textTable, bases [][]byte, base int, b []byte) []any {
|
|
l, err := locator.Unmarshal(b)
|
|
var s any
|
|
if err == nil {
|
|
s = summary(l)
|
|
if again, err := locator.Unmarshal(b); err != nil || !slices.Equal(again.Addresses, l.Addresses) {
|
|
log.Fatal("Unmarshal is not stable")
|
|
}
|
|
}
|
|
return []any{base, edits(bases[base], b), t.of(err), s}
|
|
}
|
|
|
|
type world struct {
|
|
p *profile.Profile
|
|
texts *textTable
|
|
releases map[uint64]provider.Release
|
|
dkc []byte
|
|
datas [][]byte // the data of datekeys.capsule of registry and capsule
|
|
loc *locator.Locator
|
|
rest []byte
|
|
file []byte // the envelope, header and rest
|
|
}
|
|
|
|
func (w *world) release(round uint64) provider.Release {
|
|
r, ok := w.releases[round]
|
|
if !ok {
|
|
log.Fatalf("no release of round %d in the fixtures", round)
|
|
}
|
|
return r
|
|
}
|
|
|
|
// plaintextBases are the plaintexts that the edits of plaintexts refer to:
|
|
// the locator of the envelope with two addresses, with eight, and with
|
|
// addresses that a reader rejects next to one it uses.
|
|
func (w *world) plaintextBases() ([][]byte, []rawLoc) {
|
|
two := rawOf(w.loc, []rawAddr{addrPairs("https://ejemplo.org/foto.jpg", 3000), addrPairs("ipfs://"+locatorCID, 0)})
|
|
var eight []rawAddr
|
|
for i := 0; i < 8; i++ {
|
|
eight = append(eight, addrPairs(fmt.Sprintf("https://ejemplo%d.org/%s", i, strings.Repeat("x", 400+i)), uint64(i)*1000))
|
|
}
|
|
eightLoc := rawOf(w.loc, eight)
|
|
mixed := rawOf(w.loc, []rawAddr{addrPairs("http://ejemplo.org/a", 7), addrPairs("https://[64:ff9b::7f00:1]/b", 0),
|
|
addrPairs("https://ejemplo.org/c", 9007199254740991), addrPairs("ipfs://"+locatorCID+"a", 1)})
|
|
raws := []rawLoc{two, eightLoc, mixed}
|
|
var bases [][]byte
|
|
for _, l := range raws {
|
|
bases = append(bases, l.padded())
|
|
}
|
|
return bases, raws
|
|
}
|
|
|
|
// plaintextCases breaks each base field by field and byte by byte.
|
|
func (w *world) plaintextCases(bases [][]byte, raws []rawLoc) []any {
|
|
t := w.texts
|
|
r := newRand("plaintexts")
|
|
var out []any
|
|
add := func(base int, b []byte) { out = append(out, readCase(t, bases, base, b)) }
|
|
for i, b := range bases {
|
|
add(i, b)
|
|
}
|
|
loc := w.loc
|
|
two := raws[0]
|
|
addr := func(uri string, offset uint64) rawAddr { return addrPairs(uri, offset) }
|
|
foto := addr("https://ejemplo.org/foto.jpg", 3000)
|
|
with := func(key uint64, val func(*enc)) rawLoc {
|
|
l := two
|
|
l.pairs = slices.Clone(l.pairs)
|
|
for i, p := range l.pairs {
|
|
if p.key == key {
|
|
l.pairs[i] = rawPair{key, val}
|
|
return l
|
|
}
|
|
}
|
|
l.pairs = append(l.pairs, rawPair{key, val})
|
|
slices.SortStableFunc(l.pairs, func(a, b rawPair) int { return int(a.key) - int(b.key) })
|
|
return l
|
|
}
|
|
without := func(key uint64) rawLoc {
|
|
l := two
|
|
l.pairs = slices.DeleteFunc(slices.Clone(l.pairs), func(p rawPair) bool { return p.key == key })
|
|
return l
|
|
}
|
|
addrs := func(as ...rawAddr) func(*enc) { return addrsValue(as) }
|
|
// The fields, each padded as Marshal would pad it.
|
|
for _, l := range []rawLoc{
|
|
without(0), without(1), without(2), without(3), without(4), without(5),
|
|
with(0, addrs()), with(0, addrs(foto, foto, foto, foto, foto, foto, foto, foto)), with(0, addrs(foto, foto, foto, foto, foto, foto, foto, foto, foto)),
|
|
with(0, addrs(addr("", 0))), with(0, addrs(addr("h", 0))), with(0, addrs(addr("https://ejemplo.org/"+strings.Repeat("a", 1004), 0))),
|
|
with(0, addrs(addr("https://ejemplo.org/"+strings.Repeat("a", 1005), 0))),
|
|
with(0, addrs(addr("https://ejemplo.org/a", 1))), with(0, addrs(addr("https://ejemplo.org/a", 9007199254740991))),
|
|
with(0, addrs(addr("https://ejemplo.org/a", 9007199254740992))), with(0, addrs(addr("https://ejemplo.org/a", 1<<63))),
|
|
with(0, addrs(rawAddr{pairs: []rawPair{{0, func(e *enc) { e.text("https://ejemplo.org/a") }}, {1, func(e *enc) { e.uint(0) }}}})),
|
|
with(0, addrs(rawAddr{pairs: []rawPair{{1, func(e *enc) { e.uint(5) }}}})),
|
|
with(0, addrs(rawAddr{pairs: []rawPair{{0, func(e *enc) { e.text("https://ejemplo.org/a") }}, {2, func(e *enc) { e.uint(5) }}}})),
|
|
with(0, addrs(rawAddr{pairs: []rawPair{{1, func(e *enc) { e.uint(5) }}, {0, func(e *enc) { e.text("https://ejemplo.org/a") }}}})),
|
|
with(0, addrs(rawAddr{pairs: []rawPair{{0, func(e *enc) { e.bstr([]byte("https://ejemplo.org/a")) }}}})),
|
|
with(0, addrs(rawAddr{pairs: []rawPair{{0, func(e *enc) { e.head(3, 2, 0); e.raw([]byte{0xc3, 0x28}) }}}})),
|
|
with(0, addrs(rawAddr{pairs: []rawPair{{0, func(e *enc) { e.text("https://ejemplo.org/a") }}, {1, func(e *enc) { e.head(0, 5, 2) }}}})),
|
|
with(0, addrs(rawAddr{pairs: []rawPair{{0, func(e *enc) { e.text("https://ejemplo.org/a") }}, {0, func(e *enc) { e.text("https://ejemplo.org/b") }}}})),
|
|
with(0, func(e *enc) { e.mapOf(0) }), with(0, func(e *enc) { e.text("https://ejemplo.org/a") }),
|
|
with(0, func(e *enc) { e.head(4, 1, 2); addrPairsEnc(e, "https://ejemplo.org/a") }),
|
|
with(0, addrs(addr("http://ejemplo.org/a", 0))), with(0, addrs(addr("https://127.0.0.1/a", 0), addr("ipfs://"+locatorCID, 5))),
|
|
with(1, func(e *enc) { e.bstr(loc.EnvelopeKey[:31]) }), with(1, func(e *enc) { e.bstr(append(loc.EnvelopeKey[:], 0)) }),
|
|
with(1, func(e *enc) { e.text(strings.Repeat("k", 32)) }),
|
|
with(2, func(e *enc) { e.bstr(nil) }), with(2, func(e *enc) { e.bstr([]byte{'h'}) }), with(2, func(e *enc) { e.bstr(patterned("h", 1024)) }),
|
|
with(2, func(e *enc) { e.bstr(patterned("h", 1025)) }),
|
|
with(3, func(e *enc) { e.bstr(loc.RestDigest[:16]) }), with(5, func(e *enc) { e.bstr(nil) }),
|
|
with(4, func(e *enc) { e.uint(0) }), with(4, func(e *enc) { e.uint(9007199254740991) }), with(4, func(e *enc) { e.uint(9007199254740992) }),
|
|
with(4, func(e *enc) { e.uint(1<<64 - 1) }), with(4, func(e *enc) { e.head(0, loc.RestSize, 9) }), with(4, func(e *enc) { e.text("5") }),
|
|
with(7, func(e *enc) { e.uint(0) }), with(1<<53, func(e *enc) { e.uint(0) }),
|
|
} {
|
|
add(0, l.padded())
|
|
}
|
|
// The map: its count, its keys out of order or repeated.
|
|
{
|
|
l := two
|
|
l.count = 5
|
|
add(0, l.padded())
|
|
l.count = 7
|
|
add(0, l.padded())
|
|
l = two
|
|
l.pairs = slices.Clone(l.pairs)
|
|
l.pairs[1], l.pairs[2] = l.pairs[2], l.pairs[1]
|
|
add(0, l.padded())
|
|
l = two
|
|
l.pairs = append(slices.Clone(l.pairs), l.pairs[5])
|
|
add(0, l.padded())
|
|
}
|
|
// Key 6: absent, empty, of every length near the boundaries of its head,
|
|
// not zeros, in another type, not the least multiple.
|
|
base := two.encode()
|
|
for _, pad := range []int{0, 1, 2, 22, 23, 24, 25, 254, 255, 256, 257, 4096 - len(base) - 3, 4096 - len(base) - 4, 8192 - len(base) - 4} {
|
|
add(0, two.encode(rawPair{6, func(e *enc) { e.bstr(make([]byte, pad)) }}))
|
|
}
|
|
full := 4096 - len(base) - 1
|
|
for _, size := range []int{2, 3, 5, 9} {
|
|
n := full - size
|
|
add(0, two.encode(rawPair{6, func(e *enc) { e.head(2, uint64(n), size); e.raw(make([]byte, n)) }}))
|
|
}
|
|
add(0, two.encode(rawPair{6, func(e *enc) { e.text(strings.Repeat("\x00", full-3)) }}))
|
|
nonzero := slices.Clone(bases[0])
|
|
nonzero[len(nonzero)-1] = 1
|
|
add(0, nonzero)
|
|
nonzero = slices.Clone(bases[0])
|
|
nonzero[len(base)+4] = 0x80
|
|
add(0, nonzero)
|
|
add(0, base)
|
|
add(0, append(slices.Clone(bases[0]), make([]byte, 4096)...))
|
|
add(0, append(slices.Clone(bases[0]), 0))
|
|
add(0, bases[0][:len(bases[0])-1])
|
|
add(0, nil)
|
|
add(0, []byte{0xa0})
|
|
add(0, []byte{0xa7})
|
|
// Bases at the boundaries of the padding: the least multiple and the
|
|
// next one.
|
|
for _, target := range []int{3836, 3837, 3838, 4069, 4070, 4071, 4093, 4094, 4095, 4096, 4097, 8188, 8189, 8190, 8191, 8192} {
|
|
l := lengthFor(w.loc, target)
|
|
add(0, l.padded())
|
|
if b := l.encode(); len(b)%4096 != 0 {
|
|
for _, total := range []int{(len(b)/4096 + 1) * 4096, (len(b)/4096 + 2) * 4096} {
|
|
if pad := fillTo(len(b), total); pad >= 0 {
|
|
add(0, l.encode(rawPair{6, func(e *enc) { e.bstr(make([]byte, pad)) }}))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Bytes edited at random, mostly in the map before the padding.
|
|
for i := 0; i < 360; i++ {
|
|
bi := r.IntN(len(bases))
|
|
b := slices.Clone(bases[bi])
|
|
meaningful := len(raws[bi].encode())
|
|
at := r.IntN(meaningful + 8)
|
|
if r.IntN(6) == 0 {
|
|
at = r.IntN(len(b))
|
|
}
|
|
switch r.IntN(6) {
|
|
case 0, 1:
|
|
b[at] ^= 1 << r.IntN(8)
|
|
case 2:
|
|
b[at] = byte(r.IntN(256))
|
|
case 3:
|
|
n := 1 + r.IntN(4)
|
|
b = slices.Insert(b, at, patterned(fmt.Sprint("insert", i), n)...)
|
|
case 4:
|
|
n := min(1+r.IntN(4), len(b)-at)
|
|
b = slices.Delete(b, at, at+n)
|
|
case 5:
|
|
b = b[:at]
|
|
}
|
|
add(bi, b)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func addrPairsEnc(e *enc, uri string) {
|
|
e.mapOf(1)
|
|
e.uint(0)
|
|
e.text(uri)
|
|
}
|
|
|
|
// fillTo is the length of key 6, 0 included, that makes a CBOR of base bytes
|
|
// measure total, or -1.
|
|
func fillTo(base, total int) int {
|
|
for pad := 0; base+2+pad <= total; pad++ {
|
|
if base+1+bstrHeadLen(pad)+pad == total {
|
|
return pad
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// lengthFor returns a plaintext of the envelope of loc whose CBOR without key
|
|
// 6 measures target bytes, with addresses that §44.1 accepts.
|
|
func lengthFor(loc *locator.Locator, target int) rawLoc {
|
|
for _, n := range []int{980, 880, 700} {
|
|
long := addrPairs("https://ejemplo.org/"+strings.Repeat("a", n), 0)
|
|
for count := 0; count < locator.MaxAddresses; count++ {
|
|
for k := 1; k <= locator.MaxURILen-20; k++ {
|
|
addrs := append(slices.Repeat([]rawAddr{long}, count), addrPairs("https://ejemplo.org/"+strings.Repeat("b", k), 0))
|
|
l := rawOf(loc, addrs)
|
|
n := len(l.encode())
|
|
if n == target {
|
|
return l
|
|
}
|
|
if n > target {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
log.Fatalf("no addresses make a base of %d bytes", target)
|
|
return rawLoc{}
|
|
}
|
|
|
|
// paddingRuns gives PlaintextLength on every base from -4100 to 16484, as
|
|
// runs [from, to, total] of bases with the same result.
|
|
func paddingRuns() []any {
|
|
var out []any
|
|
from, last := -4100, locator.PlaintextLength(-4100)
|
|
for b := -4099; b <= 16485; b++ {
|
|
var got int
|
|
if b <= 16484 {
|
|
got = locator.PlaintextLength(b)
|
|
}
|
|
if b == 16485 || got != last {
|
|
out = append(out, []any{from, b - 1, last})
|
|
from, last = b, got
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// marshalCases gives Marshal of locators at every limit: [addresses,
|
|
// header, rest_size, text, length, SHA-256], the header null for that of
|
|
// the envelope.
|
|
func (w *world) marshalCases() []any {
|
|
t := w.texts
|
|
var out []any
|
|
type addr = locator.Address
|
|
add := func(addrs []addr, header []byte, restSize uint64) {
|
|
l := *w.loc
|
|
l.Addresses = addrs
|
|
var h any
|
|
if header != nil {
|
|
l.EnvelopeHeader = header
|
|
h = hx(header)
|
|
}
|
|
l.RestSize = restSize
|
|
b, err := l.Marshal()
|
|
as := []any{}
|
|
for _, a := range addrs {
|
|
as = append(as, []any{compactURI(a.URI), a.Offset})
|
|
}
|
|
c := []any{as, h, restSize, t.of(err), len(b), ""}
|
|
if err == nil {
|
|
c[5] = sum(b)
|
|
}
|
|
out = append(out, c)
|
|
}
|
|
rs := w.loc.RestSize
|
|
one := []addr{{URI: "https://ejemplo.org/foto.jpg"}}
|
|
foto := addr{URI: "https://ejemplo.org/foto.jpg", Offset: 3000}
|
|
add(one, nil, rs)
|
|
add([]addr{foto, {URI: "ipfs://" + locatorCID}}, nil, rs)
|
|
add(nil, nil, rs)
|
|
add(slices.Repeat([]addr{foto}, 8), nil, rs)
|
|
add(slices.Repeat([]addr{foto}, 9), nil, rs)
|
|
add([]addr{{URI: ""}}, nil, rs)
|
|
add([]addr{{URI: "https://ejemplo.org/" + strings.Repeat("a", 1004)}}, nil, rs)
|
|
add([]addr{{URI: "https://ejemplo.org/" + strings.Repeat("a", 1005)}}, nil, rs)
|
|
add([]addr{{URI: "http://ejemplo.org/a"}}, nil, rs)
|
|
add([]addr{foto, {URI: "https://[64:ff9b::7f00:1]/"}}, nil, rs)
|
|
add([]addr{{URI: "https://ejemplo.org/a", Offset: 9007199254740991}}, nil, rs)
|
|
add([]addr{{URI: "https://ejemplo.org/a", Offset: 9007199254740992}}, nil, rs)
|
|
add([]addr{{URI: "http://ejemplo.org/a", Offset: 9007199254740992}}, nil, rs)
|
|
add(one, nil, 0)
|
|
add(one, nil, 9007199254740991)
|
|
add(one, nil, 9007199254740992)
|
|
add([]addr{{URI: "http://x"}}, nil, 9007199254740992)
|
|
add(one, []byte{}, rs)
|
|
add([]addr{{URI: ""}}, []byte{}, rs)
|
|
add(one, []byte{'h'}, rs)
|
|
add(one, patterned("header", 1024), rs)
|
|
add(one, patterned("header", 1025), rs)
|
|
// The bases of the boundaries of the padding.
|
|
for _, target := range []int{3000, 3836, 3837, 3838, 4069, 4070, 4071, 4072, 4092, 4093, 4094, 4095, 4096, 4097, 4098, 7932, 7933, 7934,
|
|
8165, 8166, 8167, 8188, 8189, 8190, 8191, 8192, 8193} {
|
|
l := lengthFor(w.loc, target)
|
|
// The addresses of l, read back from its encoding.
|
|
back, err := locator.Unmarshal(l.padded())
|
|
if err != nil {
|
|
log.Fatalf("a base of %d: %v", target, err)
|
|
}
|
|
add(back.Addresses, nil, rs)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Sealed locators
|
|
|
|
// sealPlain seals the plaintext of a locator for round as locator.Seal does,
|
|
// for a plaintext that Marshal does not write.
|
|
func (w *world) sealPlain(round uint64, plain []byte) []byte {
|
|
r := must(agewrap.NewTimeRecipient(w.p, round))
|
|
var buf bytes.Buffer
|
|
wr := must(age.Encrypt(&buf, r))
|
|
must(wr.Write(plain))
|
|
if err := wr.Close(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// openCase is Open of base edited: [base, round, release, profile, edits,
|
|
// text, summary or null]; release is the round of the release given, or a
|
|
// name for another; profile names an edit of the profile, "" for none.
|
|
func (w *world) openCase(sealedBases [][]byte, base int, round uint64, release any, prof string, b []byte) []any {
|
|
var rel provider.Release
|
|
switch x := release.(type) {
|
|
case uint64:
|
|
rel = w.release(x)
|
|
case string:
|
|
switch x {
|
|
case "flipped":
|
|
rel = w.release(round)
|
|
rel.Signature = slices.Clone(rel.Signature)
|
|
rel.Signature[len(rel.Signature)-1] ^= 1
|
|
case "short":
|
|
rel = w.release(round)
|
|
rel.Signature = rel.Signature[:47]
|
|
case "other round":
|
|
rel = w.release(1001)
|
|
rel.Round = round
|
|
default:
|
|
log.Fatalf("release %q", x)
|
|
}
|
|
}
|
|
p := w.p.Clone()
|
|
switch prof {
|
|
case "":
|
|
case "key not on the curve":
|
|
p.PublicKey = slices.Clone(p.PublicKey)
|
|
p.PublicKey[len(p.PublicKey)-1] ^= 1
|
|
case "key at infinity":
|
|
p.PublicKey = append([]byte{0xc0}, make([]byte, 95)...)
|
|
case "key of another network":
|
|
// The generator of G2, compressed: a key that no release verifies under.
|
|
p.PublicKey = must(hex.DecodeString("93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8"))
|
|
case "chain hash of another network":
|
|
p.ChainHash[0] ^= 1
|
|
default:
|
|
log.Fatalf("profile %q", prof)
|
|
}
|
|
l, err := locator.Open(p, round, rel, b)
|
|
var s any
|
|
if err == nil {
|
|
s = summary(l)
|
|
}
|
|
return []any{base, round, release, prof, edits(sealedBases[base], b), w.texts.of(err), s}
|
|
}
|
|
|
|
func (w *world) openCases(plainBases [][]byte) ([][]byte, []uint64, []any) {
|
|
setCryptoRand("sealed")
|
|
rounds := []uint64{1000, 1001, 2000, 1004}
|
|
plains := [][]byte{plainBases[0], plainBases[0], plainBases[2], plainBases[1]}
|
|
var sealed [][]byte
|
|
for i, round := range rounds {
|
|
sealed = append(sealed, w.sealPlain(round, plains[i]))
|
|
}
|
|
var out []any
|
|
add := func(base int, round uint64, release any, prof string, b []byte) {
|
|
out = append(out, w.openCase(sealed, base, round, release, prof, b))
|
|
}
|
|
// Each with its release, and with the others.
|
|
for i, round := range rounds {
|
|
for _, other := range rounds {
|
|
add(i, round, other, "", sealed[i])
|
|
if other != round {
|
|
add(i, other, other, "", sealed[i])
|
|
}
|
|
}
|
|
add(i, round, "flipped", "", sealed[i])
|
|
add(i, round, "short", "", sealed[i])
|
|
add(i, round, "other round", "", sealed[i])
|
|
}
|
|
add(0, 0, uint64(1000), "", sealed[0])
|
|
for _, prof := range []string{"key not on the curve", "key at infinity", "key of another network", "chain hash of another network"} {
|
|
add(0, 1000, uint64(1000), prof, sealed[0])
|
|
}
|
|
// The header of the first, edited line by line.
|
|
s := sealed[0]
|
|
end := must(headerEnd(s))
|
|
header := string(s[:end])
|
|
lines := strings.SplitAfter(header, "\n")
|
|
lines = lines[:len(lines)-1]
|
|
edit := func(f func(lines []string) []string) []byte {
|
|
ls := f(slices.Clone(lines))
|
|
return append([]byte(strings.Join(ls, "")), s[end:]...)
|
|
}
|
|
stanza := strings.Fields(strings.TrimSpace(lines[1]))
|
|
setStanza := func(fields ...string) []byte {
|
|
return edit(func(ls []string) []string { ls[1] = strings.Join(fields, " ") + "\n"; return ls })
|
|
}
|
|
add(0, 1000, uint64(1000), "", setStanza("->", "tlock", "1001", stanza[3]))
|
|
add(0, 1000, uint64(1000), "", setStanza("->", "tlock", "01000", stanza[3]))
|
|
add(0, 1000, uint64(1000), "", setStanza("->", "tlock", "1000", stanza[3][:63]+"0"))
|
|
add(0, 1000, uint64(1000), "", setStanza("->", "tlock", "1000"))
|
|
add(0, 1000, uint64(1000), "", setStanza("->", "tlock", "1000", stanza[3], "x"))
|
|
add(0, 1000, uint64(1000), "", setStanza("->", "tlocK", "1000", stanza[3]))
|
|
add(0, 1000, uint64(1000), "", setStanza("->", "X25519", "1000", stanza[3]))
|
|
add(0, 1000, uint64(1000), "", edit(func(ls []string) []string {
|
|
return slices.Insert(ls, 1, "-> X25519 "+base64.RawStdEncoding.EncodeToString(patterned("share", 32))+"\n", base64.RawStdEncoding.EncodeToString(patterned("body", 32))+"\n")
|
|
}))
|
|
add(0, 1000, uint64(1000), "", edit(func(ls []string) []string { return slices.Insert(ls, 1, slices.Clone(ls[1:len(ls)-1])...) }))
|
|
add(0, 1000, uint64(1000), "", edit(func(ls []string) []string { ls[0] = "age-encryption.org/v2\n"; return ls }))
|
|
add(0, 1000, uint64(1000), "", edit(func(ls []string) []string { ls[len(ls)-1] = "--- " + strings.Repeat("A", 43) + "\n"; return ls }))
|
|
add(0, 1000, uint64(1000), "", edit(func(ls []string) []string { ls[len(ls)-1] = "---\n"; return ls }))
|
|
add(0, 1000, uint64(1000), "", edit(func(ls []string) []string { return slices.Delete(ls, 2, 3) }))
|
|
add(0, 1000, uint64(1000), "", s[:end])
|
|
add(0, 1000, uint64(1000), "", s[:end+15])
|
|
add(0, 1000, uint64(1000), "", s[:end+16])
|
|
add(0, 1000, uint64(1000), "", s[:end+16+15])
|
|
add(0, 1000, uint64(1000), "", s[:len(s)-1])
|
|
add(0, 1000, uint64(1000), "", append(slices.Clone(s), 0))
|
|
add(0, 1000, uint64(1000), "", nil)
|
|
// The body of the stanza: U not on the curve, U at infinity, V and W
|
|
// changed, so that only the IBE check finds it.
|
|
bodyText := ""
|
|
for _, l := range lines[2 : len(lines)-1] {
|
|
bodyText += strings.TrimSuffix(l, "\n")
|
|
}
|
|
body := must(base64.RawStdEncoding.DecodeString(bodyText))
|
|
if len(body) != 128 {
|
|
log.Fatalf("a tlock body of %d bytes", len(body))
|
|
}
|
|
for _, f := range []func(b []byte){
|
|
func(b []byte) { b[95] ^= 1 },
|
|
func(b []byte) { copy(b, append([]byte{0xc0}, make([]byte, 95)...)) },
|
|
func(b []byte) { b[100] ^= 1 },
|
|
func(b []byte) { b[127] ^= 1 },
|
|
func(b []byte) { b[0] ^= 0x20 },
|
|
} {
|
|
nb := slices.Clone(body)
|
|
f(nb)
|
|
enc := base64.RawStdEncoding.EncodeToString(nb)
|
|
add(0, 1000, uint64(1000), "", edit(func(ls []string) []string {
|
|
body := []string{enc[:64] + "\n", enc[64:128] + "\n", enc[128:] + "\n"}
|
|
return slices.Concat(ls[:2], body, ls[len(ls)-1:])
|
|
}))
|
|
}
|
|
// The payload, byte by byte at random.
|
|
r := newRand("open")
|
|
for i := 0; i < 48; i++ {
|
|
b := slices.Clone(s)
|
|
at := r.IntN(len(b))
|
|
switch r.IntN(4) {
|
|
case 0, 1:
|
|
b[at] ^= 1 << r.IntN(8)
|
|
case 2:
|
|
b = slices.Delete(b, at, at+1)
|
|
case 3:
|
|
b = slices.Insert(b, at, byte(r.IntN(256)))
|
|
}
|
|
add(0, 1000, uint64(1000), "", b)
|
|
}
|
|
return sealed, rounds, out
|
|
}
|
|
|
|
// limitCases are files of round 1000 whose plaintext is the first base and
|
|
// zeros after it, in chunks of the STREAM that this program writes, with a
|
|
// file key it knows: Go reads at most 1 MiB of plaintext through
|
|
// io.LimitReader, so a defect after it is never seen.
|
|
func (w *world) limitCases(plain []byte) map[string]any {
|
|
setCryptoRand("limit")
|
|
rec := &recorder{r: cryptorand.Reader}
|
|
cryptorand.Reader = rec
|
|
r := must(agewrap.NewTimeRecipient(w.p, 1000))
|
|
var buf bytes.Buffer
|
|
wr := must(age.Encrypt(&buf, r))
|
|
if err := wr.Close(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
setCryptoRand("limit after")
|
|
file := buf.Bytes()
|
|
end := must(headerEnd(file))
|
|
fileKey, nonce := rec.got[:16], rec.got[len(rec.got)-16:]
|
|
if !bytes.Equal(file[end:end+16], nonce) {
|
|
log.Fatal("the nonce is not the last read")
|
|
}
|
|
streamKey := must(hkdf.Key(sha256.New, fileKey, nonce, "payload", 32))
|
|
aead := must(chacha20poly1305.New(streamKey))
|
|
const chunk = 64 << 10
|
|
content := func(n int) []byte {
|
|
b := make([]byte, n)
|
|
copy(b, plain)
|
|
return b
|
|
}
|
|
type chunkSpec struct {
|
|
n int
|
|
last bool
|
|
corrupt bool
|
|
}
|
|
build := func(chunks []chunkSpec, trailing int) []byte {
|
|
out := slices.Clone(file[:end+16])
|
|
total := 0
|
|
for _, c := range chunks {
|
|
total += c.n
|
|
}
|
|
text := content(total)
|
|
at := 0
|
|
for i, c := range chunks {
|
|
var n [12]byte
|
|
binary.BigEndian.PutUint64(n[3:11], uint64(i))
|
|
if c.last {
|
|
n[11] = 1
|
|
}
|
|
ct := aead.Seal(nil, n[:], text[at:at+c.n], nil)
|
|
if c.corrupt {
|
|
ct[0] ^= 1
|
|
}
|
|
out = append(out, ct...)
|
|
at += c.n
|
|
}
|
|
return append(out, make([]byte, trailing)...)
|
|
}
|
|
full := func(k int) []chunkSpec { return slices.Repeat([]chunkSpec{{n: chunk}}, k) }
|
|
var cases []any
|
|
add := func(name string, chunks []chunkSpec, trailing int) {
|
|
b := build(chunks, trailing)
|
|
_, err := locator.Open(w.p, 1000, w.release(1000), b)
|
|
cs := []any{}
|
|
for _, c := range chunks {
|
|
cs = append(cs, []any{c.n, c.last, c.corrupt})
|
|
}
|
|
cases = append(cases, map[string]any{"name": name, "chunks": cs, "trailing": trailing, "length": len(b), "sha256": sum(b), "text": w.texts.of(err)})
|
|
}
|
|
last := func(cs []chunkSpec) []chunkSpec { cs[len(cs)-1].last = true; return cs }
|
|
add("the first base in one chunk", []chunkSpec{{n: len(plain), last: true}}, 0)
|
|
add("16 chunks, the last of full length: 1 MiB", last(full(16)), 0)
|
|
add("16 chunks and a byte in a 17th", append(full(16), chunkSpec{n: 1, last: true}), 0)
|
|
add("16 chunks and a 17th that does not authenticate, which Go never reads", append(full(16), chunkSpec{n: 1, last: true, corrupt: true}), 0)
|
|
add("16 chunks and an empty 17th, which Go never reads", append(full(16), chunkSpec{n: 0, last: true}), 0)
|
|
add("16 chunks and nothing after them, which Go never reads", full(16), 0)
|
|
add("the 16th chunk does not authenticate", append(append(full(15), chunkSpec{n: chunk, corrupt: true}), chunkSpec{n: 1, last: true}), 0)
|
|
add("16 chunks, the last of full length, and a byte after it, which Go never reads", last(full(16)), 1)
|
|
add("15 chunks, the last of full length, and a byte after it", last(full(15)), 1)
|
|
add("15 chunks and a short 16th", append(full(15), chunkSpec{n: 10, last: true}), 0)
|
|
add("15 chunks and an empty 16th", append(full(15), chunkSpec{n: 0, last: true}), 0)
|
|
add("15 chunks and nothing after them", full(15), 0)
|
|
add("17 chunks, the last of full length", last(full(17)), 0)
|
|
add("2 chunks, the first marked as the last", []chunkSpec{{n: chunk, last: true}, {n: 5, last: true}}, 0)
|
|
return map[string]any{"round": 1000, "header": hx(file[:end]), "nonce": hx(nonce), "file_key": hx(fileKey), "stream_key": hx(streamKey), "cases": cases}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The envelope
|
|
|
|
// envelopeCases gives OpenEnvelope of the locator of the envelope, each field
|
|
// edited, on its rest, edited: [name, key, header edits, rest digest, rest
|
|
// size, capsule digest, rest edits, text, SHA-256 of the .dkc]; a field
|
|
// empty is that of the envelope.
|
|
func (w *world) envelopeCases() []any {
|
|
t := w.texts
|
|
var out []any
|
|
other := must(age.GenerateX25519Identity())
|
|
otherRaw := must(agewrap.RawX25519Identity(other))
|
|
add := func(name string, mutate func(l *locator.Locator), rest []byte) {
|
|
l := *w.loc
|
|
l.EnvelopeHeader = slices.Clone(l.EnvelopeHeader)
|
|
mutate(&l)
|
|
dkc, err := l.OpenEnvelope(rest)
|
|
c := []any{name, "", edits(w.loc.EnvelopeHeader, l.EnvelopeHeader), "", l.RestSize, "", edits(w.rest, rest), t.of(err), ""}
|
|
if l.EnvelopeKey != w.loc.EnvelopeKey {
|
|
c[1] = hx(l.EnvelopeKey[:])
|
|
}
|
|
if l.RestDigest != w.loc.RestDigest {
|
|
c[3] = hx(l.RestDigest[:])
|
|
}
|
|
if l.CapsuleDigest != w.loc.CapsuleDigest {
|
|
c[5] = hx(l.CapsuleDigest[:])
|
|
}
|
|
if err == nil {
|
|
c[8] = sum(dkc)
|
|
}
|
|
out = append(out, c)
|
|
}
|
|
none := func(*locator.Locator) {}
|
|
rest := w.rest
|
|
flip := func(b []byte, at int) []byte { b = slices.Clone(b); b[at] ^= 1; return b }
|
|
withDigest := func(r []byte) func(*locator.Locator) {
|
|
return func(l *locator.Locator) { l.RestDigest = sha256.Sum256(r); l.RestSize = uint64(len(r)) }
|
|
}
|
|
add("the rest", none, rest)
|
|
add("a byte of the rest changed", none, flip(rest, len(rest)/2))
|
|
add("the rest cut by a byte", none, rest[:len(rest)-1])
|
|
add("a byte after the rest", none, append(slices.Clone(rest), 0))
|
|
add("no rest", none, nil)
|
|
add("rest_size one byte more", func(l *locator.Locator) { l.RestSize++ }, rest)
|
|
add("rest_size of 2^53 - 1", func(l *locator.Locator) { l.RestSize = 1<<53 - 1 }, rest)
|
|
add("resto_digest of another rest", func(l *locator.Locator) { l.RestDigest[0] ^= 1 }, rest)
|
|
add("capsule_digest of another .dkc", func(l *locator.Locator) { l.CapsuleDigest[31] ^= 1 }, rest)
|
|
add("the key of another envelope", func(l *locator.Locator) { copy(l.EnvelopeKey[:], otherRaw) }, rest)
|
|
add("the key and the digest wrong", func(l *locator.Locator) { copy(l.EnvelopeKey[:], otherRaw); l.CapsuleDigest[0] ^= 1 }, rest)
|
|
// A rest changed whose digest and size the locator gives: age finds it.
|
|
for _, at := range []int{0, 15, 16, 17, len(rest) / 2, len(rest) - 17, len(rest) - 1} {
|
|
r := flip(rest, at)
|
|
add(fmt.Sprintf("the byte %d of the rest changed, with its digest", at), withDigest(r), r)
|
|
}
|
|
for _, n := range []int{0, 1, 15, 16, 17, 32, len(rest) - 16, len(rest) - 1} {
|
|
r := rest[:n]
|
|
add(fmt.Sprintf("the rest cut to %d bytes, with its digest", n), withDigest(r), r)
|
|
}
|
|
r := append(slices.Clone(rest), 7)
|
|
add("a byte after the rest, with its digest", withDigest(r), r)
|
|
// The header, edited.
|
|
header := w.loc.EnvelopeHeader
|
|
lines := strings.SplitAfter(string(header), "\n")
|
|
lines = lines[:len(lines)-1]
|
|
setHeader := func(h string) func(*locator.Locator) {
|
|
return func(l *locator.Locator) { l.EnvelopeHeader = []byte(h) }
|
|
}
|
|
add("the MAC of the header changed", setHeader(strings.Join(lines[:len(lines)-1], "")+"--- "+strings.Repeat("A", 43)+"\n"), rest)
|
|
add("the header without its MAC line", setHeader(strings.Join(lines[:len(lines)-1], "")), rest)
|
|
add("the header without its last line feed", setHeader(string(header[:len(header)-1])), rest)
|
|
add("an X25519 stanza of another share", setHeader(strings.Replace(string(header), lines[1][10:20], strings.Repeat("A", 10), 1)), rest)
|
|
add("the intro line of another version", setHeader("age-encryption.org/v2\n"+strings.Join(lines[1:], "")), rest)
|
|
add("one byte of header", setHeader("a"), rest)
|
|
add("a scrypt stanza", setHeader("age-encryption.org/v1\n-> scrypt "+base64.RawStdEncoding.EncodeToString(patterned("salt", 16))+" 10\n"+
|
|
base64.RawStdEncoding.EncodeToString(patterned("body", 32))+"\n--- "+strings.Repeat("A", 43)+"\n"), rest)
|
|
return out
|
|
}
|
|
|
|
// restInCases gives RestIn on two resources, the host with the rest of the
|
|
// envelope after its 300 bytes and nothing: [resource, offset, rest_size,
|
|
// text, SHA-256 of the rest read].
|
|
func (w *world) restInCases() ([]byte, []any) {
|
|
t := w.texts
|
|
var out []any
|
|
host := patterned("host file", 300)
|
|
file := append(slices.Clone(host), w.rest...)
|
|
add := func(resource []byte, offset, size uint64) {
|
|
l := *w.loc
|
|
l.RestSize = size
|
|
r, err := l.RestIn(resource, offset)
|
|
which := 0
|
|
if resource == nil {
|
|
which = 1
|
|
}
|
|
c := []any{which, offset, size, t.of(err), ""}
|
|
if err == nil {
|
|
c[4] = sum(r)
|
|
}
|
|
out = append(out, c)
|
|
}
|
|
n := uint64(len(file))
|
|
rs := w.loc.RestSize
|
|
for _, o := range []uint64{0, 1, 299, 300, 301, n - rs - 1, n - rs, n - rs + 1, n - 1, n, n + 1, 1 << 53} {
|
|
add(file, o, rs)
|
|
}
|
|
add(file, 300, 0)
|
|
add(file, n, 0)
|
|
add(file, n+1, 0)
|
|
add(file, 0, n)
|
|
add(file, 0, n+1)
|
|
add(nil, 0, 0)
|
|
add(nil, 0, 1)
|
|
add(file, 1, 1<<53)
|
|
return file, out
|
|
}
|
|
|
|
// hideCases gives Hide of hosts and rests: [host, rest, file, offset].
|
|
func hideCases() []any {
|
|
var out []any
|
|
for _, c := range [][2][]byte{{nil, nil}, {nil, []byte("rest")}, {[]byte("host"), nil}, {patterned("h", 100), patterned("r", 50)}} {
|
|
f, off := locator.Hide(c[0], c[1])
|
|
out = append(out, []any{hx(c[0]), hx(c[1]), hx(f), off})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// splitCases gives the split of an age file, as NewEnvelope splits it with
|
|
// headerEnd: [file, text, end]. NewEnvelope itself writes the envelope with
|
|
// age; the file of the envelope is the first case.
|
|
func (w *world) splitCases() []any {
|
|
t := w.texts
|
|
var out []any
|
|
add := func(b []byte) {
|
|
n, err := headerEnd(b)
|
|
out = append(out, []any{hx(b), t.of(err), n})
|
|
}
|
|
f := w.file
|
|
end := len(w.loc.EnvelopeHeader)
|
|
add(f)
|
|
add(f[:end])
|
|
add(f[:end-1])
|
|
add(f[:end-2])
|
|
add(nil)
|
|
add([]byte("--- x\n"))
|
|
add([]byte("\n--- x\n"))
|
|
add([]byte("\n--- x"))
|
|
add([]byte("\n--- \n"))
|
|
add([]byte("a\n---x\n--- y\nz"))
|
|
add([]byte("a\n--- y\n--- z\n"))
|
|
return out
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The extension
|
|
|
|
// infoCases gives Info.Extension: [note, profile_id, round, sealed, text,
|
|
// length of the data, its SHA-256]; sealed is a hexadecimal, an index into
|
|
// the sealed bases, or the number of zeros; the data of the first cases
|
|
// that write one are in datas.
|
|
func (w *world) infoCases(sealed [][]byte, rounds []uint64) []any {
|
|
t := w.texts
|
|
var out []any
|
|
dk := func(round uint64) datekey.DateKey {
|
|
return must(datekey.Resolve(w.p, must(datekey.RoundTime(w.p, round))))
|
|
}
|
|
add := func(note string, d datekey.DateKey, s any) {
|
|
var b []byte
|
|
switch x := s.(type) {
|
|
case nil:
|
|
case int:
|
|
b = sealed[x]
|
|
case []byte:
|
|
b = x
|
|
case string:
|
|
n := must(strconv.Atoi(x))
|
|
b = make([]byte, n)
|
|
s = map[string]any{"zeros": n}
|
|
}
|
|
if x, ok := s.([]byte); ok {
|
|
s = hx(x)
|
|
}
|
|
x, err := (&locator.Info{Note: note, DateKey: d, Sealed: b}).Extension()
|
|
c := []any{note, d.ProfileID, d.Round, s, t.of(err), 0, ""}
|
|
if err == nil {
|
|
if x.ID != extension.CapsuleID || x.Version != 1 {
|
|
log.Fatal("not datekeys.capsule")
|
|
}
|
|
c[5], c[6] = len(x.Data), sum(x.Data)
|
|
if len(w.datas) < 4 {
|
|
w.datas = append(w.datas, x.Data)
|
|
}
|
|
}
|
|
out = append(out, c)
|
|
}
|
|
d1000 := dk(1000)
|
|
add("", d1000, nil)
|
|
add("Cartas del viaje a Lisboa", d1000, nil)
|
|
add("Cartas del viaje a Lisboa", d1000, 0)
|
|
add("", d1000, 0)
|
|
for i, round := range rounds {
|
|
add("n", dk(round), i)
|
|
if round != 1000 {
|
|
add("n", d1000, i)
|
|
}
|
|
}
|
|
add("", d1000, []byte("an age file"))
|
|
add("", d1000, []byte{})
|
|
add("", d1000, "1048576")
|
|
add("", d1000, "1048577")
|
|
add("a\tb", d1000, nil)
|
|
add("a\nb", d1000, nil)
|
|
add(" a", d1000, nil)
|
|
add(strings.Repeat("n", 1024), d1000, nil)
|
|
add(strings.Repeat("n", 1025), d1000, nil)
|
|
add("\xc3\xb1and\xc3\xba", d1000, nil)
|
|
add("\xe2\x80\xae", d1000, nil)
|
|
add("", datekey.DateKey{ProfileID: "datekeys:quicknet:v1", Round: 0}, nil)
|
|
add("", datekey.DateKey{ProfileID: "datekeys:quicknet:v1", Round: 1 << 53}, nil)
|
|
add("", datekey.DateKey{ProfileID: "datekeys:quicknet:v1", Round: 1<<53 - 1}, nil)
|
|
add("", datekey.DateKey{ProfileID: "Datekeys:quicknet:v1", Round: 1000}, nil)
|
|
add("", datekey.DateKey{ProfileID: "datekeys:other:v1", Round: 1000}, nil)
|
|
add("", datekey.DateKey{}, nil)
|
|
add("a\tb", datekey.DateKey{}, []byte{})
|
|
return out
|
|
}
|
|
|
|
// openInfoCases gives Info.OpenLocator with the default registry of Go:
|
|
// [profile_id, round, sealed, release, text, summary]; sealed is an index
|
|
// into the sealed bases or null.
|
|
func (w *world) openInfoCases(sealed [][]byte) []any {
|
|
reg := must(profile.Default())
|
|
dk := func(round uint64) datekey.DateKey {
|
|
return must(datekey.Resolve(w.p, must(datekey.RoundTime(w.p, round))))
|
|
}
|
|
var out []any
|
|
add := func(d datekey.DateKey, base any, release uint64) {
|
|
var b []byte
|
|
if i, ok := base.(int); ok {
|
|
b = sealed[i]
|
|
}
|
|
l, err := (&locator.Info{DateKey: d, Sealed: b}).OpenLocator(reg, w.release(release))
|
|
var s any
|
|
if err == nil {
|
|
s = summary(l)
|
|
}
|
|
out = append(out, []any{d.ProfileID, d.Round, base, release, w.texts.of(err), s})
|
|
}
|
|
add(dk(1000), 0, 1000)
|
|
add(dk(1000), nil, 1000)
|
|
add(datekey.DateKey{ProfileID: "datekeys:other:v1", Round: 1000}, 0, 1000)
|
|
add(dk(1001), 1, 1001)
|
|
add(dk(1001), 1, 1000)
|
|
add(dk(1000), 1, 1000)
|
|
add(dk(2000), 2, 2000)
|
|
return out
|
|
}
|
|
|
|
// parseCase is ParseInfo: [id, version, base, edits, text, note, compact
|
|
// datekey, round, SHA-256 of sealed or null]: the data is the edits of the
|
|
// base of parse_bases, or absent when base is -1.
|
|
func (w *world) parseCase(bases [][]byte, id string, version uint64, data []byte) []any {
|
|
x := extension.Extension{ID: id, Version: version, Data: data}
|
|
info, err := locator.ParseInfo(x)
|
|
base, d := -1, [][]any{}
|
|
if data != nil {
|
|
// The base whose edits are the shortest.
|
|
best := -1
|
|
for i, b := range bases {
|
|
e := edits(b, data)
|
|
if n := len(asciiJSON(e)); best < 0 || n < best {
|
|
base, best, d = i, n, e
|
|
}
|
|
}
|
|
}
|
|
c := []any{id, version, base, d, w.texts.of(err), "", "", 0, nil}
|
|
if err != nil && !errors.Is(err, datekeys.ErrExtensionDataInvalid) {
|
|
log.Fatalf("ParseInfo: %v", err)
|
|
}
|
|
if err == nil {
|
|
c[5], c[6], c[7] = info.Note, info.DateKey.Compact(), info.DateKey.Round
|
|
if info.Sealed != nil {
|
|
c[8] = sum(info.Sealed)
|
|
}
|
|
}
|
|
return c
|
|
}
|
|
|
|
func (w *world) parseCases(sealed [][]byte) ([][]byte, []any) {
|
|
var out []any
|
|
var bases [][]byte
|
|
add := func(data []byte) { out = append(out, w.parseCase(bases, extension.CapsuleID, 1, data)) }
|
|
dk := must(datekey.Resolve(w.p, must(datekey.RoundTime(w.p, 1000)))).Compact()
|
|
build := func(f func(e *enc)) []byte { var e enc; f(&e); return e.b }
|
|
pairs := func(ps ...rawPair) []byte {
|
|
return build(func(e *enc) {
|
|
e.mapOf(uint64(len(ps)))
|
|
for _, p := range ps {
|
|
e.uint(p.key)
|
|
p.val(e)
|
|
}
|
|
})
|
|
}
|
|
text := func(k uint64, s string) rawPair { return rawPair{k, func(e *enc) { e.text(s) }} }
|
|
bstr := func(k uint64, b []byte) rawPair { return rawPair{k, func(e *enc) { e.bstr(b) }} }
|
|
good := pairs(text(0, "Cartas"), text(1, dk), bstr(2, sealed[0]))
|
|
bases = [][]byte{{}, good}
|
|
for _, x := range sealed[1:] {
|
|
bases = append(bases, pairs(text(1, dk), bstr(2, x)))
|
|
}
|
|
out = append(out, w.parseCase(bases, extension.NoteID, 1, good), w.parseCase(bases, extension.CapsuleID, 2, good),
|
|
w.parseCase(bases, extension.CapsuleID, 0, good), w.parseCase(bases, extension.CapsuleID, 1, nil), w.parseCase(bases, "datekeys.capsulE", 1, good))
|
|
add(good)
|
|
add(pairs(text(1, dk)))
|
|
add(pairs(text(0, "n"), text(1, dk)))
|
|
add(pairs(text(1, dk), bstr(2, sealed[0])))
|
|
add(pairs())
|
|
add(pairs(text(0, "n")))
|
|
add(pairs(text(0, ""), text(1, dk)))
|
|
add(pairs(text(0, strings.Repeat("n", 1024)), text(1, dk)))
|
|
add(pairs(text(0, strings.Repeat("n", 1025)), text(1, dk)))
|
|
add(pairs(text(0, "a\tb"), text(1, dk)))
|
|
add(pairs(text(0, "a "), text(1, dk)))
|
|
add(pairs(rawPair{0, func(e *enc) { e.head(3, 2, 0); e.raw([]byte{0xff, 0xfe}) }}, text(1, dk)))
|
|
add(pairs(rawPair{0, func(e *enc) { e.uint(5) }}, text(1, dk)))
|
|
add(pairs(text(1, dk), text(0, "n")))
|
|
add(pairs(text(1, dk), text(1, dk)))
|
|
add(pairs(text(1, dk), bstr(3, []byte{0})))
|
|
add(pairs(text(1, dk), text(1<<53, "x")))
|
|
add(pairs(text(1, "")))
|
|
add(pairs(text(1, "dk1_")))
|
|
add(pairs(text(1, dk+"A")))
|
|
add(pairs(text(1, strings.Repeat("d", 1024))))
|
|
add(pairs(text(1, strings.Repeat("d", 1025))))
|
|
add(pairs(bstr(1, []byte(dk))))
|
|
add(pairs(text(1, strings.ToUpper(dk[:4])+dk[4:])))
|
|
// A DateKey whose JSON has its members in another order (spec §19).
|
|
raw := must(base64.RawURLEncoding.DecodeString(strings.TrimPrefix(dk, datekey.Prefix)))
|
|
var members map[string]any
|
|
if err := json.Unmarshal(raw, &members); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
add(pairs(text(1, datekey.Prefix+base64.RawURLEncoding.EncodeToString(must(json.Marshal(members))))))
|
|
add(pairs(text(1, datekey.Prefix+base64.URLEncoding.EncodeToString(raw))))
|
|
// The locator: empty, another round, stanzas, garbage.
|
|
add(pairs(text(1, dk), bstr(2, nil)))
|
|
add(pairs(text(1, dk), bstr(2, []byte{0})))
|
|
for i := range sealed {
|
|
add(pairs(text(1, dk), bstr(2, sealed[i])))
|
|
}
|
|
s := sealed[0]
|
|
end := must(headerEnd(s))
|
|
add(pairs(text(1, dk), bstr(2, s[:end])))
|
|
add(pairs(text(1, dk), bstr(2, s[:end-1])))
|
|
lines := strings.SplitAfter(string(s[:end]), "\n")
|
|
lines = lines[:len(lines)-1]
|
|
stanza := strings.Fields(strings.TrimSpace(lines[1]))
|
|
withLine := func(i int, line string) []byte {
|
|
ls := slices.Clone(lines)
|
|
ls[i] = line
|
|
return append([]byte(strings.Join(ls, "")), s[end:]...)
|
|
}
|
|
for _, line := range []string{"-> tlock 01000 " + stanza[3] + "\n", "-> tlock 1000\n", "-> tlock 1000 " + stanza[3] + " x\n",
|
|
"-> tlock 1000 " + strings.Repeat("0", 64) + "\n", "-> X25519 1000 " + stanza[3] + "\n", "-> tlock 1000 " + stanza[3] + "\n"} {
|
|
add(pairs(text(1, dk), bstr(2, withLine(1, line))))
|
|
}
|
|
add(pairs(text(1, dk), bstr(2, withLine(len(lines)-1, "--- "+strings.Repeat("A", 43)+"\n"))))
|
|
two := slices.Clone(lines)
|
|
two = slices.Insert(two, 1, slices.Clone(two[1:len(two)-1])...)
|
|
add(pairs(text(1, dk), bstr(2, append([]byte(strings.Join(two, "")), s[end:]...))))
|
|
// Data edited at random.
|
|
r := newRand("parse")
|
|
for i := 0; i < 80; i++ {
|
|
b := slices.Clone(good)
|
|
at := r.IntN(min(len(b), 600))
|
|
switch r.IntN(4) {
|
|
case 0, 1:
|
|
b[at] ^= 1 << r.IntN(8)
|
|
case 2:
|
|
b = slices.Delete(b, at, at+1)
|
|
case 3:
|
|
b = b[:at]
|
|
}
|
|
add(b)
|
|
}
|
|
return bases, out
|
|
}
|
|
|
|
// registryCases give the registry of locator.Standard on datekeys.capsule
|
|
// in a .dkk: [id, version, index into datas or -1 for none,
|
|
// CheckNoncriticalIn, CheckCriticalIn,
|
|
// CheckWrite noncritical, CheckWrite critical, CheckWrite noncritical with
|
|
// extension.Standard]; CheckNoncriticalIn as the texts of the unusable.
|
|
func (w *world) registryCases(datas [][]byte) []any {
|
|
t := w.texts
|
|
std := locator.Standard()
|
|
var out []any
|
|
add := func(x extension.Extension) {
|
|
exts := []extension.Extension{x}
|
|
unusable := []any{}
|
|
for _, u := range extension.CheckNoncriticalIn(extension.AccessKey, exts, std) {
|
|
if u.ID != x.ID || u.Version != x.Version || !errors.Is(u.Err, datekeys.ErrExtensionDataInvalid) {
|
|
log.Fatal("an unusable extension of another kind")
|
|
}
|
|
unusable = append(unusable, t.of(u.Err))
|
|
}
|
|
d := slices.IndexFunc(datas, func(d []byte) bool { return x.Data != nil && bytes.Equal(d, x.Data) })
|
|
out = append(out, []any{x.ID, x.Version, d, unusable,
|
|
t.of(extension.CheckCriticalIn(extension.AccessKey, exts, std)),
|
|
t.of(extension.CheckWrite(std, extension.AccessKey, extension.Noncritical, exts)),
|
|
t.of(extension.CheckWrite(std, extension.AccessKey, extension.Critical, exts)),
|
|
t.of(extension.CheckWrite(extension.Standard{}, extension.AccessKey, extension.Noncritical, exts))})
|
|
}
|
|
for _, d := range datas {
|
|
add(extension.Extension{ID: extension.CapsuleID, Version: 1, Data: d})
|
|
}
|
|
add(extension.Extension{ID: extension.CapsuleID, Version: 1})
|
|
add(extension.Extension{ID: extension.CapsuleID, Version: 2, Data: []byte{0xa0}})
|
|
return out
|
|
}
|
|
|
|
// capsuleCases open a fixture of format 2 with its .dkk carrying
|
|
// datekeys.capsule, with locator.Standard: the result, the unusable
|
|
// extensions of the .dkk and the checks.
|
|
func (w *world) capsuleCases(testdata string, datas [][]byte) map[string]any {
|
|
t := w.texts
|
|
const name = "format2_time_and_key_portable"
|
|
dkc := must(os.ReadFile(filepath.Join(testdata, "fixtures", name+".dkc")))
|
|
dkk := must(os.ReadFile(filepath.Join(testdata, "fixtures", name+".dkk")))
|
|
now := time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC)
|
|
reg := must(profile.Default())
|
|
var cases []any
|
|
add := func(exts []extension.Extension, std bool) {
|
|
k := must(accesskey.Decode(bytes.NewReader(dkk)))
|
|
k.Noncritical = exts
|
|
var buf bytes.Buffer
|
|
if err := accesskey.Encode(&buf, k); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
opts := capsule.OpenOptions{Registry: reg, Source: fixed{w.release(1000)}, Now: func() time.Time { return now }, AccessKeyFile: bytes.NewReader(buf.Bytes())}
|
|
if std {
|
|
opts.Extensions = locator.Standard()
|
|
}
|
|
var out bytes.Buffer
|
|
opened, err := capsule.Open(context.Background(), &out, bytes.NewReader(dkc), opts)
|
|
unusable := []any{}
|
|
checks := []any{}
|
|
if opened != nil {
|
|
for _, u := range opened.UnusableAccessKeyExtensions {
|
|
unusable = append(unusable, []any{u.ID, u.Version, t.of(u.Err)})
|
|
}
|
|
for _, c := range opened.Inspection.Checks {
|
|
checks = append(checks, []any{c.Step, c.Name, c.OK, c.Detail, c.Error})
|
|
}
|
|
}
|
|
var spec []any
|
|
for _, x := range exts {
|
|
i := slices.IndexFunc(datas, func(d []byte) bool { return bytes.Equal(d, x.Data) })
|
|
spec = append(spec, []any{x.ID, x.Version, i, hx(x.Data)})
|
|
}
|
|
if spec == nil {
|
|
spec = []any{}
|
|
}
|
|
cases = append(cases, map[string]any{"noncritical": spec, "dkk_sha256": sum(buf.Bytes()), "standard": std, "text": t.of(err), "unusable": unusable,
|
|
"checks": checks, "content": sum(out.Bytes())})
|
|
}
|
|
for _, d := range datas {
|
|
x := extension.Extension{ID: extension.CapsuleID, Version: 1, Data: d}
|
|
add([]extension.Extension{x}, true)
|
|
}
|
|
add([]extension.Extension{{ID: extension.CapsuleID, Version: 1, Data: datas[0]}, {ID: "org.example", Version: 1, Data: []byte{1}}}, true)
|
|
add([]extension.Extension{{ID: extension.CapsuleID, Version: 1, Data: datas[len(datas)-1]}}, false)
|
|
add(nil, true)
|
|
return map[string]any{"fixture": name, "now": now.Format(time.RFC3339Nano), "cases": cases}
|
|
}
|
|
|
|
type fixed struct{ r provider.Release }
|
|
|
|
func (f fixed) Fetch(context.Context, *profile.Profile, provider.Condition) (provider.Release, error) {
|
|
return f.r, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// testdata/vectors/locator.json
|
|
|
|
type locatorFile struct {
|
|
Spec string `json:"spec"`
|
|
Round uint64 `json:"round"`
|
|
Sealed string `json:"locator_sealed"`
|
|
Plaintext string `json:"locator_plaintext"`
|
|
Extension string `json:"extension_data"`
|
|
Host string `json:"host"`
|
|
EnvelopeHeader string `json:"envelope_header"`
|
|
Rest string `json:"rest"`
|
|
URICases []struct {
|
|
URI string `json:"uri"`
|
|
OK bool `json:"ok"`
|
|
} `json:"uri_cases"`
|
|
Mixed struct {
|
|
Plaintext string `json:"locator_plaintext"`
|
|
Sealed string `json:"locator_sealed"`
|
|
} `json:"mixed"`
|
|
RestCases []struct {
|
|
Name string `json:"name"`
|
|
Resource string `json:"resource"`
|
|
Offset uint64 `json:"offset"`
|
|
Opens bool `json:"opens"`
|
|
} `json:"rest_cases"`
|
|
ExtensionCases []struct {
|
|
Name string `json:"name"`
|
|
Data string `json:"extension_data"`
|
|
OK bool `json:"ok"`
|
|
} `json:"extension_cases"`
|
|
PlaintextCases []struct {
|
|
Name string `json:"name"`
|
|
Plaintext string `json:"locator_plaintext"`
|
|
OK bool `json:"ok"`
|
|
} `json:"plaintext_cases"`
|
|
}
|
|
|
|
func unhex(s string) []byte { return must(hex.DecodeString(s)) }
|
|
|
|
// testdataCases give the texts of the cases of locator.json: the addresses
|
|
// in locator_uris.json, and here the extensions, the plaintexts and the
|
|
// rests.
|
|
func (w *world) testdataCases(v *locatorFile, uriTexts *textTable) ([]any, map[string]any) {
|
|
var uris []any
|
|
for _, c := range v.URICases {
|
|
u := uriCase(uriTexts, c.URI)
|
|
if (u[1] == 0) != c.OK {
|
|
log.Fatalf("%q: not the verdict of locator.json", c.URI)
|
|
}
|
|
uris = append(uris, u)
|
|
}
|
|
t := w.texts
|
|
main, err := locator.Open(w.p, v.Round, w.release(v.Round), unhex(v.Sealed))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
mixed, err := locator.Open(w.p, v.Round, w.release(v.Round), unhex(v.Mixed.Sealed))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
ext := []any{}
|
|
for _, c := range v.ExtensionCases {
|
|
_, err := locator.ParseInfo(extension.Extension{ID: extension.CapsuleID, Version: 1, Data: unhex(c.Data)})
|
|
if (err == nil) != c.OK {
|
|
log.Fatalf("%s: not the verdict of locator.json", c.Name)
|
|
}
|
|
ext = append(ext, t.of(err))
|
|
}
|
|
plain := []any{}
|
|
for _, c := range v.PlaintextCases {
|
|
_, err := locator.Unmarshal(unhex(c.Plaintext))
|
|
if (err == nil) != c.OK {
|
|
log.Fatalf("%s: not the verdict of locator.json", c.Name)
|
|
}
|
|
plain = append(plain, t.of(err))
|
|
}
|
|
rest := []any{}
|
|
for _, c := range v.RestCases {
|
|
r, err := main.RestIn(unhex(c.Resource), c.Offset)
|
|
openErr := errors.New("not read")
|
|
if err == nil {
|
|
_, openErr = main.OpenEnvelope(r)
|
|
}
|
|
rest = append(rest, []any{t.of(err), t.of(openErr)})
|
|
}
|
|
return uris, map[string]any{"main": summary(main), "mixed": summary(mixed), "extension_cases": ext, "plaintext_cases": plain, "rest_cases": rest}
|
|
}
|
|
|
|
// releases reads the releases of the fixtures, public data of drand.
|
|
func releases(testdata string) map[uint64]provider.Release {
|
|
out := map[uint64]provider.Release{}
|
|
files := must(filepath.Glob(filepath.Join(testdata, "fixtures", "*.json")))
|
|
slices.Sort(files)
|
|
for _, f := range files {
|
|
var v struct {
|
|
Release *struct {
|
|
Round uint64 `json:"round"`
|
|
Signature string `json:"signature"`
|
|
} `json:"release"`
|
|
}
|
|
if err := json.Unmarshal(must(os.ReadFile(f)), &v); err != nil || v.Release == nil {
|
|
continue
|
|
}
|
|
r := provider.Release{Round: v.Release.Round, Signature: unhex(v.Release.Signature)}
|
|
if old, ok := out[r.Round]; ok && !bytes.Equal(old.Signature, r.Signature) {
|
|
log.Fatalf("two releases of round %d", r.Round)
|
|
}
|
|
out[r.Round] = r
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func writeFile(dir, name string, b []byte) {
|
|
if len(b) > 900<<10 {
|
|
log.Printf("TOO BIG %s: %d bytes", name, len(b))
|
|
}
|
|
path := filepath.Join(dir, name)
|
|
if err := os.WriteFile(path, b, 0o644); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Printf("wrote %s, %d bytes\n", path, len(b))
|
|
}
|
|
|
|
func dartConstant(name, json, comment string) []byte {
|
|
return []byte("// Generated by tool/locator_go_vectors.go, for the tests that also run\n" +
|
|
"// compiled to JavaScript, where no file can be read. Do not edit.\n\n" +
|
|
"/// " + comment + "\n" +
|
|
"const " + name + " = r'''\n" + json + "''';\n")
|
|
}
|
|
|
|
func main() {
|
|
testdata := flag.String("testdata", "", "the directory testdata of datekeys-dart")
|
|
outDir := flag.String("out", "", "the directory test/vectors of datekeys-dart")
|
|
flag.Parse()
|
|
if *testdata == "" || *outDir == "" {
|
|
log.Fatal("usage: go run locator_go_vectors.go -testdata DIR -out DIR")
|
|
}
|
|
var v locatorFile
|
|
if err := json.Unmarshal(must(os.ReadFile(filepath.Join(*testdata, "vectors", "locator.json"))), &v); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
w := &world{p: profile.Quicknet(), texts: newTexts(), releases: releases(*testdata)}
|
|
setCryptoRand("envelope")
|
|
w.dkc = patterned("locator dkc", 777)
|
|
w.loc, w.rest = must2(locator.NewEnvelope(w.dkc))
|
|
w.file = append(slices.Clone(w.loc.EnvelopeHeader), w.rest...)
|
|
w.loc.Addresses = []locator.Address{{URI: "https://ejemplo.org/foto.jpg", Offset: 3000}, {URI: "ipfs://" + locatorCID}}
|
|
|
|
// The addresses.
|
|
ut := newTexts()
|
|
testdataURIs, testdataRest := w.testdataCases(&v, ut)
|
|
uris := uriCases(ut, newRand("uris"))
|
|
ips := ipCases(newRand("ips"))
|
|
public := publicCases(newRand("public"))
|
|
wtf8 := wtf8Cases(ut)
|
|
ud := newDocument()
|
|
ud.value("description", "The addresses of a locator (spec v0.12 §44.1) for lib/src/locator.dart; see tool/locator_go_vectors.go. testdata: [uri, text, host] of locator.CheckURI and "+
|
|
"Address.Host on the uri_cases of testdata/vectors/locator.json, in their order; uris: the same on addresses built at the edges of §44.1 and drawn from a seed; "+
|
|
"wtf8: [bytes, text] of CheckURI on bytes with a surrogate. "+
|
|
"A text is an index into texts, 0 for none. ips: [s, false] or [s, true, bytes, zone, String, publicIP] of netip.ParseAddr. public: [bytes, publicIP, String] "+
|
|
"of netip.AddrFromSlice, or [bytes, null] for a length that is no address.")
|
|
ud.value("generator", "tool/locator_go_vectors.go, "+runtime.Version())
|
|
ud.list("texts", anyList(ut.list))
|
|
ud.list("testdata", testdataURIs)
|
|
ud.list("uris", uris)
|
|
ud.list("wtf8", wtf8)
|
|
ud.list("ips", ips)
|
|
ud.list("public", public)
|
|
uj := ud.bytes()
|
|
writeFile(*outDir, "locator_uris.json", uj)
|
|
writeFile(*outDir, "locator_uris.g.dart", dartConstant("locatorUrisJson", string(uj), "The text of test/vectors/locator_uris.json."))
|
|
|
|
// The locator.
|
|
plainBases, raws := w.plaintextBases()
|
|
plaintexts := w.plaintextCases(plainBases, raws)
|
|
marshal := w.marshalCases()
|
|
sealed, rounds, opens := w.openCases(plainBases)
|
|
limit := w.limitCases(plainBases[0])
|
|
setCryptoRand("envelopes")
|
|
envelopes := w.envelopeCases()
|
|
restResource, restIn := w.restInCases()
|
|
split := w.splitCases()
|
|
setCryptoRand("info")
|
|
info := w.infoCases(sealed, rounds)
|
|
openInfo := w.openInfoCases(sealed)
|
|
parseBases, parse := w.parseCases(sealed)
|
|
datas := append(slices.Clone(w.datas), []byte{0xa0}, []byte("Cartas"))
|
|
registry := w.registryCases(datas)
|
|
capsules := w.capsuleCases(*testdata, datas)
|
|
|
|
rel := map[string]string{}
|
|
for _, round := range []uint64{1000, 1001, 1004, 2000} {
|
|
rel[strconv.FormatUint(round, 10)] = hx(w.release(round).Signature)
|
|
}
|
|
envelope := map[string]any{"dkc": hx(w.dkc), "key": hx(w.loc.EnvelopeKey[:]), "header": hx(w.loc.EnvelopeHeader), "rest": hx(w.rest),
|
|
"rest_digest": hx(w.loc.RestDigest[:]), "rest_size": w.loc.RestSize, "capsule_digest": hx(w.loc.CapsuleDigest[:])}
|
|
var sealedBases []any
|
|
for i := range sealed {
|
|
sealedBases = append(sealedBases, map[string]any{"round": rounds[i], "sealed": hx(sealed[i])})
|
|
}
|
|
|
|
section := func(d *document, full bool) {
|
|
d.value("generator", "tool/locator_go_vectors.go, "+runtime.Version())
|
|
d.list("texts", anyList(w.texts.list))
|
|
d.value("releases", rel)
|
|
d.value("envelope", envelope)
|
|
d.list("padding", paddingRuns())
|
|
d.value("testdata", testdataRest)
|
|
d.list("plaintext_bases", anyList(mapHex(plainBases)))
|
|
d.list("sealed_bases", sealedBases)
|
|
d.list("parse_bases", anyList(mapHex(parseBases)))
|
|
d.list("datas", anyList(mapHex(datas)))
|
|
d.value("rest_in_resource", hx(restResource))
|
|
if full {
|
|
d.list("plaintexts", plaintexts)
|
|
d.list("marshal", marshal)
|
|
d.list("open", opens)
|
|
d.value("limit", limit)
|
|
d.list("envelopes", envelopes)
|
|
d.list("rest_in", restIn)
|
|
d.list("hide", hideCases())
|
|
d.list("split", split)
|
|
d.list("info", info)
|
|
d.list("open_info", openInfo)
|
|
d.list("parse", parse)
|
|
d.list("registry", registry)
|
|
d.value("capsule", capsules)
|
|
return
|
|
}
|
|
d.list("plaintexts", every(plaintexts, 3))
|
|
d.list("marshal", marshal)
|
|
d.list("open", opens[:24])
|
|
d.list("envelopes", envelopes)
|
|
d.list("rest_in", restIn)
|
|
d.list("hide", hideCases())
|
|
d.list("split", split)
|
|
d.list("info", info)
|
|
d.list("open_info", openInfo)
|
|
d.list("parse", every(parse, 2))
|
|
d.list("registry", registry)
|
|
}
|
|
vd := newDocument()
|
|
vd.value("description", "The locator and the envelope of datekeys.capsule (spec v0.12 §44.1) for lib/src/locator.dart; see tool/locator_go_vectors.go for each list. "+
|
|
"A text is an index into texts, 0 for none; an edited value is a list of edits [at, delete, insert] of its base.")
|
|
section(vd, true)
|
|
vj := vd.bytes()
|
|
writeFile(*outDir, "locator_vectors.json", vj)
|
|
pd := newDocument()
|
|
pd.value("description", "Part of test/vectors/locator_vectors.json, for the tests compiled to JavaScript: every third plaintext, the first opens, every second parse, "+
|
|
"and the rest but the limit and the capsule.")
|
|
section(pd, false)
|
|
writeFile(*outDir, "locator_vectors.g.dart", dartConstant("locatorVectorsJson", string(pd.bytes()), "Part of test/vectors/locator_vectors.json."))
|
|
}
|
|
|
|
func must2[A, B any](a A, b B, err error) (A, B) {
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
return a, b
|
|
}
|
|
|
|
func mapHex(bs [][]byte) []string {
|
|
out := make([]string, len(bs))
|
|
for i, b := range bs {
|
|
out[i] = hx(b)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func every(xs []any, n int) []any {
|
|
var out []any
|
|
for i, x := range xs {
|
|
if i%n == 0 {
|
|
out = append(out, x)
|
|
}
|
|
}
|
|
return out
|
|
}
|