// authPKH... func (c *LNDConn) authPKH( myId *btcec.PrivateKey, 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], myId.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(myId, 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 }
func TestGenerateSharedSecret(t *testing.T) { privKey1, err := btcec.NewPrivateKey(btcec.S256()) if err != nil { t.Errorf("private key generation error: %s", err) return } privKey2, err := btcec.NewPrivateKey(btcec.S256()) if err != nil { t.Errorf("private key generation error: %s", err) return } secret1 := btcec.GenerateSharedSecret(privKey1, privKey2.PubKey()) secret2 := btcec.GenerateSharedSecret(privKey2, privKey1.PubKey()) if !bytes.Equal(secret1, secret2) { t.Errorf("ECDH failed, secrets mismatch - first: %x, second: %x", secret1, secret2) } }
// createCipherConn.... func (l *Listener) createCipherConn(lnConn *LNDConn) (*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 }
// authPubKey... func (c *LNDConn) authPubKey( myId *btcec.PrivateKey, 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(myId, 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], myId.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 }
// 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 }
// AuthListen... func (l *Listener) authenticateConnection( myId *btcec.PrivateKey, lnConn *LNDConn, 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(l.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 }
// NewMixHeader creates a new mix header which is capable of obliviously // routing a message through the mix-net path outline by 'paymentPath' // to a final node indicated by 'identifier' housing a message addressed to // 'dest'. This function returns the created mix header along with a derived // shared secret for each node in the path. func NewMixHeader(dest LightningAddress, identifier [securityParameter]byte, paymentPath []*btcec.PublicKey) (*MixHeader, [][sharedSecretSize]byte, error) { // Each hop performs ECDH with our ephemeral key pair to arrive at a // shared secret. Additionally, each hop randomizes the group element // for the next hop by multiplying it by the blinding factor. This way // we only need to transmit a single group element, and hops can't link // a session back to us if they have several nodes in the path. numHops := len(paymentPath) hopEphemeralPubKeys := make([]*btcec.PublicKey, numHops) hopSharedSecrets := make([][sha256.Size]byte, numHops) hopBlindingFactors := make([][sha256.Size]byte, numHops) // Generate a new ephemeral key to use for ECDH for this session. sessionKey, err := btcec.NewPrivateKey(btcec.S256()) if err != nil { return nil, nil, err } // Compute the triplet for the first hop outside of the main loop. // Within the loop each new triplet will be computed recursively based // off of the blinding factor of the last hop. hopEphemeralPubKeys[0] = sessionKey.PubKey() hopSharedSecrets[0] = sha256.Sum256(btcec.GenerateSharedSecret(sessionKey, paymentPath[0])) hopBlindingFactors[0] = computeBlindingFactor(hopEphemeralPubKeys[0], hopSharedSecrets[0][:]) // Now recursively compute the ephemeral ECDH pub keys, the shared // secret, and blinding factor for each hop. for i := 1; i <= numHops-1; i++ { // a_{n} = a_{n-1} x c_{n-1} -> (Y_prev_pub_key x prevBlindingFactor) hopEphemeralPubKeys[i] = blindGroupElement(hopEphemeralPubKeys[i-1], hopBlindingFactors[i-1][:]) // s_{n} = sha256( y_{n} x c_{n-1} ) -> // (Y_their_pub_key x x_our_priv) x all prev blinding factors yToX := blindGroupElement(paymentPath[i], sessionKey.D.Bytes()) hopSharedSecrets[i] = sha256.Sum256(multiScalarMult(yToX, hopBlindingFactors[:i]).X.Bytes()) // TODO(roasbeef): prob don't need to store all blinding factors, only the prev... // b_{n} = sha256(a_{n} || s_{n}) hopBlindingFactors[i] = computeBlindingFactor(hopEphemeralPubKeys[i], hopSharedSecrets[i][:]) } // Generate the padding, called "filler strings" in the paper. filler := generateHeaderPadding(numHops, hopSharedSecrets) // First we generate the routing info + MAC for the very last hop. mixHeader := make([]byte, 0, routingInfoSize) mixHeader = append(mixHeader, dest...) mixHeader = append(mixHeader, identifier[:]...) mixHeader = append(mixHeader, bytes.Repeat([]byte{0}, ((2*(numMaxHops-numHops)+2)*securityParameter-len(dest)))...) // Encrypt the header for the final hop with the shared secret the // destination will eventually derive, then pad the message out to full // size with the "random" filler bytes. streamBytes := generateCipherStream(generateKey("rho", hopSharedSecrets[numHops-1]), numStreamBytes) xor(mixHeader, mixHeader, streamBytes[:(2*(numMaxHops-numHops)+3)*securityParameter]) mixHeader = append(mixHeader, filler...) // Calculate a MAC over the encrypted mix header for the last hop // (including the filler bytes), using the same shared secret key as // used for encryption above. headerMac := calcMac(generateKey("mu", hopSharedSecrets[numHops-1]), mixHeader) // Now we compute the routing information for each hop, along with a // MAC of the routing info using the shared key for that hop. for i := numHops - 2; i >= 0; i-- { // The next hop from the point of view of the current hop. Node // ID's are currently the hash160 of a node's pubKey serialized // in compressed format. nodeID := btcutil.Hash160(paymentPath[i+1].SerializeCompressed()) var b bytes.Buffer b.Write(nodeID) // MAC for mix header. b.Write(headerMac[:]) // Mix header itself. b.Write(mixHeader[:(2*numMaxHops-1)*securityParameter]) streamBytes := generateCipherStream(generateKey("rho", hopSharedSecrets[i]), numStreamBytes) xor(mixHeader, b.Bytes(), streamBytes[:(2*numMaxHops+1)*securityParameter]) headerMac = calcMac(generateKey("mu", hopSharedSecrets[i]), mixHeader) } var r [routingInfoSize]byte copy(r[:], mixHeader) header := &MixHeader{ EphemeralKey: hopEphemeralPubKeys[0], RoutingInfo: r, HeaderMAC: headerMac, } return header, hopSharedSecrets, nil }
// ProcessMixHeader... // TODO(roasbeef): proto msg enum? func (s *SphinxNode) ProcessForwardingMessage(fwdMsg *ForwardingMessage) (*processMsgAction, error) { mixHeader := fwdMsg.Header onionMsg := fwdMsg.Msg dhKey := mixHeader.EphemeralKey routeInfo := mixHeader.RoutingInfo headerMac := mixHeader.HeaderMAC // Ensure that the public key is on our curve. if !s.lnKey.Curve.IsOnCurve(dhKey.X, dhKey.Y) { return nil, fmt.Errorf("pubkey isn't on secp256k1 curve") } // Compute our shared secret. sharedSecret := sha256.Sum256(btcec.GenerateSharedSecret(s.lnKey, dhKey)) // In order to mitigate replay attacks, if we've seen this particular // shared secret before, cease processing and just drop this forwarding // message. if _, ok := s.seenSecrets[sharedSecret]; ok { return nil, fmt.Errorf("shared secret previously seen") } // Using the derived shared secret, ensure the integrity of the routing // information by checking the attached MAC without leaking timing // information. calculatedMac := calcMac(generateKey("mu", sharedSecret), routeInfo[:]) if !hmac.Equal(headerMac[:], calculatedMac[:]) { return nil, fmt.Errorf("MAC mismatch, rejecting forwarding message") } // The MAC checks out, mark this current shared secret as processed in // order to mitigate future replay attacks. s.seenSecrets[sharedSecret] = struct{}{} // Attach the padding zeroes in order to properly strip an encryption // layer off the routing info revealing the routing information for the // next hop. var hopInfo [numStreamBytes]byte streamBytes := generateCipherStream(generateKey("rho", sharedSecret), numStreamBytes) headerWithPadding := append(routeInfo[:], bytes.Repeat([]byte{0}, 2*securityParameter)...) xor(hopInfo[:], headerWithPadding, streamBytes) // Are we the final hop? Or should the message be forwarded further? switch hopInfo[0] { case nullDest: // We're the exit node for a forwarding message. onionCore := lionessDecode(generateKey("pi", sharedSecret), onionMsg) // TODO(roasbeef): check ver and reject if not our net. /*destAddr, _, _ := base58.CheckDecode(string(onionCore[securityParameter : securityParameter*2])) if err != nil { return nil, err }*/ destAddr := onionCore[securityParameter : securityParameter*2] msg := onionCore[securityParameter*2:] return &processMsgAction{ action: ExitNode, destAddr: destAddr, destMsg: msg, }, nil default: // The message is destined for another mix-net node. // TODO(roasbeef): prob extract to func // Randomize the DH group element for the next hop using the // deterministic blinding factor. blindingFactor := computeBlindingFactor(dhKey, sharedSecret[:]) nextDHKey := blindGroupElement(dhKey, blindingFactor[:]) // Parse out the ID of the next node in the route. var nextHop [securityParameter]byte copy(nextHop[:], hopInfo[:securityParameter]) // MAC and MixHeader for the next hop. var nextMac [securityParameter]byte copy(nextMac[:], hopInfo[securityParameter:securityParameter*2]) var nextMixHeader [routingInfoSize]byte copy(nextMixHeader[:], hopInfo[securityParameter*2:]) // Strip a single layer of encryption from the onion for the // next hop to also process. nextOnion := lionessDecode(generateKey("pi", sharedSecret), onionMsg) nextFwdMsg := &ForwardingMessage{ Header: &MixHeader{ EphemeralKey: nextDHKey, RoutingInfo: nextMixHeader, HeaderMAC: nextMac, }, Msg: nextOnion, } return &processMsgAction{ action: MoreHops, nextHop: nextHop, fwdMsg: nextFwdMsg, }, nil } }