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>
4 days ago
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.
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>
4 days ago
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
Spec v0.12 draft: the rules of an address that the reference applied
The text of 44.1 now says what the reference already refused and a
second implementation that followed the text would have accepted:
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
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. A port is
written without leading zeros: the reference accepted 0443 and 00443 and
refused 000443, the same number, and now refuses the three. Change 6 of
section 76 records it, and section 64 lists the cases.
locator.json is made again with 30 more addresses, 247 in all, and
TestAddresses checks the same rules.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 days ago
// 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.
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>
4 days ago
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 : ] )
Spec v0.12 draft: the rules of an address that the reference applied
The text of 44.1 now says what the reference already refused and a
second implementation that followed the text would have accepted:
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
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. A port is
written without leading zeros: the reference accepted 0443 and 00443 and
refused 000443, the same number, and now refuses the three. Change 6 of
section 76 records it, and section 64 lists the cases.
locator.json is made again with 30 more addresses, 247 in all, and
TestAddresses checks the same rules.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 days ago
// 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 ) ) ) )
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>
4 days ago
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
}