diff --git a/cmd/neofs-node/netmap.go b/cmd/neofs-node/netmap.go index 575b88e76f..856dc82bee 100644 --- a/cmd/neofs-node/netmap.go +++ b/cmd/neofs-node/netmap.go @@ -388,7 +388,7 @@ type netInfo struct { netState netmap.State magic interface { - MagicNumber() (uint64, error) + MagicNumber() (uint32, error) } morphClientNetMap *nmClient.Client @@ -404,7 +404,7 @@ func (n *netInfo) Dump(ver version.Version) (*netmapSDK.NetworkInfo, error) { var ni netmapSDK.NetworkInfo ni.SetCurrentEpoch(n.netState.CurrentEpoch()) - ni.SetMagicNumber(magic) + ni.SetMagicNumber(uint64(magic)) netInfoMorph, err := n.morphClientNetMap.ReadNetworkConfiguration() if err != nil { diff --git a/cmd/neofs-node/object.go b/cmd/neofs-node/object.go index 701d23b77c..4fd8b12790 100644 --- a/cmd/neofs-node/object.go +++ b/cmd/neofs-node/object.go @@ -38,6 +38,7 @@ import ( truststorage "github.com/nspcc-dev/neofs-node/pkg/services/reputation/local/storage" "github.com/nspcc-dev/neofs-sdk-go/client" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" eaclSDK "github.com/nspcc-dev/neofs-sdk-go/eacl" netmapsdk "github.com/nspcc-dev/neofs-sdk-go/netmap" objectSDK "github.com/nspcc-dev/neofs-sdk-go/object" @@ -256,7 +257,11 @@ func initObjectService(c *cfg) { searchsvcV2.WithKeyStorage(keyStorage), ) + mNumber, err := c.shared.basics.cli.MagicNumber() + fatalOnErr(err) + sPut := putsvc.NewService(&transport{clients: putConstructor}, c, + putsvc.WithNetworkMagic(mNumber), putsvc.WithKeyStorage(keyStorage), putsvc.WithClientConstructor(putConstructor), putsvc.WithMaxSizeSource(newCachedMaxObjectSizeSource(c)), @@ -350,7 +355,7 @@ func initObjectService(c *cfg) { firstSvc = objectService.NewMetricCollector(signSvc, c.metricsCollector) } - server := objectTransportGRPC.New(firstSvc, objNode) + server := objectTransportGRPC.New(firstSvc, mNumber, objNode, neofsecdsa.SignerRFC6979(c.shared.basics.key.PrivateKey)) for _, srv := range c.cfgGRPC.servers { objectGRPC.RegisterObjectServiceServer(srv, server) diff --git a/cmd/neofs-node/transport.go b/cmd/neofs-node/transport.go index 33a5dc56e4..156be9a54c 100644 --- a/cmd/neofs-node/transport.go +++ b/cmd/neofs-node/transport.go @@ -5,6 +5,7 @@ import ( "fmt" objectGRPC "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" + "github.com/nspcc-dev/neofs-api-go/v2/refs" rawclient "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" "github.com/nspcc-dev/neofs-api-go/v2/rpc/common" "github.com/nspcc-dev/neofs-api-go/v2/rpc/grpc" @@ -12,6 +13,7 @@ import ( "github.com/nspcc-dev/neofs-api-go/v2/status" coreclient "github.com/nspcc-dev/neofs-node/pkg/core/client" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" ) type transport struct { @@ -20,17 +22,17 @@ type transport struct { // SendReplicationRequestToNode connects to described node and sends prepared // replication request message to it. -func (x *transport) SendReplicationRequestToNode(ctx context.Context, req []byte, node coreclient.NodeInfo) error { +func (x *transport) SendReplicationRequestToNode(ctx context.Context, req []byte, node coreclient.NodeInfo) (*neofscrypto.Signature, error) { c, err := x.clients.Get(node) if err != nil { - return fmt.Errorf("connect to remote node: %w", err) + return nil, fmt.Errorf("connect to remote node: %w", err) } - return c.ExecRaw(func(c *rawclient.Client) error { + var resp replicateResponse + err = c.ExecRaw(func(c *rawclient.Client) error { // this will be changed during NeoFS API Go deprecation. Code most likely be // placed in SDK m := common.CallMethodInfo{Service: "neo.fs.v2.object.ObjectService", Name: "Replicate"} - var resp replicateResponse err = rawclient.SendUnary(c, m, rawclient.BinaryMessage(req), &resp, rawclient.WithContext(ctx), rawclient.AllowBinarySendingOnly()) if err != nil { @@ -38,9 +40,13 @@ func (x *transport) SendReplicationRequestToNode(ctx context.Context, req []byte } return resp.err }) + return resp.sig, err } -type replicateResponse struct{ err error } +type replicateResponse struct { + sig *neofscrypto.Signature + err error +} func (x replicateResponse) ToGRPCMessage() grpc.Message { return new(objectGRPC.ReplicateResponse) } @@ -60,6 +66,26 @@ func (x *replicateResponse) FromGRPCMessage(gm grpc.Message) error { } x.err = apistatus.ErrorFromV2(st) + if x.err != nil { + return nil + } + + sig := m.GetObjectSignature() + if sig == nil { + return nil + } + + sigV2 := new(refs.Signature) + err := sigV2.Unmarshal(sig) + if err != nil { + return fmt.Errorf("decoding signature from proto message: %w", err) + } + + x.sig = new(neofscrypto.Signature) + err = x.sig.ReadFromV2(*sigV2) + if err != nil { + return fmt.Errorf("invalid signature: %w", err) + } return nil } diff --git a/go.mod b/go.mod index cf90a80197..1ff2d619e9 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/nspcc-dev/hrw/v2 v2.0.1 github.com/nspcc-dev/locode-db v0.6.0 github.com/nspcc-dev/neo-go v0.106.3 - github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240305074711-35bc78d84dc4 + github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea github.com/nspcc-dev/neofs-contract v0.20.0 github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.12.0.20240809202351-256513c1b29b github.com/nspcc-dev/tzhash v1.8.0 diff --git a/go.sum b/go.sum index 7b438741a8..1d4b808ed4 100644 --- a/go.sum +++ b/go.sum @@ -139,8 +139,8 @@ github.com/nspcc-dev/neo-go v0.106.3 h1:HEyhgkjQY+HfBzotMJ12xx2VuOUphkngZ4kEkjvX github.com/nspcc-dev/neo-go v0.106.3/go.mod h1:3vEwJ2ld12N7HRGCaH/l/7EwopplC/+8XdIdPDNmD/M= github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20240729160116-d8e3e57f88f2 h1:tvPkeqnIeBFhM1b1Iwwi0jJiuoxkY4Xbk8mP3W1YVUY= github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20240729160116-d8e3e57f88f2/go.mod h1:/vrbWSHc7YS1KSYhVOyyeucXW/e+1DkVBOgnBEXUCeY= -github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240305074711-35bc78d84dc4 h1:arN0Ypn+jawZpu1BND7TGRn44InAVIqKygndsx0y2no= -github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240305074711-35bc78d84dc4/go.mod h1:7Tm1NKEoUVVIUlkVwFrPh7GG5+Lmta2m7EGr4oVpBd8= +github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea h1:mK0EMGLvunXcFyq7fBURS/CsN4MH+4nlYiqn6pTwWAU= +github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea/go.mod h1:YzhD4EZmC9Z/PNyd7ysC7WXgIgURc9uCG1UWDeV027Y= github.com/nspcc-dev/neofs-contract v0.20.0 h1:ARE/3mSN+P9qi/10NBsf7QyPiYrvnxeEgYUN13vHRlo= github.com/nspcc-dev/neofs-contract v0.20.0/go.mod h1:YxtKYE/5cMNiqwWcQWzeizbB9jizauLni+p8wXxfhsQ= github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.12.0.20240809202351-256513c1b29b h1:/7jXQP5pf+M0kRFC1gg5GEdTPkvotpMHxjSXIbMZaGQ= diff --git a/pkg/core/object/replicate.go b/pkg/core/object/replicate.go new file mode 100644 index 0000000000..398d8d5b54 --- /dev/null +++ b/pkg/core/object/replicate.go @@ -0,0 +1,77 @@ +package object + +import ( + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" +) + +const ( + validInterval = 10 // in epochs + currentVersion = 7 // it is also a number of fields +) + +const ( + networkMagicKey = "network" + cidKey = "cid" + oidKey = "oid" + sizeKey = "size" + deletedKey = "deleted" + lockedKey = "locked" + validUntilKey = "validuntil" +) + +// EncodeReplicationMetaInfo uses NEO's map (strict order) serialized format as a raw +// representation of object's meta information. +// +// This (ordered) format is used (keys are strings): +// +// "network": network magic +// "cid": _raw_ container ID (32 bytes) +// "oid": _raw_ object ID (32 bytes) +// "size": payload size +// "deleted": array of _raw_ object IDs +// "locked": array of _raw_ object IDs +// "validuntil": last valid epoch number for meta information +// +// Last valid epoch is object's creation epoch + 10. +func EncodeReplicationMetaInfo(cID cid.ID, oID oid.ID, pSize uint64, + deleted, locked []oid.ID, createdAt uint64, magicNumber uint32) []byte { + kvs := []stackitem.MapElement{ + kv(networkMagicKey, magicNumber), + kv(cidKey, cID[:]), + kv(oidKey, oID[:]), + kv(sizeKey, pSize), + oidsKV(deletedKey, deleted), + oidsKV(lockedKey, locked), + kv(validUntilKey, createdAt+validInterval), + } + + result, err := stackitem.Serialize(stackitem.NewMapWithValue(kvs)) + if err != nil { + // all the errors in the stackitem relate only cases when it is + // impossible to use serialized values (too many values, unsupported + // types, etc.), unexpected errors at all + panic(fmt.Errorf("unexpected stackitem map serialization failure: %v", err)) + } + + return result +} + +func kv(k string, value any) stackitem.MapElement { + return stackitem.MapElement{ + Key: stackitem.Make(k), + Value: stackitem.Make(value), + } +} + +func oidsKV(fieldKey string, oIDs []oid.ID) stackitem.MapElement { + res := make([]stackitem.Item, 0, len(oIDs)) + for _, oID := range oIDs { + res = append(res, stackitem.NewByteArray(oID[:])) + } + + return kv(fieldKey, res) +} diff --git a/pkg/core/object/replicate_test.go b/pkg/core/object/replicate_test.go new file mode 100644 index 0000000000..3f14669163 --- /dev/null +++ b/pkg/core/object/replicate_test.go @@ -0,0 +1,69 @@ +package object + +import ( + "math/big" + "math/rand/v2" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test" + "github.com/stretchr/testify/require" +) + +func TestMetaInfo(t *testing.T) { + network := rand.Uint32() + oID := oidtest.ID() + cID := cidtest.ID() + size := rand.Uint64() + deleted := oidtest.IDs(10) + locked := oidtest.IDs(10) + validUntil := rand.Uint64() + + raw := EncodeReplicationMetaInfo(cID, oID, size, deleted, locked, validUntil, network) + item, err := stackitem.Deserialize(raw) + require.NoError(t, err) + + require.Equal(t, stackitem.MapT, item.Type()) + mm, ok := item.Value().([]stackitem.MapElement) + require.True(t, ok) + + require.Len(t, mm, currentVersion) + + require.Equal(t, networkMagicKey, string(mm[0].Key.Value().([]byte))) + require.Equal(t, network, uint32(mm[0].Value.Value().(*big.Int).Uint64())) + + require.Equal(t, cidKey, string(mm[1].Key.Value().([]byte))) + require.Equal(t, cID[:], mm[1].Value.Value().([]byte)) + + require.Equal(t, oidKey, string(mm[2].Key.Value().([]byte))) + require.Equal(t, oID[:], mm[2].Value.Value().([]byte)) + + require.Equal(t, sizeKey, string(mm[3].Key.Value().([]byte))) + require.Equal(t, size, mm[3].Value.Value().(*big.Int).Uint64()) + + require.Equal(t, deletedKey, string(mm[4].Key.Value().([]byte))) + require.Equal(t, deleted, stackItemToOIDs(t, mm[4].Value)) + + require.Equal(t, lockedKey, string(mm[5].Key.Value().([]byte))) + require.Equal(t, locked, stackItemToOIDs(t, mm[5].Value)) + + require.Equal(t, validUntilKey, string(mm[6].Key.Value().([]byte))) + require.Equal(t, validUntil+validInterval, mm[6].Value.Value().(*big.Int).Uint64()) +} + +func stackItemToOIDs(t *testing.T, value stackitem.Item) []oid.ID { + value, ok := value.(*stackitem.Array) + require.True(t, ok) + + vv := value.Value().([]stackitem.Item) + res := make([]oid.ID, 0, len(vv)) + + for _, v := range vv { + raw := v.Value().([]byte) + res = append(res, oid.ID(raw)) + } + + return res +} diff --git a/pkg/morph/client/client.go b/pkg/morph/client/client.go index 486b16f09e..e9c4bf8cfe 100644 --- a/pkg/morph/client/client.go +++ b/pkg/morph/client/client.go @@ -533,7 +533,7 @@ func (c *Client) roleList(r noderoles.Role) (keys.PublicKeys, error) { // MagicNumber returns the magic number of the network // to which the underlying RPC node client is connected. -func (c *Client) MagicNumber() (uint64, error) { +func (c *Client) MagicNumber() (uint32, error) { c.switchLock.RLock() defer c.switchLock.RUnlock() @@ -541,7 +541,7 @@ func (c *Client) MagicNumber() (uint64, error) { return 0, ErrConnectionLost } - return uint64(c.rpcActor.GetNetwork()), nil + return uint32(c.rpcActor.GetNetwork()), nil } // BlockCount returns block count of the network diff --git a/pkg/network/transport/object/grpc/replication.go b/pkg/network/transport/object/grpc/replication.go index 0945acf1e0..e73f89c1ba 100644 --- a/pkg/network/transport/object/grpc/replication.go +++ b/pkg/network/transport/object/grpc/replication.go @@ -11,11 +11,13 @@ import ( refsv2 "github.com/nspcc-dev/neofs-api-go/v2/refs" refs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" status "github.com/nspcc-dev/neofs-api-go/v2/status/grpc" + objectcore "github.com/nspcc-dev/neofs-node/pkg/core/object" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" cid "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/object" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" ) // Replicate serves neo.fs.v2.object.ObjectService/Replicate RPC. @@ -178,7 +180,18 @@ func (s *Server) Replicate(_ context.Context, req *objectGRPC.ReplicateRequest) }}, nil } - return new(objectGRPC.ReplicateResponse), nil + resp := new(objectGRPC.ReplicateResponse) + if req.GetSignObject() { + resp.ObjectSignature, err = s.metaInfoSignature(*obj) + if err != nil { + return &objectGRPC.ReplicateResponse{Status: &status.Status{ + Code: codeInternal, + Message: fmt.Sprintf("failed to sign object meta information: %v", err), + }}, nil + } + } + + return resp, nil } func objectFromMessage(gMsg *objectGRPC.Object) (*object.Object, error) { @@ -190,3 +203,42 @@ func objectFromMessage(gMsg *objectGRPC.Object) (*object.Object, error) { return object.NewFromV2(&msg), nil } + +func (s *Server) metaInfoSignature(o object.Object) ([]byte, error) { + var deleted []oid.ID + var locked []oid.ID + switch o.Type() { + case object.TypeTombstone: + var t object.Tombstone + err := t.Unmarshal(o.Payload()) + if err != nil { + return nil, fmt.Errorf("reading tombstoned objects: %w", err) + } + + deleted = t.Members() + case object.TypeLock: + var l object.Lock + err := l.Unmarshal(o.Payload()) + if err != nil { + return nil, fmt.Errorf("reading locked objects: %w", err) + } + + locked = make([]oid.ID, l.NumberOfMembers()) + l.ReadMembers(locked) + default: + } + + metaInfo := objectcore.EncodeReplicationMetaInfo(o.GetContainerID(), o.GetID(), o.PayloadSize(), deleted, locked, + o.CreationEpoch(), s.mNumber) + + var sig neofscrypto.Signature + err := sig.Calculate(s.signer, metaInfo) + if err != nil { + return nil, fmt.Errorf("signature failure: %w", err) + } + + sigV2 := new(refsv2.Signature) + sig.WriteToV2(sigV2) + + return sigV2.StableMarshal(nil), nil +} diff --git a/pkg/network/transport/object/grpc/replication_test.go b/pkg/network/transport/object/grpc/replication_test.go index e48e34b72f..a8d4490517 100644 --- a/pkg/network/transport/object/grpc/replication_test.go +++ b/pkg/network/transport/object/grpc/replication_test.go @@ -11,7 +11,9 @@ import ( objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" objectgrpc "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" + refsv2 "github.com/nspcc-dev/neofs-api-go/v2/refs" refs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" + objectcore "github.com/nspcc-dev/neofs-node/pkg/core/object" . "github.com/nspcc-dev/neofs-node/pkg/network/transport/object/grpc" objectSvc "github.com/nspcc-dev/neofs-node/pkg/services/object" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" @@ -127,8 +129,9 @@ func (x *testNode) VerifyAndStoreObject(obj object.Object) error { return x.storeErr } -func anyValidRequest(tb testing.TB, signer neofscrypto.Signer, cnr cid.ID, objID oid.ID) *objectgrpc.ReplicateRequest { +func anyValidRequest(tb testing.TB, signer neofscrypto.Signer, cnr cid.ID, objID oid.ID) (*objectgrpc.ReplicateRequest, object.Object) { obj := objecttest.Object() + obj.SetType(object.TypeRegular) obj.SetContainerID(cnr) obj.SetID(objID) @@ -141,6 +144,7 @@ func anyValidRequest(tb testing.TB, signer neofscrypto.Signer, cnr cid.ID, objID Key: neofscrypto.PublicKeyBytes(signer.Public()), Sign: sig, }, + SignObject: false, } switch signer.Scheme() { @@ -154,19 +158,19 @@ func anyValidRequest(tb testing.TB, signer neofscrypto.Signer, cnr cid.ID, objID req.Signature.Scheme = refs.SignatureScheme_ECDSA_RFC6979_SHA256_WALLET_CONNECT } - return req + return req, obj } func TestServer_Replicate(t *testing.T) { var noCallNode noCallTestNode var noCallObjSvc noCallObjectService - noCallSrv := New(noCallObjSvc, &noCallNode) + noCallSrv := New(noCallObjSvc, 0, &noCallNode, neofscryptotest.Signer()) clientSigner := neofscryptotest.Signer() clientPubKey := neofscrypto.PublicKeyBytes(clientSigner.Public()) serverPubKey := neofscrypto.PublicKeyBytes(neofscryptotest.Signer().Public()) cnr := cidtest.ID() objID := oidtest.ID() - req := anyValidRequest(t, clientSigner, cnr, objID) + req, _ := anyValidRequest(t, clientSigner, cnr, objID) t.Run("invalid/unsupported signature format", func(t *testing.T) { // note: verification is tested separately @@ -219,7 +223,7 @@ func TestServer_Replicate(t *testing.T) { expectedMsg: "unsupported scheme in the object signature field", }, } { - req := anyValidRequest(t, neofscryptotest.Signer(), cidtest.ID(), oidtest.ID()) + req, _ := anyValidRequest(t, neofscryptotest.Signer(), cidtest.ID(), oidtest.ID()) req.Signature = tc.fSig() resp, err := noCallSrv.Replicate(context.Background(), req) require.NoError(t, err, tc.name) @@ -324,7 +328,7 @@ func TestServer_Replicate(t *testing.T) { t.Run("apply storage policy failure", func(t *testing.T) { node := newTestNode(t, serverPubKey, clientPubKey, cnr, req.Object) - srv := New(noCallObjSvc, node) + srv := New(noCallObjSvc, 0, node, neofscryptotest.Signer()) node.cnrErr = errors.New("any error") @@ -336,7 +340,7 @@ func TestServer_Replicate(t *testing.T) { t.Run("client or server mismatches object's storage policy", func(t *testing.T) { node := newTestNode(t, serverPubKey, clientPubKey, cnr, req.Object) - srv := New(noCallObjSvc, node) + srv := New(noCallObjSvc, 0, node, neofscryptotest.Signer()) node.serverOutsideCnr = true node.clientOutsideCnr = true @@ -356,7 +360,7 @@ func TestServer_Replicate(t *testing.T) { t.Run("local storage failure", func(t *testing.T) { node := newTestNode(t, serverPubKey, clientPubKey, cnr, req.Object) - srv := New(noCallObjSvc, node) + srv := New(noCallObjSvc, 0, node, neofscryptotest.Signer()) node.storeErr = errors.New("any error") @@ -366,9 +370,44 @@ func TestServer_Replicate(t *testing.T) { require.Equal(t, "failed to verify and store object locally: any error", resp.GetStatus().GetMessage()) }) + t.Run("meta information signature", func(t *testing.T) { + var mNumber uint32 = 123 + signer := neofscryptotest.Signer() + reqForSignature, o := anyValidRequest(t, clientSigner, cnr, objID) + node := newTestNode(t, serverPubKey, clientPubKey, cnr, reqForSignature.Object) + srv := New(noCallObjSvc, mNumber, node, signer) + + t.Run("signature not requested", func(t *testing.T) { + resp, err := srv.Replicate(context.Background(), reqForSignature) + require.NoError(t, err) + require.EqualValues(t, 0, resp.GetStatus().GetCode()) + require.Empty(t, resp.GetStatus().GetMessage()) + require.Empty(t, resp.GetObjectSignature()) + }) + + t.Run("signature is requested", func(t *testing.T) { + reqForSignature.SignObject = true + + resp, err := srv.Replicate(context.Background(), reqForSignature) + require.NoError(t, err) + require.EqualValues(t, 0, resp.GetStatus().GetCode()) + require.Empty(t, resp.GetStatus().GetMessage()) + require.NotNil(t, resp.GetObjectSignature()) + + var sigV2 refsv2.Signature + require.NoError(t, sigV2.Unmarshal(resp.GetObjectSignature())) + + var sig neofscrypto.Signature + require.NoError(t, sig.ReadFromV2(sigV2)) + + require.Equal(t, signer.PublicKeyBytes, sig.PublicKeyBytes()) + require.True(t, sig.Verify(objectcore.EncodeReplicationMetaInfo(o.GetContainerID(), o.GetID(), o.PayloadSize(), nil, nil, o.CreationEpoch(), mNumber))) + }) + }) + t.Run("OK", func(t *testing.T) { node := newTestNode(t, serverPubKey, clientPubKey, cnr, req.Object) - srv := New(noCallObjSvc, node) + srv := New(noCallObjSvc, 0, node, neofscryptotest.Signer()) resp, err := srv.Replicate(context.Background(), req) require.NoError(t, err) @@ -395,7 +434,7 @@ func BenchmarkServer_Replicate(b *testing.B) { ctx := context.Background() var node nopNode - srv := New(nil, node) + srv := New(nil, 0, node, neofscryptotest.Signer()) for _, tc := range []struct { name string @@ -421,7 +460,7 @@ func BenchmarkServer_Replicate(b *testing.B) { }, } { b.Run(tc.name, func(b *testing.B) { - req := anyValidRequest(b, tc.newSigner(b), cidtest.ID(), oidtest.ID()) + req, _ := anyValidRequest(b, tc.newSigner(b), cidtest.ID(), oidtest.ID()) b.ReportAllocs() b.ResetTimer() diff --git a/pkg/network/transport/object/grpc/service.go b/pkg/network/transport/object/grpc/service.go index 511d5413fa..6ebe90c5c5 100644 --- a/pkg/network/transport/object/grpc/service.go +++ b/pkg/network/transport/object/grpc/service.go @@ -11,6 +11,7 @@ import ( objectSvc "github.com/nspcc-dev/neofs-node/pkg/services/object" "github.com/nspcc-dev/neofs-node/pkg/services/util" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" objectsdk "github.com/nspcc-dev/neofs-sdk-go/object" ) @@ -47,14 +48,18 @@ type Node interface { type Server struct { srv objectSvc.ServiceServer - node Node + node Node + signer neofscrypto.Signer + mNumber uint32 } // New creates, initializes and returns Server instance. -func New(c objectSvc.ServiceServer, node Node) *Server { +func New(c objectSvc.ServiceServer, magicNumber uint32, node Node, signer neofscrypto.Signer) *Server { return &Server{ - srv: c, - node: node, + srv: c, + node: node, + signer: signer, + mNumber: magicNumber, } } diff --git a/pkg/services/object/put/distributed.go b/pkg/services/object/put/distributed.go index cb0b59554f..0fe937c20a 100644 --- a/pkg/services/object/put/distributed.go +++ b/pkg/services/object/put/distributed.go @@ -1,6 +1,7 @@ package putsvc import ( + "bytes" "fmt" "math" "slices" @@ -12,6 +13,7 @@ import ( "github.com/nspcc-dev/neofs-node/pkg/network" svcutil "github.com/nspcc-dev/neofs-node/pkg/services/object/util" "github.com/nspcc-dev/neofs-node/pkg/util" + apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" "github.com/nspcc-dev/neofs-sdk-go/netmap" objectSDK "github.com/nspcc-dev/neofs-sdk-go/object" @@ -21,14 +23,16 @@ import ( type preparedObjectTarget interface { WriteObject(*objectSDK.Object, object.ContentMeta, encodedObject) error - Close() (oid.ID, error) + Close() (oid.ID, *neofscrypto.Signature, error) } type distributedTarget struct { placementIterator placementIterator - obj *objectSDK.Object - objMeta object.ContentMeta + obj *objectSDK.Object + objMeta object.ContentMeta + networkMagicNumber uint32 + objSharedMeta []byte localNodeInContainer bool localNodeSigner neofscrypto.Signer @@ -76,7 +80,7 @@ func (t *distributedTarget) WriteHeader(hdr *objectSDK.Object) error { if t.placementIterator.localOnly { t.encodedObject, err = encodeObjectWithoutPayload(*hdr, int(payloadLen)) } else { - t.encodedObject, err = encodeReplicateRequestWithoutPayload(t.localNodeSigner, *hdr, int(payloadLen)) + t.encodedObject, err = encodeReplicateRequestWithoutPayload(t.localNodeSigner, *hdr, int(payloadLen), true) } if err != nil { return fmt.Errorf("encode object into binary: %w", err) @@ -127,6 +131,18 @@ func (t *distributedTarget) Close() (oid.ID, error) { } } + var deletedObjs []oid.ID + var lockedObjs []oid.ID + switch t.objMeta.Type() { + case objectSDK.TypeTombstone: + deletedObjs = t.objMeta.Objects() + case objectSDK.TypeLock: + lockedObjs = t.objMeta.Objects() + default: + } + + t.objSharedMeta = object.EncodeReplicationMetaInfo(t.obj.GetContainerID(), t.obj.GetID(), t.obj.PayloadSize(), deletedObjs, + lockedObjs, t.obj.CreationEpoch(), t.networkMagicNumber) id, _ := t.obj.ID() return id, t.placementIterator.iterateNodesForObject(id, t.sendObject) } @@ -138,11 +154,31 @@ func (t *distributedTarget) sendObject(node nodeDesc) error { target := t.nodeTargetInitializer(node) - if err := target.WriteObject(t.obj, t.objMeta, t.encodedObject); err != nil { + err := target.WriteObject(t.obj, t.objMeta, t.encodedObject) + if err != nil { return fmt.Errorf("could not write header: %w", err) - } else if _, err := target.Close(); err != nil { + } + + _, sig, err := target.Close() + if err != nil { return fmt.Errorf("could not close object stream: %w", err) } + + if t.localNodeInContainer && !node.local { + if sig == nil { + return fmt.Errorf("%w: missing object meta signature", apistatus.ErrSignatureVerification) + } + + if !bytes.Equal(sig.PublicKeyBytes(), node.info.PublicKey()) { + return fmt.Errorf("%w: public key differs in object meta signature", apistatus.ErrSignatureVerification) + } + + if !sig.Verify(t.objSharedMeta) { + return fmt.Errorf("%w: %s node did not pass the meta information verification", + apistatus.ErrSignatureVerification, network.StringifyGroup(node.info.AddressGroup())) + } + } + return nil } diff --git a/pkg/services/object/put/local.go b/pkg/services/object/put/local.go index 1963983dda..755274e738 100644 --- a/pkg/services/object/put/local.go +++ b/pkg/services/object/put/local.go @@ -8,6 +8,7 @@ import ( objectCore "github.com/nspcc-dev/neofs-node/pkg/core/object" "github.com/nspcc-dev/neofs-sdk-go/checksum" + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" "github.com/nspcc-dev/neofs-sdk-go/object" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" "github.com/nspcc-dev/tzhash/tz" @@ -47,15 +48,15 @@ func (t *localTarget) WriteObject(obj *object.Object, meta objectCore.ContentMet return nil } -func (t *localTarget) Close() (oid.ID, error) { +func (t *localTarget) Close() (oid.ID, *neofscrypto.Signature, error) { err := putObjectLocally(t.storage, t.obj, t.meta, &t.enc) if err != nil { - return oid.ID{}, err + return oid.ID{}, nil, err } id, _ := t.obj.ID() - return id, nil + return id, nil, nil } func putObjectLocally(storage ObjectStorage, obj *object.Object, meta objectCore.ContentMeta, enc *encodedObject) error { diff --git a/pkg/services/object/put/proto.go b/pkg/services/object/put/proto.go index 75f1174e84..5cc3c4eccf 100644 --- a/pkg/services/object/put/proto.go +++ b/pkg/services/object/put/proto.go @@ -30,6 +30,7 @@ const ( _ = iota fieldNumReplicateObject fieldNumReplicateSignature + fieldSignObjectMeta ) type encodedObject struct { @@ -66,7 +67,7 @@ func encodeObjectWithoutPayload(hdr object.Object, pldLen int) (encodedObject, e return res, nil } -func encodeReplicateRequestWithoutPayload(signer neofscrypto.Signer, hdr object.Object, pldLen int) (encodedObject, error) { +func encodeReplicateRequestWithoutPayload(signer neofscrypto.Signer, hdr object.Object, pldLen int, signObjectMeta bool) (encodedObject, error) { var res encodedObject id, ok := hdr.ID() if !ok { @@ -98,6 +99,7 @@ func encodeReplicateRequestWithoutPayload(signer neofscrypto.Signer, hdr object. return res, fmt.Errorf("replicate request exceeds server limit %d", math.MaxInt) } fullLen += protowire.SizeBytes(objFldLen) + fullLen += protowire.SizeTag(fieldSignObjectMeta) + protowire.SizeVarint(protowire.EncodeBool(signObjectMeta)) res.b = getPayload() if cap(res.b) < fullLen { @@ -105,6 +107,9 @@ func encodeReplicateRequestWithoutPayload(signer neofscrypto.Signer, hdr object. res.b = make([]byte, 0, fullLen) } + // meta signature extension flag + res.b = protowire.AppendTag(res.b, fieldSignObjectMeta, protowire.VarintType) + res.b = protowire.AppendVarint(res.b, protowire.EncodeBool(signObjectMeta)) // signature res.b = protowire.AppendTag(res.b, fieldNumReplicateSignature, protowire.BytesType) res.b = protowire.AppendVarint(res.b, uint64(sigFldLen)) diff --git a/pkg/services/object/put/proto_test.go b/pkg/services/object/put/proto_test.go index b8e55d0058..fe18ea6c7a 100644 --- a/pkg/services/object/put/proto_test.go +++ b/pkg/services/object/put/proto_test.go @@ -29,7 +29,7 @@ func TestUnaryReplicateRequest(t *testing.T) { signer := neofscryptotest.Signer() // prepare request - r, err := encodeReplicateRequestWithoutPayload(signer, hdr, len(payload)) + r, err := encodeReplicateRequestWithoutPayload(signer, hdr, len(payload), true) require.NoError(t, err) require.Equal(t, len(payload), cap(r.b)-r.pldOff) require.Equal(t, len(payload), cap(r.b)-len(r.b)) @@ -55,4 +55,7 @@ func TestUnaryReplicateRequest(t *testing.T) { require.NoError(t, objv2.FromGRPCMessage(req.Object)) obj2 := *object.NewFromV2(&objv2) require.Equal(t, obj, obj2) + + // check meta signature flag + require.True(t, req.GetSignObject()) } diff --git a/pkg/services/object/put/remote.go b/pkg/services/object/put/remote.go index f0f34ab73a..a484822741 100644 --- a/pkg/services/object/put/remote.go +++ b/pkg/services/object/put/remote.go @@ -10,6 +10,7 @@ import ( objectcore "github.com/nspcc-dev/neofs-node/pkg/core/object" internalclient "github.com/nspcc-dev/neofs-node/pkg/services/object/internal/client" "github.com/nspcc-dev/neofs-node/pkg/services/object/util" + 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/netmap" "github.com/nspcc-dev/neofs-sdk-go/object" @@ -53,14 +54,13 @@ func (t *remoteTarget) WriteObject(obj *object.Object, _ objectcore.ContentMeta, return nil } -func (t *remoteTarget) Close() (oid.ID, error) { +func (t *remoteTarget) Close() (oid.ID, *neofscrypto.Signature, error) { if t.enc.hdrOff > 0 { - err := t.transport.SendReplicationRequestToNode(t.ctx, t.enc.b, t.nodeInfo) + sig, err := t.transport.SendReplicationRequestToNode(t.ctx, t.enc.b, t.nodeInfo) if err != nil { - return oid.ID{}, fmt.Errorf("replicate object to remote node (key=%x): %w", t.nodeInfo.PublicKey(), err) + return oid.ID{}, nil, fmt.Errorf("replicate object to remote node (key=%x): %w", t.nodeInfo.PublicKey(), err) } - id, _ := t.obj.ID() - return id, nil + return t.obj.GetID(), sig, nil } var sessionInfo *util.SessionInfo @@ -74,12 +74,12 @@ func (t *remoteTarget) Close() (oid.ID, error) { key, err := t.keyStorage.GetKey(sessionInfo) if err != nil { - return oid.ID{}, fmt.Errorf("(%T) could not receive private key: %w", t, err) + return oid.ID{}, nil, fmt.Errorf("(%T) could not receive private key: %w", t, err) } c, err := t.clientConstructor.Get(t.nodeInfo) if err != nil { - return oid.ID{}, fmt.Errorf("(%T) could not create SDK client %s: %w", t, t.nodeInfo, err) + return oid.ID{}, nil, fmt.Errorf("(%T) could not create SDK client %s: %w", t, t.nodeInfo, err) } var prm internalclient.PutObjectPrm @@ -94,10 +94,10 @@ func (t *remoteTarget) Close() (oid.ID, error) { res, err := internalclient.PutObject(prm) if err != nil { - return oid.ID{}, fmt.Errorf("(%T) could not put object to %s: %w", t, t.nodeInfo.AddressGroup(), err) + return oid.ID{}, nil, fmt.Errorf("(%T) could not put object to %s: %w", t, t.nodeInfo.AddressGroup(), err) } - return res.ID(), nil + return res.ID(), nil, nil } // NewRemoteSender creates, initializes and returns new RemoteSender instance. @@ -139,9 +139,13 @@ func (s *RemoteSender) PutObject(ctx context.Context, p *RemotePutPrm) error { return fmt.Errorf("parse client node info: %w", err) } - if err := t.WriteObject(p.obj, objectcore.ContentMeta{}, encodedObject{}); err != nil { + err = t.WriteObject(p.obj, objectcore.ContentMeta{}, encodedObject{}) + if err != nil { return fmt.Errorf("(%T) could not send object header: %w", s, err) - } else if _, err := t.Close(); err != nil { + } + + _, _, err = t.Close() + if err != nil { return fmt.Errorf("(%T) could not send object: %w", s, err) } diff --git a/pkg/services/object/put/service.go b/pkg/services/object/put/service.go index 04ae1a82ec..e5b2414eb1 100644 --- a/pkg/services/object/put/service.go +++ b/pkg/services/object/put/service.go @@ -10,6 +10,7 @@ import ( objutil "github.com/nspcc-dev/neofs-node/pkg/services/object/util" "github.com/nspcc-dev/neofs-node/pkg/util" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" netmapsdk "github.com/nspcc-dev/neofs-sdk-go/netmap" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" "go.uber.org/zap" @@ -35,7 +36,7 @@ type Option func(*cfg) type Transport interface { // SendReplicationRequestToNode sends a prepared replication request message to // the specified remote node. - SendReplicationRequestToNode(ctx context.Context, req []byte, node client.NodeInfo) error + SendReplicationRequestToNode(ctx context.Context, req []byte, node client.NodeInfo) (*neofscrypto.Signature, error) } type ClientConstructor interface { @@ -99,6 +100,8 @@ type cfg struct { clientConstructor ClientConstructor log *zap.Logger + + networkMagic uint32 } func defaultCfg() *cfg { @@ -202,3 +205,9 @@ func WithLogger(l *zap.Logger) Option { c.log = l } } + +func WithNetworkMagic(m uint32) Option { + return func(c *cfg) { + c.networkMagic = m + } +} diff --git a/pkg/services/object/put/streamer.go b/pkg/services/object/put/streamer.go index 7c96e27919..46517b6bf5 100644 --- a/pkg/services/object/put/streamer.go +++ b/pkg/services/object/put/streamer.go @@ -207,6 +207,7 @@ func (p *Streamer) newCommonTarget(prm *PutInitPrm) internal.Target { withBroadcast := !localOnly && (typ == object.TypeTombstone || typ == object.TypeLock) return &distributedTarget{ + networkMagicNumber: p.networkMagic, placementIterator: placementIterator{ log: p.log, neoFSNet: p.neoFSNet,