diff --git a/go.mod b/go.mod index d18a964..30dee27 100644 --- a/go.mod +++ b/go.mod @@ -12,11 +12,13 @@ require ( github.com/multiformats/go-multicodec v0.9.0 github.com/multiformats/go-varint v0.0.7 github.com/stretchr/testify v1.9.0 + gotest.tools/v3 v3.5.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/google/go-cmp v0.5.9 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mr-tron/base58 v1.2.0 // indirect diff --git a/go.sum b/go.sum index 4121534..9047c44 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -23,8 +25,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/libp2p/go-libp2p v0.36.2 h1:BbqRkDaGC3/5xfaJakLV/BrpjlAuYqSB0lRvtzL3B/U= -github.com/libp2p/go-libp2p v0.36.2/go.mod h1:XO3joasRE4Eup8yCTTP/+kX+g92mOgRaadk46LmPhHY= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-libp2p v0.36.3 h1:NHz30+G7D8Y8YmznrVZZla0ofVANrvBl2c+oARfMeDQ= github.com/libp2p/go-libp2p v0.36.3/go.mod h1:4Y5vFyCUiJuluEPmpnKYf6WFx5ViKPUYs/ixe9ANFZ8= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= @@ -35,6 +37,8 @@ github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aG github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multiaddr v0.13.0 h1:BCBzs61E3AGHcYYTv8dqRH43ZfyrqM8RXVPT8t13tLQ= +github.com/multiformats/go-multiaddr v0.13.0/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamGa/P5EIXNPLovyhQCII= github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= @@ -65,6 +69,8 @@ github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvS golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -81,5 +87,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= diff --git a/internal/envelope/example_test.go b/internal/envelope/example_test.go new file mode 100644 index 0000000..d5a7ba8 --- /dev/null +++ b/internal/envelope/example_test.go @@ -0,0 +1,136 @@ +package envelope_test + +import ( + _ "embed" + "encoding/base64" + "fmt" + "sync" + "testing" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/fluent/qp" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/ipld/go-ipld-prime/node/bindnode" + "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/internal/envelope" + "gotest.tools/v3/golden" +) + +const ( + exampleDID = "did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh" + exampleGreeting = "world" + examplePrivKeyCfg = "CAESQP9v2uqECTuIi45dyg3znQvsryvf2IXmOF/6aws6aCehm0FVrj0zHR5RZSDxWNjcpcJqsGym3sjCungX9Zt5oA4=" + exampleSignatureStr = "PZV6A2aI7n+MlyADqcqmWhkuyNrgUCDz+qSLSnI9bpasOwOhKUTx95m5Nu5CO/INa1LqzHGioD9+PVf6qdtTBg" + exampleTag = "ucan/example@v1.0.0-rc.1" + exampleTypeName = "Example" + exampleVarsigHeaderStr = "NO0BcQ" + + invalidSignatureStr = "PZV6A2aI7n+MlyADqcqmWhkuyNrgUCDz+qSLSnI9bpasOwOhKUTx95m5Nu5CO/INa1LqzHGioD9+PVf6qdtTBK" + + exampleDAGCBORFilename = "example.dagcbor" + exampleDAGJSONFilename = "example.dagjson" +) + +//go:embed testdata/example.ipldsch +var schemaBytes []byte + +var ( + once sync.Once + ts *schema.TypeSystem + err error +) + +func mustLoadSchema() *schema.TypeSystem { + once.Do(func() { + ts, err = ipld.LoadSchemaBytes(schemaBytes) + }) + + if err != nil { + panic(fmt.Errorf("failed to load IPLD schema: %s", err)) + } + + return ts +} + +func exampleType() schema.Type { + return mustLoadSchema().TypeByName(exampleTypeName) +} + +var _ envelope.Tokener = (*Example)(nil) + +type Example struct { + Hello string + Issuer string +} + +func newExample(t *testing.T) *Example { + t.Helper() + + return &Example{ + Hello: exampleGreeting, + Issuer: exampleDID, + } +} + +func (e *Example) Prototype() schema.TypedPrototype { + return bindnode.Prototype(e, exampleType()) +} + +func (*Example) Tag() string { + return exampleTag +} + +func exampleGoldenNode(t *testing.T) datamodel.Node { + t.Helper() + + cbor := golden.Get(t, exampleDAGCBORFilename) + + node, err := ipld.Decode(cbor, dagcbor.Decode) + require.NoError(t, err) + + return node +} + +func examplePrivKey(t *testing.T) crypto.PrivKey { + t.Helper() + + privKeyEnc, err := crypto.ConfigDecodeKey(examplePrivKeyCfg) + require.NoError(t, err) + + privKey, err := crypto.UnmarshalPrivateKey(privKeyEnc) + require.NoError(t, err) + + return privKey +} + +func exampleSignature(t *testing.T) []byte { + t.Helper() + + sig, err := base64.RawStdEncoding.DecodeString(exampleSignatureStr) + require.NoError(t, err) + + return sig +} + +func invalidNodeFromGolden(t *testing.T) datamodel.Node { + t.Helper() + + invalidSig, err := base64.RawStdEncoding.DecodeString(invalidSignatureStr) + require.NoError(t, err) + + envelNode := exampleGoldenNode(t) + sigPayloadNode, err := envelNode.LookupByIndex(1) + require.NoError(t, err) + + node, err := qp.BuildList(basicnode.Prototype.Any, 2, func(la datamodel.ListAssembler) { + qp.ListEntry(la, qp.Bytes(invalidSig)) + qp.ListEntry(la, qp.Node(sigPayloadNode)) + }) + require.NoError(t, err) + + return node +} diff --git a/internal/envelope/ipld.go b/internal/envelope/ipld.go new file mode 100644 index 0000000..d12b1be --- /dev/null +++ b/internal/envelope/ipld.go @@ -0,0 +1,297 @@ +// Package envelope provides functions that convert between wire-format +// encoding of a [UCAN] token's [Envelope] and the Go type representing +// a verified [TokenPayload]. +// +// Encoding functions in this package require a private key as a +// parameter so the VarsigHeader can be set and so that a +// cryptographic signature can be generated. +// +// Decoding functions in this package likewise perform the signature +// verification using a public key extracted from the TokenPayload as +// described by requirement two below. Additionally, the decode functions +// also return the CID for the verified Envelope. +// +// Types that wish to be marshaled and unmarshaled from the using +// is package have two requirements. +// +// 1. The type must implement the Tokener interface. +// +// 2. The IPLD Representation of the type must include an "iss" +// field when the TokenPayload is extracted from the Envelope. +// This field must contain the string representation of a +// "did:key" so that a public key can be extracted from the +// +// [Envelope]:https://github.com/ucan-wg/spec#envelope +// [TokenPayload]: https://github.com/ucan-wg/spec#envelope +// [UCAN]: https://ucan.xyz +package envelope + +import ( + "errors" + "io" + + "github.com/ipfs/go-cid" + "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/ipld/go-ipld-prime/fluent/qp" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/ipld/go-ipld-prime/node/bindnode" + "github.com/ipld/go-ipld-prime/schema" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/internal/varsig" +) + +const varsigHeaderKey = "h" + +// Tokener must be implemented by types that wish to be enclosed in a +// UCAN Envelope (presumbably one of the UCAN token types). +type Tokener interface { + // Prototype provides the schema representation for an IPLD type so + // that the incoming datamodel.Kinds can be mapped to the appropriate + // schema.Kinds. + Prototype() schema.TypedPrototype + + // Tag returns the expected key denoting the name of the IPLD node + // that should be processed as the token payload while decoding + // incoming bytes. + Tag() string +} + +// Decode unmarshals the input data using the format specified by the +// provided codec.Decoder into a Tokener. +// +// An error is returned if the conversion fails, or if the resulting +// Tokener is invalid. +func Decode[T Tokener](b []byte, decFn codec.Decoder) (T, cid.Cid, error) { + node, err := ipld.Decode(b, decFn) + if err != nil { + return *new(T), cid.Undef, err + } + + return FromIPLD[T](node) +} + +// DecodeReader is the same as Decode, but accept an io.Reader. +func DecodeReader[T Tokener](r io.Reader, decFn codec.Decoder) (T, cid.Cid, error) { + node, err := ipld.DecodeStreaming(r, decFn) + if err != nil { + return *new(T), cid.Undef, err + } + + return FromIPLD[T](node) +} + +// FromDagCbor unmarshals the input data into a Tokener. +// +// An error is returned if the conversion fails, or if the resulting +// Tokener is invalid. +func FromDagCbor[T Tokener](b []byte) (T, cid.Cid, error) { + return Decode[T](b, dagcbor.Decode) +} + +// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader. +func FromDagCborReader[T Tokener](r io.Reader) (T, cid.Cid, error) { + return DecodeReader[T](r, dagcbor.Decode) +} + +// FromDagJson unmarshals the input data into a Tokener. +// +// An error is returned if the conversion fails, or if the resulting +// Tokener is invalid. +func FromDagJson[T Tokener](b []byte) (T, cid.Cid, error) { + return Decode[T](b, dagjson.Decode) +} + +// FromDagJsonReader is the same as FromDagJson, but accept an io.Reader. +func FromDagJsonReader[T Tokener](r io.Reader) (T, cid.Cid, error) { + return DecodeReader[T](r, dagjson.Decode) +} + +// FromIPLD unwraps a Tokener from the provided IPLD datamodel.Node. +// +// An error is returned if the conversion fails, or if the resulting +// Tokener is invalid. +func FromIPLD[T Tokener](node datamodel.Node) (T, cid.Cid, error) { + undef := *new(T) + + signatureNode, err := node.LookupByIndex(0) + if err != nil { + return undef, cid.Undef, err + } + + signature, err := signatureNode.AsBytes() + if err != nil { + return undef, cid.Undef, err + } + + sigPayloadNode, err := node.LookupByIndex(1) + if err != nil { + return undef, cid.Undef, err + } + + varsigHeaderNode, err := sigPayloadNode.LookupByString(varsigHeaderKey) + if err != nil { + return undef, cid.Undef, err + } + + tokenPayloadNode, err := sigPayloadNode.LookupByString(undef.Tag()) + if err != nil { + return undef, cid.Undef, err + } + + // This needs to be done before converting this node to it's schema + // representation (afterwards, the field might be renamed os it's safer + // to use the wire name). + issuerNode, err := tokenPayloadNode.LookupByString("iss") + if err != nil { + return undef, cid.Undef, err + } + // ^^^ + + // Replaces the datamodel.Node in tokenPayloadNode with a + // schema.TypedNode so that we can cast it to a *token.Token after + // unwrapping it. + // vvv + nb := undef.Prototype().Representation().NewBuilder() + + err = nb.AssignNode(tokenPayloadNode) + if err != nil { + return undef, cid.Undef, err + } + + tokenPayloadNode = nb.Build() + // ^^^ + + tokenPayload := bindnode.Unwrap(tokenPayloadNode) + if tokenPayload == nil { + return undef, cid.Undef, errors.New("failed to Unwrap the TokenPayload") + } + + tkn, ok := tokenPayload.(T) + if !ok { + return undef, cid.Undef, 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. + // vvv + issuer, err := issuerNode.AsString() + if err != nil { + return undef, cid.Undef, err + } + + issuerDID, err := did.Parse(issuer) + if err != nil { + return undef, cid.Undef, err + } + + issuerPubKey, err := issuerDID.PubKey() + if err != nil { + return undef, cid.Undef, err + } + + issuerVarsigHeader, err := varsig.Encode(issuerPubKey.Type()) + if err != nil { + return undef, cid.Undef, err + } + + varsigHeader, err := varsigHeaderNode.AsBytes() + if err != nil { + return undef, cid.Undef, err + } + + if string(varsigHeader) != string(issuerVarsigHeader) { + return undef, cid.Undef, errors.New("the VarsigHeader key type doesn't match the issuer's key type") + } + + data, err := ipld.Encode(sigPayloadNode, dagcbor.Encode) + if err != nil { + return undef, cid.Undef, err + } + + ok, err = issuerPubKey.Verify(data, signature) + if err != nil || !ok { + return undef, cid.Undef, errors.New("failed to verify the token's signature") + } + // ^^^ + + return tkn, cid.Undef, nil +} + +// Encode marshals a Tokener to the format specified by the provided +// codec.Encoder. +func Encode(privKey crypto.PrivKey, token Tokener, encFn codec.Encoder) ([]byte, error) { + node, err := ToIPLD(privKey, token) + if err != nil { + return nil, err + } + + return ipld.Encode(node, encFn) +} + +// EncodeWriter is the same as Encode but outputs to an io.Writer instead +// of encoding into a []byte. +func EncodeWriter(w io.Writer, privKey crypto.PrivKey, token Tokener, encFn codec.Encoder) error { + node, err := ToIPLD(privKey, token) + if err != nil { + return err + } + + return ipld.EncodeStreaming(w, node, encFn) +} + +// ToDagCbor marshals the Tokener to the DAG-CBOR format. +func ToDagCbor(privKey crypto.PrivKey, token Tokener) ([]byte, error) { + return Encode(privKey, token, dagcbor.Encode) +} + +// ToDagCborWriter is the same as ToDagCbor but outputs to an io.Writer +// instead of encoding into a []byte. +func ToDagCborWriter(w io.Writer, privKey crypto.PrivKey, token Tokener) error { + return EncodeWriter(w, privKey, token, dagcbor.Encode) +} + +// ToDagJson marshals the Tokener to the DAG-JSON format. +func ToDagJson(privKey crypto.PrivKey, token Tokener) ([]byte, error) { + return Encode(privKey, token, dagjson.Encode) +} + +// ToDagJsonWriter is the same as ToDagJson but outputs to an io.Writer +// instead of encoding into a []byte. +func ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey, token Tokener) error { + return EncodeWriter(w, privKey, token, dagjson.Encode) +} + +// ToIPLD wraps the Tokener in an IPLD datamodel.Node. +func ToIPLD(privKey crypto.PrivKey, token Tokener) (datamodel.Node, error) { + tokenPayloadNode := bindnode.Wrap(token, token.Prototype().Type()).Representation() + + varsigHeader, err := varsig.Encode(privKey.Type()) + if err != nil { + return nil, err + } + + sigPayloadNode, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, varsigHeaderKey, qp.Bytes(varsigHeader)) + qp.MapEntry(ma, token.Tag(), qp.Node(tokenPayloadNode)) + }) + + data, err := ipld.Encode(sigPayloadNode, dagcbor.Encode) + if err != nil { + return nil, err + } + + signature, err := privKey.Sign(data) + if err != nil { + return nil, err + } + + return qp.BuildList(basicnode.Prototype.Any, 2, func(la datamodel.ListAssembler) { + qp.ListEntry(la, qp.Bytes(signature)) + qp.ListEntry(la, qp.Node(sigPayloadNode)) + }) +} diff --git a/internal/envelope/ipld_test.go b/internal/envelope/ipld_test.go new file mode 100644 index 0000000..132106d --- /dev/null +++ b/internal/envelope/ipld_test.go @@ -0,0 +1,100 @@ +package envelope_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/internal/envelope" + "gotest.tools/v3/golden" +) + +func TestDecode(t *testing.T) { + t.Parallel() + + t.Run("via FromDagCbor", func(t *testing.T) { + t.Parallel() + + data := golden.Get(t, "example.dagcbor") + + tkn, _, err := envelope.FromDagCbor[*Example](data) + require.NoError(t, err) + assert.Equal(t, exampleGreeting, tkn.Hello) + assert.Equal(t, exampleDID, tkn.Issuer) + }) + + t.Run("via FromDagJson", func(t *testing.T) { + t.Parallel() + + data := golden.Get(t, "example.dagjson") + + tkn, _, err := envelope.FromDagJson[*Example](data) + require.NoError(t, err) + assert.Equal(t, exampleGreeting, tkn.Hello) + assert.Equal(t, exampleDID, tkn.Issuer) + }) +} + +func TestEncode(t *testing.T) { + t.Parallel() + + t.Run("via ToDagCbor", func(t *testing.T) { + t.Parallel() + + data, err := envelope.ToDagCbor(examplePrivKey(t), newExample(t)) + require.NoError(t, err) + golden.AssertBytes(t, data, exampleDAGCBORFilename) + }) + + t.Run("via ToDagJson", func(t *testing.T) { + t.Parallel() + + data, err := envelope.ToDagJson(examplePrivKey(t), newExample(t)) + require.NoError(t, err) + golden.Assert(t, string(data), exampleDAGJSONFilename) + }) +} + +func TestRoundtrip(t *testing.T) { + t.Parallel() + + t.Run("via FromDagCborReader/ToDagCborWriter", func(t *testing.T) { + t.Parallel() + + data := golden.Get(t, exampleDAGCBORFilename) + + tkn, _, err := envelope.FromDagCborReader[*Example](bytes.NewReader(data)) + require.NoError(t, err) + assert.Equal(t, exampleGreeting, tkn.Hello) + assert.Equal(t, exampleDID, tkn.Issuer) + + w := &bytes.Buffer{} + require.NoError(t, envelope.ToDagCborWriter(w, examplePrivKey(t), newExample(t))) + assert.Equal(t, data, w.Bytes()) + }) + + t.Run("via FromDagCbor/ToDagCbor", func(t *testing.T) { + t.Parallel() + + dataIn := golden.Get(t, exampleDAGCBORFilename) + + tkn, _, err := envelope.FromDagCbor[*Example](dataIn) + require.NoError(t, err) + assert.Equal(t, exampleGreeting, tkn.Hello) + assert.Equal(t, exampleDID, tkn.Issuer) + + dataOut, err := envelope.ToDagCbor(examplePrivKey(t), newExample(t)) + require.NoError(t, err) + assert.Equal(t, dataIn, dataOut) + }) +} + +func TestFromIPLD_with_invalid_signature(t *testing.T) { + t.Parallel() + + node := invalidNodeFromGolden(t) + tkn, _, err := envelope.FromIPLD[*Example](node) + assert.Nil(t, tkn) + require.EqualError(t, err, "failed to verify the token's signature") +} diff --git a/internal/envelope/testdata/example.dagcbor b/internal/envelope/testdata/example.dagcbor new file mode 100644 index 0000000..d18e26e --- /dev/null +++ b/internal/envelope/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/internal/envelope/testdata/example.dagjson b/internal/envelope/testdata/example.dagjson new file mode 100644 index 0000000..3db25a5 --- /dev/null +++ b/internal/envelope/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 diff --git a/internal/envelope/testdata/example.ipldsch b/internal/envelope/testdata/example.ipldsch new file mode 100644 index 0000000..92ad02c --- /dev/null +++ b/internal/envelope/testdata/example.ipldsch @@ -0,0 +1,6 @@ +type DID string + +type Example struct { + hello String + issuer DID (rename "iss") +} diff --git a/internal/varsig/varsig.go b/internal/varsig/varsig.go index 03acce7..4645a90 100644 --- a/internal/varsig/varsig.go +++ b/internal/varsig/varsig.go @@ -24,7 +24,6 @@ package varsig import ( - "encoding/base64" "encoding/binary" "errors" "fmt" @@ -130,5 +129,5 @@ func header(vals ...multicodec.Code) string { buf = binary.AppendUvarint(buf, uint64(val)) } - return base64.RawStdEncoding.EncodeToString(buf) + return string(buf) } diff --git a/internal/varsig/varsig_test.go b/internal/varsig/varsig_test.go index eb13ed1..56c8bb5 100644 --- a/internal/varsig/varsig_test.go +++ b/internal/varsig/varsig_test.go @@ -21,7 +21,14 @@ func TestDecode(t *testing.T) { } func ExampleDecode() { - keyType, _ := varsig.Decode([]byte("NIUkEoACcQ")) + hdr, err := base64.RawStdEncoding.DecodeString("NIUkEoACcQ") + if err != nil { + fmt.Println(err.Error()) + + return + } + + keyType, _ := varsig.Decode(hdr) fmt.Println(keyType.String()) // Output: // RSA @@ -37,7 +44,7 @@ func TestEncode(t *testing.T) { func ExampleEncode() { header, _ := varsig.Encode(pb.KeyType_RSA) - fmt.Println(string(header)) + fmt.Println(base64.RawStdEncoding.EncodeToString(header)) // Output: // NIUkEoACcQ