initial commit

This commit is contained in:
Adam Babik 2019-11-04 09:08:47 +01:00
parent fb93727be7
commit 14363a42a6
No known key found for this signature in database
GPG Key ID: ED02515A1FC0D1B4
8 changed files with 3104 additions and 0 deletions

12
go.mod Normal file
View File

@ -0,0 +1,12 @@
module github.com/status-im/extkeys
go 1.13
require (
github.com/btcsuite/btcd v0.20.0-beta
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d
// go-ethereum v1.9.5 is used across all libraries in status-im
github.com/ethereum/go-ethereum v1.9.5
golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf
golang.org/x/text v0.3.2
)

43
go.sum Normal file
View File

@ -0,0 +1,43 @@
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/btcsuite/btcd v0.20.0-beta h1:DnZGUjFbRkpytojHWwy6nfUSA7vFrzWXDLpFNzt74ZA=
github.com/btcsuite/btcd v0.20.0-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ethereum/go-ethereum v1.9.5 h1:4oxsF+/3N/sTgda9XTVG4r+wMVLsveziSMcK83hPbsk=
github.com/ethereum/go-ethereum v1.9.5/go.mod h1:PwpWDrCLZrV+tfrhqqF6kPknbISMHaJv9Ln3kPCZLwY=
github.com/ethereum/go-ethereum v1.9.6 h1:EacwxMGKZezZi+m3in0Tlyk0veDQgnfZ9BjQqHAaQLM=
github.com/ethereum/go-ethereum v1.9.6/go.mod h1:PwpWDrCLZrV+tfrhqqF6kPknbISMHaJv9Ln3kPCZLwY=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf h1:fnPsqIDRbCSgumaMCRpoIoF2s4qxv0xSSS0BVZUE/ss=
golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

491
hdkey.go Normal file
View File

