Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add NetworkCookie option for isolating testnets #2658

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion config/config.go
Expand Up @@ -128,6 +128,8 @@ type Config struct {
DialRanker network.DialRanker

SwarmOpts []swarm.Option

NetworkCookie crypto.NetworkCookie
Copy link
Contributor

@Jorropo Jorropo Dec 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should rebrand this as PNET v2 (and make a specification).
They both seems to achieve the same thing, I havn't red the code enough to know if NetworkCookie is private information, but that doesn't sounds very hard to ensure we never leak the cookie.


This wouldn't solve the problems raised by @Stebalien however.

}

func (cfg *Config) makeSwarm(eventBus event.Bus, enableMetrics bool) (*swarm.Swarm, error) {
Expand Down Expand Up @@ -203,7 +205,9 @@ func (cfg *Config) addTransports(h host.Host) error {
fx.Supply(cfg.Muxers),
fx.Supply(h.ID()),
fx.Provide(func() host.Host { return h }),
fx.Provide(func() crypto.PrivKey { return h.Peerstore().PrivKey(h.ID()) }),
fx.Provide(func() crypto.PrivKey {
return crypto.AddNetworkCookieToPrivKey(h.Peerstore().PrivKey(h.ID()), cfg.NetworkCookie)
}),
fx.Provide(func() connmgr.ConnectionGater { return cfg.ConnectionGater }),
fx.Provide(func() pnet.PSK { return cfg.PSK }),
fx.Provide(func() network.ResourceManager { return cfg.ResourceManager }),
Expand Down
82 changes: 82 additions & 0 deletions core/crypto/netcookie.go
@@ -0,0 +1,82 @@
package crypto

import (
"bytes"
"encoding/hex"
"errors"
"fmt"
)

// NetworkCookie provides a way for preventing peers from different
// networks from connecting to each other
type NetworkCookie []byte

// Empty returns true if the network cookie is empty
func (nc NetworkCookie) Empty() bool {
return len(nc) == 0
}

// String returns string representation of the NetworkCookie, which is
// a hex string
func (nc NetworkCookie) String() string {
return hex.EncodeToString(nc)
}

// Equal returns true if this cookie is the same as the other cookie
func (nc NetworkCookie) Equal(other NetworkCookie) bool {
return bytes.Equal(nc, other)
}

// ParseNetworkCookie parses a hex string into a NetworkCookie
func ParseNetworkCookie(s string) (NetworkCookie, error) {
if s == "" {
return nil, nil
}
parsed, err := hex.DecodeString(s)
if err != nil {
return nil, fmt.Errorf("error decoding network cookie hex string: %w", err)
}
if len(parsed) > 255 {
return nil, errors.New("network cookie string too long")
}
return parsed, nil
}

type privKeyWithCookie struct {
PrivKey
nc NetworkCookie
}

// AddCookieToPrivKey adds network cookie to private key
// If nc is an empty NetworkCookie, the function just returns pk
func AddNetworkCookieToPrivKey(pk PrivKey, nc NetworkCookie) PrivKey {
if nc.Empty() {
return pk
}

return &privKeyWithCookie{
PrivKey: pk,
nc: nc,
}
}

// StripNetworkCookieFromPrivKey removes network cookie from the
// private key, if it's present
func StripNetworkCookieFromPrivKey(pk PrivKey) PrivKey {
if pkc, ok := pk.(*privKeyWithCookie); ok {
return pkc.PrivKey
}

return pk
}

// NetworkCookieFromPrivKey extracts network cookie from PrivKey,
// if it's present there. If it's not, the function returns an empty
// NetworkCookie
func NetworkCookieFromPrivKey(pk PrivKey) NetworkCookie {
if pkc, ok := pk.(*privKeyWithCookie); ok {
return pkc.nc
}

return nil
}
60 changes: 60 additions & 0 deletions core/crypto/netcookie_test.go
@@ -0,0 +1,60 @@
package crypto

import (
"crypto/rand"
"testing"
)

func TestNetworkCookie(t *testing.T) {
ncA, err := ParseNetworkCookie("001234")
if err != nil {
t.Fatalf("error parsing network cookie: %v", err)
}
if ncA.String() != "001234" {
t.Errorf("bad network cookie string")
}
ncB, err := ParseNetworkCookie("234567")
if err != nil {
t.Fatalf("error parsing network cookie: %v", err)
}
ncEmpty, err := ParseNetworkCookie("")
if err != nil {
t.Fatalf("error parsing empty network cookie: %v", err)
}

if ncA.Empty() || ncB.Empty() {
t.Errorf("non-empty network cookie reported as empty")
}
if !ncEmpty.Empty() {
t.Errorf("empty network cookie reported as non-empty")
}

if ncA.Equal(ncB) || ncA.Equal(ncEmpty) {
t.Errorf("non-equal cookies are reported as equal")
}

if !ncA.Equal(ncA) || !ncEmpty.Equal(nil) || !ncEmpty.Equal([]byte{}) || !ncA.Equal([]byte{0, 0x12, 0x34}) {
t.Errorf("equal cookies are reported as non-equal")
}

priv, _, err := GenerateECDSAKeyPair(rand.Reader)
if err != nil {
t.Fatalf("error generating key: %v", err)
}

if !NetworkCookieFromPrivKey(priv).Empty() {
t.Errorf("unexpected non-empty network cookie from priv key")
}

priv1 := AddNetworkCookieToPrivKey(priv, ncA)
if !NetworkCookieFromPrivKey(priv1).Equal(ncA) {
t.Errorf("bad network cookie from priv key")
}
if StripNetworkCookieFromPrivKey(priv1) != priv {
t.Errorf("StripNetworkCookieFromPrivKey didn't return the original key")
}

if AddNetworkCookieToPrivKey(priv, ncEmpty) != priv {
t.Errorf("adding empty network cookie shouldn't change the key")
}
}
17 changes: 13 additions & 4 deletions core/sec/insecure/insecure.go
Expand Up @@ -67,6 +67,7 @@ func (t *Transport) SecureInbound(_ context.Context, insecure net.Conn, p peer.I
Conn: insecure,
local: t.id,
localPubKey: t.key.GetPublic(),
nc: ci.NetworkCookieFromPrivKey(t.key),
}

if err := conn.runHandshakeSync(); err != nil {
Expand All @@ -93,6 +94,7 @@ func (t *Transport) SecureOutbound(_ context.Context, insecure net.Conn, p peer.
Conn: insecure,
local: t.id,
localPubKey: t.key.GetPublic(),
nc: ci.NetworkCookieFromPrivKey(t.key),
}

if err := conn.runHandshakeSync(); err != nil {
Expand All @@ -115,9 +117,10 @@ type Conn struct {

local, remote peer.ID
localPubKey, remotePubKey ci.PubKey
nc ci.NetworkCookie
}

func makeExchangeMessage(pubkey ci.PubKey) (*pb.Exchange, error) {
func makeExchangeMessage(pubkey ci.PubKey, netCookie ci.NetworkCookie) (*pb.Exchange, error) {
keyMsg, err := ci.PublicKeyToProto(pubkey)
if err != nil {
return nil, err
Expand All @@ -128,8 +131,9 @@ func makeExchangeMessage(pubkey ci.PubKey) (*pb.Exchange, error) {
}

return &pb.Exchange{
Id: []byte(id),
Pubkey: keyMsg,
Id: []byte(id),
Pubkey: keyMsg,
NetCookie: netCookie,
}, nil
}

Expand All @@ -140,7 +144,7 @@ func (ic *Conn) runHandshakeSync() error {
}

// Generate an Exchange message
msg, err := makeExchangeMessage(ic.localPubKey)
msg, err := makeExchangeMessage(ic.localPubKey, ic.nc)
if err != nil {
return err
}
Expand Down Expand Up @@ -169,6 +173,11 @@ func (ic *Conn) runHandshakeSync() error {
remoteID, calculatedID)
}

if !ic.nc.Equal(remoteMsg.NetCookie) {
return fmt.Errorf("remote peer has different network cookie. Expected %q, got %q",
ic.nc, ci.NetworkCookie(remoteMsg.NetCookie))
}

// Add remote ID and key to conn state
ic.remotePubKey = remotePubkey
ic.remote = remoteID
Expand Down
57 changes: 56 additions & 1 deletion core/sec/insecure/insecure_test.go
Expand Up @@ -56,14 +56,69 @@ func TestPeerIDMismatchOutbound(t *testing.T) {
require.Contains(t, clientErr.Error(), "remote peer sent unexpected peer ID")
}

func newTestTransport(t *testing.T, typ, bits int) *Transport {
func TestNetworkCookies(t *testing.T) {
type testcase struct {
name string
clientCookie string
serverCookie string
error string
}

testcases := []testcase{
{
name: "No cookie",
},
{
name: "Matching cookie",
clientCookie: "424344aa",
serverCookie: "424344aa",
},
{
name: "Non-matching cookie",
clientCookie: "424344aa",
serverCookie: "010203",
error: "remote peer has different network cookie",
},
{
name: "Cookie vs no cookie",
clientCookie: "424344aa",
error: "remote peer has different network cookie",
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
clientCookie, err := crypto.ParseNetworkCookie(tc.clientCookie)
require.NoError(t, err)
clientTpt := newTestTransportWithNetworkCookie(t, crypto.RSA, 2048, clientCookie)

serverCookie, err := crypto.ParseNetworkCookie(tc.serverCookie)
require.NoError(t, err)
serverTpt := newTestTransportWithNetworkCookie(t, crypto.Ed25519, 1024, serverCookie)

_, _, clientErr, serverErr := connect(t, clientTpt, serverTpt, serverTpt.LocalPeer(), clientTpt.LocalPeer())
if tc.error == "" {
require.NoError(t, clientErr)
require.NoError(t, serverErr)
} else {
require.ErrorContains(t, clientErr, tc.error)
}
})
}
}

func newTestTransportWithNetworkCookie(t *testing.T, typ, bits int, nc crypto.NetworkCookie) *Transport {
priv, pub, err := crypto.GenerateKeyPair(typ, bits)
require.NoError(t, err)
id, err := peer.IDFromPublicKey(pub)
require.NoError(t, err)
priv = crypto.AddNetworkCookieToPrivKey(priv, nc)
return NewWithIdentity("/test/1.0.0", id, priv)
}

func newTestTransport(t *testing.T, typ, bits int) *Transport {
return newTestTransportWithNetworkCookie(t, typ, bits, nil)
}

// Create a new pair of connected TCP sockets.
func newConnPair(t *testing.T) (net.Conn, net.Conn) {
lstnr, err := net.Listen("tcp", "localhost:0")
Expand Down
20 changes: 15 additions & 5 deletions core/sec/insecure/pb/plaintext.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions core/sec/insecure/pb/plaintext.proto
Expand Up @@ -7,4 +7,5 @@ import "core/crypto/pb/crypto.proto";
message Exchange {
optional bytes id = 1;
optional crypto.pb.PublicKey pubkey = 2;
optional bytes netCookie = 3;
}
12 changes: 12 additions & 0 deletions options.go
Expand Up @@ -598,3 +598,15 @@ func SwarmOpts(opts ...swarm.Option) Option {
return nil
}
}

// NetworkCookie provides a way to prevent peers from different
// networks from connecting to each other. Peers that lack
// NetworkCookie cannot connect to peers with NetworkCookie, and peers
// with NetworkCookie can only connect to peers with the same
// NetworkCookie.
func NetworkCookie(netCookie crypto.NetworkCookie) Option {
return func(cfg *Config) error {
cfg.NetworkCookie = netCookie
return nil
}
}