package conn import ( "bytes" "errors" "io" "testing" gogotypes "github.com/gogo/protobuf/types" "github.com/oasisprotocol/curve25519-voi/primitives/merlin" "github.com/stretchr/testify/assert" "golang.org/x/crypto/chacha20poly1305" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" cryptoenc "github.com/tendermint/tendermint/crypto/encoding" "github.com/tendermint/tendermint/internal/libs/protoio" tmp2p "github.com/tendermint/tendermint/proto/tendermint/p2p" ) 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 { lc := *c.locEphPub bz, err := protoio.MarshalDelimited(&gogotypes.BytesValue{Value: lc[:]}) if err != nil { panic(err) } copy(data, bz[c.readOffset:]) n = len(data) } else { bz, err := protoio.MarshalDelimited(&gogotypes.BytesValue{Value: []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 { pkpb, err := cryptoenc.PubKeyToProto(c.privKey.PubKey()) if err != nil { panic(err) } bz, err := protoio.MarshalDelimited(&tmp2p.AuthSigMessage{PubKey: pkpb, Sig: 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 := protoio.MarshalDelimited(&gogotypes.BytesValue{Value: []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: var ( bytes gogotypes.BytesValue remEphPub [32]byte ) err := protoio.UnmarshalDelimited(data, &bytes) if err != nil { panic(err) } copy(remEphPub[:], bytes.Value) c.remEphPub = &remEphPub 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 transcript.ExtractBytes(challenge[:], labelSecretConnectionMac) 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), "wrong wireType"}, {"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) } }) } }