Skip to content

Latest commit

 

History

History
935 lines (874 loc) · 55.5 KB

hdwallet.org

File metadata and controls

935 lines (874 loc) · 55.5 KB

BIP-32 Hierarchical deterministic wallet

Concepts and purpose

HD wallet

HD wallet
A hierarchical deterministic HD wallet deterministically derives an infinite hierarchy of private and public keys from a single 512 bits seed. The seed itself is derived from a mnemonic and an optional passphrase. Only the mnemonic with an optional passphrase is required to backup, export, and import the entire HD wallet with all derived keys. The key derivation process is deterministic, so the entire hierarchy of private and public keys can be re-derived from the same mnemonic with an optional passphrase at any time. The derived keys are organized in a tree hierarchy with an unlimited depth. Different branches of a tree can be used for selective sharing and different purposes e.g. different accounts, different cryptocurrencies, different accounting purposes.
Security of derived keys
While all private and public keys are derived from a single mnemonic with an optional passphrase, all derived keys are as secure as independently generated keys. To an external observer all derived keys look indistinguishable from independently generated keys. There is no feasible way to determine whether a key is a child or a sibling of another key. The main difference between a HD wallet that derives all keys from a single mnemonic with an optional passphrase and a non-deterministic wallet that generates each key independently is that only a single mnemonic with an optional passphrase is required to backup, export, and import a HD wallet, while every independently generated key must be backup as soon as it is generated by a non-deterministic wallet. Generating a new receiving address with a new spending private key for every transaction is recommended to preserve privacy of transactions on a blockchain
Derivation of public keys
The hierarchy of public keys to receive funds can be derived in a less secure environment e.g. web server from an extended public key without having access to the corresponding private extended key to generated respective private keys to spend received funds. Later, corresponding private keys can be derived in a more secure environment e.g. hardware wallet to spend received funds

Seed derivation

Seed derivation
A 512 bits (64 bytes) seed used to derive master extended private and public keys of a HD wallet is derived from a 128 to 256 bits entropy mnemonic with an optional passphrase. The seed derivation process uses the PBKDF2-SHA512 key derivation function with 2048 iterations of the HMAC-SHA512 message authentication code function. The mnemonic is used as a password and the passphrase is used as a salt for the PBKDF2-SHA512 key derivation function. The passphrase, if used, is an additional factor of security. A mnemonic is useless without a passphrase. A backup of a passphrase should be kept in a different place that a backup of a mnemonic. Different passphrases result in a completely different set of derived keys. The salt and the high number of HMAC-SHA512 iterations makes brute force attacks and table-based attacks much more difficult

Derivation of extended master keys

Derivation of extended master keys
The master extended private and public keys are used to derive all other keys in a HD wallet. The master extended private key is used to derive all private keys (and all public keys if necessary) in a HD wallet. The master extended public key can be used to derive only all public keys in a HD wallet. The master extended private and public keys are at the depth 0 and at the index 0. Extended private and public keys in general, and the master extended private and public keys in particular must be kept in secret to prevent others from generating child public keys to receive funds and child private keys to spend funds. A 512 bits seed derived from a mnemonic with an optional passphrase is used as data and a fixed “Bitcoin seed” string is used as a key to the HMAC-SHA512 message authentication code function to produce a 512 bits authenticated hash. The first 32 bytes of the authenticated hash are used as a master private key. The last (next) 32 bytes of the authenticated hash are used as a master chain code needed to derive both child private and child public keys. The master chain code prevents others from deriving child private and child public keys if they gained access to the corresponding parent private or parent public key. A master public key is derived from the master private key using the secp256k1 curve multiplication by multiplying the master private key by the generator point of the secp256k1 elliptic curve. A 64 bytes concatenation of the master private key with the master chain code is called the master extended private key. A 64 bytes concatenation of the master public key with the same master chain code is called the master extended public key

Derivation of extended private keys

Derivation of extended private keys
The extended private keys are identified in the tree hierarchy of a HD wallet by a depth that defines the parent-child positioning of keys, and by an index that defines the sibling positioning between derived keys. A child private key is derived from the parent extended private key, a depth, and an index of the child private key. The parent extended private key is split into a parent private key and a parent chain code. Then, a parent public key is derived from the parent private key using the secp256k1 multiplication. Next, the HMAC-SHA512 authenticated hash is computed using a concatenation of the compressed parent public key and the index of the child private key as data, and the parent chain code as a key. The first 32 bytes of the authenticated hash are used to derive a child private key by adding the parent private key to the first 32 bytes of the authenticated hash. The last (next) 32 bytes of the authenticated hash are used as a child chain code. A 64 bytes concatenation of the child private key with the child chain code is the extended private key. A child public key is derived from the child private key using the secp256k1 multiplication. A 64 bytes concatenation of the child public key with the child chain code is the extended public key
Leak extended parent public key and any child private key
An extended parent private key can be calculated from the leaked extended parent public key and any child private key. The extended parent private key is used to derive all child public keys to received funds (track ownership) and all child private keys to spend (steal) funds. The hardened derivation prevents deriving an extended parent private key from a leaked extended parent public key and a leaked child private key

Derivation of extended hardened keys

Derivation of extended hardened keys
While the extended parent and public keys are derived using the parent public compressed key, the hardened extended keys are derived using the parent private key prefixed with the 0x00 prefix. The hardened derivation is more secure as it is not possible to derive child public keys from a hardened extended parent public key. Only a hardened extended parent private key is used to derive hardened child private keys and hardened child public keys. The hardened derivation is more secure than the private derivation and should be used by default, unless there is the need to derive public keys without having access to the corresponding private keys, in which case the public derivation and the private derivation has to be used. While the extended parent and public keys use indices between 0 and 231 - 1, the hardened extended keys use indices between 231 and 232 - 1. A hardened child private key is derived from the parent extended private key, a depth, and an index of the hardened child private key starting from 231. The parent extended private key is split into a parent private key and a parent chain code. Next, the HMAC-SHA512 authenticated hash is computed using a concatenation of the parent private key prefixed with the 0x00 prefix and the index of the hardened child private key as data, and the parent chain code as a key. The first 32 bytes of the authenticated hash are used to derive a hardened child private key by adding the parent private key to the first 32 bytes of the authenticated hash. The last (next) 32 bytes of the authenticated hash are used as a child chain code. A 64 bytes concatenation of the hardened child private key with the child chain code is the hardened extended private key. A hardened child public key is derived from the hardened child private key using the secp256k1 multiplication. A 64 bytes concatenation of the hardened child public key with the child chain code is the hardened extended public key

