Merge pull request #31 from ucan-wg/read-arbitrary

Read arbitrary
This commit is contained in:
Michael Muré
2024-10-01 15:32:47 +02:00
committed by GitHub
7 changed files with 286 additions and 210 deletions

View File

@@ -82,7 +82,7 @@ func FromSealedReader(r io.Reader) (*Token, error) {
return tkn, nil return tkn, nil
} }
// Encode marshals a View to the format specified by the provided // Encode marshals a Token to the format specified by the provided
// codec.Encoder. // codec.Encoder.
func (t *Token) Encode(privKey crypto.PrivKey, encFn codec.Encoder) ([]byte, error) { func (t *Token) Encode(privKey crypto.PrivKey, encFn codec.Encoder) ([]byte, error) {
node, err := t.toIPLD(privKey) node, err := t.toIPLD(privKey)
@@ -103,7 +103,7 @@ func (t *Token) EncodeWriter(w io.Writer, privKey crypto.PrivKey, encFn codec.En
return ipld.EncodeStreaming(w, node, encFn) return ipld.EncodeStreaming(w, node, encFn)
} }
// ToDagCbor marshals the View to the DAG-CBOR format. // ToDagCbor marshals the Token to the DAG-CBOR format.
func (t *Token) ToDagCbor(privKey crypto.PrivKey) ([]byte, error) { func (t *Token) ToDagCbor(privKey crypto.PrivKey) ([]byte, error) {
return t.Encode(privKey, dagcbor.Encode) return t.Encode(privKey, dagcbor.Encode)
} }
@@ -113,7 +113,7 @@ func (t *Token) ToDagCborWriter(w io.Writer, privKey crypto.PrivKey) error {
return t.EncodeWriter(w, privKey, dagcbor.Encode) return t.EncodeWriter(w, privKey, dagcbor.Encode)
} }
// ToDagJson marshals the View to the DAG-JSON format. // ToDagJson marshals the Token to the DAG-JSON format.
func (t *Token) ToDagJson(privKey crypto.PrivKey) ([]byte, error) { func (t *Token) ToDagJson(privKey crypto.PrivKey) ([]byte, error) {
return t.Encode(privKey, dagjson.Encode) return t.Encode(privKey, dagjson.Encode)
} }
@@ -124,16 +124,16 @@ func (t *Token) ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey) error {
} }
// Decode unmarshals the input data using the format specified by the // Decode unmarshals the input data using the format specified by the
// provided codec.Decoder into a View. // provided codec.Decoder into a Token.
// //
// An error is returned if the conversion fails, or if the resulting // An error is returned if the conversion fails, or if the resulting
// View is invalid. // Token is invalid.
func Decode(b []byte, decFn codec.Decoder) (*Token, error) { func Decode(b []byte, decFn codec.Decoder) (*Token, error) {
node, err := ipld.Decode(b, decFn) node, err := ipld.Decode(b, decFn)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return fromIPLD(node) return FromIPLD(node)
} }
// DecodeReader is the same as Decode, but accept an io.Reader. // DecodeReader is the same as Decode, but accept an io.Reader.
@@ -142,13 +142,13 @@ func DecodeReader(r io.Reader, decFn codec.Decoder) (*Token, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return fromIPLD(node) return FromIPLD(node)
} }
// FromDagCbor unmarshals the input data into a View. // FromDagCbor unmarshals the input data into a Token.
// //
// An error is returned if the conversion fails, or if the resulting // An error is returned if the conversion fails, or if the resulting
// View is invalid. // Token is invalid.
func FromDagCbor(data []byte) (*Token, error) { func FromDagCbor(data []byte) (*Token, error) {
pay, err := envelope.FromDagCbor[*tokenPayloadModel](data) pay, err := envelope.FromDagCbor[*tokenPayloadModel](data)
if err != nil { if err != nil {
@@ -168,10 +168,10 @@ func FromDagCborReader(r io.Reader) (*Token, error) {
return DecodeReader(r, dagcbor.Decode) return DecodeReader(r, dagcbor.Decode)
} }
// FromDagJson unmarshals the input data into a View. // FromDagJson unmarshals the input data into a Token.
// //
// An error is returned if the conversion fails, or if the resulting // An error is returned if the conversion fails, or if the resulting
// View is invalid. // Token is invalid.
func FromDagJson(data []byte) (*Token, error) { func FromDagJson(data []byte) (*Token, error) {
return Decode(data, dagjson.Decode) return Decode(data, dagjson.Decode)
} }
@@ -181,7 +181,8 @@ func FromDagJsonReader(r io.Reader) (*Token, error) {
return DecodeReader(r, dagjson.Decode) return DecodeReader(r, dagjson.Decode)
} }
func fromIPLD(node datamodel.Node) (*Token, error) { // FromIPLD decode the given IPLD representation into a Token.
func FromIPLD(node datamodel.Node) (*Token, error) {
pay, err := envelope.FromIPLD[*tokenPayloadModel](node) pay, err := envelope.FromIPLD[*tokenPayloadModel](node)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -1,105 +1,19 @@
package tokens package tokens
import ( import (
"errors"
"fmt"
"strings"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec"
"github.com/ipld/go-ipld-prime/codec/cbor"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/ipld/go-ipld-prime/codec/json"
"github.com/ipld/go-ipld-prime/codec/raw"
"github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/datamodel"
"github.com/ucan-wg/go-ucan/tokens/internal/envelope" "github.com/ucan-wg/go-ucan/tokens/internal/envelope"
) )
// EnvelopeInfo describes the fields of an Envelope enclosing a UCAN token. type Info = envelope.Info
type EnvelopeInfo struct {
Signature []byte // Inspect inspects the given token IPLD representation and extract some envelope facts.
Tag string func Inspect(node datamodel.Node) (Info, error) {
VarsigHeader []byte return envelope.Inspect(node)
} }
// InspectEnvelope accepts arbitrary data and attempts to decode it as a // FindTag inspect the given token IPLD representation and extract the token tag.
// UCAN token's Envelope. func FindTag(node datamodel.Node) (string, error) {
func Inspect(data []byte) (EnvelopeInfo, error) { return envelope.FindTag(node)
undef := EnvelopeInfo{}
node, err := decodeAny(data)
if err != nil {
return undef, err
}
info, err := envelope.Inspect(node)
if err != nil {
return undef, err
}
iterator := info.SigPayloadNode.MapIterator()
foundVarsigHeader := false
foundTokenPayload := false
tag := ""
i := 0
for !iterator.Done() {
k, _, err := iterator.Next()
if err != nil {
return undef, err
}
key, err := k.AsString()
if err != nil {
return undef, err
}
if key == envelope.VarsigHeaderKey {
foundVarsigHeader = true
i++
continue
}
if strings.HasPrefix(key, envelope.UCANTagPrefix) {
tag = key
foundTokenPayload = true
i++
}
}
if i != 2 {
return undef, fmt.Errorf("expected two and only two fields in SigPayload: %d", i)
}
if !foundVarsigHeader {
return undef, errors.New("failed to find VarsigHeader field")
}
if !foundTokenPayload {
return undef, errors.New("failed to find TokenPayload field")
}
return EnvelopeInfo{
Signature: info.Signature,
Tag: tag,
VarsigHeader: info.VarsigHeader,
}, nil
}
func decodeAny(data []byte) (datamodel.Node, error) {
for _, decoder := range []codec.Decoder{
dagcbor.Decode,
dagjson.Decode,
cbor.Decode,
json.Decode,
raw.Decode,
} {
if node, err := ipld.Decode(data, decoder); err == nil {
return node, nil
}
}
return nil, errors.New("failed to decode (any) the provided data")
} }

View File

@@ -1,34 +0,0 @@
package tokens_test
import (
"encoding/base64"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/tokens"
"gotest.tools/v3/golden"
)
func TestInspect(t *testing.T) {
t.Parallel()
for _, filename := range []string{
"example.dagcbor",
"example.dagjson",
} {
t.Run(filename, func(t *testing.T) {
t.Parallel()
data := golden.Get(t, filename)
expSig, err := base64.RawStdEncoding.DecodeString("fPqfwL3iFpbw9SvBiq0DIbUurv9o6c36R08tC/yslGrJcwV51ghzWahxdetpEf6T5LCszXX9I/K8khvnmAxjAg")
require.NoError(t, err)
info, err := tokens.Inspect(data)
require.NoError(t, err)
assert.Equal(t, expSig, info.Signature)
assert.Equal(t, "ucan/example@v1.0.0-rc.1", info.Tag)
assert.Equal(t, []byte{0x34, 0xed, 0x1, 0x71}, info.VarsigHeader)
})
}
}

View File

@@ -16,8 +16,9 @@ import (
"github.com/ipld/go-ipld-prime/schema" "github.com/ipld/go-ipld-prime/schema"
"github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/crypto"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
) )
const ( const (

View File

@@ -27,7 +27,9 @@ package envelope
import ( import (
"errors" "errors"
"fmt"
"io" "io"
"strings"
"github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec" "github.com/ipld/go-ipld-prime/codec"
@@ -92,19 +94,7 @@ func DecodeReader[T Tokener](r io.Reader, decFn codec.Decoder) (T, error) {
// An error is returned if the conversion fails, or if the resulting // An error is returned if the conversion fails, or if the resulting
// Tokener is invalid. // Tokener is invalid.
func FromDagCbor[T Tokener](b []byte) (T, error) { func FromDagCbor[T Tokener](b []byte) (T, error) {
undef := *new(T) return Decode[T](b, dagcbor.Decode)
node, err := ipld.Decode(b, dagcbor.Decode)
if err != nil {
return undef, err
}
tkn, err := fromIPLD[T](node)
if err != nil {
return undef, err
}
return tkn, nil
} }
// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader. // FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.
@@ -130,93 +120,81 @@ func FromDagJsonReader[T Tokener](r io.Reader) (T, error) {
// An error is returned if the conversion fails, or if the resulting // An error is returned if the conversion fails, or if the resulting
// Tokener is invalid. // Tokener is invalid.
func FromIPLD[T Tokener](node datamodel.Node) (T, error) { func FromIPLD[T Tokener](node datamodel.Node) (T, error) {
undef := *new(T) zero := *new(T)
tkn, err := fromIPLD[T](node)
if err != nil {
return undef, err
}
return tkn, nil
}
func fromIPLD[T Tokener](node datamodel.Node) (T, error) {
undef := *new(T)
info, err := Inspect(node) info, err := Inspect(node)
if err != nil { if err != nil {
return undef, err return zero, err
} }
tokenPayloadNode, err := info.SigPayloadNode.LookupByString(undef.Tag()) if info.Tag != zero.Tag() {
if err != nil { return zero, errors.New("data doesn't match the expected type")
return undef, err
} }
// This needs to be done before converting this node to its schema // This needs to be done before converting this node to its schema
// representation (afterwards, the field might be renamed os it's safer // representation (afterwards, the field might be renamed os it's safer
// to use the wire name). // to use the wire name).
issuerNode, err := tokenPayloadNode.LookupByString("iss") issuerNode, err := info.tokenPayloadNode.LookupByString("iss")
if err != nil { if err != nil {
return undef, err return zero, err
} }
// Replaces the datamodel.Node in tokenPayloadNode with a // Replaces the datamodel.Node in tokenPayloadNode with a
// schema.TypedNode so that we can cast it to a *token.Token after // schema.TypedNode so that we can cast it to a *token.Token after
// unwrapping it. // unwrapping it.
nb := undef.Prototype().Representation().NewBuilder() nb := zero.Prototype().Representation().NewBuilder()
err = nb.AssignNode(tokenPayloadNode) err = nb.AssignNode(info.tokenPayloadNode)
if err != nil { if err != nil {
return undef, err return zero, err
} }
tokenPayloadNode = nb.Build() tokenPayloadNode := nb.Build()
tokenPayload := bindnode.Unwrap(tokenPayloadNode) tokenPayload := bindnode.Unwrap(tokenPayloadNode)
if tokenPayload == nil { if tokenPayload == nil {
return undef, errors.New("failed to Unwrap the TokenPayload") return zero, errors.New("failed to Unwrap the TokenPayload")
} }
tkn, ok := tokenPayload.(T) tkn, ok := tokenPayload.(T)
if !ok { if !ok {
return undef, errors.New("failed to assert the TokenPayload type as *token.Token") return zero, errors.New("failed to assert the TokenPayload type as *token.Token")
} }
// Check that the issuer's DID contains a public key with a type that // Check that the issuer's DID contains a public key with a type that
// matches the VarsigHeader and then verify the SigPayload. // matches the VarsigHeader and then verify the SigPayload.
issuer, err := issuerNode.AsString() issuer, err := issuerNode.AsString()
if err != nil { if err != nil {
return undef, err return zero, err
} }
issuerDID, err := did.Parse(issuer) issuerDID, err := did.Parse(issuer)
if err != nil { if err != nil {
return undef, err return zero, err
} }
issuerPubKey, err := issuerDID.PubKey() issuerPubKey, err := issuerDID.PubKey()
if err != nil { if err != nil {
return undef, err return zero, err
} }
issuerVarsigHeader, err := varsig.Encode(issuerPubKey.Type()) issuerVarsigHeader, err := varsig.Encode(issuerPubKey.Type())
if err != nil { if err != nil {
return undef, err return zero, err
} }
if string(info.VarsigHeader) != string(issuerVarsigHeader) { if string(info.VarsigHeader) != string(issuerVarsigHeader) {
return undef, errors.New("the VarsigHeader key type doesn't match the issuer's key type") return zero, errors.New("the VarsigHeader key type doesn't match the issuer's key type")
} }
data, err := ipld.Encode(info.SigPayloadNode, dagcbor.Encode) data, err := ipld.Encode(info.sigPayloadNode, dagcbor.Encode)
if err != nil { if err != nil {
return undef, err return zero, err
} }
ok, err = issuerPubKey.Verify(data, info.Signature) ok, err = issuerPubKey.Verify(data, info.Signature)
if err != nil || !ok { if err != nil || !ok {
return undef, errors.New("failed to verify the token's signature") return zero, errors.New("failed to verify the token's signature")
} }
return tkn, nil return tkn, nil
@@ -296,42 +274,120 @@ func ToIPLD(privKey crypto.PrivKey, token Tokener) (datamodel.Node, error) {
}) })
} }
type Info struct { // FindTag inspects the given token IPLD representation and extract the token tag.
Signature []byte func FindTag(node datamodel.Node) (string, error) {
SigPayloadNode datamodel.Node sigPayloadNode, err := node.LookupByIndex(1)
VarsigHeader []byte if err != nil {
return "", err
}
if sigPayloadNode.Kind() != datamodel.Kind_Map {
return "", fmt.Errorf("unexpected type instead of map")
}
it := sigPayloadNode.MapIterator()
i := 0
for !it.Done() {
if i >= 2 {
return "", fmt.Errorf("expected two and only two fields in SigPayload")
}
i++
k, _, err := it.Next()
if err != nil {
return "", err
}
key, err := k.AsString()
if err != nil {
return "", err
}
if strings.HasPrefix(key, UCANTagPrefix) {
return key, nil
}
}
return "", fmt.Errorf("no token tag found")
} }
type Info struct {
Tag string
Signature []byte
VarsigHeader []byte
sigPayloadNode datamodel.Node // private, we don't want to expose that
tokenPayloadNode datamodel.Node // private, we don't want to expose that
}
// Inspect inspects the given token IPLD representation and extract some envelope facts.
func Inspect(node datamodel.Node) (Info, error) { func Inspect(node datamodel.Node) (Info, error) {
var undef Info var res Info
signatureNode, err := node.LookupByIndex(0) signatureNode, err := node.LookupByIndex(0)
if err != nil { if err != nil {
return undef, err return Info{}, err
} }
signature, err := signatureNode.AsBytes() res.Signature, err = signatureNode.AsBytes()
if err != nil { if err != nil {
return undef, err return Info{}, err
} }
sigPayloadNode, err := node.LookupByIndex(1) res.sigPayloadNode, err = node.LookupByIndex(1)
if err != nil { if err != nil {
return undef, err return Info{}, err
} }
varsigHeaderNode, err := sigPayloadNode.LookupByString(VarsigHeaderKey) if res.sigPayloadNode.Kind() != datamodel.Kind_Map {
if err != nil { return Info{}, fmt.Errorf("unexpected type instead of map")
return undef, err
}
varsigHeader, err := varsigHeaderNode.AsBytes()
if err != nil {
return undef, err
} }
return Info{ it := res.sigPayloadNode.MapIterator()
Signature: signature, foundVarsigHeader := false
SigPayloadNode: sigPayloadNode, foundTokenPayload := false
VarsigHeader: varsigHeader, i := 0
}, nil
for !it.Done() {
if i >= 2 {
return Info{}, fmt.Errorf("expected two and only two fields in SigPayload")
}
i++
k, v, err := it.Next()
if err != nil {
return Info{}, err
}
key, err := k.AsString()
if err != nil {
return Info{}, err
}
switch {
case key == VarsigHeaderKey:
foundVarsigHeader = true
res.VarsigHeader, err = v.AsBytes()
if err != nil {
return Info{}, err
}
case strings.HasPrefix(key, UCANTagPrefix):
foundTokenPayload = true
res.Tag = key
res.tokenPayloadNode = v
default:
return Info{}, fmt.Errorf("unexpected key type %q", key)
}
}
if i != 2 {
return Info{}, fmt.Errorf("expected two and only two fields in SigPayload: %d", i)
}
if !foundVarsigHeader {
return Info{}, errors.New("failed to find VarsigHeader field")
}
if !foundTokenPayload {
return Info{}, errors.New("failed to find TokenPayload field")
}
return res, nil
} }

View File

@@ -3,12 +3,17 @@ package envelope_test
import ( import (
"bytes" "bytes"
"crypto/sha256" "crypto/sha256"
"encoding/base64"
"os"
"testing" "testing"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
) )
func TestDecode(t *testing.T) { func TestDecode(t *testing.T) {
@@ -149,3 +154,56 @@ func TestHash(t *testing.T) {
require.Equal(t, hash1[:], hash2) require.Equal(t, hash1[:], hash2)
require.Equal(t, hash1[:], hash3) require.Equal(t, hash1[:], hash3)
} }
func TestInspect(t *testing.T) {
t.Parallel()
data := golden.Get(t, "example.dagcbor")
node, err := ipld.Decode(data, dagcbor.Decode)
require.NoError(t, err)
expSig, err := base64.RawStdEncoding.DecodeString("fPqfwL3iFpbw9SvBiq0DIbUurv9o6c36R08tC/yslGrJcwV51ghzWahxdetpEf6T5LCszXX9I/K8khvnmAxjAg")
require.NoError(t, err)
info, err := envelope.Inspect(node)
require.NoError(t, err)
assert.Equal(t, expSig, info.Signature)
assert.Equal(t, "ucan/example@v1.0.0-rc.1", info.Tag)
assert.Equal(t, []byte{0x34, 0xed, 0x1, 0x71}, info.VarsigHeader)
}
func FuzzInspect(f *testing.F) {
data, err := os.ReadFile("testdata/example.dagcbor")
require.NoError(f, err)
f.Add(data)
f.Fuzz(func(t *testing.T, data []byte) {
node, err := ipld.Decode(data, dagcbor.Decode)
if err != nil {
t.Skip()
}
_, err = envelope.Inspect(node)
if err != nil {
t.Skip()
}
})
}
func FuzzFindTag(f *testing.F) {
data, err := os.ReadFile("testdata/example.dagcbor")
require.NoError(f, err)
f.Add(data)
f.Fuzz(func(t *testing.T, data []byte) {
node, err := ipld.Decode(data, dagcbor.Decode)
if err != nil {
t.Skip()
}
_, err = envelope.FindTag(node)
if err != nil {
t.Skip()
}
})
}

80
tokens/read.go Normal file
View File

@@ -0,0 +1,80 @@
package tokens
import (
"fmt"
"io"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ucan-wg/go-ucan/tokens/delegation"
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
)
// Decode unmarshals the input data using the format specified by the
// provided codec.Decoder into an arbitrary UCAN token.
// An error is returned if the conversion fails, or if the resulting
// Token is invalid.
// Supported and returned types are:
// - delegation.Token
func Decode(b []byte, decFn codec.Decoder) (any, error) {
node, err := ipld.Decode(b, decFn)
if err != nil {
return nil, err
}
return fromIPLD(node)
}
// DecodeReader is the same as Decode, but accept an io.Reader.
func DecodeReader(r io.Reader, decFn codec.Decoder) (any, error) {
node, err := ipld.DecodeStreaming(r, decFn)
if err != nil {
return nil, err
}
return fromIPLD(node)
}
// FromDagCbor unmarshals an arbitrary DagCbor encoded UCAN token.
// An error is returned if the conversion fails, or if the resulting
// Token is invalid.
// Supported and returned types are:
// - delegation.Token
func FromDagCbor(b []byte) (any, error) {
return Decode(b, dagcbor.Decode)
}
// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.
func FromDagCborReader(r io.Reader) (any, error) {
return DecodeReader(r, dagcbor.Decode)
}
// FromDagCbor unmarshals an arbitrary DagJson encoded UCAN token.
// An error is returned if the conversion fails, or if the resulting
// Token is invalid.
// Supported and returned types are:
// - delegation.Token
func FromDagJson(b []byte) (any, error) {
return Decode(b, dagjson.Decode)
}
// FromDagJsonReader is the same as FromDagJson, but accept an io.Reader.
func FromDagJsonReader(r io.Reader) (any, error) {
return DecodeReader(r, dagjson.Decode)
}
func fromIPLD(node datamodel.Node) (any, error) {
tag, err := envelope.FindTag(node)
if err != nil {
return nil, err
}
switch tag {
case delegation.Tag:
return delegation.FromIPLD(node)
default:
return nil, fmt.Errorf(`unknown tag "%s"`, tag)
}
}