Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/hd cherry pick #195

Merged
merged 6 commits into from
Sep 9, 2022
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
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