diff --git a/CHANGELOG.md b/CHANGELOG.md index 14b7a11..8abbc14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,22 @@ # Changelog This is the changelog for NeoFS-API-Go +## [0.7.4] - 2020-05-12 + +### Added + +- Stringify for `object.Object`. + +### Changed + +- Mechanism for creating and verifying request message signatures. +- Implementation and interface of private token storage. +- File structure of packages. + +### Updated + +- NeoFS API v0.7.4 + ## [0.7.1] - 2020-04-20 ### Added @@ -273,3 +289,4 @@ Initial public release [0.6.2]: https://github.com/nspcc-dev/neofs-api-go/compare/v0.6.1...v0.6.2 [0.7.0]: https://github.com/nspcc-dev/neofs-api-go/compare/v0.6.2...v0.7.0 [0.7.1]: https://github.com/nspcc-dev/neofs-api-go/compare/v0.7.0...v0.7.1 +[0.7.4]: https://github.com/nspcc-dev/neofs-api-go/compare/v0.7.1...v0.7.4 diff --git a/Makefile b/Makefile index 29f41bc..b99682b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PROTO_VERSION=v0.7.1 +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/accounting/sign.go b/accounting/sign.go new file mode 100644 index 0000000..1eabed4 --- /dev/null +++ b/accounting/sign.go @@ -0,0 +1,167 @@ +package accounting + +import ( + "encoding/binary" + "io" + + "github.com/nspcc-dev/neofs-api-go/service" +) + +// SignedData returns payload bytes of the request. +func (m BalanceRequest) SignedData() ([]byte, error) { + return service.SignedDataFromReader(m) +} + +// SignedDataSize returns payload size of the request. +func (m BalanceRequest) SignedDataSize() int { + return m.GetOwnerID().Size() +} + +// ReadSignedData copies payload bytes to passed buffer. +// +// If the buffer size is insufficient, io.ErrUnexpectedEOF returns. +func (m BalanceRequest) ReadSignedData(p []byte) (int, error) { + sz := m.SignedDataSize() + if len(p) < sz { + return 0, io.ErrUnexpectedEOF + } + + copy(p, m.GetOwnerID().Bytes()) + + return sz, nil +} + +// SignedData returns payload bytes of the request. +func (m GetRequest) SignedData() ([]byte, error) { + return service.SignedDataFromReader(m) +} + +// SignedDataSize returns payload size of the request. +func (m GetRequest) SignedDataSize() int { + return m.GetID().Size() + m.GetOwnerID().Size() +} + +// ReadSignedData copies payload bytes to passed buffer. +// +// If the buffer size is insufficient, io.ErrUnexpectedEOF returns. +func (m GetRequest) ReadSignedData(p []byte) (int, error) { + sz := m.SignedDataSize() + if len(p) < sz { + return 0, io.ErrUnexpectedEOF + } + + var off int + + off += copy(p[off:], m.GetID().Bytes()) + + copy(p[off:], m.GetOwnerID().Bytes()) + + return sz, nil +} + +// SignedData returns payload bytes of the request. +func (m PutRequest) SignedData() ([]byte, error) { + return service.SignedDataFromReader(m) +} + +// SignedDataSize returns payload size of the request. +func (m PutRequest) SignedDataSize() (sz int) { + sz += m.GetOwnerID().Size() + + sz += m.GetMessageID().Size() + + sz += 8 + + if amount := m.GetAmount(); amount != nil { + sz += amount.Size() + } + + return +} + +// ReadSignedData copies payload bytes to passed buffer. +// +// If the buffer size is insufficient, io.ErrUnexpectedEOF returns. +func (m PutRequest) ReadSignedData(p []byte) (int, error) { + if len(p) < m.SignedDataSize() { + return 0, io.ErrUnexpectedEOF + } + + var off int + + off += copy(p[off:], m.GetOwnerID().Bytes()) + + off += copy(p[off:], m.GetMessageID().Bytes()) + + binary.BigEndian.PutUint64(p[off:], m.GetHeight()) + off += 8 + + if amount := m.GetAmount(); amount != nil { + n, err := amount.MarshalTo(p[off:]) + off += n + if err != nil { + return off + n, err + } + } + + return off, nil +} + +// SignedData returns payload bytes of the request. +func (m ListRequest) SignedData() ([]byte, error) { + return service.SignedDataFromReader(m) +} + +// SignedDataSize returns payload size of the request. +func (m ListRequest) SignedDataSize() int { + return m.GetOwnerID().Size() +} + +// ReadSignedData copies payload bytes to passed buffer. +// +// If the buffer size is insufficient, io.ErrUnexpectedEOF returns. +func (m ListRequest) ReadSignedData(p []byte) (int, error) { + sz := m.SignedDataSize() + if len(p) < sz { + return 0, io.ErrUnexpectedEOF + } + + copy(p, m.GetOwnerID().Bytes()) + + return sz, nil +} + +// SignedData returns payload bytes of the request. +func (m DeleteRequest) SignedData() ([]byte, error) { + return service.SignedDataFromReader(m) +} + +// SignedDataSize returns payload size of the request. +func (m DeleteRequest) SignedDataSize() (sz int) { + sz += m.GetID().Size() + + sz += m.GetOwnerID().Size() + + sz += m.GetMessageID().Size() + + return +} + +// ReadSignedData copies payload bytes to passed buffer. +// +// If the buffer size is insufficient, io.ErrUnexpectedEOF returns. +func (m DeleteRequest) ReadSignedData(p []byte) (int, error) { + if len(p) < m.SignedDataSize() { + return 0, io.ErrUnexpectedEOF + } + + var off int + + off += copy(p[off:], m.GetID().Bytes()) + + off += copy(p[off:], m.GetOwnerID().Bytes()) + + off += copy(p[off:], m.GetMessageID().Bytes()) + + return off, nil +} diff --git a/accounting/sign_test.go b/accounting/sign_test.go new file mode 100644 index 0000000..dd7a819 --- /dev/null +++ b/accounting/sign_test.go @@ -0,0 +1,185 @@ +package accounting + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/decimal" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-crypto/test" + "github.com/stretchr/testify/require" +) + +func TestSignBalanceRequest(t *testing.T) { + sk := test.DecodeKey(0) + + type sigType interface { + service.SignedDataWithToken + service.SignKeyPairAccumulator + service.SignKeyPairSource + SetToken(*service.Token) + } + + items := []struct { + constructor func() sigType + payloadCorrupt []func(sigType) + }{ + { // BalanceRequest + constructor: func() sigType { + return new(BalanceRequest) + }, + payloadCorrupt: []func(sigType){ + func(s sigType) { + req := s.(*BalanceRequest) + + owner := req.GetOwnerID() + owner[0]++ + + req.SetOwnerID(owner) + }, + }, + }, + { // GetRequest + constructor: func() sigType { + return new(GetRequest) + }, + payloadCorrupt: []func(sigType){ + func(s sigType) { + req := s.(*GetRequest) + + id, err := NewChequeID() + require.NoError(t, err) + + req.SetID(id) + }, + func(s sigType) { + req := s.(*GetRequest) + + id := req.GetOwnerID() + id[0]++ + + req.SetOwnerID(id) + }, + }, + }, + { // PutRequest + constructor: func() sigType { + req := new(PutRequest) + + amount := decimal.New(1) + req.SetAmount(amount) + + return req + }, + payloadCorrupt: []func(sigType){ + func(s sigType) { + req := s.(*PutRequest) + + owner := req.GetOwnerID() + owner[0]++ + + req.SetOwnerID(owner) + }, + func(s sigType) { + req := s.(*PutRequest) + + mid := req.GetMessageID() + mid[0]++ + + req.SetMessageID(mid) + }, + func(s sigType) { + req := s.(*PutRequest) + + req.SetHeight(req.GetHeight() + 1) + }, + func(s sigType) { + req := s.(*PutRequest) + + amount := req.GetAmount() + if amount == nil { + req.SetAmount(decimal.New(0)) + } else { + req.SetAmount(amount.Add(decimal.New(amount.GetValue()))) + } + }, + }, + }, + { // ListRequest + constructor: func() sigType { + return new(ListRequest) + }, + payloadCorrupt: []func(sigType){ + func(s sigType) { + req := s.(*ListRequest) + + owner := req.GetOwnerID() + owner[0]++ + + req.SetOwnerID(owner) + }, + }, + }, + { + constructor: func() sigType { + return new(DeleteRequest) + }, + payloadCorrupt: []func(sigType){ + func(s sigType) { + req := s.(*DeleteRequest) + + id, err := NewChequeID() + require.NoError(t, err) + + req.SetID(id) + }, + func(s sigType) { + req := s.(*DeleteRequest) + + owner := req.GetOwnerID() + owner[0]++ + + req.SetOwnerID(owner) + }, + func(s sigType) { + req := s.(*DeleteRequest) + + mid := req.GetMessageID() + mid[0]++ + + req.SetMessageID(mid) + }, + }, + }, + } + + for _, item := range items { + { // token corruptions + v := item.constructor() + + token := new(service.Token) + v.SetToken(token) + + require.NoError(t, service.SignDataWithSessionToken(sk, v)) + + require.NoError(t, service.VerifyAccumulatedSignaturesWithToken(v)) + + token.SetSessionKey(append(token.GetSessionKey(), 1)) + + require.Error(t, service.VerifyAccumulatedSignaturesWithToken(v)) + } + + { // payload corruptions + for _, corruption := range item.payloadCorrupt { + v := item.constructor() + + require.NoError(t, service.SignDataWithSessionToken(sk, v)) + + require.NoError(t, service.VerifyAccumulatedSignaturesWithToken(v)) + + corruption(v) + + require.Error(t, service.VerifyAccumulatedSignaturesWithToken(v)) + } + } + } +} diff --git a/accounting/types.go b/accounting/types.go index 6a3b2e2..e16fa99 100644 --- a/accounting/types.go +++ b/accounting/types.go @@ -351,3 +351,103 @@ func (m *Settlement) Equal(s *Settlement) bool { } return len(m.Transactions) == 0 || reflect.DeepEqual(m.Transactions, s.Transactions) } + +// GetOwnerID is an OwnerID field getter. +func (m BalanceRequest) GetOwnerID() OwnerID { + return m.OwnerID +} + +// SetOwnerID is an OwnerID field setter. +func (m *BalanceRequest) SetOwnerID(owner OwnerID) { + m.OwnerID = owner +} + +// GetID is an ID field getter. +func (m GetRequest) GetID() ChequeID { + return m.ID +} + +// SetID is an ID field setter. +func (m *GetRequest) SetID(id ChequeID) { + m.ID = id +} + +// GetOwnerID is an OwnerID field getter. +func (m GetRequest) GetOwnerID() OwnerID { + return m.OwnerID +} + +// SetOwnerID is an OwnerID field setter. +func (m *GetRequest) SetOwnerID(id OwnerID) { + m.OwnerID = id +} + +// GetOwnerID is an OwnerID field getter. +func (m PutRequest) GetOwnerID() OwnerID { + return m.OwnerID +} + +// SetOwnerID is an OwnerID field setter. +func (m *PutRequest) SetOwnerID(id OwnerID) { + m.OwnerID = id +} + +// GetMessageID is a MessageID field getter. +func (m PutRequest) GetMessageID() MessageID { + return m.MessageID +} + +// SetMessageID is a MessageID field setter. +func (m *PutRequest) SetMessageID(id MessageID) { + m.MessageID = id +} + +// SetAmount is an Amount field setter. +func (m *PutRequest) SetAmount(amount *decimal.Decimal) { + m.Amount = amount +} + +// SetHeight is a Height field setter. +func (m *PutRequest) SetHeight(h uint64) { + m.Height = h +} + +// GetOwnerID is an OwnerID field getter. +func (m ListRequest) GetOwnerID() OwnerID { + return m.OwnerID +} + +// SetOwnerID is an OwnerID field setter. +func (m *ListRequest) SetOwnerID(id OwnerID) { + m.OwnerID = id +} + +// GetID is an ID field getter. +func (m DeleteRequest) GetID() ChequeID { + return m.ID +} + +// SetID is an ID field setter. +func (m *DeleteRequest) SetID(id ChequeID) { + m.ID = id +} + +// GetOwnerID is an OwnerID field getter. +func (m DeleteRequest) GetOwnerID() OwnerID { + return m.OwnerID +} + +// SetOwnerID is an OwnerID field setter. +func (m *DeleteRequest) SetOwnerID(id OwnerID) { + m.OwnerID = id +} + +// GetMessageID is a MessageID field getter. +func (m DeleteRequest) GetMessageID() MessageID { + return m.MessageID +} + +// SetMessageID is a MessageID field setter. +func (m *DeleteRequest) SetMessageID(id MessageID) { + m.MessageID = id +} diff --git a/accounting/types_test.go b/accounting/types_test.go index df81b46..a440028 100644 --- a/accounting/types_test.go +++ b/accounting/types_test.go @@ -84,3 +84,110 @@ func TestCheque(t *testing.T) { require.Equal(t, cheque.Amount, decimal.NewGAS(42)) }) } + +func TestBalanceRequest_SetOwnerID(t *testing.T) { + ownerID := OwnerID{1, 2, 3} + m := new(BalanceRequest) + + m.SetOwnerID(ownerID) + + require.Equal(t, ownerID, m.GetOwnerID()) +} + +func TestGetRequestGettersSetters(t *testing.T) { + t.Run("id", func(t *testing.T) { + id := ChequeID("test id") + m := new(GetRequest) + + m.SetID(id) + + require.Equal(t, id, m.GetID()) + }) + + t.Run("owner", func(t *testing.T) { + id := OwnerID{1, 2, 3} + m := new(GetRequest) + + m.SetOwnerID(id) + + require.Equal(t, id, m.GetOwnerID()) + }) +} + +func TestPutRequestGettersSetters(t *testing.T) { + t.Run("owner", func(t *testing.T) { + id := OwnerID{1, 2, 3} + m := new(PutRequest) + + m.SetOwnerID(id) + + require.Equal(t, id, m.GetOwnerID()) + }) + + t.Run("message ID", func(t *testing.T) { + id, err := refs.NewUUID() + require.NoError(t, err) + + m := new(PutRequest) + m.SetMessageID(id) + + require.Equal(t, id, m.GetMessageID()) + }) + + t.Run("amount", func(t *testing.T) { + amount := decimal.New(1) + m := new(PutRequest) + + m.SetAmount(amount) + + require.Equal(t, amount, m.GetAmount()) + }) + + t.Run("height", func(t *testing.T) { + h := uint64(3) + m := new(PutRequest) + + m.SetHeight(h) + + require.Equal(t, h, m.GetHeight()) + }) +} + +func TestListRequestGettersSetters(t *testing.T) { + ownerID := OwnerID{1, 2, 3} + m := new(ListRequest) + + m.SetOwnerID(ownerID) + + require.Equal(t, ownerID, m.GetOwnerID()) +} + +func TestDeleteRequestGettersSetters(t *testing.T) { + t.Run("id", func(t *testing.T) { + id := ChequeID("test id") + m := new(DeleteRequest) + + m.SetID(id) + + require.Equal(t, id, m.GetID()) + }) + + t.Run("owner", func(t *testing.T) { + id := OwnerID{1, 2, 3} + m := new(DeleteRequest) + + m.SetOwnerID(id) + + require.Equal(t, id, m.GetOwnerID()) + }) + + t.Run("message ID", func(t *testing.T) { + id, err := refs.NewUUID() + require.NoError(t, err) + + m := new(DeleteRequest) + m.SetMessageID(id) + + require.Equal(t, id, m.GetMessageID()) + }) +} diff --git a/bootstrap/sign.go b/bootstrap/sign.go new file mode 100644 index 0000000..34f7fc2 --- /dev/null +++ b/bootstrap/sign.go @@ -0,0 +1,46 @@ +package bootstrap + +import ( + "io" + + "github.com/nspcc-dev/neofs-api-go/service" +) + +// SignedData returns payload bytes of the request. +func (m Request) SignedData() ([]byte, error) { + return service.SignedDataFromReader(m) +} + +// SignedDataSize returns payload size of the request. +func (m Request) SignedDataSize() (sz int) { + sz += m.GetType().Size() + + sz += m.GetState().Size() + + info := m.GetInfo() + sz += info.Size() + + return +} + +// ReadSignedData copies payload bytes to passed buffer. +// +// If the Request size is insufficient, io.ErrUnexpectedEOF returns. +func (m Request) ReadSignedData(p []byte) (int, error) { + if len(p) < m.SignedDataSize() { + return 0, io.ErrUnexpectedEOF + } + + var off int + + off += copy(p[off:], m.GetType().Bytes()) + + off += copy(p[off:], m.GetState().Bytes()) + + info := m.GetInfo() + // FIXME: implement and use stable functions + n, err := info.MarshalTo(p[off:]) + off += n + + return off, err +} diff --git a/bootstrap/sign_test.go b/bootstrap/sign_test.go new file mode 100644 index 0000000..2c76117 --- /dev/null +++ b/bootstrap/sign_test.go @@ -0,0 +1,82 @@ +package bootstrap + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-crypto/test" + "github.com/stretchr/testify/require" +) + +func TestRequestSign(t *testing.T) { + sk := test.DecodeKey(0) + + type sigType interface { + service.SignedDataWithToken + service.SignKeyPairAccumulator + service.SignKeyPairSource + SetToken(*service.Token) + } + + items := []struct { + constructor func() sigType + payloadCorrupt []func(sigType) + }{ + { // Request + constructor: func() sigType { + return new(Request) + }, + payloadCorrupt: []func(sigType){ + func(s sigType) { + req := s.(*Request) + + req.SetType(req.GetType() + 1) + }, + func(s sigType) { + req := s.(*Request) + + req.SetState(req.GetState() + 1) + }, + func(s sigType) { + req := s.(*Request) + + info := req.GetInfo() + info.Address += "1" + + req.SetInfo(info) + }, + }, + }, + } + + for _, item := range items { + { // token corruptions + v := item.constructor() + + token := new(service.Token) + v.SetToken(token) + + require.NoError(t, service.SignDataWithSessionToken(sk, v)) + + require.NoError(t, service.VerifyAccumulatedSignaturesWithToken(v)) + + token.SetSessionKey(append(token.GetSessionKey(), 1)) + + require.Error(t, service.VerifyAccumulatedSignaturesWithToken(v)) + } + + { // payload corruptions + for _, corruption := range item.payloadCorrupt { + v := item.constructor() + + require.NoError(t, service.SignDataWithSessionToken(sk, v)) + + require.NoError(t, service.VerifyAccumulatedSignaturesWithToken(v)) + + corruption(v) + + require.Error(t, service.VerifyAccumulatedSignaturesWithToken(v)) + } + } + } +} diff --git a/bootstrap/types.go b/bootstrap/types.go index 690d81c..7ad3ec2 100644 --- a/bootstrap/types.go +++ b/bootstrap/types.go @@ -2,6 +2,7 @@ package bootstrap import ( "bytes" + "encoding/binary" "encoding/hex" "strconv" "strings" @@ -27,6 +28,8 @@ var ( _ proto.Message = (*SpreadMap)(nil) ) +var requestEndianness = binary.BigEndian + // Equals checks whether two NodeInfo has same address. func (m NodeInfo) Equals(n1 NodeInfo) bool { return m.Address == n1.Address && bytes.Equal(m.PubKey, n1.PubKey) @@ -98,3 +101,37 @@ func (m SpreadMap) String() string { ", " + "Netmap: [" + strings.Join(result, ",") + "]>" } + +// GetType is a Type field getter. +func (m Request) GetType() NodeType { + return m.Type +} + +// SetType is a Type field setter. +func (m *Request) SetType(t NodeType) { + m.Type = t +} + +// SetState is a State field setter. +func (m *Request) SetState(state Request_State) { + m.State = state +} + +// SetInfo is an Info field getter. +func (m *Request) SetInfo(info NodeInfo) { + m.Info = info +} + +// Size returns the size necessary for a binary representation of the state. +func (x Request_State) Size() int { + return 4 +} + +// Bytes returns a binary representation of the state. +func (x Request_State) Bytes() []byte { + data := make([]byte, x.Size()) + + requestEndianness.PutUint32(data, uint32(x)) + + return data +} diff --git a/bootstrap/types_test.go b/bootstrap/types_test.go new file mode 100644 index 0000000..20b1b1a --- /dev/null +++ b/bootstrap/types_test.go @@ -0,0 +1,39 @@ +package bootstrap + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRequestGettersSetters(t *testing.T) { + t.Run("type", func(t *testing.T) { + rt := NodeType(1) + m := new(Request) + + m.SetType(rt) + + require.Equal(t, rt, m.GetType()) + }) + + t.Run("state", func(t *testing.T) { + st := Request_State(1) + m := new(Request) + + m.SetState(st) + + require.Equal(t, st, m.GetState()) + }) + + t.Run("info", func(t *testing.T) { + info := NodeInfo{ + Address: "some address", + } + + m := new(Request) + + m.SetInfo(info) + + require.Equal(t, info, m.GetInfo()) + }) +} diff --git a/container/sign.go b/container/sign.go new file mode 100644 index 0000000..eafd93c --- /dev/null +++ b/container/sign.go @@ -0,0 +1,137 @@ +package container + +import ( + "encoding/binary" + "io" + + service "github.com/nspcc-dev/neofs-api-go/service" +) + +var requestEndianness = binary.BigEndian + +// SignedData returns payload bytes of the request. +func (m PutRequest) SignedData() ([]byte, error) { + return service.SignedDataFromReader(m) +} + +// SignedDataSize returns payload size of the request. +func (m PutRequest) SignedDataSize() (sz int) { + sz += m.GetMessageID().Size() + + sz += 8 + + sz += m.GetOwnerID().Size() + + rules := m.GetRules() + sz += rules.Size() + + sz += 4 + + return +} + +// ReadSignedData copies payload bytes to passed buffer. +// +// If the Request size is insufficient, io.ErrUnexpectedEOF returns. +func (m PutRequest) ReadSignedData(p []byte) (int, error) { + if len(p) < m.SignedDataSize() { + return 0, io.ErrUnexpectedEOF + } + + var off int + + off += copy(p[off:], m.GetMessageID().Bytes()) + + requestEndianness.PutUint64(p[off:], m.GetCapacity()) + off += 8 + + off += copy(p[off:], m.GetOwnerID().Bytes()) + + rules := m.GetRules() + // FIXME: implement and use stable functions + n, err := rules.MarshalTo(p[off:]) + off += n + if err != nil { + return off, err + } + + requestEndianness.PutUint32(p[off:], m.GetBasicACL()) + off += 4 + + return off, nil +} + +// SignedData returns payload bytes of the request. +func (m DeleteRequest) SignedData() ([]byte, error) { + return service.SignedDataFromReader(m) +} + +// SignedDataSize returns payload size of the request. +func (m DeleteRequest) SignedDataSize() int { + return m.GetCID().Size() +} + +// ReadSignedData copies payload bytes to passed buffer. +// +// If the Request size is insufficient, io.ErrUnexpectedEOF returns. +func (m DeleteRequest) ReadSignedData(p []byte) (int, error) { + if len(p) < m.SignedDataSize() { + return 0, io.ErrUnexpectedEOF + } + + var off int + + off += copy(p[off:], m.GetCID().Bytes()) + + return off, nil +} + +// SignedData returns payload bytes of the request. +func (m GetRequest) SignedData() ([]byte, error) { + return service.SignedDataFromReader(m) +} + +// SignedDataSize returns payload size of the request. +func (m GetRequest) SignedDataSize() int { + return m.GetCID().Size() +} + +// ReadSignedData copies payload bytes to passed buffer. +// +// If the Request size is insufficient, io.ErrUnexpectedEOF returns. +func (m GetRequest) ReadSignedData(p []byte) (int, error) { + if len(p) < m.SignedDataSize() { + return 0, io.ErrUnexpectedEOF + } + + var off int + + off += copy(p[off:], m.GetCID().Bytes()) + + return off, nil +} + +// SignedData returns payload bytes of the request. +func (m ListRequest) SignedData() ([]byte, error) { + return service.SignedDataFromReader(m) +} + +// SignedDataSize returns payload size of the request. +func (m ListRequest) SignedDataSize() int { + return m.GetOwnerID().Size() +} + +// ReadSignedData copies payload bytes to passed buffer. +// +// If the Request size is insufficient, io.ErrUnexpectedEOF returns. +func (m ListRequest) ReadSignedData(p []byte) (int, error) { + if len(p) < m.SignedDataSize() { + return 0, io.ErrUnexpectedEOF + } + + var off int + + off += copy(p[off:], m.GetOwnerID().Bytes()) + + return off, nil +} diff --git a/container/sign_test.go b/container/sign_test.go new file mode 100644 index 0000000..e469399 --- /dev/null +++ b/container/sign_test.go @@ -0,0 +1,143 @@ +package container + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-crypto/test" + "github.com/stretchr/testify/require" +) + +func TestRequestSign(t *testing.T) { + sk := test.DecodeKey(0) + + type sigType interface { + service.SignedDataWithToken + service.SignKeyPairAccumulator + service.SignKeyPairSource + SetToken(*service.Token) + } + + items := []struct { + constructor func() sigType + payloadCorrupt []func(sigType) + }{ + { // PutRequest + constructor: func() sigType { + return new(PutRequest) + }, + payloadCorrupt: []func(sigType){ + func(s sigType) { + req := s.(*PutRequest) + + id := req.GetMessageID() + id[0]++ + + req.SetMessageID(id) + }, + func(s sigType) { + req := s.(*PutRequest) + + req.SetCapacity(req.GetCapacity() + 1) + }, + func(s sigType) { + req := s.(*PutRequest) + + owner := req.GetOwnerID() + owner[0]++ + + req.SetOwnerID(owner) + }, + func(s sigType) { + req := s.(*PutRequest) + + rules := req.GetRules() + rules.ReplFactor++ + + req.SetRules(rules) + }, + func(s sigType) { + req := s.(*PutRequest) + + req.SetBasicACL(req.GetBasicACL() + 1) + }, + }, + }, + { // DeleteRequest + constructor: func() sigType { + return new(DeleteRequest) + }, + payloadCorrupt: []func(sigType){ + func(s sigType) { + req := s.(*DeleteRequest) + + cid := req.GetCID() + cid[0]++ + + req.SetCID(cid) + }, + }, + }, + { // GetRequest + constructor: func() sigType { + return new(GetRequest) + }, + payloadCorrupt: []func(sigType){ + func(s sigType) { + req := s.(*GetRequest) + + cid := req.GetCID() + cid[0]++ + + req.SetCID(cid) + }, + }, + }, + { // ListRequest + constructor: func() sigType { + return new(ListRequest) + }, + payloadCorrupt: []func(sigType){ + func(s sigType) { + req := s.(*ListRequest) + + owner := req.GetOwnerID() + owner[0]++ + + req.SetOwnerID(owner) + }, + }, + }, + } + + for _, item := range items { + { // token corruptions + v := item.constructor() + + token := new(service.Token) + v.SetToken(token) + + require.NoError(t, service.SignDataWithSessionToken(sk, v)) + + require.NoError(t, service.VerifyAccumulatedSignaturesWithToken(v)) + + token.SetSessionKey(append(token.GetSessionKey(), 1)) + + require.Error(t, service.VerifyAccumulatedSignaturesWithToken(v)) + } + + { // payload corruptions + for _, corruption := range item.payloadCorrupt { + v := item.constructor() + + require.NoError(t, service.SignDataWithSessionToken(sk, v)) + + require.NoError(t, service.VerifyAccumulatedSignaturesWithToken(v)) + + corruption(v) + + require.Error(t, service.VerifyAccumulatedSignaturesWithToken(v)) + } + } + } +} diff --git a/container/types.go b/container/types.go index e358e6d..f340aa5 100644 --- a/container/types.go +++ b/container/types.go @@ -93,3 +93,68 @@ func NewTestContainer() (*Container, error) { }, }) } + +// GetMessageID is a MessageID field getter. +func (m PutRequest) GetMessageID() MessageID { + return m.MessageID +} + +// SetMessageID is a MessageID field getter. +func (m *PutRequest) SetMessageID(id MessageID) { + m.MessageID = id +} + +// SetCapacity is a Capacity field setter. +func (m *PutRequest) SetCapacity(c uint64) { + m.Capacity = c +} + +// GetOwnerID is an OwnerID field getter. +func (m PutRequest) GetOwnerID() OwnerID { + return m.OwnerID +} + +// SetOwnerID is an OwnerID field setter. +func (m *PutRequest) SetOwnerID(owner OwnerID) { + m.OwnerID = owner +} + +// SetRules is a Rules field setter. +func (m *PutRequest) SetRules(rules netmap.PlacementRule) { + m.Rules = rules +} + +// SetBasicACL is a BasicACL field setter. +func (m *PutRequest) SetBasicACL(acl uint32) { + m.BasicACL = acl +} + +// GetCID is a CID field getter. +func (m DeleteRequest) GetCID() CID { + return m.CID +} + +// SetCID is a CID field setter. +func (m *DeleteRequest) SetCID(cid CID) { + m.CID = cid +} + +// GetCID is a CID field getter. +func (m GetRequest) GetCID() CID { + return m.CID +} + +// SetCID is a CID field setter. +func (m *GetRequest) SetCID(cid CID) { + m.CID = cid +} + +// GetOwnerID is an OwnerID field getter. +func (m ListRequest) GetOwnerID() OwnerID { + return m.OwnerID +} + +// SetOwnerID is an OwnerID field setter. +func (m *ListRequest) SetOwnerID(owner OwnerID) { + m.OwnerID = owner +} diff --git a/container/types_test.go b/container/types_test.go index fddccb3..cc171cb 100644 --- a/container/types_test.go +++ b/container/types_test.go @@ -55,3 +55,88 @@ func TestCID(t *testing.T) { require.Equal(t, cid1, cid2) }) } + +func TestPutRequestGettersSetters(t *testing.T) { + t.Run("owner", func(t *testing.T) { + owner := OwnerID{1, 2, 3} + m := new(PutRequest) + + m.SetOwnerID(owner) + + require.Equal(t, owner, m.GetOwnerID()) + }) + + t.Run("capacity", func(t *testing.T) { + cp := uint64(3) + m := new(PutRequest) + + m.SetCapacity(cp) + + require.Equal(t, cp, m.GetCapacity()) + }) + + t.Run("message ID", func(t *testing.T) { + id, err := refs.NewUUID() + require.NoError(t, err) + + m := new(PutRequest) + + m.SetMessageID(id) + + require.Equal(t, id, m.GetMessageID()) + }) + + t.Run("rules", func(t *testing.T) { + rules := netmap.PlacementRule{ + ReplFactor: 1, + } + + m := new(PutRequest) + + m.SetRules(rules) + + require.Equal(t, rules, m.GetRules()) + }) + + t.Run("basic ACL", func(t *testing.T) { + bACL := uint32(5) + m := new(PutRequest) + + m.SetBasicACL(bACL) + + require.Equal(t, bACL, m.GetBasicACL()) + }) +} + +func TestDeleteRequestGettersSetters(t *testing.T) { + t.Run("cid", func(t *testing.T) { + cid := CID{1, 2, 3} + m := new(DeleteRequest) + + m.SetCID(cid) + + require.Equal(t, cid, m.GetCID()) + }) +} + +func TestGetRequestGettersSetters(t *testing.T) { + t.Run("cid", func(t *testing.T) { + cid := CID{1, 2, 3} + m := new(GetRequest) + + m.SetCID(cid) + + require.Equal(t, cid, m.GetCID()) + }) +} + +func TestListRequestGettersSetters(t *testing.T) { + t.Run("owner", func(t *testing.T) { + owner := OwnerID{1, 2, 3} + m := new(PutRequest) + + m.SetOwnerID(owner) + + require.Equal(t, owner, m.GetOwnerID()) + }) +} diff --git a/docs/object.md b/docs/object.md index 4ec32fc..27e4bcf 100644 --- a/docs/object.md +++ b/docs/object.md @@ -149,7 +149,6 @@ calculated for XORed data. | ----- | ---- | ----- | ----------- | | Address | [refs.Address](#refs.Address) | | Address of object (container id + object id) | | OwnerID | [bytes](#bytes) | | OwnerID is a wallet address | -| Token | [session.Token](#session.Token) | | Token with session public key and user's signature | | 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) | @@ -228,7 +227,6 @@ in distributed system. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | Address | [refs.Address](#refs.Address) | | Address of object (container id + object id) | -| Raw | [bool](#bool) | | Raw is the request flag of a physically stored representation of an object | | 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) | @@ -256,7 +254,6 @@ in distributed system. | ----- | ---- | ----- | ----------- | | Address | [refs.Address](#refs.Address) | | Address of object (container id + object id) | | FullHeaders | [bool](#bool) | | FullHeaders can be set true for extended headers in the object | -| Raw | [bool](#bool) | | Raw is the request flag of a physically stored representation of an object | | 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) | @@ -296,7 +293,6 @@ in distributed system. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | Object | [Object](#object.Object) | | Object with at least container id and owner id fields | -| Token | [session.Token](#session.Token) | | Token with session public key and user's signature | | CopiesNumber | [uint32](#uint32) | | Number of the object copies to store within the RPC call (zero is processed according to the placement rules) | @@ -378,7 +374,7 @@ in distributed system. | UserHeader | [UserHeader](#object.UserHeader) | | UserHeader is a set of KV headers defined by user | | Transform | [Transform](#object.Transform) | | Transform defines transform operation (e.g. payload split) | | Tombstone | [Tombstone](#object.Tombstone) | | Tombstone header that set up in deleted objects | -| Verify | [session.VerificationHeader](#session.VerificationHeader) | | Verify header that contains session public key and user's signature | +| Token | [service.Token](#service.Token) | | Token header contains token of the session within which the object was created | | HomoHash | [bytes](#bytes) | | HomoHash is a homomorphic hash of original object payload | | PayloadChecksum | [bytes](#bytes) | | PayloadChecksum of actual object's payload | | Integrity | [IntegrityHeader](#object.IntegrityHeader) | | Integrity header with checksum of all above headers in the object | diff --git a/docs/service.md b/docs/service.md index 90e1bd2..9ed548e 100644 --- a/docs/service.md +++ b/docs/service.md @@ -14,8 +14,10 @@ - Messages - [RequestVerificationHeader](#service.RequestVerificationHeader) - - [RequestVerificationHeader.Sign](#service.RequestVerificationHeader.Sign) - [RequestVerificationHeader.Signature](#service.RequestVerificationHeader.Signature) + - [Token](#service.Token) + - [Token.Info](#service.Token.Info) + - [TokenLifetime](#service.TokenLifetime) - [service/verify_test.proto](#service/verify_test.proto) @@ -49,6 +51,7 @@ RequestMetaHeader contains information about request meta headers | TTL | [uint32](#uint32) | | TTL must be larger than zero, it decreased in every NeoFS Node | | Epoch | [uint64](#uint64) | | Epoch for user can be empty, because node sets epoch to the actual value | | Version | [uint32](#uint32) | | Version defines protocol version TODO: not used for now, should be implemented in future | +| Raw | [bool](#bool) | | Raw determines whether the request is raw or not | @@ -88,18 +91,7 @@ RequestVerificationHeader is a set of signatures of every NeoFS Node that proces | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | Signatures | [RequestVerificationHeader.Signature](#service.RequestVerificationHeader.Signature) | repeated | Signatures is a set of signatures of every passed NeoFS Node | - - - - -### Message RequestVerificationHeader.Sign - - - -| Field | Type | Label | Description | -| ----- | ---- | ----- | ----------- | -| Sign | [bytes](#bytes) | | Sign is signature of the request or session key. | -| Peer | [bytes](#bytes) | | Peer is compressed public key used for signature. | +| Token | [Token](#service.Token) | | Token is a token of the session within which the request is sent | @@ -110,11 +102,68 @@ RequestVerificationHeader is a set of signatures of every NeoFS Node that proces | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | -| Sign | [RequestVerificationHeader.Sign](#service.RequestVerificationHeader.Sign) | | Sign is a signature and public key of the request. | -| Origin | [RequestVerificationHeader.Sign](#service.RequestVerificationHeader.Sign) | | Origin used for requests, when trusted node changes it and re-sign with session key. If session key used for signature request, then Origin should contain public key of user and signed session key. | +| Sign | [bytes](#bytes) | | Sign is signature of the request or session key. | +| Peer | [bytes](#bytes) | | Peer is compressed public key used for signature. | + + + + +### Message Token +User token granting rights for object manipulation + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| TokenInfo | [Token.Info](#service.Token.Info) | | TokenInfo is a grouped information about token | +| Signature | [bytes](#bytes) | | Signature is a signature of session token information | + + + + +### Message Token.Info + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| ID | [bytes](#bytes) | | ID is a token identifier. valid UUIDv4 represented in bytes | +| 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 | +| 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 | + + + +### Token.Info.Verb +Verb is an enumeration of session request types + +| Name | Number | Description | +| ---- | ------ | ----------- | +| Put | 0 | Put refers to object.Put RPC call | +| Get | 1 | Get refers to object.Get RPC call | +| Head | 2 | Head refers to object.Head RPC call | +| Search | 3 | Search refers to object.Search RPC call | +| Delete | 4 | Delete refers to object.Delete RPC call | +| Range | 5 | Range refers to object.GetRange RPC call | +| RangeHash | 6 | RangeHash refers to object.GetRangeHash RPC call | + + diff --git a/docs/session.md b/docs/session.md index ba615c3..5ec7402 100644 --- a/docs/session.md +++ b/docs/session.md @@ -12,13 +12,6 @@ - [CreateResponse](#session.CreateResponse) -- [session/types.proto](#session/types.proto) - - - Messages - - [Token](#session.Token) - - [VerificationHeader](#session.VerificationHeader) - - - [Scalar Value Types](#scalar-value-types) @@ -37,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 | | ---- | ----- | ------ | @@ -63,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 | [Token](#session.Token) | | Init is a message to initialize session opening. Carry: owner of manipulation object; ID of manipulation object; token lifetime bounds. | -| Signed | [Token](#session.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) | @@ -77,57 +61,13 @@ session key. Session is established during 4-step handshake in one gRPC stream ### Message CreateResponse - - - -| Field | Type | Label | Description | -| ----- | ---- | ----- | ----------- | -| Unsigned | [Token](#session.Token) | | Unsigned token with token ID and session public key generated on server side | -| Result | [Token](#session.Token) | | Result is a resulting token which can be used for object placing through an trusted intermediary | - - - - - - - - -