@ -0,0 +1,491 @@
package extkeys
import (
"bytes"
"crypto/ecdsa"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"math/big"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/base58"
)
// Implementation of the following BIPs:
// - BIP32 (https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
// - BIP39 (https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki)
// - BIP44 (https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki)
//
// Referencing
// https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
// https://bitcoin.org/en/developer-guide#hardened-keys
// Reference Implementations
// https://github.com/btcsuite/btcutil/tree/master/hdkeychain
// https://github.com/WeMeetAgain/go-hdwallet
// https://github.com/ConsenSys/eth-lightwallet/blob/master/lib/keystore.js
// https://github.com/bitpay/bitcore-lib/tree/master/lib
// MUST CREATE HARDENED CHILDREN OF THE MASTER PRIVATE KEY (M) TO PREVENT
// A COMPROMISED CHILD KEY FROM COMPROMISING THE MASTER KEY.
// AS THERE ARE NO NORMAL CHILDREN FOR THE MASTER KEYS,
// THE MASTER PUBLIC KEY IS NOT USED IN HD WALLETS.
// ALL OTHER KEYS CAN HAVE NORMAL CHILDREN,
// SO THE CORRESPONDING EXTENDED PUBLIC KEYS MAY BE USED INSTEAD.
// TODO make sure we're doing this ^^^^ !!!!!!
type KeyPurpose int
const (
KeyPurposeWallet KeyPurpose = iota + 1
KeyPurposeChat
)
const (
// HardenedKeyStart defines a starting point for hardened key.
// Each extended key has 2^31 normal child keys and 2^31 hardened child keys.
// Thus the range for normal child keys is [0, 2^31 - 1] and the range for hardened child keys is [2^31, 2^32 - 1].
HardenedKeyStart = 0x80000000 // 2^31
// 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
// serializedKeyLen is the length of a serialized public or private
// extended key. It consists of 4 bytes version, 1 byte depth, 4 bytes
// fingerprint, 4 bytes child number, 32 bytes chain code, and 33 bytes
// public/private key data.
serializedKeyLen = 4 + 1 + 4 + 4 + 32 + 33 // 78 bytes
// CoinTypeBTC is BTC coin type
CoinTypeBTC = 0 // 0x80000000
// CoinTypeTestNet is test net coin type
CoinTypeTestNet = 1 // 0x80000001
// CoinTypeETH is ETH coin type
CoinTypeETH = 60 // 0x8000003c
// EmptyExtendedKeyString marker string for zero extended key
EmptyExtendedKeyString = "Zeroed extended key"
// MaxDepth is the maximum depth of an extended key.
// Extended keys with depth MaxDepth cannot derive child keys.
MaxDepth = 255
)
// errors
var (
ErrInvalidKey = errors.New("key is invalid")
ErrInvalidKeyPurpose = errors.New("key purpose is invalid")
ErrInvalidSeed = errors.New("seed is invalid")
ErrInvalidSeedLen = fmt.Errorf("the recommended size of seed is %d-%d bits", MinSeedBytes, MaxSeedBytes)
ErrDerivingHardenedFromPublic = errors.New("cannot derive a hardened key from public key")
ErrBadChecksum = errors.New("bad extended key checksum")
ErrInvalidKeyLen = errors.New("serialized extended key length is invalid")
ErrDerivingChild = errors.New("error deriving child key")
ErrInvalidMasterKey = errors.New("invalid master key supplied")
ErrMaxDepthExceeded = errors.New("max depth exceeded")
)
var (
// PrivateKeyVersion is version for private key
PrivateKeyVersion, _ = hex.DecodeString("0488ADE4")
// PublicKeyVersion is version for public key
PublicKeyVersion, _ = hex.DecodeString("0488B21E")
// EthBIP44ParentPath is BIP44 keys parent's derivation path
EthBIP44ParentPath = []uint32{
HardenedKeyStart + 44, // purpose
HardenedKeyStart + CoinTypeETH, // cointype set to ETH
HardenedKeyStart + 0, // account
0, // 0 - public, 1 - private
}
// EIP1581KeyTypeChat is used as chat key_type in the derivation of EIP1581 keys
EIP1581KeyTypeChat uint32 = 0x00
// EthEIP1581ChatParentPath is EIP-1581 chat keys parent's derivation path
EthEIP1581ChatParentPath = []uint32{
HardenedKeyStart + 43, // purpose
HardenedKeyStart + CoinTypeETH, // cointype set to ETH
HardenedKeyStart + 1581, // EIP-1581 subpurpose
HardenedKeyStart + EIP1581KeyTypeChat, // key_type (chat)
}
)
// ExtendedKey represents BIP44-compliant HD key
type ExtendedKey struct {
Version []byte // 4 bytes, mainnet: 0x0488B21E public, 0x0488ADE4 private; testnet: 0x043587CF public, 0x04358394 private
Depth uint8 // 1 byte, depth: 0x00 for master nodes, 0x01 for level-1 derived keys, ....
FingerPrint []byte // 4 bytes, fingerprint of the parent's key (0x00000000 if master key)
ChildNumber uint32 // 4 bytes, This is ser32(i) for i in xi = xpar/i, with xi the key being serialized. (0x00000000 if master key)
KeyData []byte // 33 bytes, the public key or private key data (serP(K) for public keys, 0x00 || ser256(k) for private keys)
ChainCode []byte // 32 bytes, the chain code
IsPrivate bool // (non-serialized) if false, this chain will only contain a public key and can only create a public key chain.
CachedPubKeyData []byte // (non-serialized) used for memoization of public key (calculated from a private key)
}
// nolint: gas
const masterSecret = "Bitcoin seed"
// NewMaster creates new master node, root of HD chain/tree.
// Both master and child nodes are of ExtendedKey type, and all the children derive from the root node.
func NewMaster(seed []byte) (*ExtendedKey, error) {
// Ensure seed is within expected limits
lseed := len(seed)
if lseed < MinSeedBytes || lseed > MaxSeedBytes {
return nil, ErrInvalidSeedLen
}
secretKey, chainCode, err := splitHMAC(seed, []byte(masterSecret))
if err != nil {
return nil, err
}
master := &ExtendedKey{
Version: PrivateKeyVersion,
Depth: 0,
FingerPrint: []byte{0x00, 0x00, 0x00, 0x00},
ChildNumber: 0,
KeyData: secretKey,
ChainCode: chainCode,
IsPrivate: true,
}
return master, nil
}
// Child derives extended key at a given index i.
// If parent is private, then derived key is also private. If parent is public, then derived is public.
//
// If i >= HardenedKeyStart, then hardened key is generated.
// You can only generate hardened keys from private parent keys.
// If you try generating hardened key form public parent key, ErrDerivingHardenedFromPublic is returned.
//
// There are four CKD (child key derivation) scenarios:
// 1) Private extended key -> Hardened child private extended key
// 2) Private extended key -> Non-hardened child private extended key
// 3) Public extended key -> Non-hardened child public extended key
// 4) Public extended key -> Hardened child public extended key (INVALID!)
func (k *ExtendedKey) Child(i uint32) (*ExtendedKey, error) {
if k.Depth == MaxDepth {
return nil, ErrMaxDepthExceeded
}
// A hardened child may not be created from a public extended key (Case #4).
isChildHardened := i >= HardenedKeyStart
if !k.IsPrivate && isChildHardened {
return nil, ErrDerivingHardenedFromPublic
}
keyLen := 33
seed := make([]byte, keyLen+4)
if isChildHardened {
// Case #1: 0x00 || ser256(parentKey) || ser32(i)
copy(seed[1:], k.KeyData) // 0x00 || ser256(parentKey)
} else {
// Case #2 and #3: serP(parentPubKey) || ser32(i)
copy(seed, k.pubKeyBytes())
}
binary.BigEndian.PutUint32(seed[keyLen:], i)
secretKey, chainCode, err := splitHMAC(seed, k.ChainCode)
if err != nil {
return nil, err
}
child := &ExtendedKey{
ChainCode: chainCode,
Depth: k.Depth + 1,
ChildNumber: i,
IsPrivate: k.IsPrivate,
// The fingerprint for the derived child is the first 4 bytes of parent's
FingerPrint: btcutil.Hash160(k.pubKeyBytes())[:4],
}
if k.IsPrivate {
// Case #1 or #2: childKey = parse256(IL) + parentKey
parentKeyBigInt := new(big.Int).SetBytes(k.KeyData)
keyBigInt := new(big.Int).SetBytes(secretKey)
keyBigInt.Add(keyBigInt, parentKeyBigInt)
keyBigInt.Mod(keyBigInt, btcec.S256().N)
// Make sure that child.KeyData is 32 bytes of data even if the value is represented with less bytes.
// When we derive a child of this key, we call splitHMAC that does a sha512 of a seed that is:
// - 1 byte with 0x00
// - 32 bytes for the key data
// - 4 bytes for the child key index
// If we don't padd the KeyData, it will be shifted to left in that 32 bytes space
// generating a different seed and different child key.
// This part fixes a bug we had previously and described at:
// https://medium.com/@alexberegszaszi/why-do-my-bip32-wallets-disagree-6f3254cc5846#.86inuifuq
keyData := keyBigInt.Bytes()
if len(keyData) < 32 {
extra := make([]byte, 32-len(keyData))
keyData = append(extra, keyData...)
}
child.KeyData = keyData
child.Version = PrivateKeyVersion
} else {
// Case #3: childKey = serP(point(parse256(IL)) + parentKey)
// Calculate the corresponding intermediate public key for intermediate private key.
keyx, keyy := btcec.S256().ScalarBaseMult(secretKey)
if keyx.Sign() == 0 || keyy.Sign() == 0 {
return nil, ErrInvalidKey
}
// Convert the serialized compressed parent public key into X and Y coordinates
// so it can be added to the intermediate public key.
pubKey, err := btcec.ParsePubKey(k.KeyData, btcec.S256())
if err != nil {
return nil, err
}
// childKey = serP(point(parse256(IL)) + parentKey)
childX, childY := btcec.S256().Add(keyx, keyy, pubKey.X, pubKey.Y)
pk := btcec.PublicKey{Curve: btcec.S256(), X: childX, Y: childY}
child.KeyData = pk.SerializeCompressed()
child.Version = PublicKeyVersion
}
return child, nil
}
// ChildForPurpose derives the child key at index i using a derivation path based on the purpose.
func (k *ExtendedKey) ChildForPurpose(p KeyPurpose, i uint32) (*ExtendedKey, error) {
switch p {
case KeyPurposeWallet:
return k.EthBIP44Child(i)
case KeyPurposeChat:
return k.EthEIP1581ChatChild(i)
default:
return nil, ErrInvalidKeyPurpose
}
}
// BIP44Child returns Status CKD#i (where i is child index).
// BIP44 format is used: m / purpose' / coin_type' / account' / change / address_index
// BIP44Child is depracated in favour of EthBIP44Child
// Param coinType is deprecated; we override it to always use CoinTypeETH.
func (k *ExtendedKey) BIP44Child(coinType, i uint32) (*ExtendedKey, error) {
return k.EthBIP44Child(i)
}
// BIP44Child returns Status CKD#i (where i is child index).
// BIP44 format is used: m / purpose' / coin_type' / account' / change / address_index
func (k *ExtendedKey) EthBIP44Child(i uint32) (*ExtendedKey, error) {
if !k.IsPrivate {
return nil, ErrInvalidMasterKey
}
if k.Depth != 0 {
return nil, ErrInvalidMasterKey
}
// m/44'/60'/0'/0/index
extKey, err := k.Derive(append(EthBIP44ParentPath, i))
if err != nil {
return nil, err
}
return extKey, nil
}
// EthEIP1581ChatChild returns the whisper key #i (where i is child index).
// EthEIP1581ChatChild format is used is the one defined in the EIP-1581:
// m / 43' / coin_type' / 1581' / key_type / index
func (k *ExtendedKey) EthEIP1581ChatChild(i uint32) (*ExtendedKey, error) {
if !k.IsPrivate {
return nil, ErrInvalidMasterKey
}
if k.Depth != 0 {
return nil, ErrInvalidMasterKey
}
// m/43'/60'/1581'/0/index
extKey, err := k.Derive(append(EthEIP1581ChatParentPath, i))
if err != nil {
return nil, err
}
return extKey, nil
}
// Derive returns a derived child key at a given path
func (k *ExtendedKey) Derive(path []uint32) (*ExtendedKey, error) {
var err error
extKey := k
for _, i := range path {
extKey, err = extKey.Child(i)
if err != nil {
return nil, ErrDerivingChild
}
}
return extKey, nil
}
// Neuter returns a new extended public key from a give extended private key.
// If the input extended key is already public, it will be returned unaltered.
func (k *ExtendedKey) Neuter() (*ExtendedKey, error) {
// Already an extended public key.
if !k.IsPrivate {
return k, nil
}
// Get the associated public extended key version bytes.
version, err := chaincfg.HDPrivateKeyToPublicKeyID(k.Version)
if err != nil {
return nil, err
}
// Convert it to an extended public key. The key for the new extended
// key will simply be the pubkey of the current extended private key.
return &ExtendedKey{
Version: version,
KeyData: k.pubKeyBytes(),
ChainCode: k.ChainCode,
FingerPrint: k.FingerPrint,
Depth: k.Depth,
ChildNumber: k.ChildNumber,
IsPrivate: false,
}, nil
}
// IsZeroed returns true if key is nil or empty
func (k *ExtendedKey) IsZeroed() bool {
return k == nil || len(k.KeyData) == 0
}
// String returns the extended key as a human-readable base58-encoded string.
func (k *ExtendedKey) String() string {
if k.IsZeroed() {
return EmptyExtendedKeyString
}
var childNumBytes [4]byte
binary.BigEndian.PutUint32(childNumBytes[:], k.ChildNumber)
// The serialized format is:
// version (4) || depth (1) || parent fingerprint (4)) ||
// child num (4) || chain code (32) || key data (33) || checksum (4)
serializedBytes := make([]byte, 0, serializedKeyLen+4)
serializedBytes = append(serializedBytes, k.Version...)
serializedBytes = append(serializedBytes, k.Depth)
serializedBytes = append(serializedBytes, k.FingerPrint...)
serializedBytes = append(serializedBytes, childNumBytes[:]...)
serializedBytes = append(serializedBytes, k.ChainCode...)
if k.IsPrivate {
serializedBytes = append(serializedBytes, 0x00)
serializedBytes = paddedAppend(32, serializedBytes, k.KeyData)
} else {
serializedBytes = append(serializedBytes, k.pubKeyBytes()...)
}
checkSum := chainhash.DoubleHashB(serializedBytes)[:4]
serializedBytes = append(serializedBytes, checkSum...)
return base58.Encode(serializedBytes)
}
// pubKeyBytes returns bytes for the serialized compressed public key associated
// with this extended key in an efficient manner including memoization as
// necessary.
//
// When the extended key is already a public key, the key is simply returned as
// is since it's already in the correct form. However, when the extended key is
// a private key, the public key will be calculated and memoized so future
// accesses can simply return the cached result.
func (k *ExtendedKey) pubKeyBytes() []byte {
// Just return the key if it's already an extended public key.
if !k.IsPrivate {
return k.KeyData
}
pkx, pky := btcec.S256().ScalarBaseMult(k.KeyData)
pubKey := btcec.PublicKey{Curve: btcec.S256(), X: pkx, Y: pky}
return pubKey.SerializeCompressed()
}
// ToECDSA returns the key data as ecdsa.PrivateKey
func (k *ExtendedKey) ToECDSA() *ecdsa.PrivateKey {
privKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), k.KeyData)
return privKey.ToECDSA()
}
// NewKeyFromString returns a new extended key instance from a base58-encoded
// extended key.
func NewKeyFromString(key string) (*ExtendedKey, error) {
if key == EmptyExtendedKeyString || len(key) == 0 {
return &ExtendedKey{}, nil
}
// The base58-decoded extended key must consist of a serialized payload
// plus an additional 4 bytes for the checksum.
decoded := base58.Decode(key)
if len(decoded) != serializedKeyLen+4 {
return nil, ErrInvalidKeyLen
}
// The serialized format is:
// version (4) || depth (1) || parent fingerprint (4)) ||
// child num (4) || chain code (32) || key data (33) || checksum (4)
// Split the payload and checksum up and ensure the checksum matches.
payload := decoded[:len(decoded)-4]
checkSum := decoded[len(decoded)-4:]
expectedCheckSum := chainhash.DoubleHashB(payload)[:4]
if !bytes.Equal(checkSum, expectedCheckSum) {
return nil, ErrBadChecksum
}
// Deserialize each of the payload fields.
version := payload[:4]
depth := payload[4:5][0]
fingerPrint := payload[5:9]
childNumber := binary.BigEndian.Uint32(payload[9:13])
chainCode := payload[13:45]
keyData := payload[45:78]
// The key data is a private key if it starts with 0x00. Serialized
// compressed pubkeys either start with 0x02 or 0x03.
isPrivate := keyData[0] == 0x00
if isPrivate {
// Ensure the private key is valid. It must be within the range
// of the order of the secp256k1 curve and not be 0.
keyData = keyData[1:]
keyNum := new(big.Int).SetBytes(keyData)
if keyNum.Cmp(btcec.S256().N) >= 0 || keyNum.Sign() == 0 {
return nil, ErrInvalidSeed
}
} else {
// Ensure the public key parses correctly and is actually on the
// secp256k1 curve.
_, err := btcec.ParsePubKey(keyData, btcec.S256())
if err != nil {
return nil, err
}
}
return &ExtendedKey{
Version: version,
KeyData: keyData,
ChainCode: chainCode,
FingerPrint: fingerPrint,
Depth: depth,
ChildNumber: childNumber,
IsPrivate: isPrivate,
}, nil
}