Derivation of extended public keys

Derivation of extended public keys
A big advantage of a HD wallet key derivation mechanism is that extended public keys needed to receive funds can be derived in a less secure environment e.g. web server without the corresponding extended private keys needed to spend funds. An child public key is derived from the parent extended public key, a depth, and an index of the child public key. The parent extended public key is split into a compressed parent public key and a parent chain code. Next, the HMAC-SHA512 authenticated hash is computed using a concatenation of the compressed parent public key and the index of the child public key as data, and the parent chain code as a key. The first 32 bytes of the authenticated hash are used to derive a child public key by adding the parent public key to the first 32 bytes of the authenticated hash multiplied by the secp256k1 generator point. The last (next) 32 bytes of the authenticated hash are used as a child chain code. A 64 bytes concatenation of the child public key with the child chain code is the extended public key

Derivation of extended keys defined by HD path

HD path
A HD path uniquely identifies an extended private or public key in the hierarchy of derived keys of a HD wallet. A HD path starts from either a master private key m or a master public key M and indicates the derivation path by appending derivation segments in the form /index for the normal derivation or in the form /index' for hardened derivation. The indices for hardened derivation start from 231, so this number is automatically added to the index of a hardened key e.g. /0' means 0 + 231. Each segment in a HD path represents the next level in the hierarchy of keys and increments the depth by one
HD pathDescription
m/0'The first child private key of the master key (hardened derivation)
M/1The second child public key of the master key (normal derivation)

Encoding and decoding of extended keys

Encoding of extended keys
A 64 bytes concatenation of a 32 bytes private or public key with a 32 bytes chain code is called the extended private key or the extended public key respectively. Extended keys are difficult to manage by humans. A 128 bytes hex representation of a 64 bytes extended key is quite large. An encoded extended key includes in order a version indicating whether the key is a private key or a public key, a depth of the key from the master key, a hash of the compressed parent public key, an index of the key from a parent key, a chain code, a private key prefixed with the 0x00 prefix, or a compressed public key, and the error detecting checksum. A 82 bytes concatenation of the above fields in order is encoded using the base58 encoding. The base58 encoding of an extended key is less error prone and represents an extended key in a way that humans can manage more easily. Along with a prefixed private or a compressed public key and the corresponding chain code, an encoded extended key includes the following fields in order
FieldSizeDescription
Version4 bytesPrivate key version 0x0488ade4, encoded prefix xprv
Public key version 0x0488b21e, encoded prefix xpub
Depth1 byteDepth of a key from the master key
Parent4 bytesFirst 4 bytes of the hash of a compressed parent public key
Index4 bytesIndex of a key from the parent key
Chain Code32 bytesChain code
Key33 bytesPrivate key prefixed with the 0x00 prefix
Compressed public key
Checksum4 bytesFirst 4 bytes of the hash of all the above fields
Decoding of extended keys
The decoding of an encoded extended key converts a base58 encoded extended key to its components: a version, a depth, a hash of the compressed parent public key, an index, a chain code, a private key, or a compressed public key, and an error-detecting checksum. The error detecting checksum prevents mistype extended keys from being used by a wallet

Design and implementation

ExtKey type

ExtKey type
The ExtKey type represents either an extended private key or an extended public key. The extended key type embeds the PrvKey type, which, in turn, embeds the PubKey type. The extended key type is a super set of the private key and the public key types. Along with the private key and the public key types, the extended key type contains a 32 bytes HD chain code, a depth of an extended key from the master key, an index of an extended key from the parent key, a base58 encoded extended private key xprv, a base58 encoded extended public key xpub
type ExtKey struct {
  PrvKey
  Code []byte // A chain code 32 bytes
  Depth uint8 // A depth of an extended key from the master key
  Index uint32 // An index of an extended key from the parent key
  Xprv string // An encoded HD extended private key
  Xpub string // An encoded HD extended public key
}

func NewExtPrvKey(
  prvd, pubx, puby *big.Int, code []byte, depth uint8, index uint32,
) *ExtKey {
  prv := NewPrvKey(prvd, pubx, puby)
  return &ExtKey{PrvKey: *prv, Code: code, Depth: depth, Index: index}
}

func NewExtPubKey(
  pubx, puby *big.Int, code []byte, depth uint8, index uint32,
) *ExtKey {
  pub := NewPubKey(pubx, puby)
  prv := PrvKey{PubKey: *pub}
  return &ExtKey{PrvKey: prv, Code: code, Depth: depth, Index: index}
}
    

Seed derive

Seed derive
The SeedDerive function takes a mnemonic as a password and an optional passphrase as a salt and produces a 512 bits seed by applying the PBKDF2-SHA512 key derivation function to the data and the salt with 2048 iterations of the HMAC-SHA512 message authentication code function. The seed derive function
  • Create a salt by prepending a fixed “mnemonic” string to the passphrase
  • Produce a 512 bits seed by applying the PBKDF2-SHA512 key derivation function using the mnemonic as a password, the salt, and 2048 iterations of the HMAC-SHA512 message authentication code function
func SeedDerive(mnemonic, passphrase string) []byte {
  salt := []byte("mnemonic" + passphrase)
  seed := crypto.PBKDF2SHA512([]byte(mnemonic), salt, 2048, 64)
  return seed
}
    

