Skip to content

Commit 8a67385

Browse files
committed
cln: add CLN key derivation
1 parent e8ad9e0 commit 8a67385

File tree

2 files changed

+238
-0
lines changed

2 files changed

+238
-0
lines changed

cln/derivation.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package cln
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/binary"
6+
"fmt"
7+
8+
"github.com/btcsuite/btcd/btcec/v2"
9+
"github.com/lightningnetwork/lnd/keychain"
10+
"golang.org/x/crypto/hkdf"
11+
)
12+
13+
const (
14+
KeyOffsetFunding = 0
15+
KeyOffsetRevocation = 1
16+
KeyOffsetHtlc = 2
17+
KeyOffsetPayment = 3
18+
KeyOffsetDelayed = 4
19+
)
20+
21+
var (
22+
InfoNodeID = []byte("nodeid")
23+
InfoPeerSeed = []byte("peer seed")
24+
InfoPerPeer = []byte("per-peer seed")
25+
InfoCLightning = []byte("c-lightning")
26+
)
27+
28+
// NodeKey derives a CLN node key from the given HSM secret.
29+
func NodeKey(hsmSecret [32]byte) (*btcec.PublicKey, *btcec.PrivateKey, error) {
30+
salt := make([]byte, 4)
31+
privKeyBytes, err := HkdfSha256(hsmSecret[:], salt, InfoNodeID)
32+
if err != nil {
33+
return nil, nil, err
34+
}
35+
36+
privKey, pubKey := btcec.PrivKeyFromBytes(privKeyBytes[:])
37+
return pubKey, privKey, nil
38+
}
39+
40+
// DeriveKeyPair derives a channel key pair from the given HSM secret, and the
41+
// key descriptor. The public key in the key descriptor is used as the peer's
42+
// public key, the family is converted to the CLN key type, and the index is
43+
// used as the channel's database index.
44+
func DeriveKeyPair(hsmSecret [32]byte,
45+
desc *keychain.KeyDescriptor) (*btcec.PublicKey, *btcec.PrivateKey,
46+
error) {
47+
48+
var offset int
49+
switch desc.Family {
50+
case keychain.KeyFamilyMultiSig:
51+
offset = KeyOffsetFunding
52+
53+
case keychain.KeyFamilyRevocationBase:
54+
offset = KeyOffsetRevocation
55+
56+
case keychain.KeyFamilyHtlcBase:
57+
offset = KeyOffsetHtlc
58+
59+
case keychain.KeyFamilyPaymentBase:
60+
offset = KeyOffsetPayment
61+
62+
case keychain.KeyFamilyDelayBase:
63+
offset = KeyOffsetDelayed
64+
65+
case keychain.KeyFamilyNodeKey:
66+
return NodeKey(hsmSecret)
67+
68+
default:
69+
return nil, nil, fmt.Errorf("unsupported key family for CLN: "+
70+
"%v", desc.Family)
71+
}
72+
73+
channelBase, err := HkdfSha256(hsmSecret[:], nil, InfoPeerSeed)
74+
if err != nil {
75+
return nil, nil, err
76+
}
77+
78+
peerAndChannel := make([]byte, 33+8)
79+
copy(peerAndChannel[:33], desc.PubKey.SerializeCompressed())
80+
binary.LittleEndian.PutUint64(peerAndChannel[33:], uint64(desc.Index))
81+
82+
channelSeed, err := HkdfSha256(
83+
channelBase[:], peerAndChannel, InfoPerPeer,
84+
)
85+
if err != nil {
86+
return nil, nil, err
87+
}
88+
89+
fundingKey, err := HkdfSha256WithSkip(
90+
channelSeed[:], nil, InfoCLightning, offset*32,
91+
)
92+
if err != nil {
93+
return nil, nil, err
94+
}
95+
96+
privKey, pubKey := btcec.PrivKeyFromBytes(fundingKey[:])
97+
return pubKey, privKey, nil
98+
}
99+
100+
// HkdfSha256 derives a 32-byte key from the given input key material, salt, and
101+
// info using the HKDF-SHA256 key derivation function.
102+
func HkdfSha256(key, salt, info []byte) ([32]byte, error) {
103+
return HkdfSha256WithSkip(key, salt, info, 0)
104+
}
105+
106+
// HkdfSha256WithSkip derives a 32-byte key from the given input key material,
107+
// salt, and info using the HKDF-SHA256 key derivation function and skips the
108+
// first `skip` bytes of the output.
109+
func HkdfSha256WithSkip(key, salt, info []byte, skip int) ([32]byte, error) {
110+
expander := hkdf.New(sha256.New, key, salt, info)
111+
112+
if skip > 0 {
113+
skippedBytes := make([]byte, skip)
114+
_, err := expander.Read(skippedBytes)
115+
if err != nil {
116+
return [32]byte{}, err
117+
}
118+
}
119+
120+
var outputKey [32]byte
121+
_, err := expander.Read(outputKey[:])
122+
if err != nil {
123+
return [32]byte{}, err
124+
}
125+
126+
return outputKey, nil
127+
}

