diff --git a/tokens/inspect.go b/tokens/inspect.go new file mode 100644 index 0000000..7fcedbb --- /dev/null +++ b/tokens/inspect.go @@ -0,0 +1,105 @@ +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") +} diff --git a/tokens/inspect_test.go b/tokens/inspect_test.go new file mode 100644 index 0000000..b5efa35 --- /dev/null +++ b/tokens/inspect_test.go @@ -0,0 +1,34 @@ +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) + }) + } +} diff --git a/tokens/internal/envelope/ipld.go b/tokens/internal/envelope/ipld.go index aef4afc..a683107 100644 --- a/tokens/internal/envelope/ipld.go +++ b/tokens/internal/envelope/ipld.go @@ -44,7 +44,10 @@ import ( "github.com/ucan-wg/go-ucan/tokens/internal/varsig" ) -const varsigHeaderKey = "h" +const ( + VarsigHeaderKey = "h" + UCANTagPrefix = "ucan/" +) // Tokener must be implemented by types that wish to be enclosed in a // UCAN Envelope (presumbably one of the UCAN token types). @@ -140,27 +143,12 @@ func FromIPLD[T Tokener](node datamodel.Node) (T, error) { func fromIPLD[T Tokener](node datamodel.Node) (T, error) { undef := *new(T) - signatureNode, err := node.LookupByIndex(0) + info, err := Inspect(node) if err != nil { return undef, err } - signature, err := signatureNode.AsBytes() - if err != nil { - return undef, err - } - - sigPayloadNode, err := node.LookupByIndex(1) - if err != nil { - return undef, err - } - - varsigHeaderNode, err := sigPayloadNode.LookupByString(varsigHeaderKey) - if err != nil { - return undef, err - } - - tokenPayloadNode, err := sigPayloadNode.LookupByString(undef.Tag()) + tokenPayloadNode, err := info.SigPayloadNode.LookupByString(undef.Tag()) if err != nil { return undef, err } @@ -217,21 +205,16 @@ func fromIPLD[T Tokener](node datamodel.Node) (T, error) { return undef, err } - varsigHeader, err := varsigHeaderNode.AsBytes() - if err != nil { - return undef, err - } - - if string(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") } - data, err := ipld.Encode(sigPayloadNode, dagcbor.Encode) + data, err := ipld.Encode(info.SigPayloadNode, dagcbor.Encode) if err != nil { return undef, err } - ok, err = issuerPubKey.Verify(data, signature) + ok, err = issuerPubKey.Verify(data, info.Signature) if err != nil || !ok { return undef, errors.New("failed to verify the token's signature") } @@ -293,7 +276,7 @@ func ToIPLD(privKey crypto.PrivKey, token Tokener) (datamodel.Node, error) { } sigPayloadNode, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) { - qp.MapEntry(ma, varsigHeaderKey, qp.Bytes(varsigHeader)) + qp.MapEntry(ma, VarsigHeaderKey, qp.Bytes(varsigHeader)) qp.MapEntry(ma, token.Tag(), qp.Node(tokenPayloadNode)) }) @@ -312,3 +295,43 @@ func ToIPLD(privKey crypto.PrivKey, token Tokener) (datamodel.Node, error) { qp.ListEntry(la, qp.Node(sigPayloadNode)) }) } + +type Info struct { + Signature []byte + SigPayloadNode datamodel.Node + VarsigHeader []byte +} + +func Inspect(node datamodel.Node) (Info, error) { + var undef Info + + signatureNode, err := node.LookupByIndex(0) + if err != nil { + return undef, err + } + + signature, err := signatureNode.AsBytes() + if err != nil { + return undef, err + } + + sigPayloadNode, err := node.LookupByIndex(1) + if err != nil { + return undef, err + } + + varsigHeaderNode, err := sigPayloadNode.LookupByString(VarsigHeaderKey) + if err != nil { + return undef, err + } + varsigHeader, err := varsigHeaderNode.AsBytes() + if err != nil { + return undef, err + } + + return Info{ + Signature: signature, + SigPayloadNode: sigPayloadNode, + VarsigHeader: varsigHeader, + }, nil +} diff --git a/tokens/testdata/example.dagcbor b/tokens/testdata/example.dagcbor new file mode 100644 index 0000000..d18e26e --- /dev/null +++ b/tokens/testdata/example.dagcbor @@ -0,0 +1 @@ +‚X@|úŸÀ½â–ðõ+ÁŠ­!µ.®ÿhéÍúGO- ü¬”jÉsyÖsY¨quëiþ“ä°¬Íuý#ò¼’ç˜ c¢ahD4íqxucan/example@v1.0.0-rc.1¢cissx8did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nhehelloeworld \ No newline at end of file diff --git a/tokens/testdata/example.dagjson b/tokens/testdata/example.dagjson new file mode 100644 index 0000000..3db25a5 --- /dev/null +++ b/tokens/testdata/example.dagjson @@ -0,0 +1 @@ +[{"/":{"bytes":"fPqfwL3iFpbw9SvBiq0DIbUurv9o6c36R08tC/yslGrJcwV51ghzWahxdetpEf6T5LCszXX9I/K8khvnmAxjAg"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/example@v1.0.0-rc.1":{"hello":"world","iss":"did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh"}}] \ No newline at end of file