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
|
||||
This is the changelog for NeoFS-API-Go
|
||||
|
||||
## [0.7.4] - 2020-05-12
|
||||
|
||||
### Added
|
||||
|
||||
- Stringify for `object.Object`.
|
||||
|
||||
### Changed
|
||||
|
||||
- Mechanism for creating and verifying request message signatures.
|
||||
- Implementation and interface of private token storage.
|
||||
- File structure of packages.
|
||||
|
||||
### Updated
|
||||
|
||||
- NeoFS API v0.7.4
|
||||
|
||||
## [0.7.1] - 2020-04-20
|
||||
|
||||
### Added
|
||||
|
@ -273,3 +289,4 @@ Initial public release
|
|||
[0.6.2]: https://github.com/nspcc-dev/neofs-api-go/compare/v0.6.1...v0.6.2
|
||||
[0.7.0]: https://github.com/nspcc-dev/neofs-api-go/compare/v0.6.2...v0.7.0
|
||||
[0.7.1]: https://github.com/nspcc-dev/neofs-api-go/compare/v0.7.0...v0.7.1
|
||||
[0.7.4]: https://github.com/nspcc-dev/neofs-api-go/compare/v0.7.1...v0.7.4
|
||||
|
|
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
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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))
|
||||
})
|
||||
}
|
||||
|
||||
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 (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -27,6 +28,8 @@ var (
|
|||
_ proto.Message = (*SpreadMap)(nil)
|
||||
)
|
||||
|
||||
var requestEndianness = binary.BigEndian
|
||||
|
||||
// Equals checks whether two NodeInfo has same address.
|
||||
func (m NodeInfo) Equals(n1 NodeInfo) bool {
|
||||
return m.Address == n1.Address && bytes.Equal(m.PubKey, n1.PubKey)
|
||||
|
@ -98,3 +101,37 @@ func (m SpreadMap) String() string {
|
|||
", " +
|
||||
"Netmap: [" + strings.Join(result, ",") + "]>"
|
||||
}
|
||||
|
||||
// GetType is a Type field getter.
|
||||
func (m Request) GetType() NodeType {
|
||||
return m.Type
|
||||
}
|
||||
|
||||
// SetType is a Type field setter.
|
||||
func (m *Request) SetType(t NodeType) {
|
||||
m.Type = t
|
||||
}
|
||||
|
||||
// SetState is a State field setter.
|
||||
func (m *Request) SetState(state Request_State) {
|
||||
m.State = state
|
||||
}
|
||||
|
||||
// SetInfo is an Info field getter.
|
||||
func (m *Request) SetInfo(info NodeInfo) {
|
||||
m.Info = info
|
||||
}
|
||||
|
||||
// Size returns the size necessary for a binary representation of the state.
|
||||
func (x Request_State) Size() int {
|
||||
return 4
|
||||
}
|
||||
|
||||
// Bytes returns a binary representation of the state.
|
||||
func (x Request_State) Bytes() []byte {
|
||||
data := make([]byte, x.Size())
|
||||
|
||||
requestEndianness.PutUint32(data, uint32(x))
|
||||
|
||||
return data
|
||||
}
|
||||
|
|
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)
|
||||
})
|
||||
}
|
||||
|
||||
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) |
|
||||
| OwnerID | [bytes](#bytes) | | OwnerID is a wallet address |
|
||||
| Token | [session.Token](#session.Token) | | Token with session public key and user's signature |
|
||||
| Meta | [service.RequestMetaHeader](#service.RequestMetaHeader) | | RequestMetaHeader contains information about request meta headers (should be embedded into message) |
|
||||
| Verify | [service.RequestVerificationHeader](#service.RequestVerificationHeader) | | RequestVerificationHeader is a set of signatures of every NeoFS Node that processed request (should be embedded into message) |
|
||||
|
||||
|
@ -228,7 +227,6 @@ in distributed system.
|
|||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| Address | [refs.Address](#refs.Address) | | Address of object (container id + object id) |
|
||||
| Raw | [bool](#bool) | | Raw is the request flag of a physically stored representation of an object |
|
||||
| Meta | [service.RequestMetaHeader](#service.RequestMetaHeader) | | RequestMetaHeader contains information about request meta headers (should be embedded into message) |
|
||||
| Verify | [service.RequestVerificationHeader](#service.RequestVerificationHeader) | | RequestVerificationHeader is a set of signatures of every NeoFS Node that processed request (should be embedded into message) |
|
||||
|
||||
|
@ -256,7 +254,6 @@ in distributed system.
|
|||
| ----- | ---- | ----- | ----------- |
|
||||
| Address | [refs.Address](#refs.Address) | | Address of object (container id + object id) |
|
||||
| FullHeaders | [bool](#bool) | | FullHeaders can be set true for extended headers in the object |
|
||||
| Raw | [bool](#bool) | | Raw is the request flag of a physically stored representation of an object |
|
||||
| Meta | [service.RequestMetaHeader](#service.RequestMetaHeader) | | RequestMetaHeader contains information about request meta headers (should be embedded into message) |
|
||||
| Verify | [service.RequestVerificationHeader](#service.RequestVerificationHeader) | | RequestVerificationHeader is a set of signatures of every NeoFS Node that processed request (should be embedded into message) |
|
||||
|
||||
|
@ -296,7 +293,6 @@ in distributed system.
|
|||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| Object | [Object](#object.Object) | | Object with at least container id and owner id fields |
|
||||
| Token | [session.Token](#session.Token) | | Token with session public key and user's signature |
|
||||
| CopiesNumber | [uint32](#uint32) | | Number of the object copies to store within the RPC call (zero is processed according to the placement rules) |
|
||||
|
||||
|
||||
|
@ -378,7 +374,7 @@ in distributed system.
|
|||
| UserHeader | [UserHeader](#object.UserHeader) | | UserHeader is a set of KV headers defined by user |
|
||||
| Transform | [Transform](#object.Transform) | | Transform defines transform operation (e.g. payload split) |
|
||||
| Tombstone | [Tombstone](#object.Tombstone) | | Tombstone header that set up in deleted objects |
|
||||
| Verify | [session.VerificationHeader](#session.VerificationHeader) | | Verify header that contains session public key and user's signature |
|
||||
| Token | [service.Token](#service.Token) | | Token header contains token of the session within which the object was created |
|
||||
| HomoHash | [bytes](#bytes) | | HomoHash is a homomorphic hash of original object payload |
|
||||
| PayloadChecksum | [bytes](#bytes) | | PayloadChecksum of actual object's payload |
|
||||
| Integrity | [IntegrityHeader](#object.IntegrityHeader) | | Integrity header with checksum of all above headers in the object |
|
||||
|
|
|
@ -14,8 +14,10 @@
|
|||
|
||||
- Messages
|
||||
- [RequestVerificationHeader](#service.RequestVerificationHeader)
|
||||
- [RequestVerificationHeader.Sign](#service.RequestVerificationHeader.Sign)
|
||||
- [RequestVerificationHeader.Signature](#service.RequestVerificationHeader.Signature)
|
||||
- [Token](#service.Token)
|
||||
- [Token.Info](#service.Token.Info)
|
||||
- [TokenLifetime](#service.TokenLifetime)
|
||||
|
||||
|
||||
- [service/verify_test.proto](#service/verify_test.proto)
|
||||
|
@ -49,6 +51,7 @@ RequestMetaHeader contains information about request meta headers
|
|||
| TTL | [uint32](#uint32) | | TTL must be larger than zero, it decreased in every NeoFS Node |
|
||||
| Epoch | [uint64](#uint64) | | Epoch for user can be empty, because node sets epoch to the actual value |
|
||||
| Version | [uint32](#uint32) | | Version defines protocol version TODO: not used for now, should be implemented in future |
|
||||
| Raw | [bool](#bool) | | Raw determines whether the request is raw or not |
|
||||
|
||||
|
||||
<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 |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| Signatures | [RequestVerificationHeader.Signature](#service.RequestVerificationHeader.Signature) | repeated | Signatures is a set of signatures of every passed NeoFS Node |
|
||||
|
||||
|
||||
<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. |
|
||||
| Token | [Token](#service.Token) | | Token is a token of the session within which the request is sent |
|
||||
|
||||
|
||||
<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 |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| Sign | [RequestVerificationHeader.Sign](#service.RequestVerificationHeader.Sign) | | Sign is a signature and public key of the request. |
|
||||
| Origin | [RequestVerificationHeader.Sign](#service.RequestVerificationHeader.Sign) | | Origin used for requests, when trusted node changes it and re-sign with session key. If session key used for signature request, then Origin should contain public key of user and signed session key. |
|
||||
| Sign | [bytes](#bytes) | | Sign is signature of the request or session key. |
|
||||
| Peer | [bytes](#bytes) | | Peer is compressed public key used for signature. |
|
||||
|
||||
|
||||
<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 -->
|
||||
|
||||
|
||||
<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 -->
|
||||
|
||||
|
||||
|
|
|
@ -12,13 +12,6 @@
|
|||
- [CreateResponse](#session.CreateResponse)
|
||||
|
||||
|
||||
- [session/types.proto](#session/types.proto)
|
||||
|
||||
- Messages
|
||||
- [Token](#session.Token)
|
||||
- [VerificationHeader](#session.VerificationHeader)
|
||||
|
||||
|
||||
- [Scalar Value Types](#scalar-value-types)
|
||||
|
||||
|
||||
|
@ -37,22 +30,13 @@
|
|||
|
||||
|
||||
```
|
||||
rpc Create(stream CreateRequest) returns (stream CreateResponse);
|
||||
rpc Create(CreateRequest) returns (CreateResponse);
|
||||
|
||||
```
|
||||
|
||||
#### Method Create
|
||||
|
||||
Create is a method that used to open a trusted session to manipulate
|
||||
an object. In order to put or delete object client have to obtain session
|
||||
token with trusted node. Trusted node will modify client's object
|
||||
(add missing headers, checksums, homomorphic hash) and sign id with
|
||||
session key. Session is established during 4-step handshake in one gRPC stream
|
||||
|
||||
- First client stream message SHOULD BE type of `CreateRequest_Init`.
|
||||
- First server stream message SHOULD BE type of `CreateResponse_Unsigned`.
|
||||
- Second client stream message SHOULD BE type of `CreateRequest_Signed`.
|
||||
- Second server stream message SHOULD BE type of `CreateResponse_Result`.
|
||||
Create opens new session between the client and the server
|
||||
|
||||
| Name | Input | Output |
|
||||
| ---- | ----- | ------ |
|
||||
|
@ -63,13 +47,13 @@ session key. Session is established during 4-step handshake in one gRPC stream
|
|||
<a name="session.CreateRequest"></a>
|
||||
|
||||
### Message CreateRequest
|
||||
|
||||
CreateRequest carries an information necessary for opening a session
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| Init | [Token](#session.Token) | | Init is a message to initialize session opening. Carry: owner of manipulation object; ID of manipulation object; token lifetime bounds. |
|
||||
| Signed | [Token](#session.Token) | | Signed Init message response (Unsigned) from server with user private key |
|
||||
| OwnerID | [bytes](#bytes) | | OwnerID carries an identifier of a session initiator |
|
||||
| Lifetime | [service.TokenLifetime](#service.TokenLifetime) | | Lifetime carries a lifetime of the session |
|
||||
| Meta | [service.RequestMetaHeader](#service.RequestMetaHeader) | | RequestMetaHeader contains information about request meta headers (should be embedded into message) |
|
||||
| Verify | [service.RequestVerificationHeader](#service.RequestVerificationHeader) | | RequestVerificationHeader is a set of signatures of every NeoFS Node that processed request (should be embedded into message) |
|
||||
|
||||
|
@ -77,57 +61,13 @@ session key. Session is established during 4-step handshake in one gRPC stream
|
|||
<a name="session.CreateResponse"></a>
|
||||
|
||||
### Message CreateResponse
|
||||
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| Unsigned | [Token](#session.Token) | | Unsigned token with token ID and session public key generated on server side |
|
||||
| Result | [Token](#session.Token) | | Result is a resulting token which can be used for object placing through an trusted intermediary |
|
||||
|
||||
<!-- 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
|
||||
CreateResponse carries an information about the opened session
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| Header | [VerificationHeader](#session.VerificationHeader) | | Header carries verification data of session key |
|
||||
| OwnerID | [bytes](#bytes) | | OwnerID is an owner of manipulation object |
|
||||
| FirstEpoch | [uint64](#uint64) | | FirstEpoch is an initial epoch of token lifetime |
|
||||
| LastEpoch | [uint64](#uint64) | | LastEpoch is a last epoch of token lifetime |
|
||||
| ObjectID | [bytes](#bytes) | repeated | ObjectID is an object identifier of manipulation object |
|
||||
| Signature | [bytes](#bytes) | | Signature is a token signature, signed by owner of manipulation object |
|
||||
| ID | [bytes](#bytes) | | ID is a token identifier. valid UUIDv4 represented in bytes |
|
||||
| PublicKeys | [bytes](#bytes) | repeated | PublicKeys associated with owner |
|
||||
|
||||
|
||||
<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 |
|
||||
| ID | [bytes](#bytes) | | ID carries an identifier of session token |
|
||||
| SessionKey | [bytes](#bytes) | | SessionKey carries a session public key |
|
||||
|
||||
<!-- end messages -->
|
||||
|
||||
|
|
|
@ -19,21 +19,6 @@ func (m Object) IsLinking() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// VerificationHeader returns verification header if it is presented in extended headers.
|
||||
func (m Object) VerificationHeader() (*VerificationHeader, error) {
|
||||
_, vh := m.LastHeader(HeaderType(VerifyHdr))
|
||||
if vh == nil {
|
||||
return nil, ErrHeaderNotFound
|
||||
}
|
||||
return vh.Value.(*Header_Verify).Verify, nil
|
||||
}
|
||||
|
||||
// SetVerificationHeader sets verification header in the object.
|
||||
// It will replace existing verification header or add a new one.
|
||||
func (m *Object) SetVerificationHeader(header *VerificationHeader) {
|
||||
m.SetHeader(&Header{Value: &Header_Verify{Verify: header}})
|
||||
}
|
||||
|
||||
// Links returns slice of ids of specified link type
|
||||
func (m *Object) Links(t Link_Type) []ID {
|
||||
var res []ID
|
||||
|
|
|
@ -31,7 +31,7 @@ type (
|
|||
// All object operations must have TTL, Epoch, Type, Container ID and
|
||||
// permission of usage previous network map.
|
||||
Request interface {
|
||||
service.MetaHeader
|
||||
service.SeizedRequestMetaContainer
|
||||
|
||||
CID() CID
|
||||
Type() RequestType
|
||||
|
|
Binary file not shown.
|
@ -5,7 +5,6 @@ option csharp_namespace = "NeoFS.API.Object";
|
|||
|
||||
import "refs/types.proto";
|
||||
import "object/types.proto";
|
||||
import "session/types.proto";
|
||||
import "service/meta.proto";
|
||||
import "service/verify.proto";
|
||||
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
|
||||
|
@ -58,8 +57,6 @@ service Service {
|
|||
message GetRequest {
|
||||
// Address of object (container id + object id)
|
||||
refs.Address Address = 1 [(gogoproto.nullable) = false];
|
||||
// Raw is the request flag of a physically stored representation of an object
|
||||
bool Raw = 2;
|
||||
// RequestMetaHeader contains information about request meta headers (should be embedded into message)
|
||||
service.RequestMetaHeader Meta = 98 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
|
||||
// RequestVerificationHeader is a set of signatures of every NeoFS Node that processed request (should be embedded into message)
|
||||
|
@ -82,10 +79,8 @@ message PutRequest {
|
|||
message PutHeader {
|
||||
// Object with at least container id and owner id fields
|
||||
Object Object = 1;
|
||||
// Token with session public key and user's signature
|
||||
session.Token Token = 2;
|
||||
// Number of the object copies to store within the RPC call (zero is processed according to the placement rules)
|
||||
uint32 CopiesNumber = 3;
|
||||
uint32 CopiesNumber = 2;
|
||||
}
|
||||
|
||||
oneof R {
|
||||
|
@ -112,8 +107,6 @@ message DeleteRequest {
|
|||
refs.Address Address = 1 [(gogoproto.nullable) = false];
|
||||
// OwnerID is a wallet address
|
||||
bytes OwnerID = 2 [(gogoproto.nullable) = false, (gogoproto.customtype) = "OwnerID"];
|
||||
// Token with session public key and user's signature
|
||||
session.Token Token = 3;
|
||||
// RequestMetaHeader contains information about request meta headers (should be embedded into message)
|
||||
service.RequestMetaHeader Meta = 98 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
|
||||
// RequestVerificationHeader is a set of signatures of every NeoFS Node that processed request (should be embedded into message)
|
||||
|
@ -132,8 +125,6 @@ message HeadRequest {
|
|||
refs.Address Address = 1 [(gogoproto.nullable) = false, (gogoproto.customtype) = "Address"];
|
||||
// FullHeaders can be set true for extended headers in the object
|
||||
bool FullHeaders = 2;
|
||||
// Raw is the request flag of a physically stored representation of an object
|
||||
bool Raw = 3;
|
||||
// RequestMetaHeader contains information about request meta headers (should be embedded into message)
|
||||
service.RequestMetaHeader Meta = 98 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
|
||||
// RequestVerificationHeader is a set of signatures of every NeoFS Node that processed request (should be embedded into message)
|
||||
|
|
|
@ -16,8 +16,8 @@ func TestRequest(t *testing.T) {
|
|||
&DeleteRequest{},
|
||||
&GetRangeRequest{},
|
||||
&GetRangeHashRequest{},
|
||||
MakePutRequestHeader(nil, nil),
|
||||
MakePutRequestHeader(&Object{}, nil),
|
||||
MakePutRequestHeader(nil),
|
||||
MakePutRequestHeader(&Object{}),
|
||||
}
|
||||
|
||||
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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/nspcc-dev/neofs-api-go/internal"
|
||||
"github.com/nspcc-dev/neofs-api-go/refs"
|
||||
"github.com/nspcc-dev/neofs-api-go/session"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type (
|
||||
|
@ -19,9 +22,6 @@ type (
|
|||
// Address is a type alias of object Address.
|
||||
Address = refs.Address
|
||||
|
||||
// VerificationHeader is a type alias of session's verification header.
|
||||
VerificationHeader = session.VerificationHeader
|
||||
|
||||
// PositionReader defines object reader that returns slice of bytes
|
||||
// for specified object and data range.
|
||||
PositionReader interface {
|
||||
|
@ -60,8 +60,8 @@ const (
|
|||
TransformHdr
|
||||
// TombstoneHdr is a tombstone header type.
|
||||
TombstoneHdr
|
||||
// VerifyHdr is a verification header type.
|
||||
VerifyHdr
|
||||
// TokenHdr is a token header type.
|
||||
TokenHdr
|
||||
// HomoHashHdr is a homomorphic hash header type.
|
||||
HomoHashHdr
|
||||
// PayloadChecksumHdr is a payload checksum header type.
|
||||
|
@ -175,8 +175,8 @@ func (m Header) typeOf(t isHeader_Value) (ok bool) {
|
|||
_, ok = m.Value.(*Header_Transform)
|
||||
case *Header_Tombstone:
|
||||
_, ok = m.Value.(*Header_Tombstone)
|
||||
case *Header_Verify:
|
||||
_, ok = m.Value.(*Header_Verify)
|
||||
case *Header_Token:
|
||||
_, ok = m.Value.(*Header_Token)
|
||||
case *Header_HomoHash:
|
||||
_, ok = m.Value.(*Header_HomoHash)
|
||||
case *Header_PayloadChecksum:
|
||||
|
@ -205,8 +205,8 @@ func HeaderType(t headerType) Pred {
|
|||
return func(h *Header) bool { _, ok := h.Value.(*Header_Transform); return ok }
|
||||
case TombstoneHdr:
|
||||
return func(h *Header) bool { _, ok := h.Value.(*Header_Tombstone); return ok }
|
||||
case VerifyHdr:
|
||||
return func(h *Header) bool { _, ok := h.Value.(*Header_Verify); return ok }
|
||||
case TokenHdr:
|
||||
return func(h *Header) bool { _, ok := h.Value.(*Header_Token); return ok }
|
||||
case HomoHashHdr:
|
||||
return func(h *Header) bool { _, ok := h.Value.(*Header_HomoHash); return ok }
|
||||
case PayloadChecksumHdr:
|
||||
|
@ -251,6 +251,12 @@ func (m *Object) CopyTo(o *Object) {
|
|||
HomoHash: v.HomoHash,
|
||||
},
|
||||
}
|
||||
case *Header_Token:
|
||||
o.Headers[i] = Header{
|
||||
Value: &Header_Token{
|
||||
Token: v.Token,
|
||||
},
|
||||
}
|
||||
default:
|
||||
o.Headers[i] = *proto.Clone(&m.Headers[i]).(*Header)
|
||||
}
|
||||
|
@ -266,3 +272,117 @@ func (m Object) Address() *refs.Address {
|
|||
CID: m.SystemHeader.CID,
|
||||
}
|
||||
}
|
||||
|
||||
func (m CreationPoint) String() string {
|
||||
return fmt.Sprintf(`{UnixTime=%d Epoch=%d}`, m.UnixTime, m.Epoch)
|
||||
}
|
||||
|
||||
// Stringify converts object into string format.
|
||||
func Stringify(dst io.Writer, obj *Object) error {
|
||||
// put empty line
|
||||
if _, err := fmt.Fprintln(dst); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// put object line
|
||||
if _, err := fmt.Fprintln(dst, "Object:"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// put system headers
|
||||
if _, err := fmt.Fprintln(dst, "\tSystemHeader:"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sysHeaders := []string{"ID", "CID", "OwnerID", "Version", "PayloadLength", "CreatedAt"}
|
||||
v := reflect.ValueOf(obj.SystemHeader)
|
||||
for _, key := range sysHeaders {
|
||||
if !v.FieldByName(key).IsValid() {
|
||||
return errors.Errorf("invalid system header key: %q", key)
|
||||
}
|
||||
|
||||
val := v.FieldByName(key).Interface()
|
||||
if _, err := fmt.Fprintf(dst, "\t\t- %s=%v\n", key, val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// put user headers
|
||||
if _, err := fmt.Fprintln(dst, "\tUserHeaders:"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, header := range obj.Headers {
|
||||
var (
|
||||
typ = reflect.ValueOf(header.Value)
|
||||
key string
|
||||
val interface{}
|
||||
)
|
||||
|
||||
switch t := typ.Interface().(type) {
|
||||
case *Header_Link:
|
||||
key = "Link"
|
||||
val = fmt.Sprintf(`{Type=%s ID=%s}`, t.Link.Type, t.Link.ID)
|
||||
case *Header_Redirect:
|
||||
key = "Redirect"
|
||||
val = fmt.Sprintf(`{CID=%s OID=%s}`, t.Redirect.CID, t.Redirect.ObjectID)
|
||||
case *Header_UserHeader:
|
||||
key = "UserHeader"
|
||||
val = fmt.Sprintf(`{Key=%s Val=%s}`, t.UserHeader.Key, t.UserHeader.Value)
|
||||
case *Header_Transform:
|
||||
key = "Transform"
|
||||
val = t.Transform.Type.String()
|
||||
case *Header_Tombstone:
|
||||
key = "Tombstone"
|
||||
val = "MARKED"
|
||||
case *Header_Token:
|
||||
key = "Token"
|
||||
val = fmt.Sprintf("{"+
|
||||
"ID=%s OwnerID=%s Verb=%s Address=%s Created=%d ValidUntil=%d SessionKey=%02x Signature=%02x"+
|
||||
"}",
|
||||
t.Token.GetID(),
|
||||
t.Token.GetOwnerID(),
|
||||
t.Token.GetVerb(),
|
||||
t.Token.GetAddress(),
|
||||
t.Token.CreationEpoch(),
|
||||
t.Token.ExpirationEpoch(),
|
||||
t.Token.GetSessionKey(),
|
||||
t.Token.GetSignature())
|
||||
case *Header_HomoHash:
|
||||
key = "HomoHash"
|
||||
val = t.HomoHash
|
||||
case *Header_PayloadChecksum:
|
||||
key = "PayloadChecksum"
|
||||
val = t.PayloadChecksum
|
||||
case *Header_Integrity:
|
||||
key = "Integrity"
|
||||
val = fmt.Sprintf(`{Checksum=%02x Signature=%02x}`,
|
||||
t.Integrity.HeadersChecksum,
|
||||
t.Integrity.ChecksumSignature)
|
||||
case *Header_StorageGroup:
|
||||
key = "StorageGroup"
|
||||
val = fmt.Sprintf(`{DataSize=%d Hash=%02x Lifetime={Unit=%s Value=%d}}`,
|
||||
t.StorageGroup.ValidationDataSize,
|
||||
t.StorageGroup.ValidationHash,
|
||||
t.StorageGroup.Lifetime.Unit,
|
||||
t.StorageGroup.Lifetime.Value)
|
||||
case *Header_PublicKey:
|
||||
key = "PublicKey"
|
||||
val = t.PublicKey.Value
|
||||
default:
|
||||
key = "Unknown"
|
||||
val = t
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintf(dst, "\t\t- Type=%s\n\t\t Value=%v\n", key, val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// put payload
|
||||
if _, err := fmt.Fprintf(dst, "\tPayload: %#v\n", obj.Payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
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";
|
||||
|
||||
import "refs/types.proto";
|
||||
import "session/types.proto";
|
||||
import "service/verify.proto";
|
||||
import "storagegroup/types.proto";
|
||||
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
|
||||
|
||||
|
@ -36,8 +36,8 @@ message Header {
|
|||
Transform Transform = 4;
|
||||
// Tombstone header that set up in deleted objects
|
||||
Tombstone Tombstone = 5;
|
||||
// Verify header that contains session public key and user's signature
|
||||
session.VerificationHeader Verify = 6;
|
||||
// Token header contains token of the session within which the object was created
|
||||
service.Token Token = 6;
|
||||
// HomoHash is a homomorphic hash of original object payload
|
||||
bytes HomoHash = 7 [(gogoproto.customtype) = "Hash"];
|
||||
// PayloadChecksum of actual object's payload
|
||||
|
@ -70,6 +70,8 @@ message SystemHeader {
|
|||
}
|
||||
|
||||
message CreationPoint {
|
||||
option (gogoproto.goproto_stringer) = false;
|
||||
|
||||
// UnixTime is a date of creation in unixtime format
|
||||
int64 UnixTime = 1;
|
||||
// Epoch is a date of creation in NeoFS epochs
|
||||
|
|
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"
|
||||
"strconv"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/session"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
@ -46,11 +45,10 @@ func (b ByteSize) String() string {
|
|||
|
||||
// MakePutRequestHeader combines object and session token value
|
||||
// into header of object put request.
|
||||
func MakePutRequestHeader(obj *Object, token *session.Token) *PutRequest {
|
||||
func MakePutRequestHeader(obj *Object) *PutRequest {
|
||||
return &PutRequest{
|
||||
R: &PutRequest_Header{Header: &PutRequest_PutHeader{
|
||||
Object: obj,
|
||||
Token: token,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ func (m Object) Verify() error {
|
|||
integrity := ih.Value.(*Header_Integrity).Integrity
|
||||
|
||||
// Prepare structures
|
||||
_, vh := m.LastHeader(HeaderType(VerifyHdr))
|
||||
_, vh := m.LastHeader(HeaderType(TokenHdr))
|
||||
if vh == nil {
|
||||
_, pkh := m.LastHeader(HeaderType(PublicKeyHdr))
|
||||
if pkh == nil {
|
||||
|
@ -85,7 +85,7 @@ func (m Object) Verify() error {
|
|||
}
|
||||
pubkey = pkh.Value.(*Header_PublicKey).PublicKey.Value
|
||||
} else {
|
||||
pubkey = vh.Value.(*Header_Verify).Verify.PublicKey
|
||||
pubkey = vh.Value.(*Header_Token).Token.GetSessionKey()
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"github.com/google/uuid"
|
||||
"github.com/nspcc-dev/neofs-api-go/container"
|
||||
"github.com/nspcc-dev/neofs-api-go/refs"
|
||||
"github.com/nspcc-dev/neofs-api-go/session"
|
||||
crypto "github.com/nspcc-dev/neofs-crypto"
|
||||
"github.com/nspcc-dev/neofs-crypto/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -77,11 +76,10 @@ func TestObject_Verify(t *testing.T) {
|
|||
|
||||
dataPK := crypto.MarshalPublicKey(&sessionkey.PublicKey)
|
||||
signature, err = crypto.Sign(key, dataPK)
|
||||
vh := &session.VerificationHeader{
|
||||
PublicKey: dataPK,
|
||||
KeySignature: signature,
|
||||
}
|
||||
obj.SetVerificationHeader(vh)
|
||||
tok := new(Token)
|
||||
tok.SetSignature(signature)
|
||||
tok.SetSessionKey(dataPK)
|
||||
obj.AddHeader(&Header{Value: &Header_Token{Token: tok}})
|
||||
|
||||
// validation header is not last
|
||||
t.Run("error validation header is not last", func(t *testing.T) {
|
||||
|
@ -90,7 +88,7 @@ func TestObject_Verify(t *testing.T) {
|
|||
})
|
||||
|
||||
obj.Headers = obj.Headers[:len(obj.Headers)-2]
|
||||
obj.SetVerificationHeader(vh)
|
||||
obj.AddHeader(&Header{Value: &Header_Token{Token: tok}})
|
||||
obj.SetHeader(&Header{Value: &Header_Integrity{ih}})
|
||||
|
||||
t.Run("error invalid header checksum", func(t *testing.T) {
|
||||
|
@ -115,7 +113,7 @@ func TestObject_Verify(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
obj.SetHeader(genIH)
|
||||
|
||||
t.Run("correct with vh", func(t *testing.T) {
|
||||
t.Run("correct with tok", func(t *testing.T) {
|
||||
err = obj.Verify()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
@ -123,7 +121,7 @@ func TestObject_Verify(t *testing.T) {
|
|||
pkh := Header{Value: &Header_PublicKey{&PublicKey{
|
||||
Value: crypto.MarshalPublicKey(&key.PublicKey),
|
||||
}}}
|
||||
// replace vh with pkh
|
||||
// replace tok with pkh
|
||||
obj.Headers[len(obj.Headers)-2] = pkh
|
||||
// re-sign object
|
||||
obj.Sign(sessionkey)
|
||||
|
|
|
@ -37,6 +37,23 @@ type (
|
|||
OwnerID chain.WalletAddress
|
||||
)
|
||||
|
||||
// OwnerIDSource is an interface of the container of an OwnerID value with read access.
|
||||
type OwnerIDSource interface {
|
||||
GetOwnerID() OwnerID
|
||||
}
|
||||
|
||||
// OwnerIDContainer is an interface of the container of an OwnerID value.
|
||||
type OwnerIDContainer interface {
|
||||
OwnerIDSource
|
||||
SetOwnerID(OwnerID)
|
||||
}
|
||||
|
||||
// AddressContainer is an interface of the container of object address value.
|
||||
type AddressContainer interface {
|
||||
GetAddress() Address
|
||||
SetAddress(Address)
|
||||
}
|
||||
|
||||
const (
|
||||
// UUIDSize contains size of UUID.
|
||||
UUIDSize = 16
|
||||
|
|
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
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neofs-api-go/internal"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type (
|
||||
// MetaHeader contains meta information of request.
|
||||
// It provides methods to get or set meta information meta header.
|
||||
// Also contains methods to reset and restore meta header.
|
||||
// Also contains methods to get or set request protocol version
|
||||
MetaHeader interface {
|
||||
ResetMeta() RequestMetaHeader
|
||||
RestoreMeta(RequestMetaHeader)
|
||||
|
||||
// TTLRequest to verify and update ttl requests.
|
||||
GetTTL() uint32
|
||||
SetTTL(uint32)
|
||||
|
||||
// EpochHeader gives possibility to get or set epoch in RPC Requests.
|
||||
EpochHeader
|
||||
|
||||
// VersionHeader allows get or set version of protocol request
|
||||
VersionHeader
|
||||
}
|
||||
|
||||
// EpochHeader interface gives possibility to get or set epoch in RPC Requests.
|
||||
EpochHeader interface {
|
||||
GetEpoch() uint64
|
||||
SetEpoch(v uint64)
|
||||
}
|
||||
|
||||
// VersionHeader allows get or set version of protocol request
|
||||
VersionHeader interface {
|
||||
GetVersion() uint32
|
||||
SetVersion(uint32)
|
||||
}
|
||||
|
||||
// TTLCondition is closure, that allows to validate request with ttl.
|
||||
TTLCondition func(ttl uint32) error
|
||||
)
|
||||
|
||||
const (
|
||||
// ZeroTTL is empty ttl, should produce ErrZeroTTL.
|
||||
ZeroTTL = iota
|
||||
|
||||
// NonForwardingTTL is a ttl that allows direct connections only.
|
||||
NonForwardingTTL
|
||||
|
||||
// SingleForwardingTTL is a ttl that allows connections through another node.
|
||||
SingleForwardingTTL
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrZeroTTL is raised when zero ttl is passed.
|
||||
ErrZeroTTL = internal.Error("zero ttl")
|
||||
|
||||
// ErrIncorrectTTL is raised when NonForwardingTTL is passed and NodeRole != InnerRingNode.
|
||||
ErrIncorrectTTL = internal.Error("incorrect ttl")
|
||||
)
|
||||
|
||||
// SetVersion sets protocol version to ResponseMetaHeader.
|
||||
func (m *ResponseMetaHeader) SetVersion(v uint32) { m.Version = v }
|
||||
|
||||
// SetEpoch sets Epoch to ResponseMetaHeader.
|
||||
func (m *ResponseMetaHeader) SetEpoch(v uint64) { m.Epoch = v }
|
||||
|
||||
// SetVersion sets protocol version to RequestMetaHeader.
|
||||
func (m *RequestMetaHeader) SetVersion(v uint32) { m.Version = v }
|
||||
|
||||
// SetTTL sets TTL to RequestMetaHeader.
|
||||
func (m *RequestMetaHeader) SetTTL(v uint32) { m.TTL = v }
|
||||
|
||||
// SetEpoch sets Epoch to RequestMetaHeader.
|
||||
func (m *RequestMetaHeader) SetEpoch(v uint64) { m.Epoch = v }
|
||||
|
||||
// ResetMeta returns current value and sets RequestMetaHeader to empty value.
|
||||
func (m *RequestMetaHeader) ResetMeta() RequestMetaHeader {
|
||||
// CutMeta returns current value and sets RequestMetaHeader to empty value.
|
||||
func (m *RequestMetaHeader) CutMeta() RequestMetaHeader {
|
||||
cp := *m
|
||||
m.Reset()
|
||||
return cp
|
||||
}
|
||||
|
||||
// RestoreMeta sets current RequestMetaHeader to passed value.
|
||||
func (m *RequestMetaHeader) RestoreMeta(v RequestMetaHeader) { *m = v }
|
||||
|
||||
// IRNonForwarding condition that allows NonForwardingTTL only for IR
|
||||
func IRNonForwarding(role NodeRole) TTLCondition {
|
||||
return func(ttl uint32) error {
|
||||
if ttl == NonForwardingTTL && role != InnerRingNode {
|
||||
return ErrIncorrectTTL
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessRequestTTL validates and update ttl requests.
|
||||
func ProcessRequestTTL(req MetaHeader, cond ...TTLCondition) error {
|
||||
ttl := req.GetTTL()
|
||||
|
||||
if ttl == ZeroTTL {
|
||||
return status.New(codes.InvalidArgument, ErrZeroTTL.Error()).Err()
|
||||
}
|
||||
|
||||
for i := range cond {
|
||||
if cond[i] == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// check specific condition:
|
||||
if err := cond[i](ttl); err != nil {
|
||||
if st, ok := status.FromError(errors.Cause(err)); ok {
|
||||
return st.Err()
|
||||
}
|
||||
|
||||
return status.New(codes.InvalidArgument, err.Error()).Err()
|
||||
}
|
||||
}
|
||||
|
||||
req.SetTTL(ttl - 1)
|
||||
|
||||
return nil
|
||||
func (m *RequestMetaHeader) RestoreMeta(v RequestMetaHeader) {
|
||||
*m = v
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -17,6 +17,8 @@ message RequestMetaHeader {
|
|||
// Version defines protocol version
|
||||
// TODO: not used for now, should be implemented in future
|
||||
uint32 Version = 3;
|
||||
// Raw determines whether the request is raw or not
|
||||
bool Raw = 4;
|
||||
}
|
||||
|
||||
// ResponseMetaHeader contains meta information based on request processing by server
|
||||
|
|
|
@ -3,102 +3,23 @@ package service
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type mockedRequest struct {
|
||||
msg string
|
||||
name string
|
||||
code codes.Code
|
||||
handler TTLCondition
|
||||
RequestMetaHeader
|
||||
}
|
||||
|
||||
func TestMetaRequest(t *testing.T) {
|
||||
tests := []mockedRequest{
|
||||
{
|
||||
name: "direct to ir node",
|
||||
handler: IRNonForwarding(InnerRingNode),
|
||||
RequestMetaHeader: RequestMetaHeader{TTL: NonForwardingTTL},
|
||||
},
|
||||
{
|
||||
code: codes.InvalidArgument,
|
||||
msg: ErrIncorrectTTL.Error(),
|
||||
name: "direct to storage node",
|
||||
handler: IRNonForwarding(StorageNode),
|
||||
RequestMetaHeader: RequestMetaHeader{TTL: NonForwardingTTL},
|
||||
},
|
||||
{
|
||||
msg: ErrZeroTTL.Error(),
|
||||
code: codes.InvalidArgument,
|
||||
name: "zero ttl",
|
||||
handler: IRNonForwarding(StorageNode),
|
||||
RequestMetaHeader: RequestMetaHeader{TTL: ZeroTTL},
|
||||
},
|
||||
{
|
||||
name: "default to ir node",
|
||||
handler: IRNonForwarding(InnerRingNode),
|
||||
RequestMetaHeader: RequestMetaHeader{TTL: SingleForwardingTTL},
|
||||
},
|
||||
{
|
||||
name: "default to storage node",
|
||||
handler: IRNonForwarding(StorageNode),
|
||||
RequestMetaHeader: RequestMetaHeader{TTL: SingleForwardingTTL},
|
||||
},
|
||||
{
|
||||
msg: "not found",
|
||||
code: codes.NotFound,
|
||||
name: "custom status error",
|
||||
RequestMetaHeader: RequestMetaHeader{TTL: SingleForwardingTTL},
|
||||
handler: func(_ uint32) error { return status.Error(codes.NotFound, "not found") },
|
||||
},
|
||||
{
|
||||
msg: "not found",
|
||||
code: codes.NotFound,
|
||||
name: "custom wrapped status error",
|
||||
RequestMetaHeader: RequestMetaHeader{TTL: SingleForwardingTTL},
|
||||
handler: func(_ uint32) error {
|
||||
err := status.Error(codes.NotFound, "not found")
|
||||
err = errors.Wrap(err, "some error context")
|
||||
err = errors.Wrap(err, "another error context")
|
||||
return err
|
||||
},
|
||||
func TestCutRestoreMeta(t *testing.T) {
|
||||
items := []func() SeizedMetaHeaderContainer{
|
||||
func() SeizedMetaHeaderContainer {
|
||||
m := new(RequestMetaHeader)
|
||||
m.SetEpoch(1)
|
||||
return m
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
tt := tests[i]
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
before := tt.GetTTL()
|
||||
err := ProcessRequestTTL(&tt, tt.handler)
|
||||
if tt.msg != "" {
|
||||
require.Errorf(t, err, tt.msg)
|
||||
for _, item := range items {
|
||||
v1 := item()
|
||||
m1 := v1.CutMeta()
|
||||
v1.RestoreMeta(m1)
|
||||
|
||||
state, ok := status.FromError(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, tt.code, state.Code())
|
||||
require.Equal(t, tt.msg, state.Message())
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.NotEqualf(t, before, tt.GetTTL(), "ttl should be changed: %d vs %d", before, tt.GetTTL())
|
||||
}
|
||||
})
|
||||
require.Equal(t, item(), v1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestMetaHeader_SetEpoch(t *testing.T) {
|
||||
m := new(ResponseMetaHeader)
|
||||
epoch := uint64(3)
|
||||
m.SetEpoch(epoch)
|
||||
require.Equal(t, epoch, m.GetEpoch())
|
||||
}
|
||||
|
||||
func TestRequestMetaHeader_SetVersion(t *testing.T) {
|
||||
m := new(ResponseMetaHeader)
|
||||
version := uint32(3)
|
||||
m.SetVersion(version)
|
||||
require.Equal(t, version, m.GetVersion())
|
||||
}
|
||||
|
|
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
|
||||
|
||||
// NodeRole to identify in Bootstrap service.
|
||||
type NodeRole int32
|
||||
import "encoding/binary"
|
||||
|
||||
const (
|
||||
_ NodeRole = iota
|
||||
|
@ -22,3 +21,17 @@ func (nt NodeRole) String() string {
|
|||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Size returns the size necessary for a binary representation of the NodeRole.
|
||||
func (nt NodeRole) Size() int {
|
||||
return 4
|
||||
}
|
||||
|
||||
// Bytes returns a binary representation of the NodeRole.
|
||||
func (nt NodeRole) Bytes() []byte {
|
||||
data := make([]byte, nt.Size())
|
||||
|
||||
binary.BigEndian.PutUint32(data, uint32(nt))
|
||||
|
||||
return data
|
||||
}
|
||||
|
|
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 (
|
||||
"crypto/ecdsa"
|
||||
"sync"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/internal"
|
||||
"github.com/nspcc-dev/neofs-api-go/refs"
|
||||
crypto "github.com/nspcc-dev/neofs-crypto"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type (
|
||||
// VerifiableRequest adds possibility to sign and verify request header.
|
||||
VerifiableRequest interface {
|
||||
Size() int
|
||||
MarshalTo([]byte) (int, error)
|
||||
AddSignature(*RequestVerificationHeader_Signature)
|
||||
GetSignatures() []*RequestVerificationHeader_Signature
|
||||
SetSignatures([]*RequestVerificationHeader_Signature)
|
||||
// GetSessionToken returns SessionToken interface of Token field.
|
||||
//
|
||||
// If token field value is nil, nil returns.
|
||||
func (m RequestVerificationHeader) GetSessionToken() SessionToken {
|
||||
if t := m.GetToken(); t != nil {
|
||||
return t
|
||||
}
|
||||
|
||||
// MaintainableRequest adds possibility to set and get (+validate)
|
||||
// owner (client) public key from RequestVerificationHeader.
|
||||
MaintainableRequest interface {
|
||||
GetOwner() (*ecdsa.PublicKey, error)
|
||||
SetOwner(*ecdsa.PublicKey, []byte)
|
||||
GetLastPeer() (*ecdsa.PublicKey, error)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddSignKey adds new element to Signatures field.
|
||||
//
|
||||
// Sets Sign field to passed sign. Set Peer field to marshaled passed key.
|
||||
func (m *RequestVerificationHeader) AddSignKey(sign []byte, key *ecdsa.PublicKey) {
|
||||
m.SetSignatures(
|
||||
append(
|
||||
m.GetSignatures(),
|
||||
&RequestVerificationHeader_Signature{
|
||||
Sign: sign,
|
||||
Peer: crypto.MarshalPublicKey(key),
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// GetSignKeyPairs returns the elements of Signatures field as SignKeyPair slice.
|
||||
func (m RequestVerificationHeader) GetSignKeyPairs() []SignKeyPair {
|
||||
var (
|
||||
signs = m.GetSignatures()
|
||||
res = make([]SignKeyPair, len(signs))
|
||||
)
|
||||
|
||||
for i := range signs {
|
||||
res[i] = signs[i]
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrCannotLoadPublicKey is raised when cannot unmarshal public key from RequestVerificationHeader_Sign.
|
||||
ErrCannotLoadPublicKey = internal.Error("cannot load public key")
|
||||
return res
|
||||
}
|
||||
|
||||
// ErrCannotFindOwner is raised when signatures empty in GetOwner.
|
||||
ErrCannotFindOwner = internal.Error("cannot find owner public key")
|
||||
// GetSignature returns the result of a Sign field getter.
|
||||
func (m RequestVerificationHeader_Signature) GetSignature() []byte {
|
||||
return m.GetSign()
|
||||
}
|
||||
|
||||
// ErrWrongOwner is raised when passed OwnerID not equal to present PublicKey
|
||||
ErrWrongOwner = internal.Error("wrong owner")
|
||||
)
|
||||
// GetPublicKey unmarshals and returns the result of a Peer field getter.
|
||||
func (m RequestVerificationHeader_Signature) GetPublicKey() *ecdsa.PublicKey {
|
||||
return crypto.UnmarshalPublicKey(m.GetPeer())
|
||||
}
|
||||
|
||||
// SetSignatures replaces signatures stored in RequestVerificationHeader.
|
||||
func (m *RequestVerificationHeader) SetSignatures(signatures []*RequestVerificationHeader_Signature) {
|
||||
m.Signatures = signatures
|
||||
}
|
||||
|
||||
// AddSignature adds new Signature into RequestVerificationHeader.
|
||||
func (m *RequestVerificationHeader) AddSignature(sig *RequestVerificationHeader_Signature) {
|
||||
if sig == nil {
|
||||
return
|
||||
}
|
||||
m.Signatures = append(m.Signatures, sig)
|
||||
}
|
||||
|
||||
// SetOwner adds origin (sign and public key) of owner (client) into first signature.
|
||||
func (m *RequestVerificationHeader) SetOwner(pub *ecdsa.PublicKey, sign []byte) {
|
||||
if len(m.Signatures) == 0 || pub == nil {
|
||||
return
|
||||
}
|
||||
|
||||
m.Signatures[0].Origin = &RequestVerificationHeader_Sign{
|
||||
Sign: sign,
|
||||
Peer: crypto.MarshalPublicKey(pub),
|
||||
}
|
||||
}
|
||||
|
||||
// CheckOwner validates, that passed OwnerID is equal to present PublicKey of owner.
|
||||
func (m *RequestVerificationHeader) CheckOwner(owner refs.OwnerID) error {
|
||||
if key, err := m.GetOwner(); err != nil {
|
||||
return err
|
||||
} else if user, err := refs.NewOwnerID(key); err != nil {
|
||||
return err
|
||||
} else if !user.Equal(owner) {
|
||||
return ErrWrongOwner
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOwner tries to get owner (client) public key from signatures.
|
||||
// If signatures contains not empty Origin, we should try to validate,
|
||||
// that session key was signed by owner (client), otherwise return error.
|
||||
func (m *RequestVerificationHeader) GetOwner() (*ecdsa.PublicKey, error) {
|
||||
if len(m.Signatures) == 0 {
|
||||
return nil, ErrCannotFindOwner
|
||||
}
|
||||
|
||||
// if first signature contains origin, we should try to validate session key
|
||||
if m.Signatures[0].Origin != nil {
|
||||
owner := crypto.UnmarshalPublicKey(m.Signatures[0].Origin.Peer)
|
||||
if owner == nil {
|
||||
return nil, ErrCannotLoadPublicKey
|
||||
} else if err := crypto.Verify(owner, m.Signatures[0].Peer, m.Signatures[0].Origin.Sign); err != nil {
|
||||
return nil, errors.Wrap(err, "could not verify session token")
|
||||
}
|
||||
|
||||
return owner, nil
|
||||
} else if key := crypto.UnmarshalPublicKey(m.Signatures[0].Peer); key != nil {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return nil, ErrCannotLoadPublicKey
|
||||
}
|
||||
|
||||
// GetLastPeer tries to get last peer public key from signatures.
|
||||
// If signatures has zero length, returns ErrCannotFindOwner.
|
||||
// If signatures has length equal to one, uses GetOwner.
|
||||
// Otherwise tries to unmarshal last peer public key.
|
||||
func (m *RequestVerificationHeader) GetLastPeer() (*ecdsa.PublicKey, error) {
|
||||
switch ln := len(m.Signatures); ln {
|
||||
case 0:
|
||||
return nil, ErrCannotFindOwner
|
||||
case 1:
|
||||
return m.GetOwner()
|
||||
default:
|
||||
if key := crypto.UnmarshalPublicKey(m.Signatures[ln-1].Peer); key != nil {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return nil, ErrCannotLoadPublicKey
|
||||
}
|
||||
}
|
||||
|
||||
func newSignature(key *ecdsa.PrivateKey, data []byte) (*RequestVerificationHeader_Signature, error) {
|
||||
sign, err := crypto.Sign(key, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &RequestVerificationHeader_Signature{
|
||||
RequestVerificationHeader_Sign: RequestVerificationHeader_Sign{
|
||||
Sign: sign,
|
||||
Peer: crypto.MarshalPublicKey(&key.PublicKey),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
var bytesPool = sync.Pool{New: func() interface{} {
|
||||
return make([]byte, 4.5*1024*1024) // 4.5MB
|
||||
}}
|
||||
|
||||
// SignRequestHeader receives private key and request with RequestVerificationHeader,
|
||||
// tries to marshal and sign request with passed PrivateKey, after that adds
|
||||
// new signature to headers. If something went wrong, returns error.
|
||||
func SignRequestHeader(key *ecdsa.PrivateKey, msg VerifiableRequest) error {
|
||||
// ignore meta header
|
||||
if meta, ok := msg.(MetaHeader); ok {
|
||||
h := meta.ResetMeta()
|
||||
|
||||
defer func() {
|
||||
meta.RestoreMeta(h)
|
||||
}()
|
||||
}
|
||||
|
||||
data := bytesPool.Get().([]byte)
|
||||
defer func() {
|
||||
bytesPool.Put(data)
|
||||
}()
|
||||
|
||||
if size := msg.Size(); size <= cap(data) {
|
||||
data = data[:size]
|
||||
} else {
|
||||
data = make([]byte, size)
|
||||
}
|
||||
|
||||
size, err := msg.MarshalTo(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signature, err := newSignature(key, data[:size])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg.AddSignature(signature)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyRequestHeader receives request with RequestVerificationHeader,
|
||||
// tries to marshal and verify each signature from request.
|
||||
// If something went wrong, returns error.
|
||||
func VerifyRequestHeader(msg VerifiableRequest) error {
|
||||
// ignore meta header
|
||||
if meta, ok := msg.(MetaHeader); ok {
|
||||
h := meta.ResetMeta()
|
||||
|
||||
defer func() {
|
||||
meta.RestoreMeta(h)
|
||||
}()
|
||||
}
|
||||
|
||||
data := bytesPool.Get().([]byte)
|
||||
signatures := msg.GetSignatures()
|
||||
defer func() {
|
||||
bytesPool.Put(data)
|
||||
msg.SetSignatures(signatures)
|
||||
}()
|
||||
|
||||
for i := range signatures {
|
||||
msg.SetSignatures(signatures[:i])
|
||||
peer := signatures[i].GetPeer()
|
||||
sign := signatures[i].GetSign()
|
||||
|
||||
key := crypto.UnmarshalPublicKey(peer)
|
||||
if key == nil {
|
||||
return errors.Wrapf(ErrCannotLoadPublicKey, "%d: %02x", i, peer)
|
||||
}
|
||||
|
||||
if size := msg.Size(); size <= cap(data) {
|
||||
data = data[:size]
|
||||
} else {
|
||||
data = make([]byte, size)
|
||||
}
|
||||
|
||||
if size, err := msg.MarshalTo(data); err != nil {
|
||||
return errors.Wrapf(err, "%d: %02x", i, peer)
|
||||
} else if err := crypto.Verify(key, data[:size], sign); err != nil {
|
||||
return errors.Wrapf(err, "%d: %02x", i, peer)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
// SetToken is a Token field setter.
|
||||
func (m *RequestVerificationHeader) SetToken(token *Token) {
|
||||
m.Token = token
|
||||
}
|
||||
|
||||
// testCustomField for test usage only.
|
||||
|
|
Binary file not shown.
|
@ -3,6 +3,7 @@ package service;
|
|||
option go_package = "github.com/nspcc-dev/neofs-api-go/service";
|
||||
option csharp_namespace = "NeoFS.API.Service";
|
||||
|
||||
import "refs/types.proto";
|
||||
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
|
||||
|
||||
option (gogoproto.stable_marshaler_all) = true;
|
||||
|
@ -10,22 +11,80 @@ option (gogoproto.stable_marshaler_all) = true;
|
|||
// RequestVerificationHeader is a set of signatures of every NeoFS Node that processed request
|
||||
// (should be embedded into message).
|
||||
message RequestVerificationHeader {
|
||||
message Sign {
|
||||
message Signature {
|
||||
// Sign is signature of the request or session key.
|
||||
bytes Sign = 1;
|
||||
// Peer is compressed public key used for signature.
|
||||
bytes Peer = 2;
|
||||
}
|
||||
|
||||
message Signature {
|
||||
// Sign is a signature and public key of the request.
|
||||
Sign Sign = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
|
||||
// Origin used for requests, when trusted node changes it and re-sign with session key.
|
||||
// If session key used for signature request, then Origin should contain
|
||||
// public key of user and signed session key.
|
||||
Sign Origin = 2;
|
||||
}
|
||||
|
||||
// Signatures is a set of signatures of every passed NeoFS Node
|
||||
repeated Signature Signatures = 1;
|
||||
|
||||
// Token is a token of the session within which the request is sent
|
||||
Token Token = 2;
|
||||
}
|
||||
|
||||
// User token granting rights for object manipulation
|
||||
message Token {
|
||||
message Info {
|
||||
// ID is a token identifier. valid UUIDv4 represented in bytes
|
||||
bytes ID = 1 [(gogoproto.customtype) = "TokenID", (gogoproto.nullable) = false];
|
||||
|
||||
// OwnerID is an owner of manipulation object
|
||||
bytes OwnerID = 2 [(gogoproto.customtype) = "OwnerID", (gogoproto.nullable) = false];
|
||||
|
||||
// Verb is an enumeration of session request types
|
||||
enum Verb {
|
||||
// Put refers to object.Put RPC call
|
||||
Put = 0;
|
||||
// Get refers to object.Get RPC call
|
||||
Get = 1;
|
||||
// Head refers to object.Head RPC call
|
||||
Head = 2;
|
||||
// Search refers to object.Search RPC call
|
||||
Search = 3;
|
||||
// Delete refers to object.Delete RPC call
|
||||
Delete = 4;
|
||||
// Range refers to object.GetRange RPC call
|
||||
Range = 5;
|
||||
// RangeHash refers to object.GetRangeHash RPC call
|
||||
RangeHash = 6;
|
||||
}
|
||||
|
||||
// Verb is a type of request for which the token is issued
|
||||
Verb verb = 3 [(gogoproto.customname) = "Verb"];
|
||||
|
||||
// Address is an object address for which token is issued
|
||||
refs.Address Address = 4 [(gogoproto.nullable) = false, (gogoproto.customtype) = "Address"];
|
||||
|
||||
// Lifetime is a lifetime of the session
|
||||
TokenLifetime Lifetime = 5 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
|
||||
|
||||
// SessionKey is a public key of session key
|
||||
bytes SessionKey = 6;
|
||||
}
|
||||
|
||||
// TokenInfo is a grouped information about token
|
||||
Info TokenInfo = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
|
||||
|
||||
// Signature is a signature of session token information
|
||||
bytes Signature = 8;
|
||||
}
|
||||
|
||||
// TokenLifetime carries a group of lifetime parameters of the token
|
||||
message TokenLifetime {
|
||||
// Created carries an initial epoch of token lifetime
|
||||
uint64 Created = 1;
|
||||
|
||||
// ValidUntil carries a last epoch of token lifetime
|
||||
uint64 ValidUntil = 2;
|
||||
}
|
||||
|
||||
// TODO: for variable token types and version redefine message
|
||||
// Example:
|
||||
// message Token {
|
||||
// TokenType TokenType = 1;
|
||||
// uint32 Version = 2;
|
||||
// bytes Data = 3;
|
||||
// }
|
||||
|
|
|
@ -1,201 +1,117 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/nspcc-dev/neofs-api-go/refs"
|
||||
crypto "github.com/nspcc-dev/neofs-crypto"
|
||||
"github.com/nspcc-dev/neofs-crypto/test"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func BenchmarkSignRequestHeader(b *testing.B) {
|
||||
func (m TestRequest) SignedData() ([]byte, error) {
|
||||
return SignedDataFromReader(m)
|
||||
}
|
||||
|
||||
func (m TestRequest) SignedDataSize() (sz int) {
|
||||
sz += 4
|
||||
|
||||
sz += len(m.StringField)
|
||||
|
||||
sz += len(m.BytesField)
|
||||
|
||||
sz += m.CustomField.Size()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (m TestRequest) ReadSignedData(p []byte) (int, error) {
|
||||
if len(p) < m.SignedDataSize() {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
var off int
|
||||
|
||||
binary.BigEndian.PutUint32(p[off:], uint32(m.IntField))
|
||||
off += 4
|
||||
|
||||
off += copy(p[off:], []byte(m.StringField))
|
||||
|
||||
off += copy(p[off:], m.BytesField)
|
||||
|
||||
n, err := m.CustomField.MarshalTo(p[off:])
|
||||
off += n
|
||||
|
||||
return off, err
|
||||
}
|
||||
|
||||
func BenchmarkSignDataWithSessionToken(b *testing.B) {
|
||||
key := test.DecodeKey(0)
|
||||
|
||||
custom := testCustomField{1, 2, 3, 4, 5, 6, 7, 8}
|
||||
customField := testCustomField{1, 2, 3, 4, 5, 6, 7, 8}
|
||||
|
||||
some := &TestRequest{
|
||||
IntField: math.MaxInt32,
|
||||
StringField: "TestRequestStringField",
|
||||
BytesField: make([]byte, 1<<22),
|
||||
CustomField: &custom,
|
||||
RequestMetaHeader: RequestMetaHeader{
|
||||
TTL: math.MaxInt32 - 8,
|
||||
Epoch: math.MaxInt64 - 12,
|
||||
},
|
||||
}
|
||||
token := new(Token)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
require.NoError(b, SignRequestHeader(key, some))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkVerifyRequestHeader(b *testing.B) {
|
||||
custom := testCustomField{1, 2, 3, 4, 5, 6, 7, 8}
|
||||
|
||||
some := &TestRequest{
|
||||
IntField: math.MaxInt32,
|
||||
StringField: "TestRequestStringField",
|
||||
BytesField: make([]byte, 1<<22),
|
||||
CustomField: &custom,
|
||||
RequestMetaHeader: RequestMetaHeader{
|
||||
TTL: math.MaxInt32 - 8,
|
||||
Epoch: math.MaxInt64 - 12,
|
||||
},
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
key := test.DecodeKey(i)
|
||||
require.NoError(b, SignRequestHeader(key, some))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
require.NoError(b, VerifyRequestHeader(some))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignRequestHeader(t *testing.T) {
|
||||
req := &TestRequest{
|
||||
IntField: math.MaxInt32,
|
||||
StringField: "TestRequestStringField",
|
||||
BytesField: []byte("TestRequestBytesField"),
|
||||
BytesField: make([]byte, 1<<22),
|
||||
CustomField: &customField,
|
||||
}
|
||||
|
||||
key := test.DecodeKey(0)
|
||||
peer := crypto.MarshalPublicKey(&key.PublicKey)
|
||||
req.SetTTL(math.MaxInt32 - 8)
|
||||
req.SetEpoch(math.MaxInt64 - 12)
|
||||
req.SetToken(token)
|
||||
|
||||
data, err := req.Marshal()
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
require.NoError(b, SignDataWithSessionToken(key, req))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkVerifyAccumulatedSignaturesWithToken(b *testing.B) {
|
||||
customField := testCustomField{1, 2, 3, 4, 5, 6, 7, 8}
|
||||
|
||||
token := new(Token)
|
||||
|
||||
req := &TestRequest{
|
||||
IntField: math.MaxInt32,
|
||||
StringField: "TestRequestStringField",
|
||||
BytesField: make([]byte, 1<<22),
|
||||
CustomField: &customField,
|
||||
}
|
||||
|
||||
req.SetTTL(math.MaxInt32 - 8)
|
||||
req.SetEpoch(math.MaxInt64 - 12)
|
||||
req.SetToken(token)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
key := test.DecodeKey(i)
|
||||
require.NoError(b, SignDataWithSessionToken(key, req))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
require.NoError(b, VerifyAccumulatedSignaturesWithToken(req))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestVerificationHeader_SetToken(t *testing.T) {
|
||||
id, err := refs.NewUUID()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, SignRequestHeader(key, req))
|
||||
token := new(Token)
|
||||
token.SetID(id)
|
||||
|
||||
require.Len(t, req.Signatures, 1)
|
||||
for i := range req.Signatures {
|
||||
sign := req.Signatures[i].GetSign()
|
||||
require.Equal(t, peer, req.Signatures[i].GetPeer())
|
||||
require.NoError(t, crypto.Verify(&key.PublicKey, data, sign))
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyRequestHeader(t *testing.T) {
|
||||
req := &TestRequest{
|
||||
IntField: math.MaxInt32,
|
||||
StringField: "TestRequestStringField",
|
||||
BytesField: []byte("TestRequestBytesField"),
|
||||
RequestMetaHeader: RequestMetaHeader{TTL: 10},
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
req.TTL--
|
||||
require.NoError(t, SignRequestHeader(test.DecodeKey(i), req))
|
||||
}
|
||||
|
||||
require.NoError(t, VerifyRequestHeader(req))
|
||||
}
|
||||
|
||||
func TestMaintainableRequest(t *testing.T) {
|
||||
req := &TestRequest{
|
||||
IntField: math.MaxInt32,
|
||||
StringField: "TestRequestStringField",
|
||||
BytesField: []byte("TestRequestBytesField"),
|
||||
RequestMetaHeader: RequestMetaHeader{TTL: 10},
|
||||
}
|
||||
|
||||
count := 10
|
||||
owner := test.DecodeKey(count + 1)
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
req.TTL--
|
||||
|
||||
key := test.DecodeKey(i)
|
||||
require.NoError(t, SignRequestHeader(key, req))
|
||||
|
||||
// sign first key (session key) by owner key
|
||||
if i == 0 {
|
||||
sign, err := crypto.Sign(owner, crypto.MarshalPublicKey(&key.PublicKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
req.SetOwner(&owner.PublicKey, sign)
|
||||
}
|
||||
}
|
||||
|
||||
{ // Validate owner
|
||||
user, err := refs.NewOwnerID(&owner.PublicKey)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, req.CheckOwner(user))
|
||||
}
|
||||
|
||||
{ // Good case:
|
||||
require.NoError(t, VerifyRequestHeader(req))
|
||||
|
||||
// validate, that first key (session key) was signed with owner
|
||||
signatures := req.GetSignatures()
|
||||
|
||||
require.Len(t, signatures, count)
|
||||
|
||||
pub, err := req.GetOwner()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, &owner.PublicKey, pub)
|
||||
}
|
||||
|
||||
{ // wrong owner:
|
||||
req.Signatures[0].Origin = nil
|
||||
|
||||
pub, err := req.GetOwner()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEqual(t, &owner.PublicKey, pub)
|
||||
}
|
||||
|
||||
{ // Wrong signatures:
|
||||
copy(req.Signatures[count-1].Sign, req.Signatures[count-1].Peer)
|
||||
err := VerifyRequestHeader(req)
|
||||
require.EqualError(t, errors.Cause(err), crypto.ErrInvalidSignature.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyAndSignRequestHeaderWithoutCloning(t *testing.T) {
|
||||
key := test.DecodeKey(0)
|
||||
|
||||
custom := testCustomField{1, 2, 3, 4, 5, 6, 7, 8}
|
||||
|
||||
b := &TestRequest{
|
||||
IntField: math.MaxInt32,
|
||||
StringField: "TestRequestStringField",
|
||||
BytesField: []byte("TestRequestBytesField"),
|
||||
CustomField: &custom,
|
||||
RequestMetaHeader: RequestMetaHeader{
|
||||
TTL: math.MaxInt32 - 8,
|
||||
Epoch: math.MaxInt64 - 12,
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, SignRequestHeader(key, b))
|
||||
require.NoError(t, VerifyRequestHeader(b))
|
||||
|
||||
require.Len(t, b.Signatures, 1)
|
||||
require.Equal(t, custom, *b.CustomField)
|
||||
require.Equal(t, uint32(math.MaxInt32-8), b.GetTTL())
|
||||
require.Equal(t, uint64(math.MaxInt64-12), b.GetEpoch())
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
log.SetOutput(buf)
|
||||
|
||||
cp, ok := proto.Clone(b).(*TestRequest)
|
||||
require.True(t, ok)
|
||||
require.NotEqual(t, b, cp)
|
||||
|
||||
require.Contains(t, buf.String(), "proto: don't know how to copy")
|
||||
h := new(RequestVerificationHeader)
|
||||
|
||||
h.SetToken(token)
|
||||
|
||||
require.Equal(t, token, h.GetToken())
|
||||
}
|
||||
|
|
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 csharp_namespace = "NeoFS.API.Session";
|
||||
|
||||
import "session/types.proto";
|
||||
import "service/meta.proto";
|
||||
import "service/verify.proto";
|
||||
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
|
||||
|
@ -12,42 +11,29 @@ option (gogoproto.stable_marshaler_all) = true;
|
|||
|
||||
|
||||
service Session {
|
||||
// Create is a method that used to open a trusted session to manipulate
|
||||
// an object. In order to put or delete object client have to obtain session
|
||||
// token with trusted node. Trusted node will modify client's object
|
||||
// (add missing headers, checksums, homomorphic hash) and sign id with
|
||||
// session key. Session is established during 4-step handshake in one gRPC stream
|
||||
//
|
||||
// - First client stream message SHOULD BE type of `CreateRequest_Init`.
|
||||
// - First server stream message SHOULD BE type of `CreateResponse_Unsigned`.
|
||||
// - Second client stream message SHOULD BE type of `CreateRequest_Signed`.
|
||||
// - Second server stream message SHOULD BE type of `CreateResponse_Result`.
|
||||
rpc Create (stream CreateRequest) returns (stream CreateResponse);
|
||||
// Create opens new session between the client and the server
|
||||
rpc Create (CreateRequest) returns (CreateResponse);
|
||||
}
|
||||
|
||||
|
||||
// CreateRequest carries an information necessary for opening a session
|
||||
message CreateRequest {
|
||||
// Message should be one of
|
||||
oneof Message {
|
||||
// Init is a message to initialize session opening. Carry:
|
||||
// owner of manipulation object;
|
||||
// ID of manipulation object;
|
||||
// token lifetime bounds.
|
||||
session.Token Init = 1;
|
||||
// Signed Init message response (Unsigned) from server with user private key
|
||||
session.Token Signed = 2;
|
||||
}
|
||||
// OwnerID carries an identifier of a session initiator
|
||||
bytes OwnerID = 1 [(gogoproto.nullable) = false, (gogoproto.customtype) = "OwnerID"];
|
||||
|
||||
// Lifetime carries a lifetime of the session
|
||||
service.TokenLifetime Lifetime = 2 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
|
||||
|
||||
// RequestMetaHeader contains information about request meta headers (should be embedded into message)
|
||||
service.RequestMetaHeader Meta = 98 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
|
||||
// RequestVerificationHeader is a set of signatures of every NeoFS Node that processed request (should be embedded into message)
|
||||
service.RequestVerificationHeader Verify = 99 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
|
||||
}
|
||||
|
||||
// CreateResponse carries an information about the opened session
|
||||
message CreateResponse {
|
||||
oneof Message {
|
||||
// Unsigned token with token ID and session public key generated on server side
|
||||
session.Token Unsigned = 1;
|
||||
// Result is a resulting token which can be used for object placing through an trusted intermediary
|
||||
session.Token Result = 2;
|
||||
}
|
||||
// ID carries an identifier of session token
|
||||
bytes ID = 1 [(gogoproto.customtype) = "TokenID", (gogoproto.nullable) = false];
|
||||
|
||||
// SessionKey carries a session public key
|
||||
bytes SessionKey = 2;
|
||||
}
|
||||
|
|
|
@ -1,82 +1,64 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"sync"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/refs"
|
||||
crypto "github.com/nspcc-dev/neofs-crypto"
|
||||
)
|
||||
|
||||
type simpleStore struct {
|
||||
type mapTokenStore struct {
|
||||
*sync.RWMutex
|
||||
|
||||
tokens map[TokenID]*PToken
|
||||
tokens map[PrivateTokenKey]PrivateToken
|
||||
}
|
||||
|
||||
// TODO get curve from neofs-crypto
|
||||
func defaultCurve() elliptic.Curve {
|
||||
return elliptic.P256()
|
||||
}
|
||||
|
||||
// NewSimpleStore creates simple token storage
|
||||
func NewSimpleStore() TokenStore {
|
||||
return &simpleStore{
|
||||
// NewMapTokenStore creates new PrivateTokenStore instance.
|
||||
//
|
||||
// The elements of the instance are stored in the map.
|
||||
func NewMapTokenStore() PrivateTokenStore {
|
||||
return &mapTokenStore{
|
||||
RWMutex: new(sync.RWMutex),
|
||||
tokens: make(map[TokenID]*PToken),
|
||||
tokens: make(map[PrivateTokenKey]PrivateToken),
|
||||
}
|
||||
}
|
||||
|
||||
// New returns new token with specified parameters.
|
||||
func (s *simpleStore) New(p TokenParams) *PToken {
|
||||
tid, err := refs.NewUUID()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
key, err := ecdsa.GenerateKey(defaultCurve(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.FirstEpoch > p.LastEpoch || p.OwnerID.Empty() {
|
||||
return nil
|
||||
}
|
||||
|
||||
t := &PToken{
|
||||
mtx: new(sync.Mutex),
|
||||
Token: Token{
|
||||
ID: tid,
|
||||
Header: VerificationHeader{PublicKey: crypto.MarshalPublicKey(&key.PublicKey)},
|
||||
FirstEpoch: p.FirstEpoch,
|
||||
LastEpoch: p.LastEpoch,
|
||||
ObjectID: p.ObjectID,
|
||||
OwnerID: p.OwnerID,
|
||||
PublicKeys: p.PublicKeys,
|
||||
},
|
||||
PrivateKey: key,
|
||||
}
|
||||
|
||||
// Store adds passed token to the map.
|
||||
//
|
||||
// Resulting error is always nil.
|
||||
func (s *mapTokenStore) Store(key PrivateTokenKey, token PrivateToken) error {
|
||||
s.Lock()
|
||||
s.tokens[t.ID] = t
|
||||
s.tokens[key] = token
|
||||
s.Unlock()
|
||||
|
||||
return t
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fetch tries to fetch a token with specified id.
|
||||
func (s *simpleStore) Fetch(id TokenID) *PToken {
|
||||
// Fetch returns the map element corresponding to the given key.
|
||||
//
|
||||
// Returns ErrPrivateTokenNotFound is there is no element in map.
|
||||
func (s *mapTokenStore) Fetch(key PrivateTokenKey) (PrivateToken, error) {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
return s.tokens[id]
|
||||
t, ok := s.tokens[key]
|
||||
if !ok {
|
||||
return nil, ErrPrivateTokenNotFound
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Remove removes token with id from store.
|
||||
func (s *simpleStore) Remove(id TokenID) {
|
||||
// RemoveExpired removes all the map elements that are expired in the passed epoch.
|
||||
//
|
||||
// Resulting error is always nil.
|
||||
func (s *mapTokenStore) RemoveExpired(epoch uint64) error {
|
||||
s.Lock()
|
||||
delete(s.tokens, id)
|
||||
|
||||
for key, token := range s.tokens {
|
||||
if token.Expired(epoch) {
|
||||
delete(s.tokens, key)
|
||||
}
|
||||
}
|
||||
|
||||
s.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,96 +1,111 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/refs"
|
||||
crypto "github.com/nspcc-dev/neofs-crypto"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testClient struct {
|
||||
*ecdsa.PrivateKey
|
||||
OwnerID OwnerID
|
||||
func TestMapTokenStore(t *testing.T) {
|
||||
// create new private token
|
||||
pToken, err := NewPrivateToken(0)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create map token store
|
||||
s := NewMapTokenStore()
|
||||
|
||||
// create test TokenID
|
||||
tid, err := refs.NewUUID()
|
||||
require.NoError(t, err)
|
||||
|
||||
// create test OwnerID
|
||||
ownerID := OwnerID{1, 2, 3}
|
||||
|
||||
key := PrivateTokenKey{}
|
||||
key.SetOwnerID(ownerID)
|
||||
key.SetTokenID(tid)
|
||||
|
||||
// ascertain that there is no record for the key
|
||||
_, err = s.Fetch(key)
|
||||
require.EqualError(t, err, ErrPrivateTokenNotFound.Error())
|
||||
|
||||
// save private token record
|
||||
require.NoError(t, s.Store(key, pToken))
|
||||
|
||||
// fetch private token by the key
|
||||
res, err := s.Fetch(key)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ascertain that returned token equals to initial
|
||||
require.Equal(t, pToken, res)
|
||||
}
|
||||
|
||||
func (c *testClient) Sign(data []byte) ([]byte, error) {
|
||||
return crypto.Sign(c.PrivateKey, data)
|
||||
}
|
||||
|
||||
func newTestClient(t *testing.T) *testClient {
|
||||
key, err := ecdsa.GenerateKey(defaultCurve(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
owner, err := refs.NewOwnerID(&key.PublicKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &testClient{PrivateKey: key, OwnerID: owner}
|
||||
}
|
||||
|
||||
func signToken(t *testing.T, token *PToken, c *testClient) {
|
||||
require.NotNil(t, token)
|
||||
token.SetPublicKeys(&c.PublicKey)
|
||||
|
||||
signH, err := c.Sign(token.Header.PublicKey)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, signH)
|
||||
|
||||
// data is not yet signed
|
||||
keys := UnmarshalPublicKeys(&token.Token)
|
||||
require.False(t, token.Verify(keys...))
|
||||
|
||||
signT, err := c.Sign(token.verificationData())
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, signT)
|
||||
|
||||
token.AddSignatures(signH, signT)
|
||||
require.True(t, token.Verify(keys...))
|
||||
}
|
||||
|
||||
func TestTokenStore(t *testing.T) {
|
||||
s := NewSimpleStore()
|
||||
|
||||
oid, err := refs.NewObjectID()
|
||||
require.NoError(t, err)
|
||||
|
||||
c := newTestClient(t)
|
||||
require.NotNil(t, c)
|
||||
pk := [][]byte{crypto.MarshalPublicKey(&c.PublicKey)}
|
||||
|
||||
// create new token
|
||||
token := s.New(TokenParams{
|
||||
ObjectID: []ObjectID{oid},
|
||||
OwnerID: c.OwnerID,
|
||||
PublicKeys: pk,
|
||||
})
|
||||
signToken(t, token, c)
|
||||
|
||||
// check that it can be fetched
|
||||
t1 := s.Fetch(token.ID)
|
||||
require.NotNil(t, t1)
|
||||
require.Equal(t, token, t1)
|
||||
|
||||
// create and sign another token by the same client
|
||||
t1 = s.New(TokenParams{
|
||||
ObjectID: []ObjectID{oid},
|
||||
OwnerID: c.OwnerID,
|
||||
PublicKeys: pk,
|
||||
})
|
||||
|
||||
signToken(t, t1, c)
|
||||
|
||||
data := []byte{1, 2, 3}
|
||||
sign, err := t1.SignData(data)
|
||||
require.NoError(t, err)
|
||||
require.Error(t, token.Header.VerifyData(data, sign))
|
||||
|
||||
sign, err = token.SignData(data)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, token.Header.VerifyData(data, sign))
|
||||
|
||||
s.Remove(token.ID)
|
||||
require.Nil(t, s.Fetch(token.ID))
|
||||
require.NotNil(t, s.Fetch(t1.ID))
|
||||
func TestMapTokenStore_RemoveExpired(t *testing.T) {
|
||||
// create some epoch number
|
||||
e1 := uint64(1)
|
||||
|
||||
// create private token that expires after e1
|
||||
tok1, err := NewPrivateToken(e1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create some greater than e1 epoch number
|
||||
e2 := e1 + 1
|
||||
|
||||
// create private token that expires after e2
|
||||
tok2, err := NewPrivateToken(e2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create token store instance
|
||||
s := NewMapTokenStore()
|
||||
|
||||
// create test PrivateTokenKey
|
||||
key := PrivateTokenKey{}
|
||||
key.SetOwnerID(OwnerID{1, 2, 3})
|
||||
|
||||
// create IDs for tokens
|
||||
id1, err := refs.NewUUID()
|
||||
require.NoError(t, err)
|
||||
id2, err := refs.NewUUID()
|
||||
require.NoError(t, err)
|
||||
|
||||
assertPresence := func(ids ...TokenID) {
|
||||
for i := range ids {
|
||||
key.SetTokenID(ids[i])
|
||||
_, err = s.Fetch(key)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
assertAbsence := func(ids ...TokenID) {
|
||||
for i := range ids {
|
||||
key.SetTokenID(ids[i])
|
||||
_, err = s.Fetch(key)
|
||||
require.EqualError(t, err, ErrPrivateTokenNotFound.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// store both tokens
|
||||
key.SetTokenID(id1)
|
||||
require.NoError(t, s.Store(key, tok1))
|
||||
key.SetTokenID(id2)
|
||||
require.NoError(t, s.Store(key, tok2))
|
||||
|
||||
// ascertain that both tokens are available
|
||||
assertPresence(id1, id2)
|
||||
|
||||
// perform cleaning for epoch in which both tokens are not expired
|
||||
require.NoError(t, s.RemoveExpired(e1))
|
||||
|
||||
// ascertain that both tokens are still available
|
||||
assertPresence(id1, id2)
|
||||
|
||||
// perform cleaning for epoch greater than e1 and not greater than e2
|
||||
require.NoError(t, s.RemoveExpired(e1+1))
|
||||
|
||||
// ascertain that tok1 was removed
|
||||
assertAbsence(id1)
|
||||
|
||||
// ascertain that tok2 was not removed
|
||||
assertPresence(id2)
|
||||
}
|
||||
|
|
207
session/types.go
207
session/types.go
|
@ -1,181 +1,86 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"encoding/binary"
|
||||
"sync"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/chain"
|
||||
"github.com/nspcc-dev/neofs-api-go/internal"
|
||||
"github.com/nspcc-dev/neofs-api-go/refs"
|
||||
crypto "github.com/nspcc-dev/neofs-crypto"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/nspcc-dev/neofs-api-go/service"
|
||||
)
|
||||
|
||||
type (
|
||||
// ObjectID type alias.
|
||||
ObjectID = refs.ObjectID
|
||||
// OwnerID type alias.
|
||||
OwnerID = refs.OwnerID
|
||||
// TokenID type alias.
|
||||
TokenID = refs.UUID
|
||||
// PrivateToken is an interface of session private part.
|
||||
type PrivateToken interface {
|
||||
// PublicKey must return a binary representation of session public key.
|
||||
PublicKey() []byte
|
||||
|
||||
// PToken is a wrapper around Token that allows to sign data
|
||||
// and to do thread-safe manipulations.
|
||||
PToken struct {
|
||||
Token
|
||||
// Sign must return the signature of passed data.
|
||||
//
|
||||
// Resulting signature must be verified by crypto.Verify function
|
||||
// with the session public key.
|
||||
Sign([]byte) ([]byte, error)
|
||||
|
||||
mtx *sync.Mutex
|
||||
PrivateKey *ecdsa.PrivateKey
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrWrongFirstEpoch is raised when passed Token contains wrong first epoch.
|
||||
// First epoch is an epoch since token is valid
|
||||
ErrWrongFirstEpoch = internal.Error("wrong first epoch")
|
||||
|
||||
// ErrWrongLastEpoch is raised when passed Token contains wrong last epoch.
|
||||
// Last epoch is an epoch until token is valid
|
||||
ErrWrongLastEpoch = internal.Error("wrong last epoch")
|
||||
|
||||
// ErrWrongOwner is raised when passed Token contains wrong OwnerID.
|
||||
ErrWrongOwner = internal.Error("wrong owner")
|
||||
|
||||
// ErrEmptyPublicKey is raised when passed Token contains wrong public key.
|
||||
ErrEmptyPublicKey = internal.Error("empty public key")
|
||||
|
||||
// ErrWrongObjectsCount is raised when passed Token contains wrong objects count.
|
||||
ErrWrongObjectsCount = internal.Error("wrong objects count")
|
||||
|
||||
// ErrWrongObjects is raised when passed Token contains wrong object ids.
|
||||
ErrWrongObjects = internal.Error("wrong objects")
|
||||
|
||||
// ErrInvalidSignature is raised when wrong signature is passed to VerificationHeader.VerifyData().
|
||||
ErrInvalidSignature = internal.Error("invalid signature")
|
||||
)
|
||||
|
||||
// verificationData returns byte array to sign.
|
||||
// Note: protobuf serialization is inconsistent as
|
||||
// wire order is unspecified.
|
||||
func (m *Token) verificationData() (data []byte) {
|
||||
var size int
|
||||
if l := len(m.ObjectID); l > 0 {
|
||||
size = m.ObjectID[0].Size()
|
||||
data = make([]byte, 16+l*size)
|
||||
} else {
|
||||
data = make([]byte, 16)
|
||||
}
|
||||
binary.BigEndian.PutUint64(data, m.FirstEpoch)
|
||||
binary.BigEndian.PutUint64(data[8:], m.LastEpoch)
|
||||
for i := range m.ObjectID {
|
||||
copy(data[16+i*size:], m.ObjectID[i].Bytes())
|
||||
}
|
||||
return
|
||||
// Expired must return true if and only if private token is expired in the given epoch number.
|
||||
Expired(uint64) bool
|
||||
}
|
||||
|
||||
// IsSame checks if the passed token is valid and equal to current token
|
||||
func (m *Token) IsSame(t *Token) error {
|
||||
switch {
|
||||
case m.FirstEpoch != t.FirstEpoch:
|
||||
return ErrWrongFirstEpoch
|
||||
case m.LastEpoch != t.LastEpoch:
|
||||
return ErrWrongLastEpoch
|
||||
case !m.OwnerID.Equal(t.OwnerID):
|
||||
return ErrWrongOwner
|
||||
case m.Header.PublicKey == nil:
|
||||
return ErrEmptyPublicKey
|
||||
case len(m.ObjectID) != len(t.ObjectID):
|
||||
return ErrWrongObjectsCount
|
||||
default:
|
||||
for i := range m.ObjectID {
|
||||
if !m.ObjectID[i].Equal(t.ObjectID[i]) {
|
||||
return errors.Wrapf(ErrWrongObjects, "expect %s, actual: %s", m.ObjectID[i], t.ObjectID[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
// PrivateTokenKey is a structure of private token storage key.
|
||||
type PrivateTokenKey struct {
|
||||
owner OwnerID
|
||||
token TokenID
|
||||
}
|
||||
|
||||
// Sign tries to sign current Token data and stores signature inside it.
|
||||
func (m *Token) Sign(key *ecdsa.PrivateKey) error {
|
||||
if err := m.Header.Sign(key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s, err := crypto.Sign(key, m.verificationData())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Signature = s
|
||||
return nil
|
||||
// PrivateTokenSource is an interface of private token storage with read access.
|
||||
type PrivateTokenSource interface {
|
||||
// Fetch must return the storage record corresponding to the passed key.
|
||||
//
|
||||
// Resulting error must be ErrPrivateTokenNotFound if there is no corresponding record.
|
||||
Fetch(PrivateTokenKey) (PrivateToken, error)
|
||||
}
|
||||
|
||||
// SetPublicKeys sets owner's public keys to the token
|
||||
func (m *Token) SetPublicKeys(keys ...*ecdsa.PublicKey) {
|
||||
m.PublicKeys = m.PublicKeys[:0]
|
||||
for i := range keys {
|
||||
m.PublicKeys = append(m.PublicKeys, crypto.MarshalPublicKey(keys[i]))
|
||||
}
|
||||
// EpochLifetimeStore is an interface of the storage of elements that lifetime is limited by NeoFS epoch.
|
||||
type EpochLifetimeStore interface {
|
||||
// RemoveExpired must remove all elements that are expired in the given epoch.
|
||||
RemoveExpired(uint64) error
|
||||
}
|
||||
|
||||
// Verify checks if token is correct and signed.
|
||||
func (m *Token) Verify(keys ...*ecdsa.PublicKey) bool {
|
||||
if m.FirstEpoch > m.LastEpoch {
|
||||
return false
|
||||
}
|
||||
ownerFromKeys := chain.KeysToAddress(keys...)
|
||||
if m.OwnerID.String() != ownerFromKeys {
|
||||
return false
|
||||
}
|
||||
// PrivateTokenStore is an interface of the storage of private tokens addressable by TokenID.
|
||||
type PrivateTokenStore interface {
|
||||
PrivateTokenSource
|
||||
EpochLifetimeStore
|
||||
|
||||
for i := range keys {
|
||||
if m.Header.Verify(keys[i]) && crypto.Verify(keys[i], m.verificationData(), m.Signature) == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
// Store must save passed private token in the storage under the given key.
|
||||
//
|
||||
// Resulting error must be nil if private token was stored successfully.
|
||||
Store(PrivateTokenKey, PrivateToken) error
|
||||
}
|
||||
|
||||
// AddSignatures adds token signatures.
|
||||
func (t *PToken) AddSignatures(signH, signT []byte) {
|
||||
t.mtx.Lock()
|
||||
|
||||
t.Header.KeySignature = signH
|
||||
t.Signature = signT
|
||||
|
||||
t.mtx.Unlock()
|
||||
// KeyStore is an interface of the storage of public keys addressable by OwnerID,
|
||||
type KeyStore interface {
|
||||
// Get must return the storage record corresponding to the passed key.
|
||||
//
|
||||
// Resulting error must be ErrKeyNotFound if there is no corresponding record.
|
||||
Get(context.Context, OwnerID) ([]*ecdsa.PublicKey, error)
|
||||
}
|
||||
|
||||
// SignData signs data with session private key.
|
||||
func (t *PToken) SignData(data []byte) ([]byte, error) {
|
||||
return crypto.Sign(t.PrivateKey, data)
|
||||
// CreateParamsSource is an interface of the container of session parameters with read access.
|
||||
type CreateParamsSource interface {
|
||||
refs.OwnerIDSource
|
||||
service.LifetimeSource
|
||||
}
|
||||
|
||||
// VerifyData checks if signature of data by token is equal to sign.
|
||||
func (m *VerificationHeader) VerifyData(data, sign []byte) error {
|
||||
if crypto.Verify(crypto.UnmarshalPublicKey(m.PublicKey), data, sign) != nil {
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
return nil
|
||||
// CreateParamsContainer is an interface of the container of session parameters.
|
||||
type CreateParamsContainer interface {
|
||||
refs.OwnerIDContainer
|
||||
service.LifetimeContainer
|
||||
}
|
||||
|
||||
// Verify checks if verification header was issued by id.
|
||||
func (m *VerificationHeader) Verify(keys ...*ecdsa.PublicKey) bool {
|
||||
for i := range keys {
|
||||
if crypto.Verify(keys[i], m.PublicKey, m.KeySignature) == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
// CreateResult is an interface of the container of an opened session info with read access.
|
||||
type CreateResult interface {
|
||||
service.TokenIDSource
|
||||
service.SessionKeySource
|
||||
}
|
||||
|
||||
// UnmarshalPublicKeys returns unmarshal public keys from the token
|
||||
func UnmarshalPublicKeys(t *Token) []*ecdsa.PublicKey {
|
||||
r := make([]*ecdsa.PublicKey, 0, len(t.PublicKeys))
|
||||
for i := range t.PublicKeys {
|
||||
r = append(r, crypto.UnmarshalPublicKey(t.PublicKeys[i]))
|
||||
}
|
||||
return r
|
||||
// Creator is an interface of the tool for a session opening.
|
||||
type Creator interface {
|
||||
Create(context.Context, CreateParamsSource) (CreateResult, error)
|
||||
}
|
||||
|
|
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