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