Top

- -## session/types.proto - - - - - - - -### Message Token -User token granting rights for object manipulation +CreateResponse carries an information about the opened session | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | -| Header | [VerificationHeader](#session.VerificationHeader) | | Header carries verification data of session key | -| OwnerID | [bytes](#bytes) | | OwnerID is an owner of manipulation object | -| FirstEpoch | [uint64](#uint64) | | FirstEpoch is an initial epoch of token lifetime | -| LastEpoch | [uint64](#uint64) | | LastEpoch is a last epoch of token lifetime | -| ObjectID | [bytes](#bytes) | repeated | ObjectID is an object identifier of manipulation object | -| Signature | [bytes](#bytes) | | Signature is a token signature, signed by owner of manipulation object | -| ID | [bytes](#bytes) | | ID is a token identifier. valid UUIDv4 represented in bytes | -| PublicKeys | [bytes](#bytes) | repeated | PublicKeys associated with owner | - - - - -### Message VerificationHeader - - - -| Field | Type | Label | Description | -| ----- | ---- | ----- | ----------- | -| PublicKey | [bytes](#bytes) | | PublicKey is a session public key | -| KeySignature | [bytes](#bytes) | | KeySignature is a session public key signature. Signed by trusted side | +| ID | [bytes](#bytes) | | ID carries an identifier of session token | +| SessionKey | [bytes](#bytes) | | SessionKey carries a session public key | diff --git a/object/extensions.go b/object/extensions.go index 6e577bd..be755c6 100644 --- a/object/extensions.go +++ b/object/extensions.go @@ -19,21 +19,6 @@ func (m Object) IsLinking() bool { return false } -// VerificationHeader returns verification header if it is presented in extended headers. -func (m Object) VerificationHeader() (*VerificationHeader, error) { - _, vh := m.LastHeader(HeaderType(VerifyHdr)) - if vh == nil { - return nil, ErrHeaderNotFound - } - return vh.Value.(*Header_Verify).Verify, nil -} - -// SetVerificationHeader sets verification header in the object. -// It will replace existing verification header or add a new one. -func (m *Object) SetVerificationHeader(header *VerificationHeader) { - m.SetHeader(&Header{Value: &Header_Verify{Verify: header}}) -} - // Links returns slice of ids of specified link type func (m *Object) Links(t Link_Type) []ID { var res []ID diff --git a/object/service.go b/object/service.go index 45a8d4b..0e38d70 100644 --- a/object/service.go +++ b/object/service.go @@ -31,7 +31,7 @@ type ( // All object operations must have TTL, Epoch, Type, Container ID and // permission of usage previous network map. Request interface { - service.MetaHeader + service.SeizedRequestMetaContainer CID() CID Type() RequestType diff --git a/object/service.pb.go b/object/service.pb.go index 4ac61bc..f882f5e 100644 Binary files a/object/service.pb.go and b/object/service.pb.go differ diff --git a/object/service.proto b/object/service.proto index b5042e2..91d0b99 100644 --- a/object/service.proto +++ b/object/service.proto @@ -5,7 +5,6 @@ option csharp_namespace = "NeoFS.API.Object"; import "refs/types.proto"; import "object/types.proto"; -import "session/types.proto"; import "service/meta.proto"; import "service/verify.proto"; import "github.com/gogo/protobuf/gogoproto/gogo.proto"; @@ -58,8 +57,6 @@ service Service { message GetRequest { // Address of object (container id + object id) refs.Address Address = 1 [(gogoproto.nullable) = false]; - // Raw is the request flag of a physically stored representation of an object - bool Raw = 2; // 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) @@ -82,10 +79,8 @@ message PutRequest { message PutHeader { // Object with at least container id and owner id fields Object Object = 1; - // Token with session public key and user's signature - session.Token Token = 2; // Number of the object copies to store within the RPC call (zero is processed according to the placement rules) - uint32 CopiesNumber = 3; + uint32 CopiesNumber = 2; } oneof R { @@ -112,8 +107,6 @@ message DeleteRequest { refs.Address Address = 1 [(gogoproto.nullable) = false]; // OwnerID is a wallet address bytes OwnerID = 2 [(gogoproto.nullable) = false, (gogoproto.customtype) = "OwnerID"]; - // Token with session public key and user's signature - session.Token Token = 3; // 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) @@ -132,8 +125,6 @@ message HeadRequest { refs.Address Address = 1 [(gogoproto.nullable) = false, (gogoproto.customtype) = "Address"]; // FullHeaders can be set true for extended headers in the object bool FullHeaders = 2; - // Raw is the request flag of a physically stored representation of an object - bool Raw = 3; // 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) diff --git a/object/service_test.go b/object/service_test.go index 4b02b37..5b7a358 100644 --- a/object/service_test.go +++ b/object/service_test.go @@ -16,8 +16,8 @@ func TestRequest(t *testing.T) { &DeleteRequest{}, &GetRangeRequest{}, &GetRangeHashRequest{}, - MakePutRequestHeader(nil, nil), - MakePutRequestHeader(&Object{}, nil), + MakePutRequestHeader(nil), + MakePutRequestHeader(&Object{}), } types := []RequestType{ diff --git a/object/sign.go b/object/sign.go new file mode 100644 index 0000000..1ed3efa --- /dev/null +++ b/object/sign.go @@ -0,0 +1,259 @@ +package object + +import ( + "encoding/binary" + "io" + + "github.com/nspcc-dev/neofs-api-go/service" +) + +// SignedData returns payload bytes of the request. +// +// If payload is nil, ErrHeaderNotFound returns. +func (m PutRequest) SignedData() ([]byte, error) { + return service.SignedDataFromReader(m) +} + +// ReadSignedData copies payload bytes to passed buffer. +// +// If the buffer size is insufficient, io.ErrUnexpectedEOF returns. +func (m PutRequest) ReadSignedData(p []byte) (int, error) { + r := m.GetR() + if r == nil { + return 0, ErrHeaderNotFound + } + + return r.MarshalTo(p) +} + +// SignedDataSize returns the size of payload of the Put request. +// +// If payload is nil, -1 returns. +func (m PutRequest) SignedDataSize() int { + r := m.GetR() + if r == nil { + return -1 + } + + return r.Size() +} + +// SignedData returns payload bytes of the request. +func (m GetRequest) SignedData() ([]byte, error) { + return service.SignedDataFromReader(m) +} + +// ReadSignedData copies payload bytes to passed buffer. +// +// If the buffer size is insufficient, io.ErrUnexpectedEOF returns. +func (m GetRequest) ReadSignedData(p []byte) (int, error) { + addr := m.GetAddress() + + if len(p) < m.SignedDataSize() { + return 0, io.ErrUnexpectedEOF + } + + var off int + + off += copy(p[off:], addr.CID.Bytes()) + + off += copy(p[off:], addr.ObjectID.Bytes()) + + return off, nil +} + +// SignedDataSize returns payload size of the request. +func (m GetRequest) SignedDataSize() int { + return addressSize(m.GetAddress()) +} + +// SignedData returns payload bytes of the request. +func (m HeadRequest) SignedData() ([]byte, error) { + return service.SignedDataFromReader(m) +} + +// ReadSignedData copies payload bytes to passed buffer. +// +// If the buffer size is insufficient, io.ErrUnexpectedEOF returns. +func (m HeadRequest) ReadSignedData(p []byte) (int, error) { + if len(p) < m.SignedDataSize() { + return 0, io.ErrUnexpectedEOF + } + + if m.GetFullHeaders() { + p[0] = 1 + } + + off := 1 + + off += copy(p[off:], m.Address.CID.Bytes()) + + off += copy(p[off:], m.Address.ObjectID.Bytes()) + + return off, nil +} + +// SignedDataSize returns payload size of the request. +func (m HeadRequest) SignedDataSize() int { + return addressSize(m.Address) + 1 +} + +// SignedData returns payload bytes of the request. +func (m DeleteRequest) SignedData() ([]byte, error) { + return service.SignedDataFromReader(m) +} + +// ReadSignedData copies payload bytes to passed buffer. +// +// If the buffer size is insufficient, io.ErrUnexpectedEOF returns. +func (m DeleteRequest) ReadSignedData(p []byte) (int, error) { + if len(p) < m.SignedDataSize() { + return 0, io.ErrUnexpectedEOF + } + + var off int + + off += copy(p[off:], m.OwnerID.Bytes()) + + off += copy(p[off:], addressBytes(m.Address)) + + return off, nil +} + +// SignedDataSize returns payload size of the request. +func (m DeleteRequest) SignedDataSize() int { + return m.OwnerID.Size() + addressSize(m.Address) +} + +// SignedData returns payload bytes of the request. +func (m GetRangeRequest) SignedData() ([]byte, error) { + return service.SignedDataFromReader(m) +} + +// ReadSignedData copies payload bytes to passed buffer. +// +// If the buffer size is insufficient, io.ErrUnexpectedEOF returns. +func (m GetRangeRequest) ReadSignedData(p []byte) (int, error) { + if len(p) < m.SignedDataSize() { + return 0, io.ErrUnexpectedEOF + } + + n, err := (&m.Range).MarshalTo(p) + if err != nil { + return 0, err + } + + n += copy(p[n:], addressBytes(m.GetAddress())) + + return n, nil +} + +// SignedDataSize returns payload size of the request. +func (m GetRangeRequest) SignedDataSize() int { + return (&m.Range).Size() + addressSize(m.GetAddress()) +} + +// SignedData returns payload bytes of the request. +func (m GetRangeHashRequest) SignedData() ([]byte, error) { + return service.SignedDataFromReader(m) +} + +// ReadSignedData copies payload bytes to passed buffer. +// +// If the buffer size is insufficient, io.ErrUnexpectedEOF returns. +func (m GetRangeHashRequest) ReadSignedData(p []byte) (int, error) { + if len(p) < m.SignedDataSize() { + return 0, io.ErrUnexpectedEOF + } + + var off int + + off += copy(p[off:], addressBytes(m.GetAddress())) + + off += copy(p[off:], rangeSetBytes(m.GetRanges())) + + off += copy(p[off:], m.GetSalt()) + + return off, nil +} + +// SignedDataSize returns payload size of the request. +func (m GetRangeHashRequest) SignedDataSize() int { + var sz int + + sz += addressSize(m.GetAddress()) + + sz += rangeSetSize(m.GetRanges()) + + sz += len(m.GetSalt()) + + return sz +} + +// SignedData returns payload bytes of the request. +func (m SearchRequest) SignedData() ([]byte, error) { + return service.SignedDataFromReader(m) +} + +// ReadSignedData copies payload bytes to passed buffer. +// +// If the buffer size is insufficient, io.ErrUnexpectedEOF returns. +func (m SearchRequest) ReadSignedData(p []byte) (int, error) { + if len(p) < m.SignedDataSize() { + return 0, io.ErrUnexpectedEOF + } + + var off int + + off += copy(p[off:], m.CID().Bytes()) + + binary.BigEndian.PutUint32(p[off:], m.GetQueryVersion()) + off += 4 + + off += copy(p[off:], m.GetQuery()) + + return off, nil +} + +// SignedDataSize returns payload size of the request. +func (m SearchRequest) SignedDataSize() int { + var sz int + + sz += m.CID().Size() + + sz += 4 // uint32 Version + + sz += len(m.GetQuery()) + + return sz +} + +func rangeSetSize(rs []Range) int { + return 4 + len(rs)*16 // two uint64 fields +} + +func rangeSetBytes(rs []Range) []byte { + data := make([]byte, rangeSetSize(rs)) + + binary.BigEndian.PutUint32(data, uint32(len(rs))) + + off := 4 + + for i := range rs { + binary.BigEndian.PutUint64(data[off:], rs[i].Offset) + off += 8 + + binary.BigEndian.PutUint64(data[off:], rs[i].Length) + off += 8 + } + + return data +} + +func addressSize(addr Address) int { + return addr.CID.Size() + addr.ObjectID.Size() +} + +func addressBytes(addr Address) []byte { + return append(addr.CID.Bytes(), addr.ObjectID.Bytes()...) +} diff --git a/object/sign_test.go b/object/sign_test.go new file mode 100644 index 0000000..4df1c2b --- /dev/null +++ b/object/sign_test.go @@ -0,0 +1,189 @@ +package object + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-crypto/test" + "github.com/stretchr/testify/require" +) + +func TestSignVerifyRequests(t *testing.T) { + sk := test.DecodeKey(0) + + type sigType interface { + service.SignedDataWithToken + service.SignKeyPairAccumulator + service.SignKeyPairSource + SetToken(*Token) + } + + items := []struct { + constructor func() sigType + payloadCorrupt []func(sigType) + }{ + { // PutRequest.PutHeader + constructor: func() sigType { + return MakePutRequestHeader(new(Object)) + }, + payloadCorrupt: []func(sigType){ + func(s sigType) { + obj := s.(*PutRequest).GetR().(*PutRequest_Header).Header.GetObject() + obj.SystemHeader.PayloadLength++ + }, + }, + }, + { // PutRequest.Chunk + constructor: func() sigType { + return MakePutRequestChunk(make([]byte, 10)) + }, + payloadCorrupt: []func(sigType){ + func(s sigType) { + h := s.(*PutRequest).GetR().(*PutRequest_Chunk) + h.Chunk[0]++ + }, + }, + }, + { // GetRequest + constructor: func() sigType { + return new(GetRequest) + }, + payloadCorrupt: []func(sigType){ + func(s sigType) { + s.(*GetRequest).Address.CID[0]++ + }, + func(s sigType) { + s.(*GetRequest).Address.ObjectID[0]++ + }, + }, + }, + { // HeadRequest + constructor: func() sigType { + return new(HeadRequest) + }, + payloadCorrupt: []func(sigType){ + func(s sigType) { + s.(*HeadRequest).Address.CID[0]++ + }, + func(s sigType) { + s.(*HeadRequest).Address.ObjectID[0]++ + }, + func(s sigType) { + s.(*HeadRequest).FullHeaders = true + }, + }, + }, + { // DeleteRequest + constructor: func() sigType { + return new(DeleteRequest) + }, + payloadCorrupt: []func(sigType){ + func(s sigType) { + s.(*DeleteRequest).OwnerID[0]++ + }, + func(s sigType) { + s.(*DeleteRequest).Address.CID[0]++ + }, + func(s sigType) { + s.(*DeleteRequest).Address.ObjectID[0]++ + }, + }, + }, + { // GetRangeRequest + constructor: func() sigType { + return new(GetRangeRequest) + }, + payloadCorrupt: []func(sigType){ + func(s sigType) { + s.(*GetRangeRequest).Range.Length++ + }, + func(s sigType) { + s.(*GetRangeRequest).Range.Offset++ + }, + func(s sigType) { + s.(*GetRangeRequest).Address.CID[0]++ + }, + func(s sigType) { + s.(*GetRangeRequest).Address.ObjectID[0]++ + }, + }, + }, + { // GetRangeHashRequest + constructor: func() sigType { + return &GetRangeHashRequest{ + Ranges: []Range{{}}, + Salt: []byte{1, 2, 3}, + } + }, + payloadCorrupt: []func(sigType){ + func(s sigType) { + s.(*GetRangeHashRequest).Address.CID[0]++ + }, + func(s sigType) { + s.(*GetRangeHashRequest).Address.ObjectID[0]++ + }, + func(s sigType) { + s.(*GetRangeHashRequest).Salt[0]++ + }, + func(s sigType) { + s.(*GetRangeHashRequest).Ranges[0].Length++ + }, + func(s sigType) { + s.(*GetRangeHashRequest).Ranges[0].Offset++ + }, + func(s sigType) { + s.(*GetRangeHashRequest).Ranges = nil + }, + }, + }, + { // GetRangeHashRequest + constructor: func() sigType { + return &SearchRequest{ + Query: []byte{1, 2, 3}, + } + }, + payloadCorrupt: []func(sigType){ + func(s sigType) { + s.(*SearchRequest).ContainerID[0]++ + }, + func(s sigType) { + s.(*SearchRequest).Query[0]++ + }, + func(s sigType) { + s.(*SearchRequest).QueryVersion++ + }, + }, + }, + } + + for _, item := range items { + { // token corruptions + v := item.constructor() + + token := new(Token) + v.SetToken(token) + + require.NoError(t, service.SignDataWithSessionToken(sk, v)) + + require.NoError(t, service.VerifyAccumulatedSignaturesWithToken(v)) + + token.SetSessionKey(append(token.GetSessionKey(), 1)) + + require.Error(t, service.VerifyAccumulatedSignaturesWithToken(v)) + } + + { // payload corruptions + for _, corruption := range item.payloadCorrupt { + v := item.constructor() + + require.NoError(t, service.SignDataWithSessionToken(sk, v)) + + require.NoError(t, service.VerifyAccumulatedSignaturesWithToken(v)) + + corruption(v) + + require.Error(t, service.VerifyAccumulatedSignaturesWithToken(v)) + } + } + } +} diff --git a/object/types.go b/object/types.go index aebb2fc..c8d3f25 100644 --- a/object/types.go +++ b/object/types.go @@ -3,11 +3,14 @@ package object import ( "bytes" "context" + "fmt" + "io" + "reflect" "github.com/gogo/protobuf/proto" "github.com/nspcc-dev/neofs-api-go/internal" "github.com/nspcc-dev/neofs-api-go/refs" - "github.com/nspcc-dev/neofs-api-go/session" + "github.com/pkg/errors" ) type ( @@ -19,9 +22,6 @@ type ( // Address is a type alias of object Address. Address = refs.Address - // VerificationHeader is a type alias of session's verification header. - VerificationHeader = session.VerificationHeader - // PositionReader defines object reader that returns slice of bytes // for specified object and data range. PositionReader interface { @@ -60,8 +60,8 @@ const ( TransformHdr // TombstoneHdr is a tombstone header type. TombstoneHdr - // VerifyHdr is a verification header type. - VerifyHdr + // TokenHdr is a token header type. + TokenHdr // HomoHashHdr is a homomorphic hash header type. HomoHashHdr // PayloadChecksumHdr is a payload checksum header type. @@ -175,8 +175,8 @@ func (m Header) typeOf(t isHeader_Value) (ok bool) { _, ok = m.Value.(*Header_Transform) case *Header_Tombstone: _, ok = m.Value.(*Header_Tombstone) - case *Header_Verify: - _, ok = m.Value.(*Header_Verify) + case *Header_Token: + _, ok = m.Value.(*Header_Token) case *Header_HomoHash: _, ok = m.Value.(*Header_HomoHash) case *Header_PayloadChecksum: @@ -205,8 +205,8 @@ func HeaderType(t headerType) Pred { return func(h *Header) bool { _, ok := h.Value.(*Header_Transform); return ok } case TombstoneHdr: return func(h *Header) bool { _, ok := h.Value.(*Header_Tombstone); return ok } - case VerifyHdr: - return func(h *Header) bool { _, ok := h.Value.(*Header_Verify); return ok } + case TokenHdr: + return func(h *Header) bool { _, ok := h.Value.(*Header_Token); return ok } case HomoHashHdr: return func(h *Header) bool { _, ok := h.Value.(*Header_HomoHash); return ok } case PayloadChecksumHdr: @@ -251,6 +251,12 @@ func (m *Object) CopyTo(o *Object) { HomoHash: v.HomoHash, }, } + case *Header_Token: + o.Headers[i] = Header{ + Value: &Header_Token{ + Token: v.Token, + }, + } default: o.Headers[i] = *proto.Clone(&m.Headers[i]).(*Header) } @@ -266,3 +272,117 @@ func (m Object) Address() *refs.Address { CID: m.SystemHeader.CID, } } + +func (m CreationPoint) String() string { + return fmt.Sprintf(`{UnixTime=%d Epoch=%d}`, m.UnixTime, m.Epoch) +} + +// Stringify converts object into string format. +func Stringify(dst io.Writer, obj *Object) error { + // put empty line + if _, err := fmt.Fprintln(dst); err != nil { + return err + } + + // put object line + if _, err := fmt.Fprintln(dst, "Object:"); err != nil { + return err + } + + // put system headers + if _, err := fmt.Fprintln(dst, "\tSystemHeader:"); err != nil { + return err + } + + sysHeaders := []string{"ID", "CID", "OwnerID", "Version", "PayloadLength", "CreatedAt"} + v := reflect.ValueOf(obj.SystemHeader) + for _, key := range sysHeaders { + if !v.FieldByName(key).IsValid() { + return errors.Errorf("invalid system header key: %q", key) + } + + val := v.FieldByName(key).Interface() + if _, err := fmt.Fprintf(dst, "\t\t- %s=%v\n", key, val); err != nil { + return err + } + } + + // put user headers + if _, err := fmt.Fprintln(dst, "\tUserHeaders:"); err != nil { + return err + } + + for _, header := range obj.Headers { + var ( + typ = reflect.ValueOf(header.Value) + key string + val interface{} + ) + + switch t := typ.Interface().(type) { + case *Header_Link: + key = "Link" + val = fmt.Sprintf(`{Type=%s ID=%s}`, t.Link.Type, t.Link.ID) + case *Header_Redirect: + key = "Redirect" + val = fmt.Sprintf(`{CID=%s OID=%s}`, t.Redirect.CID, t.Redirect.ObjectID) + case *Header_UserHeader: + key = "UserHeader" + val = fmt.Sprintf(`{Key=%s Val=%s}`, t.UserHeader.Key, t.UserHeader.Value) + case *Header_Transform: + key = "Transform" + val = t.Transform.Type.String() + case *Header_Tombstone: + key = "Tombstone" + val = "MARKED" + case *Header_Token: + key = "Token" + val = fmt.Sprintf("{"+ + "ID=%s OwnerID=%s Verb=%s Address=%s Created=%d ValidUntil=%d SessionKey=%02x Signature=%02x"+ + "}", + t.Token.GetID(), + t.Token.GetOwnerID(), + t.Token.GetVerb(), + t.Token.GetAddress(), + t.Token.CreationEpoch(), + t.Token.ExpirationEpoch(), + t.Token.GetSessionKey(), + t.Token.GetSignature()) + case *Header_HomoHash: + key = "HomoHash" + val = t.HomoHash + case *Header_PayloadChecksum: + key = "PayloadChecksum" + val = t.PayloadChecksum + case *Header_Integrity: + key = "Integrity" + val = fmt.Sprintf(`{Checksum=%02x Signature=%02x}`, + t.Integrity.HeadersChecksum, + t.Integrity.ChecksumSignature) + case *Header_StorageGroup: + key = "StorageGroup" + val = fmt.Sprintf(`{DataSize=%d Hash=%02x Lifetime={Unit=%s Value=%d}}`, + t.StorageGroup.ValidationDataSize, + t.StorageGroup.ValidationHash, + t.StorageGroup.Lifetime.Unit, + t.StorageGroup.Lifetime.Value) + case *Header_PublicKey: + key = "PublicKey" + val = t.PublicKey.Value + default: + key = "Unknown" + val = t + } + + if _, err := fmt.Fprintf(dst, "\t\t- Type=%s\n\t\t Value=%v\n", key, val); err != nil { + return err + } + } + + // put payload + if _, err := fmt.Fprintf(dst, "\tPayload: %#v\n", obj.Payload); err != nil { + return err + } + + return nil +} diff --git a/object/types.pb.go b/object/types.pb.go index fe47459..97106c7 100644 Binary files a/object/types.pb.go and b/object/types.pb.go differ diff --git a/object/types.proto b/object/types.proto index f21bf74..46e1549 100644 --- a/object/types.proto +++ b/object/types.proto @@ -4,7 +4,7 @@ option go_package = "github.com/nspcc-dev/neofs-api-go/object"; option csharp_namespace = "NeoFS.API.Object"; import "refs/types.proto"; -import "session/types.proto"; +import "service/verify.proto"; import "storagegroup/types.proto"; import "github.com/gogo/protobuf/gogoproto/gogo.proto"; @@ -36,8 +36,8 @@ message Header { Transform Transform = 4; // Tombstone header that set up in deleted objects Tombstone Tombstone = 5; - // Verify header that contains session public key and user's signature - session.VerificationHeader Verify = 6; + // Token header contains token of the session within which the object was created + service.Token Token = 6; // HomoHash is a homomorphic hash of original object payload bytes HomoHash = 7 [(gogoproto.customtype) = "Hash"]; // PayloadChecksum of actual object's payload @@ -70,6 +70,8 @@ message SystemHeader { } message CreationPoint { + option (gogoproto.goproto_stringer) = false; + // UnixTime is a date of creation in unixtime format int64 UnixTime = 1; // Epoch is a date of creation in NeoFS epochs diff --git a/object/types_test.go b/object/types_test.go new file mode 100644 index 0000000..3f9292d --- /dev/null +++ b/object/types_test.go @@ -0,0 +1,201 @@ +package object + +import ( + "bytes" + "testing" + + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-api-go/storagegroup" + "github.com/nspcc-dev/neofs-crypto/test" + "github.com/stretchr/testify/require" +) + +func TestStringify(t *testing.T) { + res := ` +Object: + SystemHeader: + - ID=7e0b9c6c-aabc-4985-949e-2680e577b48b + - CID=11111111111111111111111111111111 + - OwnerID=ALYeYC41emF6MrmUMc4a8obEPdgFhq9ran + - Version=1 + - PayloadLength=1 + - CreatedAt={UnixTime=1 Epoch=1} + UserHeaders: + - Type=Link + Value={Type=Child ID=7e0b9c6c-aabc-4985-949e-2680e577b48b} + - Type=Redirect + Value={CID=11111111111111111111111111111111 OID=7e0b9c6c-aabc-4985-949e-2680e577b48b} + - Type=UserHeader + Value={Key=test_key Val=test_value} + - Type=Transform + Value=Split + - Type=Tombstone + Value=MARKED + - Type=Token + Value={ID=7e0b9c6c-aabc-4985-949e-2680e577b48b OwnerID=ALYeYC41emF6MrmUMc4a8obEPdgFhq9ran Verb=Search Address=11111111111111111111111111111111/7e0b9c6c-aabc-4985-949e-2680e577b48b Created=1 ValidUntil=2 SessionKey=010203040506 Signature=010203040506} + - Type=HomoHash + Value=1111111111111111111111111111111111111111111111111111111111111111 + - Type=PayloadChecksum + Value=[1 2 3 4 5 6] + - Type=Integrity + Value={Checksum=010203040506 Signature=010203040506} + - Type=StorageGroup + Value={DataSize=5 Hash=31313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131 Lifetime={Unit=UnixTime Value=555}} + - Type=PublicKey + Value=[1 2 3 4 5 6] + Payload: []byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7} +` + + key := test.DecodeKey(0) + + uid, err := refs.NewOwnerID(&key.PublicKey) + require.NoError(t, err) + + var oid refs.UUID + + require.NoError(t, oid.Parse("7e0b9c6c-aabc-4985-949e-2680e577b48b")) + + obj := &Object{ + SystemHeader: SystemHeader{ + Version: 1, + PayloadLength: 1, + ID: oid, + OwnerID: uid, + CID: CID{}, + CreatedAt: CreationPoint{ + UnixTime: 1, + Epoch: 1, + }, + }, + Payload: []byte{1, 2, 3, 4, 5, 6, 7}, + } + + // *Header_Link + obj.Headers = append(obj.Headers, Header{ + Value: &Header_Link{ + Link: &Link{ID: oid, Type: Link_Child}, + }, + }) + + // *Header_Redirect + obj.Headers = append(obj.Headers, Header{ + Value: &Header_Redirect{ + Redirect: &Address{ObjectID: oid, CID: CID{}}, + }, + }) + + // *Header_UserHeader + obj.Headers = append(obj.Headers, Header{ + Value: &Header_UserHeader{ + UserHeader: &UserHeader{ + Key: "test_key", + Value: "test_value", + }, + }, + }) + + // *Header_Transform + obj.Headers = append(obj.Headers, Header{ + Value: &Header_Transform{ + Transform: &Transform{ + Type: Transform_Split, + }, + }, + }) + + // *Header_Tombstone + obj.Headers = append(obj.Headers, Header{ + Value: &Header_Tombstone{ + Tombstone: &Tombstone{}, + }, + }) + + token := new(Token) + token.SetID(oid) + token.SetOwnerID(uid) + token.SetVerb(service.Token_Info_Search) + token.SetAddress(Address{ObjectID: oid, CID: refs.CID{}}) + token.SetCreationEpoch(1) + token.SetExpirationEpoch(2) + token.SetSessionKey([]byte{1, 2, 3, 4, 5, 6}) + token.SetSignature([]byte{1, 2, 3, 4, 5, 6}) + + // *Header_Token + obj.Headers = append(obj.Headers, Header{ + Value: &Header_Token{ + Token: token, + }, + }) + + // *Header_HomoHash + obj.Headers = append(obj.Headers, Header{ + Value: &Header_HomoHash{ + HomoHash: Hash{}, + }, + }) + + // *Header_PayloadChecksum + obj.Headers = append(obj.Headers, Header{ + Value: &Header_PayloadChecksum{ + PayloadChecksum: []byte{1, 2, 3, 4, 5, 6}, + }, + }) + + // *Header_Integrity + obj.Headers = append(obj.Headers, Header{ + Value: &Header_Integrity{ + Integrity: &IntegrityHeader{ + HeadersChecksum: []byte{1, 2, 3, 4, 5, 6}, + ChecksumSignature: []byte{1, 2, 3, 4, 5, 6}, + }, + }, + }) + + // *Header_StorageGroup + obj.Headers = append(obj.Headers, Header{ + Value: &Header_StorageGroup{ + StorageGroup: &storagegroup.StorageGroup{ + ValidationDataSize: 5, + ValidationHash: storagegroup.Hash{}, + Lifetime: &storagegroup.StorageGroup_Lifetime{ + Unit: storagegroup.StorageGroup_Lifetime_UnixTime, + Value: 555, + }, + }, + }, + }) + + // *Header_PublicKey + obj.Headers = append(obj.Headers, Header{ + Value: &Header_PublicKey{ + PublicKey: &PublicKey{Value: []byte{1, 2, 3, 4, 5, 6}}, + }, + }) + + buf := new(bytes.Buffer) + + require.NoError(t, Stringify(buf, obj)) + require.Equal(t, res, buf.String()) +} + +func TestObject_Copy(t *testing.T) { + t.Run("token header", func(t *testing.T) { + token := new(Token) + token.SetID(service.TokenID{1, 2, 3}) + + obj := new(Object) + + obj.AddHeader(&Header{ + Value: &Header_Token{ + Token: token, + }, + }) + + cp := obj.Copy() + + _, h := cp.LastHeader(HeaderType(TokenHdr)) + require.NotNil(t, h) + require.Equal(t, token, h.GetValue().(*Header_Token).Token) + }) +} diff --git a/object/utils.go b/object/utils.go index 07f0984..33423aa 100644 --- a/object/utils.go +++ b/object/utils.go @@ -4,7 +4,6 @@ import ( "io" "strconv" - "github.com/nspcc-dev/neofs-api-go/session" "github.com/pkg/errors" ) @@ -46,11 +45,10 @@ func (b ByteSize) String() string { // MakePutRequestHeader combines object and session token value // into header of object put request. -func MakePutRequestHeader(obj *Object, token *session.Token) *PutRequest { +func MakePutRequestHeader(obj *Object) *PutRequest { return &PutRequest{ R: &PutRequest_Header{Header: &PutRequest_PutHeader{ Object: obj, - Token: token, }}, } } diff --git a/object/verification.go b/object/verification.go index a00b30a..0bcbc7c 100644 --- a/object/verification.go +++ b/object/verification.go @@ -77,7 +77,7 @@ func (m Object) Verify() error { integrity := ih.Value.(*Header_Integrity).Integrity // Prepare structures - _, vh := m.LastHeader(HeaderType(VerifyHdr)) + _, vh := m.LastHeader(HeaderType(TokenHdr)) if vh == nil { _, pkh := m.LastHeader(HeaderType(PublicKeyHdr)) if pkh == nil { @@ -85,7 +85,7 @@ func (m Object) Verify() error { } pubkey = pkh.Value.(*Header_PublicKey).PublicKey.Value } else { - pubkey = vh.Value.(*Header_Verify).Verify.PublicKey + pubkey = vh.Value.(*Header_Token).Token.GetSessionKey() } // Verify signature diff --git a/object/verification_test.go b/object/verification_test.go index b37ec70..95a6d32 100644 --- a/object/verification_test.go +++ b/object/verification_test.go @@ -6,7 +6,6 @@ import ( "github.com/google/uuid" "github.com/nspcc-dev/neofs-api-go/container" "github.com/nspcc-dev/neofs-api-go/refs" - "github.com/nspcc-dev/neofs-api-go/session" crypto "github.com/nspcc-dev/neofs-crypto" "github.com/nspcc-dev/neofs-crypto/test" "github.com/stretchr/testify/require" @@ -77,11 +76,10 @@ func TestObject_Verify(t *testing.T) { dataPK := crypto.MarshalPublicKey(&sessionkey.PublicKey) signature, err = crypto.Sign(key, dataPK) - vh := &session.VerificationHeader{ - PublicKey: dataPK, - KeySignature: signature, - } - obj.SetVerificationHeader(vh) + tok := new(Token) + tok.SetSignature(signature) + tok.SetSessionKey(dataPK) + obj.AddHeader(&Header{Value: &Header_Token{Token: tok}}) // validation header is not last t.Run("error validation header is not last", func(t *testing.T) { @@ -90,7 +88,7 @@ func TestObject_Verify(t *testing.T) { }) obj.Headers = obj.Headers[:len(obj.Headers)-2] - obj.SetVerificationHeader(vh) + obj.AddHeader(&Header{Value: &Header_Token{Token: tok}}) obj.SetHeader(&Header{Value: &Header_Integrity{ih}}) t.Run("error invalid header checksum", func(t *testing.T) { @@ -115,7 +113,7 @@ func TestObject_Verify(t *testing.T) { require.NoError(t, err) obj.SetHeader(genIH) - t.Run("correct with vh", func(t *testing.T) { + t.Run("correct with tok", func(t *testing.T) { err = obj.Verify() require.NoError(t, err) }) @@ -123,7 +121,7 @@ func TestObject_Verify(t *testing.T) { pkh := Header{Value: &Header_PublicKey{&PublicKey{ Value: crypto.MarshalPublicKey(&key.PublicKey), }}} - // replace vh with pkh + // replace tok with pkh obj.Headers[len(obj.Headers)-2] = pkh // re-sign object obj.Sign(sessionkey) diff --git a/refs/types.go b/refs/types.go index 117aa03..417eec3 100644 --- a/refs/types.go +++ b/refs/types.go @@ -37,6 +37,23 @@ 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 { + OwnerIDSource + SetOwnerID(OwnerID) +} + +// AddressContainer is an interface of the container of object address value. +type AddressContainer interface { + GetAddress() Address + SetAddress(Address) +} + const ( // UUIDSize contains size of UUID. UUIDSize = 16 diff --git a/service/alias.go b/service/alias.go new file mode 100644 index 0000000..9a40702 --- /dev/null +++ b/service/alias.go @@ -0,0 +1,20 @@ +package service + +import ( + "github.com/nspcc-dev/neofs-api-go/refs" +) + +// TokenID is a type alias of UUID ref. +type TokenID = refs.UUID + +// OwnerID is a type alias of OwnerID ref. +type OwnerID = refs.OwnerID + +// Address is a type alias of Address ref. +type Address = refs.Address + +// AddressContainer is a type alias of refs.AddressContainer. +type AddressContainer = refs.AddressContainer + +// OwnerIDContainer is a type alias of refs.OwnerIDContainer. +type OwnerIDContainer = refs.OwnerIDContainer diff --git a/service/epoch.go b/service/epoch.go new file mode 100644 index 0000000..7a7a556 --- /dev/null +++ b/service/epoch.go @@ -0,0 +1,11 @@ +package service + +// SetEpoch is an Epoch field setter. +func (m *ResponseMetaHeader) SetEpoch(v uint64) { + m.Epoch = v +} + +// SetEpoch is an Epoch field setter. +func (m *RequestMetaHeader) SetEpoch(v uint64) { + m.Epoch = v +} diff --git a/service/epoch_test.go b/service/epoch_test.go new file mode 100644 index 0000000..47316c0 --- /dev/null +++ b/service/epoch_test.go @@ -0,0 +1,21 @@ +package service + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetSetEpoch(t *testing.T) { + v := uint64(5) + + items := []EpochContainer{ + new(ResponseMetaHeader), + new(RequestMetaHeader), + } + + for _, item := range items { + item.SetEpoch(v) + require.Equal(t, v, item.GetEpoch()) + } +} diff --git a/service/errors.go b/service/errors.go new file mode 100644 index 0000000..f3a0dfc --- /dev/null +++ b/service/errors.go @@ -0,0 +1,49 @@ +package service + +import "github.com/nspcc-dev/neofs-api-go/internal" + +// ErrNilToken is returned by functions that expect +// a non-nil token argument, but received nil. +const ErrNilToken = internal.Error("token is nil") + +// ErrInvalidTTL means that the TTL value does not +// satisfy a specific criterion. +const ErrInvalidTTL = internal.Error("invalid TTL value") + +// ErrInvalidPublicKeyBytes means that the public key could not be unmarshaled. +const ErrInvalidPublicKeyBytes = internal.Error("cannot load public key") + +// ErrCannotFindOwner is raised when signatures empty in GetOwner. +const ErrCannotFindOwner = internal.Error("cannot find owner public key") + +// ErrWrongOwner is raised when passed OwnerID +// not equal to present PublicKey +const ErrWrongOwner = internal.Error("wrong owner") + +// ErrNilSignedDataSource returned by functions that expect a non-nil +// SignedDataSource, but received nil. +const ErrNilSignedDataSource = internal.Error("signed data source is nil") + +// ErrNilSignatureKeySource is returned by functions that expect a non-nil +// SignatureKeySource, but received nil. +const ErrNilSignatureKeySource = internal.Error("empty key-signature source") + +// ErrEmptyDataWithSignature is returned by functions that expect +// a non-nil DataWithSignature, but received nil. +const ErrEmptyDataWithSignature = internal.Error("empty data with signature") + +// ErrNegativeLength is returned by functions that received +// negative length for slice allocation. +const ErrNegativeLength = internal.Error("negative slice length") + +// ErrNilDataWithTokenSignAccumulator is returned by functions that expect +// a non-nil DataWithTokenSignAccumulator, but received nil. +const ErrNilDataWithTokenSignAccumulator = internal.Error("signed data with token is nil") + +// ErrNilSignatureKeySourceWithToken is returned by functions that expect +// a non-nil SignatureKeySourceWithToken, but received nil. +const ErrNilSignatureKeySourceWithToken = internal.Error("key-signature source with token is nil") + +// ErrNilSignedDataReader is returned by functions that expect +// a non-nil SignedDataReader, but received nil. +const ErrNilSignedDataReader = internal.Error("signed data reader is nil") diff --git a/service/meta.go b/service/meta.go index 5e9886d..3f01758 100644 --- a/service/meta.go +++ b/service/meta.go @@ -1,127 +1,13 @@ package service -import ( - "github.com/nspcc-dev/neofs-api-go/internal" - "github.com/pkg/errors" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -type ( - // MetaHeader contains meta information of request. - // It provides methods to get or set meta information meta header. - // Also contains methods to reset and restore meta header. - // Also contains methods to get or set request protocol version - MetaHeader interface { - ResetMeta() RequestMetaHeader - RestoreMeta(RequestMetaHeader) - - // TTLRequest to verify and update ttl requests. - GetTTL() uint32 - SetTTL(uint32) - - // EpochHeader gives possibility to get or set epoch in RPC Requests. - EpochHeader - - // VersionHeader allows get or set version of protocol request - VersionHeader - } - - // EpochHeader interface gives possibility to get or set epoch in RPC Requests. - EpochHeader interface { - GetEpoch() uint64 - SetEpoch(v uint64) - } - - // VersionHeader allows get or set version of protocol request - VersionHeader interface { - GetVersion() uint32 - SetVersion(uint32) - } - - // TTLCondition is closure, that allows to validate request with ttl. - TTLCondition func(ttl uint32) error -) - -const ( - // ZeroTTL is empty ttl, should produce ErrZeroTTL. - ZeroTTL = iota - - // NonForwardingTTL is a ttl that allows direct connections only. - NonForwardingTTL - - // SingleForwardingTTL is a ttl that allows connections through another node. - SingleForwardingTTL -) - -const ( - // ErrZeroTTL is raised when zero ttl is passed. - ErrZeroTTL = internal.Error("zero ttl") - - // ErrIncorrectTTL is raised when NonForwardingTTL is passed and NodeRole != InnerRingNode. - ErrIncorrectTTL = internal.Error("incorrect ttl") -) - -// SetVersion sets protocol version to ResponseMetaHeader. -func (m *ResponseMetaHeader) SetVersion(v uint32) { m.Version = v } - -// SetEpoch sets Epoch to ResponseMetaHeader. -func (m *ResponseMetaHeader) SetEpoch(v uint64) { m.Epoch = v } - -// SetVersion sets protocol version to RequestMetaHeader. -func (m *RequestMetaHeader) SetVersion(v uint32) { m.Version = v } - -// SetTTL sets TTL to RequestMetaHeader. -func (m *RequestMetaHeader) SetTTL(v uint32) { m.TTL = v } - -// SetEpoch sets Epoch to RequestMetaHeader. -func (m *RequestMetaHeader) SetEpoch(v uint64) { m.Epoch = v } - -// ResetMeta returns current value and sets RequestMetaHeader to empty value. -func (m *RequestMetaHeader) ResetMeta() RequestMetaHeader { +// CutMeta returns current value and sets RequestMetaHeader to empty value. +func (m *RequestMetaHeader) CutMeta() RequestMetaHeader { cp := *m m.Reset() return cp } // RestoreMeta sets current RequestMetaHeader to passed value. -func (m *RequestMetaHeader) RestoreMeta(v RequestMetaHeader) { *m = v } - -// IRNonForwarding condition that allows NonForwardingTTL only for IR -func IRNonForwarding(role NodeRole) TTLCondition { - return func(ttl uint32) error { - if ttl == NonForwardingTTL && role != InnerRingNode { - return ErrIncorrectTTL - } - - return nil - } -} - -// ProcessRequestTTL validates and update ttl requests. -func ProcessRequestTTL(req MetaHeader, cond ...TTLCondition) error { - ttl := req.GetTTL() - - if ttl == ZeroTTL { - return status.New(codes.InvalidArgument, ErrZeroTTL.Error()).Err() - } - - for i := range cond { - if cond[i] == nil { - continue - } - - // check specific condition: - if err := cond[i](ttl); err != nil { - if st, ok := status.FromError(errors.Cause(err)); ok { - return st.Err() - } - - return status.New(codes.InvalidArgument, err.Error()).Err() - } - } - - req.SetTTL(ttl - 1) - - return nil +func (m *RequestMetaHeader) RestoreMeta(v RequestMetaHeader) { + *m = v } diff --git a/service/meta.pb.go b/service/meta.pb.go index 8039990..25a5469 100644 Binary files a/service/meta.pb.go and b/service/meta.pb.go differ diff --git a/service/meta.proto b/service/meta.proto index 99b37d3..093f118 100644 --- a/service/meta.proto +++ b/service/meta.proto @@ -17,6 +17,8 @@ message RequestMetaHeader { // Version defines protocol version // TODO: not used for now, should be implemented in future uint32 Version = 3; + // Raw determines whether the request is raw or not + bool Raw = 4; } // ResponseMetaHeader contains meta information based on request processing by server diff --git a/service/meta_test.go b/service/meta_test.go index 083ccd6..a0b85ef 100644 --- a/service/meta_test.go +++ b/service/meta_test.go @@ -3,102 +3,23 @@ package service import ( "testing" - "github.com/pkg/errors" "github.com/stretchr/testify/require" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) -type mockedRequest struct { - msg string - name string - code codes.Code - handler TTLCondition - RequestMetaHeader -} - -func TestMetaRequest(t *testing.T) { - tests := []mockedRequest{ - { - name: "direct to ir node", - handler: IRNonForwarding(InnerRingNode), - RequestMetaHeader: RequestMetaHeader{TTL: NonForwardingTTL}, - }, - { - code: codes.InvalidArgument, - msg: ErrIncorrectTTL.Error(), - name: "direct to storage node", - handler: IRNonForwarding(StorageNode), - RequestMetaHeader: RequestMetaHeader{TTL: NonForwardingTTL}, - }, - { - msg: ErrZeroTTL.Error(), - code: codes.InvalidArgument, - name: "zero ttl", - handler: IRNonForwarding(StorageNode), - RequestMetaHeader: RequestMetaHeader{TTL: ZeroTTL}, - }, - { - name: "default to ir node", - handler: IRNonForwarding(InnerRingNode), - RequestMetaHeader: RequestMetaHeader{TTL: SingleForwardingTTL}, - }, - { - name: "default to storage node", - handler: IRNonForwarding(StorageNode), - RequestMetaHeader: RequestMetaHeader{TTL: SingleForwardingTTL}, - }, - { - msg: "not found", - code: codes.NotFound, - name: "custom status error", - RequestMetaHeader: RequestMetaHeader{TTL: SingleForwardingTTL}, - handler: func(_ uint32) error { return status.Error(codes.NotFound, "not found") }, - }, - { - msg: "not found", - code: codes.NotFound, - name: "custom wrapped status error", - RequestMetaHeader: RequestMetaHeader{TTL: SingleForwardingTTL}, - handler: func(_ uint32) error { - err := status.Error(codes.NotFound, "not found") - err = errors.Wrap(err, "some error context") - err = errors.Wrap(err, "another error context") - return err - }, +func TestCutRestoreMeta(t *testing.T) { + items := []func() SeizedMetaHeaderContainer{ + func() SeizedMetaHeaderContainer { + m := new(RequestMetaHeader) + m.SetEpoch(1) + return m }, } - for i := range tests { - tt := tests[i] - t.Run(tt.name, func(t *testing.T) { - before := tt.GetTTL() - err := ProcessRequestTTL(&tt, tt.handler) - if tt.msg != "" { - require.Errorf(t, err, tt.msg) + for _, item := range items { + v1 := item() + m1 := v1.CutMeta() + v1.RestoreMeta(m1) - state, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, tt.code, state.Code()) - require.Equal(t, tt.msg, state.Message()) - } else { - require.NoError(t, err) - require.NotEqualf(t, before, tt.GetTTL(), "ttl should be changed: %d vs %d", before, tt.GetTTL()) - } - }) + require.Equal(t, item(), v1) } } - -func TestRequestMetaHeader_SetEpoch(t *testing.T) { - m := new(ResponseMetaHeader) - epoch := uint64(3) - m.SetEpoch(epoch) - require.Equal(t, epoch, m.GetEpoch()) -} - -func TestRequestMetaHeader_SetVersion(t *testing.T) { - m := new(ResponseMetaHeader) - version := uint32(3) - m.SetVersion(version) - require.Equal(t, version, m.GetVersion()) -} diff --git a/service/raw.go b/service/raw.go new file mode 100644 index 0000000..0bb4b27 --- /dev/null +++ b/service/raw.go @@ -0,0 +1,6 @@ +package service + +// SetRaw is a Raw field setter. +func (m *RequestMetaHeader) SetRaw(raw bool) { + m.Raw = raw +} diff --git a/service/raw_test.go b/service/raw_test.go new file mode 100644 index 0000000..ad595ed --- /dev/null +++ b/service/raw_test.go @@ -0,0 +1,24 @@ +package service + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetSetRaw(t *testing.T) { + items := []RawContainer{ + new(RequestMetaHeader), + } + + for _, item := range items { + // init with false + item.SetRaw(false) + + item.SetRaw(true) + require.True(t, item.GetRaw()) + + item.SetRaw(false) + require.False(t, item.GetRaw()) + } +} diff --git a/service/role.go b/service/role.go index 53bcdf5..64a0074 100644 --- a/service/role.go +++ b/service/role.go @@ -1,7 +1,6 @@ package service -// NodeRole to identify in Bootstrap service. -type NodeRole int32 +import "encoding/binary" const ( _ NodeRole = iota @@ -22,3 +21,17 @@ func (nt NodeRole) String() string { return "Unknown" } } + +// Size returns the size necessary for a binary representation of the NodeRole. +func (nt NodeRole) Size() int { + return 4 +} + +// Bytes returns a binary representation of the NodeRole. +func (nt NodeRole) Bytes() []byte { + data := make([]byte, nt.Size()) + + binary.BigEndian.PutUint32(data, uint32(nt)) + + return data +} diff --git a/service/sign.go b/service/sign.go new file mode 100644 index 0000000..5b1548f --- /dev/null +++ b/service/sign.go @@ -0,0 +1,229 @@ +package service + +import ( + "crypto/ecdsa" + "sync" + + crypto "github.com/nspcc-dev/neofs-crypto" +) + +type keySign struct { + key *ecdsa.PublicKey + sign []byte +} + +var bytesPool = sync.Pool{ + New: func() interface{} { + return make([]byte, 5<<20) + }, +} + +// GetSignature is a sign field getter. +func (s keySign) GetSignature() []byte { + return s.sign +} + +// GetPublicKey is a key field getter, +func (s keySign) GetPublicKey() *ecdsa.PublicKey { + return s.key +} + +// Unites passed key with signature and returns SignKeyPair interface. +func newSignatureKeyPair(key *ecdsa.PublicKey, sign []byte) SignKeyPair { + return &keySign{ + key: key, + sign: sign, + } +} + +// Returns data from DataSignatureAccumulator for signature creation/verification. +// +// If passed DataSignatureAccumulator provides a SignedDataReader interface, data for signature is obtained +// using this interface for optimization. In this case, it is understood that reading into the slice D +// that the method DataForSignature returns does not change D. +// +// If returned length of data is negative, ErrNegativeLength returns. +func dataForSignature(src SignedDataSource) ([]byte, error) { + if src == nil { + return nil, ErrNilSignedDataSource + } + + r, ok := src.(SignedDataReader) + if !ok { + return src.SignedData() + } + + buf := bytesPool.Get().([]byte) + + if size := r.SignedDataSize(); size < 0 { + return nil, ErrNegativeLength + } else if size <= cap(buf) { + buf = buf[:size] + } else { + buf = make([]byte, size) + } + + n, err := r.ReadSignedData(buf) + if err != nil { + return nil, err + } + + return buf[:n], nil + +} + +// DataSignature returns the signature of data obtained using the private key. +// +// If passed data container is nil, ErrNilSignedDataSource returns. +// If passed private key is nil, crypto.ErrEmptyPrivateKey returns. +// If the data container or the signature function returns an error, it is returned directly. +func DataSignature(key *ecdsa.PrivateKey, src SignedDataSource) ([]byte, error) { + if key == nil { + return nil, crypto.ErrEmptyPrivateKey + } + + data, err := dataForSignature(src) + if err != nil { + return nil, err + } + defer bytesPool.Put(data) + + return crypto.Sign(key, data) +} + +// AddSignatureWithKey calculates the data signature and adds it to accumulator with public key. +// +// Any change of data provoke signature breakdown. +// +// Returns signing errors only. +func AddSignatureWithKey(key *ecdsa.PrivateKey, v DataWithSignKeyAccumulator) error { + sign, err := DataSignature(key, v) + if err != nil { + return err + } + + v.AddSignKey(sign, &key.PublicKey) + + return nil +} + +// Checks passed key-signature pairs for data from the passed container. +// +// If passed key-signatures pair set is empty, nil returns immediately. +func verifySignatures(src SignedDataSource, items ...SignKeyPair) error { + if len(items) <= 0 { + return nil + } + + data, err := dataForSignature(src) + if err != nil { + return err + } + defer bytesPool.Put(data) + + for _, signKey := range items { + if err := crypto.Verify( + signKey.GetPublicKey(), + data, + signKey.GetSignature(), + ); err != nil { + return err + } + } + + return nil +} + +// VerifySignatures checks passed key-signature pairs for data from the passed container. +// +// If passed data source is nil, ErrNilSignedDataSource returns. +// If check data is not ready, corresponding error returns. +// If at least one of the pairs is invalid, an error returns. +func VerifySignatures(src SignedDataSource, items ...SignKeyPair) error { + return verifySignatures(src, items...) +} + +// VerifyAccumulatedSignatures checks if accumulated key-signature pairs are valid. +// +// Behaves like VerifySignatures. +// If passed key-signature source is empty, ErrNilSignatureKeySource returns. +func VerifyAccumulatedSignatures(src DataWithSignKeySource) error { + if src == nil { + return ErrNilSignatureKeySource + } + + return verifySignatures(src, src.GetSignKeyPairs()...) +} + +// VerifySignatureWithKey checks data signature from the passed container with passed key. +// +// If passed data with signature is nil, ErrEmptyDataWithSignature returns. +// If passed key is nil, crypto.ErrEmptyPublicKey returns. +// A non-nil error returns if and only if the signature does not pass verification. +func VerifySignatureWithKey(key *ecdsa.PublicKey, src DataWithSignature) error { + if src == nil { + return ErrEmptyDataWithSignature + } else if key == nil { + return crypto.ErrEmptyPublicKey + } + + return verifySignatures( + src, + newSignatureKeyPair( + key, + src.GetSignature(), + ), + ) +} + +// SignDataWithSessionToken calculates data with token signature and adds it to accumulator. +// +// Any change of data or session token info provoke signature breakdown. +// +// If passed private key is nil, crypto.ErrEmptyPrivateKey returns. +// If passed DataWithTokenSignAccumulator is nil, ErrNilDataWithTokenSignAccumulator returns. +func SignDataWithSessionToken(key *ecdsa.PrivateKey, src DataWithTokenSignAccumulator) error { + if src == nil { + return ErrNilDataWithTokenSignAccumulator + } else if r, ok := src.(SignedDataReader); ok { + return AddSignatureWithKey(key, &signDataReaderWithToken{ + SignedDataSource: src, + SignKeyPairAccumulator: src, + + rdr: r, + token: src.GetSessionToken(), + }, + ) + } + + return AddSignatureWithKey(key, &signAccumWithToken{ + SignedDataSource: src, + SignKeyPairAccumulator: src, + + token: src.GetSessionToken(), + }) +} + +// VerifyAccumulatedSignaturesWithToken checks if accumulated key-signature pairs of data with token are valid. +// +// If passed DataWithTokenSignSource is nil, ErrNilSignatureKeySourceWithToken returns. +func VerifyAccumulatedSignaturesWithToken(src DataWithTokenSignSource) error { + if src == nil { + return ErrNilSignatureKeySourceWithToken + } else if r, ok := src.(SignedDataReader); ok { + return VerifyAccumulatedSignatures(&signDataReaderWithToken{ + SignedDataSource: src, + SignKeyPairSource: src, + + rdr: r, + token: src.GetSessionToken(), + }) + } + + return VerifyAccumulatedSignatures(&signAccumWithToken{ + SignedDataSource: src, + SignKeyPairSource: src, + + token: src.GetSessionToken(), + }) +} diff --git a/service/sign_test.go b/service/sign_test.go new file mode 100644 index 0000000..5cb7c40 --- /dev/null +++ b/service/sign_test.go @@ -0,0 +1,326 @@ +package service + +import ( + "crypto/ecdsa" + "crypto/rand" + "errors" + "io" + "testing" + + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/nspcc-dev/neofs-crypto/test" + "github.com/stretchr/testify/require" +) + +type testSignedDataSrc struct { + err error + data []byte + sig []byte + key *ecdsa.PublicKey + token SessionToken +} + +type testSignedDataReader struct { + *testSignedDataSrc +} + +func (s testSignedDataSrc) GetSignature() []byte { + return s.sig +} + +func (s testSignedDataSrc) GetSignKeyPairs() []SignKeyPair { + return []SignKeyPair{ + newSignatureKeyPair(s.key, s.sig), + } +} + +func (s testSignedDataSrc) SignedData() ([]byte, error) { + return s.data, s.err +} + +func (s *testSignedDataSrc) AddSignKey(sig []byte, key *ecdsa.PublicKey) { + s.key = key + s.sig = sig +} + +func testData(t *testing.T, sz int) []byte { + d := make([]byte, sz) + _, err := rand.Read(d) + require.NoError(t, err) + return d +} + +func (s testSignedDataSrc) GetSessionToken() SessionToken { + return s.token +} + +func (s testSignedDataReader) SignedDataSize() int { + return len(s.data) +} + +func (s testSignedDataReader) ReadSignedData(buf []byte) (int, error) { + if s.err != nil { + return 0, s.err + } + + var err error + if len(buf) < len(s.data) { + err = io.ErrUnexpectedEOF + } + return copy(buf, s.data), err +} + +func TestDataSignature(t *testing.T) { + var err error + + // nil private key + _, err = DataSignature(nil, nil) + require.EqualError(t, err, crypto.ErrEmptyPrivateKey.Error()) + + // create test private key + sk := test.DecodeKey(0) + + // nil private key + _, err = DataSignature(sk, nil) + require.EqualError(t, err, ErrNilSignedDataSource.Error()) + + t.Run("common signed data source", func(t *testing.T) { + // create test data source + src := &testSignedDataSrc{ + data: testData(t, 10), + } + + // create custom error for data source + src.err = errors.New("test error for data source") + + _, err = DataSignature(sk, src) + require.EqualError(t, err, src.err.Error()) + + // reset error to nil + src.err = nil + + // calculate data signature + sig, err := DataSignature(sk, src) + require.NoError(t, err) + + // ascertain that the signature passes verification + require.NoError(t, crypto.Verify(&sk.PublicKey, src.data, sig)) + }) + + t.Run("signed data reader", func(t *testing.T) { + // create test signed data reader + src := &testSignedDataSrc{ + data: testData(t, 10), + } + + // create custom error for signed data reader + src.err = errors.New("test error for signed data reader") + + sig, err := DataSignature(sk, src) + require.EqualError(t, err, src.err.Error()) + + // reset error to nil + src.err = nil + + // calculate data signature + sig, err = DataSignature(sk, src) + require.NoError(t, err) + + // ascertain that the signature passes verification + require.NoError(t, crypto.Verify(&sk.PublicKey, src.data, sig)) + }) +} + +func TestAddSignatureWithKey(t *testing.T) { + require.NoError(t, + AddSignatureWithKey( + test.DecodeKey(0), + &testSignedDataSrc{ + data: testData(t, 10), + }, + ), + ) +} + +func TestVerifySignatures(t *testing.T) { + // empty signatures + require.NoError(t, VerifySignatures(nil)) + + // create test signature source + src := &testSignedDataSrc{ + data: testData(t, 10), + } + + // create private key for test + sk := test.DecodeKey(0) + + // calculate a signature of the data + sig, err := crypto.Sign(sk, src.data) + require.NoError(t, err) + + // ascertain that verification is passed + require.NoError(t, + VerifySignatures( + src, + newSignatureKeyPair(&sk.PublicKey, sig), + ), + ) + + // break the signature + sig[0]++ + + require.Error(t, + VerifySignatures( + src, + newSignatureKeyPair(&sk.PublicKey, sig), + ), + ) + + // restore the signature + sig[0]-- + + // empty data source + require.EqualError(t, + VerifySignatures(nil, nil), + ErrNilSignedDataSource.Error(), + ) + +} + +func TestVerifyAccumulatedSignatures(t *testing.T) { + // nil signature source + require.EqualError(t, + VerifyAccumulatedSignatures(nil), + ErrNilSignatureKeySource.Error(), + ) + + // create test private key + sk := test.DecodeKey(0) + + // create signature source + src := &testSignedDataSrc{ + data: testData(t, 10), + key: &sk.PublicKey, + } + + var err error + + // calculate a signature + src.sig, err = crypto.Sign(sk, src.data) + require.NoError(t, err) + + // ascertain that verification is passed + require.NoError(t, VerifyAccumulatedSignatures(src)) + + // break the signature + src.sig[0]++ + + // ascertain that verification is failed + require.Error(t, VerifyAccumulatedSignatures(src)) +} + +func TestVerifySignatureWithKey(t *testing.T) { + // nil signature source + require.EqualError(t, + VerifySignatureWithKey(nil, nil), + ErrEmptyDataWithSignature.Error(), + ) + + // create test signature source + src := &testSignedDataSrc{ + data: testData(t, 10), + } + + // nil public key + require.EqualError(t, + VerifySignatureWithKey(nil, src), + crypto.ErrEmptyPublicKey.Error(), + ) + + // create test private key + sk := test.DecodeKey(0) + + var err error + + // calculate a signature + src.sig, err = crypto.Sign(sk, src.data) + require.NoError(t, err) + + // ascertain that verification is passed + require.NoError(t, VerifySignatureWithKey(&sk.PublicKey, src)) + + // break the signature + src.sig[0]++ + + // ascertain that verification is failed + require.Error(t, VerifySignatureWithKey(&sk.PublicKey, src)) +} + +func TestSignVerifyDataWithSessionToken(t *testing.T) { + // sign with empty DataWithTokenSignAccumulator + require.EqualError(t, + SignDataWithSessionToken(nil, nil), + ErrNilDataWithTokenSignAccumulator.Error(), + ) + + // verify with empty DataWithTokenSignSource + require.EqualError(t, + VerifyAccumulatedSignaturesWithToken(nil), + ErrNilSignatureKeySourceWithToken.Error(), + ) + + // create test session token + var ( + token = new(Token) + initVerb = Token_Info_Verb(1) + ) + + token.SetVerb(initVerb) + + // create test data with token + src := &testSignedDataSrc{ + data: testData(t, 10), + token: token, + } + + // create test private key + sk := test.DecodeKey(0) + + // sign with private key + require.NoError(t, SignDataWithSessionToken(sk, src)) + + // ascertain that verification is passed + require.NoError(t, VerifyAccumulatedSignaturesWithToken(src)) + + // break the data + src.data[0]++ + + // ascertain that verification is failed + require.Error(t, VerifyAccumulatedSignaturesWithToken(src)) + + // restore the data + src.data[0]-- + + // break the token + token.SetVerb(initVerb + 1) + + // ascertain that verification is failed + require.Error(t, VerifyAccumulatedSignaturesWithToken(src)) + + // restore the token + token.SetVerb(initVerb) + + // ascertain that verification is passed + require.NoError(t, VerifyAccumulatedSignaturesWithToken(src)) + + // wrap to data reader + rdr := &testSignedDataReader{ + testSignedDataSrc: src, + } + + // sign with private key + require.NoError(t, SignDataWithSessionToken(sk, rdr)) + + // ascertain that verification is passed + require.NoError(t, VerifyAccumulatedSignaturesWithToken(rdr)) +} diff --git a/service/token.go b/service/token.go new file mode 100644 index 0000000..32c390f --- /dev/null +++ b/service/token.go @@ -0,0 +1,231 @@ +package service + +import ( + "crypto/ecdsa" + "encoding/binary" + "io" + + "github.com/nspcc-dev/neofs-api-go/refs" +) + +type signAccumWithToken struct { + SignedDataSource + SignKeyPairAccumulator + SignKeyPairSource + + token SessionToken +} + +type signDataReaderWithToken struct { + SignedDataSource + SignKeyPairAccumulator + SignKeyPairSource + + rdr SignedDataReader + + token SessionToken +} + +const verbSize = 4 + +const fixedTokenDataSize = 0 + + refs.UUIDSize + + refs.OwnerIDSize + + verbSize + + refs.UUIDSize + + refs.CIDSize + + 8 + + 8 + +var tokenEndianness = binary.BigEndian + +// GetID is an ID field getter. +func (m Token_Info) GetID() TokenID { + return m.ID +} + +// SetID is an ID field setter. +func (m *Token_Info) SetID(id TokenID) { + m.ID = id +} + +// GetOwnerID is an OwnerID field getter. +func (m Token_Info) GetOwnerID() OwnerID { + return m.OwnerID +} + +// SetOwnerID is an OwnerID field setter. +func (m *Token_Info) SetOwnerID(id OwnerID) { + m.OwnerID = id +} + +// SetVerb is a Verb field setter. +func (m *Token_Info) SetVerb(verb Token_Info_Verb) { + m.Verb = verb +} + +// GetAddress is an Address field getter. +func (m Token_Info) GetAddress() Address { + return m.Address +} + +// SetAddress is an Address field setter. +func (m *Token_Info) SetAddress(addr Address) { + m.Address = addr +} + +// CreationEpoch is a Created field getter. +func (m TokenLifetime) CreationEpoch() uint64 { + return m.Created +} + +// SetCreationEpoch is a Created field setter. +func (m *TokenLifetime) SetCreationEpoch(e uint64) { + m.Created = e +} + +// ExpirationEpoch is a ValidUntil field getter. +func (m TokenLifetime) ExpirationEpoch() uint64 { + return m.ValidUntil +} + +// SetExpirationEpoch is a ValidUntil field setter. +func (m *TokenLifetime) SetExpirationEpoch(e uint64) { + m.ValidUntil = e +} + +// SetSessionKey is a SessionKey field setter. +func (m *Token_Info) SetSessionKey(key []byte) { + m.SessionKey = key +} + +// SetSignature is a Signature field setter. +func (m *Token) SetSignature(sig []byte) { + m.Signature = sig +} + +// Size returns the size of a binary representation of the verb. +func (x Token_Info_Verb) Size() int { + return verbSize +} + +// Bytes returns a binary representation of the verb. +func (x Token_Info_Verb) Bytes() []byte { + data := make([]byte, verbSize) + tokenEndianness.PutUint32(data, uint32(x)) + return data +} + +// AddSignKey calls a Signature field setter with passed signature. +func (m *Token) AddSignKey(sig []byte, _ *ecdsa.PublicKey) { + m.SetSignature(sig) +} + +// SignedData returns token information in a binary representation. +func (m *Token) SignedData() ([]byte, error) { + return SignedDataFromReader(m) +} + +// ReadSignedData copies a binary representation of the token information to passed buffer. +// +// If buffer length is less than required, io.ErrUnexpectedEOF returns. +func (m *Token_Info) ReadSignedData(p []byte) (int, error) { + sz := m.SignedDataSize() + if len(p) < sz { + return 0, io.ErrUnexpectedEOF + } + + copyTokenSignedData(p, m) + + return sz, nil +} + +// SignedDataSize returns the length of signed token information slice. +func (m *Token_Info) SignedDataSize() int { + return tokenInfoSize(m) +} + +func tokenInfoSize(v SessionKeySource) int { + if v == nil { + return 0 + } + return fixedTokenDataSize + len(v.GetSessionKey()) +} + +// Fills passed buffer with signing token information bytes. +// Does not check buffer length, it is understood that enough space is allocated in it. +// +// If passed SessionTokenInfo, buffer remains unchanged. +func copyTokenSignedData(buf []byte, token SessionTokenInfo) { + if token == nil { + return + } + + var off int + + off += copy(buf[off:], token.GetID().Bytes()) + + off += copy(buf[off:], token.GetOwnerID().Bytes()) + + off += copy(buf[off:], token.GetVerb().Bytes()) + + addr := token.GetAddress() + off += copy(buf[off:], addr.CID.Bytes()) + off += copy(buf[off:], addr.ObjectID.Bytes()) + + tokenEndianness.PutUint64(buf[off:], token.CreationEpoch()) + off += 8 + + tokenEndianness.PutUint64(buf[off:], token.ExpirationEpoch()) + off += 8 + + copy(buf[off:], token.GetSessionKey()) +} + +// SignedData concatenates signed data with session token information. Returns concatenation result. +// +// Token bytes are added if and only if token is not nil. +func (s signAccumWithToken) SignedData() ([]byte, error) { + data, err := s.SignedDataSource.SignedData() + if err != nil { + return nil, err + } + + tokenData := make([]byte, tokenInfoSize(s.token)) + + copyTokenSignedData(tokenData, s.token) + + return append(data, tokenData...), nil +} + +func (s signDataReaderWithToken) SignedDataSize() int { + sz := s.rdr.SignedDataSize() + if sz < 0 { + return -1 + } + + sz += tokenInfoSize(s.token) + + return sz +} + +func (s signDataReaderWithToken) ReadSignedData(p []byte) (int, error) { + dataSize := s.rdr.SignedDataSize() + if dataSize < 0 { + return 0, ErrNegativeLength + } + + sumSize := dataSize + tokenInfoSize(s.token) + + if len(p) < sumSize { + return 0, io.ErrUnexpectedEOF + } + + if n, err := s.rdr.ReadSignedData(p); err != nil { + return n, err + } + + copyTokenSignedData(p[dataSize:], s.token) + + return sumSize, nil +} diff --git a/service/token_test.go b/service/token_test.go new file mode 100644 index 0000000..ce3d2c8 --- /dev/null +++ b/service/token_test.go @@ -0,0 +1,219 @@ +package service + +import ( + "crypto/rand" + "testing" + + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-crypto/test" + "github.com/stretchr/testify/require" +) + +func TestTokenGettersSetters(t *testing.T) { + var tok SessionToken = new(Token) + + { // ID + id, err := refs.NewUUID() + require.NoError(t, err) + + tok.SetID(id) + + require.Equal(t, id, tok.GetID()) + } + + { // OwnerID + ownerID := OwnerID{} + _, err := rand.Read(ownerID[:]) + require.NoError(t, err) + + tok.SetOwnerID(ownerID) + + require.Equal(t, ownerID, tok.GetOwnerID()) + } + + { // Verb + verb := Token_Info_Verb(3) + + tok.SetVerb(verb) + + require.Equal(t, verb, tok.GetVerb()) + } + + { // Address + addr := Address{} + _, err := rand.Read(addr.CID[:]) + require.NoError(t, err) + _, err = rand.Read(addr.ObjectID[:]) + require.NoError(t, err) + + tok.SetAddress(addr) + + require.Equal(t, addr, tok.GetAddress()) + } + + { // Created + e := uint64(5) + + tok.SetCreationEpoch(e) + + require.Equal(t, e, tok.CreationEpoch()) + } + + { // ValidUntil + e := uint64(5) + + tok.SetExpirationEpoch(e) + + require.Equal(t, e, tok.ExpirationEpoch()) + } + + { // SessionKey + key := make([]byte, 10) + _, err := rand.Read(key) + require.NoError(t, err) + + tok.SetSessionKey(key) + + require.Equal(t, key, tok.GetSessionKey()) + } + + { // Signature + sig := make([]byte, 10) + _, err := rand.Read(sig) + require.NoError(t, err) + + tok.SetSignature(sig) + + require.Equal(t, sig, tok.GetSignature()) + } +} + +func TestSignToken(t *testing.T) { + token := new(Token) + + // create private key for signing + sk := test.DecodeKey(0) + pk := &sk.PublicKey + + id := TokenID{} + _, err := rand.Read(id[:]) + require.NoError(t, err) + token.SetID(id) + + ownerID := OwnerID{} + _, err = rand.Read(ownerID[:]) + require.NoError(t, err) + token.SetOwnerID(ownerID) + + verb := Token_Info_Verb(1) + token.SetVerb(verb) + + addr := Address{} + _, err = rand.Read(addr.ObjectID[:]) + require.NoError(t, err) + _, err = rand.Read(addr.CID[:]) + require.NoError(t, err) + token.SetAddress(addr) + + cEpoch := uint64(1) + token.SetCreationEpoch(cEpoch) + + fEpoch := uint64(2) + token.SetExpirationEpoch(fEpoch) + + sessionKey := make([]byte, 10) + _, err = rand.Read(sessionKey[:]) + require.NoError(t, err) + token.SetSessionKey(sessionKey) + + // sign and verify token + require.NoError(t, AddSignatureWithKey(sk, token)) + require.NoError(t, VerifySignatureWithKey(pk, token)) + + items := []struct { + corrupt func() + restore func() + }{ + { // ID + corrupt: func() { + id[0]++ + token.SetID(id) + }, + restore: func() { + id[0]-- + token.SetID(id) + }, + }, + { // Owner ID + corrupt: func() { + ownerID[0]++ + token.SetOwnerID(ownerID) + }, + restore: func() { + ownerID[0]-- + token.SetOwnerID(ownerID) + }, + }, + { // Verb + corrupt: func() { + token.SetVerb(verb + 1) + }, + restore: func() { + token.SetVerb(verb) + }, + }, + { // ObjectID + corrupt: func() { + addr.ObjectID[0]++ + token.SetAddress(addr) + }, + restore: func() { + addr.ObjectID[0]-- + token.SetAddress(addr) + }, + }, + { // CID + corrupt: func() { + addr.CID[0]++ + token.SetAddress(addr) + }, + restore: func() { + addr.CID[0]-- + token.SetAddress(addr) + }, + }, + { // Creation epoch + corrupt: func() { + token.SetCreationEpoch(cEpoch + 1) + }, + restore: func() { + token.SetCreationEpoch(cEpoch) + }, + }, + { // Expiration epoch + corrupt: func() { + token.SetExpirationEpoch(fEpoch + 1) + }, + restore: func() { + token.SetExpirationEpoch(fEpoch) + }, + }, + { // Session key + corrupt: func() { + sessionKey[0]++ + token.SetSessionKey(sessionKey) + }, + restore: func() { + sessionKey[0]-- + token.SetSessionKey(sessionKey) + }, + }, + } + + for _, v := range items { + v.corrupt() + require.Error(t, VerifySignatureWithKey(pk, token)) + v.restore() + require.NoError(t, VerifySignatureWithKey(pk, token)) + } +} diff --git a/service/ttl.go b/service/ttl.go new file mode 100644 index 0000000..28a5092 --- /dev/null +++ b/service/ttl.go @@ -0,0 +1,63 @@ +package service + +import ( + "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// TTL constants. +const ( + // ZeroTTL is an upper bound of invalid TTL values. + ZeroTTL = iota + + // NonForwardingTTL is a TTL value that does not imply a request forwarding. + NonForwardingTTL + + // SingleForwardingTTL is a TTL value that imply potential forwarding with NonForwardingTTL. + SingleForwardingTTL +) + +// SetTTL is a TTL field setter. +func (m *RequestMetaHeader) SetTTL(v uint32) { + m.TTL = v +} + +// IRNonForwarding condition that allows NonForwardingTTL only for IR. +func IRNonForwarding(role NodeRole) TTLCondition { + return func(ttl uint32) error { + if ttl == NonForwardingTTL && role != InnerRingNode { + return ErrInvalidTTL + } + + return nil + } +} + +// ProcessRequestTTL validates and updates requests with TTL. +func ProcessRequestTTL(req TTLContainer, cond ...TTLCondition) error { + ttl := req.GetTTL() + + if ttl == ZeroTTL { + return status.New(codes.InvalidArgument, ErrInvalidTTL.Error()).Err() + } + + for i := range cond { + if cond[i] == nil { + continue + } + + // check specific condition: + if err := cond[i](ttl); err != nil { + if st, ok := status.FromError(errors.Cause(err)); ok { + return st.Err() + } + + return status.New(codes.InvalidArgument, err.Error()).Err() + } + } + + req.SetTTL(ttl - 1) + + return nil +} diff --git a/service/ttl_test.go b/service/ttl_test.go new file mode 100644 index 0000000..1c982f5 --- /dev/null +++ b/service/ttl_test.go @@ -0,0 +1,99 @@ +package service + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type mockedRequest struct { + msg string + name string + code codes.Code + handler TTLCondition + RequestMetaHeader +} + +func TestMetaRequest(t *testing.T) { + tests := []mockedRequest{ + { + name: "direct to ir node", + handler: IRNonForwarding(InnerRingNode), + RequestMetaHeader: RequestMetaHeader{TTL: NonForwardingTTL}, + }, + { + code: codes.InvalidArgument, + msg: ErrInvalidTTL.Error(), + name: "direct to storage node", + handler: IRNonForwarding(StorageNode), + RequestMetaHeader: RequestMetaHeader{TTL: NonForwardingTTL}, + }, + { + msg: ErrInvalidTTL.Error(), + code: codes.InvalidArgument, + name: "zero ttl", + handler: IRNonForwarding(StorageNode), + RequestMetaHeader: RequestMetaHeader{TTL: ZeroTTL}, + }, + { + name: "default to ir node", + handler: IRNonForwarding(InnerRingNode), + RequestMetaHeader: RequestMetaHeader{TTL: SingleForwardingTTL}, + }, + { + name: "default to storage node", + handler: IRNonForwarding(StorageNode), + RequestMetaHeader: RequestMetaHeader{TTL: SingleForwardingTTL}, + }, + { + msg: "not found", + code: codes.NotFound, + name: "custom status error", + RequestMetaHeader: RequestMetaHeader{TTL: SingleForwardingTTL}, + handler: func(_ uint32) error { return status.Error(codes.NotFound, "not found") }, + }, + { + msg: "not found", + code: codes.NotFound, + name: "custom wrapped status error", + RequestMetaHeader: RequestMetaHeader{TTL: SingleForwardingTTL}, + handler: func(_ uint32) error { + err := status.Error(codes.NotFound, "not found") + err = errors.Wrap(err, "some error context") + err = errors.Wrap(err, "another error context") + return err + }, + }, + } + + for i := range tests { + tt := tests[i] + t.Run(tt.name, func(t *testing.T) { + before := tt.GetTTL() + err := ProcessRequestTTL(&tt, tt.handler) + if tt.msg != "" { + require.Errorf(t, err, tt.msg) + + state, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, tt.code, state.Code()) + require.Equal(t, tt.msg, state.Message()) + } else { + require.NoError(t, err) + require.NotEqualf(t, before, tt.GetTTL(), "ttl should be changed: %d vs %d", before, tt.GetTTL()) + } + }) + } +} + +func TestRequestMetaHeader_SetTTL(t *testing.T) { + m := new(RequestMetaHeader) + ttl := uint32(3) + + m.SetTTL(ttl) + + require.Equal(t, ttl, m.GetTTL()) +} diff --git a/service/types.go b/service/types.go new file mode 100644 index 0000000..31f4507 --- /dev/null +++ b/service/types.go @@ -0,0 +1,256 @@ +package service + +import ( + "crypto/ecdsa" +) + +// NodeRole to identify in Bootstrap service. +type NodeRole int32 + +// TTLCondition is a function type that used to verify that TTL values match a specific criterion. +// Nil error indicates compliance with the criterion. +type TTLCondition func(uint32) error + +// RawSource is an interface of the container of a boolean Raw value with read access. +type RawSource interface { + GetRaw() bool +} + +// RawContainer is an interface of the container of a boolean Raw value. +type RawContainer interface { + RawSource + SetRaw(bool) +} + +// VersionSource is an interface of the container of a numerical Version value with read access. +type VersionSource interface { + GetVersion() uint32 +} + +// VersionContainer is an interface of the container of a numerical Version value. +type VersionContainer interface { + VersionSource + SetVersion(uint32) +} + +// EpochSource is an interface of the container of a NeoFS epoch number with read access. +type EpochSource interface { + GetEpoch() uint64 +} + +// EpochContainer is an interface of the container of a NeoFS epoch number. +type EpochContainer interface { + EpochSource + SetEpoch(uint64) +} + +// TTLSource is an interface of the container of a numerical TTL value with read access. +type TTLSource interface { + GetTTL() uint32 +} + +// TTLContainer is an interface of the container of a numerical TTL value. +type TTLContainer interface { + TTLSource + SetTTL(uint32) +} + +// SeizedMetaHeaderContainer is an interface of container of RequestMetaHeader that can be cut and restored. +type SeizedMetaHeaderContainer interface { + CutMeta() RequestMetaHeader + RestoreMeta(RequestMetaHeader) +} + +// RequestMetaContainer is an interface of a fixed set of request meta value containers. +// Contains: +// - TTL value; +// - NeoFS epoch number; +// - Protocol version; +// - Raw toggle option. +type RequestMetaContainer interface { + TTLContainer + EpochContainer + VersionContainer + RawContainer +} + +// SeizedRequestMetaContainer is a RequestMetaContainer with seized meta. +type SeizedRequestMetaContainer interface { + RequestMetaContainer + SeizedMetaHeaderContainer +} + +// VerbSource is an interface of the container of a token verb value with read access. +type VerbSource interface { + GetVerb() Token_Info_Verb +} + +// VerbContainer is an interface of the container of a token verb value. +type VerbContainer interface { + VerbSource + SetVerb(Token_Info_Verb) +} + +// TokenIDSource is an interface of the container of a token ID value with read access. +type TokenIDSource interface { + GetID() TokenID +} + +// TokenIDContainer is an interface of the container of a token ID value. +type TokenIDContainer interface { + TokenIDSource + SetID(TokenID) +} + +// CreationEpochSource is an interface of the container of a creation epoch number with read access. +type CreationEpochSource interface { + CreationEpoch() uint64 +} + +// CreationEpochContainer is an interface of the container of a creation epoch number. +type CreationEpochContainer interface { + CreationEpochSource + SetCreationEpoch(uint64) +} + +// ExpirationEpochSource is an interface of the container of an expiration epoch number with read access. +type ExpirationEpochSource interface { + ExpirationEpoch() uint64 +} + +// ExpirationEpochContainer is an interface of the container of an expiration epoch number. +type ExpirationEpochContainer interface { + ExpirationEpochSource + 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 +} + +// SessionKeyContainer is an interface of the container of public session key bytes. +type SessionKeyContainer interface { + SessionKeySource + SetSessionKey([]byte) +} + +// SignatureSource is an interface of the container of signature bytes with read access. +type SignatureSource interface { + GetSignature() []byte +} + +// SignatureContainer is an interface of the container of signature bytes. +type SignatureContainer interface { + SignatureSource + SetSignature([]byte) +} + +// SessionTokenSource is an interface of the container of a SessionToken with read access. +type SessionTokenSource interface { + GetSessionToken() SessionToken +} + +// SessionTokenInfo is an interface of a fixed set of token information value containers. +// Contains: +// - ID of the token; +// - ID of the token's owner; +// - verb of the session; +// - address of the session object; +// - token lifetime; +// - public session key bytes. +type SessionTokenInfo interface { + TokenIDContainer + OwnerIDContainer + VerbContainer + AddressContainer + LifetimeContainer + SessionKeyContainer +} + +// SessionToken is an interface of token information and signature pair. +type SessionToken interface { + SessionTokenInfo + SignatureContainer +} + +// SignedDataSource is an interface of the container of a data for signing. +type SignedDataSource interface { + // Must return the required for signature byte slice. + // A non-nil error indicates that the data is not ready for signature. + SignedData() ([]byte, error) +} + +// SignedDataReader is an interface of signed data reader. +type SignedDataReader interface { + // Must return the minimum length of the slice for full reading. + // Must return a negative value if the length cannot be calculated. + SignedDataSize() int + + // Must behave like Read method of io.Reader and differ only in the reading of the signed data. + ReadSignedData([]byte) (int, error) +} + +// SignKeyPairAccumulator is an interface of a set of key-signature pairs with append access. +type SignKeyPairAccumulator interface { + AddSignKey([]byte, *ecdsa.PublicKey) +} + +// SignKeyPairSource is an interface of a set of key-signature pairs with read access. +type SignKeyPairSource interface { + GetSignKeyPairs() []SignKeyPair +} + +// SignKeyPair is an interface of key-signature pair with read access. +type SignKeyPair interface { + SignatureSource + GetPublicKey() *ecdsa.PublicKey +} + +// DataWithSignature is an interface of data-signature pair with read access. +type DataWithSignature interface { + SignedDataSource + SignatureSource +} + +// DataWithSignKeyAccumulator is an interface of data and key-signature accumulator pair. +type DataWithSignKeyAccumulator interface { + SignedDataSource + SignKeyPairAccumulator +} + +// DataWithSignKeySource is an interface of data and key-signature source pair. +type DataWithSignKeySource interface { + SignedDataSource + SignKeyPairSource +} + +// SignedDataWithToken is an interface of data-token pair with read access. +type SignedDataWithToken interface { + SignedDataSource + SessionTokenSource +} + +// DataWithTokenSignAccumulator is an interface of data-token pair with signature write access. +type DataWithTokenSignAccumulator interface { + SignedDataWithToken + SignKeyPairAccumulator +} + +// DataWithTokenSignSource is an interface of data-token pair with signature read access. +type DataWithTokenSignSource interface { + SignedDataWithToken + SignKeyPairSource +} diff --git a/service/utils.go b/service/utils.go new file mode 100644 index 0000000..17b23bb --- /dev/null +++ b/service/utils.go @@ -0,0 +1,18 @@ +package service + +// SignedDataFromReader allocates buffer and reads bytes from passed reader to it. +// +// If passed SignedDataReader is nil, ErrNilSignedDataReader returns. +func SignedDataFromReader(r SignedDataReader) ([]byte, error) { + if r == nil { + return nil, ErrNilSignedDataReader + } + + data := make([]byte, r.SignedDataSize()) + + if _, err := r.ReadSignedData(data); err != nil { + return nil, err + } + + return data, nil +} diff --git a/service/utils_test.go b/service/utils_test.go new file mode 100644 index 0000000..60a2352 --- /dev/null +++ b/service/utils_test.go @@ -0,0 +1,34 @@ +package service + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +func TestSignedDataFromReader(t *testing.T) { + // nil SignedDataReader + _, err := SignedDataFromReader(nil) + require.EqualError(t, err, ErrNilSignedDataReader.Error()) + + rdr := &testSignedDataReader{ + testSignedDataSrc: new(testSignedDataSrc), + } + + // make reader to return an error + rdr.err = errors.New("test error") + + _, err = SignedDataFromReader(rdr) + require.EqualError(t, err, rdr.err.Error()) + + // remove the error + rdr.err = nil + + // fill the data + rdr.data = testData(t, 10) + + res, err := SignedDataFromReader(rdr) + require.NoError(t, err) + require.Equal(t, rdr.data, res) +} diff --git a/service/verify.go b/service/verify.go index 9687032..62db2f5 100644 --- a/service/verify.go +++ b/service/verify.go @@ -2,226 +2,69 @@ package service import ( "crypto/ecdsa" - "sync" "github.com/nspcc-dev/neofs-api-go/internal" - "github.com/nspcc-dev/neofs-api-go/refs" crypto "github.com/nspcc-dev/neofs-crypto" - "github.com/pkg/errors" ) -type ( - // VerifiableRequest adds possibility to sign and verify request header. - VerifiableRequest interface { - Size() int - MarshalTo([]byte) (int, error) - AddSignature(*RequestVerificationHeader_Signature) - GetSignatures() []*RequestVerificationHeader_Signature - SetSignatures([]*RequestVerificationHeader_Signature) +// GetSessionToken returns SessionToken interface of Token field. +// +// If token field value is nil, nil returns. +func (m RequestVerificationHeader) GetSessionToken() SessionToken { + if t := m.GetToken(); t != nil { + return t } - // MaintainableRequest adds possibility to set and get (+validate) - // owner (client) public key from RequestVerificationHeader. - MaintainableRequest interface { - GetOwner() (*ecdsa.PublicKey, error) - SetOwner(*ecdsa.PublicKey, []byte) - GetLastPeer() (*ecdsa.PublicKey, error) + return nil +} + +// AddSignKey adds new element to Signatures field. +// +// Sets Sign field to passed sign. Set Peer field to marshaled passed key. +func (m *RequestVerificationHeader) AddSignKey(sign []byte, key *ecdsa.PublicKey) { + m.SetSignatures( + append( + m.GetSignatures(), + &RequestVerificationHeader_Signature{ + Sign: sign, + Peer: crypto.MarshalPublicKey(key), + }, + ), + ) +} + +// GetSignKeyPairs returns the elements of Signatures field as SignKeyPair slice. +func (m RequestVerificationHeader) GetSignKeyPairs() []SignKeyPair { + var ( + signs = m.GetSignatures() + res = make([]SignKeyPair, len(signs)) + ) + + for i := range signs { + res[i] = signs[i] } -) -const ( - // ErrCannotLoadPublicKey is raised when cannot unmarshal public key from RequestVerificationHeader_Sign. - ErrCannotLoadPublicKey = internal.Error("cannot load public key") + return res +} - // ErrCannotFindOwner is raised when signatures empty in GetOwner. - ErrCannotFindOwner = internal.Error("cannot find owner public key") +// GetSignature returns the result of a Sign field getter. +func (m RequestVerificationHeader_Signature) GetSignature() []byte { + return m.GetSign() +} - // ErrWrongOwner is raised when passed OwnerID not equal to present PublicKey - ErrWrongOwner = internal.Error("wrong owner") -) +// GetPublicKey unmarshals and returns the result of a Peer field getter. +func (m RequestVerificationHeader_Signature) GetPublicKey() *ecdsa.PublicKey { + return crypto.UnmarshalPublicKey(m.GetPeer()) +} // SetSignatures replaces signatures stored in RequestVerificationHeader. func (m *RequestVerificationHeader) SetSignatures(signatures []*RequestVerificationHeader_Signature) { m.Signatures = signatures } -// AddSignature adds new Signature into RequestVerificationHeader. -func (m *RequestVerificationHeader) AddSignature(sig *RequestVerificationHeader_Signature) { - if sig == nil { - return - } - m.Signatures = append(m.Signatures, sig) -} - -// SetOwner adds origin (sign and public key) of owner (client) into first signature. -func (m *RequestVerificationHeader) SetOwner(pub *ecdsa.PublicKey, sign []byte) { - if len(m.Signatures) == 0 || pub == nil { - return - } - - m.Signatures[0].Origin = &RequestVerificationHeader_Sign{ - Sign: sign, - Peer: crypto.MarshalPublicKey(pub), - } -} - -// CheckOwner validates, that passed OwnerID is equal to present PublicKey of owner. -func (m *RequestVerificationHeader) CheckOwner(owner refs.OwnerID) error { - if key, err := m.GetOwner(); err != nil { - return err - } else if user, err := refs.NewOwnerID(key); err != nil { - return err - } else if !user.Equal(owner) { - return ErrWrongOwner - } - return nil -} - -// GetOwner tries to get owner (client) public key from signatures. -// If signatures contains not empty Origin, we should try to validate, -// that session key was signed by owner (client), otherwise return error. -func (m *RequestVerificationHeader) GetOwner() (*ecdsa.PublicKey, error) { - if len(m.Signatures) == 0 { - return nil, ErrCannotFindOwner - } - - // if first signature contains origin, we should try to validate session key - if m.Signatures[0].Origin != nil { - owner := crypto.UnmarshalPublicKey(m.Signatures[0].Origin.Peer) - if owner == nil { - return nil, ErrCannotLoadPublicKey - } else if err := crypto.Verify(owner, m.Signatures[0].Peer, m.Signatures[0].Origin.Sign); err != nil { - return nil, errors.Wrap(err, "could not verify session token") - } - - return owner, nil - } else if key := crypto.UnmarshalPublicKey(m.Signatures[0].Peer); key != nil { - return key, nil - } - - return nil, ErrCannotLoadPublicKey -} - -// GetLastPeer tries to get last peer public key from signatures. -// If signatures has zero length, returns ErrCannotFindOwner. -// If signatures has length equal to one, uses GetOwner. -// Otherwise tries to unmarshal last peer public key. -func (m *RequestVerificationHeader) GetLastPeer() (*ecdsa.PublicKey, error) { - switch ln := len(m.Signatures); ln { - case 0: - return nil, ErrCannotFindOwner - case 1: - return m.GetOwner() - default: - if key := crypto.UnmarshalPublicKey(m.Signatures[ln-1].Peer); key != nil { - return key, nil - } - - return nil, ErrCannotLoadPublicKey - } -} - -func newSignature(key *ecdsa.PrivateKey, data []byte) (*RequestVerificationHeader_Signature, error) { - sign, err := crypto.Sign(key, data) - if err != nil { - return nil, err - } - - return &RequestVerificationHeader_Signature{ - RequestVerificationHeader_Sign: RequestVerificationHeader_Sign{ - Sign: sign, - Peer: crypto.MarshalPublicKey(&key.PublicKey), - }, - }, nil -} - -var bytesPool = sync.Pool{New: func() interface{} { - return make([]byte, 4.5*1024*1024) // 4.5MB -}} - -// SignRequestHeader receives private key and request with RequestVerificationHeader, -// tries to marshal and sign request with passed PrivateKey, after that adds -// new signature to headers. If something went wrong, returns error. -func SignRequestHeader(key *ecdsa.PrivateKey, msg VerifiableRequest) error { - // ignore meta header - if meta, ok := msg.(MetaHeader); ok { - h := meta.ResetMeta() - - defer func() { - meta.RestoreMeta(h) - }() - } - - data := bytesPool.Get().([]byte) - defer func() { - bytesPool.Put(data) - }() - - if size := msg.Size(); size <= cap(data) { - data = data[:size] - } else { - data = make([]byte, size) - } - - size, err := msg.MarshalTo(data) - if err != nil { - return err - } - - signature, err := newSignature(key, data[:size]) - if err != nil { - return err - } - - msg.AddSignature(signature) - - return nil -} - -// VerifyRequestHeader receives request with RequestVerificationHeader, -// tries to marshal and verify each signature from request. -// If something went wrong, returns error. -func VerifyRequestHeader(msg VerifiableRequest) error { - // ignore meta header - if meta, ok := msg.(MetaHeader); ok { - h := meta.ResetMeta() - - defer func() { - meta.RestoreMeta(h) - }() - } - - data := bytesPool.Get().([]byte) - signatures := msg.GetSignatures() - defer func() { - bytesPool.Put(data) - msg.SetSignatures(signatures) - }() - - for i := range signatures { - msg.SetSignatures(signatures[:i]) - peer := signatures[i].GetPeer() - sign := signatures[i].GetSign() - - key := crypto.UnmarshalPublicKey(peer) - if key == nil { - return errors.Wrapf(ErrCannotLoadPublicKey, "%d: %02x", i, peer) - } - - if size := msg.Size(); size <= cap(data) { - data = data[:size] - } else { - data = make([]byte, size) - } - - if size, err := msg.MarshalTo(data); err != nil { - return errors.Wrapf(err, "%d: %02x", i, peer) - } else if err := crypto.Verify(key, data[:size], sign); err != nil { - return errors.Wrapf(err, "%d: %02x", i, peer) - } - } - - return nil +// SetToken is a Token field setter. +func (m *RequestVerificationHeader) SetToken(token *Token) { + m.Token = token } // testCustomField for test usage only. diff --git a/service/verify.pb.go b/service/verify.pb.go index 9dca855..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 de0a69a..ed360be 100644 --- a/service/verify.proto +++ b/service/verify.proto @@ -3,6 +3,7 @@ package service; option go_package = "github.com/nspcc-dev/neofs-api-go/service"; option csharp_namespace = "NeoFS.API.Service"; +import "refs/types.proto"; import "github.com/gogo/protobuf/gogoproto/gogo.proto"; option (gogoproto.stable_marshaler_all) = true; @@ -10,22 +11,80 @@ option (gogoproto.stable_marshaler_all) = true; // RequestVerificationHeader is a set of signatures of every NeoFS Node that processed request // (should be embedded into message). message RequestVerificationHeader { - message Sign { + message Signature { // Sign is signature of the request or session key. bytes Sign = 1; // Peer is compressed public key used for signature. bytes Peer = 2; } - message Signature { - // Sign is a signature and public key of the request. - Sign Sign = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false]; - // Origin used for requests, when trusted node changes it and re-sign with session key. - // If session key used for signature request, then Origin should contain - // public key of user and signed session key. - Sign Origin = 2; - } - // Signatures is a set of signatures of every passed NeoFS Node repeated Signature Signatures = 1; + + // Token is a token of the session within which the request is sent + Token Token = 2; } + +// User token granting rights for object manipulation +message Token { + message Info { + // ID is a token identifier. valid UUIDv4 represented in bytes + bytes ID = 1 [(gogoproto.customtype) = "TokenID", (gogoproto.nullable) = false]; + + // OwnerID is an owner of manipulation object + bytes OwnerID = 2 [(gogoproto.customtype) = "OwnerID", (gogoproto.nullable) = false]; + + // Verb is an enumeration of session request types + enum Verb { + // Put refers to object.Put RPC call + Put = 0; + // Get refers to object.Get RPC call + Get = 1; + // Head refers to object.Head RPC call + Head = 2; + // Search refers to object.Search RPC call + Search = 3; + // Delete refers to object.Delete RPC call + Delete = 4; + // Range refers to object.GetRange RPC call + Range = 5; + // RangeHash refers to object.GetRangeHash RPC call + RangeHash = 6; + } + + // Verb is a type of request for which the token is issued + Verb verb = 3 [(gogoproto.customname) = "Verb"]; + + // Address is an object address for which token is issued + refs.Address Address = 4 [(gogoproto.nullable) = false, (gogoproto.customtype) = "Address"]; + + // 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 = 6; + } + + // TokenInfo is a grouped information about token + Info TokenInfo = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false]; + + // Signature is a signature of session token information + 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 { +// TokenType TokenType = 1; +// uint32 Version = 2; +// bytes Data = 3; +// } diff --git a/service/verify_test.go b/service/verify_test.go index 27491da..c6e4d61 100644 --- a/service/verify_test.go +++ b/service/verify_test.go @@ -1,201 +1,117 @@ package service import ( - "bytes" - "log" + "encoding/binary" + "io" "math" "testing" - "github.com/gogo/protobuf/proto" "github.com/nspcc-dev/neofs-api-go/refs" - crypto "github.com/nspcc-dev/neofs-crypto" "github.com/nspcc-dev/neofs-crypto/test" - "github.com/pkg/errors" "github.com/stretchr/testify/require" ) -func BenchmarkSignRequestHeader(b *testing.B) { +func (m TestRequest) SignedData() ([]byte, error) { + return SignedDataFromReader(m) +} + +func (m TestRequest) SignedDataSize() (sz int) { + sz += 4 + + sz += len(m.StringField) + + sz += len(m.BytesField) + + sz += m.CustomField.Size() + + return +} + +func (m TestRequest) ReadSignedData(p []byte) (int, error) { + if len(p) < m.SignedDataSize() { + return 0, io.ErrUnexpectedEOF + } + + var off int + + binary.BigEndian.PutUint32(p[off:], uint32(m.IntField)) + off += 4 + + off += copy(p[off:], []byte(m.StringField)) + + off += copy(p[off:], m.BytesField) + + n, err := m.CustomField.MarshalTo(p[off:]) + off += n + + return off, err +} + +func BenchmarkSignDataWithSessionToken(b *testing.B) { key := test.DecodeKey(0) - custom := testCustomField{1, 2, 3, 4, 5, 6, 7, 8} + customField := testCustomField{1, 2, 3, 4, 5, 6, 7, 8} - some := &TestRequest{ - IntField: math.MaxInt32, - StringField: "TestRequestStringField", - BytesField: make([]byte, 1<<22), - CustomField: &custom, - RequestMetaHeader: RequestMetaHeader{ - TTL: math.MaxInt32 - 8, - Epoch: math.MaxInt64 - 12, - }, - } + token := new(Token) - b.ResetTimer() - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - require.NoError(b, SignRequestHeader(key, some)) - } -} - -func BenchmarkVerifyRequestHeader(b *testing.B) { - custom := testCustomField{1, 2, 3, 4, 5, 6, 7, 8} - - some := &TestRequest{ - IntField: math.MaxInt32, - StringField: "TestRequestStringField", - BytesField: make([]byte, 1<<22), - CustomField: &custom, - RequestMetaHeader: RequestMetaHeader{ - TTL: math.MaxInt32 - 8, - Epoch: math.MaxInt64 - 12, - }, - } - - for i := 0; i < 10; i++ { - key := test.DecodeKey(i) - require.NoError(b, SignRequestHeader(key, some)) - } - - b.ResetTimer() - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - require.NoError(b, VerifyRequestHeader(some)) - } -} - -func TestSignRequestHeader(t *testing.T) { req := &TestRequest{ IntField: math.MaxInt32, StringField: "TestRequestStringField", - BytesField: []byte("TestRequestBytesField"), + BytesField: make([]byte, 1<<22), + CustomField: &customField, } - key := test.DecodeKey(0) - peer := crypto.MarshalPublicKey(&key.PublicKey) + req.SetTTL(math.MaxInt32 - 8) + req.SetEpoch(math.MaxInt64 - 12) + req.SetToken(token) - data, err := req.Marshal() + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + require.NoError(b, SignDataWithSessionToken(key, req)) + } +} + +func BenchmarkVerifyAccumulatedSignaturesWithToken(b *testing.B) { + customField := testCustomField{1, 2, 3, 4, 5, 6, 7, 8} + + token := new(Token) + + req := &TestRequest{ + IntField: math.MaxInt32, + StringField: "TestRequestStringField", + BytesField: make([]byte, 1<<22), + CustomField: &customField, + } + + req.SetTTL(math.MaxInt32 - 8) + req.SetEpoch(math.MaxInt64 - 12) + req.SetToken(token) + + for i := 0; i < 10; i++ { + key := test.DecodeKey(i) + require.NoError(b, SignDataWithSessionToken(key, req)) + } + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + require.NoError(b, VerifyAccumulatedSignaturesWithToken(req)) + } +} + +func TestRequestVerificationHeader_SetToken(t *testing.T) { + id, err := refs.NewUUID() require.NoError(t, err) - require.NoError(t, SignRequestHeader(key, req)) + token := new(Token) + token.SetID(id) - require.Len(t, req.Signatures, 1) - for i := range req.Signatures { - sign := req.Signatures[i].GetSign() - require.Equal(t, peer, req.Signatures[i].GetPeer()) - require.NoError(t, crypto.Verify(&key.PublicKey, data, sign)) - } -} - -func TestVerifyRequestHeader(t *testing.T) { - req := &TestRequest{ - IntField: math.MaxInt32, - StringField: "TestRequestStringField", - BytesField: []byte("TestRequestBytesField"), - RequestMetaHeader: RequestMetaHeader{TTL: 10}, - } - - for i := 0; i < 10; i++ { - req.TTL-- - require.NoError(t, SignRequestHeader(test.DecodeKey(i), req)) - } - - require.NoError(t, VerifyRequestHeader(req)) -} - -func TestMaintainableRequest(t *testing.T) { - req := &TestRequest{ - IntField: math.MaxInt32, - StringField: "TestRequestStringField", - BytesField: []byte("TestRequestBytesField"), - RequestMetaHeader: RequestMetaHeader{TTL: 10}, - } - - count := 10 - owner := test.DecodeKey(count + 1) - - for i := 0; i < count; i++ { - req.TTL-- - - key := test.DecodeKey(i) - require.NoError(t, SignRequestHeader(key, req)) - - // sign first key (session key) by owner key - if i == 0 { - sign, err := crypto.Sign(owner, crypto.MarshalPublicKey(&key.PublicKey)) - require.NoError(t, err) - - req.SetOwner(&owner.PublicKey, sign) - } - } - - { // Validate owner - user, err := refs.NewOwnerID(&owner.PublicKey) - require.NoError(t, err) - require.NoError(t, req.CheckOwner(user)) - } - - { // Good case: - require.NoError(t, VerifyRequestHeader(req)) - - // validate, that first key (session key) was signed with owner - signatures := req.GetSignatures() - - require.Len(t, signatures, count) - - pub, err := req.GetOwner() - require.NoError(t, err) - - require.Equal(t, &owner.PublicKey, pub) - } - - { // wrong owner: - req.Signatures[0].Origin = nil - - pub, err := req.GetOwner() - require.NoError(t, err) - - require.NotEqual(t, &owner.PublicKey, pub) - } - - { // Wrong signatures: - copy(req.Signatures[count-1].Sign, req.Signatures[count-1].Peer) - err := VerifyRequestHeader(req) - require.EqualError(t, errors.Cause(err), crypto.ErrInvalidSignature.Error()) - } -} - -func TestVerifyAndSignRequestHeaderWithoutCloning(t *testing.T) { - key := test.DecodeKey(0) - - custom := testCustomField{1, 2, 3, 4, 5, 6, 7, 8} - - b := &TestRequest{ - IntField: math.MaxInt32, - StringField: "TestRequestStringField", - BytesField: []byte("TestRequestBytesField"), - CustomField: &custom, - RequestMetaHeader: RequestMetaHeader{ - TTL: math.MaxInt32 - 8, - Epoch: math.MaxInt64 - 12, - }, - } - - require.NoError(t, SignRequestHeader(key, b)) - require.NoError(t, VerifyRequestHeader(b)) - - require.Len(t, b.Signatures, 1) - require.Equal(t, custom, *b.CustomField) - require.Equal(t, uint32(math.MaxInt32-8), b.GetTTL()) - require.Equal(t, uint64(math.MaxInt64-12), b.GetEpoch()) - - buf := bytes.NewBuffer(nil) - log.SetOutput(buf) - - cp, ok := proto.Clone(b).(*TestRequest) - require.True(t, ok) - require.NotEqual(t, b, cp) - - require.Contains(t, buf.String(), "proto: don't know how to copy") + h := new(RequestVerificationHeader) + + h.SetToken(token) + + require.Equal(t, token, h.GetToken()) } diff --git a/service/version.go b/service/version.go new file mode 100644 index 0000000..6f4839c --- /dev/null +++ b/service/version.go @@ -0,0 +1,11 @@ +package service + +// SetVersion is a Version field setter. +func (m *ResponseMetaHeader) SetVersion(v uint32) { + m.Version = v +} + +// SetVersion is a Version field setter. +func (m *RequestMetaHeader) SetVersion(v uint32) { + m.Version = v +} diff --git a/service/version_test.go b/service/version_test.go new file mode 100644 index 0000000..d102d30 --- /dev/null +++ b/service/version_test.go @@ -0,0 +1,21 @@ +package service + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetSetVersion(t *testing.T) { + v := uint32(7) + + items := []VersionContainer{ + new(ResponseMetaHeader), + new(RequestMetaHeader), + } + + for _, item := range items { + item.SetVersion(v) + require.Equal(t, v, item.GetVersion()) + } +} diff --git a/session/alias.go b/session/alias.go new file mode 100644 index 0000000..aa49d55 --- /dev/null +++ b/session/alias.go @@ -0,0 +1,15 @@ +package session + +import ( + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" +) + +// OwnerID is a type alias of OwnerID ref. +type OwnerID = refs.OwnerID + +// TokenID is a type alias of TokenID ref. +type TokenID = service.TokenID + +// Token is a type alias of Token. +type Token = service.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/private.go b/session/private.go new file mode 100644 index 0000000..42bb205 --- /dev/null +++ b/session/private.go @@ -0,0 +1,55 @@ +package session + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + + crypto "github.com/nspcc-dev/neofs-crypto" +) + +type pToken struct { + // private session token + sessionKey *ecdsa.PrivateKey + // last epoch of the lifetime + validUntil uint64 +} + +// NewPrivateToken creates PrivateToken instance that expires after passed epoch. +// +// Returns non-nil error on key generation error. +func NewPrivateToken(validUntil uint64) (PrivateToken, error) { + sk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + return &pToken{ + sessionKey: sk, + validUntil: validUntil, + }, nil +} + +// Sign signs data with session private key. +func (t *pToken) Sign(data []byte) ([]byte, error) { + return crypto.Sign(t.sessionKey, data) +} + +// PublicKey returns a binary representation of the session public key. +func (t *pToken) PublicKey() []byte { + return crypto.MarshalPublicKey(&t.sessionKey.PublicKey) +} + +func (t *pToken) Expired(epoch uint64) bool { + return t.validUntil < epoch +} + +// SetOwnerID is an owner ID field setter. +func (s *PrivateTokenKey) SetOwnerID(id OwnerID) { + s.owner = id +} + +// SetTokenID is a token ID field setter. +func (s *PrivateTokenKey) SetTokenID(id TokenID) { + s.token = id +} diff --git a/session/private_test.go b/session/private_test.go new file mode 100644 index 0000000..8097b97 --- /dev/null +++ b/session/private_test.go @@ -0,0 +1,70 @@ +package session + +import ( + "crypto/rand" + "testing" + + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/stretchr/testify/require" +) + +func TestPrivateToken(t *testing.T) { + // create new private token + pToken, err := NewPrivateToken(0) + require.NoError(t, err) + + // generate data to sign + data := make([]byte, 10) + _, err = rand.Read(data) + require.NoError(t, err) + + // sign data via private token + sig, err := pToken.Sign(data) + require.NoError(t, err) + + // check signature + require.NoError(t, + crypto.Verify( + crypto.UnmarshalPublicKey(pToken.PublicKey()), + data, + sig, + ), + ) +} + +func TestPToken_Expired(t *testing.T) { + e := uint64(10) + + var token PrivateToken = &pToken{ + validUntil: e, + } + + // must not be expired in the epoch before last + require.False(t, token.Expired(e-1)) + + // must not be expired in the last epoch + require.False(t, token.Expired(e)) + + // must be expired in the epoch after last + require.True(t, token.Expired(e+1)) +} + +func TestPrivateTokenKey_SetOwnerID(t *testing.T) { + ownerID := OwnerID{1, 2, 3} + + s := new(PrivateTokenKey) + + s.SetOwnerID(ownerID) + + require.Equal(t, ownerID, s.owner) +} + +func TestPrivateTokenKey_SetTokenID(t *testing.T) { + tokenID := TokenID{1, 2, 3} + + s := new(PrivateTokenKey) + + s.SetTokenID(tokenID) + + require.Equal(t, tokenID, s.token) +} diff --git a/session/request.go b/session/request.go new file mode 100644 index 0000000..73c05e5 --- /dev/null +++ b/session/request.go @@ -0,0 +1,62 @@ +package session + +import ( + "encoding/binary" + "io" + + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" +) + +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) { + return service.SignedDataFromReader(m) +} + +// 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 182ff7d..0000000 --- a/session/service.go +++ /dev/null @@ -1,58 +0,0 @@ -package session - -import ( - "context" - "crypto/ecdsa" - - "github.com/nspcc-dev/neofs-api-go/refs" - crypto "github.com/nspcc-dev/neofs-crypto" -) - -type ( - // KeyStore is an interface that describes storage, - // that allows to fetch public keys by OwnerID. - KeyStore interface { - Get(ctx context.Context, id refs.OwnerID) ([]*ecdsa.PublicKey, error) - } - - // TokenStore is a PToken storage manipulation interface. - TokenStore interface { - // New returns new token with specified parameters. - New(p TokenParams) *PToken - - // Fetch tries to fetch a token with specified id. - Fetch(id TokenID) *PToken - - // Remove removes token with id from store. - Remove(id TokenID) - } - - // TokenParams contains params to create new PToken. - TokenParams struct { - FirstEpoch uint64 - LastEpoch uint64 - ObjectID []ObjectID - OwnerID OwnerID - PublicKeys [][]byte - } -) - -// 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}} -} - -// Sign signs contents of the header with the private key. -func (m *VerificationHeader) Sign(key *ecdsa.PrivateKey) error { - s, err := crypto.Sign(key, m.PublicKey) - if err != nil { - return err - } - m.KeySignature = s - return nil -} diff --git a/session/service.pb.go b/session/service.pb.go index abd1618..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 524213a..b7eb0df 100644 --- a/session/service.proto +++ b/session/service.proto @@ -3,7 +3,6 @@ package session; option go_package = "github.com/nspcc-dev/neofs-api-go/session"; option csharp_namespace = "NeoFS.API.Session"; -import "session/types.proto"; import "service/meta.proto"; import "service/verify.proto"; import "github.com/gogo/protobuf/gogoproto/gogo.proto"; @@ -12,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. - session.Token Init = 1; - // Signed Init message response (Unsigned) from server with user private key - session.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 - session.Token Unsigned = 1; - // Result is a resulting token which can be used for object placing through an trusted intermediary - session.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/store.go b/session/store.go index f6a6655..fa33b7e 100644 --- a/session/store.go +++ b/session/store.go @@ -1,82 +1,64 @@ package session import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" "sync" - - "github.com/nspcc-dev/neofs-api-go/refs" - crypto "github.com/nspcc-dev/neofs-crypto" ) -type simpleStore struct { +type mapTokenStore struct { *sync.RWMutex - tokens map[TokenID]*PToken + tokens map[PrivateTokenKey]PrivateToken } -// TODO get curve from neofs-crypto -func defaultCurve() elliptic.Curve { - return elliptic.P256() -} - -// NewSimpleStore creates simple token storage -func NewSimpleStore() TokenStore { - return &simpleStore{ +// NewMapTokenStore creates new PrivateTokenStore instance. +// +// The elements of the instance are stored in the map. +func NewMapTokenStore() PrivateTokenStore { + return &mapTokenStore{ RWMutex: new(sync.RWMutex), - tokens: make(map[TokenID]*PToken), + tokens: make(map[PrivateTokenKey]PrivateToken), } } -// New returns new token with specified parameters. -func (s *simpleStore) New(p TokenParams) *PToken { - tid, err := refs.NewUUID() - if err != nil { - return nil - } - - key, err := ecdsa.GenerateKey(defaultCurve(), rand.Reader) - if err != nil { - return nil - } - - if p.FirstEpoch > p.LastEpoch || p.OwnerID.Empty() { - return nil - } - - t := &PToken{ - mtx: new(sync.Mutex), - Token: Token{ - ID: tid, - Header: VerificationHeader{PublicKey: crypto.MarshalPublicKey(&key.PublicKey)}, - FirstEpoch: p.FirstEpoch, - LastEpoch: p.LastEpoch, - ObjectID: p.ObjectID, - OwnerID: p.OwnerID, - PublicKeys: p.PublicKeys, - }, - PrivateKey: key, - } - +// Store adds passed token to the map. +// +// Resulting error is always nil. +func (s *mapTokenStore) Store(key PrivateTokenKey, token PrivateToken) error { s.Lock() - s.tokens[t.ID] = t + s.tokens[key] = token s.Unlock() - return t + return nil } -// Fetch tries to fetch a token with specified id. -func (s *simpleStore) Fetch(id TokenID) *PToken { +// Fetch returns the map element corresponding to the given key. +// +// Returns ErrPrivateTokenNotFound is there is no element in map. +func (s *mapTokenStore) Fetch(key PrivateTokenKey) (PrivateToken, error) { s.RLock() defer s.RUnlock() - return s.tokens[id] + t, ok := s.tokens[key] + if !ok { + return nil, ErrPrivateTokenNotFound + } + + return t, nil } -// Remove removes token with id from store. -func (s *simpleStore) Remove(id TokenID) { +// RemoveExpired removes all the map elements that are expired in the passed epoch. +// +// Resulting error is always nil. +func (s *mapTokenStore) RemoveExpired(epoch uint64) error { s.Lock() - delete(s.tokens, id) + + for key, token := range s.tokens { + if token.Expired(epoch) { + delete(s.tokens, key) + } + } + s.Unlock() + + return nil } diff --git a/session/store_test.go b/session/store_test.go index 9ad0e1d..74e0023 100644 --- a/session/store_test.go +++ b/session/store_test.go @@ -1,96 +1,111 @@ package session import ( - "crypto/ecdsa" - "crypto/rand" "testing" "github.com/nspcc-dev/neofs-api-go/refs" - crypto "github.com/nspcc-dev/neofs-crypto" "github.com/stretchr/testify/require" ) -type testClient struct { - *ecdsa.PrivateKey - OwnerID OwnerID +func TestMapTokenStore(t *testing.T) { + // create new private token + pToken, err := NewPrivateToken(0) + require.NoError(t, err) + + // create map token store + s := NewMapTokenStore() + + // create test TokenID + tid, err := refs.NewUUID() + require.NoError(t, err) + + // create test OwnerID + ownerID := OwnerID{1, 2, 3} + + key := PrivateTokenKey{} + key.SetOwnerID(ownerID) + key.SetTokenID(tid) + + // ascertain that there is no record for the key + _, err = s.Fetch(key) + require.EqualError(t, err, ErrPrivateTokenNotFound.Error()) + + // save private token record + require.NoError(t, s.Store(key, pToken)) + + // fetch private token by the key + res, err := s.Fetch(key) + require.NoError(t, err) + + // ascertain that returned token equals to initial + require.Equal(t, pToken, res) } -func (c *testClient) Sign(data []byte) ([]byte, error) { - return crypto.Sign(c.PrivateKey, data) -} - -func newTestClient(t *testing.T) *testClient { - key, err := ecdsa.GenerateKey(defaultCurve(), rand.Reader) - require.NoError(t, err) - - owner, err := refs.NewOwnerID(&key.PublicKey) - require.NoError(t, err) - - return &testClient{PrivateKey: key, OwnerID: owner} -} - -func signToken(t *testing.T, token *PToken, c *testClient) { - require.NotNil(t, token) - token.SetPublicKeys(&c.PublicKey) - - signH, err := c.Sign(token.Header.PublicKey) - require.NoError(t, err) - require.NotNil(t, signH) - - // data is not yet signed - keys := UnmarshalPublicKeys(&token.Token) - require.False(t, token.Verify(keys...)) - - signT, err := c.Sign(token.verificationData()) - require.NoError(t, err) - require.NotNil(t, signT) - - token.AddSignatures(signH, signT) - require.True(t, token.Verify(keys...)) -} - -func TestTokenStore(t *testing.T) { - s := NewSimpleStore() - - oid, err := refs.NewObjectID() - require.NoError(t, err) - - c := newTestClient(t) - require.NotNil(t, c) - pk := [][]byte{crypto.MarshalPublicKey(&c.PublicKey)} - - // create new token - token := s.New(TokenParams{ - ObjectID: []ObjectID{oid}, - OwnerID: c.OwnerID, - PublicKeys: pk, - }) - signToken(t, token, c) - - // check that it can be fetched - t1 := s.Fetch(token.ID) - require.NotNil(t, t1) - require.Equal(t, token, t1) - - // create and sign another token by the same client - t1 = s.New(TokenParams{ - ObjectID: []ObjectID{oid}, - OwnerID: c.OwnerID, - PublicKeys: pk, - }) - - signToken(t, t1, c) - - data := []byte{1, 2, 3} - sign, err := t1.SignData(data) - require.NoError(t, err) - require.Error(t, token.Header.VerifyData(data, sign)) - - sign, err = token.SignData(data) - require.NoError(t, err) - require.NoError(t, token.Header.VerifyData(data, sign)) - - s.Remove(token.ID) - require.Nil(t, s.Fetch(token.ID)) - require.NotNil(t, s.Fetch(t1.ID)) +func TestMapTokenStore_RemoveExpired(t *testing.T) { + // create some epoch number + e1 := uint64(1) + + // create private token that expires after e1 + tok1, err := NewPrivateToken(e1) + require.NoError(t, err) + + // create some greater than e1 epoch number + e2 := e1 + 1 + + // create private token that expires after e2 + tok2, err := NewPrivateToken(e2) + require.NoError(t, err) + + // create token store instance + s := NewMapTokenStore() + + // create test PrivateTokenKey + key := PrivateTokenKey{} + key.SetOwnerID(OwnerID{1, 2, 3}) + + // create IDs for tokens + id1, err := refs.NewUUID() + require.NoError(t, err) + id2, err := refs.NewUUID() + require.NoError(t, err) + + assertPresence := func(ids ...TokenID) { + for i := range ids { + key.SetTokenID(ids[i]) + _, err = s.Fetch(key) + require.NoError(t, err) + } + } + + assertAbsence := func(ids ...TokenID) { + for i := range ids { + key.SetTokenID(ids[i]) + _, err = s.Fetch(key) + require.EqualError(t, err, ErrPrivateTokenNotFound.Error()) + } + } + + // store both tokens + key.SetTokenID(id1) + require.NoError(t, s.Store(key, tok1)) + key.SetTokenID(id2) + require.NoError(t, s.Store(key, tok2)) + + // ascertain that both tokens are available + assertPresence(id1, id2) + + // perform cleaning for epoch in which both tokens are not expired + require.NoError(t, s.RemoveExpired(e1)) + + // ascertain that both tokens are still available + assertPresence(id1, id2) + + // perform cleaning for epoch greater than e1 and not greater than e2 + require.NoError(t, s.RemoveExpired(e1+1)) + + // ascertain that tok1 was removed + assertAbsence(id1) + + // ascertain that tok2 was not removed + assertPresence(id2) } diff --git a/session/types.go b/session/types.go index 4165291..ee13b92 100644 --- a/session/types.go +++ b/session/types.go @@ -1,181 +1,86 @@ package session import ( + "context" "crypto/ecdsa" - "encoding/binary" - "sync" - "github.com/nspcc-dev/neofs-api-go/chain" - "github.com/nspcc-dev/neofs-api-go/internal" "github.com/nspcc-dev/neofs-api-go/refs" - crypto "github.com/nspcc-dev/neofs-crypto" - "github.com/pkg/errors" + "github.com/nspcc-dev/neofs-api-go/service" ) -type ( - // ObjectID type alias. - ObjectID = refs.ObjectID - // OwnerID type alias. - OwnerID = refs.OwnerID - // TokenID type alias. - TokenID = refs.UUID +// PrivateToken is an interface of session private part. +type PrivateToken interface { + // PublicKey must return a binary representation of session public key. + PublicKey() []byte - // PToken is a wrapper around Token that allows to sign data - // and to do thread-safe manipulations. - PToken struct { - Token + // Sign must return the signature of passed data. + // + // Resulting signature must be verified by crypto.Verify function + // with the session public key. + Sign([]byte) ([]byte, error) - mtx *sync.Mutex - PrivateKey *ecdsa.PrivateKey - } -) - -const ( - // ErrWrongFirstEpoch is raised when passed Token contains wrong first epoch. - // First epoch is an epoch since token is valid - ErrWrongFirstEpoch = internal.Error("wrong first epoch") - - // ErrWrongLastEpoch is raised when passed Token contains wrong last epoch. - // Last epoch is an epoch until token is valid - ErrWrongLastEpoch = internal.Error("wrong last epoch") - - // ErrWrongOwner is raised when passed Token contains wrong OwnerID. - ErrWrongOwner = internal.Error("wrong owner") - - // ErrEmptyPublicKey is raised when passed Token contains wrong public key. - ErrEmptyPublicKey = internal.Error("empty public key") - - // ErrWrongObjectsCount is raised when passed Token contains wrong objects count. - ErrWrongObjectsCount = internal.Error("wrong objects count") - - // ErrWrongObjects is raised when passed Token contains wrong object ids. - ErrWrongObjects = internal.Error("wrong objects") - - // ErrInvalidSignature is raised when wrong signature is passed to VerificationHeader.VerifyData(). - ErrInvalidSignature = internal.Error("invalid signature") -) - -// verificationData returns byte array to sign. -// Note: protobuf serialization is inconsistent as -// wire order is unspecified. -func (m *Token) verificationData() (data []byte) { - var size int - if l := len(m.ObjectID); l > 0 { - size = m.ObjectID[0].Size() - data = make([]byte, 16+l*size) - } else { - data = make([]byte, 16) - } - binary.BigEndian.PutUint64(data, m.FirstEpoch) - binary.BigEndian.PutUint64(data[8:], m.LastEpoch) - for i := range m.ObjectID { - copy(data[16+i*size:], m.ObjectID[i].Bytes()) - } - return + // Expired must return true if and only if private token is expired in the given epoch number. + Expired(uint64) bool } -// IsSame checks if the passed token is valid and equal to current token -func (m *Token) IsSame(t *Token) error { - switch { - case m.FirstEpoch != t.FirstEpoch: - return ErrWrongFirstEpoch - case m.LastEpoch != t.LastEpoch: - return ErrWrongLastEpoch - case !m.OwnerID.Equal(t.OwnerID): - return ErrWrongOwner - case m.Header.PublicKey == nil: - return ErrEmptyPublicKey - case len(m.ObjectID) != len(t.ObjectID): - return ErrWrongObjectsCount - default: - for i := range m.ObjectID { - if !m.ObjectID[i].Equal(t.ObjectID[i]) { - return errors.Wrapf(ErrWrongObjects, "expect %s, actual: %s", m.ObjectID[i], t.ObjectID[i]) - } - } - } - return nil +// PrivateTokenKey is a structure of private token storage key. +type PrivateTokenKey struct { + owner OwnerID + token TokenID } -// Sign tries to sign current Token data and stores signature inside it. -func (m *Token) Sign(key *ecdsa.PrivateKey) error { - if err := m.Header.Sign(key); err != nil { - return err - } - - s, err := crypto.Sign(key, m.verificationData()) - if err != nil { - return err - } - - m.Signature = s - return nil +// PrivateTokenSource is an interface of private token storage with read access. +type PrivateTokenSource interface { + // Fetch must return the storage record corresponding to the passed key. + // + // Resulting error must be ErrPrivateTokenNotFound if there is no corresponding record. + Fetch(PrivateTokenKey) (PrivateToken, error) } -// SetPublicKeys sets owner's public keys to the token -func (m *Token) SetPublicKeys(keys ...*ecdsa.PublicKey) { - m.PublicKeys = m.PublicKeys[:0] - for i := range keys { - m.PublicKeys = append(m.PublicKeys, crypto.MarshalPublicKey(keys[i])) - } +// EpochLifetimeStore is an interface of the storage of elements that lifetime is limited by NeoFS epoch. +type EpochLifetimeStore interface { + // RemoveExpired must remove all elements that are expired in the given epoch. + RemoveExpired(uint64) error } -// Verify checks if token is correct and signed. -func (m *Token) Verify(keys ...*ecdsa.PublicKey) bool { - if m.FirstEpoch > m.LastEpoch { - return false - } - ownerFromKeys := chain.KeysToAddress(keys...) - if m.OwnerID.String() != ownerFromKeys { - return false - } +// PrivateTokenStore is an interface of the storage of private tokens addressable by TokenID. +type PrivateTokenStore interface { + PrivateTokenSource + EpochLifetimeStore - for i := range keys { - if m.Header.Verify(keys[i]) && crypto.Verify(keys[i], m.verificationData(), m.Signature) == nil { - return true - } - } - return false + // Store must save passed private token in the storage under the given key. + // + // Resulting error must be nil if private token was stored successfully. + Store(PrivateTokenKey, PrivateToken) error } -// AddSignatures adds token signatures. -func (t *PToken) AddSignatures(signH, signT []byte) { - t.mtx.Lock() - - t.Header.KeySignature = signH - t.Signature = signT - - t.mtx.Unlock() +// KeyStore is an interface of the storage of public keys addressable by OwnerID, +type KeyStore interface { + // Get must return the storage record corresponding to the passed key. + // + // Resulting error must be ErrKeyNotFound if there is no corresponding record. + Get(context.Context, OwnerID) ([]*ecdsa.PublicKey, error) } -// SignData signs data with session private key. -func (t *PToken) SignData(data []byte) ([]byte, error) { - return crypto.Sign(t.PrivateKey, data) +// CreateParamsSource is an interface of the container of session parameters with read access. +type CreateParamsSource interface { + refs.OwnerIDSource + service.LifetimeSource } -// VerifyData checks if signature of data by token is equal to sign. -func (m *VerificationHeader) VerifyData(data, sign []byte) error { - if crypto.Verify(crypto.UnmarshalPublicKey(m.PublicKey), data, sign) != nil { - return ErrInvalidSignature - } - return nil +// CreateParamsContainer is an interface of the container of session parameters. +type CreateParamsContainer interface { + refs.OwnerIDContainer + service.LifetimeContainer } -// Verify checks if verification header was issued by id. -func (m *VerificationHeader) Verify(keys ...*ecdsa.PublicKey) bool { - for i := range keys { - if crypto.Verify(keys[i], m.PublicKey, m.KeySignature) == nil { - return true - } - } - return false +// CreateResult is an interface of the container of an opened session info with read access. +type CreateResult interface { + service.TokenIDSource + service.SessionKeySource } -// UnmarshalPublicKeys returns unmarshal public keys from the token -func UnmarshalPublicKeys(t *Token) []*ecdsa.PublicKey { - r := make([]*ecdsa.PublicKey, 0, len(t.PublicKeys)) - for i := range t.PublicKeys { - r = append(r, crypto.UnmarshalPublicKey(t.PublicKeys[i])) - } - return r +// Creator is an interface of the tool for a session opening. +type Creator interface { + Create(context.Context, CreateParamsSource) (CreateResult, error) } diff --git a/session/types.pb.go b/session/types.pb.go deleted file mode 100644 index 01458dd..0000000 Binary files a/session/types.pb.go and /dev/null differ diff --git a/session/types.proto b/session/types.proto deleted file mode 100644 index 3ae49a3..0000000 --- a/session/types.proto +++ /dev/null @@ -1,35 +0,0 @@ -syntax = "proto3"; -package session; -option go_package = "github.com/nspcc-dev/neofs-api-go/session"; -option csharp_namespace = "NeoFS.API.Session"; - -import "github.com/gogo/protobuf/gogoproto/gogo.proto"; - -option (gogoproto.stable_marshaler_all) = true; - -message VerificationHeader { - // PublicKey is a session public key - bytes PublicKey = 1; - // KeySignature is a session public key signature. Signed by trusted side - bytes KeySignature = 2; -} - -// User token granting rights for object manipulation -message Token { - // Header carries verification data of session key - VerificationHeader Header = 1 [(gogoproto.nullable) = false]; - // OwnerID is an owner of manipulation object - bytes OwnerID = 2 [(gogoproto.customtype) = "OwnerID", (gogoproto.nullable) = false]; - // FirstEpoch is an initial epoch of token lifetime - uint64 FirstEpoch = 3; - // LastEpoch is a last epoch of token lifetime - uint64 LastEpoch = 4; - // ObjectID is an object identifier of manipulation object - repeated bytes ObjectID = 5 [(gogoproto.customtype) = "ObjectID", (gogoproto.nullable) = false]; - // Signature is a token signature, signed by owner of manipulation object - bytes Signature = 6; - // ID is a token identifier. valid UUIDv4 represented in bytes - bytes ID = 7 [(gogoproto.customtype) = "TokenID", (gogoproto.nullable) = false]; - // PublicKeys associated with owner - repeated bytes PublicKeys = 8; -} diff --git a/state/sign.go b/state/sign.go new file mode 100644 index 0000000..88f038c --- /dev/null +++ b/state/sign.go @@ -0,0 +1,67 @@ +package state + +import ( + "io" + + "github.com/nspcc-dev/neofs-api-go/service" +) + +// SignedData returns payload bytes of the request. +// +// Always returns an empty slice. +func (m NetmapRequest) SignedData() ([]byte, error) { + return make([]byte, 0), nil +} + +// SignedData returns payload bytes of the request. +// +// Always returns an empty slice. +func (m MetricsRequest) SignedData() ([]byte, error) { + return make([]byte, 0), nil +} + +// SignedData returns payload bytes of the request. +// +// Always returns an empty slice. +func (m HealthRequest) SignedData() ([]byte, error) { + return make([]byte, 0), nil +} + +// SignedData returns payload bytes of the request. +// +// Always returns an empty slice. +func (m DumpRequest) SignedData() ([]byte, error) { + return make([]byte, 0), nil +} + +// SignedData returns payload bytes of the request. +// +// Always returns an empty slice. +func (m DumpVarsRequest) SignedData() ([]byte, error) { + return make([]byte, 0), nil +} + +// SignedData returns payload bytes of the request. +func (m ChangeStateRequest) SignedData() ([]byte, error) { + return service.SignedDataFromReader(m) +} + +// SignedDataSize returns payload size of the request. +func (m ChangeStateRequest) SignedDataSize() int { + return m.GetState().Size() +} + +// ReadSignedData copies payload bytes to passed buffer. +// +// If the Request size is insufficient, io.ErrUnexpectedEOF returns. +func (m ChangeStateRequest) ReadSignedData(p []byte) (int, error) { + if len(p) < m.SignedDataSize() { + return 0, io.ErrUnexpectedEOF + } + + var off int + + off += copy(p[off:], m.GetState().Bytes()) + + return off, nil +} diff --git a/state/sign_test.go b/state/sign_test.go new file mode 100644 index 0000000..9b2bca9 --- /dev/null +++ b/state/sign_test.go @@ -0,0 +1,94 @@ +package state + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-crypto/test" + "github.com/stretchr/testify/require" +) + +func TestRequestSign(t *testing.T) { + sk := test.DecodeKey(0) + + type sigType interface { + service.SignedDataWithToken + service.SignKeyPairAccumulator + service.SignKeyPairSource + SetToken(*service.Token) + } + + items := []struct { + constructor func() sigType + payloadCorrupt []func(sigType) + }{ + { // NetmapRequest + constructor: func() sigType { + return new(NetmapRequest) + }, + }, + { // MetricsRequest + constructor: func() sigType { + return new(MetricsRequest) + }, + }, + { // HealthRequest + constructor: func() sigType { + return new(HealthRequest) + }, + }, + { // DumpRequest + constructor: func() sigType { + return new(DumpRequest) + }, + }, + { // DumpVarsRequest + constructor: func() sigType { + return new(DumpVarsRequest) + }, + }, + { + constructor: func() sigType { + return new(ChangeStateRequest) + }, + payloadCorrupt: []func(sigType){ + func(s sigType) { + req := s.(*ChangeStateRequest) + + req.SetState(req.GetState() + 1) + }, + }, + }, + } + + for _, item := range items { + { // token corruptions + v := item.constructor() + + token := new(service.Token) + v.SetToken(token) + + require.NoError(t, service.SignDataWithSessionToken(sk, v)) + + require.NoError(t, service.VerifyAccumulatedSignaturesWithToken(v)) + + token.SetSessionKey(append(token.GetSessionKey(), 1)) + + require.Error(t, service.VerifyAccumulatedSignaturesWithToken(v)) + } + + { // payload corruptions + for _, corruption := range item.payloadCorrupt { + v := item.constructor() + + require.NoError(t, service.SignDataWithSessionToken(sk, v)) + + require.NoError(t, service.VerifyAccumulatedSignaturesWithToken(v)) + + corruption(v) + + require.Error(t, service.VerifyAccumulatedSignaturesWithToken(v)) + } + } + } +} diff --git a/state/types.go b/state/types.go new file mode 100644 index 0000000..6b572db --- /dev/null +++ b/state/types.go @@ -0,0 +1,24 @@ +package state + +import ( + "encoding/binary" +) + +// SetState is a State field setter. +func (m *ChangeStateRequest) SetState(st ChangeStateRequest_State) { + m.State = st +} + +// Size returns the size of the state binary representation. +func (ChangeStateRequest_State) Size() int { + return 4 +} + +// Bytes returns the state binary representation. +func (x ChangeStateRequest_State) Bytes() []byte { + data := make([]byte, x.Size()) + + binary.BigEndian.PutUint32(data, uint32(x)) + + return data +} diff --git a/state/types_test.go b/state/types_test.go new file mode 100644 index 0000000..5d5f5de --- /dev/null +++ b/state/types_test.go @@ -0,0 +1,18 @@ +package state + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestChangeStateRequestGettersSetters(t *testing.T) { + t.Run("state", func(t *testing.T) { + st := ChangeStateRequest_State(1) + m := new(ChangeStateRequest) + + m.SetState(st) + + require.Equal(t, st, m.GetState()) + }) +}