From d3e889926c811f5364798f1eba0a65071f4e797d Mon Sep 17 00:00:00 2001 From: Andrey Butusov Date: Tue, 21 Jul 2026 21:04:17 +0300 Subject: [PATCH] node: send certificates for inter-node TLS connections Request client certificates on TLS endpoints and use their keys to authenticate unsigned one-hop object requests. Closes #4088. Signed-off-by: Andrey Butusov --- CHANGELOG.md | 1 + cmd/neofs-node/config.go | 3 +- cmd/neofs-node/grpc.go | 1 + cmd/neofs-node/mtls.go | 27 +++++++ cmd/neofs-node/mtls_test.go | 23 ++++++ pkg/network/cache/clients.go | 35 ++++---- pkg/network/cache/clients_internal_test.go | 20 +++-- pkg/network/peerauth/peerauth.go | 47 +++++++++++ pkg/network/peerauth/peerauth_test.go | 74 +++++++++++++++++ pkg/services/object/acl/v2/service.go | 40 +++++++--- .../object/acl/v2/service_internal_test.go | 42 ++++++++++ pkg/services/object/common/request.go | 4 + pkg/services/object/server.go | 80 ++++++++++++++----- pkg/services/object/server_internal_test.go | 57 +++++++++++++ 14 files changed, 401 insertions(+), 53 deletions(-) create mode 100644 cmd/neofs-node/mtls.go create mode 100644 cmd/neofs-node/mtls_test.go create mode 100644 pkg/network/peerauth/peerauth.go create mode 100644 pkg/network/peerauth/peerauth_test.go create mode 100644 pkg/services/object/server_internal_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 4946564914..7f655b5ac2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Changelog for NeoFS Node - SN no longer limits local PUT with static timeout (#4092) - SN no longer limits number of concurrent outgoing requests in SearchV2 handler (#4092) - Inter-node TLS certificates are verified against node keys (#4095) +- SNs exchange TLS certificates on inter-node connections (#4097) ### Removed - Compression support from FSTree (#4054) diff --git a/cmd/neofs-node/config.go b/cmd/neofs-node/config.go index cd52ddd0b4..e20fa3451e 100644 --- a/cmd/neofs-node/config.go +++ b/cmd/neofs-node/config.go @@ -409,9 +409,10 @@ func initCfg(appCfg *config.Config) *cfg { minConnTimeout := appCfg.APIClient.MinConnectionTime pingInterval := appCfg.APIClient.PingInterval pingTimeout := appCfg.APIClient.PingTimeout + getClientCertificate := clientCertificateProvider(appCfg.GRPC) newClientCache := func(scope string) *cache.Clients { return cache.NewClients(c.log.With(zap.String("scope", scope)), &buffers, streamTimeout, - minConnTimeout, pingInterval, pingTimeout, neofsecdsa.Signer(key.PrivateKey)) + minConnTimeout, pingInterval, pingTimeout, neofsecdsa.Signer(key.PrivateKey), getClientCertificate) } c.shared = shared{ basics: basicSharedConfig, diff --git a/cmd/neofs-node/grpc.go b/cmd/neofs-node/grpc.go index 10b4dfeeb5..ac862da20e 100644 --- a/cmd/neofs-node/grpc.go +++ b/cmd/neofs-node/grpc.go @@ -204,6 +204,7 @@ func buildSingleGRPCServer(c *cfg, sc grpcconfig.GRPC, maxRecvMsgSizeOpt grpc.Se } 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 new file mode 100644 index 0000000000..115fd56762 --- /dev/null +++ b/cmd/neofs-node/mtls.go @@ -0,0 +1,27 @@ +package main + +import ( + "crypto/tls" + "fmt" + + grpcconfig "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config/grpc" +) + +func clientCertificateProvider(cfgs []grpcconfig.GRPC) func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { + for i := range cfgs { + if !cfgs[i].TLS.Enabled { + continue + } + + certFile, keyFile := cfgs[i].TLS.Certificate, cfgs[i].TLS.Key + return func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return nil, fmt.Errorf("reload TLS client certificate: %w", err) + } + return &cert, nil + } + } + + return nil +} diff --git a/cmd/neofs-node/mtls_test.go b/cmd/neofs-node/mtls_test.go new file mode 100644 index 0000000000..eb9b897565 --- /dev/null +++ b/cmd/neofs-node/mtls_test.go @@ -0,0 +1,23 @@ +package main + +import ( + "testing" + + grpcconfig "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config/grpc" + "github.com/stretchr/testify/require" +) + +func TestClientCertificateProvider(t *testing.T) { + require.Nil(t, clientCertificateProvider(nil)) + + provider := clientCertificateProvider([]grpcconfig.GRPC{{ + TLS: grpcconfig.TLS{ + Enabled: true, + Certificate: "missing-certificate", + Key: "missing-key", + }, + }}) + require.NotNil(t, provider) + _, err := provider(nil) + require.ErrorContains(t, err, "reload TLS client certificate") +} diff --git a/pkg/network/cache/clients.go b/pkg/network/cache/clients.go index 14eef79314..76a5848e5a 100644 --- a/pkg/network/cache/clients.go +++ b/pkg/network/cache/clients.go @@ -45,9 +45,10 @@ type Clients struct { streamMsgTimeout time.Duration signBufPool *sync.Pool // gRPC settings - minConnTimeout time.Duration - pingInterval time.Duration - pingTimeout time.Duration + minConnTimeout time.Duration + pingInterval time.Duration + pingTimeout time.Duration + getClientCertificate func(*tls.CertificateRequestInfo) (*tls.Certificate, error) mtx sync.RWMutex conns map[string]*connections // keys are public key bytes @@ -57,16 +58,19 @@ type Clients struct { // NewClients constructs Clients initializing connection to any endpoint with // given parameters. -func NewClients(l *zap.Logger, signBufPool *sync.Pool, streamTimeout, minConnTimeout, pingInterval, pingTimeout time.Duration, signer neofscrypto.Signer) *Clients { +func NewClients(l *zap.Logger, signBufPool *sync.Pool, streamTimeout, minConnTimeout, pingInterval, pingTimeout time.Duration, signer neofscrypto.Signer, + getClientCertificate func(*tls.CertificateRequestInfo) (*tls.Certificate, error), +) *Clients { return &Clients{ - log: l, - streamMsgTimeout: streamTimeout, - signBufPool: signBufPool, - minConnTimeout: minConnTimeout, - pingInterval: pingInterval, - pingTimeout: pingTimeout, - conns: make(map[string]*connections), - signer: signer, + log: l, + streamMsgTimeout: streamTimeout, + signBufPool: signBufPool, + minConnTimeout: minConnTimeout, + pingInterval: pingInterval, + pingTimeout: pingTimeout, + getClientCertificate: getClientCertificate, + conns: make(map[string]*connections), + signer: signer, } } @@ -220,7 +224,7 @@ func (x *Clients) initConnection(ctx context.Context, pub []byte, uri string) (* if err != nil { return nil, fmt.Errorf("parse node public key: %w", err) } - transportCreds = credentials.NewTLS(newNodeTLSConfig((*ecdsa.PublicKey)(expectedKey))) + transportCreds = credentials.NewTLS(newNodeTLSConfig((*ecdsa.PublicKey)(expectedKey), x.getClientCertificate)) } else { transportCreds = insecure.NewCredentials() } @@ -262,9 +266,10 @@ func (x *Clients) initConnection(ctx context.Context, pub []byte, uri string) (* return res, nil } -func newNodeTLSConfig(expectedKey *ecdsa.PublicKey) *tls.Config { +func newNodeTLSConfig(expectedKey *ecdsa.PublicKey, getClientCertificate func(*tls.CertificateRequestInfo) (*tls.Certificate, error)) *tls.Config { return &tls.Config{ - InsecureSkipVerify: true, + InsecureSkipVerify: true, + GetClientCertificate: getClientCertificate, VerifyConnection: func(state tls.ConnectionState) error { if len(state.PeerCertificates) == 0 { return errors.New("server did not provide TLS certificate") diff --git a/pkg/network/cache/clients_internal_test.go b/pkg/network/cache/clients_internal_test.go index 93f288faa0..8f0efb004a 100644 --- a/pkg/network/cache/clients_internal_test.go +++ b/pkg/network/cache/clients_internal_test.go @@ -32,25 +32,35 @@ func TestNodeTLSConfig(t *testing.T) { require.NoError(t, err) t.Run("matching self-signed certificate", func(t *testing.T) { - cfg := newNodeTLSConfig(&expectedKey.PublicKey) + cfg := newNodeTLSConfig(&expectedKey.PublicKey, nil) require.True(t, cfg.InsecureSkipVerify) require.NoError(t, cfg.VerifyConnection(tls.ConnectionState{ PeerCertificates: []*x509.Certificate{newSelfSignedCertificate(t, expectedKey)}, })) }) + t.Run("client certificate", func(t *testing.T) { + expected := new(tls.Certificate) + cfg := newNodeTLSConfig(&expectedKey.PublicKey, func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { + return expected, nil + }) + actual, err := cfg.GetClientCertificate(nil) + require.NoError(t, err) + require.Same(t, expected, actual) + }) + t.Run("wrong key", func(t *testing.T) { key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) - err = newNodeTLSConfig(&expectedKey.PublicKey).VerifyConnection(tls.ConnectionState{ + err = newNodeTLSConfig(&expectedKey.PublicKey, nil).VerifyConnection(tls.ConnectionState{ PeerCertificates: []*x509.Certificate{newSelfSignedCertificate(t, key)}, }) require.ErrorIs(t, err, clientcore.ErrWrongPublicKey) }) t.Run("no certificate", func(t *testing.T) { - err := newNodeTLSConfig(&expectedKey.PublicKey).VerifyConnection(tls.ConnectionState{}) + err := newNodeTLSConfig(&expectedKey.PublicKey, nil).VerifyConnection(tls.ConnectionState{}) require.EqualError(t, err, "server did not provide TLS certificate") }) @@ -58,7 +68,7 @@ func TestNodeTLSConfig(t *testing.T) { key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err) - err = newNodeTLSConfig(&expectedKey.PublicKey).VerifyConnection(tls.ConnectionState{ + err = newNodeTLSConfig(&expectedKey.PublicKey, nil).VerifyConnection(tls.ConnectionState{ PeerCertificates: []*x509.Certificate{newSelfSignedCertificate(t, key)}, }) require.EqualError(t, err, "server TLS certificate has unsupported public key type *rsa.PublicKey") @@ -68,7 +78,7 @@ func TestNodeTLSConfig(t *testing.T) { key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) require.NoError(t, err) - err = newNodeTLSConfig(&expectedKey.PublicKey).VerifyConnection(tls.ConnectionState{ + err = newNodeTLSConfig(&expectedKey.PublicKey, nil).VerifyConnection(tls.ConnectionState{ PeerCertificates: []*x509.Certificate{newSelfSignedCertificate(t, key)}, }) require.EqualError(t, err, "server TLS certificate has unsupported elliptic curve P-384") diff --git a/pkg/network/peerauth/peerauth.go b/pkg/network/peerauth/peerauth.go new file mode 100644 index 0000000000..0846371f2a --- /dev/null +++ b/pkg/network/peerauth/peerauth.go @@ -0,0 +1,47 @@ +package peerauth + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/x509" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" +) + +// CertificatePublicKey returns the P-256 public key from cert in the +// compressed format. +func CertificatePublicKey(cert *x509.Certificate) ([]byte, error) { + pub, ok := cert.PublicKey.(*ecdsa.PublicKey) + if !ok { + return nil, fmt.Errorf("unsupported public key type %T", cert.PublicKey) + } + if pub.Curve != elliptic.P256() { + return nil, fmt.Errorf("unsupported elliptic curve %s", pub.Curve.Params().Name) + } + return (*keys.PublicKey)(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) ([]byte, error) { + p, ok := peer.FromContext(ctx) + if !ok { + return nil, nil + } + info, ok := p.AuthInfo.(credentials.TLSInfo) + if !ok { + return nil, nil + } + if len(info.State.PeerCertificates) == 0 { + return nil, nil + } + key, err := CertificatePublicKey(info.State.PeerCertificates[0]) + if err != nil { + return nil, fmt.Errorf("invalid TLS peer certificate: %w", err) + } + return key, nil +} diff --git a/pkg/network/peerauth/peerauth_test.go b/pkg/network/peerauth/peerauth_test.go new file mode 100644 index 0000000000..bfffc7ee91 --- /dev/null +++ b/pkg/network/peerauth/peerauth_test.go @@ -0,0 +1,74 @@ +package peerauth + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "math/big" + "testing" + "time" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" +) + +func TestCertificatePublicKey(t *testing.T) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + pub, err := CertificatePublicKey(newCertificate(t, key)) + require.NoError(t, err) + require.Equal(t, (*keys.PublicKey)(&key.PublicKey).Bytes(), pub) + + t.Run("unsupported key type", func(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + _, err = CertificatePublicKey(newCertificate(t, key)) + require.EqualError(t, err, "unsupported public key type *rsa.PublicKey") + }) + + t.Run("unsupported curve", func(t *testing.T) { + key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + require.NoError(t, err) + _, err = CertificatePublicKey(newCertificate(t, key)) + require.EqualError(t, err, "unsupported elliptic curve P-384") + }) +} + +func TestPeerPublicKey(t *testing.T) { + pub, err := PeerPublicKey(context.Background()) + require.NoError(t, err) + require.Nil(t, pub) + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + cert := newCertificate(t, key) + ctx := peer.NewContext(context.Background(), &peer.Peer{ + AuthInfo: credentials.TLSInfo{State: tls.ConnectionState{ + PeerCertificates: []*x509.Certificate{cert}, + }}, + }) + pub, err = PeerPublicKey(ctx) + require.NoError(t, err) + require.Equal(t, (*keys.PublicKey)(&key.PublicKey).Bytes(), pub) +} + +func newCertificate(t *testing.T, key crypto.Signer) *x509.Certificate { + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + } + der, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key) + require.NoError(t, err) + cert, err := x509.ParseCertificate(der) + require.NoError(t, err) + return cert +} diff --git a/pkg/services/object/acl/v2/service.go b/pkg/services/object/acl/v2/service.go index b37c1f71e0..363ae76828 100644 --- a/pkg/services/object/acl/v2/service.go +++ b/pkg/services/object/acl/v2/service.go @@ -1,6 +1,8 @@ package v2 import ( + "crypto/ecdsa" + "crypto/elliptic" "crypto/sha256" "errors" "fmt" @@ -9,6 +11,7 @@ import ( lru "github.com/hashicorp/golang-lru/v2" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" @@ -278,6 +281,28 @@ func getCredentialsFromSessionToken(token sessionv2.Token) (user.ID, []byte, err return token.OriginalIssuer(), key, nil } +func getCredentialsFromPeerPublicKey(key []byte) (user.ID, []byte, error) { + pub, err := keys.NewPublicKeyFromBytes(key, elliptic.P256()) + if err != nil { + return user.ID{}, nil, fmt.Errorf("invalid peer public key: %w", err) + } + + return user.NewFromECDSAPublicKey(ecdsa.PublicKey(*pub)), key, nil +} + +func getRequestCredentials(tokens common.RequestTokens, verifyHeader *protosession.RequestVerificationHeader) (user.ID, []byte, error) { + if tokens.AuthenticatedPeerPublicKey != nil { + return getCredentialsFromPeerPublicKey(tokens.AuthenticatedPeerPublicKey) + } + if tokens.Session != nil { + return getCredentialsFromSessionToken(*tokens.Session) + } + if tokens.SessionV1 != nil { + return getCredentialsFromSessionV1Token(*tokens.SessionV1) + } + return icrypto.GetRequestAuthor(verifyHeader) +} + type sessionTokenV2WithEncodedBody struct { sessionv2.Token body []byte @@ -480,19 +505,8 @@ func (b Service) PutRequestToInfo(request *protoobject.PutRequest, initPart *pro func (b Service) findRequestInfo(req interface { GetVerifyHeader() *protosession.RequestVerificationHeader }, idCnr cid.ID, op acl.Op, tokens common.RequestTokens) (RequestInfo, error) { - var ( - info RequestInfo - reqAuthor user.ID - reqAuthorPub []byte - err error - ) - if tokens.Session != nil { - reqAuthor, reqAuthorPub, err = getCredentialsFromSessionToken(*tokens.Session) - } else if tokens.SessionV1 != nil { - reqAuthor, reqAuthorPub, err = getCredentialsFromSessionV1Token(*tokens.SessionV1) - } else { - reqAuthor, reqAuthorPub, err = icrypto.GetRequestAuthor(req.GetVerifyHeader()) - } + var info RequestInfo + reqAuthor, reqAuthorPub, err := getRequestCredentials(tokens, req.GetVerifyHeader()) if err != nil { return info, fmt.Errorf("get request author: %w", err) } diff --git a/pkg/services/object/acl/v2/service_internal_test.go b/pkg/services/object/acl/v2/service_internal_test.go index 1a8897828f..fecbc3aaf5 100644 --- a/pkg/services/object/acl/v2/service_internal_test.go +++ b/pkg/services/object/acl/v2/service_internal_test.go @@ -5,7 +5,9 @@ import ( "time" "github.com/google/uuid" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" isessions "github.com/nspcc-dev/neofs-node/internal/sessions" + "github.com/nspcc-dev/neofs-node/pkg/services/object/common" "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" @@ -15,6 +17,7 @@ import ( oid "github.com/nspcc-dev/neofs-sdk-go/object/id" "github.com/nspcc-dev/neofs-sdk-go/session" sessionv2 "github.com/nspcc-dev/neofs-sdk-go/session/v2" + "github.com/nspcc-dev/neofs-sdk-go/user" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/stretchr/testify/require" ) @@ -159,3 +162,42 @@ func BenchmarkSessionTokenV2Verification(b *testing.B) { s.ResetTokenCheckCache() } } + +func TestGetCredentialsFromPeerPublicKey(t *testing.T) { + key, err := keys.NewPrivateKey() + require.NoError(t, err) + pub := key.PublicKey().Bytes() + + usr, actualPub, err := getCredentialsFromPeerPublicKey(pub) + require.NoError(t, err) + require.Equal(t, user.NewFromECDSAPublicKey(key.PrivateKey.PublicKey), usr) + require.Equal(t, pub, actualPub) + + _, _, err = getCredentialsFromPeerPublicKey([]byte("invalid")) + require.ErrorContains(t, err, "invalid peer public key") +} + +func TestGetRequestCredentialsPeerPriority(t *testing.T) { + key, err := keys.NewPrivateKey() + require.NoError(t, err) + pub := key.PublicKey().Bytes() + expectedUser := user.NewFromECDSAPublicKey(key.PrivateKey.PublicKey) + + for name, tokens := range map[string]common.RequestTokens{ + "V1 session": { + SessionV1: new(session.Object), + AuthenticatedPeerPublicKey: pub, + }, + "V2 session": { + Session: new(sessionv2.Token), + AuthenticatedPeerPublicKey: pub, + }, + } { + t.Run(name, func(t *testing.T) { + actualUser, actualPub, err := getRequestCredentials(tokens, nil) + require.NoError(t, err) + require.Equal(t, expectedUser, actualUser) + require.Equal(t, pub, actualPub) + }) + } +} diff --git a/pkg/services/object/common/request.go b/pkg/services/object/common/request.go index f3dc53671b..919feeb94d 100644 --- a/pkg/services/object/common/request.go +++ b/pkg/services/object/common/request.go @@ -11,4 +11,8 @@ type RequestTokens struct { Session *sessionv2.Token SessionV1 *session.Object Bearer *bearer.Token + + // AuthenticatedPeerPublicKey is the compressed ECDSA public key of an + // inter-node TLS peer authenticated by the transport layer. + AuthenticatedPeerPublicKey []byte } diff --git a/pkg/services/object/server.go b/pkg/services/object/server.go index 54aa6a6315..3f9303807f 100644 --- a/pkg/services/object/server.go +++ b/pkg/services/object/server.go @@ -24,6 +24,7 @@ import ( containercore "github.com/nspcc-dev/neofs-node/pkg/core/container" netmapcore "github.com/nspcc-dev/neofs-node/pkg/core/netmap" objectcore "github.com/nspcc-dev/neofs-node/pkg/core/object" + "github.com/nspcc-dev/neofs-node/pkg/network/peerauth" metasvc "github.com/nspcc-dev/neofs-node/pkg/services/meta" aclsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/v2" "github.com/nspcc-dev/neofs-node/pkg/services/object/common" @@ -446,9 +447,11 @@ func (s *Server) Put(gStream protoobject.ObjectService_PutServer) error { s.metrics.AddPutPayload(len(c)) } - if err = icrypto.VerifyRequestSignaturesN3(req, s.fsChain); err != nil { - err = s.sendStatusPutResponse(gStream, err, reqFirst) // assign for defer - return err + if requestNeedsSignature(gStream.Context(), req) { + if err = icrypto.VerifyRequestSignaturesN3(req, s.fsChain); err != nil { + err = s.sendStatusPutResponse(gStream, err, reqFirst) // assign for defer + return err + } } if s.fsChain.LocalNodeUnderMaintenance() { @@ -515,7 +518,7 @@ func (s *Server) Put(gStream protoobject.ObjectService_PutServer) error { return s.sendStatusPutResponse(gStream, err, reqFirst) } - if reqInfo, objOwner, err := s.reqInfoProc.PutRequestToInfo(req, initPart, cnrID, op, reqMD.tokens); err != nil { + if reqInfo, objOwner, err := s.reqInfoProc.PutRequestToInfo(req, initPart, cnrID, op, requestTokensWithPeer(gStream.Context(), req, reqMD.tokens)); err != nil { if !errors.Is(err, aclsvc.ErrSkipRequest) { if !errors.Is(err, apistatus.Error) { err = newBadRequestError(err.Error()) // defer @@ -566,8 +569,10 @@ func (s *Server) Delete(ctx context.Context, req *protoobject.DeleteRequest) (*p ) defer func() { s.pushOpExecResult(stat.MethodObjectDelete, err, t) }() - if err = icrypto.VerifyRequestSignaturesN3(req, s.fsChain); err != nil { - return s.makeStatusDeleteResponse(err, req), nil + if requestNeedsSignature(ctx, req) { + if err = icrypto.VerifyRequestSignaturesN3(req, s.fsChain); err != nil { + return s.makeStatusDeleteResponse(err, req), nil + } } if s.fsChain.LocalNodeUnderMaintenance() { @@ -591,7 +596,7 @@ func (s *Server) Delete(ctx context.Context, req *protoobject.DeleteRequest) (*p return s.makeStatusDeleteResponse(err, req), nil } - reqInfo, err := s.reqInfoProc.DeleteRequestToInfo(req, cnrID, reqMD.tokens) + reqInfo, err := s.reqInfoProc.DeleteRequestToInfo(req, cnrID, requestTokensWithPeer(ctx, req, reqMD.tokens)) if err != nil { if !errors.Is(err, apistatus.Error) { err = newBadRequestError(err.Error()) // defer @@ -667,8 +672,10 @@ func (s *Server) HeadBuffered(ctx context.Context, req *protoobject.HeadRequest) needSignResp := needSignGetResponse(req) - if err := icrypto.VerifyRequestSignaturesN3(req, s.fsChain); err != nil { - return s.makeStatusHeadResponse(err, needSignResp) + if requestNeedsSignature(ctx, req) { + if err := icrypto.VerifyRequestSignaturesN3(req, s.fsChain); err != nil { + return s.makeStatusHeadResponse(err, needSignResp) + } } if s.fsChain.LocalNodeUnderMaintenance() { @@ -692,7 +699,7 @@ func (s *Server) HeadBuffered(ctx context.Context, req *protoobject.HeadRequest) return s.makeStatusHeadResponse(err, needSignResp) } - reqInfo, err := s.reqInfoProc.HeadRequestToInfo(req, cnrID, reqMD.tokens) + reqInfo, err := s.reqInfoProc.HeadRequestToInfo(req, cnrID, requestTokensWithPeer(ctx, req, reqMD.tokens)) if err != nil { if !errors.Is(err, apistatus.Error) { err = newBadRequestError(err.Error()) // defer @@ -1011,8 +1018,10 @@ func (s *Server) Get(req *protoobject.GetRequest, gStream protoobject.ObjectServ needSignResp := needSignGetResponse(req) - if err = icrypto.VerifyRequestSignatures(req); err != nil { - return s.sendStatusGetResponse(gStream, err, needSignResp) + if requestNeedsSignature(gStream.Context(), req) { + if err = icrypto.VerifyRequestSignatures(req); err != nil { + return s.sendStatusGetResponse(gStream, err, needSignResp) + } } if s.fsChain.LocalNodeUnderMaintenance() { @@ -1036,7 +1045,7 @@ func (s *Server) Get(req *protoobject.GetRequest, gStream protoobject.ObjectServ return s.sendStatusGetResponse(gStream, err, needSignResp) } - reqInfo, err := s.reqInfoProc.GetRequestToInfo(req, cnrID, reqMD.tokens) + reqInfo, err := s.reqInfoProc.GetRequestToInfo(req, cnrID, requestTokensWithPeer(gStream.Context(), req, reqMD.tokens)) if err != nil { if !errors.Is(err, apistatus.Error) { err = newBadRequestError(err.Error()) // defer @@ -1446,8 +1455,10 @@ func (s *Server) GetRange(req *protoobject.GetRangeRequest, gStream protoobject. t = time.Now() ) defer func() { s.pushOpExecResult(stat.MethodObjectRange, err, t) }() - if err = icrypto.VerifyRequestSignaturesN3(req, s.fsChain); err != nil { - return s.sendStatusRangeResponse(gStream, err, req) + if requestNeedsSignature(gStream.Context(), req) { + if err = icrypto.VerifyRequestSignaturesN3(req, s.fsChain); err != nil { + return s.sendStatusRangeResponse(gStream, err, req) + } } if s.fsChain.LocalNodeUnderMaintenance() { @@ -1471,7 +1482,7 @@ func (s *Server) GetRange(req *protoobject.GetRangeRequest, gStream protoobject. return s.sendStatusRangeResponse(gStream, err, req) } - reqInfo, err := s.reqInfoProc.RangeRequestToInfo(req, cnrID, reqMD.tokens) + reqInfo, err := s.reqInfoProc.RangeRequestToInfo(req, cnrID, requestTokensWithPeer(gStream.Context(), req, reqMD.tokens)) if err != nil { if !errors.Is(err, apistatus.Error) { err = newBadRequestError(err.Error()) // defer @@ -1842,8 +1853,10 @@ func (s *Server) SearchV2Buffered(ctx context.Context, req *protoobject.SearchV2 t = time.Now() ) defer s.pushOpExecResult(stat.MethodObjectSearchV2, err, t) - if err = icrypto.VerifyRequestSignaturesN3(req, s.fsChain); err != nil { - return s.signSearchResponse(nil, err, req) + if requestNeedsSignature(ctx, req) { + if err = icrypto.VerifyRequestSignaturesN3(req, s.fsChain); err != nil { + return s.signSearchResponse(nil, err, req) + } } if s.fsChain.LocalNodeUnderMaintenance() { @@ -1867,7 +1880,7 @@ func (s *Server) SearchV2Buffered(ctx context.Context, req *protoobject.SearchV2 return s.signSearchResponse(nil, err, req) } - reqInfo, err := s.reqInfoProc.SearchV2RequestToInfo(req, cnrID, reqMD.tokens) + reqInfo, err := s.reqInfoProc.SearchV2RequestToInfo(req, cnrID, requestTokensWithPeer(ctx, req, reqMD.tokens)) if err != nil { if !errors.Is(err, apistatus.Error) { err = newBadRequestError(err.Error()) // defer @@ -2301,6 +2314,35 @@ func needSignGetResponse(req util.Request) bool { return util.VersionLE(req, 2, 17) } +func authenticatedPeerPublicKey(ctx context.Context) ([]byte, bool) { + key, err := peerauth.PeerPublicKey(ctx) + return key, err == nil && key != nil +} + +type requestWithVerificationHeader interface { + util.Request + GetVerifyHeader() *protosession.RequestVerificationHeader +} + +func requestNeedsSignature(ctx context.Context, req requestWithVerificationHeader) bool { + if req.GetVerifyHeader() != nil { + return true + } + meta := req.GetMetaHeader() + if meta == nil || meta.GetTtl() > 1 { + return true + } + _, ok := authenticatedPeerPublicKey(ctx) + return !ok +} + +func requestTokensWithPeer(ctx context.Context, req requestWithVerificationHeader, tokens common.RequestTokens) common.RequestTokens { + if !requestNeedsSignature(ctx, req) { + tokens.AuthenticatedPeerPublicKey, _ = authenticatedPeerPublicKey(ctx) + } + return tokens +} + func checkHeaderProtobufAgainstID(buffers iprotobuf.BuffersSlice, id oid.ID, ordered bool) error { b := buffers.ReadOnlyData() if !ordered { diff --git a/pkg/services/object/server_internal_test.go b/pkg/services/object/server_internal_test.go new file mode 100644 index 0000000000..5dcf332474 --- /dev/null +++ b/pkg/services/object/server_internal_test.go @@ -0,0 +1,57 @@ +package object + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "testing" + + "github.com/nspcc-dev/neofs-node/pkg/network/peerauth" + "github.com/nspcc-dev/neofs-node/pkg/services/object/common" + protoobject "github.com/nspcc-dev/neofs-sdk-go/proto/object" + protosession "github.com/nspcc-dev/neofs-sdk-go/proto/session" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" +) + +func TestRequestNeedsSignature(t *testing.T) { + ctx, expectedKey := tlsPeerContext(t) + + require.True(t, requestNeedsSignature(context.Background(), requestWithTTL(1))) + require.True(t, requestNeedsSignature(ctx, requestWithTTL(2))) + require.True(t, requestNeedsSignature(ctx, new(protoobject.GetRequest))) + require.False(t, requestNeedsSignature(ctx, requestWithTTL(1))) + + signedReq := requestWithTTL(1) + signedReq.VerifyHeader = new(protosession.RequestVerificationHeader) + require.True(t, requestNeedsSignature(ctx, signedReq)) + + tokens := requestTokensWithPeer(ctx, requestWithTTL(1), common.RequestTokens{}) + require.Equal(t, expectedKey, tokens.AuthenticatedPeerPublicKey) + tokens = requestTokensWithPeer(ctx, requestWithTTL(2), common.RequestTokens{}) + require.Nil(t, tokens.AuthenticatedPeerPublicKey) + tokens = requestTokensWithPeer(ctx, signedReq, common.RequestTokens{}) + require.Nil(t, tokens.AuthenticatedPeerPublicKey) +} + +func requestWithTTL(ttl uint32) *protoobject.GetRequest { + return &protoobject.GetRequest{MetaHeader: &protosession.RequestMetaHeader{Ttl: ttl}} +} + +func tlsPeerContext(t *testing.T) (context.Context, []byte) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + cert := &x509.Certificate{PublicKey: &key.PublicKey} + pub, err := peerauth.CertificatePublicKey(cert) + require.NoError(t, err) + ctx := peer.NewContext(context.Background(), &peer.Peer{ + AuthInfo: credentials.TLSInfo{State: tls.ConnectionState{ + PeerCertificates: []*x509.Certificate{cert}, + }}, + }) + return ctx, pub +}