pkg/rpc: defeat MITM attacks in most cases (#3215)
This change adds a trusted registry (via the source code) of node address to node id mappings (currently only for well known Satellites) to defeat MITM attacks to Satellites. It also extends the uplink UI such that when entering a satellite address by hand, a node id prefix can also be added to defeat MITM attacks with unknown satellites. When running uplink setup, satellite addresses can now be of the form 12EayRS2V1k@us-central-1.tardigrade.io (not even using a full node id) to ensure that the peer contacted is the peer that was expected. When using a known satellite address, the known node ids are used if no override is provided.
This commit is contained in:
parent
dd21953fd6
commit
6ede140df1
@ -6,6 +6,7 @@ package tlsopts
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
@ -48,6 +49,12 @@ func (opts *Options) ClientTLSConfig(id storj.NodeID) *tls.Config {
|
||||
return opts.tlsConfig(false, verifyIdentity(id))
|
||||
}
|
||||
|
||||
// ClientTLSConfigPrefix returns a TSLConfig for use as a client in handshaking with a peer.
|
||||
// The peer node id is validated to match the given prefix
|
||||
func (opts *Options) ClientTLSConfigPrefix(idPrefix string) *tls.Config {
|
||||
return opts.tlsConfig(false, verifyIdentityPrefix(idPrefix))
|
||||
}
|
||||
|
||||
// UnverifiedClientTLSConfig returns a TLSConfig for use as a client in handshaking with
|
||||
// an unknown peer.
|
||||
func (opts *Options) UnverifiedClientTLSConfig() *tls.Config {
|
||||
@ -106,3 +113,19 @@ func verifyIdentity(id storj.NodeID) peertls.PeerCertVerificationFunc {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func verifyIdentityPrefix(idPrefix string) peertls.PeerCertVerificationFunc {
|
||||
return func(_ [][]byte, parsedChains [][]*x509.Certificate) (err error) {
|
||||
defer mon.TaskNamed("verifyIdentityPrefix")(nil)(&err)
|
||||
peer, err := identity.PeerIdentityFromChain(parsedChains[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(peer.ID.String(), idPrefix) {
|
||||
return Error.New("peer ID did not match requested ID prefix")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,11 @@ package rpc
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/storj/internal/memory"
|
||||
"storj.io/storj/pkg/pb"
|
||||
"storj.io/storj/pkg/peertls/tlsopts"
|
||||
@ -111,6 +114,54 @@ func (d Dialer) DialAddressID(ctx context.Context, address string, id storj.Node
|
||||
return d.dial(ctx, address, d.TLSOptions.ClientTLSConfig(id))
|
||||
}
|
||||
|
||||
// DialAddressInsecureBestEffort is like DialAddressInsecure but tries to dial a node securely if
|
||||
// it can.
|
||||
//
|
||||
// nodeURL is like a storj.NodeURL but (a) requires an address and (b) does not require a
|
||||
// full node id and will work with just a node prefix. The format is either:
|
||||
// * node_host:node_port
|
||||
// * node_id_prefix@node_host:node_port
|
||||
// Examples:
|
||||
// * 33.20.0.1:7777
|
||||
// * [2001:db8:1f70::999:de8:7648:6e8]:7777
|
||||
// * 12vha9oTFnerx@33.20.0.1:7777
|
||||
// * 12vha9oTFnerx@[2001:db8:1f70::999:de8:7648:6e8]:7777
|
||||
//
|
||||
// DialAddressInsecureBestEffort:
|
||||
// * will use a node id if provided in the nodeURL paramenter
|
||||
// * will otherwise look up the node address in a known map of node address to node ids and use
|
||||
// the remembered node id.
|
||||
// * will otherwise dial insecurely
|
||||
func (d Dialer) DialAddressInsecureBestEffort(ctx context.Context, nodeURL string) (_ *Conn, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
if d.TLSOptions == nil {
|
||||
return nil, Error.New("tls options not set when required for this dial")
|
||||
}
|
||||
|
||||
var nodeIDPrefix, nodeAddress string
|
||||
parts := strings.Split(nodeURL, "@")
|
||||
switch len(parts) {
|
||||
default:
|
||||
return nil, Error.New("malformed node url: %q", nodeURL)
|
||||
case 1:
|
||||
nodeAddress = parts[0]
|
||||
case 2:
|
||||
nodeIDPrefix, nodeAddress = parts[0], parts[1]
|
||||
}
|
||||
|
||||
if len(nodeIDPrefix) > 0 {
|
||||
return d.dial(ctx, nodeAddress, d.TLSOptions.ClientTLSConfigPrefix(nodeIDPrefix))
|
||||
}
|
||||
|
||||
if nodeID, found := KnownNodeID(nodeAddress); found {
|
||||
return d.dial(ctx, nodeAddress, d.TLSOptions.ClientTLSConfig(nodeID))
|
||||
}
|
||||
|
||||
zap.S().Warnf("unknown node id for address %q: please specify node id in form node_id@node_host:node_port for added security", nodeAddress)
|
||||
return d.dial(ctx, nodeAddress, d.TLSOptions.UnverifiedClientTLSConfig())
|
||||
}
|
||||
|
||||
// DialAddressInsecure dials to the specified address and does not check the node id.
|
||||
func (d Dialer) DialAddressInsecure(ctx context.Context, address string) (_ *Conn, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
50
pkg/rpc/known_ids.go
Normal file
50
pkg/rpc/known_ids.go
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"storj.io/storj/pkg/storj"
|
||||
)
|
||||
|
||||
var (
|
||||
knownNodeIDs = map[string]storj.NodeID{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
for _, nodeURL := range []string{
|
||||
"12EayRS2V1kEsWESU9QMRseFhdxYxKicsiFmxrsLZHeLUtdps3S@us-central-1.tardigrade.io:7777",
|
||||
"12EayRS2V1kEsWESU9QMRseFhdxYxKicsiFmxrsLZHeLUtdps3S@mars.tardigrade.io:7777",
|
||||
"121RTSDpyNZVcEU84Ticf2L1ntiuUimbWgfATz21tuvgk3vzoA6@asia-east-1.tardigrade.io:7777",
|
||||
"121RTSDpyNZVcEU84Ticf2L1ntiuUimbWgfATz21tuvgk3vzoA6@saturn.tardigrade.io:7777",
|
||||
"12L9ZFwhzVpuEKMUNUqkaTLGzwY9G24tbiigLiXpmZWKwmcNDDs@europe-west-1.tardigrade.io:7777",
|
||||
"12L9ZFwhzVpuEKMUNUqkaTLGzwY9G24tbiigLiXpmZWKwmcNDDs@jupiter.tardigrade.io:7777",
|
||||
"118UWpMCHzs6CvSgWd9BfFVjw5K9pZbJjkfZJexMtSkmKxvvAW@satellite.stefan-benten.de:7777",
|
||||
} {
|
||||
url, err := storj.ParseNodeURL(nodeURL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
knownNodeIDs[url.Address] = url.ID
|
||||
host, _, err := net.SplitHostPort(url.Address)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
knownNodeIDs[host] = url.ID
|
||||
}
|
||||
}
|
||||
|
||||
// KnownNodeID looks for a well-known node id for a given address
|
||||
func KnownNodeID(address string) (id storj.NodeID, known bool) {
|
||||
id, known = knownNodeIDs[address]
|
||||
if !known {
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return id, false
|
||||
}
|
||||
id, known = knownNodeIDs[host]
|
||||
}
|
||||
return id, known
|
||||
}
|
26
pkg/rpc/known_ids_test.go
Normal file
26
pkg/rpc/known_ids_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestKnownID(t *testing.T) {
|
||||
id, ok := KnownNodeID("us-central-1.tardigrade.io:7777")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, id.String(), "12EayRS2V1kEsWESU9QMRseFhdxYxKicsiFmxrsLZHeLUtdps3S")
|
||||
_, ok = KnownNodeID("non-existent.example.com:7777")
|
||||
require.False(t, ok)
|
||||
|
||||
id, ok = KnownNodeID("us-central-1.tardigrade.io:10000")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, id.String(), "12EayRS2V1kEsWESU9QMRseFhdxYxKicsiFmxrsLZHeLUtdps3S")
|
||||
|
||||
id, ok = KnownNodeID("us-central-1.tardigrade.io")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, id.String(), "12EayRS2V1kEsWESU9QMRseFhdxYxKicsiFmxrsLZHeLUtdps3S")
|
||||
}
|
@ -48,7 +48,7 @@ var _ pb.PiecestoreServer = (*Endpoint)(nil)
|
||||
// OldConfig contains everything necessary for a server
|
||||
type OldConfig struct {
|
||||
Path string `help:"path to store data in" default:"$CONFDIR/storage"`
|
||||
WhitelistedSatellites storj.NodeURLs `help:"a comma-separated list of approved satellite node urls" devDefault:"" releaseDefault:"12EayRS2V1kEsWESU9QMRseFhdxYxKicsiFmxrsLZHeLUtdps3S@mars.tardigrade.io:7777,118UWpMCHzs6CvSgWd9BfFVjw5K9pZbJjkfZJexMtSkmKxvvAW@satellite.stefan-benten.de:7777,121RTSDpyNZVcEU84Ticf2L1ntiuUimbWgfATz21tuvgk3vzoA6@saturn.tardigrade.io:7777,12L9ZFwhzVpuEKMUNUqkaTLGzwY9G24tbiigLiXpmZWKwmcNDDs@jupiter.tardigrade.io:7777"`
|
||||
WhitelistedSatellites storj.NodeURLs `help:"a comma-separated list of approved satellite node urls" devDefault:"" releaseDefault:"12EayRS2V1kEsWESU9QMRseFhdxYxKicsiFmxrsLZHeLUtdps3S@us-central-1.tardigrade.io:7777,118UWpMCHzs6CvSgWd9BfFVjw5K9pZbJjkfZJexMtSkmKxvvAW@satellite.stefan-benten.de:7777,121RTSDpyNZVcEU84Ticf2L1ntiuUimbWgfATz21tuvgk3vzoA6@asia-east-1.tardigrade.io:7777,12L9ZFwhzVpuEKMUNUqkaTLGzwY9G24tbiigLiXpmZWKwmcNDDs@europe-west-1.tardigrade.io:7777"`
|
||||
AllocatedDiskSpace memory.Size `user:"true" help:"total allocated disk space in bytes" default:"1TB"`
|
||||
AllocatedBandwidth memory.Size `user:"true" help:"total allocated bandwidth in bytes" default:"2TB"`
|
||||
KBucketRefreshInterval time.Duration `help:"how frequently Kademlia bucket should be refreshed with node stats" default:"1h0m0s"`
|
||||
|
@ -52,7 +52,7 @@ func New(client rpc.MetainfoClient, apiKey *macaroon.APIKey) *Client {
|
||||
|
||||
// Dial dials to metainfo endpoint with the specified api key.
|
||||
func Dial(ctx context.Context, dialer rpc.Dialer, address string, apiKey *macaroon.APIKey) (*Client, error) {
|
||||
conn, err := dialer.DialAddressInsecure(ctx, address)
|
||||
conn, err := dialer.DialAddressInsecureBestEffort(ctx, address)
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user