From 994912211c6911684a1db9f39f524677e4cd1154 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 3 Jun 2020 16:28:23 +0400 Subject: [PATCH] p2p/conn: add a test for MakeSecretConnection (#4829) Refs #4154 --- libs/async/async.go | 6 +- libs/async/async_test.go | 2 +- p2p/conn/evil_secret_connection_test.go | 258 ++++++++++++++++++++++++ p2p/conn/secret_connection_test.go | 175 ++++++++-------- 4 files changed, 353 insertions(+), 88 deletions(-) create mode 100644 p2p/conn/evil_secret_connection_test.go diff --git a/libs/async/async.go b/libs/async/async.go index 79c693de8..e716821b6 100644 --- a/libs/async/async.go +++ b/libs/async/async.go @@ -2,6 +2,7 @@ package async import ( "fmt" + "runtime" "sync/atomic" ) @@ -143,7 +144,10 @@ func Parallel(tasks ...Task) (trs *TaskResultSet, ok bool) { if pnk := recover(); pnk != nil { atomic.AddInt32(numPanics, 1) // Send panic to taskResultCh. - taskResultCh <- TaskResult{nil, fmt.Errorf("panic in task %v", pnk)} + const size = 64 << 10 + buf := make([]byte, size) + buf = buf[:runtime.Stack(buf, false)] + taskResultCh <- TaskResult{nil, fmt.Errorf("panic in task %v : %s", pnk, buf)} // Closing taskResultCh lets trs.Wait() work. close(taskResultCh) // Decrement waitgroup. diff --git a/libs/async/async_test.go b/libs/async/async_test.go index c4ca96b4a..4faead444 100644 --- a/libs/async/async_test.go +++ b/libs/async/async_test.go @@ -139,7 +139,7 @@ func checkResult(t *testing.T, taskResultSet *TaskResultSet, index int, case err != nil: assert.Equal(t, err.Error(), taskResult.Error.Error(), taskName) case pnk != nil: - assert.Equal(t, pnk, taskResult.Error.Error(), taskName) + assert.Contains(t, taskResult.Error.Error(), pnk, taskName) default: assert.Nil(t, taskResult.Error, taskName) } diff --git a/p2p/conn/evil_secret_connection_test.go b/p2p/conn/evil_secret_connection_test.go new file mode 100644 index 000000000..1f662ee2a --- /dev/null +++ b/p2p/conn/evil_secret_connection_test.go @@ -0,0 +1,258 @@ +package conn + +import ( + "bytes" + "errors" + "io" + "testing" + + "github.com/gtank/merlin" + "github.com/stretchr/testify/assert" + "golang.org/x/crypto/chacha20poly1305" + + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" +) + +type buffer struct { + next bytes.Buffer +} + +func (b *buffer) Read(data []byte) (n int, err error) { + return b.next.Read(data) +} + +func (b *buffer) Write(data []byte) (n int, err error) { + return b.next.Write(data) +} + +func (b *buffer) Bytes() []byte { + return b.next.Bytes() +} + +func (b *buffer) Close() error { + return nil +} + +type evilConn struct { + secretConn *SecretConnection + buffer *buffer + + locEphPub *[32]byte + locEphPriv *[32]byte + remEphPub *[32]byte + privKey crypto.PrivKey + + readStep int + writeStep int + readOffset int + + shareEphKey bool + badEphKey bool + shareAuthSignature bool + badAuthSignature bool +} + +func newEvilConn(shareEphKey, badEphKey, shareAuthSignature, badAuthSignature bool) *evilConn { + privKey := ed25519.GenPrivKey() + locEphPub, locEphPriv := genEphKeys() + var rep [32]byte + c := &evilConn{ + locEphPub: locEphPub, + locEphPriv: locEphPriv, + remEphPub: &rep, + privKey: privKey, + + shareEphKey: shareEphKey, + badEphKey: badEphKey, + shareAuthSignature: shareAuthSignature, + badAuthSignature: badAuthSignature, + } + + return c +} + +func (c *evilConn) Read(data []byte) (n int, err error) { + if !c.shareEphKey { + return 0, io.EOF + } + + switch c.readStep { + case 0: + if !c.badEphKey { + bz, err := cdc.MarshalBinaryLengthPrefixed(c.locEphPub) + if err != nil { + panic(err) + } + copy(data, bz[c.readOffset:]) + n = len(data) + } else { + bz, err := cdc.MarshalBinaryLengthPrefixed([]byte("drop users;")) + if err != nil { + panic(err) + } + copy(data, bz) + n = len(data) + } + c.readOffset += n + + if n >= 32 { + c.readOffset = 0 + c.readStep = 1 + if !c.shareAuthSignature { + c.readStep = 2 + } + } + + return n, nil + case 1: + signature := c.signChallenge() + if !c.badAuthSignature { + bz, err := cdc.MarshalBinaryLengthPrefixed(authSigMessage{c.privKey.PubKey(), signature}) + if err != nil { + panic(err) + } + n, err = c.secretConn.Write(bz) + if err != nil { + panic(err) + } + if c.readOffset > len(c.buffer.Bytes()) { + return len(data), nil + } + copy(data, c.buffer.Bytes()[c.readOffset:]) + } else { + bz, err := cdc.MarshalBinaryLengthPrefixed([]byte("select * from users;")) + if err != nil { + panic(err) + } + n, err = c.secretConn.Write(bz) + if err != nil { + panic(err) + } + if c.readOffset > len(c.buffer.Bytes()) { + return len(data), nil + } + copy(data, c.buffer.Bytes()) + } + c.readOffset += len(data) + return n, nil + default: + return 0, io.EOF + } +} + +func (c *evilConn) Write(data []byte) (n int, err error) { + switch c.writeStep { + case 0: + err := cdc.UnmarshalBinaryLengthPrefixed(data, c.remEphPub) + if err != nil { + panic(err) + } + c.writeStep = 1 + if !c.shareAuthSignature { + c.writeStep = 2 + } + return len(data), nil + case 1: + // Signature is not needed, therefore skipped. + return len(data), nil + default: + return 0, io.EOF + } +} + +func (c *evilConn) Close() error { + return nil +} + +func (c *evilConn) signChallenge() []byte { + // Sort by lexical order. + loEphPub, hiEphPub := sort32(c.locEphPub, c.remEphPub) + + transcript := merlin.NewTranscript("TENDERMINT_SECRET_CONNECTION_TRANSCRIPT_HASH") + + transcript.AppendMessage(labelEphemeralLowerPublicKey, loEphPub[:]) + transcript.AppendMessage(labelEphemeralUpperPublicKey, hiEphPub[:]) + + // Check if the local ephemeral public key was the least, lexicographically + // sorted. + locIsLeast := bytes.Equal(c.locEphPub[:], loEphPub[:]) + + // Compute common diffie hellman secret using X25519. + dhSecret, err := computeDHSecret(c.remEphPub, c.locEphPriv) + if err != nil { + panic(err) + } + + transcript.AppendMessage(labelDHSecret, dhSecret[:]) + + // Generate the secret used for receiving, sending, challenge via HKDF-SHA2 + // on the transcript state (which itself also uses HKDF-SHA2 to derive a key + // from the dhSecret). + recvSecret, sendSecret := deriveSecrets(dhSecret, locIsLeast) + + const challengeSize = 32 + var challenge [challengeSize]byte + challengeSlice := transcript.ExtractBytes(labelSecretConnectionMac, challengeSize) + + copy(challenge[:], challengeSlice[0:challengeSize]) + + sendAead, err := chacha20poly1305.New(sendSecret[:]) + if err != nil { + panic(errors.New("invalid send SecretConnection Key")) + } + recvAead, err := chacha20poly1305.New(recvSecret[:]) + if err != nil { + panic(errors.New("invalid receive SecretConnection Key")) + } + + b := &buffer{} + c.secretConn = &SecretConnection{ + conn: b, + recvBuffer: nil, + recvNonce: new([aeadNonceSize]byte), + sendNonce: new([aeadNonceSize]byte), + recvAead: recvAead, + sendAead: sendAead, + } + c.buffer = b + + // Sign the challenge bytes for authentication. + locSignature, err := signChallenge(&challenge, c.privKey) + if err != nil { + panic(err) + } + + return locSignature +} + +// TestMakeSecretConnection creates an evil connection and tests that +// MakeSecretConnection errors at different stages. +func TestMakeSecretConnection(t *testing.T) { + testCases := []struct { + name string + conn *evilConn + errMsg string + }{ + {"refuse to share ethimeral key", newEvilConn(false, false, false, false), "EOF"}, + {"share bad ethimeral key", newEvilConn(true, true, false, false), "Insufficient bytes to decode"}, + {"refuse to share auth signature", newEvilConn(true, false, false, false), "EOF"}, + {"share bad auth signature", newEvilConn(true, false, true, true), "failed to decrypt SecretConnection"}, + {"all good", newEvilConn(true, false, true, false), ""}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + privKey := ed25519.GenPrivKey() + _, err := MakeSecretConnection(tc.conn, privKey) + if tc.errMsg != "" { + if assert.Error(t, err) { + assert.Contains(t, err.Error(), tc.errMsg) + } + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/p2p/conn/secret_connection_test.go b/p2p/conn/secret_connection_test.go index cffa3879e..0edf2e243 100644 --- a/p2p/conn/secret_connection_test.go +++ b/p2p/conn/secret_connection_test.go @@ -25,6 +25,10 @@ import ( tmrand "github.com/tendermint/tendermint/libs/rand" ) +// Run go test -update from within this module +// to update the golden test vector file +var update = flag.Bool("update", false, "update .golden files") + type kvstoreConn struct { *io.PipeReader *io.PipeWriter @@ -39,60 +43,14 @@ func (drw kvstoreConn) Close() (err error) { return err1 } -// Each returned ReadWriteCloser is akin to a net.Connection -func makeKVStoreConnPair() (fooConn, barConn kvstoreConn) { - barReader, fooWriter := io.Pipe() - fooReader, barWriter := io.Pipe() - return kvstoreConn{fooReader, fooWriter}, kvstoreConn{barReader, barWriter} +type privKeyWithNilPubKey struct { + orig crypto.PrivKey } -func makeSecretConnPair(tb testing.TB) (fooSecConn, barSecConn *SecretConnection) { - - var fooConn, barConn = makeKVStoreConnPair() - var fooPrvKey = ed25519.GenPrivKey() - var fooPubKey = fooPrvKey.PubKey() - var barPrvKey = ed25519.GenPrivKey() - var barPubKey = barPrvKey.PubKey() - - // Make connections from both sides in parallel. - var trs, ok = async.Parallel( - func(_ int) (val interface{}, abort bool, err error) { - fooSecConn, err = MakeSecretConnection(fooConn, fooPrvKey) - if err != nil { - tb.Errorf("failed to establish SecretConnection for foo: %v", err) - return nil, true, err - } - remotePubBytes := fooSecConn.RemotePubKey() - if !remotePubBytes.Equals(barPubKey) { - err = fmt.Errorf("unexpected fooSecConn.RemotePubKey. Expected %v, got %v", - barPubKey, fooSecConn.RemotePubKey()) - tb.Error(err) - return nil, false, err - } - return nil, false, nil - }, - func(_ int) (val interface{}, abort bool, err error) { - barSecConn, err = MakeSecretConnection(barConn, barPrvKey) - if barSecConn == nil { - tb.Errorf("failed to establish SecretConnection for bar: %v", err) - return nil, true, err - } - remotePubBytes := barSecConn.RemotePubKey() - if !remotePubBytes.Equals(fooPubKey) { - err = fmt.Errorf("unexpected barSecConn.RemotePubKey. Expected %v, got %v", - fooPubKey, barSecConn.RemotePubKey()) - tb.Error(err) - return nil, false, nil - } - return nil, false, nil - }, - ) - - require.Nil(tb, trs.FirstError()) - require.True(tb, ok, "Unexpected task abortion") - - return fooSecConn, barSecConn -} +func (pk privKeyWithNilPubKey) Bytes() []byte { return pk.orig.Bytes() } +func (pk privKeyWithNilPubKey) Sign(msg []byte) ([]byte, error) { return pk.orig.Sign(msg) } +func (pk privKeyWithNilPubKey) PubKey() crypto.PubKey { return nil } +func (pk privKeyWithNilPubKey) Equals(pk2 crypto.PrivKey) bool { return pk.orig.Equals(pk2) } func TestSecretConnectionHandshake(t *testing.T) { fooSecConn, barSecConn := makeSecretConnPair(t) @@ -148,26 +106,6 @@ func TestConcurrentRead(t *testing.T) { } } -func writeLots(t *testing.T, wg *sync.WaitGroup, conn io.Writer, txt string, n int) { - defer wg.Done() - for i := 0; i < n; i++ { - _, err := conn.Write([]byte(txt)) - if err != nil { - t.Errorf("failed to write to fooSecConn: %v", err) - return - } - } -} - -func readLots(t *testing.T, wg *sync.WaitGroup, conn io.Reader, n int) { - readBuffer := make([]byte, dataMaxSize) - for i := 0; i < n; i++ { - _, err := conn.Read(readBuffer) - assert.NoError(t, err) - } - wg.Done() -} - func TestSecretConnectionReadWrite(t *testing.T) { fooConn, barConn := makeKVStoreConnPair() fooWrites, barWrites := []string{}, []string{} @@ -282,13 +220,8 @@ func TestSecretConnectionReadWrite(t *testing.T) { compareWritesReads(fooWrites, barReads) compareWritesReads(barWrites, fooReads) - } -// Run go test -update from within this module -// to update the golden test vector file -var update = flag.Bool("update", false, "update .golden files") - func TestDeriveSecretsAndChallengeGolden(t *testing.T) { goldenFilepath := filepath.Join("testdata", t.Name()+".golden") if *update { @@ -322,15 +255,6 @@ func TestDeriveSecretsAndChallengeGolden(t *testing.T) { } } -type privKeyWithNilPubKey struct { - orig crypto.PrivKey -} - -func (pk privKeyWithNilPubKey) Bytes() []byte { return pk.orig.Bytes() } -func (pk privKeyWithNilPubKey) Sign(msg []byte) ([]byte, error) { return pk.orig.Sign(msg) } -func (pk privKeyWithNilPubKey) PubKey() crypto.PubKey { return nil } -func (pk privKeyWithNilPubKey) Equals(pk2 crypto.PrivKey) bool { return pk.orig.Equals(pk2) } - func TestNilPubkey(t *testing.T) { var fooConn, barConn = makeKVStoreConnPair() var fooPrvKey = ed25519.GenPrivKey() @@ -367,6 +291,26 @@ func TestNonEd25519Pubkey(t *testing.T) { }) } +func writeLots(t *testing.T, wg *sync.WaitGroup, conn io.Writer, txt string, n int) { + defer wg.Done() + for i := 0; i < n; i++ { + _, err := conn.Write([]byte(txt)) + if err != nil { + t.Errorf("failed to write to fooSecConn: %v", err) + return + } + } +} + +func readLots(t *testing.T, wg *sync.WaitGroup, conn io.Reader, n int) { + readBuffer := make([]byte, dataMaxSize) + for i := 0; i < n; i++ { + _, err := conn.Read(readBuffer) + assert.NoError(t, err) + } + wg.Done() +} + // Creates the data for a test vector file. // The file format is: // Hex(diffie_hellman_secret), loc_is_least, Hex(recvSecret), Hex(sendSecret), Hex(challenge) @@ -386,6 +330,65 @@ func createGoldenTestVectors(t *testing.T) string { return data } +// Each returned ReadWriteCloser is akin to a net.Connection +func makeKVStoreConnPair() (fooConn, barConn kvstoreConn) { + barReader, fooWriter := io.Pipe() + fooReader, barWriter := io.Pipe() + return kvstoreConn{fooReader, fooWriter}, kvstoreConn{barReader, barWriter} +} + +func makeSecretConnPair(tb testing.TB) (fooSecConn, barSecConn *SecretConnection) { + var ( + fooConn, barConn = makeKVStoreConnPair() + fooPrvKey = ed25519.GenPrivKey() + fooPubKey = fooPrvKey.PubKey() + barPrvKey = ed25519.GenPrivKey() + barPubKey = barPrvKey.PubKey() + ) + + // Make connections from both sides in parallel. + var trs, ok = async.Parallel( + func(_ int) (val interface{}, abort bool, err error) { + fooSecConn, err = MakeSecretConnection(fooConn, fooPrvKey) + if err != nil { + tb.Errorf("failed to establish SecretConnection for foo: %v", err) + return nil, true, err + } + remotePubBytes := fooSecConn.RemotePubKey() + if !remotePubBytes.Equals(barPubKey) { + err = fmt.Errorf("unexpected fooSecConn.RemotePubKey. Expected %v, got %v", + barPubKey, fooSecConn.RemotePubKey()) + tb.Error(err) + return nil, true, err + } + return nil, false, nil + }, + func(_ int) (val interface{}, abort bool, err error) { + barSecConn, err = MakeSecretConnection(barConn, barPrvKey) + if barSecConn == nil { + tb.Errorf("failed to establish SecretConnection for bar: %v", err) + return nil, true, err + } + remotePubBytes := barSecConn.RemotePubKey() + if !remotePubBytes.Equals(fooPubKey) { + err = fmt.Errorf("unexpected barSecConn.RemotePubKey. Expected %v, got %v", + fooPubKey, barSecConn.RemotePubKey()) + tb.Error(err) + return nil, true, err + } + return nil, false, nil + }, + ) + + require.Nil(tb, trs.FirstError()) + require.True(tb, ok, "Unexpected task abortion") + + return fooSecConn, barSecConn +} + +/////////////////////////////////////////////////////////////////////////////// +// Benchmarks + func BenchmarkWriteSecretConnection(b *testing.B) { b.StopTimer() b.ReportAllocs()