github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/p2p/conn/secret_connection.go (about)

     1  package conn
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/cipher"
     6  	crand "crypto/rand"
     7  	"crypto/sha256"
     8  	"crypto/subtle"
     9  	"encoding/binary"
    10  	"io"
    11  	"math"
    12  	"net"
    13  	"sync"
    14  	"time"
    15  
    16  	pool "github.com/libp2p/go-buffer-pool"
    17  	"golang.org/x/crypto/chacha20poly1305"
    18  	"golang.org/x/crypto/curve25519"
    19  	"golang.org/x/crypto/hkdf"
    20  	"golang.org/x/crypto/nacl/box"
    21  
    22  	"github.com/gnolang/gno/tm2/pkg/amino"
    23  	"github.com/gnolang/gno/tm2/pkg/async"
    24  	"github.com/gnolang/gno/tm2/pkg/crypto"
    25  	"github.com/gnolang/gno/tm2/pkg/crypto/ed25519"
    26  	"github.com/gnolang/gno/tm2/pkg/errors"
    27  )
    28  
    29  // 4 + 1024 == 1028 total frame size
    30  const dataLenSize = 4
    31  
    32  const (
    33  	dataMaxSize      = 1024
    34  	totalFrameSize   = dataMaxSize + dataLenSize
    35  	aeadSizeOverhead = 16 // overhead of poly 1305 authentication tag
    36  	aeadKeySize      = chacha20poly1305.KeySize
    37  	aeadNonceSize    = chacha20poly1305.NonceSize
    38  )
    39  
    40  var (
    41  	ErrSmallOrderRemotePubKey = errors.New("detected low order point from remote peer")
    42  	ErrSharedSecretIsZero     = errors.New("shared secret is all zeroes")
    43  )
    44  
    45  // SecretConnection implements net.Conn.
    46  // It is an implementation of the STS protocol.
    47  // See https://github.com/tendermint/classic/blob/0.1/docs/sts-final.pdf for
    48  // details on the protocol.
    49  //
    50  // Consumers of the SecretConnection are responsible for authenticating
    51  // the remote peer's pubkey against known information, like a nodeID.
    52  // Otherwise they are vulnerable to MITM.
    53  // (TODO(ismail): see also https://github.com/tendermint/classic/issues/3010)
    54  type SecretConnection struct {
    55  	// immutable
    56  	recvAead cipher.AEAD
    57  	sendAead cipher.AEAD
    58  
    59  	remPubKey crypto.PubKey
    60  	conn      io.ReadWriteCloser
    61  
    62  	// net.Conn must be thread safe:
    63  	// https://golang.org/pkg/net/#Conn.
    64  	// Since we have internal mutable state,
    65  	// we need mtxs. But recv and send states
    66  	// are independent, so we can use two mtxs.
    67  	// All .Read are covered by recvMtx,
    68  	// all .Write are covered by sendMtx.
    69  	recvMtx    sync.Mutex
    70  	recvBuffer []byte
    71  	recvNonce  *[aeadNonceSize]byte
    72  
    73  	sendMtx   sync.Mutex
    74  	sendNonce *[aeadNonceSize]byte
    75  }
    76  
    77  // MakeSecretConnection performs handshake and returns a new authenticated
    78  // SecretConnection.
    79  // Returns nil if there is an error in handshake.
    80  // Caller should call conn.Close()
    81  // See docs/sts-final.pdf for more information.
    82  func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKey) (*SecretConnection, error) {
    83  	locPubKey := locPrivKey.PubKey()
    84  
    85  	// Generate ephemeral keys for perfect forward secrecy.
    86  	locEphPub, locEphPriv := genEphKeys()
    87  
    88  	// Write local ephemeral pubkey and receive one too.
    89  	// NOTE: every 32-byte string is accepted as a Curve25519 public key
    90  	// (see DJB's Curve25519 paper: http://cr.yp.to/ecdh/curve25519-20060209.pdf)
    91  	remEphPub, err := shareEphPubKey(conn, locEphPub)
    92  	if err != nil {
    93  		return nil, err
    94  	}
    95  
    96  	// Sort by lexical order.
    97  	loEphPub, _ := sort32(locEphPub, remEphPub)
    98  
    99  	// Check if the local ephemeral public key
   100  	// was the least, lexicographically sorted.
   101  	locIsLeast := bytes.Equal(locEphPub[:], loEphPub[:])
   102  
   103  	// Compute common diffie hellman secret using X25519.
   104  	dhSecret, err := computeDHSecret(remEphPub, locEphPriv)
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  
   109  	// generate the secret used for receiving, sending, challenge via hkdf-sha2 on dhSecret
   110  	recvSecret, sendSecret, challenge := deriveSecretAndChallenge(dhSecret, locIsLeast)
   111  
   112  	sendAead, err := chacha20poly1305.New(sendSecret[:])
   113  	if err != nil {
   114  		return nil, errors.New("invalid send SecretConnection Key")
   115  	}
   116  	recvAead, err := chacha20poly1305.New(recvSecret[:])
   117  	if err != nil {
   118  		return nil, errors.New("invalid receive SecretConnection Key")
   119  	}
   120  	// Construct SecretConnection.
   121  	sc := &SecretConnection{
   122  		conn:       conn,
   123  		recvBuffer: nil,
   124  		recvNonce:  new([aeadNonceSize]byte),
   125  		sendNonce:  new([aeadNonceSize]byte),
   126  		recvAead:   recvAead,
   127  		sendAead:   sendAead,
   128  	}
   129  
   130  	// Sign the challenge bytes for authentication.
   131  	locSignature := signChallenge(challenge, locPrivKey)
   132  
   133  	// Share (in secret) each other's pubkey & challenge signature
   134  	authSigMsg, err := shareAuthSignature(sc, locPubKey, locSignature)
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  
   139  	remPubKey, remSignature := authSigMsg.Key, authSigMsg.Sig
   140  
   141  	if _, ok := remPubKey.(ed25519.PubKeyEd25519); !ok {
   142  		return nil, errors.New("expected ed25519 pubkey, got %T", remPubKey)
   143  	}
   144  
   145  	if !remPubKey.VerifyBytes(challenge[:], remSignature) {
   146  		return nil, errors.New("challenge verification failed")
   147  	}
   148  
   149  	// We've authorized.
   150  	sc.remPubKey = remPubKey
   151  	return sc, nil
   152  }
   153  
   154  // RemotePubKey returns authenticated remote pubkey
   155  func (sc *SecretConnection) RemotePubKey() crypto.PubKey {
   156  	return sc.remPubKey
   157  }
   158  
   159  // Writes encrypted frames of `totalFrameSize + aeadSizeOverhead`.
   160  // CONTRACT: data smaller than dataMaxSize is written atomically.
   161  func (sc *SecretConnection) Write(data []byte) (n int, err error) {
   162  	sc.sendMtx.Lock()
   163  	defer sc.sendMtx.Unlock()
   164  
   165  	for 0 < len(data) {
   166  		if err := func() error {
   167  			sealedFrame := pool.Get(aeadSizeOverhead + totalFrameSize)
   168  			frame := pool.Get(totalFrameSize)
   169  			defer func() {
   170  				pool.Put(sealedFrame)
   171  				pool.Put(frame)
   172  			}()
   173  			var chunk []byte
   174  			if dataMaxSize < len(data) {
   175  				chunk = data[:dataMaxSize]
   176  				data = data[dataMaxSize:]
   177  			} else {
   178  				chunk = data
   179  				data = nil
   180  			}
   181  			chunkLength := len(chunk)
   182  			binary.LittleEndian.PutUint32(frame, uint32(chunkLength))
   183  			copy(frame[dataLenSize:], chunk)
   184  
   185  			// encrypt the frame
   186  			sc.sendAead.Seal(sealedFrame[:0], sc.sendNonce[:], frame, nil)
   187  			incrNonce(sc.sendNonce)
   188  			// end encryption
   189  
   190  			_, err = sc.conn.Write(sealedFrame)
   191  			if err != nil {
   192  				return err
   193  			}
   194  			n += len(chunk)
   195  			return nil
   196  		}(); err != nil {
   197  			return n, err
   198  		}
   199  	}
   200  	return n, err
   201  }
   202  
   203  // CONTRACT: data smaller than dataMaxSize is read atomically.
   204  func (sc *SecretConnection) Read(data []byte) (n int, err error) {
   205  	sc.recvMtx.Lock()
   206  	defer sc.recvMtx.Unlock()
   207  
   208  	// read off and update the recvBuffer, if non-empty
   209  	if 0 < len(sc.recvBuffer) {
   210  		n = copy(data, sc.recvBuffer)
   211  		sc.recvBuffer = sc.recvBuffer[n:]
   212  		return
   213  	}
   214  
   215  	// read off the conn
   216  	sealedFrame := pool.Get(aeadSizeOverhead + totalFrameSize)
   217  	defer pool.Put(sealedFrame)
   218  	_, err = io.ReadFull(sc.conn, sealedFrame)
   219  	if err != nil {
   220  		return
   221  	}
   222  
   223  	// decrypt the frame.
   224  	// reads and updates the sc.recvNonce
   225  	frame := pool.Get(totalFrameSize)
   226  	defer pool.Put(frame)
   227  	_, err = sc.recvAead.Open(frame[:0], sc.recvNonce[:], sealedFrame, nil)
   228  	if err != nil {
   229  		return n, errors.New("failed to decrypt SecretConnection")
   230  	}
   231  	incrNonce(sc.recvNonce)
   232  	// end decryption
   233  
   234  	// copy checkLength worth into data,
   235  	// set recvBuffer to the rest.
   236  	chunkLength := binary.LittleEndian.Uint32(frame) // read the first four bytes
   237  	if chunkLength > dataMaxSize {
   238  		return 0, errors.New("chunkLength is greater than dataMaxSize")
   239  	}
   240  	chunk := frame[dataLenSize : dataLenSize+chunkLength]
   241  	n = copy(data, chunk)
   242  	if n < len(chunk) {
   243  		sc.recvBuffer = make([]byte, len(chunk)-n)
   244  		copy(sc.recvBuffer, chunk[n:])
   245  	}
   246  	return n, err
   247  }
   248  
   249  // Implements net.Conn
   250  func (sc *SecretConnection) Close() error                  { return sc.conn.Close() }
   251  func (sc *SecretConnection) LocalAddr() net.Addr           { return sc.conn.(net.Conn).LocalAddr() }
   252  func (sc *SecretConnection) RemoteAddr() net.Addr          { return sc.conn.(net.Conn).RemoteAddr() }
   253  func (sc *SecretConnection) SetDeadline(t time.Time) error { return sc.conn.(net.Conn).SetDeadline(t) }
   254  func (sc *SecretConnection) SetReadDeadline(t time.Time) error {
   255  	return sc.conn.(net.Conn).SetReadDeadline(t)
   256  }
   257  
   258  func (sc *SecretConnection) SetWriteDeadline(t time.Time) error {
   259  	return sc.conn.(net.Conn).SetWriteDeadline(t)
   260  }
   261  
   262  func genEphKeys() (ephPub, ephPriv *[32]byte) {
   263  	var err error
   264  	// TODO: Probably not a problem but ask Tony: different from the rust implementation (uses x25519-dalek),
   265  	// we do not "clamp" the private key scalar:
   266  	// see: https://github.com/dalek-cryptography/x25519-dalek/blob/34676d336049df2bba763cc076a75e47ae1f170f/src/x25519.rs#L56-L74
   267  	ephPub, ephPriv, err = box.GenerateKey(crand.Reader)
   268  	if err != nil {
   269  		panic("Could not generate ephemeral key-pair")
   270  	}
   271  	return
   272  }
   273  
   274  func shareEphPubKey(conn io.ReadWriteCloser, locEphPub *[32]byte) (remEphPub *[32]byte, err error) {
   275  	// Send our pubkey and receive theirs in tandem.
   276  	trs, _ := async.Parallel(
   277  		func(_ int) (val interface{}, err error, abort bool) {
   278  			_, err1 := amino.MarshalSizedWriter(conn, locEphPub)
   279  			if err1 != nil {
   280  				return nil, err1, true // abort
   281  			}
   282  			return nil, nil, false
   283  		},
   284  		func(_ int) (val interface{}, err error, abort bool) {
   285  			var _remEphPub [32]byte
   286  			_, err2 := amino.UnmarshalSizedReader(conn, &_remEphPub, 1024*1024) // TODO
   287  			if err2 != nil {
   288  				return nil, err2, true // abort
   289  			}
   290  			if hasSmallOrder(_remEphPub) {
   291  				return nil, ErrSmallOrderRemotePubKey, true
   292  			}
   293  			return _remEphPub, nil, false
   294  		},
   295  	)
   296  
   297  	// If error:
   298  	if trs.FirstError() != nil {
   299  		err = trs.FirstError()
   300  		return
   301  	}
   302  
   303  	// Otherwise:
   304  	_remEphPub := trs.FirstValue().([32]byte)
   305  	return &_remEphPub, nil
   306  }
   307  
   308  // use the same blacklist as lib sodium (see https://eprint.iacr.org/2017/806.pdf for reference):
   309  // https://github.com/jedisct1/libsodium/blob/536ed00d2c5e0c65ac01e29141d69a30455f2038/src/libsodium/crypto_scalarmult/curve25519/ref10/x25519_ref10.c#L11-L17
   310  var blacklist = [][32]byte{
   311  	// 0 (order 4)
   312  	{
   313  		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   314  		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   315  		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   316  	},
   317  	// 1 (order 1)
   318  	{
   319  		0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   320  		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   321  		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   322  	},
   323  	// 325606250916557431795983626356110631294008115727848805560023387167927233504
   324  	//   (order 8)
   325  	{
   326  		0xe0, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3,
   327  		0xfa, 0xf1, 0x9f, 0xc4, 0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32,
   328  		0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49, 0xb8, 0x00,
   329  	},
   330  	// 39382357235489614581723060781553021112529911719440698176882885853963445705823
   331  	//    (order 8)
   332  	{
   333  		0x5f, 0x9c, 0x95, 0xbc, 0xa3, 0x50, 0x8c, 0x24, 0xb1, 0xd0, 0xb1,
   334  		0x55, 0x9c, 0x83, 0xef, 0x5b, 0x04, 0x44, 0x5c, 0xc4, 0x58, 0x1c,
   335  		0x8e, 0x86, 0xd8, 0x22, 0x4e, 0xdd, 0xd0, 0x9f, 0x11, 0x57,
   336  	},
   337  	// p-1 (order 2)
   338  	{
   339  		0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
   340  		0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
   341  		0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f,
   342  	},
   343  	// p (=0, order 4)
   344  	{
   345  		0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
   346  		0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
   347  		0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f,
   348  	},
   349  	// p+1 (=1, order 1)
   350  	{
   351  		0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
   352  		0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
   353  		0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f,
   354  	},
   355  }
   356  
   357  func hasSmallOrder(pubKey [32]byte) bool {
   358  	isSmallOrderPoint := false
   359  	for _, bl := range blacklist {
   360  		if subtle.ConstantTimeCompare(pubKey[:], bl[:]) == 1 {
   361  			isSmallOrderPoint = true
   362  			break
   363  		}
   364  	}
   365  	return isSmallOrderPoint
   366  }
   367  
   368  func deriveSecretAndChallenge(dhSecret *[32]byte, locIsLeast bool) (recvSecret, sendSecret *[aeadKeySize]byte, challenge *[32]byte) {
   369  	hash := sha256.New
   370  	hkdf := hkdf.New(hash, dhSecret[:], nil, []byte("TENDERMINT_SECRET_CONNECTION_KEY_AND_CHALLENGE_GEN"))
   371  	// get enough data for 2 aead keys, and a 32 byte challenge
   372  	res := new([2*aeadKeySize + 32]byte)
   373  	_, err := io.ReadFull(hkdf, res[:])
   374  	if err != nil {
   375  		panic(err)
   376  	}
   377  
   378  	challenge = new([32]byte)
   379  	recvSecret = new([aeadKeySize]byte)
   380  	sendSecret = new([aeadKeySize]byte)
   381  	// Use the last 32 bytes as the challenge
   382  	copy(challenge[:], res[2*aeadKeySize:2*aeadKeySize+32])
   383  
   384  	// bytes 0 through aeadKeySize - 1 are one aead key.
   385  	// bytes aeadKeySize through 2*aeadKeySize -1 are another aead key.
   386  	// which key corresponds to sending and receiving key depends on whether
   387  	// the local key is less than the remote key.
   388  	if locIsLeast {
   389  		copy(recvSecret[:], res[0:aeadKeySize])
   390  		copy(sendSecret[:], res[aeadKeySize:aeadKeySize*2])
   391  	} else {
   392  		copy(sendSecret[:], res[0:aeadKeySize])
   393  		copy(recvSecret[:], res[aeadKeySize:aeadKeySize*2])
   394  	}
   395  
   396  	return
   397  }
   398  
   399  // computeDHSecret computes a Diffie-Hellman shared secret key
   400  // from our own local private key and the other's public key.
   401  //
   402  // It returns an error if the computed shared secret is all zeroes.
   403  func computeDHSecret(remPubKey, locPrivKey *[32]byte) (shrKey *[32]byte, err error) {
   404  	shrKey = new([32]byte)
   405  	curve25519.ScalarMult(shrKey, locPrivKey, remPubKey)
   406  
   407  	// reject if the returned shared secret is all zeroes
   408  	// related to: https://github.com/tendermint/classic/issues/3010
   409  	zero := new([32]byte)
   410  	if subtle.ConstantTimeCompare(shrKey[:], zero[:]) == 1 {
   411  		return nil, ErrSharedSecretIsZero
   412  	}
   413  	return
   414  }
   415  
   416  func sort32(foo, bar *[32]byte) (lo, hi *[32]byte) {
   417  	if bytes.Compare(foo[:], bar[:]) < 0 {
   418  		lo = foo
   419  		hi = bar
   420  	} else {
   421  		lo = bar
   422  		hi = foo
   423  	}
   424  	return
   425  }
   426  
   427  func signChallenge(challenge *[32]byte, locPrivKey crypto.PrivKey) (signature []byte) {
   428  	signature, err := locPrivKey.Sign(challenge[:])
   429  	// TODO(ismail): let signChallenge return an error instead
   430  	if err != nil {
   431  		panic(err)
   432  	}
   433  	return
   434  }
   435  
   436  type authSigMessage struct {
   437  	Key crypto.PubKey
   438  	Sig []byte
   439  }
   440  
   441  func shareAuthSignature(sc *SecretConnection, pubKey crypto.PubKey, signature []byte) (recvMsg authSigMessage, err error) {
   442  	// Send our info and receive theirs in tandem.
   443  	trs, _ := async.Parallel(
   444  		func(_ int) (val interface{}, err error, abort bool) {
   445  			_, err1 := amino.MarshalSizedWriter(sc, authSigMessage{pubKey, signature})
   446  			if err1 != nil {
   447  				return nil, err1, true // abort
   448  			}
   449  			return nil, nil, false
   450  		},
   451  		func(_ int) (val interface{}, err error, abort bool) {
   452  			var _recvMsg authSigMessage
   453  			_, err2 := amino.UnmarshalSizedReader(sc, &_recvMsg, 1024*1024) // TODO
   454  			if err2 != nil {
   455  				return nil, err2, true // abort
   456  			}
   457  			return _recvMsg, nil, false
   458  		},
   459  	)
   460  
   461  	// If error:
   462  	if trs.FirstError() != nil {
   463  		err = trs.FirstError()
   464  		return
   465  	}
   466  
   467  	_recvMsg := trs.FirstValue().(authSigMessage)
   468  	return _recvMsg, nil
   469  }
   470  
   471  // --------------------------------------------------------------------------------
   472  
   473  // Increment nonce little-endian by 1 with wraparound.
   474  // Due to chacha20poly1305 expecting a 12 byte nonce we do not use the first four
   475  // bytes. We only increment a 64 bit unsigned int in the remaining 8 bytes
   476  // (little-endian in nonce[4:]).
   477  func incrNonce(nonce *[aeadNonceSize]byte) {
   478  	counter := binary.LittleEndian.Uint64(nonce[4:])
   479  	if counter == math.MaxUint64 {
   480  		// Terminates the session and makes sure the nonce would not re-used.
   481  		// See https://github.com/tendermint/classic/issues/3531
   482  		panic("can't increase nonce without overflow")
   483  	}
   484  	counter++
   485  	binary.LittleEndian.PutUint64(nonce[4:], counter)
   486  }