745
hdkey_test.go Normal file
View File

@ -0,0 +1,745 @@
package extkeys
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"reflect"
"testing"
"github.com/btcsuite/btcd/chaincfg"
"github.com/ethereum/go-ethereum/crypto"
)
const (
masterPrivKey1 = "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"
masterPrivKey2 = "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U"
)
func TestBIP32Vectors(t *testing.T) {
// Test vectors 1, 2, and 3 are taken from the BIP32 specs:
// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vectors
tests := []struct {
name string
seed string
path []uint32
pubKey string
privKey string
}{
// Test vector 1
{
"test vector 1 chain m",
"000102030405060708090a0b0c0d0e0f",
[]uint32{},
"xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8",
masterPrivKey1,
},
{
"test vector 1 chain m/0H",
"000102030405060708090a0b0c0d0e0f",
[]uint32{HardenedKeyStart},
"xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw",
"xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7",
},
{
"test vector 1 chain m/0H/1",
"000102030405060708090a0b0c0d0e0f",
[]uint32{HardenedKeyStart, 1},
"xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ",
"xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs",
},
{
"test vector 1 chain m/0H/1/2H",
"000102030405060708090a0b0c0d0e0f",
[]uint32{HardenedKeyStart, 1, HardenedKeyStart + 2},
"xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5",
"xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM",
},
{
"test vector 1 chain m/0H/1/2H/2",
"000102030405060708090a0b0c0d0e0f",
[]uint32{HardenedKeyStart, 1, HardenedKeyStart + 2, 2},
"xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV",
"xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334",
},
{
"test vector 1 chain m/0H/1/2H/2/1000000000",
"000102030405060708090a0b0c0d0e0f",
[]uint32{HardenedKeyStart, 1, HardenedKeyStart + 2, 2, 1000000000},
"xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy",
"xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76",
},
// Test vector 2
{
"test vector 2 chain m",
"fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
[]uint32{},
"xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB",
masterPrivKey2,
},
{
"test vector 2 chain m/0",
"fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
[]uint32{0},
"xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH",
"xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt",
},
{
"test vector 2 chain m/0/2147483647H",
"fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
[]uint32{0, HardenedKeyStart + 2147483647},
"xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a",
"xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9",
},
{
"test vector 2 chain m/0/2147483647H/1",
"fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
[]uint32{0, HardenedKeyStart + 2147483647, 1},
"xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon",
"xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef",
},
{
"test vector 2 chain m/0/2147483647H/1/2147483646H",
"fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
[]uint32{0, HardenedKeyStart + 2147483647, 1, HardenedKeyStart + 2147483646},
"xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL",
"xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc",
},
{
"test vector 2 chain m/0/2147483647H/1/2147483646H/2",
"fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
[]uint32{0, HardenedKeyStart + 2147483647, 1, HardenedKeyStart + 2147483646, 2},
"xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt",
"xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j",
},
// Test vector 3
{
"test vector 3 chain m",
"4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be",
[]uint32{},
"xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13",
"xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6",
},
{
"test vector 3 chain m/0H",
"4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be",
[]uint32{HardenedKeyStart},
"xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y",
"xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L",
},
}
tests:
for i, test := range tests {
seed, err := hex.DecodeString(test.seed)
if err != nil {
t.Errorf("DecodeString #%d (%s): %v", i, test.name, err)
continue
}
extKey, err := NewMaster(seed)
if err != nil {
t.Errorf("NewMasterKey #%d (%s): %v", i, test.name, err)
continue
}
if !extKey.IsPrivate {
t.Error("Master node must feature private key")
continue
}
extKey, err = extKey.Derive(test.path)
if err != nil {
t.Errorf("cannot derive child: %v", err)
continue tests
}
privKeyStr := extKey.String()
if privKeyStr != test.privKey {
t.Errorf("%d (%s): private key mismatch (expects: %s, got: %s)", i, test.name, test.privKey, privKeyStr)
continue
} else {
t.Logf("test %d (%s): %s", i, test.name, extKey.String())
}
pubKey, err := extKey.Neuter()
if err != nil {
t.Errorf("failed to Neuter key #%d (%s): %v", i, test.name, err)
return
}
// neutering twice should have no effect
pubKey, err = pubKey.Neuter()
if err != nil {
t.Errorf("failed to Neuter key #%d (%s): %v", i, test.name, err)
return
}
pubKeyStr := pubKey.String()
if pubKeyStr != test.pubKey {
t.Errorf("%d (%s): public key mismatch (expects: %s, got: %s)", i, test.name, test.pubKey, pubKeyStr)
continue
} else {
t.Logf("test %d (%s, public): %s", i, test.name, extKey.String())
}
}
}
func TestChildDerivation(t *testing.T) {
type testCase struct {
name string
master string
path []uint32
wantKey string
}
// derive public keys from private keys
getPrivateChildDerivationTests := func() []testCase {
// The private extended keys for test vectors in [BIP32].
testVec1MasterPrivKey := masterPrivKey1
testVec2MasterPrivKey := masterPrivKey2
return []testCase{
// Test vector 1
{
name: "test vector 1 chain m",
master: testVec1MasterPrivKey,
path: []uint32{},
wantKey: masterPrivKey1,
},
{
name: "test vector 1 chain m/0",
master: testVec1MasterPrivKey,
path: []uint32{0},
wantKey: "xprv9uHRZZhbkedL37eZEnyrNsQPFZYRAvjy5rt6M1nbEkLSo378x1CQQLo2xxBvREwiK6kqf7GRNvsNEchwibzXaV6i5GcsgyjBeRguXhKsi4R",
},
{
name: "test vector 1 chain m/0/1",
master: testVec1MasterPrivKey,
path: []uint32{0, 1},
wantKey: "xprv9ww7sMFLzJMzy7bV1qs7nGBxgKYrgcm3HcJvGb4yvNhT9vxXC7eX7WVULzCfxucFEn2TsVvJw25hH9d4mchywguGQCZvRgsiRaTY1HCqN8G",
},
{
name: "test vector 1 chain m/0/1/2",
master: testVec1MasterPrivKey,
path: []uint32{0, 1, 2},
wantKey: "xprv9xrdP7iD2L1YZCgR9AecDgpDMZSTzP5KCfUykGXgjBxLgp1VFHsEeL3conzGAkbc1MigG1o8YqmfEA2jtkPdf4vwMaGJC2YSDbBTPAjfRUi",
},
{
name: "test vector 1 chain m/0/1/2/2",
master: testVec1MasterPrivKey,
path: []uint32{0, 1, 2, 2},
wantKey: "xprvA2J8Hq4eiP7xCEBP7gzRJGJnd9CHTkEU6eTNMrZ6YR7H5boik8daFtDZxmJDfdMSKHwroCfAfsBKWWidRfBQjpegy6kzXSkQGGoMdWKz5Xh",
},
{
name: "test vector 1 chain m/0/1/2/2/1000000000",
master: testVec1MasterPrivKey,
path: []uint32{0, 1, 2, 2, 1000000000},
wantKey: "xprvA3XhazxncJqJsQcG85Gg61qwPQKiobAnWjuPpjKhExprZjfse6nErRwTMwGe6uGWXPSykZSTiYb2TXAm7Qhwj8KgRd2XaD21Styu6h6AwFz",
},
// Test vector 2
{
name: "test vector 2 chain m",
master: testVec2MasterPrivKey,
path: []uint32{},
wantKey: masterPrivKey2,
},
{
name: "test vector 2 chain m/0",
master: testVec2MasterPrivKey,
path: []uint32{0},
wantKey: "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt",
},
{
name: "test vector 2 chain m/0/2147483647",
master: testVec2MasterPrivKey,
path: []uint32{0, 2147483647},
wantKey: "xprv9wSp6B7cXJWXZRpDbxkFg3ry2fuSyUfvboJ5Yi6YNw7i1bXmq9QwQ7EwMpeG4cK2pnMqEx1cLYD7cSGSCtruGSXC6ZSVDHugMsZgbuY62m6",
},
{
name: "test vector 2 chain m/0/2147483647/1",
master: testVec2MasterPrivKey,
path: []uint32{0, 2147483647, 1},
wantKey: "xprv9ysS5br6UbWCRCJcggvpUNMyhVWgD7NypY9gsVTMYmuRtZg8izyYC5Ey4T931WgWbfJwRDwfVFqV3b29gqHDbuEpGcbzf16pdomk54NXkSm",
},
{
name: "test vector 2 chain m/0/2147483647/1/2147483646",
master: testVec2MasterPrivKey,
path: []uint32{0, 2147483647, 1, 2147483646},
wantKey: "xprvA2LfeWWwRCxh4iqigcDMnUf2E3nVUFkntc93nmUYBtb9rpSPYWa8MY3x9ZHSLZkg4G84UefrDruVK3FhMLSJsGtBx883iddHNuH1LNpRrEp",
},
{
name: "test vector 2 chain m/0/2147483647/1/2147483646/2",
master: testVec2MasterPrivKey,
path: []uint32{0, 2147483647, 1, 2147483646, 2},
wantKey: "xprvA48ALo8BDjcRET68R5RsPzF3H7WeyYYtHcyUeLRGBPHXu6CJSGjwW7dWoeUWTEzT7LG3qk6Eg6x2ZoqD8gtyEFZecpAyvchksfLyg3Zbqam",
},
// Custom tests to trigger specific conditions.
{
// Seed 000000000000000000000000000000da.
name: "Derived privkey with zero high byte m/0",
master: "xprv9s21ZrQH143K4FR6rNeqEK4EBhRgLjWLWhA3pw8iqgAKk82ypz58PXbrzU19opYcxw8JDJQF4id55PwTsN1Zv8Xt6SKvbr2KNU5y8jN8djz",
path: []uint32{0},
wantKey: "xprv9uC5JqtViMmgcAMUxcsBCBFA7oYCNs4bozPbyvLfddjHou4rMiGEHipz94xNaPb1e4f18TRoPXfiXx4C3cDAcADqxCSRSSWLvMBRWPctSN9",
},
}
}
// derive public keys from other public keys
getPublicChildDerivationTests := func() []testCase {
// The public extended keys for test vectors in [BIP32].
testVec1MasterPubKey := "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"
testVec2MasterPubKey := "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB"
return []testCase{
// Test vector 1
{
name: "test vector 1 chain m",
master: testVec1MasterPubKey,
path: []uint32{},
wantKey: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8",
},
{
name: "test vector 1 chain m/0",
master: testVec1MasterPubKey,
path: []uint32{0},
wantKey: "xpub68Gmy5EVb2BdFbj2LpWrk1M7obNuaPTpT5oh9QCCo5sRfqSHVYWex97WpDZzszdzHzxXDAzPLVSwybe4uPYkSk4G3gnrPqqkV9RyNzAcNJ1",
},
{
name: "test vector 1 chain m/0/1",
master: testVec1MasterPubKey,
path: []uint32{0, 1},
wantKey: "xpub6AvUGrnEpfvJBbfx7sQ89Q8hEMPM65UteqEX4yUbUiES2jHfjexmfJoxCGSwFMZiPBaKQT1RiKWrKfuDV4vpgVs4Xn8PpPTR2i79rwHd4Zr",
},
{
name: "test vector 1 chain m/0/1/2",
master: testVec1MasterPubKey,
path: []uint32{0, 1, 2},
wantKey: "xpub6BqyndF6rhZqmgktFCBcapkwubGxPqoAZtQaYewJHXVKZcLdnqBVC8N6f6FSHWUghjuTLeubWyQWfJdk2G3tGgvgj3qngo4vLTnnSjAZckv",
},
{
name: "test vector 1 chain m/0/1/2/2",
master: testVec1MasterPubKey,
path: []uint32{0, 1, 2, 2},
wantKey: "xpub6FHUhLbYYkgFQiFrDiXRfQFXBB2msCxKTsNyAExi6keFxQ8sHfwpogY3p3s1ePSpUqLNYks5T6a3JqpCGszt4kxbyq7tUoFP5c8KWyiDtPp",
},
{
name: "test vector 1 chain m/0/1/2/2/1000000000",
master: testVec1MasterPubKey,
path: []uint32{0, 1, 2, 2, 1000000000},
wantKey: "xpub6GX3zWVgSgPc5tgjE6ogT9nfwSADD3tdsxpzd7jJoJMqSY12Be6VQEFwDCp6wAQoZsH2iq5nNocHEaVDxBcobPrkZCjYW3QUmoDYzMFBDu9",
},
// Test vector 2
{
name: "test vector 2 chain m",
master: testVec2MasterPubKey,
path: []uint32{},
wantKey: "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB",
},
{
name: "test vector 2 chain m/0",
master: testVec2MasterPubKey,
path: []uint32{0},
wantKey: "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH",
},
{
name: "test vector 2 chain m/0/2147483647",
master: testVec2MasterPubKey,
path: []uint32{0, 2147483647},
wantKey: "xpub6ASAVgeWMg4pmutghzHG3BohahjwNwPmy2DgM6W9wGegtPrvNgjBwuZRD7hSDFhYfunq8vDgwG4ah1gVzZysgp3UsKz7VNjCnSUJJ5T4fdD",
},
{
name: "test vector 2 chain m/0/2147483647/1",
master: testVec2MasterPubKey,
path: []uint32{0, 2147483647, 1},
wantKey: "xpub6CrnV7NzJy4VdgP5niTpqWJiFXMAca6qBm5Hfsry77SQmN1HGYHnjsZSujoHzdxf7ZNK5UVrmDXFPiEW2ecwHGWMFGUxPC9ARipss9rXd4b",
},
{
name: "test vector 2 chain m/0/2147483647/1/2147483646",
master: testVec2MasterPubKey,
path: []uint32{0, 2147483647, 1, 2147483646},
wantKey: "xpub6FL2423qFaWzHCvBndkN9cbkn5cysiUeFq4eb9t9kE88jcmY63tNuLNRzpHPdAM4dUpLhZ7aUm2cJ5zF7KYonf4jAPfRqTMTRBNkQL3Tfta",
},
{
name: "test vector 2 chain m/0/2147483647/1/2147483646/2",
master: testVec2MasterPubKey,
path: []uint32{0, 2147483647, 1, 2147483646, 2},
wantKey: "xpub6H7WkJf547AiSwAbX6xsm8Bmq9M9P1Gjequ5SipsjipWmtXSyp4C3uwzewedGEgAMsDy4jEvNTWtxLyqqHY9C12gaBmgUdk2CGmwachwnWK",
},
}
}
runTests := func(tests []testCase) {
for i, test := range tests {
extKey, err := NewKeyFromString(test.master)
if err != nil {
t.Errorf("NewKeyFromString #%d (%s): unexpected error creating extended key: %v", i, test.name, err)
continue
}
extKey, err = extKey.Derive(test.path)
if err != nil {
t.Errorf("cannot derive child: %v", err)
continue
}
gotKey := extKey.String()
if gotKey != test.wantKey {
t.Errorf("Child #%d (%s): mismatched serialized extended key -- got: %s, want: %s", i, test.name, gotKey, test.wantKey)
continue
} else {
t.Logf("test %d (%s): %s", i, test.name, extKey.String())
}
}
}
runTests(getPrivateChildDerivationTests())
runTests(getPublicChildDerivationTests())
}
func TestErrors(t *testing.T) {
// Should get an error when seed has too few bytes.
_, err := NewMaster(bytes.Repeat([]byte{0x00}, 15))
if err != ErrInvalidSeedLen {
t.Errorf("NewMaster: mismatched error -- got: %v, want: %v",
err, ErrInvalidSeedLen)
}
// Should get an error when seed has too many bytes.
_, err = NewMaster(bytes.Repeat([]byte{0x00}, 65))
if err != ErrInvalidSeedLen {
t.Errorf("NewMaster: mismatched error -- got: %v, want: %v",
err, ErrInvalidSeedLen)
}
// Generate a new key and neuter it to a public extended key.
mnemonic := NewMnemonic()
phrase, err := mnemonic.MnemonicPhrase(128, EnglishLanguage)
if err != nil {
t.Errorf("Test failed: could not create seed: %s", err)
}
password := "badpassword"
extKey, err := NewMaster(mnemonic.MnemonicSeed(phrase, password))
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
pubKey, err := extKey.Neuter()
if err != nil {
t.Errorf("Neuter: unexpected error: %v", err)
return
}
// Deriving a hardened child extended key should fail from a public key.
_, err = pubKey.Child(HardenedKeyStart)
if err != ErrDerivingHardenedFromPublic {
t.Errorf("Child: mismatched error -- got: %v, want: %v", err, ErrDerivingHardenedFromPublic)
}
_, err = pubKey.BIP44Child(CoinTypeETH, 0)
if err != ErrInvalidMasterKey {
t.Errorf("BIP44Child: mistmatched error -- got: %v, want: %v", err, ErrInvalidMasterKey)
}
childKey, _ := extKey.Child(HardenedKeyStart + 1)
_, err = childKey.BIP44Child(CoinTypeETH, 0) // this should be called from master only
if err != ErrInvalidMasterKey {
t.Errorf("BIP44Child: mistmatched error -- got: %v, want: %v", err, ErrInvalidMasterKey)
}
// NewKeyFromString failure tests.
tests := []struct {
name string
key string
err error
neuter bool
neuterErr error
extKey *ExtendedKey
}{
{
name: "invalid key length",
key: "xpub1234",
err: ErrInvalidKeyLen,
},
{
name: "bad checksum",
key: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EBygr15",
err: ErrBadChecksum,
},
{
name: "pubkey not on curve",
key: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ1hr9Rwbk95YadvBkQXxzHBSngB8ndpW6QH7zhhsXZ2jHyZqPjk",
err: errors.New("invalid square root"),
},
{
name: "unsupported version",
key: "xbad4LfUL9eKmA66w2GJdVMqhvDmYGJpTGjWRAtjHqoUY17sGaymoMV9Cm3ocn9Ud6Hh2vLFVC7KSKCRVVrqc6dsEdsTjRV1WUmkK85YEUujAPX",
err: nil,
neuter: true,
neuterErr: chaincfg.ErrUnknownHDKeyID,
},
{
name: "zeroed extended key",
key: EmptyExtendedKeyString,
err: nil,
neuter: false,
neuterErr: nil,
extKey: &ExtendedKey{},
},
{
name: "empty string",
key: "",
err: nil,
neuter: false,
neuterErr: nil,
extKey: &ExtendedKey{},
},
}
for i, test := range tests {
t.Run(test.name, func(t *testing.T) {
extKey, err := NewKeyFromString(test.key)
if !reflect.DeepEqual(err, test.err) {
t.Errorf("NewKeyFromString #%d (%s): mismatched error -- got: %v, want: %v", i, test.name, err, test.err)
return
}
if test.neuter {
_, err := extKey.Neuter()
if !reflect.DeepEqual(err, test.neuterErr) {
t.Errorf("Neuter #%d (%s): mismatched error -- got: %v, want: %v", i, test.name, err, test.neuterErr)
return
}
}
if test.extKey != nil {
if !reflect.DeepEqual(extKey, test.extKey) {
t.Errorf("ExtKey #%d (%s): mismatched extended key -- got: %+v, want: %+v", i, test.name, extKey, test.extKey)
return
}
}
})
}
}
func TestMaxDepth(t *testing.T) {
mnemonic := NewMnemonic()
phrase, err := mnemonic.MnemonicPhrase(128, EnglishLanguage)
if err != nil {
t.Errorf("Test failed: could not create mnemonic phrase: %v", err)
}
lastParentKey, err := NewMaster(mnemonic.MnemonicSeed(phrase, "test-password"))
if err != nil {
t.Errorf("couldn't create master extended key: %v", err)
}
lastParentKey.Depth = 255
_, err = lastParentKey.Child(0)
if err != ErrMaxDepthExceeded {
t.Errorf("Expected ErrMaxDepthExceeded, got %+v", err)
}
}
func TestBIP44ChildDerivation(t *testing.T) {
keyString := masterPrivKey1
derivedKey1String := "xprvA38t8tFW4vbuB7WJXEqMFmZqRrcZUKWqqMcGjjKjr2hbfvPhRtLLJGL4ayWG8shF1VkuUikVGodGshLiKRS7WrdsrGSVDQCY33qoPBxG2Kp"
derivedKey2String := "xprvA38t8tFW4vbuDgBNpekPnuMSfpWziDLdF7W9Zd3mPy6eDEkM5F17vk59RtVoFbNdBBq84EJf5CqdZhhEoBkAM4DXHQsDqvUxVnncfnDQEFg"
extKey, err := NewKeyFromString(keyString)
if err != nil {
t.Error("NewKeyFromString: cannot create extended key")
}
accounKey1, err := extKey.BIP44Child(CoinTypeETH, 0)
if err != nil {
t.Error("Error dering BIP44-compliant key")
}
if accounKey1.String() != derivedKey1String {
t.Errorf("BIP44Child: key mismatch -- got: %v, want: %v", accounKey1.String(), derivedKey1String)
}
t.Logf("Account 1 key: %s", accounKey1.String())
accounKey2, err := extKey.BIP44Child(CoinTypeETH, 1)
if err != nil {
t.Error("Error dering BIP44-compliant key")
}
if accounKey2.String() != derivedKey2String {
t.Errorf("BIP44Child: key mismatch -- got: %v, want: %v", accounKey2.String(), derivedKey2String)
}
t.Logf("Account 1 key: %s", accounKey2.String())
}
func TestChildForPurpose(t *testing.T) {
masterKey, err := NewKeyFromString(masterPrivKey1)
if err != nil {
t.Error("NewKeyFromString: cannot create master extended key")
}
bip44Child, err := masterKey.EthBIP44Child(0)
if err != nil {
t.Error("Error deriving BIP44-compliant key")
}
eip1581Child, err := masterKey.EthEIP1581ChatChild(0)
if err != nil {
t.Error("Error deriving EIP1581-compliant key")
}
walletChild, err := masterKey.ChildForPurpose(KeyPurposeWallet, 0)
if err != nil {
t.Error("Error deriving BIP44-compliant key")
}
chatChild, err := masterKey.ChildForPurpose(KeyPurposeChat, 0)
if err != nil {
t.Error("Error deriving EIP1581-compliant key")
}
// Check that ChildForPurpose with KeyPurposeWallet generates a BIP44 key
if walletChild.String() != bip44Child.String() {
t.Errorf("wrong wallet key. expected to be equal to bip44Child")
}
// Check that ChildForPurpose with KeyPurposeChat generates a EIP1581 key
if chatChild.String() != eip1581Child.String() {
t.Errorf("wrong chat key. expected to be equal to eip1581Child")
}
// Check that the key generated by ChildForPurpose with KeyPurposeChat is different from the BIP44
if walletChild.String() == chatChild.String() {
t.Errorf("wrong chat key. expected to be different from the wallet key")
}
}
func TestHDWalletCompatibility(t *testing.T) {
password := "TREZOR"
mnemonic := NewMnemonic()
mnemonicPhrase := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
seed := mnemonic.MnemonicSeed(mnemonicPhrase, password)
rootKey, err := NewMaster(seed)
if err != nil {
t.Errorf("couldn't create master extended key: %v", err)
}
expectedAddresses := []struct {
address string
pubKey string
privKey string
}{
{
address: "0x9c32F71D4DB8Fb9e1A58B0a80dF79935e7256FA6",
pubKey: "0x03986dee3b8afe24cb8ccb2ac23dac3f8c43d22850d14b809b26d6b8aa5a1f4778",
privKey: "0x62f1d86b246c81bdd8f6c166d56896a4a5e1eddbcaebe06480e5c0bc74c28224",
},
{
address: "0x7AF7283bd1462C3b957e8FAc28Dc19cBbF2FAdfe",
pubKey: "0x03462e7b95dab24fe8a57ac897d9026545ec4327c9c5e4a772e5d14cc5422f9489",
privKey: "0x49ee230b1605382ac1c40079191bca937fc30e8c2fa845b7de27a96ffcc4ddbf",
},
{
address: "0x05f48E30fCb69ADcd2A591Ebc7123be8BE72D7a1",
pubKey: "0x036650e4b2b8e731a0ef12cda892b70cb95e78ea6e576ba995019b5e9aa7d9c0f5",
privKey: "0xeef2c0702151930b84cffcaa642af58e692956314519114e78f3211a6465f28b",
},
{
address: "0xbfE91Bc05cE66013660D7Eb742F74BD324DA5F92",
pubKey: "0x0201d1c12e8fcea03a68ad5fd0d02fd0a4bfe0339618f949e2e30cf311e8b83c46",
privKey: "0xbca51d1d3529a0e0787933a2293cf46d9b973ea3ea00e28d3bd33590bc7f7156",
},
}
for i := 0; i < len(expectedAddresses); i++ {
key, err := rootKey.BIP44Child(CoinTypeETH, uint32(i))
if err != nil {
t.Errorf("Error deriving BIP44-compliant key: %s", err)
}
privateKeyECDSA := key.ToECDSA()
address := crypto.PubkeyToAddress(privateKeyECDSA.PublicKey).Hex()
if address != expectedAddresses[i].address {
t.Errorf("wrong address generated. expected %s, got %s", expectedAddresses[i].address, address)
}
pubKey := fmt.Sprintf("0x%x", (crypto.CompressPubkey(&privateKeyECDSA.PublicKey)))
if pubKey != expectedAddresses[i].pubKey {
t.Errorf("wrong public key generated. expected %s, got %s", expectedAddresses[i].pubKey, pubKey)
}
privKey := fmt.Sprintf("0x%x", crypto.FromECDSA(privateKeyECDSA))
if privKey != expectedAddresses[i].privKey {
t.Errorf("wrong private key generated. expected %s, got %s", expectedAddresses[i].privKey, privKey)
}
}
}
// TestPrivateKeyDataWithLeadingZeros is a regression test that checks
// we don't re-introduce a bug we had in the past.
// For a specific mnemonic phrase, we were deriving a wrong key/address
// at path m/44'/60'/0'/0/0 compared to other wallets.
// In this specific case, the second child key is represented in 31 bytes.
// The problem raises when deriving its child key.
// One of the step to derive the child key is calling our splitHMAC
// that returns a secretKey and a chainCode.
// Inside this function we make a sha512 of a seed that is a 37 bytes with:
// 1 byte with 0x00
// 32 bytes for the key data
// 4 bytes for the child key index
// In our case, if the key was less then 32 bytes, it was shifted to the left of that 32 bytes space,
// resulting in a different seed, and a different data returned from the sha512 call.
// https://medium.com/@alexberegszaszi/why-do-my-bip32-wallets-disagree-6f3254cc5846#.86inuifuq
// https://github.com/iancoleman/bip39/issues/58
func TestPrivateKeyDataWithLeadingZeros(t *testing.T) {
mn := NewMnemonic()
words := "radar blur cabbage chef fix engine embark joy scheme fiction master release"
key, _ := NewMaster(mn.MnemonicSeed(words, ""))
path := []uint32{
HardenedKeyStart + 44, // purpose
HardenedKeyStart + 60, // cointype
HardenedKeyStart + 0, // account
0, // change
0, // index
}
for _, part := range path {
key, _ = key.Child(part)
if length := len(key.KeyData); length != 32 {
t.Errorf("expected key length to be 32, got: %d", length)
}
}
expectedAddress := "0xaC39b311DCEb2A4b2f5d8461c1cdaF756F4F7Ae9"
address := crypto.PubkeyToAddress(key.ToECDSA().PublicKey).Hex()
if address != expectedAddress {
t.Errorf("expected address %s, got: %s", expectedAddress, address)
}
}
//func TestNewKey(t *testing.T) {
// mnemonic := NewMnemonic()
//
// phrase, err := mnemonic.MnemonicPhrase(128, EnglishLanguage)
// if err != nil {
// t.Errorf("Test failed: could not create seed: %s", err)
// }
//
// password := "badpassword"
// mnemonic.salt = "Bitcoin seed"
// key, err := NewMaster(mnemonic.MnemonicSeed(phrase, password))
// if err != nil {
// t.Error(err)
// }
// t.Logf("%x", key.KeyData)
//}

