Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion cmd/neofs-node/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions cmd/neofs-node/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
})
Expand Down
27 changes: 27 additions & 0 deletions cmd/neofs-node/mtls.go
Original file line number Diff line number Diff line change
@@ -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
}
23 changes: 23 additions & 0 deletions cmd/neofs-node/mtls_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
35 changes: 20 additions & 15 deletions pkg/network/cache/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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")
Expand Down
20 changes: 15 additions & 5 deletions pkg/network/cache/clients_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,33 +32,43 @@ 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")
})

t.Run("unsupported key type", func(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")
Expand All @@ -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")
Expand Down
47 changes: 47 additions & 0 deletions pkg/network/peerauth/peerauth.go
Original file line number Diff line number Diff line change
@@ -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
}
74 changes: 74 additions & 0 deletions pkg/network/peerauth/peerauth_test.go
Original file line number Diff line number Diff line change
@@ -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
}
40 changes: 27 additions & 13 deletions pkg/services/object/acl/v2/service.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package v2

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha256"
"errors"
"fmt"
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
Loading
Loading