Skip to content

Commit 0062964

Browse files
committed
keystore: port get_xprv, get_xpub, get_private_key to Rust
By using `bitcoin::bip32`. The `bip32::Xprv` wrapper is there to try to avoid unzeroed copies of private data. It is only a best effort attempt, as the `bitcoin` and `secp256k1` Rust crate maintainers don't support zeroizing and don't think it's worthwhile to worry about it until the Rust compiler puts in some guarantees. See rust-bitcoin/rust-secp256k1#553. `bitbox02_rust::keystore::tests::test_get_xpub` checks there is no regression. `secp256k1_get_private_key_twice` is introduced, as in C, some calls used `_get_xprv_twice()` to get to the private key, and others only `_get_xprv()` (the ones where a bitflip or similar would not be critical).
1 parent 593f763 commit 0062964

File tree

7 files changed

+125
-174
lines changed

7 files changed

+125
-174
lines changed

src/keystore.c

Lines changed: 0 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -552,68 +552,6 @@ static bool _get_xprv(const uint32_t* keypath, const size_t keypath_len, struct
552552
return true;
553553
}
554554

555-
static bool _ext_key_equal(struct ext_key* one, struct ext_key* two)
556-
{
557-
if (!MEMEQ(one->chain_code, two->chain_code, sizeof(one->chain_code))) {
558-
return false;
559-
}
560-
if (!MEMEQ(one->parent160, two->parent160, sizeof(one->parent160))) {
561-
return false;
562-
}
563-
if (one->depth != two->depth) {
564-
return false;
565-
}
566-
if (!MEMEQ(one->priv_key, two->priv_key, sizeof(one->priv_key))) {
567-
return false;
568-
}
569-
if (one->child_num != two->child_num) {
570-
return false;
571-
}
572-
if (!MEMEQ(one->hash160, two->hash160, sizeof(one->hash160))) {
573-
return false;
574-
}
575-
if (one->version != two->version) {
576-
return false;
577-
}
578-
if (!MEMEQ(one->pub_key, two->pub_key, sizeof(one->pub_key))) {
579-
return false;
580-
}
581-
return true;
582-
}
583-
584-
static bool _get_xprv_twice(
585-
const uint32_t* keypath,
586-
const size_t keypath_len,
587-
struct ext_key* xprv_out)
588-
{
589-
struct ext_key one __attribute__((__cleanup__(keystore_zero_xkey))) = {0};
590-
if (!_get_xprv(keypath, keypath_len, &one)) {
591-
return false;
592-
}
593-
if (!_get_xprv(keypath, keypath_len, xprv_out)) {
594-
return false;
595-
}
596-
if (!_ext_key_equal(&one, xprv_out)) {
597-
keystore_zero_xkey(xprv_out);
598-
return false;
599-
}
600-
return true;
601-
}
602-
603-
bool keystore_get_xpub(
604-
const uint32_t* keypath,
605-
const size_t keypath_len,
606-
struct ext_key* hdkey_neutered_out)
607-
{
608-
struct ext_key xprv __attribute__((__cleanup__(keystore_zero_xkey))) = {0};
609-
if (!_get_xprv_twice(keypath, keypath_len, &xprv)) {
610-
return false;
611-
}
612-
bip32_key_strip_private_key(&xprv); // neuter
613-
*hdkey_neutered_out = xprv;
614-
return true;
615-
}
616-
617555
void keystore_zero_xkey(struct ext_key* xkey)
618556
{
619557
util_zero(xkey, sizeof(struct ext_key));
@@ -739,19 +677,6 @@ bool keystore_get_ed25519_seed(uint8_t* seed_out)
739677
return true;
740678
}
741679

