Skip to content

Commit

Permalink
Feature/hd cherry pick (#195)
Browse files Browse the repository at this point in the history
* Cherrypick child key derivation (#140)

* Cherrypick child_key_derivation from SwingbyProtocol
https://github.com/SwingbyProtocol/tss-lib/pull/6/files#diff-e663957d1112b8c89bb7a782fe1cebe0d5e4d84a17861ae5af5cc0b59d1dbf56

* Add serialization to child key derivation
add test case from github.com/btcsuite/hdkeychain

* Making ec as parameter in key_derivation_util

* Add version string to extendedkey, to comply with BIP32

* specify curve in NewExtendedKey

* pass curve in DeriveChildKey

* amend the HD cherry pick not to break the existing API of NewLocalParty

* FillBytes not available in go 1.13

Fix test using FillBytes

Co-authored-by: ycen <[email protected]>
Co-authored-by: FitzLu <[email protected]>
  • Loading branch information
3 people authored Sep 9, 2022
1 parent bde1ac3 commit cbfa6cf
Show file tree
Hide file tree
Showing 9 changed files with 608 additions and 0 deletions.
257 changes: 257 additions & 0 deletions crypto/ckd/child_key_derivation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
// Copyright © Swingby

package ckd

import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/hmac"
"crypto/sha256"
"crypto/sha512"
"encoding/binary"
"errors"
"hash"
"math/big"

"github.com/bnb-chain/tss-lib/common"
"github.com/bnb-chain/tss-lib/crypto"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcutil/base58"
"golang.org/x/crypto/ripemd160"
)

type ExtendedKey struct {
ecdsa.PublicKey
Depth uint8
ChildIndex uint32
ChainCode []byte // 32 bytes
ParentFP []byte // parent fingerprint
Version []byte
}

// For more information about child key derivation see https://github.com/binance-chain/tss-lib/issues/104
// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki .
// The functions below do not implement the full BIP-32 specification. As mentioned in the Jira ticket above,
// we only use non-hardened derived keys.

const (

// HardenedKeyStart hardened key starts.
HardenedKeyStart = 0x80000000 // 2^31

// max Depth
maxDepth = 1<<8 - 1

PubKeyBytesLenCompressed = 33

pubKeyCompressed byte = 0x2

serializedKeyLen = 78

// MinSeedBytes is the minimum number of bytes allowed for a seed to
// a master node.
MinSeedBytes = 16 // 128 bits

// MaxSeedBytes is the maximum number of bytes allowed for a seed to
// a master node.
MaxSeedBytes = 64 // 512 bits
)

// Extended public key serialization, defined in BIP32
func (k *ExtendedKey) String() string {
// version(4) || depth(1) || parentFP (4) || childinde(4) || chaincode (32) || key(33) || checksum(4)
var childNumBytes [4]byte
binary.BigEndian.PutUint32(childNumBytes[:], k.ChildIndex)

serializedBytes := make([]byte, 0, serializedKeyLen+4)
serializedBytes = append(serializedBytes, k.Version...)
serializedBytes = append(serializedBytes, k.Depth)
serializedBytes = append(serializedBytes, k.ParentFP...)
serializedBytes = append(serializedBytes, childNumBytes[:]...)
serializedBytes = append(serializedBytes, k.ChainCode...)
pubKeyBytes := serializeCompressed(k.PublicKey.X, k.PublicKey.Y)
serializedBytes = append(serializedBytes, pubKeyBytes...)

checkSum := doubleHashB(serializedBytes)[:4]
serializedBytes = append(serializedBytes, checkSum...)
return base58.Encode(serializedBytes)
}

// NewExtendedKeyFromString returns a new extended key from a base58-encoded extended key
func NewExtendedKeyFromString(key string, curve elliptic.Curve) (*ExtendedKey, error) {
// version(4) || depth(1) || parentFP (4) || childinde(4) || chaincode (32) || key(33) || checksum(4)

decoded := base58.Decode(key)
if len(decoded) != serializedKeyLen+4 {
return nil, errors.New("invalid extended key")
}

// Split the payload and checksum up and ensure the checksum matches.
payload := decoded[:len(decoded)-4]
checkSum := decoded[len(decoded)-4:]
expectedCheckSum := doubleHashB(payload)[:4]
if !bytes.Equal(checkSum, expectedCheckSum) {
return nil, errors.New("invalid extended key")
}

// Deserialize each of the payload fields.
version := payload[:4]
depth := payload[4:5][0]
parentFP := payload[5:9]
childNum := binary.BigEndian.Uint32(payload[9:13])
chainCode := payload[13:45]
keyData := payload[45:78]

var pubKey ecdsa.PublicKey

if c, ok := curve.(*btcec.KoblitzCurve); ok {
// Ensure the public key parses correctly and is actually on the
// secp256k1 curve.
pk, err := btcec.ParsePubKey(keyData, c)
if err != nil {
return nil, err
}
pubKey = ecdsa.PublicKey(*pk)
} else {
px, py := elliptic.Unmarshal(curve, keyData)
pubKey = ecdsa.PublicKey{
Curve: curve,
X: px,
Y: py,
}
}

return &ExtendedKey{
PublicKey: pubKey,
Depth: depth,
ChildIndex: childNum,
ChainCode: chainCode,
ParentFP: parentFP,
Version: version,
}, nil
}

func doubleHashB(b []byte) []byte {
first := sha256.Sum256(b)
second := sha256.Sum256(first[:])
return second[:]
}

func calcHash(buf []byte, hasher hash.Hash) []byte {
hasher.Write(buf)
return hasher.Sum(nil)
}

func hash160(buf []byte) []byte {
return calcHash(calcHash(buf, sha256.New()), ripemd160.New())
}

func isOdd(a *big.Int) bool {
return a.Bit(0) == 1
}

// PaddedAppend append src to dst, if less than size padding 0 at start
func paddedAppend(dst []byte, srcPaddedSize int, src []byte) []byte {
return append(dst, paddedBytes(srcPaddedSize, src)...)
}

// PaddedBytes padding byte array to size length
func paddedBytes(size int, src []byte) []byte {
offset := size - len(src)
tmp := src
if offset > 0 {
tmp = make([]byte, size)
copy(tmp[offset:], src)
}
return tmp
}

// SerializeCompressed serializes a public key 33-byte compressed format
func serializeCompressed(publicKeyX *big.Int, publicKeyY *big.Int) []byte {
b := make([]byte, 0, PubKeyBytesLenCompressed)
format := pubKeyCompressed
if isOdd(publicKeyY) {
format |= 0x1
}
b = append(b, format)
return paddedAppend(b, 32, publicKeyX.Bytes())
}

func DeriveChildKeyFromHierarchy(indicesHierarchy []uint32, pk *ExtendedKey, mod *big.Int, curve elliptic.Curve) (*big.Int, *ExtendedKey, error) {
var k = pk
var err error
var childKey *ExtendedKey
mod_ := common.ModInt(mod)
ilNum := big.NewInt(0)
for index := range indicesHierarchy {
ilNumOld := ilNum
ilNum, childKey, err = DeriveChildKey(indicesHierarchy[index], k, curve)
if err != nil {
return nil, nil, err
}
k = childKey
ilNum = mod_.Add(ilNum, ilNumOld)
}
return ilNum, k, nil
}

// DeriveChildKey Derive a child key from the given parent key. The function returns "IL" ("I left"), per BIP-32 spec. It also
// returns the derived child key.
func DeriveChildKey(index uint32, pk *ExtendedKey, curve elliptic.Curve) (*big.Int, *ExtendedKey, error) {
if index >= HardenedKeyStart {
return nil, nil, errors.New("the index must be non-hardened")
}
if pk.Depth == maxDepth {
return nil, nil, errors.New("cannot derive key beyond max depth")
}

cryptoPk, err := crypto.NewECPoint(curve, pk.X, pk.Y)
if err != nil {
common.Logger.Error("error getting pubkey from extendedkey")
return nil, nil, err
}

pkPublicKeyBytes := serializeCompressed(pk.X, pk.Y)

data := make([]byte, 37)
copy(data, pkPublicKeyBytes)
binary.BigEndian.PutUint32(data[33:], index)

// I = HMAC-SHA512(Key = chainCode, Data=data)
hmac512 := hmac.New(sha512.New, pk.ChainCode)
hmac512.Write(data)
ilr := hmac512.Sum(nil)
il := ilr[:32]
childChainCode := ilr[32:]
ilNum := new(big.Int).SetBytes(il)

if ilNum.Cmp(curve.Params().N) >= 0 || ilNum.Sign() == 0 {
// falling outside of the valid range for curve private keys
err = errors.New("invalid derived key")
common.Logger.Error("error deriving child key")
return nil, nil, err
}

deltaG := crypto.ScalarBaseMult(curve, ilNum)
if deltaG.X().Sign() == 0 || deltaG.Y().Sign() == 0 {
err = errors.New("invalid child")
common.Logger.Error("error invalid child")
return nil, nil, err
}
childCryptoPk, err := cryptoPk.Add(deltaG)
if err != nil {
common.Logger.Error("error adding delta G to parent key")
return nil, nil, err
}

childPk := &ExtendedKey{
PublicKey: *childCryptoPk.ToECDSAPubKey(),
Depth: pk.Depth + 1,
ChildIndex: index,
ChainCode: childChainCode,
ParentFP: hash160(pkPublicKeyBytes)[:4],
Version: pk.Version,
}
return ilNum, childPk, nil
}
132 changes: 132 additions & 0 deletions crypto/ckd/child_key_derivation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright © 2019 Binance
//
// This file is part of Binance. The full Binance copyright notice, including
// terms governing use, modification, and redistribution, is contained in the
// file LICENSE at the root of the source code distribution tree.

package ckd_test

import (
"testing"

. "github.com/bnb-chain/tss-lib/crypto/ckd"
"github.com/btcsuite/btcd/btcec"
)

func TestPublicDerivation(t *testing.T) {
// port from https://github.com/btcsuite/btcutil/blob/master/hdkeychain/extendedkey_test.go
// The public extended keys for test vectors in [BIP32].
testVec1MasterPubKey := "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"
testVec2MasterPubKey := "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB"

tests := []struct {
name string
master string
path []uint32
wantPub string
}{
// Test vector 1
{
name: "test vector 1 chain m",
master: testVec1MasterPubKey,
path: []uint32{},
wantPub: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8",
},
{
name: "test vector 1 chain m/0",
master: testVec1MasterPubKey,
path: []uint32{0},
wantPub: "xpub68Gmy5EVb2BdFbj2LpWrk1M7obNuaPTpT5oh9QCCo5sRfqSHVYWex97WpDZzszdzHzxXDAzPLVSwybe4uPYkSk4G3gnrPqqkV9RyNzAcNJ1",
},
{
name: "test vector 1 chain m/0/1",
master: testVec1MasterPubKey,
path: []uint32{0, 1},
wantPub: "xpub6AvUGrnEpfvJBbfx7sQ89Q8hEMPM65UteqEX4yUbUiES2jHfjexmfJoxCGSwFMZiPBaKQT1RiKWrKfuDV4vpgVs4Xn8PpPTR2i79rwHd4Zr",
},
{
name: "test vector 1 chain m/0/1/2",
master: testVec1MasterPubKey,
path: []uint32{0, 1, 2},
wantPub: "xpub6BqyndF6rhZqmgktFCBcapkwubGxPqoAZtQaYewJHXVKZcLdnqBVC8N6f6FSHWUghjuTLeubWyQWfJdk2G3tGgvgj3qngo4vLTnnSjAZckv",
},
{
name: "test vector 1 chain m/0/1/2/2",
master: testVec1MasterPubKey,
path: []uint32{0, 1, 2, 2},
wantPub: "xpub6FHUhLbYYkgFQiFrDiXRfQFXBB2msCxKTsNyAExi6keFxQ8sHfwpogY3p3s1ePSpUqLNYks5T6a3JqpCGszt4kxbyq7tUoFP5c8KWyiDtPp",
},
{
name: "test vector 1 chain m/0/1/2/2/1000000000",
master: testVec1MasterPubKey,
path: []uint32{0, 1, 2, 2, 1000000000},
wantPub: "xpub6GX3zWVgSgPc5tgjE6ogT9nfwSADD3tdsxpzd7jJoJMqSY12Be6VQEFwDCp6wAQoZsH2iq5nNocHEaVDxBcobPrkZCjYW3QUmoDYzMFBDu9",
},

// Test vector 2
{
name: "test vector 2 chain m",
master: testVec2MasterPubKey,
path: []uint32{},
wantPub: "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB",
},
{
name: "test vector 2 chain m/0",
master: testVec2MasterPubKey,
path: []uint32{0},
wantPub: "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH",
},
{
name: "test vector 2 chain m/0/2147483647",
master: testVec2MasterPubKey,
path: []uint32{0, 2147483647},
wantPub: "xpub6ASAVgeWMg4pmutghzHG3BohahjwNwPmy2DgM6W9wGegtPrvNgjBwuZRD7hSDFhYfunq8vDgwG4ah1gVzZysgp3UsKz7VNjCnSUJJ5T4fdD",
},
{
name: "test vector 2 chain m/0/2147483647/1",
master: testVec2MasterPubKey,
path: []uint32{0, 2147483647, 1},
wantPub: "xpub6CrnV7NzJy4VdgP5niTpqWJiFXMAca6qBm5Hfsry77SQmN1HGYHnjsZSujoHzdxf7ZNK5UVrmDXFPiEW2ecwHGWMFGUxPC9ARipss9rXd4b",
},
{
name: "test vector 2 chain m/0/2147483647/1/2147483646",
master: testVec2MasterPubKey,
path: []uint32{0, 2147483647, 1, 2147483646},
wantPub: "xpub6FL2423qFaWzHCvBndkN9cbkn5cysiUeFq4eb9t9kE88jcmY63tNuLNRzpHPdAM4dUpLhZ7aUm2cJ5zF7KYonf4jAPfRqTMTRBNkQL3Tfta",
},
{
name: "test vector 2 chain m/0/2147483647/1/2147483646/2",
master: testVec2MasterPubKey,
path: []uint32{0, 2147483647, 1, 2147483646, 2},
wantPub: "xpub6H7WkJf547AiSwAbX6xsm8Bmq9M9P1Gjequ5SipsjipWmtXSyp4C3uwzewedGEgAMsDy4jEvNTWtxLyqqHY9C12gaBmgUdk2CGmwachwnWK",
},
}

tests:
for i, test := range tests {
extKey, err := NewExtendedKeyFromString(test.master, btcec.S256())
if err != nil {
t.Errorf("NewKeyFromString #%d (%s): unexpected error "+
"creating extended key: %v", i, test.name,
err)
continue
}

for _, childNum := range test.path {
var err error
_, extKey, err = DeriveChildKey(childNum, extKey, btcec.S256())
if err != nil {
t.Errorf("err: %v", err)
continue tests
}
}

pubStr := extKey.String()
if pubStr != test.wantPub {
t.Errorf("Derive #%d (%s): mismatched serialized "+
"public extended key -- got: %s, want: %s", i,
test.name, pubStr, test.wantPub)
continue
}
}
}
Loading

0 comments on commit cbfa6cf

Please sign in to comment.