diff --git a/Makefile b/Makefile index 62a92ec..b99682b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PROTO_VERSION=v0.7.3 +PROTO_VERSION=v0.7.4 PROTO_URL=https://github.com/nspcc-dev/neofs-api/archive/$(PROTO_VERSION).tar.gz B=\033[0;1m diff --git a/docs/service.md b/docs/service.md index eef1e49..9ed548e 100644 --- a/docs/service.md +++ b/docs/service.md @@ -17,6 +17,7 @@ - [RequestVerificationHeader.Signature](#service.RequestVerificationHeader.Signature) - [Token](#service.Token) - [Token.Info](#service.Token.Info) + - [TokenLifetime](#service.TokenLifetime) - [service/verify_test.proto](#service/verify_test.proto) @@ -129,10 +130,21 @@ User token granting rights for object manipulation | OwnerID | [bytes](#bytes) | | OwnerID is an owner of manipulation object | | verb | [Token.Info.Verb](#service.Token.Info.Verb) | | Verb is a type of request for which the token is issued | | Address | [refs.Address](#refs.Address) | | Address is an object address for which token is issued | -| Created | [uint64](#uint64) | | Created is an initial epoch of token lifetime | -| ValidUntil | [uint64](#uint64) | | ValidUntil is a last epoch of token lifetime | +| Lifetime | [TokenLifetime](#service.TokenLifetime) | | Lifetime is a lifetime of the session | | SessionKey | [bytes](#bytes) | | SessionKey is a public key of session key | + + + +### Message TokenLifetime +TokenLifetime carries a group of lifetime parameters of the token + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| Created | [uint64](#uint64) | | Created carries an initial epoch of token lifetime | +| ValidUntil | [uint64](#uint64) | | ValidUntil carries a last epoch of token lifetime | + diff --git a/docs/session.md b/docs/session.md index 4a537e6..5ec7402 100644 --- a/docs/session.md +++ b/docs/session.md @@ -30,22 +30,13 @@ ``` -rpc Create(stream CreateRequest) returns (stream CreateResponse); +rpc Create(CreateRequest) returns (CreateResponse); ``` #### Method Create -Create is a method that used to open a trusted session to manipulate -an object. In order to put or delete object client have to obtain session -token with trusted node. Trusted node will modify client's object -(add missing headers, checksums, homomorphic hash) and sign id with -session key. Session is established during 4-step handshake in one gRPC stream - -- First client stream message SHOULD BE type of `CreateRequest_Init`. -- First server stream message SHOULD BE type of `CreateResponse_Unsigned`. -- Second client stream message SHOULD BE type of `CreateRequest_Signed`. -- Second server stream message SHOULD BE type of `CreateResponse_Result`. +Create opens new session between the client and the server | Name | Input | Output | | ---- | ----- | ------ | @@ -56,13 +47,13 @@ session key. Session is established during 4-step handshake in one gRPC stream ### Message CreateRequest - +CreateRequest carries an information necessary for opening a session | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | -| Init | [service.Token](#service.Token) | | Init is a message to initialize session opening. Carry: owner of manipulation object; ID of manipulation object; token lifetime bounds. | -| Signed | [service.Token](#service.Token) | | Signed Init message response (Unsigned) from server with user private key | +| OwnerID | [bytes](#bytes) | | OwnerID carries an identifier of a session initiator | +| Lifetime | [service.TokenLifetime](#service.TokenLifetime) | | Lifetime carries a lifetime of the session | | Meta | [service.RequestMetaHeader](#service.RequestMetaHeader) | | RequestMetaHeader contains information about request meta headers (should be embedded into message) | | Verify | [service.RequestVerificationHeader](#service.RequestVerificationHeader) | | RequestVerificationHeader is a set of signatures of every NeoFS Node that processed request (should be embedded into message) | @@ -70,13 +61,13 @@ session key. Session is established during 4-step handshake in one gRPC stream ### Message CreateResponse - +CreateResponse carries an information about the opened session | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | -| Unsigned | [service.Token](#service.Token) | | Unsigned token with token ID and session public key generated on server side | -| Result | [service.Token](#service.Token) | | Result is a resulting token which can be used for object placing through an trusted intermediary | +| ID | [bytes](#bytes) | | ID carries an identifier of session token | +| SessionKey | [bytes](#bytes) | | SessionKey carries a session public key | diff --git a/refs/types.go b/refs/types.go index a29424e..417eec3 100644 --- a/refs/types.go +++ b/refs/types.go @@ -37,9 +37,14 @@ type ( OwnerID chain.WalletAddress ) +// OwnerIDSource is an interface of the container of an OwnerID value with read access. +type OwnerIDSource interface { + GetOwnerID() OwnerID +} + // OwnerIDContainer is an interface of the container of an OwnerID value. type OwnerIDContainer interface { - GetOwnerID() OwnerID + OwnerIDSource SetOwnerID(OwnerID) } diff --git a/service/token.go b/service/token.go index f431427..78fccfa 100644 --- a/service/token.go +++ b/service/token.go @@ -75,22 +75,22 @@ func (m *Token_Info) SetAddress(addr Address) { } // CreationEpoch is a Created field getter. -func (m Token_Info) CreationEpoch() uint64 { +func (m TokenLifetime) CreationEpoch() uint64 { return m.Created } // SetCreationEpoch is a Created field setter. -func (m *Token_Info) SetCreationEpoch(e uint64) { +func (m *TokenLifetime) SetCreationEpoch(e uint64) { m.Created = e } // ExpirationEpoch is a ValidUntil field getter. -func (m Token_Info) ExpirationEpoch() uint64 { +func (m TokenLifetime) ExpirationEpoch() uint64 { return m.ValidUntil } // SetExpirationEpoch is a ValidUntil field setter. -func (m *Token_Info) SetExpirationEpoch(e uint64) { +func (m *TokenLifetime) SetExpirationEpoch(e uint64) { m.ValidUntil = e } diff --git a/service/types.go b/service/types.go index c3148a0..31f4507 100644 --- a/service/types.go +++ b/service/types.go @@ -124,6 +124,18 @@ type ExpirationEpochContainer interface { SetExpirationEpoch(uint64) } +// LifetimeSource is an interface of the container of creation-expiration epoch pair with read access. +type LifetimeSource interface { + CreationEpochSource + ExpirationEpochSource +} + +// LifetimeContainer is an interface of the container of creation-expiration epoch pair. +type LifetimeContainer interface { + CreationEpochContainer + ExpirationEpochContainer +} + // SessionKeySource is an interface of the container of session key bytes with read access. type SessionKeySource interface { GetSessionKey() []byte @@ -157,16 +169,14 @@ type SessionTokenSource interface { // - ID of the token's owner; // - verb of the session; // - address of the session object; -// - creation epoch number of the token; -// - expiration epoch number of the token; +// - token lifetime; // - public session key bytes. type SessionTokenInfo interface { TokenIDContainer OwnerIDContainer VerbContainer AddressContainer - CreationEpochContainer - ExpirationEpochContainer + LifetimeContainer SessionKeyContainer } diff --git a/service/verify.pb.go b/service/verify.pb.go index 023e639..3dadf0b 100644 Binary files a/service/verify.pb.go and b/service/verify.pb.go differ diff --git a/service/verify.proto b/service/verify.proto index b25cd47..ed360be 100644 --- a/service/verify.proto +++ b/service/verify.proto @@ -58,14 +58,11 @@ message Token { // Address is an object address for which token is issued refs.Address Address = 4 [(gogoproto.nullable) = false, (gogoproto.customtype) = "Address"]; - // Created is an initial epoch of token lifetime - uint64 Created = 5; - - // ValidUntil is a last epoch of token lifetime - uint64 ValidUntil = 6; + // Lifetime is a lifetime of the session + TokenLifetime Lifetime = 5 [(gogoproto.embed) = true, (gogoproto.nullable) = false]; // SessionKey is a public key of session key - bytes SessionKey = 7; + bytes SessionKey = 6; } // TokenInfo is a grouped information about token @@ -75,6 +72,15 @@ message Token { bytes Signature = 8; } +// TokenLifetime carries a group of lifetime parameters of the token +message TokenLifetime { + // Created carries an initial epoch of token lifetime + uint64 Created = 1; + + // ValidUntil carries a last epoch of token lifetime + uint64 ValidUntil = 2; +} + // TODO: for variable token types and version redefine message // Example: // message Token { diff --git a/session/create.go b/session/create.go new file mode 100644 index 0000000..35d0540 --- /dev/null +++ b/session/create.go @@ -0,0 +1,62 @@ +package session + +import ( + "context" + "crypto/ecdsa" + + "github.com/nspcc-dev/neofs-api-go/service" + crypto "github.com/nspcc-dev/neofs-crypto" + "google.golang.org/grpc" +) + +type gRPCCreator struct { + conn *grpc.ClientConn + + key *ecdsa.PrivateKey + + clientFunc func(*grpc.ClientConn) SessionClient +} + +// NewGRPCCreator unites virtual gRPC client with private ket and returns Creator interface. +// +// If passed ClientConn is nil, ErrNilGPRCClientConn returns. +// If passed private key is nil, crypto.ErrEmptyPrivateKey returns. +func NewGRPCCreator(conn *grpc.ClientConn, key *ecdsa.PrivateKey) (Creator, error) { + if conn == nil { + return nil, ErrNilGPRCClientConn + } else if key == nil { + return nil, crypto.ErrEmptyPrivateKey + } + + return &gRPCCreator{ + conn: conn, + + key: key, + + clientFunc: NewSessionClient, + }, nil +} + +// Create constructs message, signs it with private key and sends it to a gRPC client. +// +// If passed CreateParamsSource is nil, ErrNilCreateParamsSource returns. +// If message could not be signed, an error returns. +func (s gRPCCreator) Create(ctx context.Context, p CreateParamsSource) (CreateResult, error) { + if p == nil { + return nil, ErrNilCreateParamsSource + } + + // create and fill a message + req := new(CreateRequest) + req.SetOwnerID(p.GetOwnerID()) + req.SetCreationEpoch(p.CreationEpoch()) + req.SetExpirationEpoch(p.ExpirationEpoch()) + + // sign with private key + if err := service.SignDataWithSessionToken(s.key, req); err != nil { + return nil, err + } + + // make gRPC call + return s.clientFunc(s.conn).Create(ctx, req) +} diff --git a/session/create_test.go b/session/create_test.go new file mode 100644 index 0000000..732d4fd --- /dev/null +++ b/session/create_test.go @@ -0,0 +1,103 @@ +package session + +import ( + "context" + "crypto/ecdsa" + "testing" + + "github.com/nspcc-dev/neofs-api-go/service" + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/nspcc-dev/neofs-crypto/test" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" +) + +type testSessionClient struct { + fn func(*CreateRequest) + resp *CreateResponse + err error +} + +func (s testSessionClient) Create(ctx context.Context, in *CreateRequest, opts ...grpc.CallOption) (*CreateResponse, error) { + if s.fn != nil { + s.fn(in) + } + + return s.resp, s.err +} + +func TestNewGRPCCreator(t *testing.T) { + var ( + err error + conn = new(grpc.ClientConn) + sk = new(ecdsa.PrivateKey) + ) + + // nil client connection + _, err = NewGRPCCreator(nil, sk) + require.EqualError(t, err, ErrNilGPRCClientConn.Error()) + + // nil private key + _, err = NewGRPCCreator(conn, nil) + require.EqualError(t, err, crypto.ErrEmptyPrivateKey.Error()) + + // valid params + res, err := NewGRPCCreator(conn, sk) + require.NoError(t, err) + + v := res.(*gRPCCreator) + require.Equal(t, conn, v.conn) + require.Equal(t, sk, v.key) + require.NotNil(t, v.clientFunc) +} + +func TestGRPCCreator_Create(t *testing.T) { + ctx := context.TODO() + s := new(gRPCCreator) + + // nil CreateParamsSource + _, err := s.Create(ctx, nil) + require.EqualError(t, err, ErrNilCreateParamsSource.Error()) + + var ( + ownerID = OwnerID{1, 2, 3} + created = uint64(2) + expired = uint64(4) + ) + + p := NewParams() + p.SetOwnerID(ownerID) + p.SetCreationEpoch(created) + p.SetExpirationEpoch(expired) + + // nil private key + _, err = s.Create(ctx, p) + require.Error(t, err) + + // create test private key + s.key = test.DecodeKey(0) + + // create test client + c := &testSessionClient{ + fn: func(req *CreateRequest) { + require.Equal(t, ownerID, req.GetOwnerID()) + require.Equal(t, created, req.CreationEpoch()) + require.Equal(t, expired, req.ExpirationEpoch()) + require.NoError(t, service.VerifyAccumulatedSignaturesWithToken(req)) + }, + resp: &CreateResponse{ + ID: TokenID{1, 2, 3}, + SessionKey: []byte{1, 2, 3}, + }, + err: errors.New("test error"), + } + + s.clientFunc = func(*grpc.ClientConn) SessionClient { + return c + } + + res, err := s.Create(ctx, p) + require.EqualError(t, err, c.err.Error()) + require.Equal(t, c.resp, res) +} diff --git a/session/errors.go b/session/errors.go new file mode 100644 index 0000000..3a9c129 --- /dev/null +++ b/session/errors.go @@ -0,0 +1,15 @@ +package session + +import "github.com/nspcc-dev/neofs-api-go/internal" + +// ErrNilCreateParamsSource is returned by functions that expect a non-nil +// CreateParamsSource, but received nil. +const ErrNilCreateParamsSource = internal.Error("create params source is nil") + +// ErrNilGPRCClientConn is returned by functions that expect a non-nil +// grpc.ClientConn, but received nil. +const ErrNilGPRCClientConn = internal.Error("gRPC client connection is nil") + +// ErrPrivateTokenNotFound is returned when addressed private token was +// not found in storage. +const ErrPrivateTokenNotFound = internal.Error("private token not found") diff --git a/session/request.go b/session/request.go new file mode 100644 index 0000000..0bb5176 --- /dev/null +++ b/session/request.go @@ -0,0 +1,68 @@ +package session + +import ( + "encoding/binary" + "io" + + "github.com/nspcc-dev/neofs-api-go/refs" +) + +const signedRequestDataSize = 0 + + refs.OwnerIDSize + + 8 + + 8 + +var requestEndianness = binary.BigEndian + +// NewParams creates a new CreateRequest message and returns CreateParamsContainer interface. +func NewParams() CreateParamsContainer { + return new(CreateRequest) +} + +// GetOwnerID is an OwnerID field getter. +func (m CreateRequest) GetOwnerID() OwnerID { + return m.OwnerID +} + +// SetOwnerID is an OwnerID field setter. +func (m *CreateRequest) SetOwnerID(id OwnerID) { + m.OwnerID = id +} + +// SignedData returns payload bytes of the request. +func (m CreateRequest) SignedData() ([]byte, error) { + data := make([]byte, m.SignedDataSize()) + + _, err := m.ReadSignedData(data) + if err != nil { + return nil, err + } + + return data, nil +} + +// SignedDataSize returns payload size of the request. +func (m CreateRequest) SignedDataSize() int { + return signedRequestDataSize +} + +// ReadSignedData copies payload bytes to passed buffer. +// +// If the buffer size is insufficient, io.ErrUnexpectedEOF returns. +func (m CreateRequest) ReadSignedData(p []byte) (int, error) { + sz := m.SignedDataSize() + if len(p) < sz { + return 0, io.ErrUnexpectedEOF + } + + var off int + + off += copy(p[off:], m.GetOwnerID().Bytes()) + + requestEndianness.PutUint64(p[off:], m.CreationEpoch()) + off += 8 + + requestEndianness.PutUint64(p[off:], m.ExpirationEpoch()) + + return sz, nil +} diff --git a/session/request_test.go b/session/request_test.go new file mode 100644 index 0000000..094ca66 --- /dev/null +++ b/session/request_test.go @@ -0,0 +1,92 @@ +package session + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCreateRequestGettersSetters(t *testing.T) { + t.Run("owner ID", func(t *testing.T) { + id := OwnerID{1, 2, 3} + m := new(CreateRequest) + + m.SetOwnerID(id) + + require.Equal(t, id, m.GetOwnerID()) + }) + + t.Run("lifetime", func(t *testing.T) { + e1, e2 := uint64(3), uint64(4) + m := new(CreateRequest) + + m.SetCreationEpoch(e1) + m.SetExpirationEpoch(e2) + + require.Equal(t, e1, m.CreationEpoch()) + require.Equal(t, e2, m.ExpirationEpoch()) + }) +} + +func TestCreateRequest_SignedData(t *testing.T) { + var ( + id = OwnerID{1, 2, 3} + e1 = uint64(1) + e2 = uint64(2) + ) + + // create new message + m := new(CreateRequest) + + // fill the fields + m.SetOwnerID(id) + m.SetCreationEpoch(e1) + m.SetExpirationEpoch(e2) + + // calculate initial signed data + d, err := m.SignedData() + require.NoError(t, err) + + items := []struct { + change func() + reset func() + }{ + { // OwnerID + change: func() { + id2 := id + id2[0]++ + m.SetOwnerID(id2) + }, + reset: func() { + m.SetOwnerID(id) + }, + }, + { // CreationEpoch + change: func() { + m.SetCreationEpoch(e1 + 1) + }, + reset: func() { + m.SetCreationEpoch(e1) + }, + }, + { // ExpirationEpoch + change: func() { + m.SetExpirationEpoch(e2 + 1) + }, + reset: func() { + m.SetExpirationEpoch(e2) + }, + }, + } + + for _, item := range items { + item.change() + + d2, err := m.SignedData() + require.NoError(t, err) + + require.NotEqual(t, d, d2) + + item.reset() + } +} diff --git a/session/response.go b/session/response.go new file mode 100644 index 0000000..3426d7c --- /dev/null +++ b/session/response.go @@ -0,0 +1,16 @@ +package session + +// GetID is an ID field getter. +func (m CreateResponse) GetID() TokenID { + return m.ID +} + +// SetID is an ID field setter. +func (m *CreateResponse) SetID(id TokenID) { + m.ID = id +} + +// SetSessionKey is a SessionKey field setter. +func (m *CreateResponse) SetSessionKey(key []byte) { + m.SessionKey = key +} diff --git a/session/response_test.go b/session/response_test.go new file mode 100644 index 0000000..0e1de0b --- /dev/null +++ b/session/response_test.go @@ -0,0 +1,27 @@ +package session + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCreateResponseGettersSetters(t *testing.T) { + t.Run("id", func(t *testing.T) { + id := TokenID{1, 2, 3} + m := new(CreateResponse) + + m.SetID(id) + + require.Equal(t, id, m.GetID()) + }) + + t.Run("session key", func(t *testing.T) { + key := []byte{1, 2, 3} + m := new(CreateResponse) + + m.SetSessionKey(key) + + require.Equal(t, key, m.GetSessionKey()) + }) +} diff --git a/session/service.go b/session/service.go deleted file mode 100644 index 6e293d3..0000000 --- a/session/service.go +++ /dev/null @@ -1,11 +0,0 @@ -package session - -// NewInitRequest returns new initialization CreateRequest from passed Token. -func NewInitRequest(t *Token) *CreateRequest { - return &CreateRequest{Message: &CreateRequest_Init{Init: t}} -} - -// NewSignedRequest returns new signed CreateRequest from passed Token. -func NewSignedRequest(t *Token) *CreateRequest { - return &CreateRequest{Message: &CreateRequest_Signed{Signed: t}} -} diff --git a/session/service.pb.go b/session/service.pb.go index 1088308..e68c0fd 100644 Binary files a/session/service.pb.go and b/session/service.pb.go differ diff --git a/session/service.proto b/session/service.proto index 5c22fc3..b7eb0df 100644 --- a/session/service.proto +++ b/session/service.proto @@ -11,42 +11,29 @@ option (gogoproto.stable_marshaler_all) = true; service Session { - // Create is a method that used to open a trusted session to manipulate - // an object. In order to put or delete object client have to obtain session - // token with trusted node. Trusted node will modify client's object - // (add missing headers, checksums, homomorphic hash) and sign id with - // session key. Session is established during 4-step handshake in one gRPC stream - // - // - First client stream message SHOULD BE type of `CreateRequest_Init`. - // - First server stream message SHOULD BE type of `CreateResponse_Unsigned`. - // - Second client stream message SHOULD BE type of `CreateRequest_Signed`. - // - Second server stream message SHOULD BE type of `CreateResponse_Result`. - rpc Create (stream CreateRequest) returns (stream CreateResponse); + // Create opens new session between the client and the server + rpc Create (CreateRequest) returns (CreateResponse); } - +// CreateRequest carries an information necessary for opening a session message CreateRequest { - // Message should be one of - oneof Message { - // Init is a message to initialize session opening. Carry: - // owner of manipulation object; - // ID of manipulation object; - // token lifetime bounds. - service.Token Init = 1; - // Signed Init message response (Unsigned) from server with user private key - service.Token Signed = 2; - } + // OwnerID carries an identifier of a session initiator + bytes OwnerID = 1 [(gogoproto.nullable) = false, (gogoproto.customtype) = "OwnerID"]; + + // Lifetime carries a lifetime of the session + service.TokenLifetime Lifetime = 2 [(gogoproto.embed) = true, (gogoproto.nullable) = false]; + // RequestMetaHeader contains information about request meta headers (should be embedded into message) service.RequestMetaHeader Meta = 98 [(gogoproto.embed) = true, (gogoproto.nullable) = false]; // RequestVerificationHeader is a set of signatures of every NeoFS Node that processed request (should be embedded into message) service.RequestVerificationHeader Verify = 99 [(gogoproto.embed) = true, (gogoproto.nullable) = false]; } +// CreateResponse carries an information about the opened session message CreateResponse { - oneof Message { - // Unsigned token with token ID and session public key generated on server side - service.Token Unsigned = 1; - // Result is a resulting token which can be used for object placing through an trusted intermediary - service.Token Result = 2; - } + // ID carries an identifier of session token + bytes ID = 1 [(gogoproto.customtype) = "TokenID", (gogoproto.nullable) = false]; + + // SessionKey carries a session public key + bytes SessionKey = 2; } diff --git a/session/types.go b/session/types.go index c890aaf..932fe38 100644 --- a/session/types.go +++ b/session/types.go @@ -4,7 +4,8 @@ import ( "context" "crypto/ecdsa" - "github.com/nspcc-dev/neofs-api-go/internal" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" ) // PrivateToken is an interface of session private part. @@ -55,5 +56,25 @@ type KeyStore interface { Get(context.Context, OwnerID) ([]*ecdsa.PublicKey, error) } -// ErrPrivateTokenNotFound is raised when addressed private token was not found in storage. -const ErrPrivateTokenNotFound = internal.Error("private token not found") +// CreateParamsSource is an interface of the container of session parameters with read access. +type CreateParamsSource interface { + refs.OwnerIDSource + service.LifetimeSource +} + +// CreateParamsContainer is an interface of the container of session parameters. +type CreateParamsContainer interface { + refs.OwnerIDContainer + service.LifetimeContainer +} + +// CreateResult is an interface of the container of an opened session info with read access. +type CreateResult interface { + service.TokenIDSource + service.SessionKeySource +} + +// Creator is an interface of the tool for a session opening. +type Creator interface { + Create(context.Context, CreateParamsSource) (CreateResult, error) +}