diff --git a/src/bip32.cpp b/src/bip32.cpp index 6423974..6ba946a 100644 --- a/src/bip32.cpp +++ b/src/bip32.cpp @@ -114,6 +114,43 @@ static bool HmacSha512(const unsigned char* key, int keyLen, len == 64; } +static bool PrivateKeyToCompressedPubKey(const std::vector& privateKey, + unsigned char out[33], + std::string& errorOut) +{ + if (!IsValidPrivateKey(privateKey)) + { + errorOut = "private key is invalid"; + return false; + } + + BIGNUM* bn = BN_bin2bn(&privateKey[0], 32, NULL); + BN_CTX* ctx = BN_CTX_new(); + EC_GROUP* group = EC_GROUP_new_by_curve_name(NID_secp256k1); + EC_POINT* pub = group ? EC_POINT_new(group) : NULL; + + bool ok = false; + if (bn && ctx && group && pub && + EC_POINT_mul(group, pub, bn, NULL, NULL, ctx)) + { + size_t n = EC_POINT_point2oct(group, pub, POINT_CONVERSION_COMPRESSED, + out, 33, ctx); + ok = n == 33; + } + + BN_free(bn); + BN_CTX_free(ctx); + EC_POINT_free(pub); + EC_GROUP_free(group); + + if (!ok) + { + errorOut = "could not serialize parent public key"; + return false; + } + return true; +} + bool BIP39EntropyToMnemonic(const std::vector& entropy, std::string& mnemonicOut, std::string& errorOut) @@ -284,10 +321,10 @@ bool BIP32MasterFromSeed(const std::vector& seed, return true; } -bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, - unsigned int childIndex, - BIP32PrivateNode& childOut, - std::string& errorOut) +bool BIP32DeriveChild(const BIP32PrivateNode& parent, + unsigned int childNumber, + BIP32PrivateNode& childOut, + std::string& errorOut) { childOut.privateKey.clear(); childOut.chainCode.clear(); @@ -297,20 +334,21 @@ bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, errorOut = "parent private key or chain code is invalid"; return false; } - if (childIndex >= 0x80000000U) + unsigned char data[37]; + if (childNumber & BIP32_HARDENED) { - errorOut = "child index must be non-hardened; hardening is applied here"; - return false; + data[0] = 0x00; + memcpy(data + 1, &parent.privateKey[0], 32); } - - const unsigned int hardened = childIndex | 0x80000000U; - unsigned char data[37]; - data[0] = 0x00; - memcpy(data + 1, &parent.privateKey[0], 32); - data[33] = (unsigned char)((hardened >> 24) & 0xff); - data[34] = (unsigned char)((hardened >> 16) & 0xff); - data[35] = (unsigned char)((hardened >> 8) & 0xff); - data[36] = (unsigned char)(hardened & 0xff); + else + { + if (!PrivateKeyToCompressedPubKey(parent.privateKey, data, errorOut)) + return false; + } + data[33] = (unsigned char)((childNumber >> 24) & 0xff); + data[34] = (unsigned char)((childNumber >> 16) & 0xff); + data[35] = (unsigned char)((childNumber >> 8) & 0xff); + data[36] = (unsigned char)(childNumber & 0xff); unsigned char I[64]; if (!HmacSha512(&parent.chainCode[0], 32, data, sizeof(data), I)) @@ -363,4 +401,43 @@ bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, return true; } +bool BIP32DerivePath(const BIP32PrivateNode& root, + const std::vector& path, + BIP32PrivateNode& nodeOut, + std::string& errorOut) +{ + nodeOut.privateKey.clear(); + nodeOut.chainCode.clear(); + errorOut.clear(); + if (!IsValidPrivateKey(root.privateKey) || root.chainCode.size() != 32) + { + errorOut = "root private key or chain code is invalid"; + return false; + } + + BIP32PrivateNode cur = root; + for (size_t i = 0; i < path.size(); i++) + { + BIP32PrivateNode next; + if (!BIP32DeriveChild(cur, path[i], next, errorOut)) + return false; + cur = next; + } + nodeOut = cur; + return true; +} + +bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, + unsigned int childIndex, + BIP32PrivateNode& childOut, + std::string& errorOut) +{ + if (childIndex >= BIP32_HARDENED) + { + errorOut = "child index must be non-hardened; hardening is applied here"; + return false; + } + return BIP32DeriveChild(parent, childIndex | BIP32_HARDENED, childOut, errorOut); +} + } // namespace bitflash diff --git a/src/bip32.h b/src/bip32.h index a029444..e9862d5 100644 --- a/src/bip32.h +++ b/src/bip32.h @@ -5,7 +5,7 @@ // // This module is deliberately independent from wallet.dat and GUI code. It is // the auditable foundation: mnemonic generation/validation, seed derivation, -// and hardened private child derivation over secp256k1. +// and private child derivation over secp256k1. #ifndef BITFLASH_BIP32_H #define BITFLASH_BIP32_H @@ -16,6 +16,8 @@ namespace bitflash { +static const unsigned int BIP32_HARDENED = 0x80000000U; + struct BIP32PrivateNode { std::vector privateKey; // 32-byte scalar @@ -43,6 +45,16 @@ bool BIP32MasterFromSeed(const std::vector& seed, BIP32PrivateNode& nodeOut, std::string& errorOut); +bool BIP32DeriveChild(const BIP32PrivateNode& parent, + unsigned int childNumber, + BIP32PrivateNode& childOut, + std::string& errorOut); + +bool BIP32DerivePath(const BIP32PrivateNode& root, + const std::vector& path, + BIP32PrivateNode& nodeOut, + std::string& errorOut); + bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, unsigned int childIndex, BIP32PrivateNode& childOut, diff --git a/src/test_bip32.cpp b/src/test_bip32.cpp index 610a651..6c11510 100644 --- a/src/test_bip32.cpp +++ b/src/test_bip32.cpp @@ -5,6 +5,8 @@ #include "bip32.h" +#include + #include #include #include @@ -57,6 +59,76 @@ static std::string ToHex(const std::vector& v) return out; } +static bool DecodeBase58Check(const std::string& str, std::vector& out) +{ + static const char* pszBase58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + out.clear(); + + std::vector b256(str.size() * 733 / 1000 + 1); + for (char c : str) + { + const char* p = strchr(pszBase58, c); + if (!p) + return false; + int carry = (int)(p - pszBase58); + for (std::vector::reverse_iterator it = b256.rbegin(); + it != b256.rend(); ++it) + { + carry += 58 * (*it); + *it = (unsigned char)(carry & 0xff); + carry >>= 8; + } + if (carry != 0) + return false; + } + + size_t zeros = 0; + while (zeros < str.size() && str[zeros] == '1') + zeros++; + std::vector::iterator it = b256.begin(); + while (it != b256.end() && *it == 0) + ++it; + + std::vector full; + full.assign(zeros, 0); + while (it != b256.end()) + full.push_back(*it++); + if (full.size() < 4) + return false; + + std::vector payload(full.begin(), full.end() - 4); + unsigned char h1[SHA256_DIGEST_LENGTH]; + unsigned char h2[SHA256_DIGEST_LENGTH]; + SHA256(&payload[0], payload.size(), h1); + SHA256(h1, sizeof(h1), h2); + if (memcmp(h2, &full[full.size() - 4], 4) != 0) + return false; + + out = payload; + return true; +} + +static bool DecodeXPrvNode(const std::string& xprv, BIP32PrivateNode& nodeOut) +{ + nodeOut.privateKey.clear(); + nodeOut.chainCode.clear(); + + std::vector payload; + if (!DecodeBase58Check(xprv, payload)) + return false; + if (payload.size() != 78) + return false; + if (payload[0] != 0x04 || payload[1] != 0x88 || + payload[2] != 0xad || payload[3] != 0xe4) + return false; + if (payload[45] != 0x00) + return false; + + nodeOut.chainCode.assign(payload.begin() + 13, payload.begin() + 45); + nodeOut.privateKey.assign(payload.begin() + 46, payload.end()); + return true; +} + int main() { std::string err; @@ -124,6 +196,53 @@ int main() "pre-hardened index rejected"); } + printf("bip32_paths\n"); + { + std::vector seed = FromHex("000102030405060708090a0b0c0d0e0f"); + BIP32PrivateNode master; + CHECK(BIP32MasterFromSeed(seed, master, err), "path test master derives"); + + struct PathVector + { + const char* name; + std::vector path; + const char* xprv; + }; + + std::vector vectors; + vectors.push_back({"m", + {}, + "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"}); + vectors.push_back({"m/0'", + {0 | BIP32_HARDENED}, + "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7"}); + vectors.push_back({"m/0'/1", + {0 | BIP32_HARDENED, 1}, + "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs"}); + vectors.push_back({"m/0'/1/2'", + {0 | BIP32_HARDENED, 1, 2 | BIP32_HARDENED}, + "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM"}); + vectors.push_back({"m/0'/1/2'/2", + {0 | BIP32_HARDENED, 1, 2 | BIP32_HARDENED, 2}, + "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334"}); + vectors.push_back({"m/0'/1/2'/2/1000000000", + {0 | BIP32_HARDENED, 1, 2 | BIP32_HARDENED, 2, 1000000000}, + "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76"}); + + for (const PathVector& v : vectors) + { + BIP32PrivateNode actual; + BIP32PrivateNode expected; + std::string label = std::string("derives ") + v.name; + CHECK(BIP32DerivePath(master, v.path, actual, err), label.c_str()); + label = std::string("decodes vector ") + v.name; + CHECK(DecodeXPrvNode(v.xprv, expected), label.c_str()); + label = std::string("matches vector ") + v.name; + CHECK(actual.privateKey == expected.privateKey && + actual.chainCode == expected.chainCode, label.c_str()); + } + } + printf("\n%s (%d failures)\n", g_fail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", g_fail); return g_fail == 0 ? 0 : 1;