Master derive

Master derive
The MasterDerive function takes a 512 bits seed and derives extended master private and public keys. The master derive function
  • Produces a 512 bits authenticated hash using the seed as data and a fixed “Bitcoin seed” string as a key
  • The first 32 bytes of the authenticated hash is the master private key
  • The master public key is derived from the master private key by using the secp256k1 multiplication
  • The last (next) 32 bytes of the authenticated hash is the master chain code
  • A concatenation of the master private key prefixed with the 0x00 prefix with the master chain code is the extended master private key xprv
  • A concatenation of the compressed master public key with the master chain code is the extended master public key xpub
    func MasterDerive(seed []byte) *ExtKey {
      depth, index := uint8(0), uint32(0)
      hmac := crypto.HMACSHA512(seed, []byte("Bitcoin seed"))
      prv, code := hmac[:32], hmac[32:]
      key := KeyDerive(prv)
      ekey := &ExtKey{PrvKey: *key, Code: code, Depth: depth, Index: index}
      ekey.Xprv = EkeyEncode(xprvVer, depth, nil, index, code, prv)
      ekey.Xpub = EkeyEncode(xpubVer, depth, nil, index, code, ekey.Pubc)
      return ekey
    }
            

Private derive

Private derive
The PrivateDerive function takes an extended private key, a depth of a child key from the master key, an index of a child key from the parent key, and produces child extended private and public keys. The private derive function
  • Split the parent extended private key into a 32 bytes parent private key and a 32 bytes parent chain code
  • Derive a parent public key from the parent private key by using the secp256k1 multiplication
  • Produce an authenticated hash by using the HMAC-SHA512 message authenticated code function with a concatenation of the compressed parent public key and the index of the child key as data, and the parent chain code as a key
  • Split the authenticated hash into a 32 bytes child private key base and a 32 bytes child chain code
  • Add the parent private key to the child private key base to derive the child private key
  • Derive a child public key from the child private key by using the secp256k1 multiplication
  • A concatenation of the child private key prefixed with the 0x00 prefix with the child chain code is the extended child private key xprv
  • A concatenation of the compressed child public key with the child chain code is the extended child public key xpub
func PrivateDerive(prve []byte, depth uint8, index uint32) *ExtKey {
  parPrv, parCode := prve[:32], prve[32:]
  parKey := KeyDerive(parPrv)
  idx := make([]byte, 4)
  binary.BigEndian.PutUint32(idx, index)
  var data bytes.Buffer
  data.Write(parKey.Pubc) // Parent public compressed
  data.Write(idx)
  hmac := crypto.HMACSHA512(data.Bytes(), parCode)
  prv, code := hmac[:32], hmac[32:]
  prvi := new(big.Int).SetBytes(prv)
  prvi.Add(prvi, new(big.Int).SetBytes(parPrv))
  prvi.Mod(prvi, ecc.P256k1().Params().N)
  key := KeyDerive(prvi.Bytes())
  ekey := &ExtKey{PrvKey: *key, Code: code, Depth: depth, Index: index}
  ekey.Xprv = EkeyEncode(xprvVer, depth, parKey.Pubc, index, code, ekey.Prv)
  ekey.Xpub = EkeyEncode(xpubVer, depth, parKey.Pubc, index, code, ekey.Pubc)
  return ekey
}
    

Hardened derive

Hardened derive
The HardenedDerive function takes an extended private key, a depth of a child key from the master key, an index of a child key from the parent key, and produces child hardened extended private and public keys. The hardened derive function
  • Split the parent extended private key into a 32 bytes parent private key and a 32 bytes parent chain code
  • Derive a parent public key from the parent private key by using the secp256k1 multiplication
  • Create a hardened index by adding 231 to the index
  • Produce an authenticated hash by using the HMAC-SHA512 message authenticated code function with a concatenation of the parent private key prefixed with the 0x00 prefix and the hardened index of the child key as data, and the parent chain code as a key
  • Split the authenticated hash into a 32 bytes hardened child private key base and a 32 bytes child chain code
  • Add the parent private key to the hardened child private key base to derive the hardened child private key
  • Derive a hardened child public key from the hardened child private key by using the secp256k1 multiplication
  • A concatenation of the hardened child private key prefixed with the 0x00 prefix with the child chain code is the hardened extended child private key xprv
  • A concatenation of the hardened compressed child public key with the child chain code is the hardened extended child public key xpub
func HardenedDerive(prve []byte, depth uint8, index uint32) *ExtKey {
  parPrv, parCode := prve[:32], prve[32:]
  parKey := KeyDerive(parPrv) // Only for xprv and xpub
  index += uint32(1 << 31) // Hardened key index
  idx := make([]byte, 4)
  binary.BigEndian.PutUint32(idx, index)
  var data bytes.Buffer
  data.WriteByte(0x00)
  data.Write(parPrv) // Parent private prefixed
  data.Write(idx)
  hmac := crypto.HMACSHA512(data.Bytes(), parCode)
  prv, code := hmac[:32], hmac[32:]
  prvi := new(big.Int).SetBytes(prv)
  prvi.Add(prvi, new(big.Int).SetBytes(parPrv))
  prvi.Mod(prvi, ecc.P256k1().Params().N)
  key := KeyDerive(prvi.Bytes())
  ekey := &ExtKey{PrvKey: *key, Code: code, Depth: depth, Index: index}
  ekey.Xprv = EkeyEncode(xprvVer, depth, parKey.Pubc, index, code, ekey.Prv)
  ekey.Xpub = EkeyEncode(xpubVer, depth, parKey.Pubc, index, code, ekey.Pubc)
  return ekey
}
    

Public derive