cln/derivation_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package cln
2+
3+
import (
4+
"encoding/hex"
5+
"testing"
6+
7+
"github.com/btcsuite/btcd/btcec/v2"
8+
"github.com/lightningnetwork/lnd/keychain"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
var (
13+
hsmSecret = [32]byte{
14+
0x3f, 0x0a, 0x06, 0xc6, 0x38, 0x5b, 0x74, 0x93,
15+
0xf7, 0x5a, 0xa0, 0x08, 0x9f, 0x31, 0x6a, 0x13,
16+
0xbf, 0x72, 0xbe, 0xb4, 0x30, 0xe5, 0x9e, 0x71,
17+
0xb5, 0xac, 0x5a, 0x73, 0x58, 0x1a, 0x62, 0x70,
18+
}
19+
nodeKeyBytes, _ = hex.DecodeString(
20+
"035149629152c1bee83f1e148a51400b5f24bf3e2ca53384dd801418446e" +
21+
"1f53fe",
22+
)
23+
24+
peerPubKeyBytes, _ = hex.DecodeString(
25+
"02678187ca43e6a6f62f9185be98a933bf485313061e6a05578bbd83c54e" +
26+
"88d460",
27+
)
28+
peerPubKey, _ = btcec.ParsePubKey(peerPubKeyBytes)
29+
30+
expectedFundingKeyBytes, _ = hex.DecodeString(
31+
"0326a2171c97673cc8cd7a04a043f0224c59591fc8c9de320a48f7c9b68a" +
32+
"b0ae2b",
33+
)
34+
)
35+
36+
func TestNodeKey(t *testing.T) {
37+
nodeKey, _, err := NodeKey(hsmSecret)
38+
require.NoError(t, err)
39+
40+
require.Equal(t, nodeKeyBytes, nodeKey.SerializeCompressed())
41+
}
42+
43+
func TestFundingKey(t *testing.T) {
44+
fundingKey, _, err := DeriveKeyPair(hsmSecret, &keychain.KeyDescriptor{
45+
PubKey: peerPubKey,
46+
KeyLocator: keychain.KeyLocator{
47+
Family: keychain.KeyFamilyMultiSig,
48+
Index: 1,
49+
},
50+
})
51+
require.NoError(t, err)
52+
53+
require.Equal(
54+
t, expectedFundingKeyBytes, fundingKey.SerializeCompressed(),
55+
)
56+
}
57+
58+
func TestPaymentBasePointSecret(t *testing.T) {
59+
hsmSecret2, _ := hex.DecodeString(
60+
"665b09e6fc86391f0141d957eb14ec30f8f8a58a876842792474cacc2448" +
61+
"9456",
62+
)
63+
64+
basePointPeerBytes, _ := hex.DecodeString(
65+
"0350aeef9f33a157953d3c3c1ef464bdf421204461959524e52e530c17f1" +
66+
"66f541",
67+
)
68+
69+
expectedPaymentBasePointBytes, _ := hex.DecodeString(
70+
"0339c93ca896829672510f8a4e51caef4b5f6a26f880acf5a120725a7f02" +
71+
"7b56b4",
72+
)
73+
74+
var hsmSecret [32]byte
75+
copy(hsmSecret[:], hsmSecret2)
76+
77+
basepointPeer, err := btcec.ParsePubKey(basePointPeerBytes)
78+
require.NoError(t, err)
79+
80+
nk, _, err := NodeKey(hsmSecret)
81+
require.NoError(t, err)
82+
83+
t.Logf("Node key: %x", nk.SerializeCompressed())
84+
85+
fk, _, err := DeriveKeyPair(hsmSecret, &keychain.KeyDescriptor{
86+
PubKey: basepointPeer,
87+
KeyLocator: keychain.KeyLocator{
88+
Family: keychain.KeyFamilyMultiSig,
89+
Index: 1,
90+
},
91+
})
92+
require.NoError(t, err)
93+
94+
t.Logf("Funding key: %x", fk.SerializeCompressed())
95+
96+
paymentBasePoint, _, err := DeriveKeyPair(
97+
hsmSecret, &keychain.KeyDescriptor{
98+
PubKey: basepointPeer,
99+
KeyLocator: keychain.KeyLocator{
100+
Family: keychain.KeyFamilyPaymentBase,
101+
Index: 1,
102+
},
103+
},
104+
)
105+
require.NoError(t, err)
106+
107+
require.Equal(
108+
t, expectedPaymentBasePointBytes,
109+
paymentBasePoint.SerializeCompressed(),
110+
)
111+
}

0 commit comments

Comments
 (0)