Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 93 additions & 16 deletions src/bip32.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,43 @@ static bool HmacSha512(const unsigned char* key, int keyLen,
len == 64;
}

static bool PrivateKeyToCompressedPubKey(const std::vector<unsigned char>& 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<unsigned char>& entropy,
std::string& mnemonicOut,
std::string& errorOut)
Expand Down Expand Up @@ -284,10 +321,10 @@ bool BIP32MasterFromSeed(const std::vector<unsigned char>& 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();
Expand All @@ -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))
Expand Down Expand Up @@ -363,4 +401,43 @@ bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent,
return true;
}

bool BIP32DerivePath(const BIP32PrivateNode& root,
const std::vector<unsigned int>& 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
14 changes: 13 additions & 1 deletion src/bip32.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -16,6 +16,8 @@
namespace bitflash
{

static const unsigned int BIP32_HARDENED = 0x80000000U;

struct BIP32PrivateNode
{
std::vector<unsigned char> privateKey; // 32-byte scalar
Expand Down Expand Up @@ -43,6 +45,16 @@ bool BIP32MasterFromSeed(const std::vector<unsigned char>& 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<unsigned int>& path,
BIP32PrivateNode& nodeOut,
std::string& errorOut);

bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent,
unsigned int childIndex,
BIP32PrivateNode& childOut,
Expand Down
119 changes: 119 additions & 0 deletions src/test_bip32.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

#include "bip32.h"

#include <openssl/sha.h>

#include <cstdio>
#include <cstring>
#include <string>
Expand Down Expand Up @@ -57,6 +59,76 @@ static std::string ToHex(const std::vector<unsigned char>& v)
return out;
}

static bool DecodeBase58Check(const std::string& str, std::vector<unsigned char>& out)
{
static const char* pszBase58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
out.clear();

std::vector<unsigned char> 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<unsigned char>::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<unsigned char>::iterator it = b256.begin();
while (it != b256.end() && *it == 0)
++it;

std::vector<unsigned char> full;
full.assign(zeros, 0);
while (it != b256.end())
full.push_back(*it++);
if (full.size() < 4)
return false;

std::vector<unsigned char> 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<unsigned char> 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;
Expand Down Expand Up @@ -124,6 +196,53 @@ int main()
"pre-hardened index rejected");
}

printf("bip32_paths\n");
{
std::vector<unsigned char> seed = FromHex("000102030405060708090a0b0c0d0e0f");
BIP32PrivateNode master;
CHECK(BIP32MasterFromSeed(seed, master, err), "path test master derives");

struct PathVector
{
const char* name;
std::vector<unsigned int> path;
const char* xprv;
};

std::vector<PathVector> 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;
Expand Down