From f3a5209cec2bf358403315f28a67a2fabd318d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 1 Oct 2024 13:23:37 +0200 Subject: [PATCH 1/4] tokens: read arbitrary token --- tokens/delegation/ipld.go | 25 +-- tokens/inspect.go | 191 +++++++++++------------ tokens/inspect_test.go | 54 +++---- tokens/internal/envelope/example_test.go | 3 +- tokens/internal/envelope/ipld.go | 147 ++++++++++++----- tokens/read.go | 30 ++++ 6 files changed, 265 insertions(+), 185 deletions(-) create mode 100644 tokens/read.go diff --git a/tokens/delegation/ipld.go b/tokens/delegation/ipld.go index 5a76b10..0c2dbb1 100644 --- a/tokens/delegation/ipld.go +++ b/tokens/delegation/ipld.go @@ -82,7 +82,7 @@ func FromSealedReader(r io.Reader) (*Token, error) { 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. func (t *Token) Encode(privKey crypto.PrivKey, encFn codec.Encoder) ([]byte, error) { 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) } -// 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) { 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) } -// 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) { 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 -// provided codec.Decoder into a View. +// provided codec.Decoder into a Token. // // 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) { node, err := ipld.Decode(b, decFn) if err != nil { return nil, err } - return fromIPLD(node) + return FromIPLD(node) } // 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 { 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 -// View is invalid. +// Token is invalid. func FromDagCbor(data []byte) (*Token, error) { pay, err := envelope.FromDagCbor[*tokenPayloadModel](data) if err != nil { @@ -168,10 +168,10 @@ func FromDagCborReader(r io.Reader) (*Token, error) { 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 -// View is invalid. +// Token is invalid. func FromDagJson(data []byte) (*Token, error) { return Decode(data, dagjson.Decode) } @@ -181,7 +181,8 @@ func FromDagJsonReader(r io.Reader) (*Token, error) { 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) if err != nil { return nil, err diff --git a/tokens/inspect.go b/tokens/inspect.go index 7fcedbb..c4bf134 100644 --- a/tokens/inspect.go +++ b/tokens/inspect.go @@ -1,105 +1,90 @@ package tokens -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/ucan-wg/go-ucan/tokens/internal/envelope" -) - -// EnvelopeInfo describes the fields of an Envelope enclosing a UCAN token. -type EnvelopeInfo struct { - Signature []byte - Tag string - VarsigHeader []byte -} - -// InspectEnvelope accepts arbitrary data and attempts to decode it as a -// UCAN token's Envelope. -func Inspect(data []byte) (EnvelopeInfo, error) { - 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") -} +// +// // EnvelopeInfo describes the fields of an Envelope enclosing a UCAN token. +// type EnvelopeInfo struct { +// Signature []byte +// Tag string +// VarsigHeader []byte +// } +// +// // InspectEnvelope accepts arbitrary data and attempts to decode it as a +// // UCAN token's Envelope. +// func Inspect(data []byte) (EnvelopeInfo, error) { +// 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") +// } diff --git a/tokens/inspect_test.go b/tokens/inspect_test.go index b5efa35..cbaf3b6 100644 --- a/tokens/inspect_test.go +++ b/tokens/inspect_test.go @@ -1,34 +1,24 @@ 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) - }) - } -} +// 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) +// }) +// } +// } diff --git a/tokens/internal/envelope/example_test.go b/tokens/internal/envelope/example_test.go index 4db1a4e..9f140db 100644 --- a/tokens/internal/envelope/example_test.go +++ b/tokens/internal/envelope/example_test.go @@ -16,8 +16,9 @@ import ( "github.com/ipld/go-ipld-prime/schema" "github.com/libp2p/go-libp2p/core/crypto" "github.com/stretchr/testify/require" - "github.com/ucan-wg/go-ucan/tokens/internal/envelope" "gotest.tools/v3/golden" + + "github.com/ucan-wg/go-ucan/tokens/internal/envelope" ) const ( diff --git a/tokens/internal/envelope/ipld.go b/tokens/internal/envelope/ipld.go index a683107..a85fe5d 100644 --- a/tokens/internal/envelope/ipld.go +++ b/tokens/internal/envelope/ipld.go @@ -27,7 +27,9 @@ package envelope import ( "errors" + "fmt" "io" + "strings" "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/codec" @@ -141,16 +143,20 @@ func FromIPLD[T Tokener](node datamodel.Node) (T, error) { } func fromIPLD[T Tokener](node datamodel.Node) (T, error) { - undef := *new(T) + zero := *new(T) - info, err := Inspect(node) + info, err := inspect(node) if err != nil { - return undef, err + return zero, err } - tokenPayloadNode, err := info.SigPayloadNode.LookupByString(undef.Tag()) + if info.Tag != zero.Tag() { + return zero, errors.New("data doesn't match the expected type") + } + + tokenPayloadNode, err := info.SigPayloadNode.LookupByString(info.Tag) if err != nil { - return undef, err + return zero, err } // This needs to be done before converting this node to its schema @@ -158,65 +164,65 @@ func fromIPLD[T Tokener](node datamodel.Node) (T, error) { // to use the wire name). issuerNode, err := tokenPayloadNode.LookupByString("iss") if err != nil { - return undef, err + return zero, err } // Replaces the datamodel.Node in tokenPayloadNode with a // schema.TypedNode so that we can cast it to a *token.Token after // unwrapping it. - nb := undef.Prototype().Representation().NewBuilder() + nb := zero.Prototype().Representation().NewBuilder() err = nb.AssignNode(tokenPayloadNode) if err != nil { - return undef, err + return zero, err } tokenPayloadNode = nb.Build() tokenPayload := bindnode.Unwrap(tokenPayloadNode) 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) 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 // matches the VarsigHeader and then verify the SigPayload. issuer, err := issuerNode.AsString() if err != nil { - return undef, err + return zero, err } issuerDID, err := did.Parse(issuer) if err != nil { - return undef, err + return zero, err } issuerPubKey, err := issuerDID.PubKey() if err != nil { - return undef, err + return zero, err } issuerVarsigHeader, err := varsig.Encode(issuerPubKey.Type()) if err != nil { - return undef, err + return zero, err } 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) if err != nil { - return undef, err + return zero, err } ok, err = issuerPubKey.Verify(data, info.Signature) 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 @@ -296,42 +302,109 @@ func ToIPLD(privKey crypto.PrivKey, token Tokener) (datamodel.Node, error) { }) } -type Info struct { +// FindTag inspect the given token IPLD representation and extract the token tag. +func FindTag(node datamodel.Node) (string, error) { + sigPayloadNode, err := node.LookupByIndex(1) + if err != nil { + return "", err + } + + 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 SigPayloadNode datamodel.Node VarsigHeader []byte } -func Inspect(node datamodel.Node) (Info, error) { - var undef Info +func inspect(node datamodel.Node) (info, error) { + var res info signatureNode, err := node.LookupByIndex(0) if err != nil { - return undef, err + return info{}, err } - signature, err := signatureNode.AsBytes() + res.Signature, err = signatureNode.AsBytes() if err != nil { - return undef, err + return info{}, err } - sigPayloadNode, err := node.LookupByIndex(1) + res.SigPayloadNode, err = node.LookupByIndex(1) if err != nil { - return undef, err + return info{}, err } - varsigHeaderNode, err := sigPayloadNode.LookupByString(VarsigHeaderKey) - if err != nil { - return undef, err - } - varsigHeader, err := varsigHeaderNode.AsBytes() - if err != nil { - return undef, err + it := res.SigPayloadNode.MapIterator() + foundVarsigHeader := false + foundTokenPayload := false + i := 0 + + 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 + default: + return info{}, fmt.Errorf("unexpected key type %q", key) + } } - return Info{ - Signature: signature, - SigPayloadNode: sigPayloadNode, - VarsigHeader: varsigHeader, - }, nil + 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 } diff --git a/tokens/read.go b/tokens/read.go new file mode 100644 index 0000000..43f67ad --- /dev/null +++ b/tokens/read.go @@ -0,0 +1,30 @@ +package tokens + +import ( + "fmt" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagcbor" + + "github.com/ucan-wg/go-ucan/tokens/delegation" + "github.com/ucan-wg/go-ucan/tokens/internal/envelope" +) + +func FromDagCbor(b []byte) (any, error) { + node, err := ipld.Decode(b, dagcbor.Decode) + if err != nil { + return nil, err + } + + 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) + } +} From 1b7059c029556db3ab5fbd3aa27febacced40826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 1 Oct 2024 14:17:02 +0200 Subject: [PATCH 2/4] token/read: add the usual collections of readers --- tokens/internal/envelope/ipld.go | 65 +++++++++++--------------------- tokens/read.go | 54 +++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 46 deletions(-) diff --git a/tokens/internal/envelope/ipld.go b/tokens/internal/envelope/ipld.go index a85fe5d..6803692 100644 --- a/tokens/internal/envelope/ipld.go +++ b/tokens/internal/envelope/ipld.go @@ -94,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 // Tokener is invalid. func FromDagCbor[T Tokener](b []byte) (T, error) { - undef := *new(T) - - 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 + return Decode[T](b, dagcbor.Decode) } // FromDagCborReader is the same as FromDagCbor, but accept an io.Reader. @@ -132,20 +120,9 @@ func FromDagJsonReader[T Tokener](r io.Reader) (T, error) { // An error is returned if the conversion fails, or if the resulting // Tokener is invalid. func FromIPLD[T Tokener](node datamodel.Node) (T, error) { - undef := *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) { zero := *new(T) - info, err := inspect(node) + info, err := Inspect(node) if err != nil { return zero, err } @@ -154,7 +131,7 @@ func fromIPLD[T Tokener](node datamodel.Node) (T, error) { return zero, errors.New("data doesn't match the expected type") } - tokenPayloadNode, err := info.SigPayloadNode.LookupByString(info.Tag) + tokenPayloadNode, err := info.sigPayloadNode.LookupByString(info.Tag) if err != nil { return zero, err } @@ -215,7 +192,7 @@ func fromIPLD[T Tokener](node datamodel.Node) (T, error) { 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 { return zero, err } @@ -335,50 +312,50 @@ func FindTag(node datamodel.Node) (string, error) { return "", fmt.Errorf("no token tag found") } -type info struct { +type Info struct { Tag string Signature []byte - SigPayloadNode datamodel.Node + sigPayloadNode datamodel.Node // private, we don't want to expose that VarsigHeader []byte } -func inspect(node datamodel.Node) (info, error) { - var res info +func Inspect(node datamodel.Node) (Info, error) { + var res Info signatureNode, err := node.LookupByIndex(0) if err != nil { - return info{}, err + return Info{}, err } res.Signature, err = signatureNode.AsBytes() if err != nil { - return info{}, err + return Info{}, err } - res.SigPayloadNode, err = node.LookupByIndex(1) + res.sigPayloadNode, err = node.LookupByIndex(1) if err != nil { - return info{}, err + return Info{}, err } - it := res.SigPayloadNode.MapIterator() + it := res.sigPayloadNode.MapIterator() foundVarsigHeader := false foundTokenPayload := false i := 0 for !it.Done() { if i >= 2 { - return info{}, fmt.Errorf("expected two and only two fields in SigPayload") + return Info{}, fmt.Errorf("expected two and only two fields in SigPayload") } i++ k, v, err := it.Next() if err != nil { - return info{}, err + return Info{}, err } key, err := k.AsString() if err != nil { - return info{}, err + return Info{}, err } switch { @@ -386,24 +363,24 @@ func inspect(node datamodel.Node) (info, error) { foundVarsigHeader = true res.VarsigHeader, err = v.AsBytes() if err != nil { - return info{}, err + return Info{}, err } case strings.HasPrefix(key, UCANTagPrefix): foundTokenPayload = true res.Tag = key default: - return info{}, fmt.Errorf("unexpected key type %q", key) + 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) + 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") + return Info{}, errors.New("failed to find VarsigHeader field") } if !foundTokenPayload { - return info{}, errors.New("failed to find TokenPayload field") + return Info{}, errors.New("failed to find TokenPayload field") } return res, nil diff --git a/tokens/read.go b/tokens/read.go index 43f67ad..999fc50 100644 --- a/tokens/read.go +++ b/tokens/read.go @@ -2,20 +2,70 @@ 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" ) -func FromDagCbor(b []byte) (any, error) { - node, err := ipld.Decode(b, dagcbor.Decode) +// 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 From 4201ab2dca07899d08c934479068f9b4b6732e5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 1 Oct 2024 14:25:07 +0200 Subject: [PATCH 3/4] tokens: expose Inspect and FindTag --- tokens/inspect.go | 105 +++++--------------------- tokens/inspect_test.go | 24 ------ tokens/internal/envelope/ipld.go | 32 ++++---- tokens/internal/envelope/ipld_test.go | 23 +++++- 4 files changed, 58 insertions(+), 126 deletions(-) delete mode 100644 tokens/inspect_test.go diff --git a/tokens/inspect.go b/tokens/inspect.go index c4bf134..cfb8a24 100644 --- a/tokens/inspect.go +++ b/tokens/inspect.go @@ -1,90 +1,19 @@ package tokens -// -// // EnvelopeInfo describes the fields of an Envelope enclosing a UCAN token. -// type EnvelopeInfo struct { -// Signature []byte -// Tag string -// VarsigHeader []byte -// } -// -// // InspectEnvelope accepts arbitrary data and attempts to decode it as a -// // UCAN token's Envelope. -// func Inspect(data []byte) (EnvelopeInfo, error) { -// 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") -// } +import ( + "github.com/ipld/go-ipld-prime/datamodel" + + "github.com/ucan-wg/go-ucan/tokens/internal/envelope" +) + +type Info = envelope.Info + +// Inspect inspects the given token IPLD representation and extract some envelope facts. +func Inspect(node datamodel.Node) (Info, error) { + return envelope.Inspect(node) +} + +// FindTag inspect the given token IPLD representation and extract the token tag. +func FindTag(node datamodel.Node) (string, error) { + return envelope.FindTag(node) +} diff --git a/tokens/inspect_test.go b/tokens/inspect_test.go deleted file mode 100644 index cbaf3b6..0000000 --- a/tokens/inspect_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package tokens_test - -// 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) -// }) -// } -// } diff --git a/tokens/internal/envelope/ipld.go b/tokens/internal/envelope/ipld.go index 6803692..3ba3c63 100644 --- a/tokens/internal/envelope/ipld.go +++ b/tokens/internal/envelope/ipld.go @@ -131,15 +131,10 @@ func FromIPLD[T Tokener](node datamodel.Node) (T, error) { return zero, errors.New("data doesn't match the expected type") } - tokenPayloadNode, err := info.sigPayloadNode.LookupByString(info.Tag) - if err != nil { - return zero, err - } - // This needs to be done before converting this node to its schema // representation (afterwards, the field might be renamed os it's safer // to use the wire name). - issuerNode, err := tokenPayloadNode.LookupByString("iss") + issuerNode, err := info.tokenPayloadNode.LookupByString("iss") if err != nil { return zero, err } @@ -149,12 +144,12 @@ func FromIPLD[T Tokener](node datamodel.Node) (T, error) { // unwrapping it. nb := zero.Prototype().Representation().NewBuilder() - err = nb.AssignNode(tokenPayloadNode) + err = nb.AssignNode(info.tokenPayloadNode) if err != nil { return zero, err } - tokenPayloadNode = nb.Build() + tokenPayloadNode := nb.Build() tokenPayload := bindnode.Unwrap(tokenPayloadNode) if tokenPayload == nil { @@ -279,13 +274,17 @@ func ToIPLD(privKey crypto.PrivKey, token Tokener) (datamodel.Node, error) { }) } -// FindTag inspect the given token IPLD representation and extract the token tag. +// FindTag inspects the given token IPLD representation and extract the token tag. func FindTag(node datamodel.Node) (string, error) { sigPayloadNode, err := node.LookupByIndex(1) if err != nil { return "", err } + if sigPayloadNode.Kind() != datamodel.Kind_Map { + return "", fmt.Errorf("unexpected type instead of map") + } + it := sigPayloadNode.MapIterator() i := 0 @@ -313,12 +312,14 @@ func FindTag(node datamodel.Node) (string, error) { } type Info struct { - Tag string - Signature []byte - sigPayloadNode datamodel.Node // private, we don't want to expose that - VarsigHeader []byte + 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) { var res Info @@ -337,6 +338,10 @@ func Inspect(node datamodel.Node) (Info, error) { return Info{}, err } + if res.sigPayloadNode.Kind() != datamodel.Kind_Map { + return Info{}, fmt.Errorf("unexpected type instead of map") + } + it := res.sigPayloadNode.MapIterator() foundVarsigHeader := false foundTokenPayload := false @@ -368,6 +373,7 @@ func Inspect(node datamodel.Node) (Info, error) { case strings.HasPrefix(key, UCANTagPrefix): foundTokenPayload = true res.Tag = key + res.tokenPayloadNode = v default: return Info{}, fmt.Errorf("unexpected key type %q", key) } diff --git a/tokens/internal/envelope/ipld_test.go b/tokens/internal/envelope/ipld_test.go index 7c8dcc2..76da597 100644 --- a/tokens/internal/envelope/ipld_test.go +++ b/tokens/internal/envelope/ipld_test.go @@ -3,12 +3,16 @@ package envelope_test import ( "bytes" "crypto/sha256" + "encoding/base64" "testing" + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagcbor" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ucan-wg/go-ucan/tokens/internal/envelope" "gotest.tools/v3/golden" + + "github.com/ucan-wg/go-ucan/tokens/internal/envelope" ) func TestDecode(t *testing.T) { @@ -149,3 +153,20 @@ func TestHash(t *testing.T) { require.Equal(t, hash1[:], hash2) 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) +} From b4dd8c0757a01d90cac8f6455fcdaf48407b6bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 1 Oct 2024 15:04:46 +0200 Subject: [PATCH 4/4] envelope: fuzz Inspect and FindTag --- tokens/internal/envelope/ipld_test.go | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tokens/internal/envelope/ipld_test.go b/tokens/internal/envelope/ipld_test.go index 76da597..3a0b93a 100644 --- a/tokens/internal/envelope/ipld_test.go +++ b/tokens/internal/envelope/ipld_test.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/sha256" "encoding/base64" + "os" "testing" "github.com/ipld/go-ipld-prime" @@ -170,3 +171,39 @@ func TestInspect(t *testing.T) { 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() + } + }) +}