p2p/conn: add a test for MakeSecretConnection (#4829)

Refs #4154
This commit is contained in:
Anton Kaliaev
2020-06-03 16:28:23 +04:00
committed by GitHub
parent c8483531d8
commit 994912211c
4 changed files with 353 additions and 88 deletions

View File

@@ -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)
}
})
}
}

View File

@@ -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()