Public derive
The PublicDerive function takes an extended public key, a depth of a child key from the master key, an index of a child key from the parent key, and produces an extended child public key. The public derive function
  • Split the parent extended public key into a 32 bytes parent public key and a 32 bytes parent chain code
  • Produce an authenticated hash by using the HMAC-SHA512 message authenticated code function with a concatenation of the compressed parent public key and the index of the child key as data, and the parent chain code as a key
  • Split the authenticated hash into a 32 bytes child public key base and a 32 bytes child chain code
  • Calculate a point on the secp256k1 elliptic curve by multiplying the child public key base by the generator point
  • Add the uncompressed parent public key to the point on the secp256k1 elliptic curve to derive the child public key
  • A concatenation of the compressed child public key with the child chain code is the extended child public key xpub
    func PublicDerive(pube []byte, depth uint8, index uint32) *ExtKey {
      parPubc, parCode := pube[:33], pube[33:]
      idx := make([]byte, 4)
      binary.BigEndian.PutUint32(idx, index)
      var data bytes.Buffer
      data.Write(parPubc) // Parent public compressed
      data.Write(idx)
      hmac := crypto.HMACSHA512(data.Bytes(), parCode)
      pb, code := hmac[:32], hmac[32:]
      pub := new(ecdsa.PublicKey)
      pub.Curve = ecc.P256k1()
      pub.X, pub.Y = pub.ScalarBaseMult(pb)
      parX, parY := ecc.UnmarshalCompressed(ecc.P256k1(), parPubc)
      pubx, puby := pub.Add(pub.X, pub.Y, parX, parY)
      ekey := NewExtPubKey(pubx, puby, code, depth, index)
      ekey.Xpub = EkeyEncode(xpubVer, depth, parPubc, index, code, ekey.Pubc)
      return ekey
    }
            

Path derive

Path derive
The PathDerive function takes a mnemonic, an optional passphrase, and a HD path, and derives extended private and public keys as specified by the HD path e.g. m/0'/1. The path derive function derives a 512 bits seed from the mnemonic and the optional passphrase. Next, the extended master private and public keys are derived from the seed. Then, the HD path is parsed into the derivation segments starting from either the master private key m or the master public key M. Each derivation segment increases by one the depth of the child keys from the master key. Each derivation segment indicates the index of the child key from the parent key and whether the normal derivation or the hardened derivation marked with the prime ' symbol has to be applied. The path derive function process each segment in order starting from the extended master private or public key derived from the seed, which, in turn, is derived from the mnemonic and the optional passphrase. The extended keys derived of the current derivation segment are used as input to derive child extended keys of the next derivation segment
Private derivation
If a HD path starts from an extended private key m, then either the normal private derivation /index or the hardened private derivation /index' is possible. The normal private derivation uses the compressed parent public key, while the hardened derivation uses the parent private key prefixed with the 0x00 prefix. The input for both the normal private derivation and the hardened private derivation is the same: an extended parent private key, a depth of the child from the master key, and an index of the child private key
Public derivation
If a HD path starts from an extended public key M, only the normal public derivation /index is possible. The normal public derivation uses only the compressed parent public key. The input for the normal public derivation is an extended parent public key, a depth, and an index of the child public key

The path derive function

  • Reject an invalid private m or public M HD path
  • Derive a 512 bits seed from the mnemonic and the optional passphrase
  • Derive the extended private and public keys from the seed
  • For the private derivation
    • Parse the private derivation HD path. For each derivation segment
      • Increase by one the depth of a child key
      • Parse the index of a child key
      • Perform the hardened derivation if the derivation segment indicates the hardened derivation, otherwise perform the private derivation by passing the extended parent private key, the depth, and the index in both cases
      • Use the extended child private key as input to process the next derivation segment
      • Return the final extended private key
  • For the public derivation
    • Parse the public derivation HD path. For each derivation segment
      • Increase by one the depth of a child key
      • Parse the index of a child key
      • Perform the public derivation by passing the extended parent public key, the depth, and the index
      • Use the extended child public key as input to process the next derivation segment
      • Return the final extended public key
func PathDerive(mnemonic, passphrase, path string) (*ExtKey, error) {
  if !rePrvPath.MatchString(path) && !rePubPath.MatchString(path) {
    return nil, fmt.Errorf("path derive: invalid path: %s", path)
  }
  seed := SeedDerive(mnemonic, passphrase)
  ekey := MasterDerive(seed)
  depth := uint8(0)
  if strings.HasPrefix(path, "m") { // Private key derivation
    for _, seg := range rePrvSeg.FindAllStringSubmatch(path, -1) {
      depth++
      index, _ := strconv.ParseInt(seg[1], 10, 32)
      hardened := len(seg[2]) != 0
      prve := append(ekey.Prv, ekey.Code...)
      if hardened {
        ekey = HardenedDerive(prve, depth, uint32(index))
      } else {
        ekey = PrivateDerive(prve, depth, uint32(index))
      }
    }
  } else { // Public key derivation
    for _, seg := range rePubSeg.FindAllStringSubmatch(path, -1) {
      depth++
      index, _ := strconv.ParseInt(seg[1], 10, 32)
      pube := append(ekey.Pubc, ekey.Code...)
      ekey = PublicDerive(pube, depth, uint32(index))
    }
  }
  return ekey, nil
}
    

Extended key encode

Extended key encode
The EkeyEncode function takes a 4 bytes version indicating either a private or a public key has to be encoded, a depth of the key from the master key, a compressed parent public key, or a hash of a compressed parent key, or nil for a master key, an index of the key from the parent key, a chain code, a private key, or a compressed public key, appends an error detecting checksum, and base58 encodes the extended key including all the above fields. The extended key encode function
  • Write the 4 bytes version indicating whether a private or a public key has to be encoded
  • Write the 1 byte depth of the key from the master key
  • Write the first 4 bytes of the hash of the compressed parent public key. The parent hash is a combination of the SHA256 and the RIPEMD160 hash functions applied in this order to the compressed parent public key. The parent hash of a master key is 0x00000000
  • Write the 4 bytes index of the key from the parent key
  • Write either the 33 bytes private key prefixed with the 0x00 prefix or the 33 bytes compressed public key
  • Write the first 4 bytes of the error detecting checksum. The hash of the error detecting checksum is the double SHA256 hash of the above data
  • Base58 encode the 82 bytes resulting data including the checksum
  • Return the base58 encoded extended private key or the extended public key
