Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Do not allow broken session token by SN #2727

Merged
merged 1 commit into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Changelog for NeoFS Node
- Created files are not group writable (#2589)
- IR does not create new notary requests for the SN's bootstraps but signs the received ones instead (#2717)
- IR can handle third-party notary requests without reliance on receiving the original one (#2715)
- SN validates container session token's issuer to be container's owner (#2466)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not only ownership is validated

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you mean providing a list there? the issue has the exact problem described

Copy link
Contributor

@cthulhu-rider cthulhu-rider Jan 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo issue is just a ref, changelog contains meaningful changes. Full list is an overhead, i'd say

Suggested change
- SN validates container session token's issuer to be container's owner (#2466)
- SN now validates container session tokens (#2466)


### Removed
- Deprecated `neofs-adm [...] inspect` commands (#2603)
Expand Down
87 changes: 87 additions & 0 deletions pkg/services/container/morph/executor.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package container

import (
"bytes"
"context"
"errors"
"fmt"

"github.com/mr-tron/base58"
"github.com/nspcc-dev/neofs-api-go/v2/container"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
sessionV2 "github.com/nspcc-dev/neofs-api-go/v2/session"
"github.com/nspcc-dev/neofs-api-go/v2/util/signature"
containercore "github.com/nspcc-dev/neofs-node/pkg/core/container"
containerSvc "github.com/nspcc-dev/neofs-node/pkg/services/container"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
Expand Down Expand Up @@ -80,6 +83,11 @@ func (s *morphExecutor) Put(_ context.Context, tokV2 *sessionV2.Token, body *con
if err != nil {
return nil, fmt.Errorf("invalid session token: %w", err)
}

err = s.validateToken(tokV2, nil, sessionV2.ContainerVerbPut)
if err != nil {
return nil, fmt.Errorf("session token validation: %w", err)
}
}

idCnr, err := s.wrt.Put(cnr)
Expand Down Expand Up @@ -120,6 +128,11 @@ func (s *morphExecutor) Delete(_ context.Context, tokV2 *sessionV2.Token, body *
if err != nil {
return nil, fmt.Errorf("invalid session token: %w", err)
}

err = s.validateToken(tokV2, body.GetContainerID(), sessionV2.ContainerVerbDelete)
if err != nil {
return nil, fmt.Errorf("session token validation: %w", err)
}
}

var rmWitness containercore.RemovalWitness
Expand Down Expand Up @@ -228,6 +241,11 @@ func (s *morphExecutor) SetExtendedACL(_ context.Context, tokV2 *sessionV2.Token
if err != nil {
return nil, fmt.Errorf("invalid session token: %w", err)
}

err = s.validateToken(tokV2, body.GetEACL().GetContainerID(), sessionV2.ContainerVerbSetEACL)
if err != nil {
return nil, fmt.Errorf("session token validation: %w", err)
}
}

err = s.wrt.PutEACL(eaclInfo)
Expand Down Expand Up @@ -274,3 +292,72 @@ func (s *morphExecutor) GetExtendedACL(_ context.Context, body *container.GetExt

return res, nil
}

type sessionDataSource struct {
t *sessionV2.Token
size int
}

func (d sessionDataSource) ReadSignedData(buff []byte) ([]byte, error) {
if len(buff) < d.size {
buff = make([]byte, d.size)
}

res := d.t.GetBody().StableMarshal(buff)

return res[:d.size], nil
}

func (d sessionDataSource) SignedDataSize() int {
return d.size
}

func newDataSource(t *sessionV2.Token) sessionDataSource {
return sessionDataSource{
t: t,
size: t.GetBody().StableSize(),
}
}

func (s *morphExecutor) validateToken(t *sessionV2.Token, cIDV2 *refs.ContainerID, op sessionV2.ContainerSessionVerb) error {
c := t.GetBody().GetContext()
cc, ok := c.(*sessionV2.ContainerSessionContext)
if !ok {
return errors.New("session is not container-related")
}

if verb := cc.Verb(); verb != op {
return fmt.Errorf("wrong container session operation: %s", verb)
cthulhu-rider marked this conversation as resolved.
Show resolved Hide resolved
}

err := signature.VerifyDataWithSource(newDataSource(t), t.GetSignature)
if err != nil {
return fmt.Errorf("incorrect token signature: %w", err)
}

if cIDV2 == nil { // can be nil for PUT or wildcard may be true
return nil
}

if sessionCID := cc.ContainerID().GetValue(); !bytes.Equal(sessionCID, cIDV2.GetValue()) {
return fmt.Errorf("wrong container: %s", base58.Encode(sessionCID))
}

var cID cid.ID

cthulhu-rider marked this conversation as resolved.
Show resolved Hide resolved
err = cID.ReadFromV2(*cIDV2)
if err != nil {
return fmt.Errorf("invalid container ID: %w", err)
}

cnr, err := s.rdr.Get(cID)
if err != nil {
return fmt.Errorf("reading container from the network: %w", err)
}

if issuer := t.GetBody().GetOwnerID().GetValue(); !bytes.Equal(cnr.Value.Owner().WalletBytes(), issuer) {
return fmt.Errorf("session was not issued by the container owner, issuer: %q", issuer)
}

return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are not all fields checked?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what else fields to check? that is not IR validation before we try to change smth, just a logical validation that you are not trying to make totally incorrect things (container owner do not know about your operation but you still trying it). did not want to copy IR behavior

epoch can not be ensured: the final check will be done on the IR side, no one knows at what state and when

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are other stateless conditions like lifetime field presence, signature correctness, etc. And some stateful conditions are consistent, e.g. expiraiton claim

Copy link
Member Author

@carpawell carpawell Jan 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are other stateless conditions like lifetime field presence

i would say it is more like a protocol validation (if we are talking about the presence, and epoch values i wouldn't check by the intermediate SN ever, as I've already said)

signature correctness

well... ok, done but not valuable to me

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well... ok, done but not valuable to me

words of some undefined behavior...

}
190 changes: 182 additions & 8 deletions pkg/services/container/morph/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package container_test

import (
"context"
"crypto/sha256"
"testing"

"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
Expand All @@ -11,17 +12,32 @@ import (
containerCore "github.com/nspcc-dev/neofs-node/pkg/core/container"
containerSvc "github.com/nspcc-dev/neofs-node/pkg/services/container"
containerSvcMorph "github.com/nspcc-dev/neofs-node/pkg/services/container/morph"
containerSDK "github.com/nspcc-dev/neofs-sdk-go/container"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
containertest "github.com/nspcc-dev/neofs-sdk-go/container/test"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
sessionsdk "github.com/nspcc-dev/neofs-sdk-go/session"
sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/test"
"github.com/nspcc-dev/neofs-sdk-go/user"
usertest "github.com/nspcc-dev/neofs-sdk-go/user/test"
"github.com/stretchr/testify/require"
)

type mock struct {
containerSvcMorph.Reader
cnr containerSDK.Container
}

func (m mock) Get(_ cid.ID) (*containerCore.Container, error) {
return &containerCore.Container{Value: m.cnr}, nil
}

func (m mock) GetEACL(id cid.ID) (*containerCore.EACL, error) {
return nil, nil
}

func (m mock) List(id *user.ID) ([]cid.ID, error) {
return nil, nil
}

func (m mock) Put(_ containerCore.Container) (*cid.ID, error) {
Expand All @@ -36,10 +52,7 @@ func (m mock) PutEACL(_ containerCore.EACL) error {
return nil
}

func TestInvalidToken(t *testing.T) {
m := mock{}
e := containerSvcMorph.NewExecutor(m, m)

func TestExecutor(t *testing.T) {
cnr := cidtest.ID()

var cnrV2 refs.ContainerID
Expand All @@ -62,8 +75,18 @@ func TestInvalidToken(t *testing.T) {
reqBody.SetSignature(&sigV2)
}

tok := sessiontest.Container()
tok.ApplyOnlyTo(cnr)
require.NoError(t, tok.Sign(signer))

var tokV2 session.Token
sessiontest.ContainerSigned(signer).WriteToV2(&tokV2)
tok.WriteToV2(&tokV2)

realContainer := containertest.Container(t)
realContainer.SetOwner(tok.Issuer())

m := mock{cnr: realContainer}
e := containerSvcMorph.NewExecutor(m, m)

tests := []struct {
name string
Expand Down Expand Up @@ -91,6 +114,13 @@ func TestInvalidToken(t *testing.T) {
op: func(e containerSvc.ServiceExecutor, tokV2 *session.Token) (err error) {
var reqBody container.DeleteRequestBody
reqBody.SetContainerID(&cnrV2)
sign(&reqBody)

cc, ok := tokV2.GetBody().GetContext().(*session.ContainerSessionContext)
if ok {
cc.SetVerb(session.ContainerVerbDelete)
signV2Token(t, signer, tokV2)
}

_, err = e.Delete(context.TODO(), tokV2, &reqBody)
return
Expand All @@ -103,6 +133,12 @@ func TestInvalidToken(t *testing.T) {
reqBody.SetSignature(new(refs.Signature))
sign(&reqBody)

cc, ok := tokV2.GetBody().GetContext().(*session.ContainerSessionContext)
if ok {
cc.SetVerb(session.ContainerVerbSetEACL)
signV2Token(t, signer, tokV2)
}

_, err = e.SetExtendedACL(context.TODO(), tokV2, &reqBody)
return
},
Expand All @@ -111,7 +147,7 @@ func TestInvalidToken(t *testing.T) {

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
tok := generateToken(new(session.ObjectSessionContext))
tok := generateToken(t, new(session.ObjectSessionContext), signer)
require.Error(t, test.op(e, tok))

require.NoError(t, test.op(e, &tokV2))
Expand All @@ -121,12 +157,150 @@ func TestInvalidToken(t *testing.T) {
}
}

func generateToken(ctx session.TokenContext) *session.Token {
func TestValidateToken(t *testing.T) {
cID := cidtest.ID()
var cIDV2 refs.ContainerID
cID.WriteToV2(&cIDV2)

priv, err := keys.NewPrivateKey()
require.NoError(t, err)

signer := user.NewAutoIDSigner(priv.PrivateKey)

tok := sessiontest.Container()
tok.ApplyOnlyTo(cID)
tok.ForVerb(sessionsdk.VerbContainerDelete)
require.NoError(t, tok.Sign(signer))

cnr := containertest.Container(t)
cnr.SetOwner(tok.Issuer())

var cnrV2 container.Container
cnr.WriteToV2(&cnrV2)

m := mock{cnr: cnr}
e := containerSvcMorph.NewExecutor(m, m)

t.Run("non-container token", func(t *testing.T) {
var reqBody container.DeleteRequestBody
reqBody.SetContainerID(&cIDV2)

var tokV2 session.Token
objectSession := sessiontest.Object()
require.NoError(t, objectSession.Sign(signer))

objectSession.WriteToV2(&tokV2)

_, err = e.Delete(context.TODO(), &tokV2, &reqBody)
require.Error(t, err)

return
})

t.Run("wrong verb token", func(t *testing.T) {
var reqBody container.DeleteRequestBody
reqBody.SetContainerID(&cIDV2)

var tokCopy sessionsdk.Container
tok.CopyTo(&tokCopy)
tokCopy.ForVerb(sessionsdk.VerbContainerPut)

var tokV2 session.Token
tokCopy.WriteToV2(&tokV2)

_, err = e.Delete(context.TODO(), &tokV2, &reqBody)
require.Error(t, err)

return
})

t.Run("incorrect cID", func(t *testing.T) {
var reqBody container.DeleteRequestBody
reqBody.SetContainerID(&cIDV2)

var tokV2 session.Token
var cIDV2 refs.ContainerID
cc := new(session.ContainerSessionContext)
b := new(session.TokenBody)

cIDV2.SetValue(make([]byte, sha256.Size+1))
cc.SetContainerID(&cIDV2)
b.SetContext(cc)
tokV2.SetBody(b)

_, err = e.Delete(context.TODO(), &tokV2, &reqBody)
require.Error(t, err)
})

t.Run("different container ID", func(t *testing.T) {
var reqBody container.DeleteRequestBody
reqBody.SetContainerID(&cIDV2)

var tokCopy sessionsdk.Container
tok.CopyTo(&tokCopy)
tokCopy.ApplyOnlyTo(cidtest.ID())

require.NoError(t, tokCopy.Sign(signer))

var tokV2 session.Token
tokCopy.WriteToV2(&tokV2)

_, err = e.Delete(context.TODO(), &tokV2, &reqBody)
require.Error(t, err)
})

t.Run("different issuer", func(t *testing.T) {
var reqBody container.DeleteRequestBody
reqBody.SetContainerID(&cIDV2)

var tokV2 session.Token
tok.WriteToV2(&tokV2)

var ownerV2Wrong refs.OwnerID
ownerWrong := usertest.ID(t)
ownerWrong.WriteToV2(&ownerV2Wrong)

tokV2.GetBody().SetOwnerID(&ownerV2Wrong)

_, err = e.Delete(context.TODO(), &tokV2, &reqBody)
require.Error(t, err)
})

t.Run("incorrect signature", func(t *testing.T) {
var reqBody container.DeleteRequestBody
reqBody.SetContainerID(&cIDV2)

var tokV2 session.Token
tok.WriteToV2(&tokV2)

var wrongSig refs.Signature
tokV2.SetSignature(&wrongSig)

_, err = e.Delete(context.TODO(), &tokV2, &reqBody)
require.Error(t, err)
})
}

func generateToken(t *testing.T, ctx session.TokenContext, signer user.Signer) *session.Token {
body := new(session.TokenBody)
body.SetContext(ctx)

tok := new(session.Token)
tok.SetBody(body)

signV2Token(t, signer, tok)

return tok
}

func signV2Token(t *testing.T, signer user.Signer, tokV2 *session.Token) {
sig, err := signer.Sign(tokV2.GetBody().StableMarshal(nil))
require.NoError(t, err)

var sigV2 refs.Signature
sigV2.SetKey(neofscrypto.PublicKeyBytes(signer.Public()))
sigV2.SetScheme(refs.SignatureScheme(signer.Scheme()))
sigV2.SetSign(sig)

tokV2.SetSignature(&sigV2)
}
Loading