- 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
parent
da9fd2c6e0
commit
ea5c6eb3d0
@ -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
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue