diff --git a/brontide/conn.go b/brontide/conn.go new file mode 100644 index 000000000..a61632e3a --- /dev/null +++ b/brontide/conn.go @@ -0,0 +1,171 @@ +package brontide + +import ( + "bytes" + "io" + "net" + "time" + + "github.com/roasbeef/btcd/btcec" +) + +// Conn is an implementation of net.Conn which enforces an authenticated key +// exchange and message encryption protocol dubbed "Brontide" after initial TCP +// connection establishment. In the case of a successful handshake, all +// messages sent via the .Write() method are encrypted with an AEAD cipher +// along with an encrypted length-prefix. See the BrontideMachine struct for +// additional details w.r.t to the handshake and encryption scheme. +type Conn struct { + conn net.Conn + + noise *BrontideMachine + + readBuf bytes.Buffer +} + +// A compile-time assertion to ensure that Conn meets the net.Conn interface. +var _ net.Conn = (*Conn)(nil) + +// Dial attempts to establish an encrypted+authenticated connection with the +// remote peer located at address which has remotePub as its long-term static +// public key. In the case of a handshake failure, the connection is closed and +// a non-nil error is returned. +func Dial(localPriv *btcec.PrivateKey, remotePub *btcec.PublicKey, + address string) (*Conn, error) { + + conn, err := net.Dial("tcp", address) + if err != nil { + return nil, err + } + + b := &Conn{ + conn: conn, + noise: NewBrontideMachine(true, localPriv, remotePub), + } + + // Initiate the handshake by sending the first act to the receiver. + actOne, err := b.noise.GenActOne() + if err != nil { + return nil, err + } + if _, err := conn.Write(actOne[:]); err != nil { + return nil, err + } + + // If the first act was successful (we know that address is actually + // remotePub), then read the second act after which we'll be able to + // send our static public key to the remote peer with strong forward + // secrecy. + var actTwo [ActTwoSize]byte + if _, err := io.ReadFull(conn, actTwo[:]); err != nil { + return nil, err + } + if err := b.noise.RecvActTwo(actTwo); err != nil { + return nil, err + } + + // Finally, complete the handshake by sending over our encrypted static + // key and execute the final ECDH operation. + actThree, err := b.noise.GenActThree() + if err != nil { + return nil, err + } + if _, err := conn.Write(actThree[:]); err != nil { + return nil, err + } + + return b, nil +} + +// Read reads data from the connection. Read can be made to time out and +// return a Error with Timeout() == true after a fixed time limit; see +// SetDeadline and SetReadDeadline. +// +// Part of the net.Conn interface. +func (c *Conn) Read(b []byte) (n int, err error) { + // In order to reconcile the differences between the record abstraction of + // our AEAD connection, and the stream abstraction of TCP, we maintain an + // intermediate read buffer. If this buffer becomes depleated, then we read + // the next record, and feed it into the buffer. Otherwise, we read + // directly from the buffer. + if c.readBuf.Len() == 0 { + plaintext, err := c.noise.ReadMessage(c.conn) + if err != nil { + return 0, err + } + + if _, err := c.readBuf.Write(plaintext); err != nil { + return 0, err + } + } + + return c.readBuf.Read(b) +} + +// Write writes data to the connection. Write can be made to time out and +// return a Error with Timeout() == true after a fixed time limit; see +// SetDeadline and SetWriteDeadline. +// +// Part of the net.Conn interface. +func (c *Conn) Write(b []byte) (n int, err error) { + return len(b), c.noise.WriteMessage(c.conn, b) +} + +// Close closes the connection. Any blocked Read or Write operations will be +// unblocked and return errors. +// +// Part of the net.Conn interface. +func (c *Conn) Close() error { + // TODO(roasbeef): reset brontide state? + return c.conn.Close() +} + +// LocalAddr returns the local network address. +// +// Part of the net.Conn interface. +func (c *Conn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +// RemoteAddr returns the remote network address. +// +// Part of the net.Conn interface. +func (c *Conn) RemoteAddr() net.Addr { + return c.conn.RemoteAddr() +} + +// SetDeadline sets the read and write deadlines associated with the +// connection. It is equivalent to calling both SetReadDeadline and +// SetWriteDeadline. +// +// Part of the net.Conn interface. +func (c *Conn) SetDeadline(t time.Time) error { + return c.conn.SetDeadline(t) +} + +// SetReadDeadline sets the deadline for future Read calls. A zero value for t +// means Read will not time out. +// +// Part of the net.Conn interface. +func (c *Conn) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +// SetWriteDeadline sets the deadline for future Write calls. Even if write +// times out, it may return n > 0, indicating that some of the data was +// successfully written. A zero value for t means Write will not time out. +// +// Part of the net.Conn interface. +func (c *Conn) SetWriteDeadline(t time.Time) error { + return c.conn.SetWriteDeadline(t) +} + +// RemotePub returns the remote peer's static public key. +func (c *Conn) RemotePub() *btcec.PublicKey { + return c.noise.remoteStatic +} + +// LocalPub returns the local peer's static public key. +func (c *Conn) LocalPub() *btcec.PublicKey { + return c.noise.localStatic.PubKey() +} diff --git a/brontide/listener.go b/brontide/listener.go new file mode 100644 index 000000000..a8c0e03d0 --- /dev/null +++ b/brontide/listener.go @@ -0,0 +1,110 @@ +package brontide + +import ( + "io" + "net" + + "github.com/roasbeef/btcd/btcec" +) + +// Listener is an implementation of a net.Conn which executes an authenticated +// key exchange and message encryption protocol dubeed "BrontideMachine" after +// initial connection acceptance. See the BrontideMachine struct for additional +// details w.r.t the handshake and encryption scheme used within the +// connection. +type Listener struct { + localStatic *btcec.PrivateKey + + tcp *net.TCPListener +} + +// A compile-time assertion to ensure that Conn meets the net.Listener interface. +var _ net.Listener = (*Listener)(nil) + +// NewListener returns a new net.Listener which enforces the Brontide scheme +// during both initial connection establishment and data transfer. +func NewListener(localStatic *btcec.PrivateKey, listenAddr string) (*Listener, + error) { + addr, err := net.ResolveTCPAddr("tcp", listenAddr) + if err != nil { + return nil, err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return nil, err + } + + return &Listener{ + localStatic: localStatic, + tcp: l, + }, nil +} + +// Accept waits for and returns the next connection to the listener. All +// incoming connections are authenticated via the three act Brontide +// key-exchange scheme. This funciton will fail with a non-nil error in the +// case that either the handhska breaks down, or the remote peer doesn't know +// our static public key. +// +// Part of the net.Listener interface. +func (l *Listener) Accept() (net.Conn, error) { + conn, err := l.tcp.Accept() + if err != nil { + return nil, err + } + + brontideConn := &Conn{ + conn: conn, + noise: NewBrontideMachine(false, l.localStatic, nil), + } + + // Attempt to carry out the first act of the handshake protocol. If the + // connecting node doesn't know our long-term static public key, then + // this portion will fail with a non-nil error. + var actOne [ActOneSize]byte + if _, err := io.ReadFull(conn, actOne[:]); err != nil { + return nil, err + } + if err := brontideConn.noise.RecvActOne(actOne); err != nil { + return nil, err + } + + // Next, progress the handshake processes by sending over our ephemeral + // key for the session along with an authenticating tag. + actTwo, err := brontideConn.noise.GenActTwo() + if err != nil { + return nil, err + } + if _, err := conn.Write(actTwo[:]); err != nil { + return nil, err + } + + // Finally, finish the handhskae processes by reading and decrypting + // the conneciton peer's static public key. If this succeeeds then both + // sides have mutually authenticated each other. + var actThree [ActThreeSize]byte + if _, err := io.ReadFull(conn, actThree[:]); err != nil { + return nil, err + } + if err := brontideConn.noise.RecvActThree(actThree); err != nil { + return nil, err + } + + return brontideConn, nil +} + +// Close closes the listener. Any blocked Accept operations will be unblocked +// and return errors. +// +// Part of the net.Listener interface. +func (l *Listener) Close() error { + return l.tcp.Close() +} + +// Addr returns the listener's network address. +// +// Part of the net.Listener interface. +func (l *Listener) Addr() net.Addr { + return l.tcp.Addr() +} diff --git a/brontide/noise.go b/brontide/noise.go new file mode 100644 index 000000000..fdf91924b --- /dev/null +++ b/brontide/noise.go @@ -0,0 +1,587 @@ +package brontide + +import ( + "crypto/cipher" + "crypto/sha256" + "encoding/binary" + "io" + + "golang.org/x/crypto/hkdf" + + "github.com/aead/chacha20" + "github.com/roasbeef/btcd/btcec" +) + +const ( + // protocolName is the precise instantiation of the Noise protocol + // handshake at the center of Brontide. This value will be used as part + // of the prologue. If the initiator and responder aren't using the + // exact same string for this value, along with prologue of the Bitcoin + // network, then the initial handshake will fail. + protocolName = "Noise_XK_secp256k1_ChaChaPoly_SHA256" +) + +// TODO(roasbeef): free buffer pool? + +// cipherState encapsulates the state for the AEAD which will be used to +// encrypt+authenticate any payloads sent during the handshake, and messages +// sent once the handshake has completed. +type cipherState struct { + // nonce is the nonce passed into the chacha20-poly1305 instance for + // encryption+decryption. The nonce is incremented after each succesful + // encryption/decryption. + // + // TODO(roasbeef): this should actually be 96 bit + nonce uint64 + + // secretKey is the shared symmetric key which will be used to + // instantiate the cipher. + // + // TODO(roasbeef): m-lock?? + secretKey [32]byte + + // cipher is an instance of the ChaCha20-Poly1305 AEAD construction + // created using the secretKey above. + cipher cipher.AEAD +} + +// Encrypt returns a ciphertext which is the encryption of the plainText +// observing the passed associatedData within the AEAD construction. +func (c *cipherState) Encrypt(associatedData, cipherText, plainText []byte) []byte { + defer func() { + c.nonce++ + }() + + var nonce [12]byte + binary.LittleEndian.PutUint64(nonce[:], c.nonce) + + return c.cipher.Seal(cipherText, nonce[:], plainText, associatedData) +} + +// Decrypt attempts to decrypt the passed ciphertext observing the specified +// associatedData within the AEAD construction. In the case that the final MAC +// check fails, then a non-nil error will be returned. +func (c *cipherState) Decrypt(associatedData, plainText, cipherText []byte) ([]byte, error) { + defer func() { + c.nonce++ + }() + + var nonce [12]byte + binary.LittleEndian.PutUint64(nonce[:], c.nonce) + + return c.cipher.Open(plainText, nonce[:], cipherText, associatedData) +} + +// InitializeKey initializes the secret key and AEAD cipher scheme based off of +// the passed key. +func (c *cipherState) InitializeKey(key [32]byte) { + c.secretKey = key + c.nonce = 0 + c.cipher = chacha20.NewChaCha20Poly1305(&c.secretKey) +} + +// symmetricState encapsulates a cipherState object and houses the ephemeral +// handshake digest state. This struct is used during the handshake to derive +// new shared secrets based off of the result of ECDH operations. Ultimately, +// the final key yielded by this struct is the result of an incremental +// Triple-DH operation. +type symmetricState struct { + cipherState + + // chainingKey is used as the salt to the HKDF function to derive a new + // chaining key as well as a new tempKey which is used for + // encryption/decryption. + chainingKey [32]byte + + // tempKey is the latter 32 bytes resulted from the latest HKDF + // iteration. This key is used to encrypt/decrypt any handshake + // messages or payloads sent until the next DH operation is executed. + tempKey [32]byte + + // handshakeDigest is the cummulative hash digest of all handshake + // messages sent from start to finish. This value is never transmitted + // to the other side, but will be used as the AD when + // encrypting/decrypting messages using our AEAD construction. + handshakeDigest [32]byte +} + +// mixKey is implements a basic HKDF-based key rachet. This method is called +// with the result of each DH output generated during the handshake process. +// The first 32 bytes extract from the HKDF reader is the next chaining key, +// then latter 32 bytes become the temp secret key using within any future AEAD +// operations until another DH operation is performed. +func (s *symmetricState) mixKey(input []byte) { + var info []byte + + secret := input + salt := s.chainingKey + h := hkdf.New(sha256.New, secret, salt[:], info) + + // hkdf(input, ck, zero) + // | + // | \ + // | \ + // ck k + h.Read(s.chainingKey[:]) + h.Read(s.tempKey[:]) + + // cipher.k = temp_key + s.InitializeKey(s.tempKey) +} + +// mixHash hashes the passed input data into the cummulative handshake digest. +// The running result of this value (h) is used as the associated data in all +// decryption/encryption operations. +func (s *symmetricState) mixHash(data []byte) { + h := sha256.New() + h.Write(s.handshakeDigest[:]) + h.Write(data) + + copy(s.handshakeDigest[:], h.Sum(nil)) +} + +// EncryptAndHash returns the authenticated encryption of the passed plaintext. +// When encrypting the handshake digest (h) is used as the associated data to +// the AEAD cipher. +func (s *symmetricState) EncryptAndHash(plaintext []byte) []byte { + ciphertext := s.Encrypt(s.handshakeDigest[:], nil, plaintext) + + s.mixHash(ciphertext) + + return ciphertext +} + +// DecryptAndHash returns the authenticated decryption of the passed +// ciphertext. When encrypting the handshake digest (h) is used as the +// associated data to the AEAD cipher. +func (s *symmetricState) DecryptAndHash(ciphertext []byte) ([]byte, error) { + plaintext, err := s.Decrypt(s.handshakeDigest[:], nil, ciphertext) + if err != nil { + return nil, err + } + + s.mixHash(ciphertext) + + return plaintext, nil +} + +// InitializeSymmetric initializes the symmetric state by setting the handshake +// digest (h) and the chaining key (ck) to protocol name. +func (s *symmetricState) InitializeSymmetric(protocolName []byte) { + var empty [32]byte + + s.handshakeDigest = sha256.Sum256(protocolName) + s.chainingKey = s.handshakeDigest + s.InitializeKey(empty) +} + +// handshakeState encapsulates the symmetricState and keeps track of all the +// public keys (static and ephemeral) for both sides during the handshake +// transscript. If the handshake completes successfuly, then two instances of a +// cipherState are emitted: one to encrypt messages from initiator to +// responder, and the other for the opposite direction. +type handshakeState struct { + symmetricState + + initiator bool + + localStatic *btcec.PrivateKey + localEphemeral *btcec.PrivateKey + + remoteStatic *btcec.PublicKey + remoteEphemeral *btcec.PublicKey +} + +// newHandshakeState returns a new instance of the handshake state initialized +// with the prologue and protocol name. If this is the respodner's handshake +// state, then the remotePub can be nil. +func newHandshakeState(initiator bool, prologue []byte, + localPub *btcec.PrivateKey, remotePub *btcec.PublicKey) handshakeState { + + h := handshakeState{ + initiator: initiator, + localStatic: localPub, + remoteStatic: remotePub, + } + + // Set the current chainking key and handshake digest to the hash of + // the protocol name, and additionally mix in the prologue. If either + // sides disagree about the prologue or protocol name, then the + // handshake will fail. + h.InitializeSymmetric([]byte(protocolName)) + h.mixHash(prologue) + + // In Noise_XK, then initiator should know the responder's static + // public key, therefore we include the responder's static key in the + // handshake digest. If the initiator gets this value wrong, then the + // handshake will fail. + if initiator { + h.mixHash(remotePub.SerializeCompressed()) + } else { + h.mixHash(localPub.PubKey().SerializeCompressed()) + } + + return h +} + +// BrontideMachine is a state-machine which implements Brontide: an +// Authenticated-key Exchange in Three Acts. Brontide is derived from the Noise +// framework, specifically implementing the Noise_XK handshake. Once the +// initial 3-act handshake has completed all messages are encrypted with a +// chacha20 AEAD cipher. On the wire, all messages are prefixed with an +// authenticated+encrypted length field. Additionally, the encrypted+auth'd +// length prefix is used as the AD when encrypting+decryption messages. This +// construction provides confidentiallity of packet length, avoids introducing +// a padding-oracle, and binds the encrypted packet length to the packet +// itself. +// +// The acts proceeds the following order (initiator on the left): +// GenActOne() -> +// RecvActOne() +// <- GenActTwo() +// RecvActTwo() +// GenActThree() -> +// RecvActThree() +// +// This exchange corresponds to the following Noise handshake: +// <- s +// ... +// -> e, es +// <- e, ee +// -> s, se +type BrontideMachine struct { + sendCipher cipherState + recvCipher cipherState + + handshakeState +} + +// NewBrontideMachine creates a new instance of the brontide state-machine. If +// the responder (listener) is creating the object, then the remotePub should +// be nil. The handshake state within brontide is initialized using the ascii +// string "bitcoin" as the prologue. +func NewBrontideMachine(initiator bool, localPub *btcec.PrivateKey, + remotePub *btcec.PublicKey) *BrontideMachine { + + handshake := newHandshakeState(initiator, []byte("bitcoin"), localPub, + remotePub) + + return &BrontideMachine{handshakeState: handshake} +} + +const ( + // ActOneSize is the size of the packet sent from initiator to + // responder in ActOne. The packet consists of an ephemeral key in + // compressed format, and a 16-byte poly1305 tag. + // + // 33 + 16 + ActOneSize = 49 + + // ActTwoSize is the size the packet sent from responder to initiator + // in ActTwo. The packet consists of an ephemeral key in compressed + // format and a 16-byte poly1305 tag. + // + // 33 + 16 + ActTwoSize = 49 + + // ActThreeSize is the size of the packet sent from initiator to + // responder in ActThree. The packet consists of the initiators static + // key encrypted with strong forward secrecy and a 16-byte poly1035 + // tag. + // + // 33 + 16 + 16 + ActThreeSize = 65 +) + +// GenActOne generates the initial packet (act one) to be sent from initiator +// to responder. During act one the initiator generates a fresh ephemeral key, +// hashes it into the handshake digest, and performs an ECDH between this key +// and the responder's static key. Future payloads are encrypted with a key +// dervied from this result. +// +// -> e, es +func (b *BrontideMachine) GenActOne() ([ActOneSize]byte, error) { + var ( + err error + actOne [ActOneSize]byte + ) + + // e + b.localEphemeral, err = btcec.NewPrivateKey(btcec.S256()) + if err != nil { + return actOne, err + } + + ephemeral := b.localEphemeral.PubKey().SerializeCompressed() + b.mixHash(ephemeral) + + // es + s := btcec.GenerateSharedSecret(b.localEphemeral, b.remoteStatic) + b.mixKey(s) + + authPayload := b.EncryptAndHash([]byte{}) + + copy(actOne[:33], ephemeral) + copy(actOne[33:], authPayload) + + return actOne, nil +} + +// RecvActOne processes the act one packet sent by the initiator. The responder +// executes the mirroed actions to that of the initiator extending the +// handshake digest and deriving a new shared secret based on a ECDH with the +// initiator's ephemeral key and reponder's static key. +func (b *BrontideMachine) RecvActOne(actOne [ActOneSize]byte) error { + var ( + err error + e [33]byte + p [16]byte + ) + + copy(e[:], actOne[:33]) + copy(p[:], actOne[33:]) + + // e + b.remoteEphemeral, err = btcec.ParsePubKey(e[:], btcec.S256()) + if err != nil { + return err + } + b.mixHash(b.remoteEphemeral.SerializeCompressed()) + + // es + s := btcec.GenerateSharedSecret(b.localStatic, b.remoteEphemeral) + b.mixKey(s) + + // If the initiator doesn't know our static key, then this operation + // will fail. + if _, err := b.DecryptAndHash(p[:]); err != nil { + return err + } + + return nil +} + +// GenActTwo generates the second packet (act two) to be sent from the +// responder to the initiator. The packet for act two is identify to that of +// act one, but then results in a different ECDH operation between the +// initiator's and responder's ephemeral keys. +// +// <- e, ee +func (b *BrontideMachine) GenActTwo() ([ActTwoSize]byte, error) { + var ( + err error + actTwo [ActTwoSize]byte + ) + + // e + b.localEphemeral, err = btcec.NewPrivateKey(btcec.S256()) + if err != nil { + return actTwo, err + } + + ephemeral := b.localEphemeral.PubKey().SerializeCompressed() + b.mixHash(b.localEphemeral.PubKey().SerializeCompressed()) + + // ee + s := btcec.GenerateSharedSecret(b.localEphemeral, b.remoteEphemeral) + b.mixKey(s) + + authPayload := b.EncryptAndHash([]byte{}) + + copy(actTwo[:33], ephemeral) + copy(actTwo[33:], authPayload) + + return actTwo, nil +} + +// RecvActTwo processes the second packet (act two) sent from the responder to +// the initiator. A succesful processing of this packet authenticates the +// initiator to the responder. +func (b *BrontideMachine) RecvActTwo(actTwo [ActTwoSize]byte) error { + var ( + err error + e [33]byte + p [16]byte + ) + + copy(e[:], actTwo[:33]) + copy(p[:], actTwo[33:]) + + // e + b.remoteEphemeral, err = btcec.ParsePubKey(e[:], btcec.S256()) + if err != nil { + return err + } + b.mixHash(b.remoteEphemeral.SerializeCompressed()) + + // ee + s := btcec.GenerateSharedSecret(b.localEphemeral, b.remoteEphemeral) + b.mixKey(s) + + if _, err := b.DecryptAndHash(p[:]); err != nil { + return err + } + + return nil +} + +// GenActThree creates the final (act three) packet of the handshake. Act three +// is to be sent from the initiator to the responder. The purpose of act three +// is to transmit the initiator's public key under strong forwad secrecy to the +// responder. This act also includes the final ECDH operation which yields the +// final session. +// +// -> s, se +func (b *BrontideMachine) GenActThree() ([ActThreeSize]byte, error) { + var actThree [ActThreeSize]byte + + ourPubkey := b.localStatic.PubKey().SerializeCompressed() + ciphertext := b.EncryptAndHash(ourPubkey) + + s := btcec.GenerateSharedSecret(b.localStatic, b.remoteEphemeral) + b.mixKey(s) + + authPayload := b.EncryptAndHash([]byte{}) + + copy(actThree[:49], ciphertext) + copy(actThree[49:], authPayload) + + // With the final ECDH operation complete, derive the session sending + // and receiving keys. + b.split() + + return actThree, nil +} + +// RecvActThree processes the final act (act three) sent from the initiator to +// the responder. After processing this act, the responder learns of the +// initiators's static public key. Decryption of the static key serves to +// authenticate the initiator to the responder. +func (b *BrontideMachine) RecvActThree(actThree [ActThreeSize]byte) error { + var ( + err error + s [33 + 16]byte + p [16]byte + ) + + copy(s[:], actThree[:33+16]) + copy(p[:], actThree[33+16:]) + + // s + remotePub, err := b.DecryptAndHash(s[:]) + if err != nil { + return err + } + b.remoteStatic, err = btcec.ParsePubKey(remotePub, btcec.S256()) + if err != nil { + return err + } + + // se + se := btcec.GenerateSharedSecret(b.localEphemeral, b.remoteStatic) + b.mixKey(se) + + if _, err := b.DecryptAndHash(p[:]); err != nil { + return err + } + + // With the final ECDH operation complete, derive the session sending + // and receiving keys. + b.split() + + return nil +} + +// split is the final wrap-up act to be executed at the end of a succesful +// three act handshake. This function creates to internal cipherState +// instances: one which is used to encrypt messages from the initiator to the +// responder, and another which is used to encrypt message for the opposite +// direction. +func (b *BrontideMachine) split() { + var ( + empty []byte + sendKey [32]byte + recvKey [32]byte + ) + + h := hkdf.New(sha256.New, b.chainingKey[:], empty, empty) + + // If we're the initiator the the frist 32 bytes are used to encrypt + // our messages and the second 32-bytes to decrypt their messages. For + // the responder the opposite is true. + if b.initiator { + h.Read(sendKey[:]) + b.sendCipher = cipherState{} + b.sendCipher.InitializeKey(sendKey) + + h.Read(recvKey[:]) + b.recvCipher = cipherState{} + b.recvCipher.InitializeKey(recvKey) + } else { + h.Read(recvKey[:]) + b.recvCipher = cipherState{} + b.recvCipher.InitializeKey(recvKey) + + h.Read(sendKey[:]) + b.sendCipher = cipherState{} + b.sendCipher.InitializeKey(sendKey) + } +} + +// WriteMessage writes the next message p to the passed io.Writer. The +// ciphertext of the message is pre-pended with an encrypt+auth'd length which +// must be used as the AD to the AEAD construction when being decrypted by the +// other side. +func (b *BrontideMachine) WriteMessage(w io.Writer, p []byte) error { + // The full length of the packet includes the 16 byte MAC. + fullLength := uint64(len(p) + 16) + + // TODO(roasbeef): The Summit decided on 24 bits? + var pktLen [8]byte + binary.BigEndian.PutUint64(pktLen[:], fullLength) + + // First, write out the encrypted+MAC'd length prefix for the packet. + cipherLen := b.sendCipher.Encrypt(nil, nil, pktLen[:]) + if _, err := w.Write(cipherLen); err != nil { + return err + } + + // Next, write out the encrypted packet itself. We use the encrypted + // packet length above as the AD to the cipher in order to bind both + // messages together thwarting an active attack. + cipherText := b.sendCipher.Encrypt(cipherLen, nil, p) + if _, err := w.Write(cipherText); err != nil { + return err + } + + return nil +} + +// ReadMessage attemps to read the next message from the passed io.Reader. In +// the case of an authentication error, a non-nil error is returned. +func (b *BrontideMachine) ReadMessage(r io.Reader) ([]byte, error) { + var cipherLen [8 + 16]byte + if _, err := io.ReadFull(r, cipherLen[:]); err != nil { + return nil, err + } + + // Attempt to decrypt+auth the packet length present in the stream. + pktLenBytes, err := b.recvCipher.Decrypt(nil, nil, cipherLen[:]) + if err != nil { + return nil, err + } + + // Next, using the length read from the packet header, read the + // encrypted packet itself. + pktLen := binary.BigEndian.Uint64(pktLenBytes) + ciperText := make([]byte, pktLen) + if _, err := io.ReadFull(r, ciperText[:]); err != nil { + return nil, err + } + + // Finally, return the decrypted packet ensuring that the encrypted + // packet length is authenticated along with the packet itself. + return b.recvCipher.Decrypt(cipherLen[:], nil, ciperText) +} + +// TODO(roasbeef): key rotation diff --git a/brontide/noise_test.go b/brontide/noise_test.go new file mode 100644 index 000000000..d228955b0 --- /dev/null +++ b/brontide/noise_test.go @@ -0,0 +1,98 @@ +package brontide + +import ( + "bytes" + "net" + "testing" + + "github.com/roasbeef/btcd/btcec" +) + +func TestConnectionCorrectness(t *testing.T) { + // First, generate the long-term private keys both ends of the connection + // within our test. + localPriv, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + t.Fatalf("unable to generate local priv key: %v", err) + } + remotePriv, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + t.Fatalf("unable to generate remote priv key: %v", err) + } + + // Having a port of ":0" means a random port, and interface will be + // chosen for our listener. + addr := ":0" + + // Our listener will be local, and the connection remote. + listener, err := NewListener(localPriv, addr) + if err != nil { + t.Fatalf("unable to create listener: %v", err) + } + defer listener.Close() + + listenAddr := listener.Addr().String() + + // Initiate a connection with a separate goroutine, and listen with our + // main one. If both errors are nil, then encryption+auth was succesful. + errChan := make(chan error) + connChan := make(chan net.Conn) + go func() { + conn, err := Dial(remotePriv, localPriv.PubKey(), listenAddr) + errChan <- err + connChan <- conn + }() + + localConn, listenErr := listener.Accept() + if listenErr != nil { + t.Fatalf("unable to accept connection: %v", listenErr) + } + + if dialErr := <-errChan; err != nil { + t.Fatalf("unable to establish connection: %v", dialErr) + } + conn := <-connChan + + // Test out some message full-message reads. + for i := 0; i < 10; i++ { + msg := []byte("hello" + string(i)) + + if _, err := conn.Write(msg); err != nil { + t.Fatalf("remote conn failed to write: %v", err) + } + + readBuf := make([]byte, len(msg)) + if _, err := localConn.Read(readBuf); err != nil { + t.Fatalf("local conn failed to read: %v", err) + } + + if !bytes.Equal(readBuf, msg) { + t.Fatalf("messages don't match, %v vs %v", + string(readBuf), string(msg)) + } + } + + // Now try incremental message reads. This simulates first writing a + // message header, then a message body. + outMsg := []byte("hello world") + if _, err := conn.Write(outMsg); err != nil { + t.Fatalf("remote conn failed to write: %v", err) + } + + readBuf := make([]byte, len(outMsg)) + if _, err := localConn.Read(readBuf[:len(outMsg)/2]); err != nil { + t.Fatalf("local conn failed to read: %v", err) + } + if _, err := localConn.Read(readBuf[len(outMsg)/2:]); err != nil { + t.Fatalf("local conn failed to read: %v", err) + } + + if !bytes.Equal(outMsg, readBuf) { + t.Fatalf("messages don't match, %v vs %v", + string(readBuf), string(outMsg)) + } +} + +func TestNoiseIdentityHiding(t *testing.T) { + // TODO(roasbeef): fin +}