@ -17,7 +17,7 @@ import (
"errors"
"fmt"
"io"
"net/ url "
"net/ netip "
"strings"
"filippo.io/age"
@ -59,6 +59,12 @@ type Info struct {
// Extension returns the extension for the noncritical array of a .dkk.
func ( i * Info ) Extension ( ) ( extension . Extension , error ) {
if d , err := datekey . Parse ( i . DateKey . Compact ( ) ) ; err != nil || d != i . DateKey {
return extension . Extension { } , errors . New ( "locator: Info.DateKey is not a canonical DateKey" )
}
if i . Sealed != nil && ( len ( i . Sealed ) < 1 || len ( i . Sealed ) > maxSealed ) {
return extension . Extension { } , fmt . Errorf ( "locator: a sealed locator of %d bytes, not 1 to %d" , len ( i . Sealed ) , maxSealed )
}
if i . Note != "" {
if err := extension . CheckNote ( i . Note ) ; err != nil {
return extension . Extension { } , err
@ -183,28 +189,130 @@ func CheckURI(uri string) error {
return errors . New ( "locator: an address with a character outside printable ASCII" )
}
}
u, err := url . Parse ( uri )
scheme, host , err := splitAuthority ( uri )
if err != nil {
return fmt . Errorf ( "locator: an address that is not a URI: %w" , err )
}
if u . User != nil || strings . Contains ( u . Host , "@" ) {
return errors . New ( "locator: an address with userinfo" )
return err
}
switch u. S cheme {
switch scheme {
case "https" :
if u . Hostname ( ) == "" {
return errors . New ( "locator: an https address without a host" )
}
return checkHost ( host )
case "ipfs" :
if ! isCIDv1 ( u. H ost) {
if ! isCIDv1 ( host ) {
return errors . New ( "locator: an ipfs address without a CID v1" )
}
default :
return fmt . Errorf ( "locator: the scheme %q: only https and ipfs" , u . Scheme )
return nil
}
return fmt . Errorf ( "locator: the scheme %q: only https and ipfs" , scheme )
}
// splitAuthority returns the scheme and the raw host of an address, without
// decoding anything: a percent sign in the authority, userinfo and a
// malformed port are refused, so that the host a reader shows is the host an
// HTTP client would use (spec v0.11, §44.1).
func splitAuthority ( uri string ) ( scheme , host string , err error ) {
scheme , rest , ok := strings . Cut ( uri , "://" )
if ! ok || scheme == "" {
return "" , "" , errors . New ( "locator: an address without a scheme and ://" )
}
authority := rest
if i := strings . IndexAny ( rest , "/?#" ) ; i >= 0 {
authority = rest [ : i ]
}
if strings . ContainsAny ( authority , "%@\\" ) {
return "" , "" , errors . New ( "locator: an address with a percent sign, userinfo or a backslash in its authority" )
}
host = authority
if scheme == "https" {
if strings . HasPrefix ( authority , "[" ) {
end := strings . Index ( authority , "]" )
if end < 0 {
return "" , "" , errors . New ( "locator: an address with an unclosed IPv6 literal" )
}
host = authority [ : end + 1 ]
if tail := authority [ end + 1 : ] ; tail != "" {
if err := checkPort ( tail ) ; err != nil {
return "" , "" , err
}
}
} else if h , port , found := strings . Cut ( authority , ":" ) ; found {
host = h
if err := checkPort ( ":" + port ) ; err != nil {
return "" , "" , err
}
}
}
return scheme , host , nil
}
func checkPort ( s string ) error {
if len ( s ) < 2 || s [ 0 ] != ':' || len ( s ) > 6 {
return errors . New ( "locator: an address with a malformed port" )
}
n := 0
for _ , c := range s [ 1 : ] {
if c < '0' || c > '9' {
return errors . New ( "locator: an address with a malformed port" )
}
n = n * 10 + int ( c - '0' )
}
if n < 1 || n > 65535 {
return errors . New ( "locator: an address with a port outside 1 to 65535" )
}
return nil
}
// checkHost accepts a name of letters, digits, hyphens and dots, or an IP
// literal that is not loopback, private, link-local or unspecified: the spec
// forbids following a redirect to those, and an address that starts there
// would defeat the same rule (§44.1). A name whose last label is numeric, or
// is a hexadecimal number, is refused: some clients read it as an IPv4
// address in a form that netip does not.
func checkHost ( host string ) error {
if host == "" {
return errors . New ( "locator: an https address without a host" )
}
if strings . HasPrefix ( host , "[" ) {
a , err := netip . ParseAddr ( strings . Trim ( host , "[]" ) )
if err != nil || ! publicIP ( a ) {
return errors . New ( "locator: an https address with an IPv6 literal that is not public" )
}
return nil
}
labels := strings . Split ( host , "." )
for _ , l := range labels {
if l == "" || len ( l ) > 63 || l [ 0 ] == '-' || l [ len ( l ) - 1 ] == '-' {
return errors . New ( "locator: an https address with a malformed host" )
}
for i := 0 ; i < len ( l ) ; i ++ {
c := l [ i ]
if ! ( c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-' ) {
return errors . New ( "locator: an https address whose host is not letters, digits and hyphens: write its punycode form" )
}
}
}
last := labels [ len ( labels ) - 1 ]
if allDigits ( last ) || strings . HasPrefix ( strings . ToLower ( last ) , "0x" ) {
a , err := netip . ParseAddr ( host )
if err != nil || ! a . Is4 ( ) || ! publicIP ( a ) {
return errors . New ( "locator: an https address with a numeric host that is not a public IPv4 address" )
}
}
return nil
}
func allDigits ( s string ) bool {
for i := 0 ; i < len ( s ) ; i ++ {
if s [ i ] < '0' || s [ i ] > '9' {
return false
}
}
return s != ""
}
func publicIP ( a netip . Addr ) bool {
return ! ( a . IsLoopback ( ) || a . IsPrivate ( ) || a . IsLinkLocalUnicast ( ) || a . IsLinkLocalMulticast ( ) || a . IsMulticast ( ) || a . IsUnspecified ( ) )
}
// isCIDv1 reports whether s looks like a CID v1 in base32, which starts with
// 'b': it checks the alphabet and the length, not the multihash.
func isCIDv1 ( s string ) bool {
@ -222,14 +330,11 @@ func isCIDv1(s string) bool {
// Host returns what a reader shows before it downloads: the host of an https
// address, or the CID of an ipfs one (spec §44.1).
func ( a Address ) Host ( ) string {
u , err := url . Parse ( a . URI )
if err != nil {
if CheckURI ( a . URI ) != nil {
return ""
}
if u . Scheme == "ipfs" {
return u . Host
}
return u . Hostname ( )
_ , host , _ := splitAuthority ( a . URI )
return strings . Trim ( host , "[]" )
}
// Locator is the plaintext of the sealed locator (spec §44.1).
@ -258,6 +363,14 @@ func (l *Locator) validate() error {
if n := len ( l . EnvelopeHeader ) ; n < 1 || n > MaxHeaderLen {
return fmt . Errorf ( "locator: an envelope header of %d bytes, not 1 to %d" , n , MaxHeaderLen )
}
if l . RestSize > codec . MaxSafeUint {
return errors . New ( "locator: a rest larger than 2^53 - 1 bytes" )
}
for _ , a := range l . Addresses {
if a . Offset > codec . MaxSafeUint {
return errors . New ( "locator: an offset larger than 2^53 - 1" )
}
}
return nil
}
@ -346,6 +459,7 @@ func (l *Locator) Marshal() ([]byte, error) {
if pad < 0 {
return base , nil
}
defer clear ( base ) // it holds I_SOBRE
var p codec . Encoder
l . encode ( & p , pad )
out , err := p . Out ( )
@ -384,7 +498,7 @@ func Unmarshal(b []byte) (*Locator, error) {
case 3 :
err = copyBstr ( d , l . RestDigest [ : ] )
case 4 :
l . RestSize , err = d . Uint ( 1 << 63 - 1 )
l . RestSize , err = d . Uint ( codec . MaxSafeUint )
case 5 :
err = copyBstr ( d , l . CapsuleDigest [ : ] )
case 6 :
@ -417,6 +531,7 @@ func Unmarshal(b []byte) (*Locator, error) {
}
// The length is the one Marshal gives: nothing else is canonical.
want , err := l . Marshal ( )
defer clear ( want )
if err != nil || ! bytes . Equal ( want , b ) {
return nil , fmt . Errorf ( "locator: the plaintext is not %d or the least multiple of %d that holds it: %w" , Block , Block , datekeys . ErrNonCanonicalCBOR )
}
@ -453,7 +568,7 @@ func decodeAddresses(d *codec.Decoder, l *Locator) error {
case 0 :
a . URI , err = d . Text ( MaxURILen )
case 1 :
if a . Offset , err = d . Uint ( 1 << 63 - 1 ) ; err == nil && a . Offset == 0 {
if a . Offset , err = d . Uint ( codec . MaxSafeUint ) ; err == nil && a . Offset == 0 {
err = fmt . Errorf ( "an offset of 0 is written by leaving it out: %w" , datekeys . ErrNonCanonicalCBOR )
}
default :