Merge pull request #23 from ucan-wg/envelope2
refactor(envelope): more tests/docs and functions not a type
This commit is contained in:
2
go.mod
2
go.mod
@@ -12,11 +12,13 @@ require (
|
|||||||
github.com/multiformats/go-multicodec v0.9.0
|
github.com/multiformats/go-multicodec v0.9.0
|
||||||
github.com/multiformats/go-varint v0.0.7
|
github.com/multiformats/go-varint v0.0.7
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
|
gotest.tools/v3 v3.5.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // 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/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||||
|
|||||||
12
go.sum
12
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/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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
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=
|
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/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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
||||||
github.com/libp2p/go-libp2p v0.36.2/go.mod h1:XO3joasRE4Eup8yCTTP/+kX+g92mOgRaadk46LmPhHY=
|
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 h1:NHz30+G7D8Y8YmznrVZZla0ofVANrvBl2c+oARfMeDQ=
|
||||||
github.com/libp2p/go-libp2p v0.36.3/go.mod h1:4Y5vFyCUiJuluEPmpnKYf6WFx5ViKPUYs/ixe9ANFZ8=
|
github.com/libp2p/go-libp2p v0.36.3/go.mod h1:4Y5vFyCUiJuluEPmpnKYf6WFx5ViKPUYs/ixe9ANFZ8=
|
||||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
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-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 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
|
||||||
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
|
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 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
|
||||||
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
|
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=
|
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.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 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
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/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.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||||
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||||
|
|||||||
136
internal/envelope/example_test.go
Normal file
136
internal/envelope/example_test.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
297
internal/envelope/ipld.go
Normal file
297
internal/envelope/ipld.go
Normal file
@@ -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))
|
||||||
|
})
|
||||||
|
}
|
||||||
100
internal/envelope/ipld_test.go
Normal file
100
internal/envelope/ipld_test.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
1
internal/envelope/testdata/example.dagcbor
vendored
Normal file
1
internal/envelope/testdata/example.dagcbor
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
‚X@|úŸÀ½â–ðõ+ÁŠ!µ.®ÿhéÍúGO-ü¬”jÉsyÖsY¨quëiþ“ä°¬Íuý#ò¼’ç˜c¢ahD4íqxucan/example@v1.0.0-rc.1¢cissx8did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nhehelloeworld
|
||||||
1
internal/envelope/testdata/example.dagjson
vendored
Normal file
1
internal/envelope/testdata/example.dagjson
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[{"/":{"bytes":"fPqfwL3iFpbw9SvBiq0DIbUurv9o6c36R08tC/yslGrJcwV51ghzWahxdetpEf6T5LCszXX9I/K8khvnmAxjAg"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/example@v1.0.0-rc.1":{"hello":"world","iss":"did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh"}}]
|
||||||
6
internal/envelope/testdata/example.ipldsch
vendored
Normal file
6
internal/envelope/testdata/example.ipldsch
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
type DID string
|
||||||
|
|
||||||
|
type Example struct {
|
||||||
|
hello String
|
||||||
|
issuer DID (rename "iss")
|
||||||
|
}
|
||||||
@@ -24,7 +24,6 @@
|
|||||||
package varsig
|
package varsig
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -130,5 +129,5 @@ func header(vals ...multicodec.Code) string {
|
|||||||
buf = binary.AppendUvarint(buf, uint64(val))
|
buf = binary.AppendUvarint(buf, uint64(val))
|
||||||
}
|
}
|
||||||
|
|
||||||
return base64.RawStdEncoding.EncodeToString(buf)
|
return string(buf)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,14 @@ func TestDecode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ExampleDecode() {
|
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())
|
fmt.Println(keyType.String())
|
||||||
// Output:
|
// Output:
|
||||||
// RSA
|
// RSA
|
||||||
@@ -37,7 +44,7 @@ func TestEncode(t *testing.T) {
|
|||||||
|
|
||||||
func ExampleEncode() {
|
func ExampleEncode() {
|
||||||
header, _ := varsig.Encode(pb.KeyType_RSA)
|
header, _ := varsig.Encode(pb.KeyType_RSA)
|
||||||
fmt.Println(string(header))
|
fmt.Println(base64.RawStdEncoding.EncodeToString(header))
|
||||||
|
|
||||||
// Output:
|
// Output:
|
||||||
// NIUkEoACcQ
|
// NIUkEoACcQ
|
||||||
|
|||||||
Reference in New Issue
Block a user