Merge branch 'v1' into v1-policy-subset-selection

This commit is contained in:
Fabio Bozzo
2024-09-24 19:41:39 +02:00
39 changed files with 812 additions and 304 deletions

View File

@@ -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
View File

@@ -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

View File

@@ -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) {

View File

@@ -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
View 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")
})
}

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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 (

View File

@@ -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

View File

@@ -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)

View File

@@ -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()
}

View File

@@ -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)
}

View 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
}
}

View File

@@ -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

View 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)
}
})
})
}

View 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
}

View 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)
})
}

View File

@@ -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="

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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) {