multi: store KeyLocator in OpenChannel, use ECDH

This commit adds a RevocationKeyLocator field to the OpenChannel
struct so that the SCB derivation doesn't have to brute-force the
sha chain root key and match the public key. ECDH derivation is now
used to derive the key instead of regular private key derivation a
la DerivePrivKey. The legacy can still be used to recover old
channels.
This commit is contained in:
eugene 2020-10-21 13:34:05 -04:00
parent 986e69c81b
commit bb84f0ebc8
No known key found for this signature in database
GPG Key ID: 118759E83439A9B1
6 changed files with 183 additions and 58 deletions

@ -124,19 +124,34 @@ type Single struct {
func NewSingle(channel *channeldb.OpenChannel, func NewSingle(channel *channeldb.OpenChannel,
nodeAddrs []net.Addr) Single { nodeAddrs []net.Addr) Single {
// TODO(roasbeef): update after we start to store the KeyLoc for var shaChainRootDesc keychain.KeyDescriptor
// shachain root
// We'll need to obtain the shachain root which is derived directly // If the channel has a populated RevocationKeyLocator, then we can
// from a private key in our keychain. // just store that instead of the public key.
var b bytes.Buffer if channel.RevocationKeyLocator.Family == keychain.KeyFamilyRevocationRoot {
channel.RevocationProducer.Encode(&b) // Can't return an error. shaChainRootDesc = keychain.KeyDescriptor{
KeyLocator: channel.RevocationKeyLocator,
}
} else {
// If the RevocationKeyLocator is not populated, then we'll need
// to obtain a public point for the shachain root and store that.
// This is the legacy scheme.
var b bytes.Buffer
_ = channel.RevocationProducer.Encode(&b) // Can't return an error.
// Once we have the root, we'll make a public key from it, such that // Once we have the root, we'll make a public key from it, such that
// the backups plaintext don't carry any private information. When we // the backups plaintext don't carry any private information. When
// go to recover, we'll present this in order to derive the private // we go to recover, we'll present this in order to derive the
// key. // private key.
_, shaChainPoint := btcec.PrivKeyFromBytes(btcec.S256(), b.Bytes()) _, shaChainPoint := btcec.PrivKeyFromBytes(btcec.S256(), b.Bytes())
shaChainRootDesc = keychain.KeyDescriptor{
PubKey: shaChainPoint,
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamilyRevocationRoot,
},
}
}
// If a channel is unconfirmed, the block height of the ShortChannelID // If a channel is unconfirmed, the block height of the ShortChannelID
// is zero. This will lead to problems when trying to restore that // is zero. This will lead to problems when trying to restore that
@ -149,21 +164,16 @@ func NewSingle(channel *channeldb.OpenChannel,
} }
single := Single{ single := Single{
IsInitiator: channel.IsInitiator, IsInitiator: channel.IsInitiator,
ChainHash: channel.ChainHash, ChainHash: channel.ChainHash,
FundingOutpoint: channel.FundingOutpoint, FundingOutpoint: channel.FundingOutpoint,
ShortChannelID: chanID, ShortChannelID: chanID,
RemoteNodePub: channel.IdentityPub, RemoteNodePub: channel.IdentityPub,
Addresses: nodeAddrs, Addresses: nodeAddrs,
Capacity: channel.Capacity, Capacity: channel.Capacity,
LocalChanCfg: channel.LocalChanCfg, LocalChanCfg: channel.LocalChanCfg,
RemoteChanCfg: channel.RemoteChanCfg, RemoteChanCfg: channel.RemoteChanCfg,
ShaChainRootDesc: keychain.KeyDescriptor{ ShaChainRootDesc: shaChainRootDesc,
PubKey: shaChainPoint,
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamilyRevocationRoot,
},
},
} }
switch { switch {

@ -194,6 +194,10 @@ const (
// A tlv type definition used to serialize an outpoint's indexStatus // A tlv type definition used to serialize an outpoint's indexStatus
// for use in the outpoint index. // for use in the outpoint index.
indexStatusType tlv.Type = 0 indexStatusType tlv.Type = 0
// A tlv type definition used to serialize and deserialize a KeyLocator
// from the database.
keyLocType tlv.Type = 1
) )
// indexStatus is an enum-like type that describes what state the // indexStatus is an enum-like type that describes what state the
@ -719,6 +723,11 @@ type OpenChannel struct {
// was a revocation (true) or a commitment signature (false). // was a revocation (true) or a commitment signature (false).
LastWasRevoke bool LastWasRevoke bool
// RevocationKeyLocator stores the KeyLocator information that we will
// need to derive the shachain root for this channel. This allows us to
// have private key isolation from lnd.
RevocationKeyLocator keychain.KeyLocator
// TODO(roasbeef): eww // TODO(roasbeef): eww
Db *DB Db *DB
@ -3286,6 +3295,20 @@ func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error {
return err return err
} }
// Write the RevocationKeyLocator as the first entry in a tlv stream.
keyLocRecord := MakeKeyLocRecord(
keyLocType, &channel.RevocationKeyLocator,
)
tlvStream, err := tlv.NewStream(keyLocRecord)
if err != nil {
return err
}
if err := tlvStream.Encode(&w); err != nil {
return err
}
if err := chanBucket.Put(chanInfoKey, w.Bytes()); err != nil { if err := chanBucket.Put(chanInfoKey, w.Bytes()); err != nil {
return err return err
} }
@ -3475,6 +3498,16 @@ func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error {
} }
} }
keyLocRecord := MakeKeyLocRecord(keyLocType, &channel.RevocationKeyLocator)
tlvStream, err := tlv.NewStream(keyLocRecord)
if err != nil {
return err
}
if err := tlvStream.Decode(r); err != nil {
return err
}
channel.Packager = NewChannelPackager(channel.ShortChannelID) channel.Packager = NewChannelPackager(channel.ShortChannelID)
// Finally, read the optional shutdown scripts. // Finally, read the optional shutdown scripts.