func EkeyEncode(
  version []byte, depth uint8, parent []byte, index uint32, code, key []byte,
) string {
  var data bytes.Buffer
  data.Write(version)
  data.Write([]byte{depth})
  switch {
  case parent == nil: // Master key
    data.Write([]byte{0x00, 0x00, 0x00, 0x00})
  case len(parent) == 4: // Parent hash
    data.Write(parent)
  default: // Parent pubc
    hash := crypto.RIPEMD160(crypto.SHA256(parent))
    data.Write(hash[:4])
  }
  idx := make([]byte, 4)
  binary.BigEndian.PutUint32(idx, index)
  data.Write(idx)
  data.Write(code)
  if len(key) == 32 { // Private key
    data.Write([]byte{0x00})
  }
  data.Write(key)
  csum := crypto.SHA256(crypto.SHA256(data.Bytes()))
  data.Write(csum[:4])
  str := crypto.Base58Enc(data.Bytes())
  return str
}
    

Extended key decode

Extended key decode
The EkeyDecode function takes an encoded extended private or public key and decodes the key into the extended private or public key, the depth of the key from the master key, and the index of the key from the parent key. The extended key decode function checks the embedded error detecting checksum and rejects invalid keys. The extended key decode function
  • Base58 decode the encoded extended private or public key
  • Reject an invalid key if the embedded checksum does not match the computed checksum
  • Parse the 4 bytes version
  • Parse the 1 byte depth of the key from the master key
  • Parse the 4 bytes hash of the compressed parent public key
  • Parse the 4 bytes index of the key from the parent key
  • Parse the 32 bytes chain code
  • Parse either the 33 bytes private key prefixed with the 0x00 prefix, or the 33 bytes compressed public key
  • Return either the extended private key or the extended public key
func EkeyDecode(str string) (*ExtKey, error) {
  data, err := crypto.Base58Dec(str)
  if err != nil {
    return nil, err
  }
  csum := data[78:]
  hash := crypto.SHA256(crypto.SHA256(data[:78]))
  if !slices.Equal(hash[:4], csum) {
    return nil, fmt.Errorf("extended key decode: invalid checksum")
  }
  version := data[:4]
  depth := uint8(data[4])
  parent := data[5:9]
  index := binary.BigEndian.Uint32(data[9:13])
  code := data[13:45]
  if slices.Equal(version, xprvVer) { // Decode a private key
    prv := data[46:78]
    key := KeyDerive(prv)
    ekey := &ExtKey{PrvKey: *key, Code: code, Depth: depth, Index: index}
    ekey.Xprv = EkeyEncode(xprvVer, depth, parent, index, code, ekey.Prv)
    ekey.Xpub = EkeyEncode(xpubVer, depth, parent, index, code, ekey.Pubc)
    return ekey, nil
  } else { // Decode a public key
    pubc := data[45:78]
    pubx, puby := ecc.UnmarshalCompressed(ecc.P256k1(), pubc)
    ekey := NewExtPubKey(pubx, puby, code, depth, index)
    ekey.Xpub = EkeyEncode(xpubVer, depth, parent, index, code, ekey.Pubc)
    return ekey, nil
  }
}
    

Testing and usage

Testing all hd * CLI commands

go build -o wallet; ./hdwallet/cli-test.nu

Using hd seed and hd master private hardened public CLI commands

Show the help and usage instructions of the wallet hd command

./wallet hd
# NAME:
#    wallet hd - Derive extended master and child private and public keys

# USAGE:
#    wallet hd [command [command options]]

# COMMANDS:
#    seed      Derive a seed from a mnemonic and an optional passphrase
#                stdin: a mnemonic string
#                stdout: a seed in hex
#    master    Derive extended master private and public keys from a seed
#                stdin: a seed in hex
#                stdout: extended master private and public keys in hex in YAML
#    private   Derive extended private and public keys from an extended parent private key
#                stdin: an extended parent private key in hex
#                stdout: extended child private and public keys in hex in YAML
#    hardened  Derive hardened extended private and public keys from an extended parent private key
#                stdin: an extended parent private key in hex
#                stdout: hardened extended child private and public keys in hex in YAML
#    public    Derive an extended public key from an extended parent public key
#                stdin: an extended parent public key in hex
#                stdout: an extended child public key in hex in YAML
#    path      Derive extended private and public keys defined by a HD path
#                stdin: a mnemonic string
#                stdout: extended private and public keys in hex in YAML
#    decode    Decode a base58 encoded extended private or public key
#                stdin: a base58 encoded extended private or public key
#                stdout: an extended private or public key in hex in YAML

# OPTIONS:
#    --help, -h  show help

Generate a mnemonic with 128 bits of entropy. Derive a 512 bits seed from the mnemonic and an optional passphrase. Pass the seed to derive the extended master private and public keys

