Merge branch 'release/0.7.4'

This commit is contained in:
Leonard Lyubich 2020-05-12 11:08:25 +03:00
commit 841f261e09
80 changed files with 4903 additions and 1155 deletions

View file

@ -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

View file

@ -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
View 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
View 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))
}
}
}
}

View file

@ -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
}

View file

@ -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
View 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
View 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))
}
}
}
}

View file

@ -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
View 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
View 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
View 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))
}
}
}
}

View file

@ -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
}

View file

@ -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())
})
}

View file

@ -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 |

View file

@ -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 -->

View file

@ -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 -->

View file

@ -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

View file

@ -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.

View file

@ -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)

View file

@ -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
View 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
View 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))
}
}
}
}

View file

@ -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.

View file

@ -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
View 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)
})
}

View file

@ -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,
}},
}
}

View file

@ -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

View file

@ -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)

View file

@ -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
View 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
View 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
View 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
View 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")

View file

@ -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.

View file

@ -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

View file

@ -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
View 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
View 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())
}
}

View file

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}

View file

@ -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.

View file

@ -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;
// }

View file

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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())
})
}

View file

@ -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.

View file

@ -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;
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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.

View file

@ -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
View 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
View 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
View 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
View 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())
})
}