Merge pull request #2054 from tendermint/dev/secret_connection

secret connection update
This commit is contained in:
Alexander Simmerl 2018-07-26 00:29:36 +02:00 committed by GitHub
commit bdab37a626
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 104 additions and 112 deletions

View File

@ -5,6 +5,7 @@ BREAKING CHANGES:
- breaks serialization/signing of all messages with a timestamp - breaks serialization/signing of all messages with a timestamp
- [abci] Removed Fee from ResponseDeliverTx and ResponseCheckTx - [abci] Removed Fee from ResponseDeliverTx and ResponseCheckTx
- [tools] Removed `make ensure_deps` in favor of `make get_vendor_deps` - [tools] Removed `make ensure_deps` in favor of `make get_vendor_deps`
- [p2p] Remove salsa and ripemd primitives, in favor of using chacha as a stream cipher, and hkdf
FEATURES: FEATURES:
- [tools] Added `make check_dep` - [tools] Added `make check_dep`

2
Gopkg.lock generated
View File

@ -143,6 +143,7 @@
".", ".",
"hcl/ast", "hcl/ast",
"hcl/parser", "hcl/parser",
"hcl/printer",
"hcl/scanner", "hcl/scanner",
"hcl/strconv", "hcl/strconv",
"hcl/token", "hcl/token",
@ -538,6 +539,7 @@
"github.com/tendermint/go-amino", "github.com/tendermint/go-amino",
"golang.org/x/crypto/bcrypt", "golang.org/x/crypto/bcrypt",
"golang.org/x/crypto/chacha20poly1305", "golang.org/x/crypto/chacha20poly1305",
"golang.org/x/crypto/curve25519",
"golang.org/x/crypto/hkdf", "golang.org/x/crypto/hkdf",
"golang.org/x/crypto/nacl/box", "golang.org/x/crypto/nacl/box",
"golang.org/x/crypto/nacl/secretbox", "golang.org/x/crypto/nacl/secretbox",

View File

@ -27,27 +27,24 @@ Both handshakes have configurable timeouts (they should complete quickly).
### Authenticated Encryption Handshake ### Authenticated Encryption Handshake
Tendermint implements the Station-to-Station protocol Tendermint implements the Station-to-Station protocol
using ED25519 keys for Diffie-Helman key-exchange and NACL SecretBox for encryption. using X25519 keys for Diffie-Helman key-exchange and chacha20poly1305 for encryption.
It goes as follows: It goes as follows:
- generate an emphemeral ED25519 keypair - generate an ephemeral X25519 keypair
- send the ephemeral public key to the peer - send the ephemeral public key to the peer
- wait to receive the peer's ephemeral public key - wait to receive the peer's ephemeral public key
- compute the Diffie-Hellman shared secret using the peers ephemeral public key and our ephemeral private key - compute the Diffie-Hellman shared secret using the peers ephemeral public key and our ephemeral private key
- generate two nonces to use for encryption (sending and receiving) as follows: - generate two keys to use for encryption (sending and receiving) and a challenge for authentication as follows:
- sort the ephemeral public keys in ascending order and concatenate them - create a hkdf-sha256 instance with the key being the diffie hellman shared secret, and info parameter as
- RIPEMD160 the result `TENDERMINT_SECRET_CONNECTION_KEY_AND_CHALLENGE_GEN`
- append 4 empty bytes (extending the hash to 24-bytes) - get 96 bytes of output from hkdf-sha256
- the result is nonce1 - if we had the smaller ephemeral pubkey, use the first 32 bytes for the key for receiving, the second 32 bytes for sending; else the opposite
- flip the last bit of nonce1 to get nonce2 - use the last 32 bytes of output for the challenge
- if we had the smaller ephemeral pubkey, use nonce1 for receiving, nonce2 for sending; - use a seperate nonce for receiving and sending. Both nonces start at 0, and should support the full 96 bit nonce range
else the opposite - all communications from now on are encrypted in 1024 byte frames,
- all communications from now on are encrypted using the shared secret and the nonces, where each nonce using the respective secret and nonce. Each nonce is incremented by one after each use.
increments by 2 every time it is used
- we now have an encrypted channel, but still need to authenticate - we now have an encrypted channel, but still need to authenticate
- generate a common challenge to sign: - sign the common challenge obtained from the hkdf with our persistent private key
- SHA256 of the sorted (lowest first) and concatenated ephemeral pub keys - send the amino encoded persistent pubkey and signature to the peer
- sign the common challenge with our persistent private key
- send the go-wire encoded persistent pubkey and signature to the peer
- wait to receive the persistent public key and signature from the peer - wait to receive the persistent public key and signature from the peer
- verify the signature on the challenge using the peer's persistent public key - verify the signature on the challenge using the peer's persistent public key

