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:
JT Olio 2019-10-12 14:34:41 -06:00 committed by GitHub
parent dd21953fd6
commit 6ede140df1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 152 additions and 2 deletions

View File

@ -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
}
}

View File

@ -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
View 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
View 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")
}

View File

@ -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"`

View File

@ -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)
}