diff --git a/CHANGELOG.md b/CHANGELOG.md index 7785a8d520..9e8c4a24fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Changelog for NeoFS Node ### Added ### Fixed -- IR exponentially retries updating SN lists in the Container contract in error cases (#3344) +- IR exponentially retries updating SN lists in the Container contract in error cases (#3344) - Epoch stale after NEO RPC connection loss by IR (#3397) ### Changed @@ -14,10 +14,16 @@ Changelog for NeoFS Node - SN caches up to 1000 session token verification results until the next epoch (#3369) - SN no longer wastes memory for logger in ACL and GET/HEAD/RANGE handlers (#3408, #3412) - Inner ring ticks epoch based on real time, not blocks count (#3402) +- IR, CLI and SN ignore signatures of object GET/HEAD responses (#3406) +- IR, CLI and SN verify object checksums from GET/HEAD responses (except SN proxy case) (#3406) +- SN does not sign object GET/HEAD responses to requests with API version > v2.17 (#3406) +- SN now verifies object header from proxy GET/HEAD responses against requested ID (#3406) +- SN now verifies payload from proxy GET responses against in-header checksum (#3406) ### Removed ### Updated +- `github.com/nspcc-dev/neofs-sdk-go` dependency to `v1.0.0-rc.13.0.20250623124459-a9cfab652dc0` (#3406) ### Updating from v0.47.1 diff --git a/go.mod b/go.mod index fdcb455b06..a8c7a68817 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/nspcc-dev/neo-go v0.110.0 github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea github.com/nspcc-dev/neofs-contract v0.23.0 - github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.13.0.20250610144537-4b8bd696a7eb + github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.13.0.20250623124459-a9cfab652dc0 github.com/nspcc-dev/tzhash v1.8.2 github.com/olekukonko/tablewriter v0.0.5 github.com/panjf2000/ants/v2 v2.9.0 diff --git a/go.sum b/go.sum index c5a7c2a3c3..1a331b1fad 100644 --- a/go.sum +++ b/go.sum @@ -201,8 +201,8 @@ github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea h1:mK 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.23.0 h1:F5ciU0wPqSbycPY8qOtb4PvgnSZBNQ5Jp9tdeVSKu4o= github.com/nspcc-dev/neofs-contract v0.23.0/go.mod h1:it6Su92UvEFQDsMOfDIXapLu0j5TQSOvkS2YdUlPdgo= -github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.13.0.20250610144537-4b8bd696a7eb h1:aQ6W8/8SIvcJwH1QF+NuwB8Uvt6LYFCOcRHLgynejeA= -github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.13.0.20250610144537-4b8bd696a7eb/go.mod h1:j/NUu5iOGFkOVYM42XoC1X9DZD0/y89Pws++w5vxtQk= +github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.13.0.20250623124459-a9cfab652dc0 h1:cWNNmSe0fJd81URPq8dp/8nkJywuO655/M8gdFgfVWg= +github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.13.0.20250623124459-a9cfab652dc0/go.mod h1:j/NUu5iOGFkOVYM42XoC1X9DZD0/y89Pws++w5vxtQk= github.com/nspcc-dev/rfc6979 v0.2.3 h1:QNVykGZ3XjFwM/88rGfV3oj4rKNBy+nYI6jM7q19hDI= github.com/nspcc-dev/rfc6979 v0.2.3/go.mod h1:q3sCL1Ed7homjqYK8KmFSzEmm+7Ngyo7PePbZanhaDE= github.com/nspcc-dev/tzhash v1.8.2 h1:ebRCbPoEuoqrhC6sSZmrT/jI3h1SzCWakxxV6gp5QAg= diff --git a/pkg/services/object/server.go b/pkg/services/object/server.go index 7f784f3990..e91aa03e1f 100644 --- a/pkg/services/object/server.go +++ b/pkg/services/object/server.go @@ -9,6 +9,7 @@ import ( "encoding/binary" "errors" "fmt" + "hash" "io" "sync" "time" @@ -551,12 +552,14 @@ func (s *Server) Delete(ctx context.Context, req *protoobject.DeleteRequest) (*p return s.signDeleteResponse(&protoobject.DeleteResponse{Body: &rb}), nil } -func (s *Server) signHeadResponse(resp *protoobject.HeadResponse) *protoobject.HeadResponse { - resp.VerifyHeader = util.SignResponse(&s.signer, resp) +func (s *Server) signHeadResponse(resp *protoobject.HeadResponse, sign bool) *protoobject.HeadResponse { + if sign { + resp.VerifyHeader = util.SignResponse(&s.signer, resp) + } return resp } -func (s *Server) makeStatusHeadResponse(err error) *protoobject.HeadResponse { +func (s *Server) makeStatusHeadResponse(err error, sign bool) *protoobject.HeadResponse { var splitErr *object.SplitInfoError if errors.As(err, &splitErr) { return s.signHeadResponse(&protoobject.HeadResponse{ @@ -565,11 +568,11 @@ func (s *Server) makeStatusHeadResponse(err error) *protoobject.HeadResponse { SplitInfo: splitErr.SplitInfo().ProtoMessage(), }, }, - }) + }, sign) } return s.signHeadResponse(&protoobject.HeadResponse{ MetaHeader: s.makeResponseMetaHeader(util.ToStatus(err)), - }) + }, sign) } func (s *Server) Head(ctx context.Context, req *protoobject.HeadRequest) (*protoobject.HeadResponse, error) { @@ -579,45 +582,47 @@ func (s *Server) Head(ctx context.Context, req *protoobject.HeadRequest) (*proto ) defer func() { s.pushOpExecResult(stat.MethodObjectHead, err, t) }() + needSignResp := needSignGetResponse(req) + if err := icrypto.VerifyRequestSignaturesN3(req, s.fsChain); err != nil { - return s.makeStatusHeadResponse(err), nil + return s.makeStatusHeadResponse(err, needSignResp), nil } if s.fsChain.LocalNodeUnderMaintenance() { - return s.makeStatusHeadResponse(apistatus.ErrNodeUnderMaintenance), nil + return s.makeStatusHeadResponse(apistatus.ErrNodeUnderMaintenance, needSignResp), nil } reqInfo, err := s.reqInfoProc.HeadRequestToInfo(req) if err != nil { - return s.makeStatusHeadResponse(err), nil + return s.makeStatusHeadResponse(err, needSignResp), nil } if !s.aclChecker.CheckBasicACL(reqInfo) { err = basicACLErr(reqInfo) // needed for defer - return s.makeStatusHeadResponse(err), nil + return s.makeStatusHeadResponse(err, needSignResp), nil } err = s.aclChecker.CheckEACL(req, reqInfo) if err != nil { err = eACLErr(reqInfo, err) // needed for defer - return s.makeStatusHeadResponse(err), nil + return s.makeStatusHeadResponse(err, needSignResp), nil } var resp protoobject.HeadResponse p, err := convertHeadPrm(s.signer, req, &resp) if err != nil { - return s.makeStatusHeadResponse(err), nil + return s.makeStatusHeadResponse(err, needSignResp), nil } err = s.handlers.Head(ctx, p) if err != nil { - return s.makeStatusHeadResponse(err), nil + return s.makeStatusHeadResponse(err, needSignResp), nil } err = s.aclChecker.CheckEACL(&resp, reqInfo) if err != nil { err = eACLErr(reqInfo, err) // defer - return s.makeStatusHeadResponse(err), nil + return s.makeStatusHeadResponse(err, needSignResp), nil } - return s.signHeadResponse(&resp), nil + return s.signHeadResponse(&resp, needSignResp), nil } type headResponse struct { @@ -705,29 +710,22 @@ func convertHeadPrm(signer ecdsa.PrivateKey, req *protoobject.HeadRequest, resp return nil, err } - nodePub := node.PublicKey() var hdr *object.Object return hdr, c.ForEachGRPCConn(ctx, func(ctx context.Context, conn *grpc.ClientConn) error { var err error - hdr, err = getHeaderFromRemoteNode(ctx, conn, nodePub, req) + hdr, err = getHeaderFromRemoteNode(ctx, conn, req, addr.Object()) return err // TODO: log error }) }) return p, nil } -func getHeaderFromRemoteNode(ctx context.Context, conn *grpc.ClientConn, nodePub []byte, req *protoobject.HeadRequest) (*object.Object, error) { +func getHeaderFromRemoteNode(ctx context.Context, conn *grpc.ClientConn, req *protoobject.HeadRequest, reqOID oid.ID) (*object.Object, error) { resp, err := protoobject.NewObjectServiceClient(conn).Head(ctx, req) if err != nil { return nil, fmt.Errorf("sending the request failed: %w", err) } - if err := internal.VerifyResponseKeyV2(nodePub, resp); err != nil { - return nil, err - } - if err := neofscrypto.VerifyResponseWithBuffer(resp, nil); err != nil { - return nil, fmt.Errorf("response verification failed: %w", err) - } if err := checkStatus(resp.GetMetaHeader().GetStatus()); err != nil { return nil, err } @@ -773,6 +771,10 @@ func getHeaderFromRemoteNode(ctx context.Context, conn *grpc.ClientConn, nodePub return nil, errors.New("missing signature") } + if err := checkHeaderAgainstID(v.Header.Header, reqOID); err != nil { + return nil, err + } + hdr = v.Header.Header idSig = v.Header.Signature case *protoobject.HeadResponse_Body_SplitInfo: @@ -962,12 +964,14 @@ func getHashesFromRemoteNode(ctx context.Context, conn *grpc.ClientConn, nodePub return resp.GetBody().GetHashList(), nil } -func (s *Server) sendGetResponse(stream protoobject.ObjectService_GetServer, resp *protoobject.GetResponse) error { - resp.VerifyHeader = util.SignResponse(&s.signer, resp) +func (s *Server) sendGetResponse(stream protoobject.ObjectService_GetServer, resp *protoobject.GetResponse, sign bool) error { + if sign { + resp.VerifyHeader = util.SignResponse(&s.signer, resp) + } return stream.Send(resp) } -func (s *Server) sendStatusGetResponse(stream protoobject.ObjectService_GetServer, err error) error { +func (s *Server) sendStatusGetResponse(stream protoobject.ObjectService_GetServer, err error, sign bool) error { var splitErr *object.SplitInfoError if errors.As(err, &splitErr) { return s.sendGetResponse(stream, &protoobject.GetResponse{ @@ -976,17 +980,19 @@ func (s *Server) sendStatusGetResponse(stream protoobject.ObjectService_GetServe SplitInfo: splitErr.SplitInfo().ProtoMessage(), }, }, - }) + }, sign) } return s.sendGetResponse(stream, &protoobject.GetResponse{ MetaHeader: s.makeResponseMetaHeader(util.ToStatus(err)), - }) + }, sign) } type getStream struct { base protoobject.ObjectService_GetServer srv *Server reqInfo aclsvc.RequestInfo + + signResponse bool } func (s *getStream) WriteHeader(hdr *object.Object) error { @@ -1003,7 +1009,7 @@ func (s *getStream) WriteHeader(hdr *object.Object) error { if err := s.srv.aclChecker.CheckEACL(resp, s.reqInfo); err != nil { return eACLErr(s.reqInfo, err) } - return s.srv.sendGetResponse(s.base, resp) + return s.srv.sendGetResponse(s.base, resp, s.signResponse) } func (s *getStream) WriteChunk(chunk []byte) error { @@ -1015,7 +1021,7 @@ func (s *getStream) WriteChunk(chunk []byte) error { }, }, } - if err := s.srv.sendGetResponse(s.base, newResp); err != nil { + if err := s.srv.sendGetResponse(s.base, newResp, s.signResponse); err != nil { return err } } @@ -1029,39 +1035,43 @@ func (s *Server) Get(req *protoobject.GetRequest, gStream protoobject.ObjectServ t = time.Now() ) defer func() { s.pushOpExecResult(stat.MethodObjectGet, err, t) }() + + needSignResp := needSignGetResponse(req) + if err = icrypto.VerifyRequestSignatures(req); err != nil { - return s.sendStatusGetResponse(gStream, err) + return s.sendStatusGetResponse(gStream, err, needSignResp) } if s.fsChain.LocalNodeUnderMaintenance() { - return s.sendStatusGetResponse(gStream, apistatus.ErrNodeUnderMaintenance) + return s.sendStatusGetResponse(gStream, apistatus.ErrNodeUnderMaintenance, needSignResp) } reqInfo, err := s.reqInfoProc.GetRequestToInfo(req) if err != nil { - return s.sendStatusGetResponse(gStream, err) + return s.sendStatusGetResponse(gStream, err, needSignResp) } if !s.aclChecker.CheckBasicACL(reqInfo) { err = basicACLErr(reqInfo) // needed for defer - return s.sendStatusGetResponse(gStream, err) + return s.sendStatusGetResponse(gStream, err, needSignResp) } err = s.aclChecker.CheckEACL(req, reqInfo) if err != nil { err = eACLErr(reqInfo, err) // needed for defer - return s.sendStatusGetResponse(gStream, err) + return s.sendStatusGetResponse(gStream, err, needSignResp) } p, err := convertGetPrm(s.signer, req, &getStream{ - base: gStream, - srv: s, - reqInfo: reqInfo, + base: gStream, + srv: s, + reqInfo: reqInfo, + signResponse: needSignResp, }) if err != nil { - return s.sendStatusGetResponse(gStream, err) + return s.sendStatusGetResponse(gStream, err, needSignResp) } err = s.handlers.Get(gStream.Context(), p) if err != nil { - return s.sendStatusGetResponse(gStream, err) + return s.sendStatusGetResponse(gStream, err, needSignResp) } return nil } @@ -1096,12 +1106,17 @@ func convertGetPrm(signer ecdsa.PrivateKey, req *protoobject.GetRequest, stream } var onceResign sync.Once - var onceHdr sync.Once - var respondedPayload int meta := req.GetMetaHeader() if meta == nil { return getsvc.Prm{}, errors.New("missing meta header") } + + proxyCtx := getProxyContext{ + req: req, + reqOID: addr.Object(), + respStream: stream, + } + p.SetRequestForwarder(func(ctx context.Context, node client.NodeInfo, c client.MultiAddressClient) (*object.Object, error) { var err error onceResign.Do(func() { @@ -1116,9 +1131,8 @@ func convertGetPrm(signer ecdsa.PrivateKey, req *protoobject.GetRequest, stream return nil, err } - nodePub := node.PublicKey() return nil, c.ForEachGRPCConn(ctx, func(ctx context.Context, conn *grpc.ClientConn) error { - err := continueGetFromRemoteNode(ctx, conn, nodePub, req, stream, &onceHdr, &respondedPayload) + err := proxyCtx.continueWithConn(ctx, conn) if errors.Is(err, io.EOF) { return nil } @@ -1128,9 +1142,22 @@ func convertGetPrm(signer ecdsa.PrivateKey, req *protoobject.GetRequest, stream return p, nil } -func continueGetFromRemoteNode(ctx context.Context, conn *grpc.ClientConn, nodePub []byte, req *protoobject.GetRequest, - stream *getStream, onceHdr *sync.Once, respondedPayload *int) error { - getStream, err := protoobject.NewObjectServiceClient(conn).Get(ctx, req) +type getProxyContext struct { + req *protoobject.GetRequest + reqOID oid.ID + respStream *getStream + + onceHdr sync.Once + + payloadLenCheck uint64 + payloadHashCheck []byte + + respondedPayload int + payloadHashGot hash.Hash +} + +func (x *getProxyContext) continueWithConn(ctx context.Context, conn *grpc.ClientConn) error { + getStream, err := protoobject.NewObjectServiceClient(conn).Get(ctx, x.req) if err != nil { return fmt.Errorf("stream opening failed: %w", err) } @@ -1149,12 +1176,6 @@ func continueGetFromRemoteNode(ctx context.Context, conn *grpc.ClientConn, nodeP return fmt.Errorf("reading the response failed: %w", err) } - if err = internal.VerifyResponseKeyV2(nodePub, resp); err != nil { - return err - } - if err := neofscrypto.VerifyResponseWithBuffer(resp, nil); err != nil { - return fmt.Errorf("response verification failed: %w", err) - } if err := checkStatus(resp.GetMetaHeader().GetStatus()); err != nil { return err } @@ -1170,6 +1191,17 @@ func continueGetFromRemoteNode(ctx context.Context, conn *grpc.ClientConn, nodeP if v == nil || v.Init == nil { return errors.New("nil header oneof field") } + + if v.Init.Header == nil { + return errors.New("invalid response: missing header") + } + if v.Init.Header.PayloadHash == nil { + return errors.New("invalid response: invalid header: missing payload hash") + } + if err := checkHeaderAgainstID(v.Init.Header, x.reqOID); err != nil { + return err + } + mo := &protoobject.Object{ ObjectId: v.Init.ObjectId, Signature: v.Init.Signature, @@ -1180,27 +1212,40 @@ func continueGetFromRemoteNode(ctx context.Context, conn *grpc.ClientConn, nodeP if err != nil { return err } - onceHdr.Do(func() { - err = stream.WriteHeader(obj) + x.onceHdr.Do(func() { + err = x.respStream.WriteHeader(obj) }) if err != nil { return fmt.Errorf("could not write object header in Get forwarder: %w", err) } + + x.payloadLenCheck = v.Init.Header.PayloadLength + x.payloadHashCheck = v.Init.Header.PayloadHash.Sum + x.payloadHashGot = sha256.New() case *protoobject.GetResponse_Body_Chunk: if !headWas { return errors.New("incorrect message sequence") } fullChunk := v.Chunk - respChunk := chunkToSend(*respondedPayload, readPayload, fullChunk) + respChunk := chunkToSend(x.respondedPayload, readPayload, fullChunk) if len(respChunk) == 0 { readPayload += len(fullChunk) continue } - if err := stream.WriteChunk(respChunk); err != nil { + + x.payloadHashGot.Write(respChunk) // never returns an error according to docs + + if uint64(x.respondedPayload+len(respChunk)) == x.payloadLenCheck { + if !bytes.Equal(x.payloadHashGot.Sum(nil), x.payloadHashCheck) { // not merged via && for readability + return errors.New("received payload mismatches checksum from header") + } + } + + if err := x.respStream.WriteChunk(respChunk); err != nil { return fmt.Errorf("could not write object chunk in Get forwarder: %w", err) } readPayload += len(fullChunk) - *respondedPayload += len(respChunk) + x.respondedPayload += len(respChunk) case *protoobject.GetResponse_Body_SplitInfo: if v == nil || v.SplitInfo == nil { return errors.New("nil split info oneof field") @@ -2256,3 +2301,22 @@ func chunkToSend(global, local int, chunk []byte) []byte { } return chunk[global-local:] } + +func needSignGetResponse(req interface { + GetMetaHeader() *protosession.RequestMetaHeader +}) bool { + ver := req.GetMetaHeader().GetVersion() + mjr := ver.GetMajor() // NPE-safe + return mjr < 2 || mjr == 2 && ver.GetMinor() <= 17 +} + +func checkHeaderAgainstID(hdr *protoobject.Header, id oid.ID) error { + b := make([]byte, hdr.MarshaledSize()) + hdr.MarshalStable(b) + + if oid.NewFromObjectHeaderBinary(b) != id { + return errors.New("received header mismatches ID") + } + + return nil +}