$env.PATH = $env.PATH | prepend ("." | path expand)
let mnem = wallet mnemonic generate --bits 128
print $mnem
# inspire fox supply idle museum quarter fade venture hammer setup illegal draft
let seed = $mnem | wallet hd seed --passphrase "secret"
print $seed
# 953de269b05b9a75ee4b0f4e73395f637b42a3a6df5874dc6ff161cc2e064698e03cfca0007705ad3902f66ddb053b03b580fa0cba9d103baa66cd0ea25651c7
let mst = $seed | wallet hd master | from yaml
print $mst
# ╭───────┬───────────────────────────────────────────────────────────────────────────────────╮
# │ prv   │ 85902f89bf0ab828b418e392eb2adcb3dcdf5d2ff3653b58447b474b17c962fb                  │
# │ pub   │ 04b9954badc8ad563863e8f69f0340f3a8d4e74eb83f93fd6f99d4aaae058b505791c3a0495717cbb │
# │       │ d84643e79571941711591ad05523ef802642c43e9ee5f8314                                 │
# │ pubc  │ 02b9954badc8ad563863e8f69f0340f3a8d4e74eb83f93fd6f99d4aaae058b5057                │
# │ code  │ e57eb516b3bf29e41be3fa99eb9eaa299d5cc43bae0bac52deeb58e1bc9868ec                  │
# │ depth │ 0                                                                                 │
# │ index │ 0                                                                                 │
# │ xprv  │ xprv9s21ZrQH143K4LswDQrMzrVaj6b5t48daRD3oV2Kk8bUvU1iAFga1gQzAzYA1mHQYyzwMKELusAEv │
# │       │ mVFq9fRwZx7U7WsF8cexRx5kgj4S3j                                                    │
# │ xpub  │ xpub661MyMwAqRbcGpxQKSPNMzSKH8RaHWrUwe8ebsRwJU8ToGLrhnzpZUjU2FxqEeWM4DQbXA461gbyi │
# │       │ w3XVaa4Yixw1kKqC2i4rDQtzeUtc2A                                                    │
# ╰───────┴───────────────────────────────────────────────────────────────────────────────────╯

Pass the extended master private key to the private derivation to derive the extended private and public keys of the depth 1 and the index 0

let prve = $mst.prv ++ $mst.code
let prv = $prve | wallet hd private --depth 1 --index 0 | from yaml
print $prv
# ╭───────┬───────────────────────────────────────────────────────────────────────────────────╮
# │ prv   │ b3d3066c86d78e2abb56906b4218f52faccf40dba298ce4eb8424ba49049549f                  │
# │ pub   │ 049075cfc92e3960e65d7b562f15484627c11765be4cc970138b1fe7614e78611597a400aff5b8368 │
# │       │ d897ff7b164639ec4d6c994ce20ec2488ddfb4ac610c52d8c                                 │
# │ pubc  │ 029075cfc92e3960e65d7b562f15484627c11765be4cc970138b1fe7614e786115                │
# │ code  │ d619c8594fd251953ecaf5427dcf5e5103f71f7ccd7f95c1efd396d103215220                  │
# │ depth │ 1                                                                                 │
# │ index │ 0                                                                                 │
# │ xprv  │ xprv9vewQDR7GSxHmbd17QusMZyKksczNVxLQCok1DcaLjnUvhS4xwibCKj6jVR2D6PEGUmDrkeGpZdf8 │
# │       │ wfEc6QcfcfQk1tUaUG4LBivUmttvv9                                                    │
# │ xpub  │ xpub69eHoix16pWaz5hUDSSsihv4JuTUmxgBmRjLoc2Bu5KToVmDWV2qk83aakBDKfAibMs9Jo1viGnXL │
# │       │ YvhZam76iKVz1RJc4k2ZPnp19A7qq7                                                    │
# ╰───────┴───────────────────────────────────────────────────────────────────────────────────╯

Pass the extended master private key to the hardened derivation to derive the hardened extended private and public keys of the depth 1 and the index 0. Note, that the keys from the private derivation and the keys from the hardened derivation are different. Also note, that the index of the hardened key starts from 231

let hrd = $prve | wallet hd hardened --depth 1 --index 0 | from yaml
print $hrd
# ╭───────┬───────────────────────────────────────────────────────────────────────────────────╮
# │ prv   │ cd9d4def320fd514ce41863f2974be5b9e899efa3a2b6ac87b07f56c9ceb3af4                  │
# │ pub   │ 0415b96c5f8897abffd58da78f2e53b1714f3b6de90c738db6d40c3b165917d799b97d4ccae3bf649 │
# │       │ 09869e55d7e43bfe886a9f3924a5a140308a3174fa4d0f154                                 │
# │ pubc  │ 0215b96c5f8897abffd58da78f2e53b1714f3b6de90c738db6d40c3b165917d799                │
# │ code  │ b509e6dc4aa193c5b6bd1b7d16fc0608d7ce8eab345091906b40a843efec7cc0                  │
# │ depth │ 1                                                                                 │
# │ index │ 2147483648                                                                        │
# │ xprv  │ xprv9vewQDRFc7VFwYpTerFSLbMKxXktsnTsvjGAfmdvjSMd8yiFNCdiNgvZKngY4bQbKG1YEDzoABMSY │
# │       │ 7BkLQt6tRzCWnDzLrApQTjVXuMXKaV                                                    │
# │ xpub  │ xpub69eHoix9SV3ZA2tvksnShjJ4WZbPHFBjHxBmUA3YHmtc1n3PujwxvVF3B2KKH3feV4vcZg1HNpTj3 │
# │       │ FgaMwuUM4srVNneo8D9boJr1VpsUBC                                                    │
# ╰───────┴───────────────────────────────────────────────────────────────────────────────────╯

Pass the extended master public key to the public derivation to derive the extended public key of the depth 1 and the index 0. Note, that the extended public key of the public derivation is equal to the extended public key of the private derivation

let pube = $mst.pubc ++ $mst.code
let pub = $pube | wallet hd public --depth 1 --index 0 | from yaml
print $pub
# ╭───────┬───────────────────────────────────────────────────────────────────────────────────╮
# │ pub   │ 049075cfc92e3960e65d7b562f15484627c11765be4cc970138b1fe7614e78611597a400aff5b8368 │
# │       │ d897ff7b164639ec4d6c994ce20ec2488ddfb4ac610c52d8c                                 │
# │ pubc  │ 029075cfc92e3960e65d7b562f15484627c11765be4cc970138b1fe7614e786115                │
# │ code  │ d619c8594fd251953ecaf5427dcf5e5103f71f7ccd7f95c1efd396d103215220                  │
# │ depth │ 1                                                                                 │
# │ index │ 0                                                                                 │
# │ xpub  │ xpub69eHoix16pWaz5hUDSSsihv4JuTUmxgBmRjLoc2Bu5KToVmDWV2qk83aakBDKfAibMs9Jo1viGnXL │
# │       │ YvhZam76iKVz1RJc4k2ZPnp19A7qq7                                                    │
# ╰───────┴───────────────────────────────────────────────────────────────────────────────────╯