742-
USE_RESULT bool keystore_encode_xpub_at_keypath(
743-
const uint32_t* keypath,
744-
size_t keypath_len,
745-
uint8_t* out)
746-
{
747-
struct ext_key derived_xpub __attribute__((__cleanup__(keystore_zero_xkey))) = {0};
748-
if (!keystore_get_xpub(keypath, keypath_len, &derived_xpub)) {
749-
return false;
750-
}
751-
return bip32_key_serialize(&derived_xpub, BIP32_FLAG_KEY_PUBLIC, out, BIP32_SERIALIZED_LEN) ==
752-
WALLY_OK;
753-
}
754-
755680
static bool _schnorr_keypair(
756681
const uint32_t* keypath,
757682
size_t keypath_len,
@@ -808,19 +733,6 @@ bool keystore_secp256k1_schnorr_sign(
808733
return secp256k1_schnorrsig_verify(ctx, sig64_out, msg32, 32, &pubkey) == 1;
809734
}
810735

811-
bool keystore_secp256k1_get_private_key(
812-
const uint32_t* keypath,
813-
const size_t keypath_len,
814-
uint8_t* key_out)
815-
{
816-
struct ext_key xprv __attribute__((__cleanup__(keystore_zero_xkey))) = {0};
817-
if (!_get_xprv_twice(keypath, keypath_len, &xprv)) {
818-
return false;
819-
}
820-
memcpy(key_out, xprv.priv_key + 1, 32);
821-
return true;
822-
}
823-
824736
#ifdef TESTING
825737
void keystore_mock_unlocked(const uint8_t* seed, size_t seed_len, const uint8_t* bip39_seed)
826738
{

src/keystore.h

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -146,18 +146,6 @@ USE_RESULT bool keystore_bip39_mnemonic_to_seed(
146146
uint8_t* seed_out,
147147
size_t* seed_len_out);
148148

149-
/**
150-
* Can be used only if the keystore is unlocked. Returns the derived xpub,
151-
* using bip32 derivation. Derivation is done from the xprv master, so hardened
152-
* derivation is allowed.
153-
* On success, xpub_out must be destroyed using keystore_zero_xkey().
154-
* @return true on success, false on failure.
155-
*/
156-
USE_RESULT bool keystore_get_xpub(
157-
const uint32_t* keypath,
158-
size_t keypath_len,
159-
struct ext_key* hdkey_neutered_out);
160-
161149
/**
162150
* Safely destroy a xpub or xprv.
163151
*/
@@ -240,16 +228,6 @@ USE_RESULT bool keystore_get_u2f_seed(uint8_t* seed_out);
240228
*/
241229
USE_RESULT bool keystore_get_ed25519_seed(uint8_t* seed_out);
242230

243-
/**
244-
* Encode an xpub at the given `keypath` as 78 bytes according to BIP32. The version bytes are
245-
* the ones corresponding to `xpub`, i.e. 0x0488B21E.
246-
* `out` must be `BIP32_SERIALIZED_LEN` long.
247-
*/
248-
USE_RESULT bool keystore_encode_xpub_at_keypath(
249-
const uint32_t* keypath,
250-
size_t keypath_len,
251-
uint8_t* out);
252-
253231
/**
254232
* Sign a message that verifies against the pubkey tweaked using BIP-86.
255233
*
@@ -267,18 +245,6 @@ USE_RESULT bool keystore_secp256k1_schnorr_sign(
267245
const uint8_t* tweak,
268246
uint8_t* sig64_out);
269247

270-
/**
271-
* Get the private key at the keypath.
272-
*
273-
* @param[in] keypath derivation keypath
274-
* @param[in] keypath_len number of elements in keypath
275-
* @param[out] key_out resulting private key, must be 32 bytes.
276-
*/
277-
USE_RESULT bool keystore_secp256k1_get_private_key(
278-
const uint32_t* keypath,
279-
size_t keypath_len,
280-
uint8_t* key_out);
281-
282248
#ifdef TESTING
283249
/**
284250
* convenience to mock the keystore state (locked, seed) in tests.

src/rust/bitbox02-rust/src/bip32.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,46 @@
1313
// limitations under the License.
1414

1515
use super::pb;
16+
use alloc::boxed::Box;
1617
use alloc::string::String;
1718
use alloc::vec::Vec;
1819

1920
pub use pb::btc_pub_request::XPubType;
2021

2122
use bitcoin::hashes::Hash;
23+
use zeroize::Zeroize;
24+
25+
// Wrapper of `bitcoin::bip32::Xpriv` to imlement zeroizing on drop.
26+
#[derive(PartialEq)]
27+
pub struct Xprv {
28+
// Boxed so moves don't leave copies.
29+
pub xprv: Box<bitcoin::bip32::Xpriv>,
30+
}
31+
32+
impl Drop for Xprv {
33+
fn drop(&mut self) {
34+
self.xprv.depth.zeroize();
35+
let pf: &mut [u8; 4] = self.xprv.parent_fingerprint.as_mut();
36+
pf.zeroize();
37+
self.xprv.child_number = bitcoin::bip32::ChildNumber::from(0u32);
38+
self.xprv.private_key.non_secure_erase();
39+
let chain_code: &mut [u8; 32] = self.xprv.chain_code.as_mut();
40+
chain_code.zeroize();
41+
}
42+
}
43+
44+
impl From<bitcoin::bip32::Xpriv> for Xprv {
45+
/// Convert from `bitcoin::bip32::Xpriv`, attempting to Box it without leaving copies by using
46+
/// [RVO](https://github.com/rust-lang/rfcs/pull/2884).
47+
///
48+
/// This is only a best effort attempt, as RVO is not guaranteed, and copies might be left on
49+
/// the stack in various places, e.g. in the hmac-sha512 engine inside `new_master()`, etc.
50+
fn from(value: bitcoin::bip32::Xpriv) -> Self {
51+
let xprv = Box::<bitcoin::bip32::Xpriv>::new_uninit();
52+
let xprv = Box::write(xprv, value);
53+
Self { xprv }
54+
}
55+
}
2256

2357
#[derive(Clone)]
2458
pub struct Xpub {

src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -784,7 +784,7 @@ async fn _process(
784784
if let Some(ref mut silent_payment) = silent_payment {
785785
let keypair = bitcoin::key::UntweakedKeypair::from_seckey_slice(
786786
silent_payment.get_secp(),
787-
&bitbox02::keystore::secp256k1_get_private_key(&tx_input.keypath)?,
787+
&crate::keystore::secp256k1_get_private_key(&tx_input.keypath)?,
788788
)
789789
.unwrap();
790790
// For Taproot, only key path spends are allowed in silent payments, and we need to

src/rust/bitbox02-rust/src/keystore.rs

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,56 @@ pub fn get_bip39_mnemonic() -> Result<zeroize::Zeroizing<String>, ()> {
3131
keystore::bip39_mnemonic_from_seed(&keystore::copy_seed()?)
3232
}
3333

34-
/// Derives an xpub from the keystore seed at the given keypath.
34+
fn get_xprv(keypath: &[u32]) -> Result<bip32::Xprv, ()> {
35+
let bip39_seed = keystore::copy_bip39_seed()?;
36+
let xprv: bip32::Xprv =
37+
bitcoin::bip32::Xpriv::new_master(bitcoin::NetworkKind::Main, &bip39_seed)
38+
.map_err(|_| ())?
39+
.into();
40+
let secp = bitcoin::secp256k1::Secp256k1::new();
41+
Ok(xprv
42+
.xprv
43+
.derive_priv(&secp, &bip32::keypath_from_slice(keypath))
44+
.map_err(|_| ())?
45+
.into())
46+
}
47+
48+
fn get_xprv_twice(keypath: &[u32]) -> Result<bip32::Xprv, ()> {
49+
let xprv = get_xprv(keypath)?;
50+
if xprv == get_xprv(keypath)? {
51+
Ok(xprv)
52+
} else {
53+
Err(())
54+
}
55+
}
56+
57+
/// Get the private key at the keypath.
58+
pub fn secp256k1_get_private_key(keypath: &[u32]) -> Result<zeroize::Zeroizing<Vec<u8>>, ()> {
59+
let xprv = get_xprv(keypath)?;
60+
Ok(zeroize::Zeroizing::new(
61+
xprv.xprv.private_key.secret_bytes().to_vec(),
62+
))
63+
}
64+
65+
/// Get the private key at the keypath, computed twice to mitigate the risk of bitflips.
66+
pub fn secp256k1_get_private_key_twice(keypath: &[u32]) -> Result<zeroize::Zeroizing<Vec<u8>>, ()> {
67+
let privkey = secp256k1_get_private_key(keypath)?;
68+
if privkey == secp256k1_get_private_key(keypath)? {
69+
Ok(privkey)
70+
} else {
71+
Err(())
72+
}
73+
}
74+
75+
/// Can be used only if the keystore is unlocked. Returns the derived xpub,
76+
/// using bip32 derivation. Derivation is done from the xprv master, so hardened
77+
/// derivation is allowed.
3578
pub fn get_xpub(keypath: &[u32]) -> Result<bip32::Xpub, ()> {
36-
// Convert from C keystore to Rust by encoding the xpub in C and decoding it in Rust.
37-
bip32::Xpub::from_bytes(&keystore::encode_xpub_at_keypath(keypath)?)
79+
let xpriv = get_xprv_twice(keypath)?;
80+
let secp = bitcoin::secp256k1::Secp256k1::new();
81+
let xpub = bitcoin::bip32::Xpub::from_priv(&secp, &xpriv.xprv);
82+
83+
Ok(bip32::Xpub::from(xpub))
3884
}
3985

4086
/// Returns fingerprint of the root public key at m/, which are the first four bytes of its hash160
@@ -45,7 +91,7 @@ pub fn root_fingerprint() -> Result<Vec<u8>, ()> {
4591
}
4692

4793
fn bip85_entropy(keypath: &[u32]) -> Result<zeroize::Zeroizing<Vec<u8>>, ()> {
48-
let priv_key = keystore::secp256k1_get_private_key(keypath)?;
94+
let priv_key = secp256k1_get_private_key_twice(keypath)?;
4995
let mut mac = SimpleHmac::<Sha512>::new_from_slice(b"bip-entropy-from-k").unwrap();
5096
mac.update(&priv_key);
5197
let mut out = zeroize::Zeroizing::new(vec![0u8; 64]);
@@ -112,6 +158,40 @@ mod tests {
112158
mock_memory, mock_unlocked, mock_unlocked_using_mnemonic, TEST_MNEMONIC,
113159
};
114160

161+
#[test]
162+
fn test_secp256k1_get_private_key() {
163+
keystore::lock();
164+
let keypath = &[84 + HARDENED, 0 + HARDENED, 0 + HARDENED, 0, 0];
165+
assert!(secp256k1_get_private_key(keypath).is_err());
166+
167+
mock_unlocked_using_mnemonic(
168+
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
169+
"",
170+
);
171+
172+
assert_eq!(
173+
hex::encode(secp256k1_get_private_key(keypath).unwrap()),
174+
"4604b4b710fe91f584fff084e1a9159fe4f8408fff380596a604948474ce4fa3"
175+
);
176+
}
177+
178+
#[test]
179+
fn test_secp256k1_get_private_key_twice() {
180+
keystore::lock();
181+
let keypath = &[84 + HARDENED, 0 + HARDENED, 0 + HARDENED, 0, 0];
182+
assert!(secp256k1_get_private_key_twice(keypath).is_err());
183+
184+
mock_unlocked_using_mnemonic(
185+
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
186+
"",
187+
);
188+
189+
assert_eq!(
190+
hex::encode(secp256k1_get_private_key_twice(keypath).unwrap()),
191+
"4604b4b710fe91f584fff084e1a9159fe4f8408fff380596a604948474ce4fa3"
192+
);
193+
}
194+
115195
#[test]
116196
fn test_get_bip39_mnemonic() {
117197
keystore::lock();
@@ -125,6 +205,8 @@ mod tests {
125205
#[test]
126206
fn test_get_xpub() {
127207
let keypath = &[44 + HARDENED, 0 + HARDENED, 0 + HARDENED];
208+
// Also test with unhardened and non-zero elements.
209+
let keypath_5 = &[44 + HARDENED, 1 + HARDENED, 10 + HARDENED, 1, 100];
128210

129211
keystore::lock();
130212
assert!(get_xpub(keypath).is_err());
@@ -142,6 +224,10 @@ mod tests {
142224
get_xpub(keypath).unwrap().serialize_str(bip32::XPubType::Xpub).unwrap(),
143225
"xpub6Cj6NNCGj2CRPHvkuEG1rbW3nrNCAnLjaoTg1P67FCGoahSsbg9WQ7YaMEEP83QDxt2kZ3hTPAPpGdyEZcfAC1C75HfR66UbjpAb39f4PnG",
144226
);
227+
assert_eq!(
228+
get_xpub(keypath_5).unwrap().serialize_str(bip32::XPubType::Xpub).unwrap(),
229+
"xpub6HHn1zdtf1RjePopiTV5nxf8jY2xwbJicTQ91jV4cUJZ5EnbvXyBGDhqWt8B9JxxBt9vExi4pdWzrbrM43qSFs747VCGmSy2DPWAhg9MkUg",
230+
);
145231

146232
// 18 words
147233
mock_unlocked_using_mnemonic(

src/rust/bitbox02-sys/build.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,13 @@ const ALLOWLIST_FNS: &[&str] = &[
7171
"keystore_copy_seed",
7272
"keystore_copy_bip39_seed",
7373
"keystore_create_and_store_seed",
74-
"keystore_encode_xpub_at_keypath",
7574
"keystore_encrypt_and_store_seed",
7675
"keystore_get_bip39_word",
7776
"keystore_get_ed25519_seed",
7877
"keystore_get_u2f_seed",
7978
"keystore_is_locked",
8079
"keystore_lock",
8180
"keystore_mock_unlocked",
82-
"keystore_secp256k1_get_private_key",
8381
"keystore_secp256k1_nonce_commit",
8482
"keystore_secp256k1_schnorr_sign",
8583
"keystore_secp256k1_sign",

0 commit comments

Comments
 (0)