diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d998f8a82..a691d55112 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Changelog for NeoFS Node ### Changed - SNs exchange TLS certificates on inter-node connections (#4097) +- SNs no longer sign TTL=1 requests over mutually authenticated inter-node connections (#4100) ### Removed diff --git a/cmd/neofs-node/grpc.go b/cmd/neofs-node/grpc.go index ac862da20e..d4aa3edd0e 100644 --- a/cmd/neofs-node/grpc.go +++ b/cmd/neofs-node/grpc.go @@ -14,6 +14,7 @@ import ( "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config" grpcconfig "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config/grpc" + "github.com/nspcc-dev/neofs-node/pkg/network/peerauth" "github.com/nspcc-dev/neofs-sdk-go/object" iprotobuf "github.com/nspcc-dev/neofs-sdk-go/proto/protobuf" "go.uber.org/zap" @@ -197,14 +198,20 @@ func buildSingleGRPCServer(c *cfg, sc grpcconfig.GRPC, maxRecvMsgSizeOpt grpc.Se // read certificate from disk on each handshake to pick up renewals automatically. creds := credentials.NewTLS(&tls.Config{ - GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) { + GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return nil, fmt.Errorf("reload TLS certificate: %w", err) } + if hello.ServerName == peerauth.TLSServerName { + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + ClientAuth: tls.RequireAnyClientCert, + VerifyPeerCertificate: verifyInterNodePeer(c), + }, nil + } return &tls.Config{ Certificates: []tls.Certificate{cert}, - ClientAuth: tls.RequestClientCert, }, nil }, }) diff --git a/cmd/neofs-node/mtls.go b/cmd/neofs-node/mtls.go index 115fd56762..7c954dc18a 100644 --- a/cmd/neofs-node/mtls.go +++ b/cmd/neofs-node/mtls.go @@ -1,12 +1,48 @@ package main import ( + "bytes" "crypto/tls" + "crypto/x509" "fmt" grpcconfig "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config/grpc" + "github.com/nspcc-dev/neofs-node/pkg/network/peerauth" + "github.com/nspcc-dev/neofs-sdk-go/netmap" + "go.uber.org/zap" ) +func verifyInterNodePeer(c *cfg) func([][]byte, [][]*x509.Certificate) error { + return func(rawCerts [][]byte, _ [][]*x509.Certificate) error { + pub, err := peerauth.CertificatePublicKeyFromRaw(rawCerts) + if err != nil { + return err + } + if isNetmapNode(c, pub) { + return nil + } + c.log.Warn("reject inter-node TLS peer absent from network map", zap.Binary("public key", pub)) + return fmt.Errorf("TLS peer is not a network map node") + } +} + +func isNetmapNode(c *cfg, pub []byte) bool { + v := c.netMap.Load() + if v == nil { + return false + } + nm, ok := v.(netmap.NetMap) + if !ok { + return false + } + for _, node := range nm.Nodes() { + if bytes.Equal(node.PublicKey(), pub) { + return true + } + } + return false +} + func clientCertificateProvider(cfgs []grpcconfig.GRPC) func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { for i := range cfgs { if !cfgs[i].TLS.Enabled { diff --git a/cmd/neofs-node/mtls_test.go b/cmd/neofs-node/mtls_test.go index 6d30206d36..e2d393d690 100644 --- a/cmd/neofs-node/mtls_test.go +++ b/cmd/neofs-node/mtls_test.go @@ -1,10 +1,19 @@ package main import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "math/big" "testing" + "time" grpcconfig "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config/grpc" + "github.com/nspcc-dev/neofs-node/pkg/network/peerauth" + "github.com/nspcc-dev/neofs-sdk-go/netmap" "github.com/stretchr/testify/require" + "go.uber.org/zap" ) func TestClientCertificateProvider(t *testing.T) { @@ -28,3 +37,38 @@ func TestClientCertificateProvider(t *testing.T) { _, err = provider(nil) require.ErrorContains(t, err, "client-certificate") } + +func TestVerifyInterNodePeer(t *testing.T) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + cert := testTLSCertificate(t, key) + pub, err := peerauth.CertificatePublicKey(cert) + require.NoError(t, err) + + var node netmap.NodeInfo + node.SetPublicKey(pub.Bytes()) + var nm netmap.NetMap + nm.SetNodes([]netmap.NodeInfo{node}) + c := &cfg{internals: internals{log: zap.NewNop()}} + c.netMap.Store(nm) + + require.NoError(t, verifyInterNodePeer(c)([][]byte{cert.Raw}, nil)) + + otherKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + otherCert := testTLSCertificate(t, otherKey) + require.ErrorContains(t, verifyInterNodePeer(c)([][]byte{otherCert.Raw}, nil), "not a network map node") +} + +func testTLSCertificate(t *testing.T, key *ecdsa.PrivateKey) *x509.Certificate { + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + NotBefore: time.Now().Add(-time.Minute), + NotAfter: time.Now().Add(time.Hour), + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) + require.NoError(t, err) + cert, err := x509.ParseCertificate(der) + require.NoError(t, err) + return cert +} diff --git a/cmd/neofs-node/object.go b/cmd/neofs-node/object.go index 1f39ce8fd6..a48953fcaf 100644 --- a/cmd/neofs-node/object.go +++ b/cmd/neofs-node/object.go @@ -400,6 +400,10 @@ type reputationClient struct { cons *reputationClientConstructor } +func (c *reputationClient) IsMutuallyAuthenticated() bool { + return clientcore.IsMutuallyAuthenticated(c.MultiAddressClient) +} + func (c *reputationClient) submitResult(err error) { currEpoch := c.cons.netState.CurrentEpoch() sat := err == nil diff --git a/config/example/node.yaml b/config/example/node.yaml index 4a1105032f..d81c5dc711 100644 --- a/config/example/node.yaml +++ b/config/example/node.yaml @@ -46,6 +46,7 @@ grpc: conn_limit: 1 # connection limits; exceeding connection will not be declined, just blocked before active number decreases or client timeouts tls: enabled: true # use TLS for a gRPC connection (min version is TLS 1.2) + # For inter-node mTLS, certificate public key must match the node key announced in the network map. certificate: /path/to/cert # path to TLS certificate key: /path/to/key # path to TLS key diff --git a/go.mod b/go.mod index 2d2beef659..192c2f78e4 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/nspcc-dev/neo-go v0.121.0 github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea github.com/nspcc-dev/neofs-contract v0.26.1 - github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.21 + github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.21.0.20260730184513-bfe4935dbbd5 github.com/nspcc-dev/tzhash v1.8.4 github.com/panjf2000/ants/v2 v2.11.5 github.com/prometheus/client_golang v1.23.2 diff --git a/go.sum b/go.sum index f312fdbeae..c6644a3b59 100644 --- a/go.sum +++ b/go.sum @@ -191,8 +191,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.26.1 h1:7Ii7Q4L3au408LOsIWKiSgfnT1g8G9jo3W7381d41T8= github.com/nspcc-dev/neofs-contract v0.26.1/go.mod h1:pevVF9OWdEN5bweKxOu6ryZv9muCEtS1ppzYM4RfBIo= -github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.21 h1:6LPpzMvEn8Y3mdOFHFtcrWC5GU1QGr5I8A701YufMt0= -github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.21/go.mod h1:cdLGU2E3f13UVE9nkHus171POaf2O4YyeXenUxQnGyQ= +github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.21.0.20260730184513-bfe4935dbbd5 h1:VVdNinqcXM0Om2U69dZnpEY3WKMjCROG0hJOViC4N5Q= +github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.21.0.20260730184513-bfe4935dbbd5/go.mod h1:cdLGU2E3f13UVE9nkHus171POaf2O4YyeXenUxQnGyQ= github.com/nspcc-dev/rfc6979 v0.2.4 h1:NBgsdCjhLpEPJZqmC9rciMZDcSY297po2smeaRjw57k= github.com/nspcc-dev/rfc6979 v0.2.4/go.mod h1:86ylDw6Kss+P6v4QAJqo1Sp3mC0/Zr9G97xSjQ9TuFg= github.com/nspcc-dev/tzhash v1.8.4 h1:lvuPGWsqEo9dVEvo/kdNLKv/Cy0yxRs9z5hJp8VcBuo= diff --git a/pkg/core/client/client.go b/pkg/core/client/client.go index f28b6ce7a7..345edf75c9 100644 --- a/pkg/core/client/client.go +++ b/pkg/core/client/client.go @@ -56,3 +56,10 @@ type MultiAddressClient interface { // this client (min(server, client) effectively). APIVersion() *protorefs.Version } + +// IsMutuallyAuthenticated reports whether all connections of c use mutual TLS. +// Clients that do not expose this property are treated as unauthenticated. +func IsMutuallyAuthenticated(c any) bool { + x, ok := c.(interface{ IsMutuallyAuthenticated() bool }) + return ok && x.IsMutuallyAuthenticated() +} diff --git a/pkg/network/cache/clients.go b/pkg/network/cache/clients.go index b25582e60d..23912924f2 100644 --- a/pkg/network/cache/clients.go +++ b/pkg/network/cache/clients.go @@ -14,6 +14,7 @@ import ( "maps" "slices" "sync" + "sync/atomic" "time" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" @@ -21,6 +22,7 @@ import ( "github.com/nspcc-dev/neofs-node/internal/uriutil" clientcore "github.com/nspcc-dev/neofs-node/pkg/core/client" "github.com/nspcc-dev/neofs-node/pkg/network" + "github.com/nspcc-dev/neofs-node/pkg/network/peerauth" "github.com/nspcc-dev/neofs-sdk-go/client" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" @@ -157,6 +159,7 @@ func (x *Clients) syncWithNetmapSN(ctx context.Context, sn netmap.NodeInfo) erro if slices.Contains(as, ma) { return false } + delete(conns.mtls, ma) if err := c.Close(); err != nil { x.log.Info("failed to close connection to the SN address no longer present in the new network map", zap.String("address", ma), zap.Error(err)) @@ -172,13 +175,15 @@ func (x *Clients) syncWithNetmapSN(ctx context.Context, sn netmap.NodeInfo) erro continue } x.log.Info("initializing connection to new SN address in the new network map...", zap.String("address", ma)) - c, _, err := x.initConnection(ctx, pub, ma) + mtls := new(atomic.Bool) + c, _, err := x.initConnection(ctx, pub, ma, mtls) if err != nil { x.log.Info("failed to init connection to new SN address in the new network map", zap.String("address", ma), zap.Error(err)) continue } conns.m[ma] = c + conns.mtls[ma] = mtls x.log.Info("connection to new SN address in the new network map successfully initialized", zap.String("address", ma)) } @@ -187,12 +192,14 @@ func (x *Clients) syncWithNetmapSN(ctx context.Context, sn netmap.NodeInfo) erro func (x *Clients) initConnections(ctx context.Context, pub []byte, addrs iter.Seq[string]) (*connections, error) { m := make(map[string]*client.Client) + mtls := make(map[string]*atomic.Bool) l := x.log.With(zap.String("public key", hex.EncodeToString(pub))) var ver *protorefs.Version for s := range addrs { l.Info("initializing connection to the SN...", zap.String("address", s)) - c, v, err := x.initConnection(ctx, pub, s) + endpointMTLS := new(atomic.Bool) + c, v, err := x.initConnection(ctx, pub, s, endpointMTLS) if err != nil { // TODO: if at least one address is OK, SN can be operational for cl := range maps.Values(m) { @@ -205,17 +212,20 @@ func (x *Clients) initConnections(ctx context.Context, pub []byte, addrs iter.Se } l.Info("connection to the SN successfully initialized", zap.String("address", s)) m[s] = c + mtls[s] = endpointMTLS } var hexKey = hex.EncodeToString(pub) return &connections{ - log: x.log.With(zap.String("SN public key", hexKey)), - nodeID: hexKey, - m: m, - apiVersion: ver, + log: x.log.With(zap.String("SN public key", hexKey)), + nodeID: hexKey, + hasClientCertificate: x.getClientCertificate != nil, + m: m, + mtls: mtls, + apiVersion: ver, }, nil } -func (x *Clients) initConnection(ctx context.Context, pub []byte, uri string) (*client.Client, *protorefs.Version, error) { +func (x *Clients) initConnection(ctx context.Context, pub []byte, uri string, mutuallyAuthenticated *atomic.Bool) (*client.Client, *protorefs.Version, error) { // FIXME: pending removal in #3982. var a network.Address if err := a.FromString(uri); err != nil { @@ -232,7 +242,17 @@ func (x *Clients) initConnection(ctx context.Context, pub []byte, uri string) (* if err != nil { return nil, nil, fmt.Errorf("parse node public key: %w", err) } - transportCreds = credentials.NewTLS(newNodeTLSConfig((*ecdsa.PublicKey)(expectedKey), x.getClientCertificate)) + getClientCertificate := x.getClientCertificate + if getClientCertificate != nil && mutuallyAuthenticated != nil { + getClientCertificate = func(info *tls.CertificateRequestInfo) (*tls.Certificate, error) { + cert, err := x.getClientCertificate(info) + if err == nil && cert != nil { + mutuallyAuthenticated.Store(true) + } + return cert, err + } + } + transportCreds = credentials.NewTLS(newNodeTLSConfig((*ecdsa.PublicKey)(expectedKey), getClientCertificate)) } else { transportCreds = insecure.NewCredentials() } @@ -256,6 +276,11 @@ func (x *Clients) initConnection(ctx context.Context, pub []byte, uri string) (* _ = grpcConn.Close() return res, nil, fmt.Errorf("init NeoFS API client from gRPC client conn: %w", err) } + if mutuallyAuthenticated != nil { + res.SetLocalRequestSigningStatus(func() bool { + return !mutuallyAuthenticated.Load() + }) + } ctx, cancel := context.WithTimeout(ctx, x.streamMsgTimeout) defer cancel() @@ -284,7 +309,7 @@ func (x *Clients) initConnection(ctx context.Context, pub []byte, uri string) (* } func newNodeTLSConfig(expectedKey *ecdsa.PublicKey, getClientCertificate func(*tls.CertificateRequestInfo) (*tls.Certificate, error)) *tls.Config { - return &tls.Config{ + cfg := &tls.Config{ InsecureSkipVerify: true, GetClientCertificate: getClientCertificate, VerifyConnection: func(state tls.ConnectionState) error { @@ -306,17 +331,49 @@ func newNodeTLSConfig(expectedKey *ecdsa.PublicKey, getClientCertificate func(*t return nil }, } + if getClientCertificate != nil { + cfg.ServerName = peerauth.TLSServerName + } + return cfg } type connections struct { - log *zap.Logger - nodeID string + log *zap.Logger + nodeID string + hasClientCertificate bool mtx sync.RWMutex m map[string]*client.Client // keys are multiaddrs + mtls map[string]*atomic.Bool apiVersion *protorefs.Version } +func (x *connections) IsMutuallyAuthenticated() bool { + if !x.hasClientCertificate { + return false + } + + x.mtx.RLock() + defer x.mtx.RUnlock() + if len(x.m) == 0 { + return false + } + for uri := range x.m { + if authenticated, ok := x.mtls[uri]; !ok || !authenticated.Load() { + return false + } + var a network.Address + if err := a.FromString(uri); err != nil { + return false + } + _, withTLS, err := uriutil.Parse(a.URIAddr()) + if err != nil || !withTLS { + return false + } + } + return true +} + func (x *connections) closeAll() { for ma, c := range x.all { if err := c.Close(); err != nil { diff --git a/pkg/network/cache/clients_internal_test.go b/pkg/network/cache/clients_internal_test.go index 019f38c66e..490bd74879 100644 --- a/pkg/network/cache/clients_internal_test.go +++ b/pkg/network/cache/clients_internal_test.go @@ -11,10 +11,13 @@ import ( "crypto/x509" "errors" "math/big" + "sync/atomic" "testing" "time" clientcore "github.com/nspcc-dev/neofs-node/pkg/core/client" + "github.com/nspcc-dev/neofs-node/pkg/network/peerauth" + "github.com/nspcc-dev/neofs-sdk-go/client" "github.com/stretchr/testify/require" ) @@ -34,6 +37,7 @@ func TestNodeTLSConfig(t *testing.T) { t.Run("matching self-signed certificate", func(t *testing.T) { cfg := newNodeTLSConfig(&expectedKey.PublicKey, nil) require.True(t, cfg.InsecureSkipVerify) + require.Empty(t, cfg.ServerName) require.NoError(t, cfg.VerifyConnection(tls.ConnectionState{ PeerCertificates: []*x509.Certificate{newSelfSignedCertificate(t, expectedKey)}, })) @@ -44,6 +48,7 @@ func TestNodeTLSConfig(t *testing.T) { cfg := newNodeTLSConfig(&expectedKey.PublicKey, func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { return expected, nil }) + require.Equal(t, peerauth.TLSServerName, cfg.ServerName) actual, err := cfg.GetClientCertificate(nil) require.NoError(t, err) require.Same(t, expected, actual) @@ -86,10 +91,43 @@ func TestNodeTLSConfig(t *testing.T) { } func TestInvalidTLSPublicKey(t *testing.T) { - _, _, err := new(Clients).initConnection(context.Background(), []byte("invalid"), "/dns4/example.com/tcp/443/tls") + _, _, err := new(Clients).initConnection(context.Background(), []byte("invalid"), "/dns4/example.com/tcp/443/tls", nil) require.ErrorContains(t, err, "parse node public key") } +func TestConnectionsIsMutuallyAuthenticated(t *testing.T) { + for _, tc := range []struct { + name string + hasClientCertificate bool + endpoints []string + peerRequestedCert bool + expected bool + }{ + {name: "single TLS endpoint", hasClientCertificate: true, endpoints: []string{"/dns4/example.com/tcp/443/tls"}, peerRequestedCert: true, expected: true}, + {name: "multiple TLS endpoints", hasClientCertificate: true, endpoints: []string{"/dns4/one.example.com/tcp/443/tls", "/dns4/two.example.com/tcp/443/tls"}, peerRequestedCert: true, expected: true}, + {name: "server did not request certificate", hasClientCertificate: true, endpoints: []string{"/dns4/example.com/tcp/443/tls"}}, + {name: "no client certificate", endpoints: []string{"/dns4/example.com/tcp/443/tls"}}, + {name: "plain endpoint", hasClientCertificate: true, endpoints: []string{"/dns4/example.com/tcp/8080"}}, + {name: "mixed endpoints", hasClientCertificate: true, endpoints: []string{"/dns4/example.com/tcp/443/tls", "/dns4/example.com/tcp/8080"}}, + {name: "empty endpoints", hasClientCertificate: true}, + {name: "invalid endpoint", hasClientCertificate: true, endpoints: []string{"invalid"}}, + } { + t.Run(tc.name, func(t *testing.T) { + conns := &connections{ + hasClientCertificate: tc.hasClientCertificate, + m: make(map[string]*client.Client, len(tc.endpoints)), + mtls: make(map[string]*atomic.Bool, len(tc.endpoints)), + } + for i := range tc.endpoints { + conns.m[tc.endpoints[i]] = nil + conns.mtls[tc.endpoints[i]] = new(atomic.Bool) + conns.mtls[tc.endpoints[i]].Store(tc.peerRequestedCert) + } + require.Equal(t, tc.expected, conns.IsMutuallyAuthenticated()) + }) + } +} + func newSelfSignedCertificate(t *testing.T, key crypto.Signer) *x509.Certificate { now := time.Now() tmpl := x509.Certificate{ diff --git a/pkg/network/peerauth/peerauth.go b/pkg/network/peerauth/peerauth.go index ea66c1a69c..c0137894d1 100644 --- a/pkg/network/peerauth/peerauth.go +++ b/pkg/network/peerauth/peerauth.go @@ -12,6 +12,11 @@ import ( "google.golang.org/grpc/peer" ) +// TLSServerName is the TLS SNI a storage node sends when dialing another node. +// It allows a shared public endpoint to distinguish inter-node mTLS from +// regular client TLS connections. +const TLSServerName = "neofs.internode.mtls" + // CertificatePublicKey returns the P-256 public key from cert. func CertificatePublicKey(cert *x509.Certificate) (*keys.PublicKey, error) { pub, ok := cert.PublicKey.(*ecdsa.PublicKey) @@ -24,6 +29,23 @@ func CertificatePublicKey(cert *x509.Certificate) (*keys.PublicKey, error) { return (*keys.PublicKey)(pub), nil } +// CertificatePublicKeyFromRaw returns the P-256 public key from the first TLS +// certificate in a handshake chain. +func CertificatePublicKeyFromRaw(rawCerts [][]byte) ([]byte, error) { + if len(rawCerts) == 0 { + return nil, fmt.Errorf("missing TLS peer certificate") + } + cert, err := x509.ParseCertificate(rawCerts[0]) + if err != nil { + return nil, fmt.Errorf("parse TLS peer certificate: %w", err) + } + pub, err := CertificatePublicKey(cert) + if err != nil { + return nil, err + } + return pub.Bytes(), nil +} + // PeerPublicKey returns the public key authenticated by the TLS connection. // It returns nil when the request has no TLS client certificate. func PeerPublicKey(ctx context.Context) (*keys.PublicKey, error) { diff --git a/pkg/services/object/range.go b/pkg/services/object/range.go index 278bfb0ca7..7470ea49d5 100644 --- a/pkg/services/object/range.go +++ b/pkg/services/object/range.go @@ -27,7 +27,7 @@ type rangeStreamProgress struct { // - [apistatus.ErrObjectNotFound] on 404 status // - nil on other API statuses // - any other transport/protocol error otherwise -func (s *rangeStream) continueWithConn(ctx context.Context, conn *grpc.ClientConn) error { +func (s *rangeStream) continueWithConn(ctx context.Context, conn *grpc.ClientConn, req *protoobject.GetRangeRequest) error { stream, err := conn.NewStream(ctx, &protoobject.ObjectService_ServiceDesc.Streams[3], protoobject.ObjectService_GetRange_FullMethodName, grpc.StaticMethod(), grpc.ForceCodecV2(iprotobuf.BufferedCodec{}), @@ -35,7 +35,7 @@ func (s *rangeStream) continueWithConn(ctx context.Context, conn *grpc.ClientCon if err != nil { return fmt.Errorf("stream opening failed: %w", err) } - if err = stream.SendMsg(s.req); err != nil { + if err = stream.SendMsg(req); err != nil { return fmt.Errorf("send request: %w", err) } if err = stream.CloseSend(); err != nil { diff --git a/pkg/services/object/server.go b/pkg/services/object/server.go index 7b9e0cb0b8..ee1a582c5d 100644 --- a/pkg/services/object/server.go +++ b/pkg/services/object/server.go @@ -422,11 +422,6 @@ func (s *Server) Put(gStream protoobject.ObjectService_PutServer) error { s.metrics.AddPutPayload(len(c)) } - if err = icrypto.VerifyRequestSignaturesN3(ctx, req, s.fsChain); err != nil { - err = s.sendStatusPutResponse(gStream, err, reqFirst) // assign for defer - return err - } - if s.fsChain.LocalNodeUnderMaintenance() { return s.sendStatusPutResponse(gStream, apistatus.ErrNodeUnderMaintenance, reqFirst) } @@ -443,6 +438,10 @@ func (s *Server) Put(gStream protoobject.ObjectService_PutServer) error { err = s.sendStatusPutResponse(gStream, fmt.Errorf("invalid object put stream part type %T", v), reqFirst) // assign for defer return err case *protoobject.PutRequest_Body_Init_: + if err = icrypto.VerifyRequestSignaturesN3(ctx, req, s.fsChain); err != nil { + err = s.sendStatusPutResponse(gStream, err, reqFirst) // assign for defer + return err + } if v.Init == nil { err = newBadRequestError(invalidRequestBodyMessage + ": missing init field") // defer return s.sendStatusPutResponse(gStream, err, reqFirst) @@ -824,20 +823,17 @@ func convertHeadPrm(signer ecdsa.PrivateKey, cnr container.Container, req *proto return getsvc.HeadPrm{}, errors.New("missing meta header") } - var updatedRequest bool - p.SetTransportFunc(func(ctx context.Context, c clientcore.MultiAddressClient) (mem.BufferSlice, iprotobuf.BuffersSlice, error) { - if !updatedRequest { - updatedRequest = true - req = &protoobject.HeadRequest{ - Body: req.Body, - MetaHeader: &protosession.RequestMetaHeader{ - Version: c.APIVersion(), - Ttl: 1, - }, - } + outReq := &protoobject.HeadRequest{ + Body: req.Body, + MetaHeader: &protosession.RequestMetaHeader{ + Version: c.APIVersion(), + Ttl: 1, + }, + } + if shouldSignOutgoingRequest(c, outReq) { var err error - req.VerifyHeader, err = neofscrypto.SignRequestWithBuffer(neofsecdsa.Signer(signer), req, nil) + outReq.VerifyHeader, err = neofscrypto.SignRequestWithBuffer(neofsecdsa.Signer(signer), outReq, nil) if err != nil { return nil, iprotobuf.BuffersSlice{}, err } @@ -847,7 +843,7 @@ func convertHeadPrm(signer ecdsa.PrivateKey, cnr container.Container, req *proto var hdr iprotobuf.BuffersSlice return respBuf, hdr, c.ForAnyGRPCConn(ctx, func(ctx context.Context, conn *grpc.ClientConn) error { var err error - respBuf, hdr, err = getHeaderFromRemoteNode(ctx, conn, req, objID) + respBuf, hdr, err = getHeaderFromRemoteNode(ctx, conn, outReq, objID) return err // TODO: log error }) }) @@ -1392,31 +1388,28 @@ func convertGetPrm(signer ecdsa.PrivateKey, cnr container.Container, req *protoo proxyCtx.resolveRange = p.ResolveRange } - var updatedRequest bool - p.SetTransportFunc(func(ctx context.Context, c clientcore.MultiAddressClient) error { - if !updatedRequest { - updatedRequest = true - - req = &protoobject.GetRequest{ - Body: req.Body, - MetaHeader: &protosession.RequestMetaHeader{ - Version: c.APIVersion(), - Ttl: 1, - }, - } - if proxyCtx.suppressInit { - req.Body.PayloadOnly = false - } + outReq := &protoobject.GetRequest{ + Body: req.Body, + MetaHeader: &protosession.RequestMetaHeader{ + Version: c.APIVersion(), + Ttl: 1, + }, + } + if proxyCtx.suppressInit { + outReq.Body = proto.Clone(outReq.Body).(*protoobject.GetRequest_Body) + outReq.Body.PayloadOnly = false + } + if shouldSignOutgoingRequest(c, outReq) { var err error - req.VerifyHeader, err = neofscrypto.SignRequestWithBuffer(neofsecdsa.Signer(signer), req, nil) + outReq.VerifyHeader, err = neofscrypto.SignRequestWithBuffer(neofsecdsa.Signer(signer), outReq, nil) if err != nil { return err } } return c.ForAnyGRPCConn(ctx, func(ctx context.Context, conn *grpc.ClientConn) error { - return proxyCtx.continueWithConn(ctx, req, conn) // TODO: log error + return proxyCtx.continueWithConn(ctx, outReq, conn) // TODO: log error }) }) return p, nil @@ -1653,28 +1646,28 @@ func convertRangePrm(signer ecdsa.PrivateKey, cnr container.Container, req *prot return p, nil } - var onceResign sync.Once meta := req.GetMetaHeader() if meta == nil { return getsvc.RangePrm{}, errors.New("missing meta header") } p.SetTransportFunc(func(ctx context.Context, c clientcore.MultiAddressClient) error { - var err error - onceResign.Do(func() { - req = &protoobject.GetRangeRequest{ - Body: req.Body, - MetaHeader: &protosession.RequestMetaHeader{ - Version: c.APIVersion(), - Ttl: 1, - }, + outReq := &protoobject.GetRangeRequest{ + Body: req.Body, + MetaHeader: &protosession.RequestMetaHeader{ + Version: c.APIVersion(), + Ttl: 1, + }, + } + if shouldSignOutgoingRequest(c, outReq) { + var err error + outReq.VerifyHeader, err = neofscrypto.SignRequestWithBuffer(neofsecdsa.Signer(signer), outReq, nil) + if err != nil { + return err } - req.VerifyHeader, err = neofscrypto.SignRequestWithBuffer(neofsecdsa.Signer(signer), req, nil) - }) - if err != nil { - return err } - - return c.ForAnyGRPCConn(ctx, stream.continueWithConn) + return c.ForAnyGRPCConn(ctx, func(ctx context.Context, conn *grpc.ClientConn) error { + return stream.continueWithConn(ctx, conn, outReq) + }) }) return p, nil } @@ -2100,10 +2093,6 @@ func (s *Server) ProcessSearch(ctx context.Context, req *protoobject.SearchV2Req Ttl: 1, }, } - if req.VerifyHeader, err = neofscrypto.SignRequestWithBuffer[*protoobject.SearchV2Request_Body](neofsecdsa.Signer(s.signer), req, nil); err != nil { - return nil, nil, fmt.Errorf("sign request: %w", err) - } - var optimizedNodes = (len(body.Filters) != 0) && slices.ContainsFunc(body.Filters, func(filt *protoobject.SearchFilter) bool { return !strings.HasPrefix(filt.Key, "$Object:") && !strings.HasPrefix(filt.Key, "__NEOFS__") @@ -2185,11 +2174,23 @@ func (s *Server) searchOnRemoteNode(ctx context.Context, node netmap.NodeInfo, r return nil, false, fmt.Errorf("get node client: %w", err) } + outReq := &protoobject.SearchV2Request{ + Body: req.Body, + MetaHeader: req.MetaHeader, + } + if shouldSignOutgoingRequest(c, outReq) { + var err error + outReq.VerifyHeader, err = neofscrypto.SignRequestWithBuffer[*protoobject.SearchV2Request_Body](neofsecdsa.Signer(s.signer), outReq, nil) + if err != nil { + return nil, false, fmt.Errorf("sign request: %w", err) + } + } + var items []client.SearchResultItem var more bool return items, more, c.ForAnyGRPCConn(ctx, func(ctx context.Context, conn *grpc.ClientConn) error { var err error - items, more, err = searchOnRemoteAddress(ctx, conn, req) + items, more, err = searchOnRemoteAddress(ctx, conn, outReq) return err // TODO: log error }) } @@ -2385,6 +2386,11 @@ func needSignGetResponse(req util.Request) bool { return util.VersionLE(req, 2, 17) } +func shouldSignOutgoingRequest(c any, req util.Request) bool { + meta := req.GetMetaHeader() + return meta == nil || meta.GetTtl() != 1 || !clientcore.IsMutuallyAuthenticated(c) +} + func checkHeaderProtobufAgainstID(buffers iprotobuf.BuffersSlice, id oid.ID, ordered bool) error { b := buffers.ReadOnlyData() if !ordered {