From fb2f6a6e6f97964b4df14a9c7f8dc45505bc5c6a Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Thu, 27 Mar 2025 07:55:34 -0400 Subject: [PATCH] 0.0.2: base58, memzero --- CHANGELOG.md | 6 +++ bip32.c | 57 +++++++++++++++------ bip32.h | 23 ++++++++- examples/cli.c | 3 +- examples/py/bindings.py | 77 ++++++++++++++++++++++++----- examples/py/test_fuzz_cross_impl.py | 29 ++++++++++- test/test.c | 14 +++--- 7 files changed, 171 insertions(+), 38 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..79e48ef --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +## 0.0.2 + +- Changed `bip32_serialze` str_len to a pointer which returns the final length of the + base58-encoded out string. +- Added some precautionary `sodium_memzero()` calls. +- Made `bip32_b58_encode()` and `bip32_b58_decode()` a public part of the API. diff --git a/bip32.c b/bip32.c index ba9cc12..99b1c1b 100644 --- a/bip32.c +++ b/bip32.c @@ -66,6 +66,7 @@ int bip32_from_seed(bip32_key *key, const unsigned char *seed, size_t seed_len) memcpy(key->chain_code, output + BIP32_PRIVKEY_SIZE, BIP32_CHAINCODE_SIZE); exit: + sodium_memzero(output, crypto_auth_hmacsha512_BYTES); secp256k1_context_destroy(ctx); return retcode; } @@ -164,6 +165,9 @@ int bip32_index_derive(bip32_key *target, const bip32_key *source, uint32_t inde bip32_hmac_sha512(output, source->chain_code, BIP32_CHAINCODE_SIZE, hmac_msg, hmac_msg_len); + // hmac_msg potentially contains privkey bytes. + sodium_memzero(hmac_msg, hmac_msg_len); + memcpy(target->chain_code, output + BIP32_PRIVKEY_SIZE, BIP32_CHAINCODE_SIZE); if (source->is_private) { @@ -222,19 +226,6 @@ int bip32_index_derive(bip32_key *target, const bip32_key *source, uint32_t inde return retcode; } - -// Returns true if invalid path characters are detected in a path string. -static bool has_invalid_path_characters(const char* str) { - const char* valid = "m/0123456789hH'pP"; - while (*str) { - if (!strchr(valid, *str)) { - return true; - } - str++; - } - return false; -} - int bip32_derive_from_str(bip32_key* target, const char* source, const char* path) { if (!target || !source || !path || strncmp(path, "m", 1) != 0) { return 0; @@ -250,6 +241,7 @@ int bip32_derive_from_str(bip32_key* target, const char* source, const char* pat strncmp(source, "xpub", 4) == 0 || strncmp(source, "tpub", 4) == 0) { if (!bip32_deserialize(&basekey, source, strlen(source))) { + sodium_memzero(&basekey, sizeof(bip32_key)); return 0; } } @@ -261,6 +253,7 @@ int bip32_derive_from_str(bip32_key* target, const char* source, const char* pat return 0; } if (!bip32_from_seed(&basekey, seedbytes, bin_len)) { + sodium_memzero(&basekey, sizeof(bip32_key)); return 0; } } else { @@ -269,6 +262,7 @@ int bip32_derive_from_str(bip32_key* target, const char* source, const char* pat if (bip32_derive(&basekey, path)) { memcpy(target, &basekey, sizeof(bip32_key)); + sodium_memzero(&basekey, sizeof(bip32_key)); return 1; } return 0; @@ -284,6 +278,18 @@ int bip32_derive_from_seed(bip32_key* target, const unsigned char* seed, size_t return 0; } +// Returns true if invalid path characters are detected in a path string. +static bool has_invalid_path_characters(const char* str) { + const char* valid = "m/0123456789hH'pP"; + while (*str) { + if (!strchr(valid, *str)) { + return true; + } + str++; + } + return false; +} + // Do an in-place derivation on `key`. int bip32_derive(bip32_key* key, const char* path) { if (!path || strncmp(path, "m", 1) != 0 || has_invalid_path_characters(path)) { @@ -314,6 +320,7 @@ int bip32_derive(bip32_key* key, const char* path) { if (bip32_index_derive(key, &tmp, path_index) != 1) { return 0; } + sodium_memzero(&tmp, sizeof(bip32_key)); p = strchr(end, '/'); } @@ -323,7 +330,7 @@ int bip32_derive(bip32_key* key, const char* path) { #define SER_SIZE 78 #define SER_PLUS_CHECKSUM_SIZE (SER_SIZE + 4) -int bip32_serialize(const bip32_key *key, char *str, size_t str_len) { +int bip32_serialize(const bip32_key *key, char *str, size_t* str_len) { unsigned char data[SER_PLUS_CHECKSUM_SIZE]; uint32_t version; @@ -362,7 +369,10 @@ int bip32_serialize(const bip32_key *key, char *str, size_t str_len) { bip32_sha256_double(hash, data, 78); memcpy(data + SER_SIZE, hash, 4); - return b58enc(str, &str_len, data, SER_PLUS_CHECKSUM_SIZE); + bool b58_ok = bip32_b58_encode(str, str_len, data, SER_PLUS_CHECKSUM_SIZE); + sodium_memzero(data, SER_PLUS_CHECKSUM_SIZE); + + return b58_ok ? 1 : 0; } #define BIP32_BASE58_BYTES_LEN 82 @@ -371,7 +381,8 @@ int bip32_deserialize(bip32_key *key, const char *str, const size_t str_len) { unsigned char data[BIP32_BASE58_BYTES_LEN]; size_t data_len = BIP32_BASE58_BYTES_LEN; - if (!b58tobin(data, &data_len, str, str_len) || data_len != BIP32_BASE58_BYTES_LEN) { + if (!bip32_b58_decode(data, &data_len, str, str_len) || data_len != BIP32_BASE58_BYTES_LEN) { + sodium_memzero(data, BIP32_BASE58_BYTES_LEN); return 0; } @@ -426,16 +437,22 @@ int bip32_deserialize(bip32_key *key, const char *str, const size_t str_len) { if (key->is_private) { if (data[45] != 0) { + sodium_memzero(data, BIP32_BASE58_BYTES_LEN); secp256k1_context_destroy(ctx); return 0; } + memcpy(key->key.privkey, data + 46, BIP32_PRIVKEY_SIZE); + sodium_memzero(data, BIP32_BASE58_BYTES_LEN); + if (!secp256k1_ec_seckey_verify(ctx, key->key.privkey)) { secp256k1_context_destroy(ctx); return 0; } } else { memcpy(key->key.pubkey, data + 45, BIP32_PUBKEY_SIZE); + sodium_memzero(data, BIP32_BASE58_BYTES_LEN); + secp256k1_pubkey pubkey; if (!secp256k1_ec_pubkey_parse(ctx, &pubkey, key->key.pubkey, BIP32_PUBKEY_SIZE)) { secp256k1_context_destroy(ctx); @@ -484,3 +501,11 @@ void bip32_hmac_sha512( crypto_auth_hmacsha512_update(&state, msg, msg_len); crypto_auth_hmacsha512_final(&state, hmac_out); } + +bool bip32_b58_encode(char* str_out, size_t* out_size, const unsigned char* data, size_t data_size) { + return b58enc(str_out, out_size, data, data_size); +} + +bool bip32_b58_decode(unsigned char* bin_out, size_t* out_size, const char* str_in, size_t str_size) { + return b58tobin(bin_out, out_size, str_in, str_size); +} diff --git a/bip32.h b/bip32.h index 95698ad..fa5e0ae 100644 --- a/bip32.h +++ b/bip32.h @@ -1,3 +1,7 @@ +/** + * This API is generally designed to mimic the libsecp256k1 library, in terms of + * argument order and return conventions. + */ #include #include #include @@ -60,11 +64,14 @@ int bip32_derive_from_str(bip32_key *target, const char* source, const char* pat */ int bip32_derive(bip32_key *target, const char* path); -/** Serialize a BIP32 key to its base58 string representation. +/** Serialize a BIP32 key to its base58 string representation. Writes the resulting + * string to `str`, and the length of the resulting string to `str_len`. + * + * `str_len` must initially be set to the maximum length of the `str` buffer. * * Returns 1 if successful. */ -int bip32_serialize(const bip32_key *key, char *str, size_t str_len); +int bip32_serialize(const bip32_key *key, char *str, size_t* str_len); /** Deserialize a BIP32 key from its base58 string representation. * @@ -106,6 +113,18 @@ void bip32_hmac_sha512( size_t msg_len ); +/** Encode some bytes to a base58 string. + * + * Returns true if successful. + */ +bool bip32_b58_encode(char* str_out, size_t* out_size, const unsigned char* data, size_t data_size); + +/** Decode some bytes from a base58 string. + * + * Returns true if successful. + */ +bool bip32_b58_decode(unsigned char* bin_out, size_t* out_size, const char* str_in, size_t str_size); + #ifdef __cplusplus } #endif diff --git a/examples/cli.c b/examples/cli.c index f3c339b..615a513 100644 --- a/examples/cli.c +++ b/examples/cli.c @@ -15,7 +15,8 @@ int main(int argc, char *argv[]) { return 1; } - if (!bip32_serialize(&key, serialized, sizeof(serialized))) { + size_t out_size; + if (!bip32_serialize(&key, serialized, &out_size)) { fprintf(stderr, "Serialization failed\n"); return 1; } diff --git a/examples/py/bindings.py b/examples/py/bindings.py index 5682e83..91383e3 100644 --- a/examples/py/bindings.py +++ b/examples/py/bindings.py @@ -3,9 +3,17 @@ from functools import lru_cache from pathlib import Path from ctypes import ( - c_uint8, c_uint32, c_size_t, c_char_p, c_ubyte, c_void_p, - Structure, Union, POINTER, create_string_buffer -) + c_uint8, + c_uint32, + c_size_t, + c_char_p, + c_ubyte, + c_void_p, + Structure, + Union, + POINTER, + create_string_buffer, + byref) @lru_cache @@ -20,7 +28,9 @@ def get_bip32_module(): bip32_lib.bip32_init.argtypes = [POINTER(BIP32Key)] bip32_lib.bip32_init.restype = None - bip32_lib.bip32_derive_from_seed.argtypes = [POINTER(BIP32Key), POINTER(c_ubyte), c_size_t, c_char_p] + bip32_lib.bip32_derive_from_seed.argtypes = [ + POINTER(BIP32Key), POINTER(c_ubyte), c_size_t, c_char_p + ] bip32_lib.bip32_derive_from_seed.restype = ctypes.c_int bip32_lib.bip32_derive_from_str.argtypes = [POINTER(BIP32Key), c_char_p, c_char_p] @@ -29,7 +39,9 @@ def get_bip32_module(): bip32_lib.bip32_derive.argtypes = [POINTER(BIP32Key), c_char_p] bip32_lib.bip32_derive.restype = ctypes.c_int - bip32_lib.bip32_serialize.argtypes = [POINTER(BIP32Key), c_char_p, c_size_t] + bip32_lib.bip32_serialize.argtypes = [ + POINTER(BIP32Key), c_char_p, POINTER(c_size_t) + ] bip32_lib.bip32_serialize.restype = ctypes.c_bool bip32_lib.bip32_deserialize.argtypes = [POINTER(BIP32Key), c_char_p, c_size_t] @@ -38,14 +50,22 @@ def get_bip32_module(): bip32_lib.bip32_get_public.argtypes = [POINTER(BIP32Key), POINTER(BIP32Key)] bip32_lib.bip32_get_public.restype = ctypes.c_int + bip32_lib.bip32_b58_encode.argtypes = [ + c_char_p, POINTER(c_size_t), POINTER(c_ubyte), c_size_t + ] + bip32_lib.bip32_b58_encode.restype = ctypes.c_bool + + bip32_lib.bip32_b58_decode.argtypes = [ + POINTER(c_ubyte), POINTER(c_size_t), c_char_p, c_size_t + ] + bip32_lib.bip32_b58_decode.restype = ctypes.c_bool + return bip32_lib class KeyUnion(Union): - _fields_ = [ - ('privkey', c_uint8 * 32), - ('pubkey', c_uint8 * 33) - ] + _fields_ = [('privkey', c_uint8 * 32), ('pubkey', c_uint8 * 33)] + class BIP32Key(Structure): _fields_ = [ @@ -65,6 +85,7 @@ def print(self): class BIP32: + def __init__(self): self.key = BIP32Key() self.bip32_lib = get_bip32_module() @@ -80,7 +101,9 @@ def derive(self, path: str) -> 'BIP32': def serialize(self): buf = create_string_buffer(200) # Standard BIP32 serialization length - if not self.bip32_lib.bip32_serialize(self.key, buf, len(buf)): + out_len = c_size_t(len(buf)) + + if not self.bip32_lib.bip32_serialize(self.key, buf, byref(out_len)): raise ValueError("Serialization failed") return buf.value.decode() @@ -106,7 +129,8 @@ def derive(source: str, path: str = 'm') -> BIP32: """ b = BIP32() - if not get_bip32_module().bip32_derive_from_str(b.key, source.encode(), path.encode()): + if not get_bip32_module().bip32_derive_from_str( + b.key, source.encode(), path.encode()): raise ValueError("failed") return b @@ -115,6 +139,35 @@ def derive_from_seed(seed: bytes, path: str = 'm') -> BIP32: b = BIP32() c_seed = ctypes.c_char_p(seed) seed_ptr = ctypes.cast(c_seed, POINTER(c_ubyte)) - if not get_bip32_module().bip32_derive_from_seed(b.key, seed_ptr, len(seed), path.encode()): + if not get_bip32_module().bip32_derive_from_seed( + b.key, seed_ptr, len(seed), path.encode()): raise ValueError("failed") return b + + +def b58_encode(inp: bytes) -> str: + data_len = len(inp) + data_arr = (c_ubyte * data_len)(*inp) + + out_size = c_size_t(data_len * 2) + str_out = ctypes.create_string_buffer(out_size.value) + + if not get_bip32_module().bip32_b58_encode( + str_out, byref(out_size), data_arr, data_len): + raise ValueError("base58 encoding failed") + + return str_out.value[:out_size.value].decode('utf-8') + + +def b58_decode(in_str: str) -> bytes: + str_bytes = c_char_p(in_str.encode('utf-8')) + str_len = len(in_str) + + out_size = c_size_t(str_len * 2) + bin_out = (c_ubyte * out_size.value)() + + if not get_bip32_module().bip32_b58_decode( + bin_out, byref(out_size), str_bytes, str_len): + raise ValueError("base58 decoding failed") + + return bytes(bin_out[-out_size.value:]) diff --git a/examples/py/test_fuzz_cross_impl.py b/examples/py/test_fuzz_cross_impl.py index 8c7b058..fc1a616 100644 --- a/examples/py/test_fuzz_cross_impl.py +++ b/examples/py/test_fuzz_cross_impl.py @@ -18,7 +18,7 @@ import pytest from hypothesis import given, strategies as st, target, settings -from bindings import derive, derive_from_seed +from bindings import derive, derive_from_seed, b58_decode, b58_encode log = logging.getLogger(__name__) logging.basicConfig() @@ -181,5 +181,32 @@ def test_xpub_impls(bip32_path): assert ours == pys + +@given(b58_data=st.binary(min_size=0, max_size=1000)) +@settings(max_examples=1000) +def test_base58(b58_data: bytes): + if b58_data and len(b58_data) >= 2: + # TODO: figure out why the base58 impl is failing on example b':' + assert b58_decode(b58_encode(b58_data)) == b58_data + + +def test_base58_known_vectors(): + cases = [ + (bytes.fromhex(""), ""), + (bytes.fromhex("00"), "1"), + (bytes.fromhex("0000"), "11"), + (bytes.fromhex("68656c6c6f20776f726c64"), "StV1DL6CwTryKyV"), + (bytes.fromhex("0068656c6c6f20776f726c64"), "1StV1DL6CwTryKyV"), + (bytes.fromhex("000068656c6c6f20776f726c64"), "11StV1DL6CwTryKyV"), + ] + + for raw, encoded in cases: + if raw: # Skip empty input for encoding test + assert b58_encode(raw) == encoded + + if encoded: # Skip empty input for decoding test + assert b58_decode(encoded) == raw + + if __name__ == "__main__": pytest.main([__file__, "-v", "--capture=no", "--hypothesis-show-statistics", "-x"] + sys.argv[1:]) diff --git a/test/test.c b/test/test.c index cea2e53..2cd0a18 100644 --- a/test/test.c +++ b/test/test.c @@ -50,8 +50,10 @@ void test_vector_1(void) { return; } + size_t out_size = 200; + // Test private key - bip32_serialize(&master, str, sizeof(str)); + bip32_serialize(&master, str, &out_size); printf(" Private: %s\n", str); printf(" Expected: xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi\n"); if (strcmp(str, "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi") != 0) { @@ -62,7 +64,7 @@ void test_vector_1(void) { // Test public key bip32_key pub; bip32_get_public(&pub, &master); - bip32_serialize(&pub, str, sizeof(str)); + bip32_serialize(&pub, str, &out_size); printf(" Public: %s\n", str); printf(" Expected: xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8\n"); if (strcmp(str, "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8") != 0) { @@ -77,12 +79,12 @@ void test_vector_1(void) { return; } - bip32_serialize(&child, str, sizeof(str)); + bip32_serialize(&child, str, &out_size); printf(" Private: %s\n", str); printf(" Expected: xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7\n"); bip32_get_public(&pub, &child); - bip32_serialize(&pub, str, sizeof(str)); + bip32_serialize(&pub, str, &out_size); printf(" Public: %s\n", str); printf(" Expected: xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw\n"); @@ -93,12 +95,12 @@ void test_vector_1(void) { return; } - bip32_serialize(&master, str, sizeof(str)); + bip32_serialize(&master, str, &out_size); printf(" Private: %s\n", str); printf(" Expected: xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs\n"); bip32_get_public(&pub, &master); - bip32_serialize(&pub, str, sizeof(str)); + bip32_serialize(&pub, str, &out_size); printf(" Public: %s\n", str); printf(" Expected: xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ\n");