github.com/evdatsion/aphelion-dpos-bft@v0.32.1/p2p/conn/secret_connection.go (about)

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