@ -48,20 +48,7 @@ type chanDBRestorer struct {
func (c *chanDBRestorer) openChannelShell(backup chanbackup.Single) ( func (c *chanDBRestorer) openChannelShell(backup chanbackup.Single) (
*channeldb.ChannelShell, error) { *channeldb.ChannelShell, error) {
// First, we'll also need to obtain the private key for the shachain var err error
// root from the encoded public key.
//
// TODO(roasbeef): now adds req for hardware signers to impl
// shachain...
privKey, err := c.secretKeys.DerivePrivKey(backup.ShaChainRootDesc)
if err != nil {
return nil, fmt.Errorf("unable to derive shachain root key: %v", err)
}
revRoot, err := chainhash.NewHash(privKey.Serialize())
if err != nil {
return nil, err
}
shaChainProducer := shachain.NewRevocationProducer(*revRoot)
// Each of the keys in our local channel config only have their // Each of the keys in our local channel config only have their
// locators populate, so we'll re-derive the raw key now as we'll need // locators populate, so we'll re-derive the raw key now as we'll need
@ -97,6 +84,53 @@ func (c *chanDBRestorer) openChannelShell(backup chanbackup.Single) (
return nil, fmt.Errorf("unable to derive htlc key: %v", err) return nil, fmt.Errorf("unable to derive htlc key: %v", err)
} }
// The shachain root that seeds RevocationProducer for this channel.
// It currently has two possible formats.
var revRoot *chainhash.Hash
// If the PubKey field is non-nil, then this shachain root is using the
// legacy non-ECDH scheme.
if backup.ShaChainRootDesc.PubKey != nil {
ltndLog.Debugf("Using legacy revocation producer format for "+
"channel point %v", backup.FundingOutpoint)
// Obtain the private key for the shachain root from the
// encoded public key.
privKey, err := c.secretKeys.DerivePrivKey(
backup.ShaChainRootDesc,
)
if err != nil {
return nil, fmt.Errorf("could not derive private key "+
"for legacy channel revocation root format: "+
"%v", err)
}
revRoot, err = chainhash.NewHash(privKey.Serialize())
if err != nil {
return nil, err
}
} else {
ltndLog.Debugf("Using new ECDH revocation producer format "+
"for channel point %v", backup.FundingOutpoint)
// This is the scheme in which the shachain root is derived via
// an ECDH operation on the private key of ShaChainRootDesc and
// our public multisig key.
ecdh, err := c.secretKeys.ECDH(
backup.ShaChainRootDesc,
backup.LocalChanCfg.MultiSigKey.PubKey,
)
if err != nil {
return nil, fmt.Errorf("unable to derive shachain "+
"root: %v", err)
}
ch := chainhash.Hash(ecdh)
revRoot = &ch
}
shaChainProducer := shachain.NewRevocationProducer(*revRoot)
var chanType channeldb.ChannelType var chanType channeldb.ChannelType
switch backup.Version { switch backup.Version {
@ -119,8 +153,8 @@ func (c *chanDBRestorer) openChannelShell(backup chanbackup.Single) (
return nil, fmt.Errorf("unknown Single version: %v", err) return nil, fmt.Errorf("unknown Single version: %v", err)
} }
ltndLog.Infof("SCB Recovery: created channel shell for ChannelPoint(%v), "+ ltndLog.Infof("SCB Recovery: created channel shell for ChannelPoint"+
"chan_type=%v", backup.FundingOutpoint, chanType) "(%v), chan_type=%v", backup.FundingOutpoint, chanType)
chanShell := channeldb.ChannelShell{ chanShell := channeldb.ChannelShell{
NodeAddrs: backup.Addresses, NodeAddrs: backup.Addresses,

@ -10,6 +10,7 @@ import (
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding" "github.com/lightningnetwork/lnd/lnwallet/chanfunding"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
@ -161,6 +162,10 @@ type ChannelReservation struct {
chanFunder chanfunding.Assembler chanFunder chanfunding.Assembler
fundingIntent chanfunding.Intent fundingIntent chanfunding.Intent
// nextRevocationKeyLoc stores the key locator information for this
// channel.
nextRevocationKeyLoc keychain.KeyLocator
} }
// NewChannelReservation creates a new channel reservation. This function is // NewChannelReservation creates a new channel reservation. This function is

@ -0,0 +1,51 @@
// +build !rpctest
package lnwallet
import (
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/shachain"
)
// nextRevocationProducer creates a new revocation producer, deriving the
// revocation root by applying ECDH to a new key from our revocation root family
// and the multisig key we use for the channel.
func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation,
keyRing keychain.KeyRing) (shachain.Producer, error) {
// Derive the next key in the revocation root family.
nextRevocationKeyDesc, err := keyRing.DeriveNextKey(
keychain.KeyFamilyRevocationRoot,
)
if err != nil {
return nil, err
}
// If the DeriveNextKey call returns the first key with Index 0, we need
// to re-derive the key as the keychain/btcwallet.go DerivePrivKey call
// special-cases Index 0.
if nextRevocationKeyDesc.Index == 0 {
nextRevocationKeyDesc, err = keyRing.DeriveNextKey(
keychain.KeyFamilyRevocationRoot,
)
if err != nil {
return nil, err
}
}
res.nextRevocationKeyLoc = nextRevocationKeyDesc.KeyLocator
// Perform an ECDH operation between the private key described in
// nextRevocationKeyDesc and our public multisig key. The result will be
// used to seed the revocation producer.
revRoot, err := l.ECDH(
nextRevocationKeyDesc, res.ourContribution.MultiSigKey.PubKey,
)
if err != nil {
return nil, err
}
// Once we have the root, we can then generate our shachain producer
// and from that generate the per-commitment point.
return shachain.NewRevocationProducer(revRoot), nil
}

@ -1071,26 +1071,13 @@ func (l *LightningWallet) initOurContribution(reservation *ChannelReservation,
return err return err
} }
// With the above keys created, we'll also need to initialization our // With the above keys created, we'll also need to initialize our
// initial revocation tree state. // revocation tree state, and from that generate the per-commitment point.
nextRevocationKeyDesc, err := keyRing.DeriveNextKey( producer, err := l.nextRevocationProducer(reservation, keyRing)
keychain.KeyFamilyRevocationRoot,
)
if err != nil {
return err
}
revocationRoot, err := l.DerivePrivKey(nextRevocationKeyDesc)
if err != nil { if err != nil {
return err return err
} }
// Once we have the root, we can then generate our shachain producer
// and from that generate the per-commitment point.
revRoot, err := chainhash.NewHash(revocationRoot.Serialize())
if err != nil {
return err
}
producer := shachain.NewRevocationProducer(*revRoot)
firstPreimage, err := producer.AtIndex(0) firstPreimage, err := producer.AtIndex(0)
if err != nil { if err != nil {
return err return err
@ -1727,6 +1714,8 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs
res.partialState.RemoteShutdownScript = res.partialState.RemoteShutdownScript =
res.theirContribution.UpfrontShutdown res.theirContribution.UpfrontShutdown
res.partialState.RevocationKeyLocator = res.nextRevocationKeyLoc
// Add the complete funding transaction to the DB, in its open bucket // Add the complete funding transaction to the DB, in its open bucket
// which will be used for the lifetime of this channel. // which will be used for the lifetime of this channel.
nodeAddr := res.nodeAddr nodeAddr := res.nodeAddr
@ -1891,6 +1880,9 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
// which will be used for the lifetime of this channel. // which will be used for the lifetime of this channel.
chanState.LocalChanCfg = pendingReservation.ourContribution.toChanConfig() chanState.LocalChanCfg = pendingReservation.ourContribution.toChanConfig()
chanState.RemoteChanCfg = pendingReservation.theirContribution.toChanConfig() chanState.RemoteChanCfg = pendingReservation.theirContribution.toChanConfig()
chanState.RevocationKeyLocator = pendingReservation.nextRevocationKeyLoc
err = chanState.SyncPending(pendingReservation.nodeAddr, uint32(bestHeight)) err = chanState.SyncPending(pendingReservation.nodeAddr, uint32(bestHeight))
if err != nil { if err != nil {
req.err <- err req.err <- err