Test data: locator.json with the cases of spec v0.12, section 64

- uri_cases: the first and the last address of each IPv4 block of 44.1,
  with the public addresses next to them; the IPv6 blocks and the
  addresses that hold an IPv4 one; the local names; characters outside
  RFC 3986 and broken percent signs; "." and ".." segments; base32 that
  is not a CID v1.
- mixed: a locator whose http and NAT64 addresses a reader rejects, and
  whose third address it uses to find the rest.
- rest_cases: the rest alone, a host with bytes after the rest, a rest
  with a byte changed, an offset that is not its own, a rest cut short.
- extension_cases: a locator sealed for round 1001 with a DateKey of
  round 1000, and the other data that a reader cannot use.
- plaintext_cases: change 7, the bases 4094, 4070 and 3837 completed to
  4096 with an empty key 6 or a length not in its shortest form, and a
  defect in each field of the map.
- padding_cases: the bases 3837, 4070, 4094 and 4095 and those of the
  next multiple, checked against the rule of 44.1.

The generator checks every case against this module and moves to its
own file. The vector is frozen: delete it to make it again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.12
dev 2 days ago
parent da9fd2c6e0
commit ea5c6eb3d0

@ -42,8 +42,9 @@ type CMSVectorCase struct {
}
// LocatorVectorFile is testdata/vectors/locator.json: the extension
// datekeys.capsule of a .dkk and what it points to (spec v0.11, §44.1). It is
// made once and frozen: the envelope and the sealed locator hold randomness.
// datekeys.capsule of a .dkk and what it points to (spec v0.12, §44.1), and
// what a reader rejects and uses of it (§64). It is made once and frozen: the
// envelope and the sealed locators hold randomness.
type LocatorVectorFile struct {
Description string `json:"description"`
Spec string `json:"spec"`
@ -77,6 +78,21 @@ type LocatorVectorFile struct {
PaddingCases []LocatorPaddingCase `json:"padding_cases"`
// URICases give the verdict of the rules of §44.1 on an address.
URICases []LocatorURICase `json:"uri_cases"`
// Mixed is a second locator of the same envelope, sealed for Round, whose
// addresses break and meet the rules of §44.1: a reader reads it, rejects
// the ones that break them and uses the others.
Mixed LocatorMixed `json:"mixed"`
// RestCases are resources, as a reader downloads them, with the offset
// that an address gives, and whether the rest read there opens the
// envelope of the locator.
RestCases []LocatorRestCase `json:"rest_cases"`
// ExtensionCases are data of datekeys.capsule and whether a reader can use
// them: one it cannot is unusable, never the .dkk (§54).
ExtensionCases []LocatorExtensionCase `json:"extension_cases"`
// PlaintextCases are plaintexts of a locator of the same envelope, each
// with one defect or none, and whether a reader reads them: the map of
// §44.1 and the length that key 6 completes.
PlaintextCases []LocatorPlaintextCase `json:"plaintext_cases"`
}
// LocatorVectorAddress is an address of the locator.
@ -98,3 +114,46 @@ type LocatorURICase struct {
URI string `json:"uri"`
OK bool `json:"ok"`
}
// LocatorMixed is a sealed locator, its plaintext and its addresses, each
// with whether a reader uses it.
type LocatorMixed struct {
Plaintext string `json:"locator_plaintext"`
Sealed string `json:"locator_sealed"`
Addresses []LocatorMixedAddress `json:"addresses"`
}
// LocatorMixedAddress is an address of a locator and whether a reader uses
// it.
type LocatorMixedAddress struct {
URI string `json:"uri"`
Offset uint64 `json:"offset"`
Usable bool `json:"usable"`
}
// LocatorRestCase is a resource in hexadecimal, the offset where an address
// says that the rest starts in it, and whether the rest opens the envelope:
// a reader reads RestSize bytes from the offset, whatever follows, and checks
// that their SHA-256 is RestDigest and that of the .dkc, CapsuleDigest.
type LocatorRestCase struct {
Name string `json:"name"`
Resource string `json:"resource"`
Offset uint64 `json:"offset"`
Opens bool `json:"opens"`
}
// LocatorExtensionCase is the data of a datekeys.capsule extension, version
// 1, and whether a reader can use it.
type LocatorExtensionCase struct {
Name string `json:"name"`
Data string `json:"extension_data"`
OK bool `json:"ok"`
}
// LocatorPlaintextCase is the plaintext of a locator and whether a reader
// reads it.
type LocatorPlaintextCase struct {
Name string `json:"name"`
Plaintext string `json:"locator_plaintext"`
OK bool `json:"ok"`
}

@ -18,12 +18,8 @@ import (
"g.activething.com/go/DateKeys/authorkey"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/datekey"
"g.activething.com/go/DateKeys/extension"
"g.activething.com/go/DateKeys/internal/cms/cmstest"
"g.activething.com/go/DateKeys/internal/testkit"
"g.activething.com/go/DateKeys/locator"
"g.activething.com/go/DateKeys/profile"
)
// frozenVectors writes testdata/vectors/security_cms.json and locator.json
@ -790,78 +786,3 @@ func sha256Of(b []byte) []byte {
h := sha256.Sum256(b)
return h[:]
}
// locatorVectors makes the vector of the extension datekeys.capsule: a .dkc of
// patterned bytes in an envelope, hidden in a host, its locator sealed with
// tlock for round 1000, and the data of the extension with a note.
func locatorVectors() (any, error) {
p := profile.Quicknet()
dkc := patterned("locator dkc", 5000)
loc, rest, err := locator.NewEnvelope(dkc)
if err != nil {
return nil, err
}
host := patterned("host file", 3000)
file, offset := locator.Hide(host, rest)
const cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
loc.Addresses = []locator.Address{{URI: "https://ejemplo.org/foto.jpg", Offset: offset}, {URI: "ipfs://" + cid}}
plain, err := loc.Marshal()
if err != nil {
return nil, err
}
const round = 1000
sealed, err := locator.Seal(p, round, loc)
if err != nil {
return nil, err
}
dk, err := datekey.Resolve(p, mustRoundTime(p, round))
if err != nil {
return nil, err
}
const note = "Cartas del viaje a Lisboa"
x, err := (&locator.Info{Note: note, DateKey: dk, Sealed: sealed}).Extension()
if err != nil {
return nil, err
}
if x.ID != extension.CapsuleID {
return nil, errors.New("not datekeys.capsule")
}
v := testkit.LocatorVectorFile{
Spec: testkit.SpecVersion,
Description: "The extension datekeys.capsule of a .dkk and what it points to (spec v0.11, 44.1): an envelope of age with its header apart from its rest, " +
"the rest hidden in a host file, the locator sealed with tlock for round 1000, and the data of the extension. Frozen. See testdata/README.md.",
Round: round, DateKey: dk.Compact(), Note: note, DKC: hex.EncodeToString(dkc), Rest: hex.EncodeToString(rest), Header: hex.EncodeToString(loc.EnvelopeHeader),
Host: hex.EncodeToString(file), HostOffset: offset, Plaintext: hex.EncodeToString(plain), Sealed: hex.EncodeToString(sealed), Extension: hex.EncodeToString(x.Data),
EnvelopeKey: hex.EncodeToString(loc.EnvelopeKey[:]), RestDigest: hex32(loc.RestDigest), RestSize: loc.RestSize, CapsuleDigest: hex32(loc.CapsuleDigest),
}
for _, a := range loc.Addresses {
v.Addresses = append(v.Addresses, testkit.LocatorVectorAddress{URI: a.URI, Offset: a.Offset, Host: a.Host()})
}
for _, base := range []int{100, 3000, 4000, 4060, 4066, 4067, 4068, 4069, 4070, 4071, 4072, 4090, 4094, 4095, 4096, 4097, 4100, 8160, 8190, 8192, 8193, 12000} {
v.PaddingCases = append(v.PaddingCases, testkit.LocatorPaddingCase{Base: base, Total: locator.PlaintextLength(base)})
}
for _, c := range []struct {
uri string
ok bool
}{
{"https://ejemplo.org/a.bin", true}, {"https://ejemplo.org:8443/x?y=1", true}, {"ipfs://" + cid, true}, {"ipfs://" + cid + "/ruta", true},
{"", false}, {"http://ejemplo.org/a", false}, {"file:///etc/passwd", false}, {"ftp://x/y", false}, {"https://user:pass@ejemplo.org/", false},
{"https://", false}, {"ipfs://notacid", false}, {"https://ejemplo.org/ñ", false}, {"https://ejemplo.org/a b", false},
{"https://%D0%B0pple.com/x", false}, {"https://ejemplo.org%E2%80%AEtxt.exe/", false}, {"https://127.0.0.1/", false}, {"https://[::1]/", false},
{"https://0x7f000001/", false}, {"https://a.com:99999999/", false}, {"https://a.com:0/", false}, {"https://xn--pple-43d.com/", true},
} {
if (locator.CheckURI(c.uri) == nil) != c.ok {
return nil, fmt.Errorf("the address %q: accepted %v, want %v", c.uri, !c.ok, c.ok)
}
v.URICases = append(v.URICases, testkit.LocatorURICase{URI: c.uri, OK: c.ok})
}
return v, nil
}
func mustRoundTime(p *profile.Profile, round uint64) time.Time {
t, err := datekey.RoundTime(p, round)
if err != nil {
panic(err)
}
return t
}

@ -0,0 +1,668 @@
package main
import (
"bytes"
"crypto/sha256"
"encoding/base32"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"slices"
"strings"
"time"
"filippo.io/age"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/agewrap"
"g.activething.com/go/DateKeys/codec"
"g.activething.com/go/DateKeys/datekey"
"g.activething.com/go/DateKeys/extension"
"g.activething.com/go/DateKeys/internal/testkit"
"g.activething.com/go/DateKeys/locator"
"g.activething.com/go/DateKeys/profile"
)
// locatorCID is the CID v1 of the vectors: dag-pb, SHA-256, in base32.
const locatorCID = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
// locatorVectors makes the vector of the extension datekeys.capsule: a .dkc of
// patterned bytes in an envelope, hidden in a host, its locator sealed with
// tlock for round 1000, and the data of the extension with a note. On the
// same envelope it adds what a reader rejects and what it uses (spec v0.12,
// §44.1 and §64): addresses, a locator with addresses that a reader rejects
// next to one it uses, resources of the rest, data of the extension and
// plaintexts of the locator. Each case is checked against this module.
//
// It labels locator.json, as every file of testdata, with SpecVersion: its
// cases are those of the draft v0.12, which this branch implements.
func locatorVectors() (any, error) {
p := profile.Quicknet()
dkc := patterned("locator dkc", 5000)
loc, rest, err := locator.NewEnvelope(dkc)
if err != nil {
return nil, err
}
host := patterned("host file", 3000)
file, offset := locator.Hide(host, rest)
loc.Addresses = []locator.Address{{URI: "https://ejemplo.org/foto.jpg", Offset: offset}, {URI: "ipfs://" + locatorCID}}
plain, err := loc.Marshal()
if err != nil {
return nil, err
}
const round = 1000
sealed, err := locator.Seal(p, round, loc)
if err != nil {
return nil, err
}
dk, err := datekey.Resolve(p, mustRoundTime(p, round))
if err != nil {
return nil, err
}
const note = "Cartas del viaje a Lisboa"
x, err := (&locator.Info{Note: note, DateKey: dk, Sealed: sealed}).Extension()
if err != nil {
return nil, err
}
if x.ID != extension.CapsuleID {
return nil, errors.New("not datekeys.capsule")
}
v := testkit.LocatorVectorFile{
Spec: testkit.SpecVersion,
Description: "The extension datekeys.capsule of a .dkk and what it points to (spec v0.12, 44.1): an envelope of age with its header apart from its rest, " +
"the rest hidden in a host file, the locator sealed with tlock for round 1000, and the data of the extension. On the same envelope, what a reader " +
"rejects and what it uses (64): addresses, a locator with rejected and usable addresses, resources of the rest, data of the extension and " +
"plaintexts of the locator. Frozen. See testdata/README.md.",
Round: round, DateKey: dk.Compact(), Note: note, DKC: hex.EncodeToString(dkc), Rest: hex.EncodeToString(rest), Header: hex.EncodeToString(loc.EnvelopeHeader),
Host: hex.EncodeToString(file), HostOffset: offset, Plaintext: hex.EncodeToString(plain), Sealed: hex.EncodeToString(sealed), Extension: hex.EncodeToString(x.Data),
EnvelopeKey: hex.EncodeToString(loc.EnvelopeKey[:]), RestDigest: hex32(loc.RestDigest), RestSize: loc.RestSize, CapsuleDigest: hex32(loc.CapsuleDigest),
}
for _, a := range loc.Addresses {
v.Addresses = append(v.Addresses, testkit.LocatorVectorAddress{URI: a.URI, Offset: a.Offset, Host: a.Host()})
}
if v.PaddingCases, err = paddingCases(); err != nil {
return nil, err
}
if v.URICases, err = uriCases(); err != nil {
return nil, err
}
if v.Mixed, err = mixedLocator(p, round, loc, file, offset, dkc); err != nil {
return nil, err
}
if v.RestCases, err = restCases(loc, file, rest, offset, dkc); err != nil {
return nil, err
}
if v.ExtensionCases, err = extensionCases(p, round, loc, dk, note); err != nil {
return nil, err
}
if v.PlaintextCases, err = plaintextCases(loc, plain); err != nil {
return nil, err
}
return v, nil
}
// paddingCases gives the length of the plaintext of a locator for lengths of
// its CBOR without key 6 around the boundaries where key 6 cannot complete a
// multiple of 4096, checked against the rule of §44.1: the least multiple that
// holds it, or the next one when 1, 2, 26 or 259 bytes are missing.
func paddingCases() ([]testkit.LocatorPaddingCase, error) {
var out []testkit.LocatorPaddingCase
for _, base := range []int{100, 3000, 3836, 3837, 3838, 4000, 4060, 4066, 4067, 4068, 4069, 4070, 4071, 4072, 4090, 4093, 4094, 4095, 4096, 4097, 4100,
7932, 7933, 7934, 8160, 8165, 8166, 8167, 8189, 8190, 8191, 8192, 8193, 12000, 12287, 12288} {
need := (locator.Block - base%locator.Block) % locator.Block
want := base + need
switch need {
case 1, 2, 26, 259:
want += locator.Block
}
if got := locator.PlaintextLength(base); got != want {
return nil, fmt.Errorf("the padding of a base of %d bytes: %d, want %d", base, got, want)
}
out = append(out, testkit.LocatorPaddingCase{Base: base, Total: want})
}
return out, nil
}
// uriCases gives addresses and whether the rules of §44.1 accept them: those
// of v0.11, and those of §64 in v0.12, an address of each block that is not
// public with its neighbours that are, the local names, the characters
// outside RFC 3986, the "." and ".." segments and base32 that is not a CID v1.
func uriCases() ([]testkit.LocatorURICase, error) {
var cases []testkit.LocatorURICase
add := func(ok bool, uris ...string) {
for _, u := range uris {
cases = append(cases, testkit.LocatorURICase{URI: u, OK: ok})
}
}
// Spec v0.11.
add(true, "https://ejemplo.org/a.bin", "https://ejemplo.org:8443/x?y=1", "ipfs://"+locatorCID, "ipfs://"+locatorCID+"/ruta", "https://xn--pple-43d.com/")
add(false, "", "http://ejemplo.org/a", "file:///etc/passwd", "ftp://x/y", "https://user:pass@ejemplo.org/", "https://", "ipfs://notacid",
"https://ejemplo.org/ñ", "https://ejemplo.org/a b", "https://%D0%B0pple.com/x", "https://ejemplo.org%E2%80%AEtxt.exe/", "https://127.0.0.1/",
"https://[::1]/", "https://0x7f000001/", "https://a.com:99999999/", "https://a.com:0/")
// The form: a scheme and an authority, no userinfo, a port of 1 to 65535.
add(true, "https://ejemplo.org", "https://ejemplo.org/a#parte", "https://ejemplo.org/~ana/(1),x;y=z!$&'*+", "https://ejemplo.org/a%20b",
"https://ejemplo.org/%41", "https://ejemplo.org:65535/", "https://[2606:4700::1111]:8443/a")
add(false, "https:/ejemplo.org/a", "https:ejemplo.org/a", "//ejemplo.org/a", "ejemplo.org/a", "https://@ejemplo.org/", "https://ejemplo.org:443@otro.org/",
"https://ejemplo.org:65536/", "https://ejemplo.org:/", "https://ejemplo.org:8a/", "https://[2606:4700::1111]x/", "https://[2606:4700::1111/")
// Characters that RFC 3986 does not allow, and percent signs.
add(false, "https://ejemplo.org/a\"b", "https://ejemplo.org/a<b", "https://ejemplo.org/a>b", "https://ejemplo.org/a\\b", "https://ejemplo.org/a^b",
"https://ejemplo.org/a`b", "https://ejemplo.org/a{b", "https://ejemplo.org/a|b", "https://ejemplo.org/a}b", "https://ejemplo.org/a\x00b",
"https://ejemplo.org/a\tb", "https://ejemplo.org/a\nb", "https://ejemplo.org/a\x7fb", "https://ejémplo.org/", "https://ejemplo.org/%",
"https://ejemplo.org/%4", "https://ejemplo.org/%zz", "https://ejemplo.org/%4g", "https://ejempl%6F.org/", "https://ejemplo.org\\otro.org/")
// "." and ".." segments, written or with %2e, in the path only.
add(false, "https://ejemplo.org/../a", "https://ejemplo.org/a/../b", "https://ejemplo.org/./a", "https://ejemplo.org/a/.", "https://ejemplo.org/a/..",
"https://ejemplo.org/%2e%2e/a", "https://ejemplo.org/%2E%2E/a", "https://ejemplo.org/.%2e/a", "https://ejemplo.org/%2e/a", "https://ejemplo.org/a/..?b=1",
"https://ejemplo.org/a/..#b", "ipfs://"+locatorCID+"/../../ipns/x", "ipfs://"+locatorCID+"/%2e%2e/x")
add(true, "https://ejemplo.org/..a", "https://ejemplo.org/a..", "https://ejemplo.org/...", "https://ejemplo.org/.well-known/a", "https://ejemplo.org/%2e%2ea",
"https://ejemplo.org/a//b", "https://ejemplo.org/a?b=/../c", "https://ejemplo.org/a#/../b")
// The IPv4 blocks of §44.1: the first and the last address of each, and
// the public addresses next to them.
for _, b := range []struct{ first, last, before, after string }{
{"0.0.0.0", "0.255.255.255", "", "1.0.0.0"},
{"10.0.0.0", "10.255.255.255", "9.255.255.255", "11.0.0.0"},
{"100.64.0.0", "100.127.255.255", "100.63.255.255", "100.128.0.0"},
{"127.0.0.0", "127.255.255.255", "126.255.255.255", "128.0.0.0"},
{"169.254.0.0", "169.254.255.255", "169.253.255.255", "169.255.0.0"},
{"172.16.0.0", "172.31.255.255", "172.15.255.255", "172.32.0.0"},
{"192.0.0.0", "192.0.0.255", "191.255.255.255", "192.0.1.0"},
{"192.0.2.0", "192.0.2.255", "192.0.1.255", "192.0.3.0"},
{"192.88.99.0", "192.88.99.255", "192.88.98.255", "192.88.100.0"},
{"192.168.0.0", "192.168.255.255", "192.167.255.255", "192.169.0.0"},
{"198.18.0.0", "198.19.255.255", "198.17.255.255", "198.20.0.0"},
{"198.51.100.0", "198.51.100.255", "198.51.99.255", "198.51.101.0"},
{"203.0.113.0", "203.0.113.255", "203.0.112.255", "203.0.114.0"},
{"224.0.0.0", "239.255.255.255", "223.255.255.255", ""},
{"240.0.0.0", "255.255.255.255", "", ""},
} {
add(false, "https://"+b.first+"/", "https://"+b.last+"/")
for _, a := range []string{b.before, b.after} {
if a != "" {
add(true, "https://"+a+"/")
}
}
}
add(false, "https://10.1.2.3/", "https://172.20.0.1/", "https://192.168.1.1/", "https://169.254.169.254/", "https://100.100.100.200/")
add(true, "https://8.8.8.8/", "https://1.1.1.1:8443/a")
// IPv6: 2000::/3 outside its four blocks, which leaves out the addresses
// that hold an IPv4 one.
add(false, "https://[::]/", "https://[::ffff:127.0.0.1]/", "https://[::ffff:8.8.8.8]/", "https://[::8.8.8.8]/", "https://[64:ff9b::7f00:1]/",
"https://[64:ff9b::808:808]/", "https://[64:ff9b:1::808:808]/", "https://[2002::1]/", "https://[2002:808:808::1]/", "https://[2001::1]/",
"https://[2001:0:4136:e378:8000:63bf:3fff:fdd2]/", "https://[2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff]/", "https://[2001:db8::1]/",
"https://[2001:db8:ffff:ffff:ffff:ffff:ffff:ffff]/", "https://[3fff::1]/", "https://[3fff:fff:ffff:ffff:ffff:ffff:ffff:ffff]/", "https://[fe80::1]/",
"https://[fe80::1%25eth0]/", "https://[fec0::1]/", "https://[fc00::1]/", "https://[fd12:3456::1]/", "https://[ff02::1]/", "https://[100::1]/",
"https://[1fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]/", "https://[4000::]/", "https://[1.2.3.4]/", "https://[v1.x]/")
add(true, "https://[2000::]/", "https://[2001:200::]/", "https://[2001:db7:ffff:ffff:ffff:ffff:ffff:ffff]/", "https://[2001:db9::]/", "https://[2003::1]/",
"https://[3ffe::1]/", "https://[3fff:1000::]/", "https://[2606:4700:4700::1111]/", "https://[2a00:1450:4001:830::200e]/")
// Names: those that only a machine or a local network resolves, and a
// last segment that is numeric or starts with 0x.
add(false, "https://localhost/", "https://foo.localhost/", "https://intranet/", "https://a.local/", "https://router.home.arpa/", "https://home.arpa/",
"https://x.internal/", "https://x.invalid/", "https://x.test/", "https://x.example/",
"https://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion/", "https://ejemplo.0x10/", "https://ejemplo.0xg/", "https://ejemplo.0x/",
"https://2130706433/", "https://127.1/", "https://1.2.3.4.5/", "https://ejemplo.org.1/", "https://a_b.ejemplo.org/")
add(true, "https://localhost.ejemplo.org/", "https://local.ejemplo.org/", "https://example.com/", "https://test.ejemplo.org/", "https://onion.ejemplo.org/",
"https://123.ejemplo.org/", "https://a-b.ejemplo.org/")
// CIDs: "b", version 1, a codec and a multihash whose length is that of
// its digest, with nothing after it.
digest := sha256.Sum256([]byte("locator raw cid"))
long := patterned("locator sha2-512 cid", 64)
add(true, "ipfs://"+cidV1(slices.Concat([]byte{0x01, 0x55, 0x12, 0x20}, digest[:])), "ipfs://"+cidV1(slices.Concat([]byte{0x01, 0x70, 0x13, 0x40}, long)),
"ipfs://"+locatorCID+"/a/b.jpg")
add(false, "ipfs://b", "ipfs://Qm"+strings.Repeat("a", 44), "ipfs://B"+strings.ToUpper(locatorCID[1:]),
"ipfs://"+cidV1(slices.Concat([]byte{0x00, 0x70, 0x12, 0x20}, digest[:])), "ipfs://"+cidV1(slices.Concat([]byte{0x02, 0x70, 0x12, 0x20}, digest[:])),
"ipfs://"+cidV1(slices.Concat([]byte{0x01, 0x55, 0x12, 0x20}, digest[:31])), "ipfs://"+cidV1(slices.Concat([]byte{0x01, 0x55, 0x12, 0x20}, digest[:], []byte{0})),
"ipfs://"+locatorCID[:len(locatorCID)-1]+"1", "ipfs://"+locatorCID[:20]+"0"+locatorCID[21:])
seen := map[string]bool{}
for _, c := range cases {
if seen[c.URI] {
return nil, fmt.Errorf("the address %q twice", c.URI)
}
seen[c.URI] = true
if (locator.CheckURI(c.URI) == nil) != c.OK {
return nil, fmt.Errorf("the address %q: accepted %v, want %v", c.URI, !c.OK, c.OK)
}
}
return cases, nil
}
// cidV1 writes the bytes of a CID in base32 with the prefix "b" of multibase:
// lower case, without padding.
func cidV1(b []byte) string {
return "b" + strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b))
}
// mixedLocator seals for round a locator of the envelope of loc whose first
// two addresses a reader rejects: an http one, and an IPv6 address of NAT64
// that leads to 127.0.0.1 (spec v0.12, §76, change 6). A writer never writes
// them, so its plaintext is encoded here. A reader keeps the locator and uses
// the third address, which finds the rest in the host.
func mixedLocator(p *profile.Profile, round uint64, loc *locator.Locator, file []byte, offset uint64, dkc []byte) (testkit.LocatorMixed, error) {
m := testkit.LocatorMixed{Addresses: []testkit.LocatorMixedAddress{
{URI: "http://ejemplo.org/foto.jpg", Offset: offset},
{URI: "https://[64:ff9b::7f00:1]/foto.jpg", Offset: offset},
{URI: "https://ejemplo.org/foto.jpg", Offset: offset, Usable: true},
}}
var addrs []rawAddress
var usable []locator.Address
for _, a := range m.Addresses {
addrs = append(addrs, rawAddress{uri: a.URI, offset: a.Offset})
if a.Usable {
usable = append(usable, locator.Address{URI: a.URI, Offset: a.Offset})
}
}
plain, err := paddedLocator(loc, addrs, loc.RestSize)
if err != nil {
return m, err
}
sealed, err := sealLocator(p, round, plain)
if err != nil {
return m, err
}
back, err := locator.Open(p, round, testkit.Release(round), sealed)
if err != nil {
return m, fmt.Errorf("the mixed locator: %w", err)
}
if !slices.Equal(back.Usable(), usable) {
return m, fmt.Errorf("the mixed locator: usable %v", back.Usable())
}
r, err := back.RestIn(file, usable[0].Offset)
if err != nil {
return m, err
}
if got, err := back.OpenEnvelope(r); err != nil || !bytes.Equal(got, dkc) {
return m, fmt.Errorf("the mixed locator does not open the envelope: %v", err)
}
m.Plaintext, m.Sealed = hex.EncodeToString(plain), hex.EncodeToString(sealed)
return m, nil
}
// restCases gives resources of the rest of loc and whether the rest read from
// them at an offset opens the envelope (spec §44.1, §64): a reader reads only
// RestSize bytes from the offset, and their SHA-256 must be resto_digest.
func restCases(loc *locator.Locator, file, rest []byte, offset uint64, dkc []byte) ([]testkit.LocatorRestCase, error) {
changed := bytes.Clone(rest)
changed[len(changed)/2] ^= 1
var out []testkit.LocatorRestCase
for _, c := range []struct {
name string
resource []byte
offset uint64
opens bool
}{
{"the rest alone, at an address without an offset", rest, 0, true},
{"the host with the rest at its offset and 1000 bytes after it: only rest_size bytes from the offset are read", slices.Concat(file, patterned("after the rest", 1000)), offset, true},
{"the rest with a byte changed, as when a host is recompressed: its SHA-256 is not resto_digest", changed, 0, false},
{"the host at an offset that is not the one of the rest: a byte earlier", file, offset - 1, false},
{"the rest cut by a byte", rest[:len(rest)-1], 0, false},
} {
opens := false
if r, err := loc.RestIn(c.resource, c.offset); err == nil {
got, err := loc.OpenEnvelope(r)
opens = err == nil && bytes.Equal(got, dkc)
}
if opens != c.opens {
return nil, fmt.Errorf("the resource %q: opens %v, want %v", c.name, opens, c.opens)
}
out = append(out, testkit.LocatorRestCase{Name: c.name, Resource: hex.EncodeToString(c.resource), Offset: c.offset, Opens: c.opens})
}
return out, nil
}
// extensionCases gives data of datekeys.capsule that a reader can use and
// cannot (spec §44.1, §54, §64), encoded here so that each has only the
// defect that its name says: a writer never writes them.
func extensionCases(p *profile.Profile, round uint64, loc *locator.Locator, dk datekey.DateKey, note string) ([]testkit.LocatorExtensionCase, error) {
other, err := locator.Seal(p, round+1, loc)
if err != nil {
return nil, err
}
// The members of the JSON of the DateKey in another order (§19).
raw, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(dk.Compact(), datekey.Prefix))
if err != nil {
return nil, err
}
var members map[string]any
if err := json.Unmarshal(raw, &members); err != nil {
return nil, err
}
sorted, err := json.Marshal(members)
if err != nil {
return nil, err
}
reordered := datekey.Prefix + base64.RawURLEncoding.EncodeToString(sorted)
if reordered == dk.Compact() {
return nil, errors.New("the members of the DateKey are already sorted")
}
var out []testkit.LocatorExtensionCase
for _, c := range []struct {
name string
enc func(e *codec.Encoder)
ok bool
}{
{"the note and the date, without a locator", func(e *codec.Encoder) { e.Map(2); e.Uint(0); e.Text(note); e.Uint(1); e.Text(dk.Compact()) }, true},
{"a locator sealed for round 1001, and compact_datekey of round 1000: unusable", func(e *codec.Encoder) {
e.Map(3)
e.Uint(0)
e.Text(note)
e.Uint(1)
e.Text(dk.Compact())
e.Uint(2)
e.Bstr(other)
}, false},
{"a locator that is not an age file", func(e *codec.Encoder) {
e.Map(2)
e.Uint(1)
e.Text(dk.Compact())
e.Uint(2)
e.Bstr([]byte("an age file"))
}, false},
{"an empty locator", func(e *codec.Encoder) { e.Map(2); e.Uint(1); e.Text(dk.Compact()); e.Uint(2); e.Bstr([]byte{}) }, false},
{"no compact_datekey", func(e *codec.Encoder) { e.Map(1); e.Uint(0); e.Text(note) }, false},
{"compact_datekey with the members of its JSON in another order: not its canonical form", func(e *codec.Encoder) { e.Map(1); e.Uint(1); e.Text(reordered) }, false},
{"a note with a tab, which the rules of the public note forbid", func(e *codec.Encoder) {
e.Map(2)
e.Uint(0)
e.Text("Cartas\tdel viaje")
e.Uint(1)
e.Text(dk.Compact())
}, false},
{"a key 3, which 44.1 does not define", func(e *codec.Encoder) { e.Map(2); e.Uint(1); e.Text(dk.Compact()); e.Uint(3); e.Bstr([]byte{0}) }, false},
} {
var e codec.Encoder
c.enc(&e)
data, err := e.Out()
if err != nil {
return nil, err
}
_, err = locator.ParseInfo(extension.Extension{ID: extension.CapsuleID, Version: 1, Data: data})
if (err == nil) != c.ok || err != nil && !errors.Is(err, datekeys.ErrExtensionDataInvalid) {
return nil, fmt.Errorf("the extension %q: %v", c.name, err)
}
out = append(out, testkit.LocatorExtensionCase{Name: c.name, Data: hex.EncodeToString(data), OK: c.ok})
}
return out, nil
}
// plaintextCases gives plaintexts of a locator of the envelope of loc, each
// with the defect that its name says or none, and whether a reader reads them
// (spec v0.12, §44.1 and its CDDL; §76, change 7). They are encoded here: a
// writer never writes the ones with a defect.
func plaintextCases(loc *locator.Locator, plain []byte) ([]testkit.LocatorPlaintextCase, error) {
var out []testkit.LocatorPlaintextCase
add := func(name string, b []byte, ok bool) error {
if _, err := locator.Unmarshal(b); (err == nil) != ok {
return fmt.Errorf("the plaintext %q: read %v, want %v: %v", name, err == nil, ok, err)
}
out = append(out, testkit.LocatorPlaintextCase{Name: name, Plaintext: hex.EncodeToString(b), OK: ok})
return nil
}
// The bases that key 6 cannot complete to 4096: 4094 lacks 2 bytes, 4070
// lacks 26 and 3837, 259. Only a key 6 of no byte, or one whose length is
// not in its shortest form, would do it.
a4094, err := addressesFor(loc, 4094)
if err != nil {
return nil, err
}
l4094 := *loc
l4094.Addresses = nil
for _, a := range a4094 {
l4094.Addresses = append(l4094.Addresses, locator.Address{URI: a.uri, Offset: a.offset})
}
b, err := l4094.Marshal()
if err != nil || len(b) != 2*locator.Block {
return nil, fmt.Errorf("a base of 4094 bytes: %d bytes, %v", len(b), err)
}
if err := add("a base of 4094 bytes, which key 6 completes to 8192", b, true); err != nil {
return nil, err
}
if b, err = locatorCBOR(loc, a4094, loc.RestSize, 0); err != nil {
return nil, err
}
if err := add("a base of 4094 bytes completed to 4096 with an empty key 6, 06 40: key 6 has at least one byte", b, false); err != nil {
return nil, err
}
for _, c := range []struct {
base, pad int
head []byte
name string
}{
{4070, 23, []byte{0x58, 0x17}, "a base of 4070 bytes completed to 4096 with a key 6 of 23 bytes whose length takes two bytes, 58 17: not its shortest form"},
{3837, 255, []byte{0x59, 0x00, 0xff}, "a base of 3837 bytes completed to 4096 with a key 6 of 255 bytes whose length takes three bytes, 59 00 ff: not its shortest form"},
} {
addrs, err := addressesFor(loc, c.base)
if err != nil {
return nil, err
}
short, err := locatorCBOR(loc, addrs, loc.RestSize, c.pad)
if err != nil {
return nil, err
}
// The key, the shortest head of the byte string and the zeros end it.
at := len(short) - c.pad - bstrHeadLen(c.pad)
if len(short) != locator.Block-1 || short[at-1] != 6 {
return nil, fmt.Errorf("a base of %d bytes: %d bytes", c.base, len(short))
}
if err := add(c.name, slices.Concat(short[:at], c.head, short[len(short)-c.pad:]), false); err != nil {
return nil, err
}
}
// The fields of the map, each padded to the length that Marshal gives, so
// that it has no other defect.
one := []rawAddress{{uri: "https://ejemplo.org/foto.jpg"}}
var eight, nine []rawAddress
for i := range locator.MaxAddresses + 1 {
a := rawAddress{uri: fmt.Sprintf("https://ejemplo.org/%d.jpg", i+1)}
if i < locator.MaxAddresses {
eight = append(eight, a)
}
nine = append(nine, a)
}
for _, c := range []struct {
name string
addrs []rawAddress
restSize uint64
ok bool
}{
{"eight addresses, the most", eight, loc.RestSize, true},
{"no address", nil, loc.RestSize, false},
{"nine addresses", nine, loc.RestSize, false},
{"an empty address", []rawAddress{{uri: ""}}, loc.RestSize, false},
{"an address of 1025 bytes: the locator does not read, not only the address", []rawAddress{{uri: "https://ejemplo.org/" + strings.Repeat("a", 1005)}}, loc.RestSize, false},
{"an offset of 0 written: an offset of 0 is written by leaving it out", []rawAddress{{uri: "https://ejemplo.org/foto.jpg", zero: true}}, loc.RestSize, false},
{"an offset of 2^53, not a safe integer", []rawAddress{{uri: "https://ejemplo.org/foto.jpg", offset: 1 << 53}}, loc.RestSize, false},
{"resto_size of 2^53, not a safe integer", one, 1 << 53, false},
} {
b, err := paddedLocator(loc, c.addrs, c.restSize)
if err != nil {
return nil, err
}
if err := add(c.name, b, c.ok); err != nil {
return nil, err
}
}
// The length: the least multiple, and nothing but zeros in key 6.
base, err := locatorCBOR(loc, one, loc.RestSize, -1)
if err != nil {
return nil, err
}
two, err := locatorCBOR(loc, one, loc.RestSize, fill(len(base), 2*locator.Block))
if err != nil || len(two) != 2*locator.Block {
return nil, fmt.Errorf("two blocks: %d bytes, %v", len(two), err)
}
if plain[len(plain)-1] != 0 || len(plain) != locator.Block {
return nil, errors.New("the plaintext of the vector does not end in its padding")
}
nonzero := bytes.Clone(plain)
nonzero[len(nonzero)-1] = 1
for _, c := range []struct {
name string
b []byte
}{
{"8192 bytes where 4096 suffice", two},
{"padding that is not zeros", nonzero},
{"the plaintext cut by a byte", plain[:len(plain)-1]},
{"a byte after the map", slices.Concat(plain, []byte{0})},
} {
if err := add(c.name, c.b, false); err != nil {
return nil, err
}
}
return out, nil
}
// rawAddress is an address as the plaintext of a locator writes it; zero
// writes key 1 even when the offset is 0.
type rawAddress struct {
uri string
offset uint64
zero bool
}
// locatorCBOR encodes the plaintext of a locator with the envelope of loc as
// Locator.Marshal does, without its checks: pad < 0 leaves key 6 out.
func locatorCBOR(loc *locator.Locator, addrs []rawAddress, restSize uint64, pad int) ([]byte, error) {
var e codec.Encoder
if pad < 0 {
e.Map(6)
} else {
e.Map(7)
}
e.Uint(0)
e.Array(len(addrs))
for _, a := range addrs {
if a.offset == 0 && !a.zero {
e.Map(1)
} else {
e.Map(2)
}
e.Uint(0)
e.Text(a.uri)
if a.offset != 0 || a.zero {
e.Uint(1)
e.Uint(a.offset)
}
}
e.Uint(1)
e.Bstr(loc.EnvelopeKey[:])
e.Uint(2)
e.Bstr(loc.EnvelopeHeader)
e.Uint(3)
e.Bstr(loc.RestDigest[:])
e.Uint(4)
e.Uint(restSize)
e.Uint(5)
e.Bstr(loc.CapsuleDigest[:])
if pad >= 0 {
e.Uint(6)
e.Bstr(make([]byte, pad))
}
return e.Out()
}
// paddedLocator is locatorCBOR with the key 6 that makes it the least
// multiple of 4096 bytes that holds it, or none when it already is one (spec
// §44.1).
func paddedLocator(loc *locator.Locator, addrs []rawAddress, restSize uint64) ([]byte, error) {
base, err := locatorCBOR(loc, addrs, restSize, -1)
if err != nil {
return nil, err
}
total := locator.PlaintextLength(len(base))
if total == len(base) {
return base, nil
}
pad := fill(len(base), total)
if pad < 1 {
return nil, fmt.Errorf("no key 6 completes %d bytes from %d", total, len(base))
}
return locatorCBOR(loc, addrs, restSize, pad)
}
// fill returns the length of key 6, 0 included, that makes a CBOR of base
// bytes measure total, or -1 when none does.
func fill(base, total int) int {
for pad := 0; base+2+pad <= total; pad++ {
if base+1+bstrHeadLen(pad)+pad == total {
return pad
}
}
return -1
}
// bstrHeadLen is the length of the shortest head of a byte string of n bytes.
func bstrHeadLen(n int) int {
switch {
case n < 24:
return 1
case n < 256:
return 2
case n < 65536:
return 3
}
return 5
}
// addressesFor returns addresses that §44.1 accepts and that make the CBOR of
// a locator of the envelope of loc, without key 6, measure base bytes.
func addressesFor(loc *locator.Locator, base int) ([]rawAddress, error) {
for _, n := range []int{980, 880} {
long := rawAddress{uri: "https://ejemplo.org/" + strings.Repeat("a", n)}
for count := range locator.MaxAddresses {
for k := 1; k <= locator.MaxURILen-20; k++ {
addrs := append(slices.Repeat([]rawAddress{long}, count), rawAddress{uri: "https://ejemplo.org/" + strings.Repeat("b", k)})
b, err := locatorCBOR(loc, addrs, loc.RestSize, -1)
if err != nil {
return nil, err
}
if len(b) == base {
return addrs, nil
}
if len(b) > base {
break
}
}
}
}
return nil, fmt.Errorf("no addresses make a base of %d bytes", base)
}
// sealLocator seals the plaintext of a locator for round as locator.Seal
// does, for a plaintext that Marshal does not write.
func sealLocator(p *profile.Profile, round uint64, plain []byte) ([]byte, error) {
r, err := agewrap.NewTimeRecipient(p, round)
if err != nil {
return nil, err
}
var buf bytes.Buffer
w, err := age.Encrypt(&buf, r)
if err != nil {
return nil, err
}
if _, err := w.Write(plain); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func mustRoundTime(p *profile.Profile, round uint64) time.Time {
t, err := datekey.RoundTime(p, round)
if err != nil {
panic(err)
}
return t
}

@ -17,8 +17,15 @@ func FuzzUnmarshal(f *testing.F) {
if err := testkit.ReadJSON("../testdata/vectors/locator.json", &v); err != nil {
f.Fatal(err)
}
if b, err := hex.DecodeString(v.Plaintext); err == nil {
f.Add(b)
for _, s := range []string{v.Plaintext, v.Mixed.Plaintext} {
if b, err := hex.DecodeString(s); err == nil {
f.Add(b)
}
}
for _, c := range v.PlaintextCases {
if b, err := hex.DecodeString(c.Plaintext); err == nil {
f.Add(b)
}
}
f.Add([]byte{0xa0})
f.Fuzz(func(t *testing.T, b []byte) {
@ -44,6 +51,11 @@ func FuzzParseInfo(f *testing.F) {
if b, err := hex.DecodeString(v.Extension); err == nil {
f.Add(b)
}
for _, c := range v.ExtensionCases {
if b, err := hex.DecodeString(c.Data); err == nil {
f.Add(b)
}
}
f.Fuzz(func(t *testing.T, b []byte) {
_, err := locator.ParseInfo(extension.Extension{ID: extension.CapsuleID, Version: 1, Data: b})
if err != nil && !bytes.Contains([]byte(err.Error()), []byte("ERR_EXTENSION_DATA_INVALID")) {

@ -3,9 +3,12 @@ package locator_test
import (
"bytes"
"encoding/hex"
"errors"
"path/filepath"
"slices"
"testing"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/extension"
"g.activething.com/go/DateKeys/internal/testkit"
"g.activething.com/go/DateKeys/locator"
@ -24,8 +27,10 @@ func unhex(t *testing.T, s string) []byte {
// testdata/vectors/locator.json is frozen: the extension of the .dkk reads,
// the sealed locator opens with the release of its round, the rest is found
// in the host at its offset, the envelope gives the .dkc back, and the rules
// of the addresses and of the padding give what the file says (spec v0.11,
// §44.1).
// of the addresses and of the padding give what the file says (spec v0.12,
// §44.1). So do its cases of §64: a locator whose addresses a reader rejects
// and uses in turn, the resources of the rest, the data of the extension and
// the plaintexts of the locator.
func TestLocatorVectors(t *testing.T) {
var v testkit.LocatorVectorFile
if err := testkit.ReadJSON(filepath.Join("..", "testdata", "vectors", "locator.json"), &v); err != nil {
@ -73,4 +78,59 @@ func TestLocatorVectors(t *testing.T) {
t.Errorf("%q: accepted %v, want %v", c.URI, got, c.OK)
}
}
// The mixed locator reads: a reader rejects the addresses that break the
// rules and uses the other, which finds the rest in the host.
want := unhex(t, v.DKC)
mixed, err := locator.Open(p, v.Round, testkit.Release(v.Round), unhex(t, v.Mixed.Sealed))
if err != nil {
t.Fatal(err)
}
if back, err := locator.Unmarshal(unhex(t, v.Mixed.Plaintext)); err != nil || len(back.Addresses) != len(v.Mixed.Addresses) || len(mixed.Addresses) != len(v.Mixed.Addresses) {
t.Fatalf("the mixed locator: %v", err)
}
var usable []locator.Address
for i, a := range v.Mixed.Addresses {
if mixed.Addresses[i] != (locator.Address{URI: a.URI, Offset: a.Offset}) {
t.Errorf("mixed address %d: %+v", i, mixed.Addresses[i])
}
if a.Usable {
usable = append(usable, mixed.Addresses[i])
}
}
if got := mixed.Usable(); len(usable) == 0 || !slices.Equal(got, usable) {
t.Fatalf("usable: %v, want %v", got, usable)
}
if rest, err = mixed.RestIn(unhex(t, v.Host), usable[0].Offset); err != nil {
t.Fatal(err)
}
if dkc, err := mixed.OpenEnvelope(rest); err != nil || !bytes.Equal(dkc, want) {
t.Fatalf("the envelope of the mixed locator: %v", err)
}
// A reader reads RestSize bytes from the offset, whatever follows, and
// uses them only when their SHA-256 is resto_digest.
for _, c := range v.RestCases {
opens := false
if r, err := loc.RestIn(unhex(t, c.Resource), c.Offset); err == nil {
dkc, err := loc.OpenEnvelope(r)
opens = err == nil && bytes.Equal(dkc, want)
}
if opens != c.Opens {
t.Errorf("%s: opens %v, want %v", c.Name, opens, c.Opens)
}
}
// An extension that a reader cannot use carries only
// ERR_EXTENSION_DATA_INVALID (spec §54).
for _, c := range v.ExtensionCases {
_, err := locator.ParseInfo(extension.Extension{ID: extension.CapsuleID, Version: 1, Data: unhex(t, c.Data)})
if (err == nil) != c.OK || err != nil && !errors.Is(err, datekeys.ErrExtensionDataInvalid) {
t.Errorf("%s: %v", c.Name, err)
}
}
for _, c := range v.PlaintextCases {
if _, err := locator.Unmarshal(unhex(t, c.Plaintext)); (err == nil) != c.OK {
t.Errorf("%s: %v", c.Name, err)
}
}
}

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save

Powered by TurnKey Linux.