diff --git a/delegation/schema_test.go b/delegation/schema_test.go deleted file mode 100644 index 0f534df..0000000 --- a/delegation/schema_test.go +++ /dev/null @@ -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) - } -} diff --git a/go.mod b/go.mod index 30dee27..02f05fc 100644 --- a/go.mod +++ b/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 diff --git a/capability/command/command.go b/pkg/command/command.go similarity index 100% rename from capability/command/command.go rename to pkg/command/command.go diff --git a/capability/command/command_errors.go b/pkg/command/command_errors.go similarity index 100% rename from capability/command/command_errors.go rename to pkg/command/command_errors.go diff --git a/capability/command/command_test.go b/pkg/command/command_test.go similarity index 98% rename from capability/command/command_test.go rename to pkg/command/command_test.go index bf878b7..fc896df 100644 --- a/capability/command/command_test.go +++ b/pkg/command/command_test.go @@ -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) { diff --git a/pkg/meta/meta.go b/pkg/meta/meta.go index 176c5fe..9dd8dbb 100644 --- a/pkg/meta/meta.go +++ b/pkg/meta/meta.go @@ -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() +} diff --git a/pkg/meta/meta_test.go b/pkg/meta/meta_test.go new file mode 100644 index 0000000..7b1d994 --- /dev/null +++ b/pkg/meta/meta_test.go @@ -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") + }) +} diff --git a/capability/policy/glob.go b/pkg/policy/glob.go similarity index 100% rename from capability/policy/glob.go rename to pkg/policy/glob.go diff --git a/capability/policy/glob_test.go b/pkg/policy/glob_test.go similarity index 100% rename from capability/policy/glob_test.go rename to pkg/policy/glob_test.go diff --git a/capability/policy/ipld.go b/pkg/policy/ipld.go similarity index 99% rename from capability/policy/ipld.go rename to pkg/policy/ipld.go index 2aea8d8..e3c67d1 100644 --- a/capability/policy/ipld.go +++ b/pkg/policy/ipld.go @@ -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) { diff --git a/capability/policy/ipld_errors.go b/pkg/policy/ipld_errors.go similarity index 100% rename from capability/policy/ipld_errors.go rename to pkg/policy/ipld_errors.go diff --git a/capability/policy/ipld_test.go b/pkg/policy/ipld_test.go similarity index 100% rename from capability/policy/ipld_test.go rename to pkg/policy/ipld_test.go diff --git a/capability/policy/literal/literal.go b/pkg/policy/literal/literal.go similarity index 100% rename from capability/policy/literal/literal.go rename to pkg/policy/literal/literal.go diff --git a/capability/policy/match.go b/pkg/policy/match.go similarity index 98% rename from capability/policy/match.go rename to pkg/policy/match.go index 6da07ca..31ebb37 100644 --- a/capability/policy/match.go +++ b/pkg/policy/match.go @@ -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 { diff --git a/capability/policy/match_test.go b/pkg/policy/match_test.go similarity index 99% rename from capability/policy/match_test.go rename to pkg/policy/match_test.go index 7ea0bcf..33407ef 100644 --- a/capability/policy/match_test.go +++ b/pkg/policy/match_test.go @@ -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) { diff --git a/capability/policy/policy.go b/pkg/policy/policy.go similarity index 98% rename from capability/policy/policy.go rename to pkg/policy/policy.go index 7c6dc7d..8090d64 100644 --- a/capability/policy/policy.go +++ b/pkg/policy/policy.go @@ -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 ( diff --git a/capability/policy/selector/parsing.go b/pkg/policy/selector/parsing.go similarity index 100% rename from capability/policy/selector/parsing.go rename to pkg/policy/selector/parsing.go diff --git a/capability/policy/selector/selector.go b/pkg/policy/selector/selector.go similarity index 100% rename from capability/policy/selector/selector.go rename to pkg/policy/selector/selector.go diff --git a/capability/policy/selector/selector_test.go b/pkg/policy/selector/selector_test.go similarity index 100% rename from capability/policy/selector/selector_test.go rename to pkg/policy/selector/selector_test.go diff --git a/capability/policy/selector/supported_test.go b/pkg/policy/selector/supported_test.go similarity index 98% rename from capability/policy/selector/supported_test.go rename to pkg/policy/selector/supported_test.go index de8f83c..a7e9917 100644 --- a/capability/policy/selector/supported_test.go +++ b/pkg/policy/selector/supported_test.go @@ -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 diff --git a/delegation/delegation.go b/tokens/delegation/delegation.go similarity index 71% rename from delegation/delegation.go rename to tokens/delegation/delegation.go index b89560d..b950bb9 100644 --- a/delegation/delegation.go +++ b/tokens/delegation/delegation.go @@ -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) diff --git a/delegation/delegation.ipldsch b/tokens/delegation/delegation.ipldsch similarity index 100% rename from delegation/delegation.ipldsch rename to tokens/delegation/delegation.ipldsch diff --git a/delegation/delegation_test.go b/tokens/delegation/delegation_test.go similarity index 75% rename from delegation/delegation_test.go rename to tokens/delegation/delegation_test.go index 27e8954..0e307bd 100644 --- a/delegation/delegation_test.go +++ b/tokens/delegation/delegation_test.go @@ -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() -} diff --git a/delegation/ipld.go b/tokens/delegation/ipld.go similarity index 61% rename from delegation/ipld.go rename to tokens/delegation/ipld.go index a900238..5a76b10 100644 --- a/delegation/ipld.go +++ b/tokens/delegation/ipld.go @@ -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) -} diff --git a/tokens/delegation/options.go b/tokens/delegation/options.go new file mode 100644 index 0000000..83ffa03 --- /dev/null +++ b/tokens/delegation/options.go @@ -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 + } +} diff --git a/delegation/schema.go b/tokens/delegation/schema.go similarity index 82% rename from delegation/schema.go rename to tokens/delegation/schema.go index 33b8f46..fe702a6 100644 --- a/delegation/schema.go +++ b/tokens/delegation/schema.go @@ -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 diff --git a/tokens/delegation/schema_test.go b/tokens/delegation/schema_test.go new file mode 100644 index 0000000..fbfc919 --- /dev/null +++ b/tokens/delegation/schema_test.go @@ -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) + } + }) + }) +} diff --git a/delegation/testdata/new.dagjson b/tokens/delegation/testdata/new.dagjson similarity index 100% rename from delegation/testdata/new.dagjson rename to tokens/delegation/testdata/new.dagjson diff --git a/delegation/testdata/root.dagjson b/tokens/delegation/testdata/root.dagjson similarity index 100% rename from delegation/testdata/root.dagjson rename to tokens/delegation/testdata/root.dagjson diff --git a/tokens/internal/envelope/cid.go b/tokens/internal/envelope/cid.go new file mode 100644 index 0000000..410acbb --- /dev/null +++ b/tokens/internal/envelope/cid.go @@ -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 +} diff --git a/tokens/internal/envelope/cid_test.go b/tokens/internal/envelope/cid_test.go new file mode 100644 index 0000000..fd4be1b --- /dev/null +++ b/tokens/internal/envelope/cid_test.go @@ -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) + }) +} diff --git a/internal/envelope/example_test.go b/tokens/internal/envelope/example_test.go similarity index 95% rename from internal/envelope/example_test.go rename to tokens/internal/envelope/example_test.go index d5a7ba8..4db1a4e 100644 --- a/internal/envelope/example_test.go +++ b/tokens/internal/envelope/example_test.go @@ -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=" diff --git a/internal/envelope/ipld.go b/tokens/internal/envelope/ipld.go similarity index 82% rename from internal/envelope/ipld.go rename to tokens/internal/envelope/ipld.go index d12b1be..aef4afc 100644 --- a/internal/envelope/ipld.go +++ b/tokens/internal/envelope/ipld.go @@ -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 diff --git a/internal/envelope/ipld_test.go b/tokens/internal/envelope/ipld_test.go similarity index 58% rename from internal/envelope/ipld_test.go rename to tokens/internal/envelope/ipld_test.go index 132106d..7c8dcc2 100644 --- a/internal/envelope/ipld_test.go +++ b/tokens/internal/envelope/ipld_test.go @@ -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) +} diff --git a/internal/envelope/testdata/example.dagcbor b/tokens/internal/envelope/testdata/example.dagcbor similarity index 100% rename from internal/envelope/testdata/example.dagcbor rename to tokens/internal/envelope/testdata/example.dagcbor diff --git a/internal/envelope/testdata/example.dagjson b/tokens/internal/envelope/testdata/example.dagjson similarity index 100% rename from internal/envelope/testdata/example.dagjson rename to tokens/internal/envelope/testdata/example.dagjson diff --git a/internal/envelope/testdata/example.ipldsch b/tokens/internal/envelope/testdata/example.ipldsch similarity index 100% rename from internal/envelope/testdata/example.ipldsch rename to tokens/internal/envelope/testdata/example.ipldsch diff --git a/internal/varsig/varsig.go b/tokens/internal/varsig/varsig.go similarity index 100% rename from internal/varsig/varsig.go rename to tokens/internal/varsig/varsig.go diff --git a/internal/varsig/varsig_test.go b/tokens/internal/varsig/varsig_test.go similarity index 94% rename from internal/varsig/varsig_test.go rename to tokens/internal/varsig/varsig_test.go index 56c8bb5..a1ab1cd 100644 --- a/internal/varsig/varsig_test.go +++ b/tokens/internal/varsig/varsig_test.go @@ -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) {