diff --git a/token/bearer.go b/token/bearer.go new file mode 100644 index 0000000..3cec8b4 --- /dev/null +++ b/token/bearer.go @@ -0,0 +1,188 @@ +package token + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "errors" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neofs-api-go/v2/acl" + "github.com/nspcc-dev/neofs-api-go/v2/refs" + v2signature "github.com/nspcc-dev/neofs-api-go/v2/signature" + "github.com/nspcc-dev/neofs-sdk-go/eacl" + "github.com/nspcc-dev/neofs-sdk-go/owner" + "github.com/nspcc-dev/neofs-sdk-go/util/signature" +) + +var ( + errNilBearerToken = errors.New("bearer token is not set") + errNilBearerTokenBody = errors.New("bearer token body is not set") + errNilBearerTokenEACL = errors.New("bearer token EACL table is not set") +) + +type BearerToken struct { + token acl.BearerToken +} + +// ToV2 converts BearerToken to v2 BearerToken message. +// +// Nil BearerToken converts to nil. +func (b *BearerToken) ToV2() *acl.BearerToken { + if b == nil { + return nil + } + + return &b.token +} + +func (b *BearerToken) SetLifetime(exp, nbf, iat uint64) { + body := b.token.GetBody() + if body == nil { + body = new(acl.BearerTokenBody) + } + + lt := new(acl.TokenLifetime) + lt.SetExp(exp) + lt.SetNbf(nbf) + lt.SetIat(iat) + + body.SetLifetime(lt) + b.token.SetBody(body) +} + +func (b *BearerToken) SetEACLTable(table *eacl.Table) { + body := b.token.GetBody() + if body == nil { + body = new(acl.BearerTokenBody) + } + + body.SetEACL(table.ToV2()) + b.token.SetBody(body) +} + +func (b *BearerToken) SetOwner(id *owner.ID) { + body := b.token.GetBody() + if body == nil { + body = new(acl.BearerTokenBody) + } + + body.SetOwnerID(id.ToV2()) + b.token.SetBody(body) +} + +func (b *BearerToken) SignToken(key *ecdsa.PrivateKey) error { + err := sanityCheck(b) + if err != nil { + return err + } + + signWrapper := v2signature.StableMarshalerWrapper{SM: b.token.GetBody()} + + return signature.SignDataWithHandler(key, signWrapper, func(key []byte, sig []byte) { + bearerSignature := new(refs.Signature) + bearerSignature.SetKey(key) + bearerSignature.SetSign(sig) + b.token.SetSignature(bearerSignature) + }) +} + +// Issuer returns owner.ID associated with the key that signed bearer token. +// To pass node validation it should be owner of requested container. Returns +// nil if token is not signed. +func (b *BearerToken) Issuer() *owner.ID { + pub, _ := keys.NewPublicKeyFromBytes(b.token.GetSignature().GetKey(), elliptic.P256()) + wallet, err := owner.NEO3WalletFromPublicKey((*ecdsa.PublicKey)(pub)) + if err != nil { + return nil + } + + return owner.NewIDFromNeo3Wallet(wallet) +} + +// NewBearerToken creates and initializes blank BearerToken. +// +// Defaults: +// - signature: nil; +// - eacl: nil; +// - ownerID: nil; +// - exp: 0; +// - nbf: 0; +// - iat: 0. +func NewBearerToken() *BearerToken { + b := new(BearerToken) + b.token = acl.BearerToken{} + b.token.SetBody(new(acl.BearerTokenBody)) + + return b +} + +// ToV2 converts BearerToken to v2 BearerToken message. +func NewBearerTokenFromV2(v2 *acl.BearerToken) *BearerToken { + if v2 == nil { + v2 = new(acl.BearerToken) + } + + return &BearerToken{ + token: *v2, + } +} + +// sanityCheck if bearer token is ready to be issued. +func sanityCheck(b *BearerToken) error { + switch { + case b == nil: + return errNilBearerToken + case b.token.GetBody() == nil: + return errNilBearerTokenBody + case b.token.GetBody().GetEACL() == nil: + return errNilBearerTokenEACL + } + + // consider checking EACL sanity there, lifetime correctness, etc. + + return nil +} + +// Marshal marshals BearerToken into a protobuf binary form. +// +// Buffer is allocated when the argument is empty. +// Otherwise, the first buffer is used. +func (b *BearerToken) Marshal(bs ...[]byte) ([]byte, error) { + var buf []byte + if len(bs) > 0 { + buf = bs[0] + } + + return b.ToV2(). + StableMarshal(buf) +} + +// Unmarshal unmarshals protobuf binary representation of BearerToken. +func (b *BearerToken) Unmarshal(data []byte) error { + fV2 := new(acl.BearerToken) + if err := fV2.Unmarshal(data); err != nil { + return err + } + + *b = *NewBearerTokenFromV2(fV2) + + return nil +} + +// MarshalJSON encodes BearerToken to protobuf JSON format. +func (b *BearerToken) MarshalJSON() ([]byte, error) { + return b.ToV2(). + MarshalJSON() +} + +// UnmarshalJSON decodes BearerToken from protobuf JSON format. +func (b *BearerToken) UnmarshalJSON(data []byte) error { + fV2 := new(acl.BearerToken) + if err := fV2.UnmarshalJSON(data); err != nil { + return err + } + + *b = *NewBearerTokenFromV2(fV2) + + return nil +} diff --git a/token/bearer_test.go b/token/bearer_test.go new file mode 100644 index 0000000..1cd4b1e --- /dev/null +++ b/token/bearer_test.go @@ -0,0 +1,84 @@ +package token_test + +import ( + "crypto/ecdsa" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neofs-sdk-go/eacl" + "github.com/nspcc-dev/neofs-sdk-go/owner" + "github.com/nspcc-dev/neofs-sdk-go/token" + tokentest "github.com/nspcc-dev/neofs-sdk-go/token/test" + "github.com/stretchr/testify/require" +) + +func TestBearerToken_Issuer(t *testing.T) { + bearerToken := token.NewBearerToken() + + t.Run("non signed token", func(t *testing.T) { + require.Nil(t, bearerToken.Issuer()) + }) + + t.Run("signed token", func(t *testing.T) { + p, err := keys.NewPrivateKey() + require.NoError(t, err) + + wallet, err := owner.NEO3WalletFromPublicKey((*ecdsa.PublicKey)(p.PublicKey())) + require.NoError(t, err) + + ownerID := owner.NewIDFromNeo3Wallet(wallet) + + bearerToken.SetEACLTable(eacl.NewTable()) + require.NoError(t, bearerToken.SignToken(&p.PrivateKey)) + require.True(t, ownerID.Equal(bearerToken.Issuer())) + }) +} + +func TestFilterEncoding(t *testing.T) { + f := tokentest.Generate() + + t.Run("binary", func(t *testing.T) { + data, err := f.Marshal() + require.NoError(t, err) + + f2 := token.NewBearerToken() + require.NoError(t, f2.Unmarshal(data)) + + require.Equal(t, f, f2) + }) + + t.Run("json", func(t *testing.T) { + data, err := f.MarshalJSON() + require.NoError(t, err) + + d2 := token.NewBearerToken() + require.NoError(t, d2.UnmarshalJSON(data)) + + require.Equal(t, f, d2) + }) +} + +func TestBearerToken_ToV2(t *testing.T) { + t.Run("nil", func(t *testing.T) { + var x *token.BearerToken + + require.Nil(t, x.ToV2()) + }) +} + +func TestNewBearerToken(t *testing.T) { + t.Run("default values", func(t *testing.T) { + tkn := token.NewBearerToken() + + // convert to v2 message + tknV2 := tkn.ToV2() + + require.NotNil(t, tknV2.GetBody()) + require.Zero(t, tknV2.GetBody().GetLifetime().GetExp()) + require.Zero(t, tknV2.GetBody().GetLifetime().GetNbf()) + require.Zero(t, tknV2.GetBody().GetLifetime().GetIat()) + require.Nil(t, tknV2.GetBody().GetEACL()) + require.Nil(t, tknV2.GetBody().GetOwnerID()) + require.Nil(t, tknV2.GetSignature()) + }) +} diff --git a/token/test/generate.go b/token/test/generate.go new file mode 100644 index 0000000..899dba3 --- /dev/null +++ b/token/test/generate.go @@ -0,0 +1,40 @@ +package tokentest + +import ( + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + eacltest "github.com/nspcc-dev/neofs-sdk-go/eacl/test" + ownertest "github.com/nspcc-dev/neofs-sdk-go/owner/test" + "github.com/nspcc-dev/neofs-sdk-go/token" +) + +// Generate returns random token.BearerToken. +// +// Resulting token is unsigned. +func Generate() *token.BearerToken { + x := token.NewBearerToken() + + x.SetLifetime(3, 2, 1) + x.SetOwner(ownertest.GenerateID()) + x.SetEACLTable(eacltest.Table()) + + return x +} + +// GenerateSigned returns signed random token.BearerToken. +// +// Panics if token could not be signed (actually unexpected). +func GenerateSigned() *token.BearerToken { + tok := Generate() + + p, err := keys.NewPrivateKey() + if err != nil { + panic(err) + } + + err = tok.SignToken(&p.PrivateKey) + if err != nil { + panic(err) + } + + return tok +}