Skip to content

Commit 8b76ce0

Browse files
authored
Merge pull request #2 from jamesob/jamesob-25-03-impl-change
refine the `bip32_derive` interface
2 parents 5920cff + 55306f9 commit 8b76ce0

File tree

11 files changed

+127
-47
lines changed

11 files changed

+127
-47
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ libbip32.so:
4343
$(CC) $(CFLAGS) -shared -fPIC bip32.c $(LDFLAGS) -o libbip32.so
4444

4545
fuzz_target: libbip32.so
46-
$(CC) -fsanitize=fuzzer,address fuzz.c libbip32.so $(LDFLAGS) -o fuzz_target
46+
$(CC) -fsanitize=fuzzer,address test/fuzz.c libbip32.so $(LDFLAGS) -o fuzz_target
4747

4848
.PHONY: test
4949
test: libbip32.so

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,16 @@ Install libsecp256k1
2121
```bash
2222
git clone https://github.com/bitcoin-core/secp256k1.git && \
2323
cd secp256k1 && ./autogen.sh && ./configure && make && sudo make install
24+
25+
# NOTE: in production, you should verify the associated GPG sigs and/or pin hashes.
2426
```
2527

2628
Install libsodium
2729
```bash
2830
git clone https://github.com/jedisct1/libsodium.git && \
2931
cd libsodium && ./autogen.sh -sb && ./configure && make && sudo make install
32+
33+
# NOTE: in production, you should verify the associated GPG sigs and/or pin hashes.
3034
```
3135

