Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions cln/derivation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package cln

import (
"crypto/sha256"
"encoding/binary"
"fmt"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/keychain"
"golang.org/x/crypto/hkdf"
)

const (
KeyOffsetFunding = 0
KeyOffsetRevocation = 1
KeyOffsetHtlc = 2
KeyOffsetPayment = 3
KeyOffsetDelayed = 4
)

var (
InfoNodeID = []byte("nodeid")
InfoPeerSeed = []byte("peer seed")
InfoPerPeer = []byte("per-peer seed")
InfoCLightning = []byte("c-lightning")
)

// NodeKey derives a CLN node key from the given HSM secret.
func NodeKey(hsmSecret [32]byte) (*btcec.PublicKey, *btcec.PrivateKey, error) {
salt := make([]byte, 4)
privKeyBytes, err := HkdfSha256(hsmSecret[:], salt, InfoNodeID)
if err != nil {
return nil, nil, err
}

privKey, pubKey := btcec.PrivKeyFromBytes(privKeyBytes[:])
return pubKey, privKey, nil
}

// DeriveKeyPair derives a channel key pair from the given HSM secret, and the
// key descriptor. The public key in the key descriptor is used as the peer's
// public key, the family is converted to the CLN key type, and the index is
// used as the channel's database index.
func DeriveKeyPair(hsmSecret [32]byte,
desc *keychain.KeyDescriptor) (*btcec.PublicKey, *btcec.PrivateKey,
error) {

var offset int
switch desc.Family {
case keychain.KeyFamilyMultiSig:
offset = KeyOffsetFunding

case keychain.KeyFamilyRevocationBase:
offset = KeyOffsetRevocation

case keychain.KeyFamilyHtlcBase:
offset = KeyOffsetHtlc

case keychain.KeyFamilyPaymentBase:
offset = KeyOffsetPayment

case keychain.KeyFamilyDelayBase:
offset = KeyOffsetDelayed

case keychain.KeyFamilyNodeKey:
return NodeKey(hsmSecret)

default:
return nil, nil, fmt.Errorf("unsupported key family for CLN: "+
"%v", desc.Family)
}

channelBase, err := HkdfSha256(hsmSecret[:], nil, InfoPeerSeed)
if err != nil {
return nil, nil, err
}

peerAndChannel := make([]byte, 33+8)
copy(peerAndChannel[:33], desc.PubKey.SerializeCompressed())
binary.LittleEndian.PutUint64(peerAndChannel[33:], uint64(desc.Index))

channelSeed, err := HkdfSha256(
channelBase[:], peerAndChannel, InfoPerPeer,
)
if err != nil {
return nil, nil, err
}

fundingKey, err := HkdfSha256WithSkip(
channelSeed[:], nil, InfoCLightning, offset*32,
)
if err != nil {
return nil, nil, err
}

privKey, pubKey := btcec.PrivKeyFromBytes(fundingKey[:])
return pubKey, privKey, nil
}

// HkdfSha256 derives a 32-byte key from the given input key material, salt, and
// info using the HKDF-SHA256 key derivation function.
func HkdfSha256(key, salt, info []byte) ([32]byte, error) {
return HkdfSha256WithSkip(key, salt, info, 0)
}

// HkdfSha256WithSkip derives a 32-byte key from the given input key material,
// salt, and info using the HKDF-SHA256 key derivation function and skips the
// first `skip` bytes of the output.
func HkdfSha256WithSkip(key, salt, info []byte, skip int) ([32]byte, error) {
expander := hkdf.New(sha256.New, key, salt, info)

if skip > 0 {
skippedBytes := make([]byte, skip)
_, err := expander.Read(skippedBytes)
if err != nil {
return [32]byte{}, err
}
}

var outputKey [32]byte
_, err := expander.Read(outputKey[:])
if err != nil {
return [32]byte{}, err
}

return outputKey, nil
}
111 changes: 111 additions & 0 deletions cln/derivation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package cln

import (
"encoding/hex"
"testing"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/keychain"
"github.com/stretchr/testify/require"
)

var (
hsmSecret = [32]byte{
0x3f, 0x0a, 0x06, 0xc6, 0x38, 0x5b, 0x74, 0x93,
0xf7, 0x5a, 0xa0, 0x08, 0x9f, 0x31, 0x6a, 0x13,
0xbf, 0x72, 0xbe, 0xb4, 0x30, 0xe5, 0x9e, 0x71,
0xb5, 0xac, 0x5a, 0x73, 0x58, 0x1a, 0x62, 0x70,
}
nodeKeyBytes, _ = hex.DecodeString(
"035149629152c1bee83f1e148a51400b5f24bf3e2ca53384dd801418446e" +
"1f53fe",
)

peerPubKeyBytes, _ = hex.DecodeString(
"02678187ca43e6a6f62f9185be98a933bf485313061e6a05578bbd83c54e" +
"88d460",
)
peerPubKey, _ = btcec.ParsePubKey(peerPubKeyBytes)

expectedFundingKeyBytes, _ = hex.DecodeString(
"0326a2171c97673cc8cd7a04a043f0224c59591fc8c9de320a48f7c9b68a" +
"b0ae2b",
)
)

func TestNodeKey(t *testing.T) {
nodeKey, _, err := NodeKey(hsmSecret)
require.NoError(t, err)

require.Equal(t, nodeKeyBytes, nodeKey.SerializeCompressed())
}

