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/ab", "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 }