Keccak256 hash a “transaction”. ECDSA sign the hash with the private key derived using the private derivation. Verify the signature using the public key derived using the public derivation. Confirm that that signature is valid, while the private signing key and the public verifying keys have been derived using different derivation paths: the private derivation and the public derivation

let hash = "transaction" | wallet keccak256
print $hash
# bb2a99297e1d12a9b91d4f90d5dd4b160d93c84a9e3b4daa916fec14ec852e05
let sig = $hash | wallet ecdsa sign --prv $prv.prv
print $sig
# 21cabdea3a37e5642b16e0e345446de3fcebf46829bf2d39e2611d34c38925ea1ce624c827614a7e20e7018142f9afcfed413fa8408920d5cfac2c5cb62edc3d00
let valid = $hash | wallet ecdsa verify --sig $sig --pub $pub.pubc | into bool
print $valid
# true

Using hd path CLI commands

Derive a mnemonic with 128 bits of entropy. Pass the mnemonic along with an optional passphrase and a HD path m/1/2/3 to derive the extended private and public keys using the private derivation

$env.PATH = $env.PATH | prepend ("." | path expand)
let mnem = wallet mnemonic generate --bits 128
print $mnem
# solar theory purchase swim memory craft obscure horse crumble casual mixture patch
let prv = $mnem | wallet hd path --passphrase "secret" --path "m/1/2/3"
  | from yaml
print $prv
# ╭───────┬───────────────────────────────────────────────────────────────────────────────────╮
# │ prv   │ 69670b963d137dd82513b022016e84398c9fa9df043876f0822c031c1a7eb4ae                  │
# │ pub   │ 04942054c003684a4327fe57383f8d8036775b779d4fa8f9b9197567a00cb3190e98b0450f9eaef09 │
# │       │ 3d704eacbf6b072bcc3255d4366193ade20728263705ff873                                 │
# │ pubc  │ 03942054c003684a4327fe57383f8d8036775b779d4fa8f9b9197567a00cb3190e                │
# │ code  │ 15ee7cca39efa21ea59466d237b83bde19472aa98105b62eafb7dbd424fdd19b                  │
# │ depth │ 3                                                                                 │
# │ index │ 3                                                                                 │
# │ xprv  │ xprv9ycZe59jJBCFwxrBqNkkndwH8pgnwrqogab13vDqYjifSbH59F2zWbYAkc6odmJNyBXrsptKXGUF8 │
# │       │ SRdqfFSmQNGtGpVcuQrauqw7Xc2bQc                                                    │
# │ xpub  │ xpub6Cbv3agd8YkZASvewQHm9mt1grXHMKZf3oWbrJdT75FeKPcDgnMF4PrebuQ8aH61yBm1TPfPrLwXa │
# │       │ dMQ6PFHg2uwdVhDwTu2eHKvHPTUbrj                                                    │
# ╰───────┴───────────────────────────────────────────────────────────────────────────────────╯

Pass the mnemonic along with the optional passphrase and a HD path m/1'/2/3 to derive the extended private and public keys using the hardened derivation for the first derivation segment and using the private derivation for the next two derivation segments. Note, that even a single hardened derivation derives completely different keys

let hrd = $mnem | wallet hd path --passphrase "secret" --path "m/1'/2/3"
  | from yaml
print $hrd
# ╭───────┬───────────────────────────────────────────────────────────────────────────────────╮
# │ prv   │ af2f45c6183270c9bf9d4f8a6b4c9869cad3698aaa089614790b6159735a1565                  │
# │ pub   │ 043b5b3b087fa7c829e9655a7958c419979c1b9509eee48ee8e8f40d05b22b96b7f3f5367703b3954 │
# │       │ 2599b14ac6cda26670ada1cc3bca81cb70db1d0dd32fe5378                                 │
# │ pubc  │ 023b5b3b087fa7c829e9655a7958c419979c1b9509eee48ee8e8f40d05b22b96b7                │
# │ code  │ 279fcf8e47a09ae23223b12edadcc951913377c928c2fef5dd2006ba12e3187d                  │
# │ depth │ 3                                                                                 │
# │ index │ 3                                                                                 │
# │ xprv  │ xprv9y3CbGsDkQLEH554QxZ9gA3C3bE2whPB8wHeooMpKDL1UD1eZP54FmEFR1z9gKTpQz9Yxcn3w8eWv │
# │       │ jQCKshCmW4Y8ejRxhopi2Lfq8S2mBw                                                    │
# │ xpub  │ xpub6C2YznQ7amtXVZ9XWz6A3Hyvbd4XMA72WADFcBmRsYrzM1Lo6vPJoZYjGG8uSd76XoTdZRm5UHWSg │
# │       │ JdjQYZmDwFPUgzrGHCbwwQ3xz4QPU1                                                    │
# ╰───────┴───────────────────────────────────────────────────────────────────────────────────╯

Pass the mnemonic along with the optional passphrase and a HD path M/1/2/3 to derive the extended public key using the public derivation. Note, that the extended public key of the public derivation is equal to the extended public key of the private derivation, as the HD path is the same, however derivation mechanisms are different

let pub = $mnem | wallet hd path --passphrase "secret" --path "M/1/2/3"
  | from yaml