func TestFundingKey(t *testing.T) {
fundingKey, _, err := DeriveKeyPair(hsmSecret, &keychain.KeyDescriptor{
PubKey: peerPubKey,
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamilyMultiSig,
Index: 1,
},
})
require.NoError(t, err)

require.Equal(
t, expectedFundingKeyBytes, fundingKey.SerializeCompressed(),
)
}

func TestPaymentBasePointSecret(t *testing.T) {
hsmSecret2, _ := hex.DecodeString(
"665b09e6fc86391f0141d957eb14ec30f8f8a58a876842792474cacc2448" +
"9456",
)

basePointPeerBytes, _ := hex.DecodeString(
"0350aeef9f33a157953d3c3c1ef464bdf421204461959524e52e530c17f1" +
"66f541",
)

expectedPaymentBasePointBytes, _ := hex.DecodeString(
"0339c93ca896829672510f8a4e51caef4b5f6a26f880acf5a120725a7f02" +
"7b56b4",
)

var hsmSecret [32]byte
copy(hsmSecret[:], hsmSecret2)

basepointPeer, err := btcec.ParsePubKey(basePointPeerBytes)
require.NoError(t, err)

nk, _, err := NodeKey(hsmSecret)
require.NoError(t, err)

t.Logf("Node key: %x", nk.SerializeCompressed())

fk, _, err := DeriveKeyPair(hsmSecret, &keychain.KeyDescriptor{
PubKey: basepointPeer,
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamilyMultiSig,
Index: 1,
},
})
require.NoError(t, err)

t.Logf("Funding key: %x", fk.SerializeCompressed())

paymentBasePoint, _, err := DeriveKeyPair(
hsmSecret, &keychain.KeyDescriptor{
PubKey: basepointPeer,
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamilyPaymentBase,
Index: 1,
},
},
)
require.NoError(t, err)

require.Equal(
t, expectedPaymentBasePointBytes,
paymentBasePoint.SerializeCompressed(),
)
}
128 changes: 128 additions & 0 deletions cln/signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package cln

import (
"errors"
"fmt"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/wallet"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
)

type Signer struct {
HsmSecret [32]byte
}

func (s *Signer) SignOutputRaw(tx *wire.MsgTx,
signDesc *input.SignDescriptor) (input.Signature, error) {

// First attempt to fetch the private key which corresponds to the
// specified public key.
privKey, err := s.FetchPrivateKey(&signDesc.KeyDesc)
if err != nil {
return nil, err
}

return lnd.SignOutputRawWithPrivateKey(tx, signDesc, privKey)
}

func (s *Signer) FetchPrivateKey(
descriptor *keychain.KeyDescriptor) (*btcec.PrivateKey, error) {

_, privKey, err := DeriveKeyPair(s.HsmSecret, descriptor)
return privKey, err
}

func (s *Signer) FindMultisigKey(targetPubkey, peerPubKey *btcec.PublicKey,
maxNumKeys uint32) (*keychain.KeyDescriptor, error) {

// Loop through the local multisig keys to find the target key.
for index := range maxNumKeys {
privKey, err := s.FetchPrivateKey(&keychain.KeyDescriptor{
PubKey: peerPubKey,
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamilyMultiSig,
Index: index,
},
})
if err != nil {
return nil, fmt.Errorf("error deriving funding "+
"private key: %w", err)
}

currentPubkey := privKey.PubKey()
if !targetPubkey.IsEqual(currentPubkey) {
continue
}

return &keychain.KeyDescriptor{
PubKey: peerPubKey,
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamilyMultiSig,
Index: index,
},
}, nil
}

return nil, errors.New("no matching pubkeys found")
}

func (s *Signer) AddPartialSignature(packet *psbt.Packet,
keyDesc keychain.KeyDescriptor, utxo *wire.TxOut, witnessScript []byte,
inputIndex int) error {

// Now we add our partial signature.
prevOutFetcher := wallet.PsbtPrevOutputFetcher(packet)
signDesc := &input.SignDescriptor{
KeyDesc: keyDesc,
WitnessScript: witnessScript,
Output: utxo,
InputIndex: inputIndex,
HashType: txscript.SigHashAll,
PrevOutputFetcher: prevOutFetcher,
SigHashes: txscript.NewTxSigHashes(
packet.UnsignedTx, prevOutFetcher,
),
}
ourSigRaw, err := s.SignOutputRaw(packet.UnsignedTx, signDesc)
if err != nil {
return fmt.Errorf("error signing with our key: %w", err)
}
ourSig := append(ourSigRaw.Serialize(), byte(txscript.SigHashAll))

// Because of the way we derive keys in CLN, the public key in the key
// descriptor is the peer's public key, not our own. So we need to
// derive our own public key from the private key.
ourPrivKey, err := s.FetchPrivateKey(&keyDesc)
if err != nil {
return fmt.Errorf("error fetching private key for descriptor "+
"%v: %w", keyDesc, err)
}
ourPubKey := ourPrivKey.PubKey()

// Great, we were able to create our sig, let's add it to the PSBT.
updater, err := psbt.NewUpdater(packet)
if err != nil {
return fmt.Errorf("error creating PSBT updater: %w", err)
}
status, err := updater.Sign(
inputIndex, ourSig, ourPubKey.SerializeCompressed(), nil,
witnessScript,
)
if err != nil {
return fmt.Errorf("error adding signature to PSBT: %w", err)
}
if status != 0 {
return fmt.Errorf("unexpected status for signature update, "+
"got %d wanted 0", status)
}

return nil
}

var _ lnd.ChannelSigner = (*Signer)(nil)
Loading