270
mnemonic.go Normal file

File diff suppressed because one or more lines are too long

127
mnemonic_test.go Normal file
View File

@ -0,0 +1,127 @@
package extkeys
import (
"encoding/json"
"fmt"
"os"
"testing"
)
type VectorsFile struct {
Data map[string][][6]string
vectors []*Vector
}
type Vector struct {
language, password, input, mnemonic, seed, xprv string
}
func TestNewMnemonic(t *testing.T) {
m1 := NewMnemonic()
if m1.salt != defaultSalt {
t.Errorf("expected default salt, got: %q", m1.salt)
}
}
func TestMnemonic_WordList(t *testing.T) {
m := NewMnemonic()
_, err := m.WordList(EnglishLanguage)
if err != nil {
t.Errorf("expected WordList to return WordList without errors, got: %s", err)
}
indexes := []Language{-1, Language(len(m.wordLists))}
for _, index := range indexes {
_, err := m.WordList(index)
if err == nil {
t.Errorf("expected WordList to return an error with index %d", index)
}
}
}
// TestMnemonicPhrase
func TestMnemonicPhrase(t *testing.T) {
mnemonic := NewMnemonic()
// test strength validation
strengths := []EntropyStrength{127, 129, 257}
for _, s := range strengths {
_, err := mnemonic.MnemonicPhrase(s, EnglishLanguage)
if err != ErrInvalidEntropyStrength {
t.Errorf("Entropy strength '%d' should be invalid", s)
}
}
// test mnemonic generation
t.Log("Test mnemonic generation:")
for _, language := range mnemonic.AvailableLanguages() {
phrase, err := mnemonic.MnemonicPhrase(EntropyStrength128, language)
t.Logf("Mnemonic (%s): %s", Languages[language], phrase)
if err != nil {
t.Errorf("Test failed: could not create seed: %s", err)
}
if !mnemonic.ValidMnemonic(phrase, language) {
t.Error("Seed is not valid Mnenomic")
}
}
// run against test vectors
vectorsFile, err := LoadVectorsFile("mnemonic_vectors.json")
if err != nil {
t.Error(err)
}
t.Log("Test against pre-computed seed vectors:")
stats := map[string]int{}
for _, vector := range vectorsFile.vectors {
stats[vector.language]++
mnemonic := NewMnemonic()
seed := mnemonic.MnemonicSeed(vector.mnemonic, vector.password)
if fmt.Sprintf("%x", seed) != vector.seed {
t.Errorf("Test failed (%s): incorrect seed (%x) generated (expected: %s)", vector.language, seed, vector.seed)
return
}
//t.Logf("Test passed: correct seed (%x) generated (expected: %s)", seed, vector.seed)
rootKey, err := NewMaster(seed)
if err != nil {
t.Error(err)
}
if rootKey.String() != vector.xprv {
t.Errorf("Test failed (%s): incorrect xprv (%s) generated (expected: %s)", vector.language, rootKey, vector.xprv)
}
}
for language, count := range stats {
t.Logf("[%s]: %d tests completed", language, count)
}
}
func LoadVectorsFile(path string) (*VectorsFile, error) {
fp, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("Test failed: cannot open vectors file: %s", err)
}
var vectorsFile VectorsFile
if err := json.NewDecoder(fp).Decode(&vectorsFile); err != nil {
return nil, fmt.Errorf("Test failed: cannot parse vectors file: %s", err)
}
// load data into Vector structs
for language, data := range vectorsFile.Data {
for _, item := range data {
vectorsFile.vectors = append(vectorsFile.vectors, &Vector{language, item[0], item[1], item[2], item[3], item[4]})
}
}
return &vectorsFile, nil
}
func (v *Vector) String() string {
return fmt.Sprintf("{password: %s, input: %s, mnemonic: %s, seed: %s, xprv: %s}",
v.password, v.input, v.mnemonic, v.seed, v.xprv)
}

