diff --git a/service/token.go b/service/token.go index 71ea655..077e672 100644 --- a/service/token.go +++ b/service/token.go @@ -1,7 +1,12 @@ package service import ( + "crypto/ecdsa" + "encoding/binary" + + "github.com/nspcc-dev/neofs-api-go/internal" "github.com/nspcc-dev/neofs-api-go/refs" + crypto "github.com/nspcc-dev/neofs-crypto" ) // VerbContainer is an interface of the container of a token verb value. @@ -57,8 +62,13 @@ type SessionToken interface { SignatureContainer } +// ErrEmptyToken is raised when passed Token is nil. +const ErrEmptyToken = internal.Error("token is empty") + var _ SessionToken = (*Token)(nil) +var tokenEndianness = binary.BigEndian + // GetID is an ID field getter. func (m Token_Info) GetID() TokenID { return m.ID @@ -123,3 +133,89 @@ func (m *Token_Info) SetSessionKey(key []byte) { func (m *Token) SetSignature(sig []byte) { m.Signature = sig } + +// Returns byte slice that is used for creation/verification of the token signature. +func verificationTokenData(token SessionToken) []byte { + var sz int + + id := token.GetID() + sz += id.Size() + + ownerID := token.GetOwnerID() + sz += ownerID.Size() + + verb := uint32(token.GetVerb()) + sz += 4 + + addr := token.GetAddress() + sz += addr.CID.Size() + addr.ObjectID.Size() + + cEpoch := token.CreationEpoch() + sz += 8 + + fEpoch := token.ExpirationEpoch() + sz += 8 + + key := token.GetSessionKey() + sz += len(key) + + data := make([]byte, sz) + + var off int + + tokenEndianness.PutUint32(data, verb) + off += 4 + + tokenEndianness.PutUint64(data[off:], cEpoch) + off += 8 + + tokenEndianness.PutUint64(data[off:], fEpoch) + off += 8 + + off += copy(data[off:], id.Bytes()) + off += copy(data[off:], ownerID.Bytes()) + off += copy(data[off:], addr.CID.Bytes()) + off += copy(data[off:], addr.ObjectID.Bytes()) + off += copy(data[off:], key) + + return data +} + +// SignToken calculates and stores the signature of token information. +// +// If passed token is nil, ErrEmptyToken returns. +// If passed private key is nil, crypto.ErrEmptyPrivateKey returns. +func SignToken(token SessionToken, key *ecdsa.PrivateKey) error { + if token == nil { + return ErrEmptyToken + } else if key == nil { + return crypto.ErrEmptyPrivateKey + } + + sig, err := crypto.Sign(key, verificationTokenData(token)) + if err != nil { + return err + } + + token.SetSignature(sig) + + return nil +} + +// VerifyTokenSignature checks if token was signed correctly. +// +// If passed token is nil, ErrEmptyToken returns. +// If passed public key is nil, crypto.ErrEmptyPublicKey returns. +func VerifyTokenSignature(token SessionToken, key *ecdsa.PublicKey) error { + if token == nil { + return ErrEmptyToken + } else if key == nil { + return crypto.ErrEmptyPublicKey + } + + return crypto.Verify( + key, + verificationTokenData(token), + token.GetSignature(), + ) +} diff --git a/service/token_test.go b/service/token_test.go index 1a55406..0b28084 100644 --- a/service/token_test.go +++ b/service/token_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/nspcc-dev/neofs-api-go/refs" + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/nspcc-dev/neofs-crypto/test" "github.com/stretchr/testify/require" ) @@ -86,3 +88,155 @@ func TestTokenGettersSetters(t *testing.T) { require.Equal(t, sig, tok.GetSignature()) } } + +func TestSignToken(t *testing.T) { + // nil token + require.EqualError(t, + SignToken(nil, nil), + ErrEmptyToken.Error(), + ) + + require.EqualError(t, + VerifyTokenSignature(nil, nil), + ErrEmptyToken.Error(), + ) + + var token SessionToken = new(Token) + + // nil key + require.EqualError(t, + SignToken(token, nil), + crypto.ErrEmptyPrivateKey.Error(), + ) + + require.EqualError(t, + VerifyTokenSignature(token, nil), + crypto.ErrEmptyPublicKey.Error(), + ) + + // 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, SignToken(token, sk)) + require.NoError(t, VerifyTokenSignature(token, pk)) + + 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, VerifyTokenSignature(token, pk)) + v.restore() + require.NoError(t, VerifyTokenSignature(token, pk)) + } +}