Merge branch 'v1' into v1-policy-subset-selection
This commit is contained in:
@@ -1,82 +0,0 @@
|
||||
package delegation_test
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gotest.tools/v3/golden"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/delegation"
|
||||
)
|
||||
|
||||
//go:embed delegation.ipldsch
|
||||
var schemaBytes []byte
|
||||
|
||||
func TestSchemaRoundTrip(t *testing.T) {
|
||||
// const delegationJson = `
|
||||
// {
|
||||
// "aud":"did:key:def456",
|
||||
// "cmd":"/foo/bar",
|
||||
// "exp":123456,
|
||||
// "iss":"did:key:abc123",
|
||||
// "meta":{
|
||||
// "bar":"baaar",
|
||||
// "foo":"fooo"
|
||||
// },
|
||||
// "nbf":123456,
|
||||
// "nonce":{
|
||||
// "/":{
|
||||
// "bytes":"c3VwZXItcmFuZG9t"
|
||||
// }
|
||||
// },
|
||||
// "pol":[
|
||||
// ["==", ".status", "draft"],
|
||||
// ["all", ".reviewer", [
|
||||
// ["like", ".email", "*@example.com"]]
|
||||
// ],
|
||||
// ["any", ".tags", [
|
||||
// ["or", [
|
||||
// ["==", ".", "news"],
|
||||
// ["==", ".", "press"]]
|
||||
// ]]
|
||||
// ]
|
||||
// ],
|
||||
// "sub":""
|
||||
// }
|
||||
// `
|
||||
|
||||
delegationJson := golden.Get(t, "new.dagjson")
|
||||
privKey := privKey(t, issuerPrivKeyCfg)
|
||||
|
||||
// format: dagJson --> PayloadModel --> dagCbor --> PayloadModel --> dagJson
|
||||
// function: DecodeDagJson() EncodeDagCbor() DecodeDagCbor() EncodeDagJson()
|
||||
|
||||
p1, err := delegation.FromDagJson([]byte(delegationJson))
|
||||
require.NoError(t, err)
|
||||
|
||||
cborBytes, err := p1.ToDagCbor(privKey)
|
||||
require.NoError(t, err)
|
||||
fmt.Println("cborBytes length", len(cborBytes))
|
||||
fmt.Println("cbor", string(cborBytes))
|
||||
|
||||
p2, err := delegation.FromDagCbor(cborBytes)
|
||||
require.NoError(t, err)
|
||||
fmt.Println("read Cbor", p2)
|
||||
|
||||
readJson, err := p2.ToDagJson(privKey)
|
||||
require.NoError(t, err)
|
||||
fmt.Println("readJson length", len(readJson))
|
||||
fmt.Println("json: ", string(readJson))
|
||||
|
||||
require.JSONEq(t, string(delegationJson), string(readJson))
|
||||
}
|
||||
|
||||
func BenchmarkSchemaLoad(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = ipld.LoadSchemaBytes(schemaBytes)
|
||||
}
|
||||
}
|
||||
2
go.mod
2
go.mod
@@ -10,6 +10,7 @@ require (
|
||||
github.com/libp2p/go-libp2p v0.36.3
|
||||
github.com/multiformats/go-multibase v0.2.0
|
||||
github.com/multiformats/go-multicodec v0.9.0
|
||||
github.com/multiformats/go-multihash v0.2.3
|
||||
github.com/multiformats/go-varint v0.0.7
|
||||
github.com/stretchr/testify v1.9.0
|
||||
gotest.tools/v3 v3.5.1
|
||||
@@ -24,7 +25,6 @@ require (
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||
github.com/multiformats/go-base36 v0.2.0 // indirect
|
||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/polydawn/refmt v0.89.0 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/capability/command"
|
||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||
)
|
||||
|
||||
func TestTop(t *testing.T) {
|
||||
@@ -2,12 +2,16 @@ package meta
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
)
|
||||
|
||||
var ErrUnsupported = errors.New("failure adding unsupported type to meta")
|
||||
|
||||
var ErrNotFound = errors.New("key-value not found in meta")
|
||||
|
||||
// Meta is a container for meta key-value pairs in a UCAN token.
|
||||
@@ -113,8 +117,20 @@ func (m *Meta) Add(key string, val any) error {
|
||||
case datamodel.Node:
|
||||
m.Values[key] = val
|
||||
default:
|
||||
panic("invalid value type")
|
||||
return fmt.Errorf("%w: %s", ErrUnsupported, fqtn(val))
|
||||
}
|
||||
m.Keys = append(m.Keys, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func fqtn(val any) string {
|
||||
var name string
|
||||
|
||||
t := reflect.TypeOf(val)
|
||||
for t.Kind() == reflect.Pointer {
|
||||
name += "*"
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
return name + t.PkgPath() + "." + t.Name()
|
||||
}
|
||||
|
||||
23
pkg/meta/meta_test.go
Normal file
23
pkg/meta/meta_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package meta_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestMeta_Add(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type Unsupported struct{}
|
||||
|
||||
t.Run("error if not primative or Node", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := (&meta.Meta{}).Add("invalid", &Unsupported{})
|
||||
require.ErrorIs(t, err, meta.ErrUnsupported)
|
||||
assert.ErrorContains(t, err, "*github.com/ucan-wg/go-ucan/pkg/meta_test.Unsupported")
|
||||
})
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/ipld/go-ipld-prime/must"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/capability/policy/selector"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||
)
|
||||
|
||||
func FromIPLD(node datamodel.Node) (Policy, error) {
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/must"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/capability/policy/selector"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||
)
|
||||
|
||||
func (p Policy) Filter(sel selector.Selector) Policy {
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/capability/policy/literal"
|
||||
"github.com/ucan-wg/go-ucan/capability/policy/selector"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||
)
|
||||
|
||||
func TestMatch(t *testing.T) {
|
||||
@@ -5,7 +5,7 @@ package policy
|
||||
import (
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/capability/policy/selector"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/capability/policy/selector"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||
)
|
||||
|
||||
// TestSupported Forms runs tests against the Selector according to the
|
||||
@@ -1,19 +1,30 @@
|
||||
// Package delegation implements the UCAN [delegation] specification with
|
||||
// an immutable Token type as well as methods to convert the Token to and
|
||||
// from the [envelope]-enclosed, signed and DAG-CBOR-encoded form that
|
||||
// should most commonly be used for transport and storage.
|
||||
//
|
||||
// [delegation]: https://github.com/ucan-wg/delegation/tree/v1_ipld
|
||||
// [envelope]: https://github.com/ucan-wg/spec#envelope
|
||||
package delegation
|
||||
|
||||
// TODO: change the "delegation" link above when the specification is merged
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/capability/command"
|
||||
"github.com/ucan-wg/go-ucan/capability/policy"
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||
)
|
||||
|
||||
// Token is an immutable type that holds the fields of a UCAN delegation.
|
||||
type Token struct {
|
||||
// Issuer DID (sender)
|
||||
issuer did.DID
|
||||
@@ -33,6 +44,8 @@ type Token struct {
|
||||
notBefore *time.Time
|
||||
// The timestamp at which the Invocation becomes invalid
|
||||
expiration *time.Time
|
||||
// The CID of the Token when enclosed in an Envelope and encoded to DAG-CBOR
|
||||
cid cid.Cid
|
||||
}
|
||||
|
||||
// New creates a validated Token from the provided parameters and options.
|
||||
@@ -50,6 +63,7 @@ func New(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Po
|
||||
policy: pol,
|
||||
meta: meta.NewMeta(),
|
||||
nonce: nil,
|
||||
cid: cid.Undef,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
@@ -132,6 +146,13 @@ func (t *Token) Expiration() *time.Time {
|
||||
return t.expiration
|
||||
}
|
||||
|
||||
// CID returns the content identifier of the Token model when enclosed
|
||||
// in an Envelope and encoded to DAG-CBOR.
|
||||
// Returns cid.Undef if the token has not been serialized or deserialized yet.
|
||||
func (t *Token) CID() cid.Cid {
|
||||
return t.cid
|
||||
}
|
||||
|
||||
func (t *Token) validate() error {
|
||||
var errs error
|
||||
|
||||
@@ -151,70 +172,6 @@ func (t *Token) validate() error {
|
||||
return errs
|
||||
}
|
||||
|
||||
type Option func(*Token) error
|
||||
|
||||
// WithExpiration set's the Token's optional "expiration" field to the
|
||||
// value of the provided time.Time.
|
||||
func WithExpiration(exp time.Time) Option {
|
||||
return func(t *Token) error {
|
||||
if exp.Before(time.Now()) {
|
||||
return fmt.Errorf("a Token's expiration should be set to a time in the future: %s", exp.String())
|
||||
}
|
||||
|
||||
t.expiration = &exp
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithMeta adds a key/value pair in the "meta" field.
|
||||
// WithMeta can be used multiple times in the same call.
|
||||
// Accepted types for the value are: bool, string, int, int32, int64, []byte,
|
||||
// and ipld.Node.
|
||||
func WithMeta(key string, val any) Option {
|
||||
return func(t *Token) error {
|
||||
return t.meta.Add(key, val)
|
||||
}
|
||||
}
|
||||
|
||||
// WithNotBefore set's the Token's optional "notBefore" field to the value
|
||||
// of the provided time.Time.
|
||||
func WithNotBefore(nbf time.Time) Option {
|
||||
return func(t *Token) error {
|
||||
if nbf.Before(time.Now()) {
|
||||
return fmt.Errorf("a Token's \"not before\" field should be set to a time in the future: %s", nbf.String())
|
||||
}
|
||||
|
||||
t.notBefore = &nbf
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSubject sets the Tokens's optional "subject" field to the value of
|
||||
// provided did.DID.
|
||||
//
|
||||
// This Option should only be used with the New constructor - since
|
||||
// Subject is a required parameter when creating a Token via the Root
|
||||
// constructor, any value provided via this Option will be silently
|
||||
// overwritten.
|
||||
func WithSubject(sub did.DID) Option {
|
||||
return func(t *Token) error {
|
||||
t.subject = sub
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithNonce sets the Token's nonce with the given value.
|
||||
// If this option is not used, a random 12-byte nonce is generated for this required field.
|
||||
func WithNonce(nonce []byte) Option {
|
||||
return func(t *Token) error {
|
||||
t.nonce = nonce
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tokenFromModel build a decoded view of the raw IPLD data.
|
||||
// This function also serves as validation.
|
||||
func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
||||
@@ -277,6 +234,7 @@ func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
||||
}
|
||||
|
||||
// generateNonce creates a 12-byte random nonce.
|
||||
// TODO: some crypto scheme require more, is that our case?
|
||||
func generateNonce() ([]byte, error) {
|
||||
res := make([]byte, 12)
|
||||
_, err := rand.Read(res)
|
||||
@@ -1,7 +1,6 @@
|
||||
package delegation_test
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -9,10 +8,10 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"gotest.tools/v3/golden"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/capability/command"
|
||||
"github.com/ucan-wg/go-ucan/capability/policy"
|
||||
"github.com/ucan-wg/go-ucan/delegation"
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||
"github.com/ucan-wg/go-ucan/tokens/delegation"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -64,6 +63,9 @@ const (
|
||||
]
|
||||
]
|
||||
`
|
||||
|
||||
newCID = "zdpuAn9JgGPvnt2WCmTaKktZdbuvcVGTg9bUT5kQaufwUtZ6e"
|
||||
rootCID = "zdpuAkgGmUp5JrXvehGuuw9JA8DLQKDaxtK3R8brDQQVC2i5X"
|
||||
)
|
||||
|
||||
func TestConstructors(t *testing.T) {
|
||||
@@ -86,7 +88,7 @@ func TestConstructors(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("New", func(t *testing.T) {
|
||||
dlg, err := delegation.New(privKey, aud, cmd, pol,
|
||||
tkn, err := delegation.New(privKey, aud, cmd, pol,
|
||||
delegation.WithNonce([]byte(nonce)),
|
||||
delegation.WithSubject(sub),
|
||||
delegation.WithExpiration(exp),
|
||||
@@ -95,7 +97,7 @@ func TestConstructors(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := dlg.ToDagJson(privKey)
|
||||
data, err := tkn.ToDagJson(privKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log(string(data))
|
||||
@@ -106,7 +108,7 @@ func TestConstructors(t *testing.T) {
|
||||
t.Run("Root", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dlg, err := delegation.Root(privKey, aud, cmd, pol,
|
||||
tkn, err := delegation.Root(privKey, aud, cmd, pol,
|
||||
delegation.WithNonce([]byte(nonce)),
|
||||
delegation.WithExpiration(exp),
|
||||
delegation.WithMeta("foo", "fooo"),
|
||||
@@ -114,7 +116,7 @@ func TestConstructors(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := dlg.ToDagJson(privKey)
|
||||
data, err := tkn.ToDagJson(privKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log(string(data))
|
||||
@@ -123,9 +125,7 @@ func TestConstructors(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func privKey(t *testing.T, privKeyCfg string) crypto.PrivKey {
|
||||
t.Helper()
|
||||
|
||||
func privKey(t require.TestingT, privKeyCfg string) crypto.PrivKey {
|
||||
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -134,23 +134,3 @@ func privKey(t *testing.T, privKeyCfg string) crypto.PrivKey {
|
||||
|
||||
return privKey
|
||||
}
|
||||
|
||||
func TestKey(t *testing.T) {
|
||||
// TODO: why is this broken?
|
||||
t.Skip("TODO: why is this broken?")
|
||||
|
||||
priv, _, err := crypto.GenerateEd25519Key(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
privMar, err := crypto.MarshalPrivateKey(priv)
|
||||
require.NoError(t, err)
|
||||
|
||||
privCfg := crypto.ConfigEncodeKey(privMar)
|
||||
t.Log(privCfg)
|
||||
|
||||
id, err := did.FromPubKey(priv.GetPublic())
|
||||
require.NoError(t, err)
|
||||
t.Log(id)
|
||||
|
||||
t.Fail()
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package delegation
|
||||
import (
|
||||
"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"
|
||||
@@ -11,13 +12,80 @@ import (
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/internal/envelope"
|
||||
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
|
||||
)
|
||||
|
||||
// ToSealed wraps the delegation token in an envelope, generates the
|
||||
// signature, encodes the result to DAG-CBOR and calculates the CID of
|
||||
// the resulting binary data.
|
||||
func (t *Token) ToSealed(privKey crypto.PrivKey) ([]byte, cid.Cid, error) {
|
||||
data, err := t.ToDagCbor(privKey)
|
||||
if err != nil {
|
||||
return nil, cid.Undef, err
|
||||
}
|
||||
|
||||
id, err := envelope.CIDFromBytes(data)
|
||||
if err != nil {
|
||||
return nil, cid.Undef, err
|
||||
}
|
||||
|
||||
return data, id, nil
|
||||
}
|
||||
|
||||
// ToSealedWriter is the same as Seal but accepts an io.Writer.
|
||||
func (t *Token) ToSealedWriter(w io.Writer, privKey crypto.PrivKey) (cid.Cid, error) {
|
||||
cidWriter := envelope.NewCIDWriter(w)
|
||||
|
||||
if err := t.ToDagCborWriter(cidWriter, privKey); err != nil {
|
||||
return cid.Undef, err
|
||||
}
|
||||
|
||||
return cidWriter.CID()
|
||||
}
|
||||
|
||||
// FromSealed decodes the provided binary data from the DAG-CBOR format,
|
||||
// verifies that the envelope's signature is correct based on the public
|
||||
// key taken from the issuer (iss) field and calculates the CID of the
|
||||
// incoming data.
|
||||
func FromSealed(data []byte) (*Token, error) {
|
||||
tkn, err := FromDagCbor(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := envelope.CIDFromBytes(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tkn.cid = id
|
||||
|
||||
return tkn, nil
|
||||
}
|
||||
|
||||
// FromSealedReader is the same as Unseal but accepts an io.Reader.
|
||||
func FromSealedReader(r io.Reader) (*Token, error) {
|
||||
cidReader := envelope.NewCIDReader(r)
|
||||
|
||||
tkn, err := FromDagCborReader(cidReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := cidReader.CID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tkn.cid = id
|
||||
|
||||
return tkn, nil
|
||||
}
|
||||
|
||||
// Encode marshals a View 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)
|
||||
node, err := t.toIPLD(privKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -27,7 +95,7 @@ func (t *Token) Encode(privKey crypto.PrivKey, encFn codec.Encoder) ([]byte, err
|
||||
|
||||
// EncodeWriter is the same as Encode but accepts an io.Writer.
|
||||
func (t *Token) EncodeWriter(w io.Writer, privKey crypto.PrivKey, encFn codec.Encoder) error {
|
||||
node, err := t.ToIPLD(privKey)
|
||||
node, err := t.toIPLD(privKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -55,9 +123,81 @@ func (t *Token) ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey) error {
|
||||
return t.EncodeWriter(w, privKey, dagjson.Encode)
|
||||
}
|
||||
|
||||
// ToIPLD wraps the View in an IPLD datamodel.Node.
|
||||
func (t *Token) ToIPLD(privKey crypto.PrivKey) (datamodel.Node, error) {
|
||||
// Decode unmarshals the input data using the format specified by the
|
||||
// provided codec.Decoder into a View.
|
||||
//
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// View 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)
|
||||
}
|
||||
|
||||
// DecodeReader is the same as Decode, but accept an io.Reader.
|
||||
func DecodeReader(r io.Reader, decFn codec.Decoder) (*Token, error) {
|
||||
node, err := ipld.DecodeStreaming(r, decFn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fromIPLD(node)
|
||||
}
|
||||
|
||||
// FromDagCbor unmarshals the input data into a View.
|
||||
//
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// View is invalid.
|
||||
func FromDagCbor(data []byte) (*Token, error) {
|
||||
pay, err := envelope.FromDagCbor[*tokenPayloadModel](data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tkn, err := tokenFromModel(*pay)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tkn, err
|
||||
}
|
||||
|
||||
// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.
|
||||
func FromDagCborReader(r io.Reader) (*Token, error) {
|
||||
return DecodeReader(r, dagcbor.Decode)
|
||||
}
|
||||
|
||||
// FromDagJson unmarshals the input data into a View.
|
||||
//
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// View is invalid.
|
||||
func FromDagJson(data []byte) (*Token, error) {
|
||||
return Decode(data, dagjson.Decode)
|
||||
}
|
||||
|
||||
// FromDagJsonReader is the same as FromDagJson, but accept an io.Reader.
|
||||
func FromDagJsonReader(r io.Reader) (*Token, error) {
|
||||
return DecodeReader(r, dagjson.Decode)
|
||||
}
|
||||
|
||||
func fromIPLD(node datamodel.Node) (*Token, error) {
|
||||
pay, err := envelope.FromIPLD[*tokenPayloadModel](node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tkn, err := tokenFromModel(*pay)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tkn, err
|
||||
}
|
||||
|
||||
func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) {
|
||||
var sub *string
|
||||
|
||||
if t.subject != did.Undef {
|
||||
s := t.subject.String()
|
||||
sub = &s
|
||||
@@ -94,64 +234,3 @@ func (t *Token) ToIPLD(privKey crypto.PrivKey) (datamodel.Node, error) {
|
||||
|
||||
return envelope.ToIPLD(privKey, model)
|
||||
}
|
||||
|
||||
// Decode unmarshals the input data using the format specified by the
|
||||
// provided codec.Decoder into a View.
|
||||
//
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// View 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)
|
||||
}
|
||||
|
||||
// DecodeReader is the same as Decode, but accept an io.Reader.
|
||||
func DecodeReader(r io.Reader, decFn codec.Decoder) (*Token, error) {
|
||||
node, err := ipld.DecodeStreaming(r, decFn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return FromIPLD(node)
|
||||
}
|
||||
|
||||
// FromDagCbor unmarshals the input data into a View.
|
||||
//
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// View is invalid.
|
||||
func FromDagCbor(data []byte) (*Token, error) {
|
||||
return Decode(data, dagcbor.Decode)
|
||||
}
|
||||
|
||||
// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.
|
||||
func FromDagCborReader(r io.Reader) (*Token, error) {
|
||||
return DecodeReader(r, dagcbor.Decode)
|
||||
}
|
||||
|
||||
// FromDagJson unmarshals the input data into a View.
|
||||
//
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// View is invalid.
|
||||
func FromDagJson(data []byte) (*Token, error) {
|
||||
return Decode(data, dagjson.Decode)
|
||||
}
|
||||
|
||||
// FromDagJsonReader is the same as FromDagJson, but accept an io.Reader.
|
||||
func FromDagJsonReader(r io.Reader) (*Token, error) {
|
||||
return DecodeReader(r, dagjson.Decode)
|
||||
}
|
||||
|
||||
// FromIPLD unwraps a View from the provided IPLD datamodel.Node
|
||||
//
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// View is invalid.
|
||||
func FromIPLD(node datamodel.Node) (*Token, error) {
|
||||
tkn, _, err := envelope.FromIPLD[*tokenPayloadModel](node) // TODO add CID to view
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tokenFromModel(*tkn)
|
||||
}
|
||||
72
tokens/delegation/options.go
Normal file
72
tokens/delegation/options.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package delegation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
)
|
||||
|
||||
// Option is a type that allows optional fields to be set during the
|
||||
// creation of a Token.
|
||||
type Option func(*Token) error
|
||||
|
||||
// WithExpiration set's the Token's optional "expiration" field to the
|
||||
// value of the provided time.Time.
|
||||
func WithExpiration(exp time.Time) Option {
|
||||
return func(t *Token) error {
|
||||
if exp.Before(time.Now()) {
|
||||
return fmt.Errorf("a Token's expiration should be set to a time in the future: %s", exp.String())
|
||||
}
|
||||
|
||||
t.expiration = &exp
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithMeta adds a key/value pair in the "meta" field.
|
||||
//
|
||||
// WithMeta can be used multiple times in the same call.
|
||||
// Accepted types for the value are: bool, string, int, int32, int64, []byte,
|
||||
// and ipld.Node.
|
||||
func WithMeta(key string, val any) Option {
|
||||
return func(t *Token) error {
|
||||
return t.meta.Add(key, val)
|
||||
}
|
||||
}
|
||||
|
||||
// WithNotBefore set's the Token's optional "notBefore" field to the value
|
||||
// of the provided time.Time.
|
||||
func WithNotBefore(nbf time.Time) Option {
|
||||
return func(t *Token) error {
|
||||
if nbf.Before(time.Now()) {
|
||||
return fmt.Errorf("a Token's \"not before\" field should be set to a time in the future: %s", nbf.String())
|
||||
}
|
||||
|
||||
t.notBefore = &nbf
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSubject sets the Tokens's optional "subject" field to the value of
|
||||
// provided did.DID.
|
||||
//
|
||||
// This Option should only be used with the New constructor - since
|
||||
// Subject is a required parameter when creating a Token via the Root
|
||||
// constructor, any value provided via this Option will be silently
|
||||
// overwritten.
|
||||
func WithSubject(sub did.DID) Option {
|
||||
return func(t *Token) error {
|
||||
t.subject = sub
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithNonce sets the Token's nonce with the given value.
|
||||
// If this option is not used, a random 12-byte nonce is generated for this required field.
|
||||
func WithNonce(nonce []byte) Option {
|
||||
return func(t *Token) error {
|
||||
t.nonce = nonce
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,18 @@ import (
|
||||
"github.com/ipld/go-ipld-prime/node/bindnode"
|
||||
"github.com/ipld/go-ipld-prime/schema"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/internal/envelope"
|
||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
|
||||
)
|
||||
|
||||
// [Tag] is the string used as a key within the SigPayload that identifies
|
||||
// that the TokenPayload is a delegation.
|
||||
//
|
||||
// [Tag]: https://github.com/ucan-wg/delegation/tree/v1_ipld#type-tag
|
||||
const Tag = "ucan/dlg@1.0.0-rc.1"
|
||||
|
||||
// TODO: update the above Tag URL once the delegation specification is merged.
|
||||
|
||||
//go:embed delegation.ipldsch
|
||||
var schemaBytes []byte
|
||||
|
||||
177
tokens/delegation/schema_test.go
Normal file
177
tokens/delegation/schema_test.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package delegation_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gotest.tools/v3/golden"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/tokens/delegation"
|
||||
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
|
||||
)
|
||||
|
||||
//go:embed delegation.ipldsch
|
||||
var schemaBytes []byte
|
||||
|
||||
func TestSchemaRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
delegationJson := golden.Get(t, "new.dagjson")
|
||||
privKey := privKey(t, issuerPrivKeyCfg)
|
||||
|
||||
t.Run("via buffers", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// format: dagJson --> PayloadModel --> dagCbor --> PayloadModel --> dagJson
|
||||
// function: DecodeDagJson() Seal() Unseal() EncodeDagJson()
|
||||
|
||||
p1, err := delegation.FromDagJson(delegationJson)
|
||||
require.NoError(t, err)
|
||||
|
||||
cborBytes, id, err := p1.ToSealed(privKey)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
|
||||
fmt.Println("cborBytes length", len(cborBytes))
|
||||
fmt.Println("cbor", string(cborBytes))
|
||||
|
||||
p2, err := delegation.FromSealed(cborBytes)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, id, p2.CID())
|
||||
fmt.Println("read Cbor", p2)
|
||||
|
||||
readJson, err := p2.ToDagJson(privKey)
|
||||
require.NoError(t, err)
|
||||
fmt.Println("readJson length", len(readJson))
|
||||
fmt.Println("json: ", string(readJson))
|
||||
|
||||
assert.JSONEq(t, string(delegationJson), string(readJson))
|
||||
})
|
||||
|
||||
t.Run("via streaming", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
buf := bytes.NewBuffer(delegationJson)
|
||||
|
||||
// format: dagJson --> PayloadModel --> dagCbor --> PayloadModel --> dagJson
|
||||
// function: DecodeDagJson() Seal() Unseal() EncodeDagJson()
|
||||
|
||||
p1, err := delegation.FromDagJsonReader(buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
cborBytes := &bytes.Buffer{}
|
||||
id, err := p1.ToSealedWriter(cborBytes, privKey)
|
||||
t.Log(len(id.Bytes()), id.Bytes())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
|
||||
|
||||
// buf = bytes.NewBuffer(cborBytes.Bytes())
|
||||
p2, err := delegation.FromSealedReader(cborBytes)
|
||||
require.NoError(t, err)
|
||||
t.Log(len(p2.CID().Bytes()), p2.CID().Bytes())
|
||||
assert.Equal(t, envelope.CIDToBase58BTC(id), envelope.CIDToBase58BTC(p2.CID()))
|
||||
|
||||
readJson := &bytes.Buffer{}
|
||||
require.NoError(t, p2.ToDagJsonWriter(readJson, privKey))
|
||||
|
||||
assert.JSONEq(t, string(delegationJson), readJson.String())
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkSchemaLoad(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = ipld.LoadSchemaBytes(schemaBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRoundTrip(b *testing.B) {
|
||||
delegationJson := golden.Get(b, "new.dagjson")
|
||||
privKey := privKey(b, issuerPrivKeyCfg)
|
||||
|
||||
b.Run("via buffers", func(b *testing.B) {
|
||||
p1, _ := delegation.FromDagJson(delegationJson)
|
||||
cborBytes, _, _ := p1.ToSealed(privKey)
|
||||
p2, _ := delegation.FromSealed(cborBytes)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
b.Run("FromDagJson", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = delegation.FromDagJson(delegationJson)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Seal", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _, _ = p1.ToSealed(privKey)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Unseal", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = delegation.FromSealed(cborBytes)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ToDagJson", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = p2.ToDagJson(privKey)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("via streaming", func(b *testing.B) {
|
||||
p1, _ := delegation.FromDagJsonReader(bytes.NewReader(delegationJson))
|
||||
cborBuf := &bytes.Buffer{}
|
||||
_, _ = p1.ToSealedWriter(cborBuf, privKey)
|
||||
cborBytes := cborBuf.Bytes()
|
||||
p2, _ := delegation.FromSealedReader(bytes.NewReader(cborBytes))
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
b.Run("FromDagJsonReader", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
reader := bytes.NewReader(delegationJson)
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = reader.Seek(0, 0)
|
||||
_, _ = delegation.FromDagJsonReader(reader)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("SealWriter", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
buf := &bytes.Buffer{}
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf.Reset()
|
||||
_, _ = p1.ToSealedWriter(buf, privKey)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("UnsealReader", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
reader := bytes.NewReader(cborBytes)
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = reader.Seek(0, 0)
|
||||
_, _ = delegation.FromSealedReader(reader)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ToDagJsonReader", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
buf := &bytes.Buffer{}
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf.Reset()
|
||||
_ = p2.ToDagJsonWriter(buf, privKey)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
124
tokens/internal/envelope/cid.go
Normal file
124
tokens/internal/envelope/cid.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package envelope
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"hash"
|
||||
"io"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/multiformats/go-multibase"
|
||||
"github.com/multiformats/go-multicodec"
|
||||
"github.com/multiformats/go-multihash"
|
||||
)
|
||||
|
||||
var b58BTCEnc = multibase.MustNewEncoder(multibase.Base58BTC)
|
||||
|
||||
// CIDToBase56BTC is a utility method to convert a CIDv1 to the canonical
|
||||
// string representation used by UCAN.
|
||||
func CIDToBase58BTC(id cid.Cid) string {
|
||||
return id.Encode(b58BTCEnc)
|
||||
}
|
||||
|
||||
// CIDFromBytes returns the UCAN content identifier for an arbitrary slice
|
||||
// of bytes.
|
||||
func CIDFromBytes(b []byte) (cid.Cid, error) {
|
||||
return cid.V1Builder{
|
||||
Codec: uint64(multicodec.DagCbor),
|
||||
MhType: multihash.SHA2_256,
|
||||
MhLength: 0,
|
||||
}.Sum(b)
|
||||
}
|
||||
|
||||
var _ io.Reader = (*CIDReader)(nil)
|
||||
|
||||
// CIDReader wraps an io.Reader and includes a hash.Hash that is
|
||||
// incrementally updated as data is read from the child io.Reader.
|
||||
type CIDReader struct {
|
||||
hash hash.Hash
|
||||
r io.Reader
|
||||
err error
|
||||
}
|
||||
|
||||
// NewCIDReader initializes a hash.Hash to calculate the CID's hash and
|
||||
// returns the wrapped io.Reader.
|
||||
func NewCIDReader(r io.Reader) *CIDReader {
|
||||
h := sha256.New()
|
||||
h.Reset()
|
||||
|
||||
return &CIDReader{
|
||||
hash: h,
|
||||
r: r,
|
||||
}
|
||||
}
|
||||
|
||||
// CID returns the UCAN-formatted cid.Cid created from the hash calculated
|
||||
// as bytes were read from the inner io.Reader.
|
||||
func (r *CIDReader) CID() (cid.Cid, error) {
|
||||
if r.err != nil {
|
||||
return cid.Undef, r.err // TODO: Wrap to say it's an error during streaming?
|
||||
}
|
||||
|
||||
return cidFromHash(r.hash)
|
||||
}
|
||||
|
||||
// Read implements io.Reader.
|
||||
func (r *CIDReader) Read(p []byte) (n int, err error) {
|
||||
n, err = r.r.Read(p)
|
||||
if err != nil && err != io.EOF {
|
||||
r.err = err
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = r.hash.Write(p[:n])
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var _ io.Writer = (*CIDWriter)(nil)
|
||||
|
||||
// CIDWriter wraps an io.Writer and includes a hash.Hash that is
|
||||
// incrementally updated as data is written to the child io.Writer.
|
||||
type CIDWriter struct {
|
||||
hash hash.Hash
|
||||
w io.Writer
|
||||
err error
|
||||
}
|
||||
|
||||
// NewCIDWriter initializes a hash.Hash to calculate the CID's hash and
|
||||
// returns the wrapped io.Writer.
|
||||
func NewCIDWriter(w io.Writer) *CIDWriter {
|
||||
h := sha256.New()
|
||||
h.Reset()
|
||||
|
||||
return &CIDWriter{
|
||||
hash: h,
|
||||
w: w,
|
||||
}
|
||||
}
|
||||
|
||||
// CID returns the UCAN-formatted cid.Cid created from the hash calculated
|
||||
// as bytes were written from the inner io.Reader.
|
||||
func (w *CIDWriter) CID() (cid.Cid, error) {
|
||||
return cidFromHash(w.hash)
|
||||
}
|
||||
|
||||
// Write implements io.Writer.
|
||||
func (w *CIDWriter) Write(p []byte) (n int, err error) {
|
||||
if _, err = w.hash.Write(p); err != nil {
|
||||
w.err = err
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return w.w.Write(p)
|
||||
}
|
||||
|
||||
func cidFromHash(hash hash.Hash) (cid.Cid, error) {
|
||||
mh, err := multihash.Encode(hash.Sum(nil), multihash.SHA2_256)
|
||||
if err != nil {
|
||||
return cid.Undef, err
|
||||
}
|
||||
|
||||
return cid.NewCidV1(uint64(multicodec.DagCbor), mh), nil
|
||||
}
|
||||
86
tokens/internal/envelope/cid_test.go
Normal file
86
tokens/internal/envelope/cid_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package envelope_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/multiformats/go-multicodec"
|
||||
"github.com/multiformats/go-multihash"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gotest.tools/v3/golden"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
|
||||
)
|
||||
|
||||
func TestCidFromBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expData := golden.Get(t, "example.dagcbor")
|
||||
expHash, err := multihash.Sum(expData, uint64(multicodec.Sha2_256), -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := envelope.ToDagCbor(examplePrivKey(t), newExample(t))
|
||||
require.NoError(t, err)
|
||||
|
||||
id, err := envelope.CIDFromBytes(data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, exampleCID, envelope.CIDToBase58BTC(id))
|
||||
assert.Equal(t, expHash, id.Hash())
|
||||
}
|
||||
|
||||
func TestStreaming(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expData := []byte("this is a test")
|
||||
|
||||
expCID, err := cid.V1Builder{
|
||||
Codec: uint64(multicodec.DagCbor),
|
||||
MhType: multihash.SHA2_256,
|
||||
MhLength: 0,
|
||||
}.Sum(expData)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("CIDReader()", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r, w := io.Pipe() //nolint:varnamelen
|
||||
cidReader := envelope.NewCIDReader(r)
|
||||
|
||||
go func() {
|
||||
_, err := w.Write(expData)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, w.Close())
|
||||
}()
|
||||
|
||||
actData, err := io.ReadAll(cidReader)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expData, actData)
|
||||
|
||||
actCID, err := cidReader.CID()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expCID, actCID)
|
||||
})
|
||||
|
||||
t.Run("CIDWriter", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r, w := io.Pipe() //nolint:varnamelen
|
||||
cidWriter := envelope.NewCIDWriter(w)
|
||||
|
||||
go func() {
|
||||
_, err := cidWriter.Write(expData)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, w.Close())
|
||||
}()
|
||||
|
||||
actData, err := io.ReadAll(r)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expData, actData)
|
||||
|
||||
actCID, err := cidWriter.CID()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expCID, actCID)
|
||||
})
|
||||
}
|
||||
@@ -16,11 +16,12 @@ 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/internal/envelope"
|
||||
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
|
||||
const (
|
||||
exampleCID = "zdpuAyw6R5HvKSPzztuzXNYFx3ZGoMHMuAsXL6u3xLGQriRXQ"
|
||||
exampleDID = "did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh"
|
||||
exampleGreeting = "world"
|
||||
examplePrivKeyCfg = "CAESQP9v2uqECTuIi45dyg3znQvsryvf2IXmOF/6aws6aCehm0FVrj0zHR5RZSDxWNjcpcJqsGym3sjCungX9Zt5oA4="
|
||||
@@ -8,8 +8,7 @@
|
||||
//
|
||||
// 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.
|
||||
// described by requirement two below.
|
||||
//
|
||||
// Types that wish to be marshaled and unmarshaled from the using
|
||||
// is package have two requirements.
|
||||
@@ -30,7 +29,6 @@ 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"
|
||||
@@ -41,8 +39,9 @@ import (
|
||||
"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"
|
||||
"github.com/ucan-wg/go-ucan/tokens/internal/varsig"
|
||||
)
|
||||
|
||||
const varsigHeaderKey = "h"
|
||||
@@ -66,20 +65,20 @@ type Tokener interface {
|
||||
//
|
||||
// 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) {
|
||||
func Decode[T Tokener](b []byte, decFn codec.Decoder) (T, error) {
|
||||
node, err := ipld.Decode(b, decFn)
|
||||
if err != nil {
|
||||
return *new(T), cid.Undef, err
|
||||
return *new(T), 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) {
|
||||
func DecodeReader[T Tokener](r io.Reader, decFn codec.Decoder) (T, error) {
|
||||
node, err := ipld.DecodeStreaming(r, decFn)
|
||||
if err != nil {
|
||||
return *new(T), cid.Undef, err
|
||||
return *new(T), err
|
||||
}
|
||||
|
||||
return FromIPLD[T](node)
|
||||
@@ -89,12 +88,24 @@ func DecodeReader[T Tokener](r io.Reader, decFn codec.Decoder) (T, cid.Cid, erro
|
||||
//
|
||||
// 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)
|
||||
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
|
||||
}
|
||||
|
||||
// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.
|
||||
func FromDagCborReader[T Tokener](r io.Reader) (T, cid.Cid, error) {
|
||||
func FromDagCborReader[T Tokener](r io.Reader) (T, error) {
|
||||
return DecodeReader[T](r, dagcbor.Decode)
|
||||
}
|
||||
|
||||
@@ -102,12 +113,12 @@ func FromDagCborReader[T Tokener](r io.Reader) (T, cid.Cid, error) {
|
||||
//
|
||||
// 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) {
|
||||
func FromDagJson[T Tokener](b []byte) (T, 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) {
|
||||
func FromDagJsonReader[T Tokener](r io.Reader) (T, error) {
|
||||
return DecodeReader[T](r, dagjson.Decode)
|
||||
}
|
||||
|
||||
@@ -115,111 +126,117 @@ func FromDagJsonReader[T Tokener](r io.Reader) (T, cid.Cid, error) {
|
||||
//
|
||||
// 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) {
|
||||
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) {
|
||||
undef := *new(T)
|
||||
|
||||
signatureNode, err := node.LookupByIndex(0)
|
||||
if err != nil {
|
||||
return undef, cid.Undef, err
|
||||
return undef, err
|
||||
}
|
||||
|
||||
signature, err := signatureNode.AsBytes()
|
||||
if err != nil {
|
||||
return undef, cid.Undef, err
|
||||
return undef, err
|
||||
}
|
||||
|
||||
sigPayloadNode, err := node.LookupByIndex(1)
|
||||
if err != nil {
|
||||
return undef, cid.Undef, err
|
||||
return undef, err
|
||||
}
|
||||
|
||||
varsigHeaderNode, err := sigPayloadNode.LookupByString(varsigHeaderKey)
|
||||
if err != nil {
|
||||
return undef, cid.Undef, err
|
||||
return undef, err
|
||||
}
|
||||
|
||||
tokenPayloadNode, err := sigPayloadNode.LookupByString(undef.Tag())
|
||||
if err != nil {
|
||||
return undef, cid.Undef, err
|
||||
return undef, err
|
||||
}
|
||||
|
||||
// This needs to be done before converting this node to it's schema
|
||||
// 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")
|
||||
if err != nil {
|
||||
return undef, cid.Undef, err
|
||||
return 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
|
||||
return undef, err
|
||||
}
|
||||
|
||||
tokenPayloadNode = nb.Build()
|
||||
// ^^^
|
||||
|
||||
tokenPayload := bindnode.Unwrap(tokenPayloadNode)
|
||||
if tokenPayload == nil {
|
||||
return undef, cid.Undef, errors.New("failed to Unwrap the TokenPayload")
|
||||
return 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")
|
||||
return 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
|
||||
return undef, err
|
||||
}
|
||||
|
||||
issuerDID, err := did.Parse(issuer)
|
||||
if err != nil {
|
||||
return undef, cid.Undef, err
|
||||
return undef, err
|
||||
}
|
||||
|
||||
issuerPubKey, err := issuerDID.PubKey()
|
||||
if err != nil {
|
||||
return undef, cid.Undef, err
|
||||
return undef, err
|
||||
}
|
||||
|
||||
issuerVarsigHeader, err := varsig.Encode(issuerPubKey.Type())
|
||||
if err != nil {
|
||||
return undef, cid.Undef, err
|
||||
return undef, err
|
||||
}
|
||||
|
||||
varsigHeader, err := varsigHeaderNode.AsBytes()
|
||||
if err != nil {
|
||||
return undef, cid.Undef, err
|
||||
return 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")
|
||||
return 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
|
||||
return 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 undef, errors.New("failed to verify the token's signature")
|
||||
}
|
||||
// ^^^
|
||||
|
||||
return tkn, cid.Undef, nil
|
||||
return tkn, nil
|
||||
}
|
||||
|
||||
// Encode marshals a Tokener to the format specified by the provided
|
||||
@@ -2,11 +2,12 @@ package envelope_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/ucan-wg/go-ucan/internal/envelope"
|
||||
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
|
||||
@@ -18,7 +19,7 @@ func TestDecode(t *testing.T) {
|
||||
|
||||
data := golden.Get(t, "example.dagcbor")
|
||||
|
||||
tkn, _, err := envelope.FromDagCbor[*Example](data)
|
||||
tkn, err := envelope.FromDagCbor[*Example](data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, exampleGreeting, tkn.Hello)
|
||||
assert.Equal(t, exampleDID, tkn.Issuer)
|
||||
@@ -29,7 +30,7 @@ func TestDecode(t *testing.T) {
|
||||
|
||||
data := golden.Get(t, "example.dagjson")
|
||||
|
||||
tkn, _, err := envelope.FromDagJson[*Example](data)
|
||||
tkn, err := envelope.FromDagJson[*Example](data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, exampleGreeting, tkn.Hello)
|
||||
assert.Equal(t, exampleDID, tkn.Issuer)
|
||||
@@ -59,12 +60,27 @@ func TestEncode(t *testing.T) {
|
||||
func TestRoundtrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
t.Run("via FromDagCborReader/ToDagCborWriter", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := golden.Get(t, exampleDAGCBORFilename)
|
||||
|
||||
tkn, _, err := envelope.FromDagCborReader[*Example](bytes.NewReader(data))
|
||||
tkn, err := envelope.FromDagCborReader[*Example](bytes.NewReader(data))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, exampleGreeting, tkn.Hello)
|
||||
assert.Equal(t, exampleDID, tkn.Issuer)
|
||||
@@ -74,27 +90,62 @@ func TestRoundtrip(t *testing.T) {
|
||||
assert.Equal(t, data, w.Bytes())
|
||||
})
|
||||
|
||||
t.Run("via FromDagCbor/ToDagCbor", func(t *testing.T) {
|
||||
t.Run("via FromDagJson/ToDagJson", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dataIn := golden.Get(t, exampleDAGCBORFilename)
|
||||
dataIn := golden.Get(t, exampleDAGJSONFilename)
|
||||
|
||||
tkn, _, err := envelope.FromDagCbor[*Example](dataIn)
|
||||
tkn, err := envelope.FromDagJson[*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))
|
||||
dataOut, err := envelope.ToDagJson(examplePrivKey(t), newExample(t))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, dataIn, dataOut)
|
||||
})
|
||||
|
||||
t.Run("via FromDagJsonReader/ToDagJsonrWriter", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := golden.Get(t, exampleDAGJSONFilename)
|
||||
|
||||
tkn, err := envelope.FromDagJsonReader[*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.ToDagJsonWriter(w, examplePrivKey(t), newExample(t)))
|
||||
assert.Equal(t, data, w.Bytes())
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIPLD_with_invalid_signature(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
node := invalidNodeFromGolden(t)
|
||||
tkn, _, err := envelope.FromIPLD[*Example](node)
|
||||
tkn, err := envelope.FromIPLD[*Example](node)
|
||||
assert.Nil(t, tkn)
|
||||
require.EqualError(t, err, "failed to verify the token's signature")
|
||||
}
|
||||
|
||||
func TestHash(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
msg := []byte("this is a test")
|
||||
|
||||
hash1 := sha256.Sum256(msg)
|
||||
|
||||
hasher := sha256.New()
|
||||
|
||||
for _, b := range msg {
|
||||
hasher.Write([]byte{b})
|
||||
}
|
||||
|
||||
hash2 := hasher.Sum(nil)
|
||||
hash3 := hasher.Sum(nil)
|
||||
|
||||
require.Equal(t, hash1[:], hash2)
|
||||
require.Equal(t, hash1[:], hash3)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/libp2p/go-libp2p/core/crypto/pb"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/internal/varsig"
|
||||
"github.com/ucan-wg/go-ucan/tokens/internal/varsig"
|
||||
)
|
||||
|
||||
func TestDecode(t *testing.T) {
|
||||
Reference in New Issue
Block a user