diff --git a/lndc/conn.go b/lndc/conn.go new file mode 100644 index 00000000..71186217 --- /dev/null +++ b/lndc/conn.go @@ -0,0 +1,368 @@ +package lndc + +import ( + "bytes" + "crypto/cipher" + "encoding/binary" + "fmt" + "net" + "time" + + "github.com/btcsuite/btcutil" + "github.com/btcsuite/fastsha256" + "github.com/codahale/chacha20poly1305" + + "github.com/btcsuite/btcd/btcec" +) + +// Conn... +type Conn struct { + longTermPriv *btcec.PrivateKey + + remotePub *btcec.PublicKey + remoteLNId [16]byte + + myNonceInt uint64 + remoteNonceInt uint64 + + // If Authed == false, the remotePub is the EPHEMERAL key. + // once authed == true, remotePub is who you're actually talking to. + authed bool + + // chachaStream saves some time as you don't have to init it with + // the session key every time. Make SessionKey redundant; remove later. + chachaStream cipher.AEAD + + // ViaPbx specifies whether this is a direct TCP connection or an + // encapsulated PBX connection. + // If going ViaPbx, Cn isn't used channels are used for Read() and + // Write(), which are filled by the PBXhandler. + viaPbx bool + pbxIncoming chan []byte + pbxOutgoing chan []byte + + version uint8 + + readBuf bytes.Buffer + + conn net.Conn +} + +// NewConn... +func NewConn(connPrivKey *btcec.PrivateKey, conn net.Conn) *Conn { + return &Conn{longTermPriv: connPrivKey, conn: conn} +} + +// Dial... +func (c *Conn) Dial(address string, remoteId []byte) error { + var err error + if c.conn != nil { + return fmt.Errorf("connection already established") + } + + // Before dialing out to the remote host, verify that `remoteId` is either + // a pubkey or a pubkey hash. + if len(remoteId) != 33 && len(remoteId) != 20 { + return fmt.Errorf("must supply either remote pubkey or " + + "pubkey hash") + } + + // First, open the TCP connection itself. + c.conn, err = net.Dial("tcp", address) + if err != nil { + return err + } + + // Calc remote LNId; need this for creating pbx connections just because + // LNid is in the struct does not mean it's authed! + if len(remoteId) == 20 { + copy(c.remoteLNId[:], remoteId[:16]) + } else { + theirAdr := btcutil.Hash160(remoteId) + copy(c.remoteLNId[:], theirAdr[:16]) + } + + // Make up an ephemeral keypair for this session. + ourEphemeralPriv, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + return err + } + ourEphemeralPub := ourEphemeralPriv.PubKey() + + // Sned 1. Send my ephemeral pubkey. Can add version bits. + if _, err = writeClear(c.conn, ourEphemeralPub.SerializeCompressed()); err != nil { + return err + } + + // Read, then deserialize their ephemeral public key. + theirEphPubBytes, err := readClear(c.conn) + if err != nil { + return err + } + theirEphPub, err := btcec.ParsePubKey(theirEphPubBytes, btcec.S256()) + if err != nil { + return err + } + + // Do non-interactive diffie with ephemeral pubkeys. Sha256 for good + // luck. + sessionKey := fastsha256.Sum256( + btcec.GenerateSharedSecret(ourEphemeralPriv, theirEphPub), + ) + + // Now that we've derive the session key, we can initialize the + // chacha20poly1305 AEAD instance which will be used for the remainder of + // the session. + c.chachaStream, err = chacha20poly1305.New(sessionKey[:]) + if err != nil { + return err + } + + // display private key for debug only + fmt.Printf("made session key %x\n", sessionKey) + + c.myNonceInt = 1 << 63 + c.remoteNonceInt = 0 + + c.remotePub = theirEphPub + c.authed = false + + // Session is now open and confidential but not yet authenticated... + // So auth! + if len(remoteId) == 20 { + // Only know pubkey hash (20 bytes). + err = c.authPKH(remoteId, ourEphemeralPub.SerializeCompressed()) + } else { + // Must be 33 byte pubkey. + err = c.authPubKey(remoteId, ourEphemeralPub.SerializeCompressed()) + } + if err != nil { + return err + } + + return nil +} + +// authPubKey... +func (c *Conn) authPubKey(remotePubBytes, localEphPubBytes []byte) error { + if c.authed { + return fmt.Errorf("%s already authed", c.remotePub) + } + + // Since we already know their public key, we can immediately generate + // the DH proof without an additional round-trip. + theirPub, err := btcec.ParsePubKey(remotePubBytes, btcec.S256()) + if err != nil { + return err + } + theirPKH := btcutil.Hash160(remotePubBytes) + idDH := fastsha256.Sum256(btcec.GenerateSharedSecret(c.longTermPriv, theirPub)) + myDHproof := btcutil.Hash160(append(c.remotePub.SerializeCompressed(), idDH[:]...)) + + // Send over the 73 byte authentication message: my pubkey, their + // pubkey hash, DH proof. + var authMsg [73]byte + copy(authMsg[:33], c.longTermPriv.PubKey().SerializeCompressed()) + copy(authMsg[33:], theirPKH) + copy(authMsg[53:], myDHproof) + if _, err = c.conn.Write(authMsg[:]); err != nil { + return nil + } + + // Await, their response. They should send only the 20-byte DH proof. + resp := make([]byte, 20) + _, err = c.conn.Read(resp) + if err != nil { + return err + } + + // Verify that their proof matches our locally computed version. + theirDHproof := btcutil.Hash160(append(localEphPubBytes, idDH[:]...)) + if bytes.Equal(resp, theirDHproof) == false { + return fmt.Errorf("invalid DH proof %x", theirDHproof) + } + + // Proof checks out, auth complete. + c.remotePub = theirPub + theirAdr := btcutil.Hash160(theirPub.SerializeCompressed()) + copy(c.remoteLNId[:], theirAdr[:16]) + c.authed = true + + return nil +} + +// authPKH... +func (c *Conn) authPKH(theirPKH, localEphPubBytes []byte) error { + if c.authed { + return fmt.Errorf("%s already authed", c.remotePub) + } + if len(theirPKH) != 20 { + return fmt.Errorf("remote PKH must be 20 bytes, got %d", + len(theirPKH)) + } + + // Send 53 bytes: our pubkey, and the remote's pubkey hash. + var greetingMsg [53]byte + copy(greetingMsg[:33], c.longTermPriv.PubKey().SerializeCompressed()) + copy(greetingMsg[:33], theirPKH) + if _, err := c.conn.Write(greetingMsg[:]); err != nil { + return err + } + + // Wait for their response. + // TODO(tadge): add timeout here + // * NOTE(roasbeef): read timeout should be set on the underlying + // net.Conn. + resp := make([]byte, 53) + if _, err := c.conn.Read(resp); err != nil { + return err + } + + // Parse their long-term public key, and generate the DH proof. + theirPub, err := btcec.ParsePubKey(resp[:33], btcec.S256()) + if err != nil { + return err + } + idDH := fastsha256.Sum256(btcec.GenerateSharedSecret(c.longTermPriv, theirPub)) + fmt.Printf("made idDH %x\n", idDH) + theirDHproof := btcutil.Hash160(append(localEphPubBytes, idDH[:]...)) + + // Verify that their DH proof matches the one we just generated. + if bytes.Equal(resp[33:], theirDHproof) == false { + return fmt.Errorf("Invalid DH proof %x", theirDHproof) + } + + // If their DH proof checks out, then send our own. + myDHproof := btcutil.Hash160(append(c.remotePub.SerializeCompressed(), idDH[:]...)) + if _, err = c.conn.Write(myDHproof); err != nil { + return err + } + + // Proof sent, auth complete. + c.remotePub = theirPub + theirAdr := btcutil.Hash160(theirPub.SerializeCompressed()) + copy(c.remoteLNId[:], theirAdr[:16]) + c.authed = true + + return 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 { + // The buffer is empty, so read the next cipher text. + ctext, err := readClear(c.conn) + if err != nil { + return 0, err + } + + // Encode the current remote nonce, so we can use it to decrypt + // the cipher text. + var nonceBuf [8]byte + binary.BigEndian.PutUint64(nonceBuf[:], c.remoteNonceInt) + + fmt.Printf("decrypt %d byte from %x nonce %d\n", + len(ctext), c.remoteLNId, c.remoteNonceInt) + + c.remoteNonceInt++ // increment remote nonce, no matter what... + + msg, err := c.chachaStream.Open(nil, nonceBuf[:], ctext, nil) + if err != nil { + fmt.Printf("decrypt %d byte ciphertext failed\n", len(ctext)) + return 0, err + } + + if _, err := c.readBuf.Write(msg); 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) { + if b == nil { + return 0, fmt.Errorf("write to %x nil", c.remoteLNId) + } + fmt.Printf("Encrypt %d byte plaintext to %x nonce %d\n", + len(b), c.remoteLNId, c.myNonceInt) + + // first encrypt message with shared key + var nonceBuf [8]byte + binary.BigEndian.PutUint64(nonceBuf[:], c.myNonceInt) + c.myNonceInt++ // increment mine + + ctext := c.chachaStream.Seal(nil, nonceBuf[:], b, nil) + if err != nil { + return 0, err + } + if len(ctext) > 65530 { + return 0, fmt.Errorf("Write to %x too long, %d bytes", + c.remoteLNId, len(ctext)) + } + + // use writeClear to prepend length / destination header + return writeClear(c.conn, ctext) +} + +// 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 { + c.myNonceInt = 0 + c.remoteNonceInt = 0 + c.remotePub = nil + + return c.conn.Close() +} + +// LocalAddr returns the local network address. +// Part of the net.Conn interface. +// If PBX reports address of pbx host. +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) +} + +var _ net.Conn = (*Conn)(nil) diff --git a/lndc/listener.go b/lndc/listener.go new file mode 100644 index 00000000..99d3fc11 --- /dev/null +++ b/lndc/listener.go @@ -0,0 +1,206 @@ +package lndc + +import ( + "bytes" + "fmt" + "net" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/fastsha256" + "github.com/codahale/chacha20poly1305" +) + +// Listener... +type Listener struct { + longTermPriv *btcec.PrivateKey + + tcp *net.TCPListener +} + +var _ net.Listener = (*Listener)(nil) + +// NewListener... +func NewListener(localPriv *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{localPriv, l}, nil +} + +// Accept waits for and returns the next connection to the listener. +// Part of the net.Listener interface. +func (l *Listener) Accept() (c net.Conn, err error) { + conn, err := l.tcp.Accept() + if err != nil { + return nil, nil + } + + lndc := NewConn(l.longTermPriv, conn) + + // Exchange an ephemeral public key with the remote connection in order + // to establish a confidential connection before we attempt to + // authenticated. + ephemeralKey, err := l.createCipherConn(lndc) + if err != nil { + return nil, err + } + + // Now that we've established an encrypted connection, authenticate the + // identity of the remote host. + ephemeralPub := ephemeralKey.PubKey().SerializeCompressed() + if err := l.authenticateConnection(lndc, ephemeralPub); err != nil { + lndc.Close() + return nil, err + } + + return lndc, nil +} + +// createCipherConn.... +func (l *Listener) createCipherConn(lnConn *Conn) (*btcec.PrivateKey, error) { + var err error + var theirEphPubBytes []byte + + // First, read and deserialize their ephemeral public key. + theirEphPubBytes, err = readClear(lnConn.conn) + if err != nil { + return nil, err + } + if len(theirEphPubBytes) != 33 { + return nil, fmt.Errorf("Got invalid %d byte eph pubkey %x\n", + len(theirEphPubBytes), theirEphPubBytes) + } + theirEphPub, err := btcec.ParsePubKey(theirEphPubBytes, btcec.S256()) + if err != nil { + return nil, err + } + + // Once we've parsed and verified their key, generate, and send own + // ephemeral key pair for use within this session. + myEph, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + return nil, err + } + if _, err := writeClear(lnConn.conn, myEph.PubKey().SerializeCompressed()); err != nil { + return nil, err + } + + // Now that we have both keys, do non-interactive diffie with ephemeral + // pubkeys, sha256 for good luck. + sessionKey := fastsha256.Sum256( + btcec.GenerateSharedSecret(myEph, theirEphPub), + ) + + lnConn.chachaStream, err = chacha20poly1305.New(sessionKey[:]) + + // display private key for debug only + fmt.Printf("made session key %x\n", sessionKey) + + lnConn.remoteNonceInt = 1 << 63 + lnConn.myNonceInt = 0 + + lnConn.remotePub = theirEphPub + lnConn.authed = false + + return myEph, nil +} + +// AuthListen... +func (l *Listener) authenticateConnection(lnConn *Conn, localEphPubBytes []byte) error { + var err error + + // TODO(roasbeef): should be using read/write clear here? + slice := make([]byte, 73) + n, err := lnConn.conn.Read(slice) + if err != nil { + fmt.Printf("Read error: %s\n", err.Error()) + return err + } + + fmt.Printf("read %d bytes\n", n) + authmsg := slice[:n] + if len(authmsg) != 53 && len(authmsg) != 73 { + return fmt.Errorf("got auth message of %d bytes, "+ + "expect 53 or 73", len(authmsg)) + } + + myPKH := btcutil.Hash160(l.longTermPriv.PubKey().SerializeCompressed()) + if !bytes.Equal(myPKH, authmsg[33:53]) { + return fmt.Errorf( + "remote host asking for PKH %x, that's not me", authmsg[33:53]) + } + + // do DH with id keys + theirPub, err := btcec.ParsePubKey(authmsg[:33], btcec.S256()) + if err != nil { + return err + } + idDH := fastsha256.Sum256( + btcec.GenerateSharedSecret(lnConn.longTermPriv, theirPub), + ) + + myDHproof := btcutil.Hash160( + append(lnConn.remotePub.SerializeCompressed(), idDH[:]...), + ) + theirDHproof := btcutil.Hash160(append(localEphPubBytes, idDH[:]...)) + + // If they already know our public key, then execute the fast path. + // Verify their DH proof, and send our own. + if len(authmsg) == 73 { + // Verify their DH proof. + if !bytes.Equal(authmsg[53:], theirDHproof) { + return fmt.Errorf("invalid DH proof from %s", + lnConn.RemoteAddr().String()) + } + + // Their DH proof checks out, so send ours now. + if _, err = lnConn.conn.Write(myDHproof); err != nil { + return err + } + } else { + // Otherwise, they don't yet know our public key. So we'll send + // it over to them, so we can both compute the DH proof. + msg := append(l.longTermPriv.PubKey().SerializeCompressed(), myDHproof...) + if _, err = lnConn.conn.Write(msg); err != nil { + return err + } + + resp := make([]byte, 20) + if _, err := lnConn.conn.Read(resp); err != nil { + return err + } + + // Verify their DH proof. + if bytes.Equal(resp, theirDHproof) == false { + return fmt.Errorf("Invalid DH proof %x", theirDHproof) + } + } + + theirAdr := btcutil.Hash160(theirPub.SerializeCompressed()) + copy(lnConn.remoteLNId[:], theirAdr[:16]) + lnConn.remotePub = theirPub + lnConn.authed = true + + return 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/lndc/lndc_test.go b/lndc/lndc_test.go new file mode 100644 index 00000000..0d2fae97 --- /dev/null +++ b/lndc/lndc_test.go @@ -0,0 +1,97 @@ +package lndc + +import ( + "bytes" + "fmt" + "sync" + "testing" + + "github.com/btcsuite/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 will be chosen for our + // listener. + addr := "127.0.0.1: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) + } + conn := NewConn(remotePriv, nil) + + var wg sync.WaitGroup + var dialErr error + + // Initiate a connection with a separate goroutine, and listen with our + // main one. If both errors are nil, then encryption+auth was succesful. + wg.Add(1) + go func() { + dialErr = conn.Dial(listener.Addr().String(), + localPriv.PubKey().SerializeCompressed()) + wg.Done() + }() + + localConn, listenErr := listener.Accept() + if listenErr != nil { + t.Fatalf("unable to accept connection: %v", listenErr) + } + + wg.Wait() + + if dialErr != nil { + t.Fatalf("unable to establish connection: %v", dialErr) + } + + // 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") + fmt.Println("write") + 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)) + } +} diff --git a/lndc/netio.go b/lndc/netio.go index 38ba9f3b..c7525d15 100644 --- a/lndc/netio.go +++ b/lndc/netio.go @@ -2,632 +2,90 @@ package lndc import ( "bytes" - "crypto/cipher" "encoding/binary" "fmt" "io" "net" - "time" - - "github.com/btcsuite/btcd/btcec" - "github.com/btcsuite/fastsha256" - "github.com/codahale/chacha20poly1305" - "github.com/lightningnetwork/lnd/lnwire" - "golang.org/x/crypto/ripemd160" ) -/* good ol' OP_HASH160, which is just ripemd160(sha256(input)) */ -func H160(input []byte) []byte { - rp := ripemd160.New() - shaout := fastsha256.Sum256(input) - _, _ = rp.Write(shaout[:]) - return rp.Sum(nil) -} - -// Lightning Network Data Conection. Encrypted. -type LNDConn struct { - Version uint8 - Cn net.Conn - RemotePub *btcec.PublicKey - RemoteLNId [16]byte - MyNonceInt uint64 - RemoteNonceInt uint64 - // if Authed == false, the RemotePub is the EPHEMERAL key. - // once authed == true, RemotePub is who you're actually talking to. - Authed bool - // chachaStream saves some time as you don't have to init it with - // the session key every time. Make SessionKey redundant; remove later - chachaStream cipher.AEAD - - // ViaPbx specifies whether this is a direct TCP connection or an - // encapsulated PBX connection. - // if going ViaPbx, Cn isn't used - // channels are used for Read() and Write(), - // which are filled by the PBXhandler. - ViaPbx bool - PbxIncoming chan []byte - PbxOutgoing chan []byte -} - -/* new & improved tcp open session. -There's connector A and listener B. Once the connection is set up there's no -difference, but there can be during the setup. -Setup: -1 -> A sends B ephemeral secp256k1 pubkey (33 bytes) -2 <- B sends A ephemeral secp256k1 pubkey (33 bytes) -A and B do DH, get a shared secret. -========== -Seesion is open! Done! Well not quite. Session is confidential but not -yet authenticated. From here on, can use the Send() and Recv() functions with -chacha20poly1305. -========== -The DH proof is h160(remote eph pubkey, IDDH secret) -A initiates auth. - -If A does not know B's pubkey but only B's pubkey hash: - -1 -> A sends [PubKeyA, PubKeyHashB] (53 bytes) -B computes ID pubkey DH -2 <- B sends [PubkeyB, DH proof] (53 bytes) -3 -> A sends DH proof (20 bytes) -done. - -This exchange can be sped up if A already knows B's pubkey: - -A already knows who they're talking to, or trying to talk to -1 -> A sends [PubKeyA, PubkeyHashB, DH proof] (73 bytes) -2 <- B sends DH proof (20 bytes) - -A and B both verify those H160 hashes, and if matching consider their -session counterparty authenticated. - -A possible weakness of the DH proof is if B re-uses eph keys. That potentially -makes *A*'s proof weaker though. A gets to choose the proof B creates. As -long as your software makes new eph keys each time, you should be -*/ - -// Open creates and auths an lndc connections, -// after the TCP or pbx connection is already assigned / dialed. -func (lndc *LNDConn) Open( - me *btcec.PrivateKey, remote []byte) error { - // make TCP connection to listening host - var err error - - if len(remote) != 33 && len(remote) != 20 { - return fmt.Errorf("must supply either remote pubkey or pubkey hash") - } - // calc remote LNId; need this for creating pbx connections - // just because LNid is in the struct does not mean it's authed! - if len(remote) == 20 { - copy(lndc.RemoteLNId[:], remote[:16]) - } else { - theirAdr := H160(remote) - copy(lndc.RemoteLNId[:], theirAdr[:16]) - } - - // make up an ephtemeral keypair. Doesn't exit this function. - myEph, err := btcec.NewPrivateKey(btcec.S256()) - if err != nil { - return nil - } - - // Sned 1. Send my ephemeral pubkey. Can add version bits. - err = lndc.writeClear(myEph.PubKey().SerializeCompressed()) - if err != nil { - return err - } - - // read their ephemeral pubkey - TheirEphPubBytes, err := lndc.readClear() - if err != nil { - return err - } - - // deserialize their ephemeral pubkey - TheirEphPub, err := btcec.ParsePubKey(TheirEphPubBytes, btcec.S256()) - if err != nil { - return err - } - // do non-interactive diffie with ephemeral pubkeys - // sha256 for good luck - sessionKey := - fastsha256.Sum256(btcec.GenerateSharedSecret(myEph, TheirEphPub)) - - lndc.chachaStream, err = chacha20poly1305.New(sessionKey[:]) - if err != nil { - return err - } - // display private key for debug only - fmt.Printf("made session key %x\n", sessionKey) - - lndc.MyNonceInt = 1 << 63 - lndc.RemoteNonceInt = 0 - - lndc.RemotePub = TheirEphPub - lndc.Authed = false - - // session is now open and confidential but not yet authenticated. - // So auth! - if len(remote) == 20 { // only know pubkey hash (20 bytes) - return lndc.AuthPKH(me, remote, myEph.PubKey().SerializeCompressed()) - } else { // must be 33 byte pubkey - return lndc.AuthPubKey(me, remote, myEph.PubKey().SerializeCompressed()) - } -} - -// TcpListen takes a lndc that's just connected, and sets it up. -// Call this just after a connection comes in. -// calls AuthListen, waiting for the auth step before returning. -// in the case of a pbx connection, there is no lndc.Cn -func (lndc *LNDConn) Setup(me *btcec.PrivateKey) error { - var err error - var TheirEphPubBytes []byte - - TheirEphPubBytes, err = lndc.readClear() - if err != nil { - return err - } - - if len(TheirEphPubBytes) != 33 { - return fmt.Errorf("Got invalid %d byte eph pubkey %x\n", - len(TheirEphPubBytes), TheirEphPubBytes) - } - // deserialize their ephemeral pubkey - TheirEphPub, err := btcec.ParsePubKey(TheirEphPubBytes, btcec.S256()) - if err != nil { - return err - } - - // their key looks OK, make our own - myEph, err := btcec.NewPrivateKey(btcec.S256()) - if err != nil { - return nil - } - - // and send out our eph key - err = lndc.writeClear(myEph.PubKey().SerializeCompressed()) - if err != nil { - return err - } - - // do non-interactive diffie with ephemeral pubkeys - // sha256 for good luck - sessionKey := - fastsha256.Sum256(btcec.GenerateSharedSecret(myEph, TheirEphPub)) - - lndc.chachaStream, err = chacha20poly1305.New(sessionKey[:]) - - // display private key for debug only - fmt.Printf("made session key %x\n", sessionKey) - - lndc.RemoteNonceInt = 1 << 63 - lndc.MyNonceInt = 0 - - lndc.RemotePub = TheirEphPub - lndc.Authed = false - - // session is open and confidential but not yet authenticated. - // Listen for auth message - return lndc.AuthListen(me, myEph.PubKey().SerializeCompressed()) -} - -func (lndc *LNDConn) AuthListen( - myId *btcec.PrivateKey, localEphPubBytes []byte) error { - - var err error - slice := make([]byte, 65535) - n, err := lndc.Read(slice) - if err != nil { - return err - } - fmt.Printf("read %d bytes\n", n) - slice = slice[:n] - if err != nil { - fmt.Printf("Read error: %s\n", err.Error()) - err2 := lndc.Close() - if err2 != nil { - return err2 - } - return err - } - - authmsg := slice - if len(authmsg) != 53 && len(authmsg) != 73 { - err = lndc.Close() - if err != nil { - return err - } - return fmt.Errorf( - "Got auth message of %d bytes, expect 53 or 73", len(authmsg)) - } - theirPub, err := btcec.ParsePubKey(authmsg[:33], btcec.S256()) - if err != nil { - return err - } - myPKH := H160(myId.PubKey().SerializeCompressed()) - if bytes.Equal(myPKH, authmsg[33:53]) == false { - err = lndc.Close() - if err != nil { - return err - } - return fmt.Errorf( - "remote host asking for PKH %x, that's not me", authmsg[33:53]) - } - - // do DH with id keys - idDH := fastsha256.Sum256(btcec.GenerateSharedSecret(myId, theirPub)) - myDHproof := H160(append(lndc.RemotePub.SerializeCompressed(), idDH[:]...)) - theirDHproof := H160(append(localEphPubBytes, idDH[:]...)) - if len(authmsg) == 73 { // quick mode - // verify their DH proof - if bytes.Equal(authmsg[53:], theirDHproof) == false { - err = lndc.Close() - if err != nil { - return err - } - return fmt.Errorf( - "Invalid DH proof from %s", lndc.Cn.RemoteAddr().String()) - } - // looks good, send my own - _, err = lndc.Write(myDHproof) - if err != nil { - err2 := lndc.Close() - if err2 != nil { - return err2 - } - return err - } - // and we're authed - } else { // 53 byte, they don't know my pubkey - msg := append(myId.PubKey().SerializeCompressed(), myDHproof...) - _, err = lndc.Write(msg) - if err != nil { - err2 := lndc.Close() - if err2 != nil { - return err2 - } - return err - } - resp := make([]byte, 65535) - n, err := lndc.Read(resp) - if err != nil { - return err - } - resp = resp[:n] - - if n != 20 { - err2 := lndc.Close() - if err2 != nil { - return err2 - } - fmt.Errorf("expected 20 byte DH proof, got %d", n) - } - - // verify their DH proof - if bytes.Equal(resp, theirDHproof) == false { - err = lndc.Close() - if err != nil { - return err - } - return fmt.Errorf("Invalid DH proof %x", theirDHproof) - } - //proof looks good, auth - } - lndc.RemotePub = theirPub - theirAdr := H160(theirPub.SerializeCompressed()) - copy(lndc.RemoteLNId[:], theirAdr[:16]) - lndc.Authed = true - return nil -} - -func (lndc *LNDConn) AuthPKH( - myId *btcec.PrivateKey, theirPKH, localEphPubBytes []byte) error { - var err error - - if myId == nil { - return fmt.Errorf("can't auth: supplied privkey is nil") - } - if lndc.Authed { - return fmt.Errorf("%s already authed", lndc.RemotePub) - } - if len(theirPKH) != 20 { - return fmt.Errorf("remote PKH must be 20 bytes, got %d", len(theirPKH)) - } - - // send 53 bytes; my pubkey, and remote pubkey hash - msg := myId.PubKey().SerializeCompressed() - msg = append(msg, theirPKH...) - - _, err = lndc.Write(msg) - if err != nil { - return err - } - // wait for their response. - // TODO add timeout here - resp := make([]byte, 65535) - n, err := lndc.Read(resp) - if err != nil { - return err - } - resp = resp[:n] - // response should be 53 bytes, their pubkey and DH proof - if n != 53 { - return fmt.Errorf( - "PKH auth response should be 53 bytes, got %d", len(resp)) - } - theirPub, err := btcec.ParsePubKey(resp[:33], btcec.S256()) - if err != nil { - return err - } - idDH := fastsha256.Sum256(btcec.GenerateSharedSecret(myId, theirPub)) - fmt.Printf("made idDH %x\n", idDH) - - theirDHproof := H160(append(localEphPubBytes, idDH[:]...)) - // verify their DH proof - if bytes.Equal(resp[33:], theirDHproof) == false { - return fmt.Errorf("Invalid DH proof %x", theirDHproof) - } - // their DH proof checks out, send our own - myDHproof := H160(append(lndc.RemotePub.SerializeCompressed(), idDH[:]...)) - _, err = lndc.Write(myDHproof) - if err != nil { - return err - } - // proof sent, auth complete - lndc.RemotePub = theirPub - theirAdr := H160(theirPub.SerializeCompressed()) - copy(lndc.RemoteLNId[:], theirAdr[:16]) - lndc.Authed = true - return nil -} - -func (lndc *LNDConn) AuthPubKey( - myId *btcec.PrivateKey, remotePubBytes, localEphPubBytes []byte) error { - - var err error - if myId == nil { - return fmt.Errorf("can't auth: supplied privkey is nil") - } - if lndc.Authed { - return fmt.Errorf("%s already authed", lndc.RemotePub) - } - theirPub, err := btcec.ParsePubKey(remotePubBytes, btcec.S256()) - if err != nil { - return err - } - theirPKH := H160(remotePubBytes) - // I know enough to generate DH, do so - idDH := fastsha256.Sum256(btcec.GenerateSharedSecret(myId, theirPub)) - myDHproof := H160(append(lndc.RemotePub.SerializeCompressed(), idDH[:]...)) - // message is 73 bytes; my pubkey, their pubkey hash, DH proof - msg := myId.PubKey().SerializeCompressed() - msg = append(msg, theirPKH...) - msg = append(msg, myDHproof...) - - _, err = lndc.Write(msg) - if err != nil { - return err - } - resp := make([]byte, 65535) - n, err := lndc.Read(resp) - if err != nil { - return err - } - resp = resp[:n] - if n != 20 { - fmt.Errorf("expected 20 byte DH proof, got %d", len(resp)) - } - - theirDHproof := H160(append(localEphPubBytes, idDH[:]...)) - // verify their DH proof - if bytes.Equal(resp, theirDHproof) == false { - return fmt.Errorf("Invalid DH proof %x", theirDHproof) - } - // proof checks out, auth complete - lndc.RemotePub = theirPub - theirAdr := H160(theirPub.SerializeCompressed()) - copy(lndc.RemoteLNId[:], theirAdr[:16]) - lndc.Authed = true - return nil -} - -func (lndc *LNDConn) WhoAreYou(host string) (*btcec.PublicKey, error) { - var err error - if lndc.Cn == nil { - return nil, fmt.Errorf("no connection to ask on") - } - fmt.Printf("connecting to address %s\n", host) - // make TCP connection to listening host - lndc.Cn, err = net.Dial("tcp", host) - if err != nil { - return nil, err - } - err = lndc.writeClear([]byte("WHOAREYOU")) - if err != nil { - return nil, err - } - var b bytes.Buffer - lndc.Read(b.Bytes()) - - IamResp, err := lndc.readClear() - if err != nil { - return nil, err - } - return btcec.ParsePubKey(IamResp[:33], btcec.S256()) -} - -/* -Make ETcpCons adhere to Conn interface. -need: -Read(b []byte) (n int, err error) -Write(b []byte) (n int, err error) -Close() error -LocalAddr() Addr -RemoteAddr() Addr -SetDeadline(t time.Time) error -SetReadDeadline(t time.Time) error -SetWriteDeadline(t time.Time) error - -ETcpCons can be either regular TCP connections, which is good, or PBX-routed -connections, which is worse, but there are levels of connectivity we are -prepared to accept. - -When it's PBX, don't try to use Cn -*/ - -func (lndc *LNDConn) Read(b []byte) (n int, err error) { - // first get message length from first 2 bytes - var ctext []byte - ctext, err = lndc.readClear() - if err != nil { - return 0, err - } - - // now decrypt - nonceBuf := new(bytes.Buffer) - err = binary.Write(nonceBuf, binary.BigEndian, lndc.RemoteNonceInt) - fmt.Printf("decrypt %d byte from %x nonce %d\n", - len(ctext), lndc.RemoteLNId, lndc.RemoteNonceInt) - lndc.RemoteNonceInt++ // increment remote nonce, no matter what... - - msg, err := lndc.chachaStream.Open(nil, nonceBuf.Bytes(), ctext, nil) - if err != nil { - fmt.Printf("decrypt %d byte ciphertext failed\n", len(ctext)) - return 0, err - } - - n = copy(b, msg) - if n < len(msg) { - return 0, fmt.Errorf( - "Can't read from %x: Slice provided too small. %d bytes, need %d", - lndc.RemoteLNId, len(b), len(msg)) - } - - return n, nil -} - -func (lndc *LNDConn) Write(b []byte) (n int, err error) { - if b == nil { - return 0, fmt.Errorf("Write to %x nil", lndc.RemoteLNId) - } - fmt.Printf("Encrypt %d byte plaintext to %x nonce %d\n", - len(b), lndc.RemoteLNId, lndc.MyNonceInt) - // first encrypt message with shared key - nonceBuf := new(bytes.Buffer) - err = binary.Write(nonceBuf, binary.BigEndian, lndc.MyNonceInt) - if err != nil { - return 0, err - } - lndc.MyNonceInt++ // increment mine - - ctext := lndc.chachaStream.Seal(nil, nonceBuf.Bytes(), b, nil) - if err != nil { - return 0, err - } - if len(ctext) > 65530 { - return 0, fmt.Errorf("Write to %x too long, %d bytes", - lndc.RemoteLNId, len(ctext)) - } - - // use writeClear to prepend length / destination header - err = lndc.writeClear(ctext) - // returns len of how much you put in, not how much written on the wire - return len(b), err -} - -func (lndc *LNDConn) Close() error { - // return lndc.Cn.Close() - // return nil - lndc.MyNonceInt = 0 - lndc.RemoteNonceInt = 0 - lndc.RemotePub = nil - - // if lndc.ViaPbx { - // return nil // don't close pbx connection - // } - err := lndc.Cn.Close() - - return err -} - -// network address; if PBX reports address of pbx host -func (lndc *LNDConn) LocalAddr() net.Addr { - return lndc.Cn.LocalAddr() -} -func (lndc *LNDConn) RemoteAddr() net.Addr { - return lndc.Cn.RemoteAddr() -} - -// timing stuff for net.conn compatibility -func (lndc *LNDConn) SetDeadline(t time.Time) error { - return lndc.Cn.SetDeadline(t) -} -func (lndc *LNDConn) SetReadDeadline(t time.Time) error { - - return lndc.Cn.SetReadDeadline(t) -} -func (lndc *LNDConn) SetWriteDeadline(t time.Time) error { - return lndc.Cn.SetWriteDeadline(t) -} - -/* ReadClear and WriteClear don't encrypt but directly read and write to the -underlying data link, only adding or subtracting a 2 byte length header. -All Read() and Write() calls for lndc's use these functions internally -(they aren't exported). They're also used in the key agreement phase. -*/ - -// encapsulation for sending to Pbx host. -// put FWDMSG, then 16 byte destination ID, then message (with msgtype) -func (lndc *LNDConn) PbxEncapsulate(b *[]byte) { - fmt.Printf("PbxEncapsulate %d byte message, dest ID %x\n", - len(*b), lndc.RemoteLNId) - *b = append(lndc.RemoteLNId[:], *b...) - *b = append([]byte{lnwire.MSGID_FWDMSG}, *b...) -} - -func (lndc *LNDConn) readClear() ([]byte, error) { +// New & improved tcp open session. +// There's connector A and listener B. Once the connection is set up there's no +// difference, but there can be during the setup. +// Setup: +// 1 -> A sends B ephemeral secp256k1 pubkey (33 bytes) +// 2 <- B sends A ephemeral secp256k1 pubkey (33 bytes) +// A and B do DH, get a shared secret. +// ========== +// Seesion is open! Done! Well not quite. Session is confidential but not +// yet authenticated. From here on, can use the Send() and Recv() functions with +// chacha20poly1305. +// ========== + +// Nodes authenticate by doing a DH with their persistent identity keys, and then +// exchanging hash based proofs that they got the same shared IDDH secret. +// The DH proof is h160(remote eph pubkey, IDDH secret) +// A initiates auth. +// +// If A does not know B's pubkey but only B's pubkey hash: +// +// 1 -> A sends [PubKeyA, PubKeyHashB] (53 bytes) +// B computes ID pubkey DH +// 2 <- B sends [PubkeyB, DH proof] (53 bytes) +// 3 -> A sends DH proof (20 bytes) +// done. +// +// This exchange can be sped up if A already knows B's pubkey: +// +// A already knows who they're talking to, or trying to talk to +// 1 -> A sends [PubKeyA, PubkeyHashB, DH proof] (73 bytes) +// 2 <- B sends DH proof (20 bytes) +// +// A and B both verify those H160 hashes, and if matching consider their +// session counterparty authenticated. +// +// A possible weakness of the DH proof is if B re-uses eph keys. That potentially +// makes *A*'s proof weaker though. A gets to choose the proof B creates. As +// long as your software makes new eph keys each time, you should be OK. + +// readClear and writeClear don't encrypt but directly read and write to the +// underlying data link, only adding or subtracting a 2 byte length header. +// All Read() and Write() calls for lndc's use these functions internally +// (they aren't exported). They're also used in the key agreement phase. + +// readClear reads the next length-prefixed message from the underlying raw +// TCP connection. +func readClear(c net.Conn) ([]byte, error) { var msgLen uint16 - // needs to be pointer or will silently not do anything. - var msg []byte - if lndc.ViaPbx { //pbx mode - msg = <-lndc.PbxIncoming - } else { // normal tcp mode - err := binary.Read(lndc.Cn, binary.BigEndian, &msgLen) - if err != nil { - return nil, err - } - // fmt.Printf("%d byte LMsg incoming\n", msgLen) - msg = make([]byte, msgLen) - _, err = io.ReadFull(lndc.Cn, msg) - if err != nil { - return nil, err - } + if err := binary.Read(c, binary.BigEndian, &msgLen); err != nil { + return nil, err } + + msg := make([]byte, msgLen) + if _, err := io.ReadFull(c, msg); err != nil { + return nil, err + } + return msg, nil } -func (lndc *LNDConn) writeClear(msg []byte) error { - var err error +// TODO(roasbeef): incorporate buffer pool + +// writeClear writes the passed message with a prefixed 2-byte length header. +func writeClear(conn net.Conn, msg []byte) (int, error) { if len(msg) > 65530 { - return fmt.Errorf("LMsg too long, %d bytes", len(msg)) + return 0, fmt.Errorf("lmsg too long, %d bytes", len(msg)) } - if msg == nil { - return fmt.Errorf("LMsg nil") + + // Add 2 byte length header (pbx doesn't need it) and send over TCP. + var msgBuf bytes.Buffer + if err := binary.Write(&msgBuf, binary.BigEndian, uint16(len(msg))); err != nil { + return 0, err } - if lndc.ViaPbx { - lndc.PbxEncapsulate(&msg) - lndc.PbxOutgoing <- msg - } else { - // add 2 byte length header (pbx doesn't need it) and send over TCP - hdr := new(bytes.Buffer) - err = binary.Write(hdr, binary.BigEndian, uint16(len(msg))) - if err != nil { - return err - } - msg = append(hdr.Bytes(), msg...) - _, err = lndc.Cn.Write(msg) - return err + + if _, err := msgBuf.Write(msg); err != nil { + return 0, err } - return nil + + return conn.Write(msgBuf.Bytes()) }