1364
mnemonic_vectors.json Normal file

File diff suppressed because it is too large Load Diff

52
utils.go Normal file
View File

@ -0,0 +1,52 @@
package extkeys
import (
"crypto/hmac"
"crypto/sha512"
"errors"
"math/big"
"github.com/btcsuite/btcd/btcec"
)
// errors
var (
ErrInvalidSecretKey = errors.New("generated secret key cannot be used")
)
func splitHMAC(seed, salt []byte) (secretKey, chainCode []byte, err error) {
data := hmac.New(sha512.New, salt)
if _, err = data.Write(seed); err != nil {
return
}
I := data.Sum(nil)
// Split I into two 32-byte sequences, IL and IR.
// IL = master secret key
// IR = master chain code
secretKey = I[:32]
chainCode = I[32:]
// IL (secretKey) is expected to be a 256-bit integer (it is used as parse256(IL)),
// and consequently that integer must be within range for SECP256k1 private key.
//
// There's tiny possibility (<1 in 2^127) this invariant is violated:
// error is returned in that case, and simple resolution is to request another child with i incremented.
keyBigInt := new(big.Int).SetBytes(secretKey)
if keyBigInt.Cmp(btcec.S256().N) >= 0 || keyBigInt.Sign() == 0 {
err = ErrInvalidSecretKey
}
return
}
// paddedAppend appends the src byte slice to dst, returning the new slice.
// If the length of the source is smaller than the passed size, leading zero
// bytes are appended to the dst slice before appending src.
// nolint: unparam
func paddedAppend(size uint, dst, src []byte) []byte {
for i := 0; i < int(size)-len(src); i++ {
dst = append(dst, 0)
}
return append(dst, src...)
}