View File

@ -1,9 +1,3 @@
// Uses nacl's secret_box to encrypt a net.Conn.
// It is (meant to be) an implementation of the STS protocol.
// Note we do not (yet) assume that a remote peer's pubkey
// is known ahead of time, and thus we are technically
// still vulnerable to MITM. (TODO!)
// See docs/sts-final.pdf for more info
package conn package conn
import ( import (
@ -16,36 +10,45 @@ import (
"net" "net"
"time" "time"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/nacl/box" "golang.org/x/crypto/nacl/box"
"golang.org/x/crypto/nacl/secretbox"
"golang.org/x/crypto/ripemd160"
"github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto"
cmn "github.com/tendermint/tendermint/libs/common" cmn "github.com/tendermint/tendermint/libs/common"
"golang.org/x/crypto/hkdf"
) )
// 4 + 1024 == 1028 total frame size // 4 + 1024 == 1028 total frame size
const dataLenSize = 4 const dataLenSize = 4
const dataMaxSize = 1024 const dataMaxSize = 1024
const totalFrameSize = dataMaxSize + dataLenSize const totalFrameSize = dataMaxSize + dataLenSize
const sealedFrameSize = totalFrameSize + secretbox.Overhead const aeadSizeOverhead = 16 // overhead of poly 1305 authentication tag
const aeadKeySize = chacha20poly1305.KeySize
const aeadNonceSize = chacha20poly1305.NonceSize
// Implements net.Conn // SecretConnection implements net.conn.
// It is an implementation of the STS protocol.
// Note we do not (yet) assume that a remote peer's pubkey
// is known ahead of time, and thus we are technically
// still vulnerable to MITM. (TODO!)
// See docs/sts-final.pdf for more info
type SecretConnection struct { type SecretConnection struct {
conn io.ReadWriteCloser conn io.ReadWriteCloser
recvBuffer []byte recvBuffer []byte
recvNonce *[24]byte recvNonce *[aeadNonceSize]byte
sendNonce *[24]byte sendNonce *[aeadNonceSize]byte
recvSecret *[aeadKeySize]byte
sendSecret *[aeadKeySize]byte
remPubKey crypto.PubKey remPubKey crypto.PubKey
shrSecret *[32]byte // shared secret
} }
// Performs handshake and returns a new authenticated SecretConnection. // MakeSecretConnection performs handshake and returns a new authenticated
// Returns nil if error in handshake. // SecretConnection.
// Returns nil if there is an error in handshake.
// Caller should call conn.Close() // Caller should call conn.Close()
// See docs/sts-final.pdf for more information. // See docs/sts-final.pdf for more information.
func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKey) (*SecretConnection, error) { func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKey) (*SecretConnection, error) {
locPubKey := locPrivKey.PubKey() locPubKey := locPrivKey.PubKey()
// Generate ephemeral keys for perfect forward secrecy. // Generate ephemeral keys for perfect forward secrecy.
@ -59,29 +62,27 @@ func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKey) (*
return nil, err return nil, err
} }
// Compute common shared secret.
shrSecret := computeSharedSecret(remEphPub, locEphPriv)
// Sort by lexical order. // Sort by lexical order.
loEphPub, hiEphPub := sort32(locEphPub, remEphPub) loEphPub, _ := sort32(locEphPub, remEphPub)
// Check if the local ephemeral public key // Check if the local ephemeral public key
// was the least, lexicographically sorted. // was the least, lexicographically sorted.
locIsLeast := bytes.Equal(locEphPub[:], loEphPub[:]) locIsLeast := bytes.Equal(locEphPub[:], loEphPub[:])
// Generate nonces to use for secretbox. // Compute common diffie hellman secret using X25519.
recvNonce, sendNonce := genNonces(loEphPub, hiEphPub, locIsLeast) dhSecret := computeDHSecret(remEphPub, locEphPriv)
// Generate common challenge to sign. // generate the secret used for receiving, sending, challenge via hkdf-sha2 on dhSecret
challenge := genChallenge(loEphPub, hiEphPub) recvSecret, sendSecret, challenge := deriveSecretAndChallenge(dhSecret, locIsLeast)
// Construct SecretConnection. // Construct SecretConnection.
sc := &SecretConnection{ sc := &SecretConnection{
conn: conn, conn: conn,
recvBuffer: nil, recvBuffer: nil,
recvNonce: recvNonce, recvNonce: new([aeadNonceSize]byte),
sendNonce: sendNonce, sendNonce: new([aeadNonceSize]byte),
shrSecret: shrSecret, recvSecret: recvSecret,
sendSecret: sendSecret,
} }
// Sign the challenge bytes for authentication. // Sign the challenge bytes for authentication.
@ -92,6 +93,7 @@ func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKey) (*
if err != nil { if err != nil {
return nil, err return nil, err
} }
remPubKey, remSignature := authSigMsg.Key, authSigMsg.Sig remPubKey, remSignature := authSigMsg.Key, authSigMsg.Sig
if !remPubKey.VerifyBytes(challenge[:], remSignature) { if !remPubKey.VerifyBytes(challenge[:], remSignature) {
return nil, errors.New("Challenge verification failed") return nil, errors.New("Challenge verification failed")
@ -102,7 +104,7 @@ func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKey) (*
return sc, nil return sc, nil
} }
// Returns authenticated remote pubkey // RemotePubKey returns authenticated remote pubkey
func (sc *SecretConnection) RemotePubKey() crypto.PubKey { func (sc *SecretConnection) RemotePubKey() crypto.PubKey {
return sc.remPubKey return sc.remPubKey
} }
@ -124,14 +126,17 @@ func (sc *SecretConnection) Write(data []byte) (n int, err error) {
binary.BigEndian.PutUint32(frame, uint32(chunkLength)) binary.BigEndian.PutUint32(frame, uint32(chunkLength))
copy(frame[dataLenSize:], chunk) copy(frame[dataLenSize:], chunk)
aead, err := chacha20poly1305.New(sc.sendSecret[:])
if err != nil {
return n, errors.New("Invalid SecretConnection Key")
}
// encrypt the frame // encrypt the frame
var sealedFrame = make([]byte, sealedFrameSize) var sealedFrame = make([]byte, aeadSizeOverhead+totalFrameSize)
secretbox.Seal(sealedFrame[:0], frame, sc.sendNonce, sc.shrSecret) aead.Seal(sealedFrame[:0], sc.sendNonce[:], frame, nil)
// fmt.Printf("secretbox.Seal(sealed:%X,sendNonce:%X,shrSecret:%X\n", sealedFrame, sc.sendNonce, sc.shrSecret) incrNonce(sc.sendNonce)
incr2Nonce(sc.sendNonce)
// end encryption // end encryption
_, err := sc.conn.Write(sealedFrame) _, err = sc.conn.Write(sealedFrame)
if err != nil { if err != nil {
return n, err return n, err
} }
@ -148,7 +153,11 @@ func (sc *SecretConnection) Read(data []byte) (n int, err error) {
return return
} }
sealedFrame := make([]byte, sealedFrameSize) aead, err := chacha20poly1305.New(sc.recvSecret[:])
if err != nil {
return n, errors.New("Invalid SecretConnection Key")
}
sealedFrame := make([]byte, totalFrameSize+aeadSizeOverhead)
_, err = io.ReadFull(sc.conn, sealedFrame) _, err = io.ReadFull(sc.conn, sealedFrame)
if err != nil { if err != nil {
return return
@ -156,12 +165,11 @@ func (sc *SecretConnection) Read(data []byte) (n int, err error) {
// decrypt the frame // decrypt the frame
var frame = make([]byte, totalFrameSize) var frame = make([]byte, totalFrameSize)
// fmt.Printf("secretbox.Open(sealed:%X,recvNonce:%X,shrSecret:%X\n", sealedFrame, sc.recvNonce, sc.shrSecret) _, err = aead.Open(frame[:0], sc.recvNonce[:], sealedFrame, nil)
_, ok := secretbox.Open(frame[:0], sealedFrame, sc.recvNonce, sc.shrSecret) if err != nil {
if !ok {
return n, errors.New("Failed to decrypt SecretConnection") return n, errors.New("Failed to decrypt SecretConnection")
} }
incr2Nonce(sc.recvNonce) incrNonce(sc.recvNonce)
// end decryption // end decryption
var chunkLength = binary.BigEndian.Uint32(frame) // read the first two bytes var chunkLength = binary.BigEndian.Uint32(frame) // read the first two bytes
@ -176,6 +184,7 @@ func (sc *SecretConnection) Read(data []byte) (n int, err error) {
} }
// Implements net.Conn // Implements net.Conn
// nolint
func (sc *SecretConnection) Close() error { return sc.conn.Close() } func (sc *SecretConnection) Close() error { return sc.conn.Close() }
func (sc *SecretConnection) LocalAddr() net.Addr { return sc.conn.(net.Conn).LocalAddr() } func (sc *SecretConnection) LocalAddr() net.Addr { return sc.conn.(net.Conn).LocalAddr() }
func (sc *SecretConnection) RemoteAddr() net.Addr { return sc.conn.(net.Conn).RemoteAddr() } func (sc *SecretConnection) RemoteAddr() net.Addr { return sc.conn.(net.Conn).RemoteAddr() }
@ -204,18 +213,16 @@ func shareEphPubKey(conn io.ReadWriteCloser, locEphPub *[32]byte) (remEphPub *[3
var _, err1 = cdc.MarshalBinaryWriter(conn, locEphPub) var _, err1 = cdc.MarshalBinaryWriter(conn, locEphPub)
if err1 != nil { if err1 != nil {
return nil, err1, true // abort return nil, err1, true // abort
} else {
return nil, nil, false
} }
return nil, nil, false
}, },
func(_ int) (val interface{}, err error, abort bool) { func(_ int) (val interface{}, err error, abort bool) {
var _remEphPub [32]byte var _remEphPub [32]byte
var _, err2 = cdc.UnmarshalBinaryReader(conn, &_remEphPub, 1024*1024) // TODO var _, err2 = cdc.UnmarshalBinaryReader(conn, &_remEphPub, 1024*1024) // TODO
if err2 != nil { if err2 != nil {
return nil, err2, true // abort return nil, err2, true // abort
} else {
return _remEphPub, nil, false
} }
return _remEphPub, nil, false
}, },
) )
@ -230,9 +237,40 @@ func shareEphPubKey(conn io.ReadWriteCloser, locEphPub *[32]byte) (remEphPub *[3
return &_remEphPub, nil return &_remEphPub, nil
} }
func computeSharedSecret(remPubKey, locPrivKey *[32]byte) (shrSecret *[32]byte) { func deriveSecretAndChallenge(dhSecret *[32]byte, locIsLeast bool) (recvSecret, sendSecret *[aeadKeySize]byte, challenge *[32]byte) {
shrSecret = new([32]byte) hash := sha256.New
box.Precompute(shrSecret, remPubKey, locPrivKey) hkdf := hkdf.New(hash, dhSecret[:], nil, []byte("TENDERMINT_SECRET_CONNECTION_KEY_AND_CHALLENGE_GEN"))
// get enough data for 2 aead keys, and a 32 byte challenge
res := new([2*aeadKeySize + 32]byte)
_, err := io.ReadFull(hkdf, res[:])
if err != nil {
panic(err)
}
challenge = new([32]byte)
recvSecret = new([aeadKeySize]byte)
sendSecret = new([aeadKeySize]byte)
// Use the last 32 bytes as the challenge
copy(challenge[:], res[2*aeadKeySize:2*aeadKeySize+32])
// bytes 0 through aeadKeySize - 1 are one aead key.
// bytes aeadKeySize through 2*aeadKeySize -1 are another aead key.
// which key corresponds to sending and receiving key depends on whether
// the local key is less than the remote key.
if locIsLeast {
copy(recvSecret[:], res[0:aeadKeySize])
copy(sendSecret[:], res[aeadKeySize:aeadKeySize*2])
} else {
copy(sendSecret[:], res[0:aeadKeySize])
copy(recvSecret[:], res[aeadKeySize:aeadKeySize*2])
}
return
}
func computeDHSecret(remPubKey, locPrivKey *[32]byte) (shrKey *[32]byte) {
shrKey = new([32]byte)
curve25519.ScalarMult(shrKey, locPrivKey, remPubKey)
return return
} }
@ -247,25 +285,6 @@ func sort32(foo, bar *[32]byte) (lo, hi *[32]byte) {
return return
} }
func genNonces(loPubKey, hiPubKey *[32]byte, locIsLo bool) (recvNonce, sendNonce *[24]byte) {
nonce1 := hash24(append(loPubKey[:], hiPubKey[:]...))
nonce2 := new([24]byte)
copy(nonce2[:], nonce1[:])
nonce2[len(nonce2)-1] ^= 0x01
if locIsLo {
recvNonce = nonce1
sendNonce = nonce2
} else {
recvNonce = nonce2
sendNonce = nonce1
}
return
}
func genChallenge(loPubKey, hiPubKey *[32]byte) (challenge *[32]byte) {
return hash32(append(loPubKey[:], hiPubKey[:]...))
}
func signChallenge(challenge *[32]byte, locPrivKey crypto.PrivKey) (signature crypto.Signature) { func signChallenge(challenge *[32]byte, locPrivKey crypto.PrivKey) (signature crypto.Signature) {
signature, err := locPrivKey.Sign(challenge[:]) signature, err := locPrivKey.Sign(challenge[:])
// TODO(ismail): let signChallenge return an error instead // TODO(ismail): let signChallenge return an error instead
@ -288,18 +307,16 @@ func shareAuthSignature(sc *SecretConnection, pubKey crypto.PubKey, signature cr
var _, err1 = cdc.MarshalBinaryWriter(sc, authSigMessage{pubKey, signature}) var _, err1 = cdc.MarshalBinaryWriter(sc, authSigMessage{pubKey, signature})
if err1 != nil { if err1 != nil {
return nil, err1, true // abort return nil, err1, true // abort
} else {
return nil, nil, false
} }
return nil, nil, false
}, },
func(_ int) (val interface{}, err error, abort bool) { func(_ int) (val interface{}, err error, abort bool) {
var _recvMsg authSigMessage var _recvMsg authSigMessage
var _, err2 = cdc.UnmarshalBinaryReader(sc, &_recvMsg, 1024*1024) // TODO var _, err2 = cdc.UnmarshalBinaryReader(sc, &_recvMsg, 1024*1024) // TODO
if err2 != nil { if err2 != nil {
return nil, err2, true // abort return nil, err2, true // abort
} else {
return _recvMsg, nil, false
} }
return _recvMsg, nil, false
}, },
) )
@ -315,36 +332,11 @@ func shareAuthSignature(sc *SecretConnection, pubKey crypto.PubKey, signature cr
//-------------------------------------------------------------------------------- //--------------------------------------------------------------------------------
// sha256
func hash32(input []byte) (res *[32]byte) {
hasher := sha256.New()
hasher.Write(input) // nolint: errcheck, gas
resSlice := hasher.Sum(nil)
res = new([32]byte)
copy(res[:], resSlice)
return
}
// We only fill in the first 20 bytes with ripemd160
func hash24(input []byte) (res *[24]byte) {
hasher := ripemd160.New()
hasher.Write(input) // nolint: errcheck, gas
resSlice := hasher.Sum(nil)
res = new([24]byte)
copy(res[:], resSlice)
return
}
// increment nonce big-endian by 2 with wraparound.
func incr2Nonce(nonce *[24]byte) {
incrNonce(nonce)
incrNonce(nonce)
}
// increment nonce big-endian by 1 with wraparound. // increment nonce big-endian by 1 with wraparound.
func incrNonce(nonce *[24]byte) { func incrNonce(nonce *[aeadNonceSize]byte) {
for i := 23; 0 <= i; i-- { for i := aeadNonceSize - 1; 0 <= i; i-- {
nonce[i]++ nonce[i]++
// if this byte wrapped around to zero, we need to increment the next byte
if nonce[i] != 0 { if nonce[i] != 0 {
return return
} }