diff --git a/pkg/services/tree/replicator.go b/pkg/services/tree/replicator.go index 87cced5b4..16399ef91 100644 --- a/pkg/services/tree/replicator.go +++ b/pkg/services/tree/replicator.go @@ -52,16 +52,10 @@ func (s *Service) replicateLoop(ctx context.Context) { func (s *Service) replicate(ctx context.Context, op movePair) error { req := newApplyRequest(&op) - // TODO(@fyrchik): #1328 access control - //err := signature.SignDataWithHandler(s.key, req, func(key, sign []byte) { - // req.Signature = &Signature{ - // Key: key, - // Sign: sign, - // } - //}) - //if err != nil { - // return fmt.Errorf("can't sign data: %w", err) - //} + err := signMessage(req, s.key) + if err != nil { + return fmt.Errorf("can't sign data: %w", err) + } nodes, err := s.getContainerNodes(op.cid) if err != nil { diff --git a/pkg/services/tree/service.go b/pkg/services/tree/service.go index 8cf6236cd..9647e0b87 100644 --- a/pkg/services/tree/service.go +++ b/pkg/services/tree/service.go @@ -6,9 +6,9 @@ import ( "errors" "fmt" - "github.com/nspcc-dev/neofs-api-go/v2/signature" "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/pilorama" cidSDK "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/eacl" "go.uber.org/zap" ) @@ -61,7 +61,7 @@ func (s *Service) Add(_ context.Context, req *AddRequest) (*AddResponse, error) return nil, err } - err := s.verifyClient(req, cid, req.GetSignature().GetKey()) + err := s.verifyClient(req, cid, b.GetBearerToken(), eacl.OperationPut) if err != nil { return nil, err } @@ -91,7 +91,7 @@ func (s *Service) AddByPath(_ context.Context, req *AddByPathRequest) (*AddByPat return nil, err } - err := s.verifyClient(req, cid, req.GetSignature().GetKey()) + err := s.verifyClient(req, cid, b.GetBearerToken(), eacl.OperationPut) if err != nil { return nil, err } @@ -133,7 +133,7 @@ func (s *Service) Remove(_ context.Context, req *RemoveRequest) (*RemoveResponse return nil, err } - err := s.verifyClient(req, cid, req.GetSignature().GetKey()) + err := s.verifyClient(req, cid, b.GetBearerToken(), eacl.OperationPut) if err != nil { return nil, err } @@ -164,7 +164,7 @@ func (s *Service) Move(_ context.Context, req *MoveRequest) (*MoveResponse, erro return nil, err } - err := s.verifyClient(req, cid, req.GetSignature().GetKey()) + err := s.verifyClient(req, cid, b.GetBearerToken(), eacl.OperationPut) if err != nil { return nil, err } @@ -194,6 +194,11 @@ func (s *Service) GetNodeByPath(_ context.Context, req *GetNodeByPathRequest) (* return nil, err } + err := s.verifyClient(req, cid, b.GetBearerToken(), eacl.OperationGet) + if err != nil { + return nil, err + } + attr := b.GetPathAttribute() if len(attr) == 0 { attr = pilorama.AttributeFilename @@ -255,6 +260,11 @@ func (s *Service) GetSubTree(req *GetSubTreeRequest, srv TreeService_GetSubTreeS return err } + err := s.verifyClient(req, cid, b.GetBearerToken(), eacl.OperationGet) + if err != nil { + return err + } + queue := []nodeDepthPair{{[]uint64{b.GetRootId()}, 0}} for len(queue) != 0 { @@ -293,7 +303,7 @@ func (s *Service) GetSubTree(req *GetSubTreeRequest, srv TreeService_GetSubTreeS // Apply locally applies operation from the remote node to the tree. func (s *Service) Apply(_ context.Context, req *ApplyRequest) (*ApplyResponse, error) { - err := signature.VerifyServiceMessage(req) + err := verifyMessage(req) if err != nil { return nil, err } diff --git a/pkg/services/tree/service.pb.go b/pkg/services/tree/service.pb.go index 6a58fd550..50027db5c 100644 Binary files a/pkg/services/tree/service.pb.go and b/pkg/services/tree/service.pb.go differ diff --git a/pkg/services/tree/service.proto b/pkg/services/tree/service.proto index 0da34f652..fa19ff0eb 100644 --- a/pkg/services/tree/service.proto +++ b/pkg/services/tree/service.proto @@ -36,6 +36,7 @@ message AddRequest { string tree_id = 2; uint64 parent_id = 3; repeated KeyValue meta = 4; + bytes bearer_token = 5; } Body body = 1; @@ -59,6 +60,7 @@ message AddByPathRequest { string path_attribute = 3; repeated string path = 4; repeated KeyValue meta = 5; + bytes bearer_token = 6; } Body body = 1; @@ -81,6 +83,7 @@ message RemoveRequest { bytes container_id = 1; string tree_id = 2; uint64 node_id = 3; + bytes bearer_token = 4; } Body body = 1; @@ -104,6 +107,7 @@ message MoveRequest { uint64 parent_id = 3; uint64 node_id = 4; repeated KeyValue meta = 5; + bytes bearer_token = 6; } Body body = 1; @@ -128,6 +132,7 @@ message GetNodeByPathRequest { repeated string attributes = 5; bool latest_only = 6; bool all_attributes = 7; + bytes bearer_token = 8; } Body body = 1; @@ -157,6 +162,7 @@ message GetSubTreeRequest { // Optional depth of the traversal. Zero means return only root. // Maximum depth is 10. uint32 depth = 4; + bytes bearer_token = 5; } Body body = 1; diff --git a/pkg/services/tree/service_neofs.pb.go b/pkg/services/tree/service_neofs.pb.go new file mode 100644 index 000000000..5f86e7583 Binary files /dev/null and b/pkg/services/tree/service_neofs.pb.go differ diff --git a/pkg/services/tree/signature.go b/pkg/services/tree/signature.go index 299f3ad34..bfdd5debb 100644 --- a/pkg/services/tree/signature.go +++ b/pkg/services/tree/signature.go @@ -7,16 +7,26 @@ import ( "fmt" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - "github.com/nspcc-dev/neofs-api-go/v2/signature" + "github.com/nspcc-dev/neofs-api-go/v2/refs" + "github.com/nspcc-dev/neofs-sdk-go/bearer" cidSDK "github.com/nspcc-dev/neofs-sdk-go/container/id" + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" + neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" + "github.com/nspcc-dev/neofs-sdk-go/eacl" "github.com/nspcc-dev/neofs-sdk-go/user" ) -func (s *Service) verifyClient(req interface{}, cid cidSDK.ID, rawKey []byte) error { - // TODO(@fyrchik): #1328 access control - return nil - //nolint:govet - err := signature.VerifyServiceMessage(req) +type message interface { + SignedDataSize() int + ReadSignedData([]byte) ([]byte, error) + GetSignature() *Signature + SetSignature(*Signature) +} + +// verifyClient verifies that the request for a client operation was either signed by owner +// or contains a valid bearer token. +func (s *Service) verifyClient(req message, cid cidSDK.ID, rawBearer []byte, op eacl.Operation) error { + err := verifyMessage(req) if err != nil { return err } @@ -28,17 +38,95 @@ func (s *Service) verifyClient(req interface{}, cid cidSDK.ID, rawKey []byte) er ownerID := cnr.Value.Owner() - pub, err := keys.NewPublicKeyFromBytes(rawKey, elliptic.P256()) + if len(rawBearer) == 0 { // must be signed by the owner + // No error is expected because `VerifyDataWithSource` checks the signature. + // However, we may use different algorithms in the future, thus this check. + pub, err := keys.NewPublicKeyFromBytes(req.GetSignature().GetKey(), elliptic.P256()) + if err != nil { + return fmt.Errorf("invalid public key: %w", err) + } + + var actualID user.ID + user.IDFromKey(&actualID, (ecdsa.PublicKey)(*pub)) + + if !actualID.Equals(ownerID) { + return errors.New("`Move` request must be signed by a container owner") + } + return nil + } + + var bt bearer.Token + if err := bt.Unmarshal(rawBearer); err != nil { + return fmt.Errorf("invalid bearer token: %w", err) + } + if !bearer.ResolveIssuer(bt).Equals(ownerID) { + return errors.New("bearer token must be signed by the container owner") + } + if !bt.AssertContainer(cid) { + return errors.New("bearer token is created for another container") + } + if !bt.VerifySignature() { + return errors.New("invalid bearer token signature") + } + + tb := bt.EACLTable() + + // The default action should be DENY, so we use RoleOthers to allow token issuer + // to restrict everyone not affected by the previous rules. + // This can be simplified after nspcc-dev/neofs-sdk-go#243 . + action, found := eacl.NewValidator().CalculateAction(new(eacl.ValidationUnit). + WithEACLTable(&tb). + WithContainerID(&cid). + WithRole(eacl.RoleOthers). + WithSenderKey(req.GetSignature().GetKey()). + WithOperation(op)) + if !found || action != eacl.ActionAllow { + return errors.New("operation denied by bearer eACL") + } + return nil +} + +func verifyMessage(m message) error { + binBody, err := m.ReadSignedData(nil) if err != nil { - return fmt.Errorf("invalid public key: %w", err) + return fmt.Errorf("marshal request body: %w", err) } - var actualID user.ID - user.IDFromKey(&actualID, (ecdsa.PublicKey)(*pub)) + sig := m.GetSignature() - if !actualID.Equals(ownerID) { - return errors.New("`Move` request must be signed by a container owner") + // TODO(@cthulhu-rider): #1387 use Signature message from NeoFS API to avoid conversion + var sigV2 refs.Signature + sigV2.SetKey(sig.GetKey()) + sigV2.SetSign(sig.GetSign()) + sigV2.SetScheme(refs.ECDSA_SHA512) + + var sigSDK neofscrypto.Signature + sigSDK.ReadFromV2(sigV2) + + if !sigSDK.Verify(binBody) { + return errors.New("invalid signature") } + return nil +} + +func signMessage(m message, key *ecdsa.PrivateKey) error { + binBody, err := m.ReadSignedData(nil) + if err != nil { + return err + } + + keySDK := neofsecdsa.Signer(*key) + data, err := keySDK.Sign(binBody) + if err != nil { + return err + } + + rawPub := make([]byte, keySDK.Public().MaxEncodedSize()) + rawPub = rawPub[:keySDK.Public().Encode(rawPub)] + m.SetSignature(&Signature{ + Key: rawPub, + Sign: data, + }) return nil } diff --git a/pkg/services/tree/signature_test.go b/pkg/services/tree/signature_test.go new file mode 100644 index 000000000..9adfe7864 --- /dev/null +++ b/pkg/services/tree/signature_test.go @@ -0,0 +1,206 @@ +package tree + +import ( + "crypto/ecdsa" + "crypto/sha256" + "errors" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neofs-api-go/v2/acl" + containercore "github.com/nspcc-dev/neofs-node/pkg/core/container" + "github.com/nspcc-dev/neofs-node/pkg/core/netmap" + "github.com/nspcc-dev/neofs-sdk-go/bearer" + "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" + eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" + netmapSDK "github.com/nspcc-dev/neofs-sdk-go/netmap" + "github.com/nspcc-dev/neofs-sdk-go/user" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +type dummyNetmapSource struct { + netmap.Source +} + +type dummyContainerSource map[string]*containercore.Container + +func (s dummyContainerSource) Get(id cid.ID) (*containercore.Container, error) { + cnt, ok := s[id.String()] + if !ok { + return nil, errors.New("container not found") + } + return cnt, nil +} + +func testContainer(owner user.ID) container.Container { + var r netmapSDK.ReplicaDescriptor + r.SetNumberOfObjects(1) + + var pp netmapSDK.PlacementPolicy + pp.AddReplicas(r) + + var cnt container.Container + cnt.SetOwner(owner) + cnt.SetPlacementPolicy(pp) + + return cnt +} + +func TestMessageSign(t *testing.T) { + privs := make([]*keys.PrivateKey, 4) + for i := range privs { + p, err := keys.NewPrivateKey() + require.NoError(t, err) + privs[i] = p + } + + cid1 := cidtest.ID() + cid2 := cidtest.ID() + + var ownerID user.ID + user.IDFromKey(&ownerID, (ecdsa.PublicKey)(*privs[0].PublicKey())) + + s := &Service{ + cfg: cfg{ + log: zaptest.NewLogger(t), + key: &privs[0].PrivateKey, + nmSource: dummyNetmapSource{}, + cnrSource: dummyContainerSource{ + cid1.String(): &containercore.Container{ + Value: testContainer(ownerID), + }, + }, + }, + } + + rawCID1 := make([]byte, sha256.Size) + cid1.Encode(rawCID1) + + req := &MoveRequest{ + Body: &MoveRequest_Body{ + ContainerId: rawCID1, + ParentId: 1, + NodeId: 2, + Meta: []*KeyValue{ + {Key: "kkk", Value: []byte("vvv")}, + }, + }, + } + + t.Run("missing signature, no panic", func(t *testing.T) { + require.Error(t, s.verifyClient(req, cid2, nil, eaclSDK.OperationUnknown)) + }) + + require.NoError(t, signMessage(req, &privs[0].PrivateKey)) + require.NoError(t, s.verifyClient(req, cid1, nil, eaclSDK.OperationUnknown)) + + t.Run("invalid CID", func(t *testing.T) { + require.Error(t, s.verifyClient(req, cid2, nil, eaclSDK.OperationUnknown)) + }) + t.Run("invalid key", func(t *testing.T) { + require.NoError(t, signMessage(req, &privs[1].PrivateKey)) + require.Error(t, s.verifyClient(req, cid1, nil, eaclSDK.OperationUnknown)) + }) + + t.Run("bearer", func(t *testing.T) { + t.Run("invalid bearer", func(t *testing.T) { + req.Body.BearerToken = []byte{0xFF} + require.NoError(t, signMessage(req, &privs[0].PrivateKey)) + require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationPut)) + }) + + t.Run("invalid bearer CID", func(t *testing.T) { + bt := testBearerToken(cid2, privs[1].PublicKey(), privs[2].PublicKey()) + require.NoError(t, bt.Sign(privs[0].PrivateKey)) + req.Body.BearerToken = bt.Marshal() + + require.NoError(t, signMessage(req, &privs[1].PrivateKey)) + require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationPut)) + }) + t.Run("invalid bearer owner", func(t *testing.T) { + bt := testBearerToken(cid1, privs[1].PublicKey(), privs[2].PublicKey()) + require.NoError(t, bt.Sign(privs[1].PrivateKey)) + req.Body.BearerToken = bt.Marshal() + + require.NoError(t, signMessage(req, &privs[1].PrivateKey)) + require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationPut)) + }) + t.Run("invalid bearer signature", func(t *testing.T) { + bt := testBearerToken(cid1, privs[1].PublicKey(), privs[2].PublicKey()) + require.NoError(t, bt.Sign(privs[0].PrivateKey)) + + var bv2 acl.BearerToken + bt.WriteToV2(&bv2) + bv2.GetSignature().SetSign([]byte{1, 2, 3}) + req.Body.BearerToken = bv2.StableMarshal(nil) + + require.NoError(t, signMessage(req, &privs[1].PrivateKey)) + require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationPut)) + }) + + bt := testBearerToken(cid1, privs[1].PublicKey(), privs[2].PublicKey()) + require.NoError(t, bt.Sign(privs[0].PrivateKey)) + req.Body.BearerToken = bt.Marshal() + + t.Run("put and get", func(t *testing.T) { + require.NoError(t, signMessage(req, &privs[1].PrivateKey)) + require.NoError(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationPut)) + require.NoError(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationGet)) + }) + t.Run("only get", func(t *testing.T) { + require.NoError(t, signMessage(req, &privs[2].PrivateKey)) + require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationPut)) + require.NoError(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationGet)) + }) + t.Run("none", func(t *testing.T) { + require.NoError(t, signMessage(req, &privs[3].PrivateKey)) + require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationPut)) + require.Error(t, s.verifyClient(req, cid1, req.GetBody().GetBearerToken(), eaclSDK.OperationGet)) + }) + }) +} + +func testBearerToken(cid cid.ID, forPut, forGet *keys.PublicKey) bearer.Token { + tgtGet := eaclSDK.NewTarget() + tgtGet.SetRole(eaclSDK.RoleUnknown) + tgtGet.SetBinaryKeys([][]byte{forPut.Bytes(), forGet.Bytes()}) + + rGet := eaclSDK.NewRecord() + rGet.SetAction(eaclSDK.ActionAllow) + rGet.SetOperation(eaclSDK.OperationGet) + rGet.SetTargets(*tgtGet) + + tgtPut := eaclSDK.NewTarget() + tgtPut.SetRole(eaclSDK.RoleUnknown) + tgtPut.SetBinaryKeys([][]byte{forPut.Bytes()}) + + rPut := eaclSDK.NewRecord() + rPut.SetAction(eaclSDK.ActionAllow) + rPut.SetOperation(eaclSDK.OperationPut) + rPut.SetTargets(*tgtPut) + + tb := eaclSDK.NewTable() + tb.AddRecord(rGet) + tb.AddRecord(rPut) + + tgt := eaclSDK.NewTarget() + tgt.SetRole(eaclSDK.RoleOthers) + + for _, op := range []eaclSDK.Operation{eaclSDK.OperationGet, eaclSDK.OperationPut} { + r := eaclSDK.NewRecord() + r.SetAction(eaclSDK.ActionDeny) + r.SetTargets(*tgt) + r.SetOperation(op) + tb.AddRecord(r) + } + + tb.SetCID(cid) + + var b bearer.Token + b.SetEACLTable(*tb) + + return b +} diff --git a/pkg/services/tree/types_neofs.pb.go b/pkg/services/tree/types_neofs.pb.go new file mode 100644 index 000000000..e6053d96b Binary files /dev/null and b/pkg/services/tree/types_neofs.pb.go differ