You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/internal/testkit/genfixtures/locatorvectors.go

687 lines
29 KiB

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 v0.12.
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, base32 that is not a CID v1,
// and what the text of v0.12 fixes about segments, case, leading zeros and
// the canonical form of a CID.
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:])
// What the text of v0.12 fixes besides, as the reference already did:
// segments of 1 to 63 characters that neither start nor end with a
// hyphen, the scheme in lower case, 0x and the local names in either
// case, an IPv4 and a port without leading zeros, and a CID of at most
// 128 characters in canonical base32, with minimal varints and a digest
// of at least one byte.
add(true, "https://"+strings.Repeat("a", 63)+".org/", "https://a--b.ejemplo.org/", "https://Ejemplo.ORG:443/",
"ipfs://"+cidV1(slices.Concat([]byte{0x01, 0x55, 0x00, 0x4b}, patterned("locator identity cid", 75))))
add(false, "https://"+strings.Repeat("a", 64)+".org/", "https://-a.ejemplo.org/", "https://a-.ejemplo.org/", "https://ejemplo.org./", "https://a..b.org/",
"https://.ejemplo.org/", "HTTPS://ejemplo.org/", "Https://ejemplo.org/", "IPFS://"+locatorCID, "https://ejemplo.0X10/", "https://nas.LOCAL/",
"https://x.Home.Arpa/", "https://foo.LocalHost/", "https://01.2.3.4/", "https://8.8.8.08/", "https://8.8.8.256/",
"https://ejemplo.org:0443/", "https://ejemplo.org:00443/", "https://ejemplo.org:000443/", "https://[2606:4700::1111]:0443/",
"ipfs://"+locatorCID[:len(locatorCID)-1]+"j", "ipfs://"+locatorCID+"==", "ipfs://b"+strings.ToUpper(locatorCID[1:]),
"ipfs://"+cidV1(slices.Concat([]byte{0x81, 0x00, 0x55, 0x12, 0x20}, digest[:])), "ipfs://"+cidV1([]byte{0x01, 0x55, 0x00, 0x00}),
"ipfs://"+cidV1(slices.Concat([]byte{0x01, 0x55, 0x00, 0x50}, patterned("locator identity cid", 80))))
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
}

Powered by TurnKey Linux.