print $pub
# ╭───────┬───────────────────────────────────────────────────────────────────────────────────╮
# │ pub   │ 04942054c003684a4327fe57383f8d8036775b779d4fa8f9b9197567a00cb3190e98b0450f9eaef09 │
# │       │ 3d704eacbf6b072bcc3255d4366193ade20728263705ff873                                 │
# │ pubc  │ 03942054c003684a4327fe57383f8d8036775b779d4fa8f9b9197567a00cb3190e                │
# │ code  │ 15ee7cca39efa21ea59466d237b83bde19472aa98105b62eafb7dbd424fdd19b                  │
# │ depth │ 3                                                                                 │
# │ index │ 3                                                                                 │
# │ xpub  │ xpub6Cbv3agd8YkZASvewQHm9mt1grXHMKZf3oWbrJdT75FeKPcDgnMF4PrebuQ8aH61yBm1TPfPrLwXa │
# │       │ dMQ6PFHg2uwdVhDwTu2eHKvHPTUbrj                                                    │
# ╰───────┴───────────────────────────────────────────────────────────────────────────────────╯

Using hd decode CLI commands

Derive a mnemonic with 128 bits of entropy. Pass the mnemonic along with an optional passphrase and a HD path m/1/2/3 to derive the extended private and public keys using the private derivation

$env.PATH = $env.PATH | prepend ("." | path expand)
let mnem = wallet mnemonic generate --bits 128
print $mnem
# area boring more trick public exist spray flame junk height denial fade
let key = $mnem | wallet hd path --passphrase "secret" --path "m/1/2/3"
  | from yaml
print $key
# ╭───────┬───────────────────────────────────────────────────────────────────────────────────╮
# │ prv   │ 56b69966b1198121cccb9e6cdafeea6e72b98944b3c77dd28cb9a0dfd4e17246                  │
# │ pub   │ 046174a7849c1386ab1593e427939fb21ad18efe09b667b86250115b586e732b7e5bfab6721291002 │
# │       │ e3209a68aa659250d5dcfe6c6101f8085dae8f8e8b588b315                                 │
# │ pubc  │ 036174a7849c1386ab1593e427939fb21ad18efe09b667b86250115b586e732b7e                │
# │ code  │ 6719d551409c263206ec17bef32babc88dc97e7b0bba5c484cee2c5a7fa722ee                  │
# │ depth │ 3                                                                                 │
# │ index │ 3                                                                                 │
# │ xprv  │ xprv9yzG3jBuhbgwLHF4ybkwjngykRSC7bgW934mQ3FUiho5P4AW6Bcs1oxFnfijomRjPvpRrjNFisUXo │
# │       │ 4x3aek1pipoE6FjFSkABaUcPeDJkuA                                                    │
# │ xpub  │ xpub6CycTEioXyFEYmKY5dHx6vdiJTGgX4QMWFzNCRf6H3L4FrVediw7ZcGjdxmyprQoyVdcAn3Dj1wBT │
# │       │ akA49K368r9xdrceaoWevPgD1ih33Z                                                    │
# ╰───────┴───────────────────────────────────────────────────────────────────────────────────╯

Decode the encoded private key xprv of the extended private key. Note, that the decoded private key is equal to the extended private key

let prv = $key.xprv | wallet hd decode | from yaml
print $prv
# ╭───────┬───────────────────────────────────────────────────────────────────────────────────╮
# │ prv   │ 56b69966b1198121cccb9e6cdafeea6e72b98944b3c77dd28cb9a0dfd4e17246                  │
# │ pub   │ 046174a7849c1386ab1593e427939fb21ad18efe09b667b86250115b586e732b7e5bfab6721291002 │
# │       │ e3209a68aa659250d5dcfe6c6101f8085dae8f8e8b588b315                                 │
# │ pubc  │ 036174a7849c1386ab1593e427939fb21ad18efe09b667b86250115b586e732b7e                │
# │ code  │ 6719d551409c263206ec17bef32babc88dc97e7b0bba5c484cee2c5a7fa722ee                  │
# │ depth │ 3                                                                                 │
# │ index │ 3                                                                                 │
# │ xprv  │ xprv9yzG3jBuhbgwLHF4ybkwjngykRSC7bgW934mQ3FUiho5P4AW6Bcs1oxFnfijomRjPvpRrjNFisUXo │
# │       │ 4x3aek1pipoE6FjFSkABaUcPeDJkuA                                                    │
# │ xpub  │ xpub6CycTEioXyFEYmKY5dHx6vdiJTGgX4QMWFzNCRf6H3L4FrVediw7ZcGjdxmyprQoyVdcAn3Dj1wBT │
# │       │ akA49K368r9xdrceaoWevPgD1ih33Z                                                    │
# ╰───────┴───────────────────────────────────────────────────────────────────────────────────╯

Decode the encoded public key xpub of the extended public key. Note, that the decoded public key is equal to the extended public key

let pub = $key.xpub | wallet hd decode | from yaml
print $pub
# ╭───────┬───────────────────────────────────────────────────────────────────────────────────╮
# │ pub   │ 046174a7849c1386ab1593e427939fb21ad18efe09b667b86250115b586e732b7e5bfab6721291002 │
# │       │ e3209a68aa659250d5dcfe6c6101f8085dae8f8e8b588b315                                 │
# │ pubc  │ 036174a7849c1386ab1593e427939fb21ad18efe09b667b86250115b586e732b7e                │
# │ code  │ 6719d551409c263206ec17bef32babc88dc97e7b0bba5c484cee2c5a7fa722ee                  │
# │ depth │ 3                                                                                 │
# │ index │ 3                                                                                 │
# │ xpub  │ xpub6CycTEioXyFEYmKY5dHx6vdiJTGgX4QMWFzNCRf6H3L4FrVediw7ZcGjdxmyprQoyVdcAn3Dj1wBT │
# │       │ akA49K368r9xdrceaoWevPgD1ih33Z                                                    │
# ╰───────┴───────────────────────────────────────────────────────────────────────────────────╯