Merge branch 'release/0.7.4'
This commit is contained in:
commit
841f261e09
80 changed files with 4903 additions and 1155 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -1,6 +1,22 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
This is the changelog for NeoFS-API-Go
|
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
|
## [0.7.1] - 2020-04-20
|
||||||
|
|
||||||
### Added
|
### 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.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.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.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
|
||||||
|
|
2
Makefile
2
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
|
PROTO_URL=https://github.com/nspcc-dev/neofs-api/archive/$(PROTO_VERSION).tar.gz
|
||||||
|
|
||||||
B=\033[0;1m
|
B=\033[0;1m
|
||||||
|
|
167
accounting/sign.go
Normal file
167
accounting/sign.go
Normal file
|
@ -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
|
||||||
|
}
|
185
accounting/sign_test.go
Normal file
185
accounting/sign_test.go
Normal file
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -351,3 +351,103 @@ func (m *Settlement) Equal(s *Settlement) bool {
|
||||||
}
|
}
|
||||||
return len(m.Transactions) == 0 || reflect.DeepEqual(m.Transactions, s.Transactions)
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -84,3 +84,110 @@ func TestCheque(t *testing.T) {
|
||||||
require.Equal(t, cheque.Amount, decimal.NewGAS(42))
|
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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
46
bootstrap/sign.go
Normal file
46
bootstrap/sign.go
Normal file
|
@ -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
|
||||||
|
}
|
82
bootstrap/sign_test.go
Normal file
82
bootstrap/sign_test.go
Normal file
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package bootstrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -27,6 +28,8 @@ var (
|
||||||
_ proto.Message = (*SpreadMap)(nil)
|
_ proto.Message = (*SpreadMap)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var requestEndianness = binary.BigEndian
|
||||||
|
|
||||||
// Equals checks whether two NodeInfo has same address.
|
// Equals checks whether two NodeInfo has same address.
|
||||||
func (m NodeInfo) Equals(n1 NodeInfo) bool {
|
func (m NodeInfo) Equals(n1 NodeInfo) bool {
|
||||||
return m.Address == n1.Address && bytes.Equal(m.PubKey, n1.PubKey)
|
return m.Address == n1.Address && bytes.Equal(m.PubKey, n1.PubKey)
|
||||||
|
@ -98,3 +101,37 @@ func (m SpreadMap) String() string {
|
||||||
", " +
|
", " +
|
||||||
"Netmap: [" + strings.Join(result, ",") + "]>"
|
"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
|
||||||
|
}
|
||||||
|
|
39
bootstrap/types_test.go
Normal file
39
bootstrap/types_test.go
Normal file
|
@ -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())
|
||||||
|
})
|
||||||
|
}
|
137
container/sign.go
Normal file
137
container/sign.go
Normal file
|
@ -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
|
||||||
|
}
|
143
container/sign_test.go
Normal file
143
container/sign_test.go
Normal file
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -55,3 +55,88 @@ func TestCID(t *testing.T) {
|
||||||
require.Equal(t, cid1, cid2)
|
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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -149,7 +149,6 @@ calculated for XORed data.
|
||||||
| ----- | ---- | ----- | ----------- |
|
| ----- | ---- | ----- | ----------- |
|
||||||
| Address | [refs.Address](#refs.Address) | | Address of object (container id + object id) |
|
| Address | [refs.Address](#refs.Address) | | Address of object (container id + object id) |
|
||||||
| OwnerID | [bytes](#bytes) | | OwnerID is a wallet address |
|
| 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) |
|
| 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) |
|
| 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 |
|
| Field | Type | Label | Description |
|
||||||
| ----- | ---- | ----- | ----------- |
|
| ----- | ---- | ----- | ----------- |
|
||||||
| Address | [refs.Address](#refs.Address) | | Address of object (container id + object id) |
|
| 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) |
|
| 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) |
|
| 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) |
|
| 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 |
|
| 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) |
|
| 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) |
|
| 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 |
|
| Field | Type | Label | Description |
|
||||||
| ----- | ---- | ----- | ----------- |
|
| ----- | ---- | ----- | ----------- |
|
||||||
| Object | [Object](#object.Object) | | Object with at least container id and owner id fields |
|
| 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) |
|
| 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 |
|
| 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) |
|
| Transform | [Transform](#object.Transform) | | Transform defines transform operation (e.g. payload split) |
|
||||||
| Tombstone | [Tombstone](#object.Tombstone) | | Tombstone header that set up in deleted objects |
|
| 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 |
|
| HomoHash | [bytes](#bytes) | | HomoHash is a homomorphic hash of original object payload |
|
||||||
| PayloadChecksum | [bytes](#bytes) | | PayloadChecksum of actual object's 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 |
|
| Integrity | [IntegrityHeader](#object.IntegrityHeader) | | Integrity header with checksum of all above headers in the object |
|
||||||
|
|
|
@ -14,8 +14,10 @@
|
||||||
|
|
||||||
- Messages
|
- Messages
|
||||||
- [RequestVerificationHeader](#service.RequestVerificationHeader)
|
- [RequestVerificationHeader](#service.RequestVerificationHeader)
|
||||||
- [RequestVerificationHeader.Sign](#service.RequestVerificationHeader.Sign)
|
|
||||||
- [RequestVerificationHeader.Signature](#service.RequestVerificationHeader.Signature)
|
- [RequestVerificationHeader.Signature](#service.RequestVerificationHeader.Signature)
|
||||||
|
- [Token](#service.Token)
|
||||||
|
- [Token.Info](#service.Token.Info)
|
||||||
|
- [TokenLifetime](#service.TokenLifetime)
|
||||||
|
|
||||||
|
|
||||||
- [service/verify_test.proto](#service/verify_test.proto)
|
- [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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
||||||
|
|
||||||
|
|
||||||
<a name="service.ResponseMetaHeader"></a>
|
<a name="service.ResponseMetaHeader"></a>
|
||||||
|
@ -88,18 +91,7 @@ RequestVerificationHeader is a set of signatures of every NeoFS Node that proces
|
||||||
| Field | Type | Label | Description |
|
| Field | Type | Label | Description |
|
||||||
| ----- | ---- | ----- | ----------- |
|
| ----- | ---- | ----- | ----------- |
|
||||||
| Signatures | [RequestVerificationHeader.Signature](#service.RequestVerificationHeader.Signature) | repeated | Signatures is a set of signatures of every passed NeoFS Node |
|
| Signatures | [RequestVerificationHeader.Signature](#service.RequestVerificationHeader.Signature) | repeated | Signatures is a set of signatures of every passed NeoFS Node |
|
||||||
|
| Token | [Token](#service.Token) | | Token is a token of the session within which the request is sent |
|
||||||
|
|
||||||
<a name="service.RequestVerificationHeader.Sign"></a>
|
|
||||||
|
|
||||||
### 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. |
|
|
||||||
|
|
||||||
|
|
||||||
<a name="service.RequestVerificationHeader.Signature"></a>
|
<a name="service.RequestVerificationHeader.Signature"></a>
|
||||||
|
@ -110,11 +102,68 @@ RequestVerificationHeader is a set of signatures of every NeoFS Node that proces
|
||||||
|
|
||||||
| Field | Type | Label | Description |
|
| Field | Type | Label | Description |
|
||||||
| ----- | ---- | ----- | ----------- |
|
| ----- | ---- | ----- | ----------- |
|
||||||
| Sign | [RequestVerificationHeader.Sign](#service.RequestVerificationHeader.Sign) | | Sign is a signature and public key of the request. |
|
| Sign | [bytes](#bytes) | | Sign is signature of the request or session key. |
|
||||||
| 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. |
|
| Peer | [bytes](#bytes) | | Peer is compressed public key used for signature. |
|
||||||
|
|
||||||
|
|
||||||
|
<a name="service.Token"></a>
|
||||||
|
|
||||||
|
### 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 |
|
||||||
|
|
||||||
|
|
||||||
|
<a name="service.Token.Info"></a>
|
||||||
|
|
||||||
|
### 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 |
|
||||||
|
|
||||||
|
|
||||||
|
<a name="service.TokenLifetime"></a>
|
||||||
|
|
||||||
|
### 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 |
|
||||||
|
|
||||||
<!-- end messages -->
|
<!-- end messages -->
|
||||||
|
|
||||||
|
|
||||||
|
<a name="service.Token.Info.Verb"></a>
|
||||||
|
|
||||||
|
### 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 |
|
||||||
|
|
||||||
|
|
||||||
<!-- end enums -->
|
<!-- end enums -->
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,13 +12,6 @@
|
||||||
- [CreateResponse](#session.CreateResponse)
|
- [CreateResponse](#session.CreateResponse)
|
||||||
|
|
||||||
|
|
||||||
- [session/types.proto](#session/types.proto)
|
|
||||||
|
|
||||||
- Messages
|
|
||||||
- [Token](#session.Token)
|
|
||||||
- [VerificationHeader](#session.VerificationHeader)
|
|
||||||
|
|
||||||
|
|
||||||
- [Scalar Value Types](#scalar-value-types)
|
- [Scalar Value Types](#scalar-value-types)
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,22 +30,13 @@
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
rpc Create(stream CreateRequest) returns (stream CreateResponse);
|
rpc Create(CreateRequest) returns (CreateResponse);
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Method Create
|
#### Method Create
|
||||||
|
|
||||||
Create is a method that used to open a trusted session to manipulate
|
Create opens new session between the client and the server
|
||||||
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`.
|
|
||||||
|
|
||||||
| Name | Input | Output |
|
| Name | Input | Output |
|
||||||
| ---- | ----- | ------ |
|
| ---- | ----- | ------ |
|
||||||
|
@ -63,13 +47,13 @@ session key. Session is established during 4-step handshake in one gRPC stream
|
||||||
<a name="session.CreateRequest"></a>
|
<a name="session.CreateRequest"></a>
|
||||||
|
|
||||||
### Message CreateRequest
|
### Message CreateRequest
|
||||||
|
CreateRequest carries an information necessary for opening a session
|
||||||
|
|
||||||
|
|
||||||
| Field | Type | Label | Description |
|
| 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. |
|
| OwnerID | [bytes](#bytes) | | OwnerID carries an identifier of a session initiator |
|
||||||
| Signed | [Token](#session.Token) | | Signed Init message response (Unsigned) from server with user private key |
|
| 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) |
|
| 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) |
|
| 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
|
||||||
<a name="session.CreateResponse"></a>
|
<a name="session.CreateResponse"></a>
|
||||||
|
|
||||||
### Message CreateResponse
|
### Message CreateResponse
|
||||||
|
CreateResponse carries an information about the opened session
|
||||||
|
|
||||||
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
<!-- end messages -->
|
|
||||||
|
|
||||||
<!-- end enums -->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a name="session/types.proto"></a>
|
|
||||||
<p align="right"><a href="#top">Top</a></p>
|
|
||||||
|
|
||||||
## session/types.proto
|
|
||||||
|
|
||||||
|
|
||||||
<!-- end services -->
|
|
||||||
|
|
||||||
|
|
||||||
<a name="session.Token"></a>
|
|
||||||
|
|
||||||
### Message Token
|
|
||||||
User token granting rights for object manipulation
|
|
||||||
|
|
||||||
|
|
||||||
| Field | Type | Label | Description |
|
| Field | Type | Label | Description |
|
||||||
| ----- | ---- | ----- | ----------- |
|
| ----- | ---- | ----- | ----------- |
|
||||||
| Header | [VerificationHeader](#session.VerificationHeader) | | Header carries verification data of session key |
|
| ID | [bytes](#bytes) | | ID carries an identifier of session token |
|
||||||
| OwnerID | [bytes](#bytes) | | OwnerID is an owner of manipulation object |
|
| SessionKey | [bytes](#bytes) | | SessionKey carries a session public key |
|
||||||
| 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 |
|
|
||||||
|
|
||||||
|
|
||||||
<a name="session.VerificationHeader"></a>
|
|
||||||
|
|
||||||
### 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 |
|
|
||||||
|
|
||||||
<!-- end messages -->
|
<!-- end messages -->
|
||||||
|
|
||||||
|
|
|
@ -19,21 +19,6 @@ func (m Object) IsLinking() bool {
|
||||||
return false
|
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
|
// Links returns slice of ids of specified link type
|
||||||
func (m *Object) Links(t Link_Type) []ID {
|
func (m *Object) Links(t Link_Type) []ID {
|
||||||
var res []ID
|
var res []ID
|
||||||
|
|
|
@ -31,7 +31,7 @@ type (
|
||||||
// All object operations must have TTL, Epoch, Type, Container ID and
|
// All object operations must have TTL, Epoch, Type, Container ID and
|
||||||
// permission of usage previous network map.
|
// permission of usage previous network map.
|
||||||
Request interface {
|
Request interface {
|
||||||
service.MetaHeader
|
service.SeizedRequestMetaContainer
|
||||||
|
|
||||||
CID() CID
|
CID() CID
|
||||||
Type() RequestType
|
Type() RequestType
|
||||||
|
|
Binary file not shown.
|
@ -5,7 +5,6 @@ option csharp_namespace = "NeoFS.API.Object";
|
||||||
|
|
||||||
import "refs/types.proto";
|
import "refs/types.proto";
|
||||||
import "object/types.proto";
|
import "object/types.proto";
|
||||||
import "session/types.proto";
|
|
||||||
import "service/meta.proto";
|
import "service/meta.proto";
|
||||||
import "service/verify.proto";
|
import "service/verify.proto";
|
||||||
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
|
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
|
||||||
|
@ -58,8 +57,6 @@ service Service {
|
||||||
message GetRequest {
|
message GetRequest {
|
||||||
// Address of object (container id + object id)
|
// Address of object (container id + object id)
|
||||||
refs.Address Address = 1 [(gogoproto.nullable) = false];
|
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)
|
// RequestMetaHeader contains information about request meta headers (should be embedded into message)
|
||||||
service.RequestMetaHeader Meta = 98 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
|
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)
|
// 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 {
|
message PutHeader {
|
||||||
// Object with at least container id and owner id fields
|
// Object with at least container id and owner id fields
|
||||||
Object Object = 1;
|
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)
|
// 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 {
|
oneof R {
|
||||||
|
@ -112,8 +107,6 @@ message DeleteRequest {
|
||||||
refs.Address Address = 1 [(gogoproto.nullable) = false];
|
refs.Address Address = 1 [(gogoproto.nullable) = false];
|
||||||
// OwnerID is a wallet address
|
// OwnerID is a wallet address
|
||||||
bytes OwnerID = 2 [(gogoproto.nullable) = false, (gogoproto.customtype) = "OwnerID"];
|
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)
|
// RequestMetaHeader contains information about request meta headers (should be embedded into message)
|
||||||
service.RequestMetaHeader Meta = 98 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
|
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)
|
// 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"];
|
refs.Address Address = 1 [(gogoproto.nullable) = false, (gogoproto.customtype) = "Address"];
|
||||||
// FullHeaders can be set true for extended headers in the object
|
// FullHeaders can be set true for extended headers in the object
|
||||||
bool FullHeaders = 2;
|
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)
|
// RequestMetaHeader contains information about request meta headers (should be embedded into message)
|
||||||
service.RequestMetaHeader Meta = 98 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
|
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)
|
// RequestVerificationHeader is a set of signatures of every NeoFS Node that processed request (should be embedded into message)
|
||||||
|
|
|
@ -16,8 +16,8 @@ func TestRequest(t *testing.T) {
|
||||||
&DeleteRequest{},
|
&DeleteRequest{},
|
||||||
&GetRangeRequest{},
|
&GetRangeRequest{},
|
||||||
&GetRangeHashRequest{},
|
&GetRangeHashRequest{},
|
||||||
MakePutRequestHeader(nil, nil),
|
MakePutRequestHeader(nil),
|
||||||
MakePutRequestHeader(&Object{}, nil),
|
MakePutRequestHeader(&Object{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
types := []RequestType{
|
types := []RequestType{
|
||||||
|
|
259
object/sign.go
Normal file
259
object/sign.go
Normal file
|
@ -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()...)
|
||||||
|
}
|
189
object/sign_test.go
Normal file
189
object/sign_test.go
Normal file
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
140
object/types.go
140
object/types.go
|
@ -3,11 +3,14 @@ package object
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"github.com/gogo/protobuf/proto"
|
"github.com/gogo/protobuf/proto"
|
||||||
"github.com/nspcc-dev/neofs-api-go/internal"
|
"github.com/nspcc-dev/neofs-api-go/internal"
|
||||||
"github.com/nspcc-dev/neofs-api-go/refs"
|
"github.com/nspcc-dev/neofs-api-go/refs"
|
||||||
"github.com/nspcc-dev/neofs-api-go/session"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -19,9 +22,6 @@ type (
|
||||||
// Address is a type alias of object Address.
|
// Address is a type alias of object Address.
|
||||||
Address = refs.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
|
// PositionReader defines object reader that returns slice of bytes
|
||||||
// for specified object and data range.
|
// for specified object and data range.
|
||||||
PositionReader interface {
|
PositionReader interface {
|
||||||
|
@ -60,8 +60,8 @@ const (
|
||||||
TransformHdr
|
TransformHdr
|
||||||
// TombstoneHdr is a tombstone header type.
|
// TombstoneHdr is a tombstone header type.
|
||||||
TombstoneHdr
|
TombstoneHdr
|
||||||
// VerifyHdr is a verification header type.
|
// TokenHdr is a token header type.
|
||||||
VerifyHdr
|
TokenHdr
|
||||||
// HomoHashHdr is a homomorphic hash header type.
|
// HomoHashHdr is a homomorphic hash header type.
|
||||||
HomoHashHdr
|
HomoHashHdr
|
||||||
// PayloadChecksumHdr is a payload checksum header type.
|
// 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)
|
_, ok = m.Value.(*Header_Transform)
|
||||||
case *Header_Tombstone:
|
case *Header_Tombstone:
|
||||||
_, ok = m.Value.(*Header_Tombstone)
|
_, ok = m.Value.(*Header_Tombstone)
|
||||||
case *Header_Verify:
|
case *Header_Token:
|
||||||
_, ok = m.Value.(*Header_Verify)
|
_, ok = m.Value.(*Header_Token)
|
||||||
case *Header_HomoHash:
|
case *Header_HomoHash:
|
||||||
_, ok = m.Value.(*Header_HomoHash)
|
_, ok = m.Value.(*Header_HomoHash)
|
||||||
case *Header_PayloadChecksum:
|
case *Header_PayloadChecksum:
|
||||||
|
@ -205,8 +205,8 @@ func HeaderType(t headerType) Pred {
|
||||||
return func(h *Header) bool { _, ok := h.Value.(*Header_Transform); return ok }
|
return func(h *Header) bool { _, ok := h.Value.(*Header_Transform); return ok }
|
||||||
case TombstoneHdr:
|
case TombstoneHdr:
|
||||||
return func(h *Header) bool { _, ok := h.Value.(*Header_Tombstone); return ok }
|
return func(h *Header) bool { _, ok := h.Value.(*Header_Tombstone); return ok }
|
||||||
case VerifyHdr:
|
case TokenHdr:
|
||||||
return func(h *Header) bool { _, ok := h.Value.(*Header_Verify); return ok }
|
return func(h *Header) bool { _, ok := h.Value.(*Header_Token); return ok }
|
||||||
case HomoHashHdr:
|
case HomoHashHdr:
|
||||||
return func(h *Header) bool { _, ok := h.Value.(*Header_HomoHash); return ok }
|
return func(h *Header) bool { _, ok := h.Value.(*Header_HomoHash); return ok }
|
||||||
case PayloadChecksumHdr:
|
case PayloadChecksumHdr:
|
||||||
|
@ -251,6 +251,12 @@ func (m *Object) CopyTo(o *Object) {
|
||||||
HomoHash: v.HomoHash,
|
HomoHash: v.HomoHash,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
case *Header_Token:
|
||||||
|
o.Headers[i] = Header{
|
||||||
|
Value: &Header_Token{
|
||||||
|
Token: v.Token,
|
||||||
|
},
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
o.Headers[i] = *proto.Clone(&m.Headers[i]).(*Header)
|
o.Headers[i] = *proto.Clone(&m.Headers[i]).(*Header)
|
||||||
}
|
}
|
||||||
|
@ -266,3 +272,117 @@ func (m Object) Address() *refs.Address {
|
||||||
CID: m.SystemHeader.CID,
|
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
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
|
@ -4,7 +4,7 @@ option go_package = "github.com/nspcc-dev/neofs-api-go/object";
|
||||||
option csharp_namespace = "NeoFS.API.Object";
|
option csharp_namespace = "NeoFS.API.Object";
|
||||||
|
|
||||||
import "refs/types.proto";
|
import "refs/types.proto";
|
||||||
import "session/types.proto";
|
import "service/verify.proto";
|
||||||
import "storagegroup/types.proto";
|
import "storagegroup/types.proto";
|
||||||
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
|
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
|
||||||
|
|
||||||
|
@ -36,8 +36,8 @@ message Header {
|
||||||
Transform Transform = 4;
|
Transform Transform = 4;
|
||||||
// Tombstone header that set up in deleted objects
|
// Tombstone header that set up in deleted objects
|
||||||
Tombstone Tombstone = 5;
|
Tombstone Tombstone = 5;
|
||||||
// Verify header that contains session public key and user's signature
|
// Token header contains token of the session within which the object was created
|
||||||
session.VerificationHeader Verify = 6;
|
service.Token Token = 6;
|
||||||
// HomoHash is a homomorphic hash of original object payload
|
// HomoHash is a homomorphic hash of original object payload
|
||||||
bytes HomoHash = 7 [(gogoproto.customtype) = "Hash"];
|
bytes HomoHash = 7 [(gogoproto.customtype) = "Hash"];
|
||||||
// PayloadChecksum of actual object's payload
|
// PayloadChecksum of actual object's payload
|
||||||
|
@ -70,6 +70,8 @@ message SystemHeader {
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreationPoint {
|
message CreationPoint {
|
||||||
|
option (gogoproto.goproto_stringer) = false;
|
||||||
|
|
||||||
// UnixTime is a date of creation in unixtime format
|
// UnixTime is a date of creation in unixtime format
|
||||||
int64 UnixTime = 1;
|
int64 UnixTime = 1;
|
||||||
// Epoch is a date of creation in NeoFS epochs
|
// Epoch is a date of creation in NeoFS epochs
|
||||||
|
|
201
object/types_test.go
Normal file
201
object/types_test.go
Normal file
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/session"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -46,11 +45,10 @@ func (b ByteSize) String() string {
|
||||||
|
|
||||||
// MakePutRequestHeader combines object and session token value
|
// MakePutRequestHeader combines object and session token value
|
||||||
// into header of object put request.
|
// into header of object put request.
|
||||||
func MakePutRequestHeader(obj *Object, token *session.Token) *PutRequest {
|
func MakePutRequestHeader(obj *Object) *PutRequest {
|
||||||
return &PutRequest{
|
return &PutRequest{
|
||||||
R: &PutRequest_Header{Header: &PutRequest_PutHeader{
|
R: &PutRequest_Header{Header: &PutRequest_PutHeader{
|
||||||
Object: obj,
|
Object: obj,
|
||||||
Token: token,
|
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,7 +77,7 @@ func (m Object) Verify() error {
|
||||||
integrity := ih.Value.(*Header_Integrity).Integrity
|
integrity := ih.Value.(*Header_Integrity).Integrity
|
||||||
|
|
||||||
// Prepare structures
|
// Prepare structures
|
||||||
_, vh := m.LastHeader(HeaderType(VerifyHdr))
|
_, vh := m.LastHeader(HeaderType(TokenHdr))
|
||||||
if vh == nil {
|
if vh == nil {
|
||||||
_, pkh := m.LastHeader(HeaderType(PublicKeyHdr))
|
_, pkh := m.LastHeader(HeaderType(PublicKeyHdr))
|
||||||
if pkh == nil {
|
if pkh == nil {
|
||||||
|
@ -85,7 +85,7 @@ func (m Object) Verify() error {
|
||||||
}
|
}
|
||||||
pubkey = pkh.Value.(*Header_PublicKey).PublicKey.Value
|
pubkey = pkh.Value.(*Header_PublicKey).PublicKey.Value
|
||||||
} else {
|
} else {
|
||||||
pubkey = vh.Value.(*Header_Verify).Verify.PublicKey
|
pubkey = vh.Value.(*Header_Token).Token.GetSessionKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify signature
|
// Verify signature
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/nspcc-dev/neofs-api-go/container"
|
"github.com/nspcc-dev/neofs-api-go/container"
|
||||||
"github.com/nspcc-dev/neofs-api-go/refs"
|
"github.com/nspcc-dev/neofs-api-go/refs"
|
||||||
"github.com/nspcc-dev/neofs-api-go/session"
|
|
||||||
crypto "github.com/nspcc-dev/neofs-crypto"
|
crypto "github.com/nspcc-dev/neofs-crypto"
|
||||||
"github.com/nspcc-dev/neofs-crypto/test"
|
"github.com/nspcc-dev/neofs-crypto/test"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -77,11 +76,10 @@ func TestObject_Verify(t *testing.T) {
|
||||||
|
|
||||||
dataPK := crypto.MarshalPublicKey(&sessionkey.PublicKey)
|
dataPK := crypto.MarshalPublicKey(&sessionkey.PublicKey)
|
||||||
signature, err = crypto.Sign(key, dataPK)
|
signature, err = crypto.Sign(key, dataPK)
|
||||||
vh := &session.VerificationHeader{
|
tok := new(Token)
|
||||||
PublicKey: dataPK,
|
tok.SetSignature(signature)
|
||||||
KeySignature: signature,
|
tok.SetSessionKey(dataPK)
|
||||||
}
|
obj.AddHeader(&Header{Value: &Header_Token{Token: tok}})
|
||||||
obj.SetVerificationHeader(vh)
|
|
||||||
|
|
||||||
// validation header is not last
|
// validation header is not last
|
||||||
t.Run("error validation header is not last", func(t *testing.T) {
|
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.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}})
|
obj.SetHeader(&Header{Value: &Header_Integrity{ih}})
|
||||||
|
|
||||||
t.Run("error invalid header checksum", func(t *testing.T) {
|
t.Run("error invalid header checksum", func(t *testing.T) {
|
||||||
|
@ -115,7 +113,7 @@ func TestObject_Verify(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
obj.SetHeader(genIH)
|
obj.SetHeader(genIH)
|
||||||
|
|
||||||
t.Run("correct with vh", func(t *testing.T) {
|
t.Run("correct with tok", func(t *testing.T) {
|
||||||
err = obj.Verify()
|
err = obj.Verify()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
@ -123,7 +121,7 @@ func TestObject_Verify(t *testing.T) {
|
||||||
pkh := Header{Value: &Header_PublicKey{&PublicKey{
|
pkh := Header{Value: &Header_PublicKey{&PublicKey{
|
||||||
Value: crypto.MarshalPublicKey(&key.PublicKey),
|
Value: crypto.MarshalPublicKey(&key.PublicKey),
|
||||||
}}}
|
}}}
|
||||||
// replace vh with pkh
|
// replace tok with pkh
|
||||||
obj.Headers[len(obj.Headers)-2] = pkh
|
obj.Headers[len(obj.Headers)-2] = pkh
|
||||||
// re-sign object
|
// re-sign object
|
||||||
obj.Sign(sessionkey)
|
obj.Sign(sessionkey)
|
||||||
|
|
|
@ -37,6 +37,23 @@ type (
|
||||||
OwnerID chain.WalletAddress
|
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 (
|
const (
|
||||||
// UUIDSize contains size of UUID.
|
// UUIDSize contains size of UUID.
|
||||||
UUIDSize = 16
|
UUIDSize = 16
|
||||||
|
|
20
service/alias.go
Normal file
20
service/alias.go
Normal file
|
@ -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
|
11
service/epoch.go
Normal file
11
service/epoch.go
Normal file
|
@ -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
|
||||||
|
}
|
21
service/epoch_test.go
Normal file
21
service/epoch_test.go
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
49
service/errors.go
Normal file
49
service/errors.go
Normal file
|
@ -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")
|
122
service/meta.go
122
service/meta.go
|
@ -1,127 +1,13 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
// CutMeta returns current value and sets RequestMetaHeader to empty value.
|
||||||
"github.com/nspcc-dev/neofs-api-go/internal"
|
func (m *RequestMetaHeader) CutMeta() RequestMetaHeader {
|
||||||
"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 {
|
|
||||||
cp := *m
|
cp := *m
|
||||||
m.Reset()
|
m.Reset()
|
||||||
return cp
|
return cp
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestoreMeta sets current RequestMetaHeader to passed value.
|
// RestoreMeta sets current RequestMetaHeader to passed value.
|
||||||
func (m *RequestMetaHeader) RestoreMeta(v RequestMetaHeader) { *m = v }
|
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
|
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
|
@ -17,6 +17,8 @@ message RequestMetaHeader {
|
||||||
// Version defines protocol version
|
// Version defines protocol version
|
||||||
// TODO: not used for now, should be implemented in future
|
// TODO: not used for now, should be implemented in future
|
||||||
uint32 Version = 3;
|
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
|
// ResponseMetaHeader contains meta information based on request processing by server
|
||||||
|
|
|
@ -3,102 +3,23 @@ package service
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockedRequest struct {
|
func TestCutRestoreMeta(t *testing.T) {
|
||||||
msg string
|
items := []func() SeizedMetaHeaderContainer{
|
||||||
name string
|
func() SeizedMetaHeaderContainer {
|
||||||
code codes.Code
|
m := new(RequestMetaHeader)
|
||||||
handler TTLCondition
|
m.SetEpoch(1)
|
||||||
RequestMetaHeader
|
return m
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range tests {
|
for _, item := range items {
|
||||||
tt := tests[i]
|
v1 := item()
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
m1 := v1.CutMeta()
|
||||||
before := tt.GetTTL()
|
v1.RestoreMeta(m1)
|
||||||
err := ProcessRequestTTL(&tt, tt.handler)
|
|
||||||
if tt.msg != "" {
|
|
||||||
require.Errorf(t, err, tt.msg)
|
|
||||||
|
|
||||||
state, ok := status.FromError(err)
|
require.Equal(t, item(), v1)
|
||||||
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_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())
|
|
||||||
}
|
|
||||||
|
|
6
service/raw.go
Normal file
6
service/raw.go
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
// SetRaw is a Raw field setter.
|
||||||
|
func (m *RequestMetaHeader) SetRaw(raw bool) {
|
||||||
|
m.Raw = raw
|
||||||
|
}
|
24
service/raw_test.go
Normal file
24
service/raw_test.go
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
// NodeRole to identify in Bootstrap service.
|
import "encoding/binary"
|
||||||
type NodeRole int32
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
_ NodeRole = iota
|
_ NodeRole = iota
|
||||||
|
@ -22,3 +21,17 @@ func (nt NodeRole) String() string {
|
||||||
return "Unknown"
|
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
|
||||||
|
}
|
||||||
|
|
229
service/sign.go
Normal file
229
service/sign.go
Normal file
|
@ -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(),
|
||||||
|
})
|
||||||
|
}
|
326
service/sign_test.go
Normal file
326
service/sign_test.go
Normal file
|
@ -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))
|
||||||
|
}
|
231
service/token.go
Normal file
231
service/token.go
Normal file
|
@ -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
|
||||||
|
}
|
219
service/token_test.go
Normal file
219
service/token_test.go
Normal file
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
63
service/ttl.go
Normal file
63
service/ttl.go
Normal file
|
@ -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
|
||||||
|
}
|
99
service/ttl_test.go
Normal file
99
service/ttl_test.go
Normal file
|
@ -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())
|
||||||
|
}
|
256
service/types.go
Normal file
256
service/types.go
Normal file
|
@ -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
|
||||||
|
}
|
18
service/utils.go
Normal file
18
service/utils.go
Normal file
|
@ -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
|
||||||
|
}
|
34
service/utils_test.go
Normal file
34
service/utils_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -2,226 +2,69 @@ package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/internal"
|
"github.com/nspcc-dev/neofs-api-go/internal"
|
||||||
"github.com/nspcc-dev/neofs-api-go/refs"
|
|
||||||
crypto "github.com/nspcc-dev/neofs-crypto"
|
crypto "github.com/nspcc-dev/neofs-crypto"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
// GetSessionToken returns SessionToken interface of Token field.
|
||||||
// VerifiableRequest adds possibility to sign and verify request header.
|
//
|
||||||
VerifiableRequest interface {
|
// If token field value is nil, nil returns.
|
||||||
Size() int
|
func (m RequestVerificationHeader) GetSessionToken() SessionToken {
|
||||||
MarshalTo([]byte) (int, error)
|
if t := m.GetToken(); t != nil {
|
||||||
AddSignature(*RequestVerificationHeader_Signature)
|
return t
|
||||||
GetSignatures() []*RequestVerificationHeader_Signature
|
|
||||||
SetSignatures([]*RequestVerificationHeader_Signature)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MaintainableRequest adds possibility to set and get (+validate)
|
return nil
|
||||||
// owner (client) public key from RequestVerificationHeader.
|
}
|
||||||
MaintainableRequest interface {
|
|
||||||
GetOwner() (*ecdsa.PublicKey, error)
|
// AddSignKey adds new element to Signatures field.
|
||||||
SetOwner(*ecdsa.PublicKey, []byte)
|
//
|
||||||
GetLastPeer() (*ecdsa.PublicKey, error)
|
// 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 (
|
return res
|
||||||
// ErrCannotLoadPublicKey is raised when cannot unmarshal public key from RequestVerificationHeader_Sign.
|
}
|
||||||
ErrCannotLoadPublicKey = internal.Error("cannot load public key")
|
|
||||||
|
|
||||||
// ErrCannotFindOwner is raised when signatures empty in GetOwner.
|
// GetSignature returns the result of a Sign field getter.
|
||||||
ErrCannotFindOwner = internal.Error("cannot find owner public key")
|
func (m RequestVerificationHeader_Signature) GetSignature() []byte {
|
||||||
|
return m.GetSign()
|
||||||
|
}
|
||||||
|
|
||||||
// ErrWrongOwner is raised when passed OwnerID not equal to present PublicKey
|
// GetPublicKey unmarshals and returns the result of a Peer field getter.
|
||||||
ErrWrongOwner = internal.Error("wrong owner")
|
func (m RequestVerificationHeader_Signature) GetPublicKey() *ecdsa.PublicKey {
|
||||||
)
|
return crypto.UnmarshalPublicKey(m.GetPeer())
|
||||||
|
}
|
||||||
|
|
||||||
// SetSignatures replaces signatures stored in RequestVerificationHeader.
|
// SetSignatures replaces signatures stored in RequestVerificationHeader.
|
||||||
func (m *RequestVerificationHeader) SetSignatures(signatures []*RequestVerificationHeader_Signature) {
|
func (m *RequestVerificationHeader) SetSignatures(signatures []*RequestVerificationHeader_Signature) {
|
||||||
m.Signatures = signatures
|
m.Signatures = signatures
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddSignature adds new Signature into RequestVerificationHeader.
|
// SetToken is a Token field setter.
|
||||||
func (m *RequestVerificationHeader) AddSignature(sig *RequestVerificationHeader_Signature) {
|
func (m *RequestVerificationHeader) SetToken(token *Token) {
|
||||||
if sig == nil {
|
m.Token = token
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// testCustomField for test usage only.
|
// testCustomField for test usage only.
|
||||||
|
|
Binary file not shown.
|
@ -3,6 +3,7 @@ package service;
|
||||||
option go_package = "github.com/nspcc-dev/neofs-api-go/service";
|
option go_package = "github.com/nspcc-dev/neofs-api-go/service";
|
||||||
option csharp_namespace = "NeoFS.API.Service";
|
option csharp_namespace = "NeoFS.API.Service";
|
||||||
|
|
||||||
|
import "refs/types.proto";
|
||||||
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
|
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
|
||||||
|
|
||||||
option (gogoproto.stable_marshaler_all) = true;
|
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
|
// RequestVerificationHeader is a set of signatures of every NeoFS Node that processed request
|
||||||
// (should be embedded into message).
|
// (should be embedded into message).
|
||||||
message RequestVerificationHeader {
|
message RequestVerificationHeader {
|
||||||
message Sign {
|
message Signature {
|
||||||
// Sign is signature of the request or session key.
|
// Sign is signature of the request or session key.
|
||||||
bytes Sign = 1;
|
bytes Sign = 1;
|
||||||
// Peer is compressed public key used for signature.
|
// Peer is compressed public key used for signature.
|
||||||
bytes Peer = 2;
|
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
|
// Signatures is a set of signatures of every passed NeoFS Node
|
||||||
repeated Signature Signatures = 1;
|
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;
|
||||||
|
// }
|
||||||
|
|
|
@ -1,201 +1,117 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"encoding/binary"
|
||||||
"log"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gogo/protobuf/proto"
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/refs"
|
"github.com/nspcc-dev/neofs-api-go/refs"
|
||||||
crypto "github.com/nspcc-dev/neofs-crypto"
|
|
||||||
"github.com/nspcc-dev/neofs-crypto/test"
|
"github.com/nspcc-dev/neofs-crypto/test"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/stretchr/testify/require"
|
"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)
|
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{
|
token := new(Token)
|
||||||
IntField: math.MaxInt32,
|
|
||||||
StringField: "TestRequestStringField",
|
|
||||||
BytesField: make([]byte, 1<<22),
|
|
||||||
CustomField: &custom,
|
|
||||||
RequestMetaHeader: RequestMetaHeader{
|
|
||||||
TTL: math.MaxInt32 - 8,
|
|
||||||
Epoch: math.MaxInt64 - 12,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
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{
|
req := &TestRequest{
|
||||||
IntField: math.MaxInt32,
|
IntField: math.MaxInt32,
|
||||||
StringField: "TestRequestStringField",
|
StringField: "TestRequestStringField",
|
||||||
BytesField: []byte("TestRequestBytesField"),
|
BytesField: make([]byte, 1<<22),
|
||||||
|
CustomField: &customField,
|
||||||
}
|
}
|
||||||
|
|
||||||
key := test.DecodeKey(0)
|
req.SetTTL(math.MaxInt32 - 8)
|
||||||
peer := crypto.MarshalPublicKey(&key.PublicKey)
|
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, err)
|
||||||
|
|
||||||
require.NoError(t, SignRequestHeader(key, req))
|
token := new(Token)
|
||||||
|
token.SetID(id)
|
||||||
|
|
||||||
require.Len(t, req.Signatures, 1)
|
h := new(RequestVerificationHeader)
|
||||||
for i := range req.Signatures {
|
|
||||||
sign := req.Signatures[i].GetSign()
|
h.SetToken(token)
|
||||||
require.Equal(t, peer, req.Signatures[i].GetPeer())
|
|
||||||
require.NoError(t, crypto.Verify(&key.PublicKey, data, sign))
|
require.Equal(t, token, h.GetToken())
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
11
service/version.go
Normal file
11
service/version.go
Normal file
|
@ -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
|
||||||
|
}
|
21
service/version_test.go
Normal file
21
service/version_test.go
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
15
session/alias.go
Normal file
15
session/alias.go
Normal file
|
@ -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
|
62
session/create.go
Normal file
62
session/create.go
Normal file
|
@ -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)
|
||||||
|
}
|
103
session/create_test.go
Normal file
103
session/create_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
15
session/errors.go
Normal file
15
session/errors.go
Normal file
|
@ -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")
|
55
session/private.go
Normal file
55
session/private.go
Normal file
|
@ -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
|
||||||
|
}
|
70
session/private_test.go
Normal file
70
session/private_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
62
session/request.go
Normal file
62
session/request.go
Normal file
|
@ -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
|
||||||
|
}
|
92
session/request_test.go
Normal file
92
session/request_test.go
Normal file
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
16
session/response.go
Normal file
16
session/response.go
Normal file
|
@ -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
|
||||||
|
}
|
27
session/response_test.go
Normal file
27
session/response_test.go
Normal file
|
@ -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())
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
Binary file not shown.
|
@ -3,7 +3,6 @@ package session;
|
||||||
option go_package = "github.com/nspcc-dev/neofs-api-go/session";
|
option go_package = "github.com/nspcc-dev/neofs-api-go/session";
|
||||||
option csharp_namespace = "NeoFS.API.Session";
|
option csharp_namespace = "NeoFS.API.Session";
|
||||||
|
|
||||||
import "session/types.proto";
|
|
||||||
import "service/meta.proto";
|
import "service/meta.proto";
|
||||||
import "service/verify.proto";
|
import "service/verify.proto";
|
||||||
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
|
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
|
||||||
|
@ -12,42 +11,29 @@ option (gogoproto.stable_marshaler_all) = true;
|
||||||
|
|
||||||
|
|
||||||
service Session {
|
service Session {
|
||||||
// Create is a method that used to open a trusted session to manipulate
|
// Create opens new session between the client and the server
|
||||||
// an object. In order to put or delete object client have to obtain session
|
rpc Create (CreateRequest) returns (CreateResponse);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateRequest carries an information necessary for opening a session
|
||||||
message CreateRequest {
|
message CreateRequest {
|
||||||
// Message should be one of
|
// OwnerID carries an identifier of a session initiator
|
||||||
oneof Message {
|
bytes OwnerID = 1 [(gogoproto.nullable) = false, (gogoproto.customtype) = "OwnerID"];
|
||||||
// Init is a message to initialize session opening. Carry:
|
|
||||||
// owner of manipulation object;
|
// Lifetime carries a lifetime of the session
|
||||||
// ID of manipulation object;
|
service.TokenLifetime Lifetime = 2 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
|
||||||
// token lifetime bounds.
|
|
||||||
session.Token Init = 1;
|
|
||||||
// Signed Init message response (Unsigned) from server with user private key
|
|
||||||
session.Token Signed = 2;
|
|
||||||
}
|
|
||||||
// RequestMetaHeader contains information about request meta headers (should be embedded into message)
|
// RequestMetaHeader contains information about request meta headers (should be embedded into message)
|
||||||
service.RequestMetaHeader Meta = 98 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
|
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)
|
// 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];
|
service.RequestVerificationHeader Verify = 99 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateResponse carries an information about the opened session
|
||||||
message CreateResponse {
|
message CreateResponse {
|
||||||
oneof Message {
|
// ID carries an identifier of session token
|
||||||
// Unsigned token with token ID and session public key generated on server side
|
bytes ID = 1 [(gogoproto.customtype) = "TokenID", (gogoproto.nullable) = false];
|
||||||
session.Token Unsigned = 1;
|
|
||||||
// Result is a resulting token which can be used for object placing through an trusted intermediary
|
// SessionKey carries a session public key
|
||||||
session.Token Result = 2;
|
bytes SessionKey = 2;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,82 +1,64 @@
|
||||||
package session
|
package session
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/refs"
|
|
||||||
crypto "github.com/nspcc-dev/neofs-crypto"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type simpleStore struct {
|
type mapTokenStore struct {
|
||||||
*sync.RWMutex
|
*sync.RWMutex
|
||||||
|
|
||||||
tokens map[TokenID]*PToken
|
tokens map[PrivateTokenKey]PrivateToken
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO get curve from neofs-crypto
|
// NewMapTokenStore creates new PrivateTokenStore instance.
|
||||||
func defaultCurve() elliptic.Curve {
|
//
|
||||||
return elliptic.P256()
|
// The elements of the instance are stored in the map.
|
||||||
}
|
func NewMapTokenStore() PrivateTokenStore {
|
||||||
|
return &mapTokenStore{
|
||||||
// NewSimpleStore creates simple token storage
|
|
||||||
func NewSimpleStore() TokenStore {
|
|
||||||
return &simpleStore{
|
|
||||||
RWMutex: new(sync.RWMutex),
|
RWMutex: new(sync.RWMutex),
|
||||||
tokens: make(map[TokenID]*PToken),
|
tokens: make(map[PrivateTokenKey]PrivateToken),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns new token with specified parameters.
|
// Store adds passed token to the map.
|
||||||
func (s *simpleStore) New(p TokenParams) *PToken {
|
//
|
||||||
tid, err := refs.NewUUID()
|
// Resulting error is always nil.
|
||||||
if err != nil {
|
func (s *mapTokenStore) Store(key PrivateTokenKey, token PrivateToken) error {
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Lock()
|
s.Lock()
|
||||||
s.tokens[t.ID] = t
|
s.tokens[key] = token
|
||||||
s.Unlock()
|
s.Unlock()
|
||||||
|
|
||||||
return t
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch tries to fetch a token with specified id.
|
// Fetch returns the map element corresponding to the given key.
|
||||||
func (s *simpleStore) Fetch(id TokenID) *PToken {
|
//
|
||||||
|
// Returns ErrPrivateTokenNotFound is there is no element in map.
|
||||||
|
func (s *mapTokenStore) Fetch(key PrivateTokenKey) (PrivateToken, error) {
|
||||||
s.RLock()
|
s.RLock()
|
||||||
defer s.RUnlock()
|
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.
|
// RemoveExpired removes all the map elements that are expired in the passed epoch.
|
||||||
func (s *simpleStore) Remove(id TokenID) {
|
//
|
||||||
|
// Resulting error is always nil.
|
||||||
|
func (s *mapTokenStore) RemoveExpired(epoch uint64) error {
|
||||||
s.Lock()
|
s.Lock()
|
||||||
delete(s.tokens, id)
|
|
||||||
|
for key, token := range s.tokens {
|
||||||
|
if token.Expired(epoch) {
|
||||||
|
delete(s.tokens, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
s.Unlock()
|
s.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,96 +1,111 @@
|
||||||
package session
|
package session
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/rand"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/refs"
|
"github.com/nspcc-dev/neofs-api-go/refs"
|
||||||
crypto "github.com/nspcc-dev/neofs-crypto"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
type testClient struct {
|
func TestMapTokenStore(t *testing.T) {
|
||||||
*ecdsa.PrivateKey
|
// create new private token
|
||||||
OwnerID OwnerID
|
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) {
|
func TestMapTokenStore_RemoveExpired(t *testing.T) {
|
||||||
return crypto.Sign(c.PrivateKey, data)
|
// create some epoch number
|
||||||
}
|
e1 := uint64(1)
|
||||||
|
|
||||||
func newTestClient(t *testing.T) *testClient {
|
// create private token that expires after e1
|
||||||
key, err := ecdsa.GenerateKey(defaultCurve(), rand.Reader)
|
tok1, err := NewPrivateToken(e1)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
owner, err := refs.NewOwnerID(&key.PublicKey)
|
// create some greater than e1 epoch number
|
||||||
require.NoError(t, err)
|
e2 := e1 + 1
|
||||||
|
|
||||||
return &testClient{PrivateKey: key, OwnerID: owner}
|
// create private token that expires after e2
|
||||||
}
|
tok2, err := NewPrivateToken(e2)
|
||||||
|
require.NoError(t, err)
|
||||||
func signToken(t *testing.T, token *PToken, c *testClient) {
|
|
||||||
require.NotNil(t, token)
|
// create token store instance
|
||||||
token.SetPublicKeys(&c.PublicKey)
|
s := NewMapTokenStore()
|
||||||
|
|
||||||
signH, err := c.Sign(token.Header.PublicKey)
|
// create test PrivateTokenKey
|
||||||
require.NoError(t, err)
|
key := PrivateTokenKey{}
|
||||||
require.NotNil(t, signH)
|
key.SetOwnerID(OwnerID{1, 2, 3})
|
||||||
|
|
||||||
// data is not yet signed
|
// create IDs for tokens
|
||||||
keys := UnmarshalPublicKeys(&token.Token)
|
id1, err := refs.NewUUID()
|
||||||
require.False(t, token.Verify(keys...))
|
require.NoError(t, err)
|
||||||
|
id2, err := refs.NewUUID()
|
||||||
signT, err := c.Sign(token.verificationData())
|
require.NoError(t, err)
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, signT)
|
assertPresence := func(ids ...TokenID) {
|
||||||
|
for i := range ids {
|
||||||
token.AddSignatures(signH, signT)
|
key.SetTokenID(ids[i])
|
||||||
require.True(t, token.Verify(keys...))
|
_, err = s.Fetch(key)
|
||||||
}
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
func TestTokenStore(t *testing.T) {
|
}
|
||||||
s := NewSimpleStore()
|
|
||||||
|
assertAbsence := func(ids ...TokenID) {
|
||||||
oid, err := refs.NewObjectID()
|
for i := range ids {
|
||||||
require.NoError(t, err)
|
key.SetTokenID(ids[i])
|
||||||
|
_, err = s.Fetch(key)
|
||||||
c := newTestClient(t)
|
require.EqualError(t, err, ErrPrivateTokenNotFound.Error())
|
||||||
require.NotNil(t, c)
|
}
|
||||||
pk := [][]byte{crypto.MarshalPublicKey(&c.PublicKey)}
|
}
|
||||||
|
|
||||||
// create new token
|
// store both tokens
|
||||||
token := s.New(TokenParams{
|
key.SetTokenID(id1)
|
||||||
ObjectID: []ObjectID{oid},
|
require.NoError(t, s.Store(key, tok1))
|
||||||
OwnerID: c.OwnerID,
|
key.SetTokenID(id2)
|
||||||
PublicKeys: pk,
|
require.NoError(t, s.Store(key, tok2))
|
||||||
})
|
|
||||||
signToken(t, token, c)
|
// ascertain that both tokens are available
|
||||||
|
assertPresence(id1, id2)
|
||||||
// check that it can be fetched
|
|
||||||
t1 := s.Fetch(token.ID)
|
// perform cleaning for epoch in which both tokens are not expired
|
||||||
require.NotNil(t, t1)
|
require.NoError(t, s.RemoveExpired(e1))
|
||||||
require.Equal(t, token, t1)
|
|
||||||
|
// ascertain that both tokens are still available
|
||||||
// create and sign another token by the same client
|
assertPresence(id1, id2)
|
||||||
t1 = s.New(TokenParams{
|
|
||||||
ObjectID: []ObjectID{oid},
|
// perform cleaning for epoch greater than e1 and not greater than e2
|
||||||
OwnerID: c.OwnerID,
|
require.NoError(t, s.RemoveExpired(e1+1))
|
||||||
PublicKeys: pk,
|
|
||||||
})
|
// ascertain that tok1 was removed
|
||||||
|
assertAbsence(id1)
|
||||||
signToken(t, t1, c)
|
|
||||||
|
// ascertain that tok2 was not removed
|
||||||
data := []byte{1, 2, 3}
|
assertPresence(id2)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
207
session/types.go
207
session/types.go
|
@ -1,181 +1,86 @@
|
||||||
package session
|
package session
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/ecdsa"
|
"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"
|
"github.com/nspcc-dev/neofs-api-go/refs"
|
||||||
crypto "github.com/nspcc-dev/neofs-crypto"
|
"github.com/nspcc-dev/neofs-api-go/service"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
// PrivateToken is an interface of session private part.
|
||||||
// ObjectID type alias.
|
type PrivateToken interface {
|
||||||
ObjectID = refs.ObjectID
|
// PublicKey must return a binary representation of session public key.
|
||||||
// OwnerID type alias.
|
PublicKey() []byte
|
||||||
OwnerID = refs.OwnerID
|
|
||||||
// TokenID type alias.
|
|
||||||
TokenID = refs.UUID
|
|
||||||
|
|
||||||
// PToken is a wrapper around Token that allows to sign data
|
// Sign must return the signature of passed data.
|
||||||
// and to do thread-safe manipulations.
|
//
|
||||||
PToken struct {
|
// Resulting signature must be verified by crypto.Verify function
|
||||||
Token
|
// with the session public key.
|
||||||
|
Sign([]byte) ([]byte, error)
|
||||||
|
|
||||||
mtx *sync.Mutex
|
// Expired must return true if and only if private token is expired in the given epoch number.
|
||||||
PrivateKey *ecdsa.PrivateKey
|
Expired(uint64) bool
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSame checks if the passed token is valid and equal to current token
|
// PrivateTokenKey is a structure of private token storage key.
|
||||||
func (m *Token) IsSame(t *Token) error {
|
type PrivateTokenKey struct {
|
||||||
switch {
|
owner OwnerID
|
||||||
case m.FirstEpoch != t.FirstEpoch:
|
token TokenID
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign tries to sign current Token data and stores signature inside it.
|
// PrivateTokenSource is an interface of private token storage with read access.
|
||||||
func (m *Token) Sign(key *ecdsa.PrivateKey) error {
|
type PrivateTokenSource interface {
|
||||||
if err := m.Header.Sign(key); err != nil {
|
// Fetch must return the storage record corresponding to the passed key.
|
||||||
return err
|
//
|
||||||
}
|
// Resulting error must be ErrPrivateTokenNotFound if there is no corresponding record.
|
||||||
|
Fetch(PrivateTokenKey) (PrivateToken, error)
|
||||||
s, err := crypto.Sign(key, m.verificationData())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Signature = s
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPublicKeys sets owner's public keys to the token
|
// EpochLifetimeStore is an interface of the storage of elements that lifetime is limited by NeoFS epoch.
|
||||||
func (m *Token) SetPublicKeys(keys ...*ecdsa.PublicKey) {
|
type EpochLifetimeStore interface {
|
||||||
m.PublicKeys = m.PublicKeys[:0]
|
// RemoveExpired must remove all elements that are expired in the given epoch.
|
||||||
for i := range keys {
|
RemoveExpired(uint64) error
|
||||||
m.PublicKeys = append(m.PublicKeys, crypto.MarshalPublicKey(keys[i]))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify checks if token is correct and signed.
|
// PrivateTokenStore is an interface of the storage of private tokens addressable by TokenID.
|
||||||
func (m *Token) Verify(keys ...*ecdsa.PublicKey) bool {
|
type PrivateTokenStore interface {
|
||||||
if m.FirstEpoch > m.LastEpoch {
|
PrivateTokenSource
|
||||||
return false
|
EpochLifetimeStore
|
||||||
}
|
|
||||||
ownerFromKeys := chain.KeysToAddress(keys...)
|
|
||||||
if m.OwnerID.String() != ownerFromKeys {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range keys {
|
// Store must save passed private token in the storage under the given key.
|
||||||
if m.Header.Verify(keys[i]) && crypto.Verify(keys[i], m.verificationData(), m.Signature) == nil {
|
//
|
||||||
return true
|
// Resulting error must be nil if private token was stored successfully.
|
||||||
}
|
Store(PrivateTokenKey, PrivateToken) error
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddSignatures adds token signatures.
|
// KeyStore is an interface of the storage of public keys addressable by OwnerID,
|
||||||
func (t *PToken) AddSignatures(signH, signT []byte) {
|
type KeyStore interface {
|
||||||
t.mtx.Lock()
|
// Get must return the storage record corresponding to the passed key.
|
||||||
|
//
|
||||||
t.Header.KeySignature = signH
|
// Resulting error must be ErrKeyNotFound if there is no corresponding record.
|
||||||
t.Signature = signT
|
Get(context.Context, OwnerID) ([]*ecdsa.PublicKey, error)
|
||||||
|
|
||||||
t.mtx.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignData signs data with session private key.
|
// CreateParamsSource is an interface of the container of session parameters with read access.
|
||||||
func (t *PToken) SignData(data []byte) ([]byte, error) {
|
type CreateParamsSource interface {
|
||||||
return crypto.Sign(t.PrivateKey, data)
|
refs.OwnerIDSource
|
||||||
|
service.LifetimeSource
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyData checks if signature of data by token is equal to sign.
|
// CreateParamsContainer is an interface of the container of session parameters.
|
||||||
func (m *VerificationHeader) VerifyData(data, sign []byte) error {
|
type CreateParamsContainer interface {
|
||||||
if crypto.Verify(crypto.UnmarshalPublicKey(m.PublicKey), data, sign) != nil {
|
refs.OwnerIDContainer
|
||||||
return ErrInvalidSignature
|
service.LifetimeContainer
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify checks if verification header was issued by id.
|
// CreateResult is an interface of the container of an opened session info with read access.
|
||||||
func (m *VerificationHeader) Verify(keys ...*ecdsa.PublicKey) bool {
|
type CreateResult interface {
|
||||||
for i := range keys {
|
service.TokenIDSource
|
||||||
if crypto.Verify(keys[i], m.PublicKey, m.KeySignature) == nil {
|
service.SessionKeySource
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalPublicKeys returns unmarshal public keys from the token
|
// Creator is an interface of the tool for a session opening.
|
||||||
func UnmarshalPublicKeys(t *Token) []*ecdsa.PublicKey {
|
type Creator interface {
|
||||||
r := make([]*ecdsa.PublicKey, 0, len(t.PublicKeys))
|
Create(context.Context, CreateParamsSource) (CreateResult, error)
|
||||||
for i := range t.PublicKeys {
|
|
||||||
r = append(r, crypto.UnmarshalPublicKey(t.PublicKeys[i]))
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
|
@ -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;
|
|
||||||
}
|
|
67
state/sign.go
Normal file
67
state/sign.go
Normal file
|
@ -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
|
||||||
|
}
|
94
state/sign_test.go
Normal file
94
state/sign_test.go
Normal file
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
state/types.go
Normal file
24
state/types.go
Normal file
|
@ -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
|
||||||
|
}
|
18
state/types_test.go
Normal file
18
state/types_test.go
Normal file
|
@ -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())
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in a new issue