3236
Install this library
@@ -35,6 +39,13 @@ git clone https://github.com/jamesob/cbip32.git && \
3539
make && sudo make install
3640
```
3741

42+
## Tests
43+
44+
- [Vectors from BIP32](./examples/py/test_bip32.py)
45+
- [Cross-implementation fuzz](./examples/py/test_fuzz_cross_impl.py)
46+
- [C unittests](./test/test.c)
47+
- [C fuzz](./test/fuzz.c)
48+
3849
## Performance
3950

4051
The Python bindings for this implementation have been shown to be

bip32.c

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -235,22 +235,19 @@ static bool has_invalid_path_characters(const char* str) {
235235
return false;
236236
}
237237

238-
int bip32_derive(bip32_key *target, const char* source, const char* path) {
238+
int bip32_derive_from_str(bip32_key* target, const char* source, const char* path) {
239239
if (!target || !source || !path || strncmp(path, "m", 1) != 0) {
240240
return 0;
241241
}
242242
if (strlen(source) < 1) {
243243
return 0;
244244
}
245-
if (has_invalid_path_characters(path)) {
246-
return 0;
247-
}
248245
bip32_key basekey;
249246
size_t source_len = strlen(source);
250-
251-
if (strncmp(source, "xprv", 4) == 0 ||
247+
248+
if (strncmp(source, "xprv", 4) == 0 ||
252249
strncmp(source, "tprv", 4) == 0 ||
253-
strncmp(source, "xpub", 4) == 0 ||
250+
strncmp(source, "xpub", 4) == 0 ||
254251
strncmp(source, "tpub", 4) == 0) {
255252
if (!bip32_deserialize(&basekey, source, strlen(source))) {
256253
return 0;
@@ -269,36 +266,57 @@ int bip32_derive(bip32_key *target, const char* source, const char* path) {
269266
} else {
270267
return 0;
271268
}
272-
269+
270+
if (bip32_derive(&basekey, path)) {
271+
memcpy(target, &basekey, sizeof(bip32_key));
272+
return 1;
273+
}
274+
return 0;
275+
}
276+
277+
int bip32_derive_from_seed(bip32_key* target, const unsigned char* seed, size_t seed_len, const char* path) {
278+
if (!bip32_from_seed(target, seed, seed_len)) {
279+
return 0;
280+
}
281+
if (bip32_derive(target, path)) {
282+
return 1;
283+
}
284+
return 0;
285+
}
286+
287+
// Do an in-place derivation on `key`.
288+
int bip32_derive(bip32_key* key, const char* path) {
289+
if (!path || strncmp(path, "m", 1) != 0 || has_invalid_path_characters(path)) {
290+
return 0;
291+
}
292+
273293
char *p = (char*)strchr(path, '/');
274294
if (!p) {
275-
memcpy(target, &basekey, sizeof(bip32_key));
276295
return 1;
277296
}
278-
297+
279298
while (p && *p) {
280299
char *end;
281300
uint32_t path_index = strtoul(p + 1, &end, 10);
282-
301+
283302
if (errno == ERANGE || path_index > INT_MAX || end == p + 1) {
284303
// Overflow detected.
285304
return 0;
286305
}
287-
306+
288307
if (*end == '\'' || *end == 'h' || *end == 'H' || *end == 'p' || *end == 'P') {
289308
path_index |= HARDENED_INDEX;
290309
end++;
291310
}
292311

293312
bip32_key tmp;
294-
memcpy(&tmp, &basekey, sizeof(bip32_key));
295-
if (bip32_index_derive(&basekey, &tmp, path_index) != 1) {
313+
memcpy(&tmp, key, sizeof(bip32_key));
314+
if (bip32_index_derive(key, &tmp, path_index) != 1) {
296315
return 0;
297316
}
298317
p = strchr(end, '/');
299318
}
300-
301-
memcpy(target, &basekey, sizeof(bip32_key));
319+
302320
return 1;
303321
}
304322

@@ -308,37 +326,37 @@ int bip32_derive(bip32_key *target, const char* source, const char* path) {
308326
int bip32_serialize(const bip32_key *key, char *str, size_t str_len) {
309327
unsigned char data[SER_PLUS_CHECKSUM_SIZE];
310328
uint32_t version;
311-
329+
312330
// Set version bytes based on network and key type
313331
if (key->is_private) {
314332
version = key->is_testnet ? VERSION_TPRIV : VERSION_XPRIV;
315333
} else {
316334
version = key->is_testnet ? VERSION_TPUB : VERSION_XPUB;
317335
}
318336
version = to_big_endian(version);
319-
337+
320338
memcpy(data, &version, sizeof(version));
321-
339+
322340
data[4] = key->depth;
323-
341+
324342
// Write parent fingerprint
325343
uint32_t parfinger = key->parent_fingerprint;
326344
memcpy(data + 5, &parfinger, sizeof(parfinger));
327345

328346
// Write child number in big-endian
329347
uint32_t childnum = to_big_endian(key->child_number);
330348
memcpy(data + 9, &childnum, sizeof(childnum));
331-
349+
332350
// Copy chain code
333351
memcpy(data + 13, key->chain_code, 32);
334-
352+
335353
if (key->is_private) {
336354
data[45] = 0;
337355
memcpy(data + 46, key->key.privkey, 32);
338356
} else {
339357
memcpy(data + 45, key->key.pubkey, 33);
340358
}
341-
359+
342360
// Add checksum and base58 encode
343361
uint8_t hash[32];
344362
bip32_sha256_double(hash, data, 78);
@@ -374,7 +392,7 @@ int bip32_deserialize(bip32_key *key, const char *str, const size_t str_len) {
374392
key->is_private = 0;
375393
break;
376394
case VERSION_XPRIV:
377-
key->is_testnet = 0;
395+
key->is_testnet = 0;
378396
key->is_private = 1;
379397
break;
380398
case VERSION_TPRIV:
@@ -454,10 +472,10 @@ void bip32_sha256_double(uint8_t *hash, const uint8_t *data, size_t len) {
454472
}
455473

456474
void bip32_hmac_sha512(
457-
unsigned char* hmac_out,
458-
const unsigned char* key,
459-
size_t key_len,
460-
const unsigned char* msg,
475+
unsigned char* hmac_out,
476+
const unsigned char* key,
477+
size_t key_len,
478+
const unsigned char* msg,
461479
size_t msg_len
462480
) {
463481
assert(sodium_init() >= 0);

bip32.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,24 @@ void bip32_init(bip32_key *key);
4141
*/
4242
int bip32_from_seed(bip32_key *key, const unsigned char *seed, size_t seed_len);
4343

44+
/** Derive a BIP32 path from a raw seed.
45+
*
46+
* Returns 1 if successful.
47+
*/
48+
int bip32_derive_from_seed(bip32_key* target, const unsigned char* seed, size_t seed_len, const char* path);
49+
4450
/** Derive a BIP32 path. `source` as a null-terminated string that can either be a
4551
* 32 byte seed (secret), or a serialized BIP32 key (xprv*, xpub*, tprv*, tpub*).
4652
*
4753
* Returns 1 if successful.
4854
*/
49-
int bip32_derive(bip32_key *target, const char* source, const char* path);
55+
int bip32_derive_from_str(bip32_key *target, const char* source, const char* path);
56+
57+
/** Derive a BIP32 key along a path in-place. This is destructive on `target`.
58+
*
59+
* Returns 1 if successful.
60+
*/
61+
int bip32_derive(bip32_key *target, const char* path);
5062

5163
/** Serialize a BIP32 key to its base58 string representation.
5264
*

examples/cli.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ int main(int argc, char *argv[]) {
1010
bip32_key key;
1111
char serialized[112];
1212

13-
if (!bip32_derive(&key, argv[1], argv[2])) {
13+
if (!bip32_derive_from_str(&key, argv[1], argv[2])) {
1414
fprintf(stderr, "Derivation failed\n");
1515
return 1;
1616
}

examples/go/cbip32.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ func Derive(source, path string) (*BIP32Key, bool) {
2525
defer C.free(unsafe.Pointer(cSource))
2626
defer C.free(unsafe.Pointer(cPath))
2727

28-
result := C.bip32_derive(&key.cKey, cSource, cPath)
28+
result := C.bip32_derive_from_str(&key.cKey, cSource, cPath)
2929
return key, result == 1
3030
}

examples/py/bindings.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from functools import lru_cache
44
from pathlib import Path
55
from ctypes import (
6-
c_uint8, c_uint32, c_size_t, c_char_p, c_void_p,
6+
c_uint8, c_uint32, c_size_t, c_char_p, c_ubyte, c_void_p,
77
Structure, Union, POINTER, create_string_buffer
88
)
99

@@ -20,10 +20,13 @@ def get_bip32_module():
2020
bip32_lib.bip32_init.argtypes = [POINTER(BIP32Key)]
2121
bip32_lib.bip32_init.restype = None
2222

23-
bip32_lib.bip32_derive.argtypes = [POINTER(BIP32Key), c_char_p, c_char_p]
24-
bip32_lib.bip32_derive.restype = ctypes.c_int
23+
bip32_lib.bip32_derive_from_seed.argtypes = [POINTER(BIP32Key), POINTER(c_ubyte), c_size_t, c_char_p]
24+
bip32_lib.bip32_derive_from_seed.restype = ctypes.c_int
25+
26+
bip32_lib.bip32_derive_from_str.argtypes = [POINTER(BIP32Key), c_char_p, c_char_p]
27+
bip32_lib.bip32_derive_from_str.restype = ctypes.c_int
2528

26-
bip32_lib.bip32_derive.argtypes = [POINTER(BIP32Key), c_char_p, c_char_p]
29+
bip32_lib.bip32_derive.argtypes = [POINTER(BIP32Key), c_char_p]
2730
bip32_lib.bip32_derive.restype = ctypes.c_int
2831

2932
bip32_lib.bip32_serialize.argtypes = [POINTER(BIP32Key), c_char_p, c_size_t]
@@ -103,6 +106,15 @@ def derive(source: str, path: str = 'm') -> BIP32:
103106
104107
"""
105108
b = BIP32()
106-
if not get_bip32_module().bip32_derive(b.key, source.encode(), path.encode()):
109+
if not get_bip32_module().bip32_derive_from_str(b.key, source.encode(), path.encode()):
110+
raise ValueError("failed")
111+
return b
112+
113+
114+
def derive_from_seed(seed: bytes, path: str = 'm') -> BIP32:
115+
b = BIP32()
116+
c_seed = ctypes.c_char_p(seed)
117+
seed_ptr = ctypes.cast(c_seed, POINTER(c_ubyte))
118+
if not get_bip32_module().bip32_derive_from_seed(b.key, seed_ptr, len(seed), path.encode()):
107119
raise ValueError("failed")
108120
return b

examples/py/test_bip32.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import unittest
22

3-
from bindings import derive
3+
from bindings import derive, derive_from_seed
44

55

66
class TestBIP32(unittest.TestCase):
@@ -16,6 +16,12 @@ def test_vectors(self):
1616
pub = derived.get_public()
1717
self.assertEqual(pub.serialize(), expected["xpub"])
1818

19+
# Ensure the equivalent `derive_from_seed` call yields the
20+
# same result.
21+
from_seed = derive_from_seed(bytes.fromhex(seed), path)
22+
self.assertEqual(from_seed.serialize(), derived.serialize())
23+
24+
1925
def test_bad_vectors(self):
2026
for case in BAD_VECTORS:
2127
ser, msg = case.strip().split(' ', 1)

examples/py/test_fuzz_cross_impl.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
# ]
99
# ///
1010
import sys
11-
import random
1211
import logging
1312
import time
1413
from contextlib import contextmanager
@@ -19,7 +18,7 @@
1918
import pytest
2019
from hypothesis import given, strategies as st, target, settings
2120

22-
from bindings import derive
21+
from bindings import derive, derive_from_seed
2322

2423
log = logging.getLogger(__name__)
2524
logging.basicConfig()
@@ -130,7 +129,9 @@ def bip32_paths(draw):
130129
@given(seed_hex_str=valid_seeds, bip32_path=py_compatible_bip32_paths())
131130
@settings(max_examples=2_000)
132131
def test_versus_py(seed_hex_str, bip32_path):
133-
"""Compare implementations of BIP32 on a random seed and path."""
132+
"""
133+
Compare implementations of BIP32 on a random seed and path.
134+
"""
134135
with timer('ours'):
135136
ours = our_derive(seed_hex_str, bip32_path)
136137
with timer('python-bip32'):
@@ -139,6 +140,21 @@ def test_versus_py(seed_hex_str, bip32_path):
139140
assert ours == pys
140141

141142

143+
@given(seedhex=valid_seeds, path=py_compatible_bip32_paths())
144+
@settings(max_examples=2_000)
145+
def test_versus_ourselves(seedhex, path):
146+
"""
147+
Ensure that our different derive functions work properly.
148+
"""
149+
seed = bytes.fromhex(seedhex)
150+
from_seed = INVALID_KEY
151+
try:
152+
from_seed = derive_from_seed(seed, path).serialize()
153+
except Exception:
154+
pass
155+
assert our_derive(seedhex, path) == from_seed
156+
157+
142158
@given(seed_hex_str=valid_seeds, bip32_path=py_compatible_bip32_paths())
143159
@settings(max_examples=100, deadline=5000) # verstable is slooooww, so allow 5s tests
144160
def test_versus_vs(seed_hex_str, bip32_path):

test/fuzz.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
2727

2828
if (path[0] != 'm') path[0] = 'm';
2929

30-
bip32_derive(&target, source, path);
30+
bip32_derive_from_str(&target, source, path);
3131

3232
free(source);
3333
free(path);

0 commit comments

Comments
 (0)