diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 88abd3b..a3b875e 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -4,14 +4,14 @@ name: build-check on: [push, pull_request] jobs: build-check: - name: build-check Go ${{ matrix.go }} - runs-on: ubuntu-20.04 + name: Go Test + runs-on: ubuntu-latest strategy: matrix: go: + - '1.17' + - '1.16' - '1.15' - - '1.14' - - '1.13' steps: - uses: actions/checkout@v2 - uses: actions/setup-go@v1 @@ -19,8 +19,8 @@ jobs: go-version: ${{ matrix.go }} - run: make build_check test-coverage: - name: build-check Go ${{ matrix.go }} - runs-on: ubuntu-20.04 + name: Go Test With Code Coverage + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-go@v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d83068 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +coverage.out diff --git a/README.md b/README.md index 6ba4f80..5c7e315 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,15 @@ [![Build check](https://github.com/tyler-smith/go-bip32/workflows/build-check/badge.svg?branch=master)](https://github.com/tyler-smith/go-bip32/actions?query=workflow%3Abuild-check+branch%3Amaster) [![Go Report Card](https://goreportcard.com/badge/github.com/tyler-smith/go-bip32)](https://goreportcard.com/report/github.com/tyler-smith/go-bip32) -[![Coverage Status](https://coveralls.io/repos/github/tyler-smith/go-bip32/badge.svg?branch=TS_v2)](https://coveralls.io/github/tyler-smith/go-bip32?branch=TS_v2) +[![Coverage Status](https://coveralls.io/repos/github/tyler-smith/go-bip32/badge.svg)](https://coveralls.io/github/tyler-smith/go-bip32) An implementation of the BIP32 spec for Hierarchical Deterministic Bitcoin addresses as a simple Go library. The semantics of derived keys are up to the user. [BIP43](https://github.com/bitcoin/bips/blob/master/bip-0043.mediawiki) and [BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) are good schemes to implement with this library. An additional library for either or both of those on top of this library should be developed. +The library supports "Bitcoin" and "ed25519" curves for [Universal private key derivation from master private key](https://github.com/satoshilabs/slips/blob/master/slip-0010.md). + ## Example -It's very unlikely, but possible, that a given index does not produce a valid +It's very unlikely, but possible, that a given index does not produce a valid private key. Error checking is skipped in this example for brevity but should be handled in real code. In such a case, a ErrInvalidPrivateKey is returned. ErrInvalidPrivateKey should be handled by trying the next index for a child key. diff --git a/bip32.go b/bip32.go index 8ca4121..0647111 100644 --- a/bip32.go +++ b/bip32.go @@ -2,9 +2,7 @@ package bip32 import ( "bytes" - "crypto/hmac" "crypto/rand" - "crypto/sha512" "encoding/hex" "errors" ) @@ -42,8 +40,28 @@ var ( // ErrInvalidPublicKey is returned when a derived public key is invalid ErrInvalidPublicKey = errors.New("Invalid public key") + + // ErrUnsupportedEd25519PublicKeyDerivation is returned when a public child key is derived with ed25519 curve. + ErrUnsupportedEd25519PublicKeyDerivation = errors.New("Public key for ed25519 is not supported for normal derivation") ) +// The supported curves. +// @link SupportedCurves +const ( + Bitcoin Curve = iota // secp256k1 + Ed25519 +) + +// curvesSalt corresponds to the supported curves @SupportedCurves by the index. +var curvesSalt = [][]byte{ + []byte("Bitcoin seed"), + []byte("ed25519 seed"), +} + +// Curve defines the private key for the HMAC hash that generates the master key. +// https://github.com/satoshilabs/slips/blob/master/slip-0010.md#master-key-generation +type Curve byte + // Key represents a bip32 extended key type Key struct { Key []byte // 33 bytes @@ -51,19 +69,25 @@ type Key struct { ChildNumber []byte // 4 bytes FingerPrint []byte // 4 bytes ChainCode []byte // 32 bytes - Depth byte // 1 bytes + Depth byte // 1 byte IsPrivate bool // unserialized + + // The Deserialize function sets it bip32.Bitcoin by default. + // see https://github.com/satoshilabs/slips/blob/master/slip-0132.md#registered-hd-version-bytes + curve Curve } -// NewMasterKey creates a new master extended key from a seed -func NewMasterKey(seed []byte) (*Key, error) { +// NewMasterKeyWithCurve creates a new master extended key from a seed +// with a given curve algorithm. +func NewMasterKeyWithCurve(seed []byte, curve Curve) (*Key, error) { + if int(curve) > len(curvesSalt) { + panic("unsupported curve, only bip32.Bitcoin and bit32.Ed25519 are supported") + } // Generate key and chaincode - hmac := hmac.New(sha512.New, []byte("Bitcoin seed")) - _, err := hmac.Write(seed) + intermediary, err := hmac512(seed, curvesSalt[curve]) if err != nil { return nil, err } - intermediary := hmac.Sum(nil) // Split it into our key and chain code keyBytes := intermediary[:32] @@ -84,11 +108,18 @@ func NewMasterKey(seed []byte) (*Key, error) { ChildNumber: []byte{0x00, 0x00, 0x00, 0x00}, FingerPrint: []byte{0x00, 0x00, 0x00, 0x00}, IsPrivate: true, + curve: curve, } return key, nil } +// NewMasterKey creates a new master extended key from a seed +// using the Bitcoin curve. +func NewMasterKey(seed []byte) (*Key, error) { + return NewMasterKeyWithCurve(seed, Bitcoin) +} + // NewChildKey derives a child key from a given parent as outlined by bip32 func (key *Key) NewChildKey(childIdx uint32) (*Key, error) { // Fail early if trying to create hardned child from public key @@ -96,6 +127,16 @@ func (key *Key) NewChildKey(childIdx uint32) (*Key, error) { return nil, ErrHardnedChildPublicKey } + if key.curve == Ed25519 { + if !key.IsPrivate { + return nil, ErrUnsupportedEd25519PublicKeyDerivation + } + // With ed25519 curve all derivation-path indexes will be promoted to hardened indexes. + if childIdx < FirstHardenedChild { + childIdx += FirstHardenedChild + } + } + intermediary, err := key.getIntermediary(childIdx) if err != nil { return nil, err @@ -107,26 +148,37 @@ func (key *Key) NewChildKey(childIdx uint32) (*Key, error) { ChainCode: intermediary[32:], Depth: key.Depth + 1, IsPrivate: key.IsPrivate, + curve: key.curve, } // Bip32 CKDpriv if key.IsPrivate { childKey.Version = PrivateWalletVersion - fingerprint, err := hash160(publicKeyForPrivateKey(key.Key)) - if err != nil { - return nil, err + + var publicKey []byte + + // https://github.com/satoshilabs/slips/blob/master/slip-0010.md#private-parent-key--private-child-key + if childKey.curve == Ed25519 { + childKey.Key = intermediary[:32] + publicKey = publicKeyForPrivateKeyEd25519(key.Key) + } else { + childKey.Key = addPrivateKeys(intermediary[:32], key.Key) + // Validate key + if err := validatePrivateKey(childKey.Key); err != nil { + return nil, err + } + publicKey = publicKeyForPrivateKeyBitcoin(key.Key) } - childKey.FingerPrint = fingerprint[:4] - childKey.Key = addPrivateKeys(intermediary[:32], key.Key) - // Validate key - err = validatePrivateKey(childKey.Key) + fingerprint, err := hash160(publicKey) if err != nil { return nil, err } + childKey.FingerPrint = fingerprint[:4] + // Bip32 CKDpub } else { - keyBytes := publicKeyForPrivateKey(intermediary[:32]) + keyBytes := publicKeyForPrivateKeyBitcoin(intermediary[:32]) // Validate key err := validateChildPublicKey(keyBytes) @@ -157,19 +209,14 @@ func (key *Key) getIntermediary(childIdx uint32) ([]byte, error) { data = append([]byte{0x0}, key.Key...) } else { if key.IsPrivate { - data = publicKeyForPrivateKey(key.Key) + data = publicKeyForPrivateKeyBitcoin(key.Key) } else { data = key.Key } } data = append(data, childIndexBytes...) - hmac := hmac.New(sha512.New, key.ChainCode) - _, err := hmac.Write(data) - if err != nil { - return nil, err - } - return hmac.Sum(nil), nil + return hmac512(data, key.ChainCode) } // PublicKey returns the public version of key or return a copy @@ -178,7 +225,11 @@ func (key *Key) PublicKey() *Key { keyBytes := key.Key if key.IsPrivate { - keyBytes = publicKeyForPrivateKey(keyBytes) + if key.curve == Ed25519 { + keyBytes = publicKeyForPrivateKeyEd25519(keyBytes) + } else { + keyBytes = publicKeyForPrivateKeyBitcoin(keyBytes) + } } return &Key{ @@ -189,6 +240,7 @@ func (key *Key) PublicKey() *Key { FingerPrint: key.FingerPrint, ChainCode: key.ChainCode, IsPrivate: false, + curve: key.curve, } } diff --git a/bip32_test.go b/bip32_test.go index b93cda7..207091e 100644 --- a/bip32_test.go +++ b/bip32_test.go @@ -2,9 +2,11 @@ package bip32 import ( "encoding/hex" + "fmt" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type testMasterKey struct { @@ -21,7 +23,7 @@ type testChildKey struct { hexPubKey string } -func TestBip32TestVectors(t *testing.T) { +func TestBip32TestVectors_Bitcoin(t *testing.T) { hStart := FirstHardenedChild vector1 := testMasterKey{ @@ -103,17 +105,19 @@ func TestBip32TestVectors(t *testing.T) { }, } - testVectorKeyPairs(t, vector1) - testVectorKeyPairs(t, vector2) - testVectorKeyPairs(t, vector3) + testVectorKeyPairs(t, vector1, Bitcoin) + testVectorKeyPairs(t, vector2, Bitcoin) + testVectorKeyPairs(t, vector3, Bitcoin) } -func testVectorKeyPairs(t *testing.T, vector testMasterKey) { +func testVectorKeyPairs(t *testing.T, vector testMasterKey, curve Curve) { + t.Helper() + // Decode master seed into hex seed, _ := hex.DecodeString(vector.seed) // Generate a master private and public key - privKey, err := NewMasterKey(seed) + privKey, err := NewMasterKeyWithCurve(seed, curve) assert.NoError(t, err) pubKey := privKey.PublicKey() @@ -140,8 +144,188 @@ func testVectorKeyPairs(t *testing.T, vector testMasterKey) { } } +type testKeyHex struct { + fingerPrint string + chainCode string + privateKey string + publicKey string +} + +func (tk *testKeyHex) Assert(t *testing.T, key *Key) { + t.Helper() + + fingerPrint, _ := hex.DecodeString(tk.fingerPrint) + assert.Equal(t, fingerPrint, key.FingerPrint, "finger print") + + chainCode, _ := hex.DecodeString(tk.chainCode) + assert.Equal(t, chainCode, key.ChainCode, "chain code") + + privateKey, _ := hex.DecodeString(tk.privateKey) + assert.Equal(t, privateKey, key.Key, "private key") + + publicKey, _ := hex.DecodeString(tk.publicKey) + assert.Equal(t, publicKey, key.PublicKey().Key, "public key") +} + +type testVector struct { + path uint32 + key testKeyHex + child *testVector +} + +func TestBip32TestVectors_Ed25519(t *testing.T) { + cases := []struct { + seed string + vector testVector + }{ + // https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vector-1-for-ed25519 + { + seed: "000102030405060708090a0b0c0d0e0f", + vector: testVector{ + key: testKeyHex{ + fingerPrint: "00000000", + chainCode: "90046a93de5380a72b5e45010748567d5ea02bbf6522f979e05c0d8d8ca9fffb", + privateKey: "2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7", + publicKey: "00a4b2856bfec510abab89753fac1ac0e1112364e7d250545963f135f2a33188ed", + }, + child: &testVector{ + path: 0, + key: testKeyHex{ + fingerPrint: "ddebc675", + chainCode: "8b59aa11380b624e81507a27fedda59fea6d0b779a778918a2fd3590e16e9c69", + privateKey: "68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3", + publicKey: "008c8a13df77a28f3445213a0f432fde644acaa215fc72dcdf300d5efaa85d350c", + }, + child: &testVector{ + path: 1, + key: testKeyHex{ + fingerPrint: "13dab143", + chainCode: "a320425f77d1b5c2505a6b1b27382b37368ee640e3557c315416801243552f14", + privateKey: "b1d0bad404bf35da785a64ca1ac54b2617211d2777696fbffaf208f746ae84f2", + publicKey: "001932a5270f335bed617d5b935c80aedb1a35bd9fc1e31acafd5372c30f5c1187", + }, + child: &testVector{ + path: 2, + key: testKeyHex{ + fingerPrint: "ebe4cb29", + chainCode: "2e69929e00b5ab250f49c3fb1c12f252de4fed2c1db88387094a0f8c4c9ccd6c", + privateKey: "92a5b23c0b8a99e37d07df3fb9966917f5d06e02ddbd909c7e184371463e9fc9", + publicKey: "00ae98736566d30ed0e9d2f4486a64bc95740d89c7db33f52121f8ea8f76ff0fc1", + }, + child: &testVector{ + path: 2, + key: testKeyHex{ + fingerPrint: "316ec1c6", + chainCode: "8f6d87f93d750e0efccda017d662a1b31a266e4a6f5993b15f5c1f07f74dd5cc", + privateKey: "30d1dc7e5fc04c31219ab25a27ae00b50f6fd66622f6e9c913253d6511d1e662", + publicKey: "008abae2d66361c879b900d204ad2cc4984fa2aa344dd7ddc46007329ac76c429c", + }, + child: &testVector{ + path: 1000000000, + key: testKeyHex{ + fingerPrint: "d6322ccd", + chainCode: "68789923a0cac2cd5a29172a475fe9e0fb14cd6adb5ad98a3fa70333e7afa230", + privateKey: "8f94d394a8e8fd6b1bc2f3f49f5c47e385281d5c17e65324b0f62483e37e8793", + publicKey: "003c24da049451555d51a7014a37337aa4e12d41e485abccfa46b47dfb2af54b7a", + }, + }, + }, + }, + }, + }, + }, + }, + // https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vector-2-for-ed25519 + { + seed: "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542", + vector: testVector{ + key: testKeyHex{ + fingerPrint: "00000000", + chainCode: "ef70a74db9c3a5af931b5fe73ed8e1a53464133654fd55e7a66f8570b8e33c3b", + privateKey: "171cb88b1b3c1db25add599712e36245d75bc65a1a5c9e18d76f9f2b1eab4012", + publicKey: "008fe9693f8fa62a4305a140b9764c5ee01e455963744fe18204b4fb948249308a", + }, + child: &testVector{ + path: 0, + key: testKeyHex{ + fingerPrint: "31981b50", + chainCode: "0b78a3226f915c082bf118f83618a618ab6dec793752624cbeb622acb562862d", + privateKey: "1559eb2bbec5790b0c65d8693e4d0875b1747f4970ae8b650486ed7470845635", + publicKey: "0086fab68dcb57aa196c77c5f264f215a112c22a912c10d123b0d03c3c28ef1037", + }, + child: &testVector{ + path: 2147483647, + key: testKeyHex{ + fingerPrint: "1e9411b1", + chainCode: "138f0b2551bcafeca6ff2aa88ba8ed0ed8de070841f0c4ef0165df8181eaad7f", + privateKey: "ea4f5bfe8694d8bb74b7b59404632fd5968b774ed545e810de9c32a4fb4192f4", + publicKey: "005ba3b9ac6e90e83effcd25ac4e58a1365a9e35a3d3ae5eb07b9e4d90bcf7506d", + }, + child: &testVector{ + path: 1, + key: testKeyHex{ + fingerPrint: "fcadf38c", + chainCode: "73bd9fff1cfbde33a1b846c27085f711c0fe2d66fd32e139d3ebc28e5a4a6b90", + privateKey: "3757c7577170179c7868353ada796c839135b3d30554bbb74a4b1e4a5a58505c", + publicKey: "002e66aa57069c86cc18249aecf5cb5a9cebbfd6fadeab056254763874a9352b45", + }, + child: &testVector{ + path: 2147483646, + key: testKeyHex{ + fingerPrint: "aca70953", + chainCode: "0902fe8a29f9140480a00ef244bd183e8a13288e4412d8389d140aac1794825a", + privateKey: "5837736c89570de861ebc173b1086da4f505d4adb387c6a1b1342d5e4ac9ec72", + publicKey: "00e33c0f7d81d843c572275f287498e8d408654fdf0d1e065b84e2e6f157aab09b", + }, + child: &testVector{ + path: 2, + key: testKeyHex{ + fingerPrint: "422c654b", + chainCode: "5d70af781f3a37b829f0d060924d5e960bdc02e85423494afc0b1a41bbe196d4", + privateKey: "551d333177df541ad876a60ea71f00447931c0a9da16f227c11ea080d7391b8d", + publicKey: "0047150c75db263559a70d5778bf36abbab30fb061ad69f69ece61a72b0cfa4fc0", + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, c := range cases { + t.Run(c.seed, func(t *testing.T) { + seed, _ := hex.DecodeString(c.seed) + + masterKey, err := NewMasterKeyWithCurve(seed, Ed25519) + require.NoError(t, err) + + c.vector.key.Assert(t, masterKey) + + key := masterKey + + path := "m" + + tc := c.vector.child + for tc != nil { + var err error + path += fmt.Sprintf("/%d", tc.path) + t.Run(path, func(t *testing.T) { + key, err = key.NewChildKey(tc.path) + require.NoError(t, err) + + tc.key.Assert(t, key) + }) + + tc = tc.child + } + }) + } +} + func TestPublicParentPublicChildDerivation(t *testing.T) { - // Generated using https://iancoleman.github.io/bip39/ + // Generated using https://iancoleman.io/bip39/ // Root key: // xprv9s21ZrQH143K2Cfj4mDZBcEecBmJmawReGwwoAou2zZzG45bM6cFPJSvobVTCB55L6Ld2y8RzC61CpvadeAnhws3CHsMFhNjozBKGNgucYm // Derivation Path m/44'/60'/0'/0: diff --git a/go.mod b/go.mod index b45d3bf..402566e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/tyler-smith/go-bip32 -go 1.14 +go 1.15 require ( github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e diff --git a/utils.go b/utils.go index 4628f9d..4083ea1 100644 --- a/utils.go +++ b/utils.go @@ -2,7 +2,10 @@ package bip32 import ( "bytes" + "crypto/ed25519" + "crypto/hmac" "crypto/sha256" + "crypto/sha512" "encoding/binary" "fmt" "io" @@ -70,6 +73,14 @@ func hash160(data []byte) ([]byte, error) { return hash2, nil } +func hmac512(data, key []byte) ([]byte, error) { + hmac := hmac.New(sha512.New, key) + if _, err := hmac.Write(data); err != nil { + return nil, err + } + return hmac.Sum(nil), nil +} + // // Encoding // @@ -100,10 +111,17 @@ func base58Decode(data string) ([]byte, error) { } // Keys -func publicKeyForPrivateKey(key []byte) []byte { +func publicKeyForPrivateKeyBitcoin(key []byte) []byte { return compressPublicKey(curve.ScalarBaseMult(key)) } +func publicKeyForPrivateKeyEd25519(key []byte) []byte { + var publicKey [33]byte + privateKey := ed25519.NewKeyFromSeed(key) + copy(publicKey[1:], privateKey[32:]) + return publicKey[:] +} + func addPublicKeys(key1 []byte, key2 []byte) []byte { x1, y1 := expandPublicKey(key1) x2, y2 := expandPublicKey(key2)