//go:build ignore // Prints the vectors of the locator of datekeys-ts, src/lib/dkc/locator.ts, // ipaddr.ts, ageio.ts and envelope.ts, into src/lib/dkc/testing/: the // results, the normative codes and the texts of package locator of // datekeys-go at the tag spec-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 JavaScript string 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 is the generator of the stage 7a of datekeys-dart, with the seeds of // this repository, so that the cases drawn are other than those of Dart. // 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. // // It imports only public packages of the reference implementation, and runs // in a module of it without changing anything there: an export of the tag // spec-v0.12, so that no change in progress in datekeys-go is read. // // git -C ../datekeys-go archive spec-v0.12 | tar -x -C /tmp/dkgo // (cd /tmp/dkgo && go run .../datekeys-ts/scripts/locator-go-vectors.go \ // -testdata .../datekeys-ts/testdata -out .../datekeys-ts/src/lib/dkc/testing) 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-ts locator: " + 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, escaped as the generator of datekeys-dart escapes it. 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 JavaScript string 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 main() { testdata := flag.String("testdata", "", "the directory testdata of datekeys-ts") outDir := flag.String("out", "", "the directory src/lib/dkc/testing of datekeys-ts") 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 src/lib/dkc/locator.ts; see scripts/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", "scripts/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) // 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])}) } vd := newDocument() vd.value("description", "The locator and the envelope of datekeys.capsule (spec v0.12 §44.1) for src/lib/dkc/locator.ts and envelope.ts; see scripts/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.") vd.value("generator", "scripts/locator-go-vectors.go, "+runtime.Version()) vd.list("texts", anyList(w.texts.list)) vd.value("releases", rel) vd.value("envelope", envelope) vd.list("padding", paddingRuns()) vd.value("testdata", testdataRest) vd.list("plaintext_bases", anyList(mapHex(plainBases))) vd.list("sealed_bases", sealedBases) vd.list("parse_bases", anyList(mapHex(parseBases))) vd.list("datas", anyList(mapHex(datas))) vd.value("rest_in_resource", hx(restResource)) vd.list("plaintexts", plaintexts) vd.list("marshal", marshal) vd.list("open", opens) vd.value("limit", limit) vd.list("envelopes", envelopes) vd.list("rest_in", restIn) vd.list("hide", hideCases()) vd.list("split", split) vd.list("info", info) vd.list("open_info", openInfo) vd.list("parse", parse) vd.list("registry", registry) vd.value("capsule", capsules) vj := vd.bytes() writeFile(*outDir, "locator-vectors.json", vj) } 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 }