Skip to content

Commit 2a6c2f8

Browse files
committed
Merge remote-tracking branch 'benma/rust-tests'
2 parents c023f4f + 7543353 commit 2a6c2f8

File tree

12 files changed

+220
-318
lines changed

12 files changed

+220
-318
lines changed

src/keystore.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,17 @@ static void _free_string(char** str)
322322

323323
USE_RESULT static keystore_error_t _retain_seed(const uint8_t* seed, size_t seed_len)
324324
{
325+
#ifdef TESTING
326+
const uint8_t test_unstretched_retained_seed_encryption_key[32] =
327+
"\xfe\x09\x76\x01\x14\x52\xa7\x22\x12\xe4\xb8\xbd\x57\x2b\x5b\xe3\x01\x41\xa3\x56\xf1\x13"
328+
"\x37\xd2\x9d\x35\xea\x8f\xf9\x97\xbe\xfc";
329+
memcpy(
330+
_unstretched_retained_seed_encryption_key,
331+
test_unstretched_retained_seed_encryption_key,
332+
32);
333+
#else
325334
random_32_bytes(_unstretched_retained_seed_encryption_key);
335+
#endif
326336
uint8_t retained_seed_encryption_key[32] = {0};
327337
UTIL_CLEANUP_32(retained_seed_encryption_key);
328338
keystore_error_t result = _stretch_retained_seed_encryption_key(
@@ -344,7 +354,17 @@ USE_RESULT static keystore_error_t _retain_seed(const uint8_t* seed, size_t seed
344354

345355
USE_RESULT static bool _retain_bip39_seed(const uint8_t* bip39_seed)
346356
{
357+
#ifdef TESTING
358+
const uint8_t test_unstretched_retained_bip39_seed_encryption_key[32] =
359+
"\x9b\x44\xc7\x04\x88\x93\xfa\xaf\x6e\x2d\x76\x25\xd1\x3d\x8f\x1c\xab\x07\x65\xfd\x61\xf1"
360+
"\x59\xd9\x71\x3e\x08\x15\x5d\x06\x71\x7c";
361+
memcpy(
362+
_unstretched_retained_bip39_seed_encryption_key,
363+
test_unstretched_retained_bip39_seed_encryption_key,
364+
32);
365+
#else
347366
random_32_bytes(_unstretched_retained_bip39_seed_encryption_key);
367+
#endif
348368
uint8_t retained_bip39_seed_encryption_key[32] = {0};
349369
UTIL_CLEANUP_32(retained_bip39_seed_encryption_key);
350370
if (_stretch_retained_seed_encryption_key(

src/memory/memory.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,17 @@ bool memory_get_salt_root(uint8_t* salt_root_out)
759759
return !MEMEQ(salt_root_out, empty, sizeof(empty));
760760
}
761761

762+
#ifdef TESTING
763+
bool memory_set_salt_root(const uint8_t* salt_root)
764+
{
765+
chunk_1_t chunk = {0};
766+
CLEANUP_CHUNK(chunk);
767+
_read_chunk(CHUNK_1, chunk_bytes);
768+
memcpy(chunk.fields.salt_root, salt_root, 32);
769+
return _write_chunk(CHUNK_1, chunk.bytes);
770+
}
771+
#endif
772+
762773
bool memory_get_noise_static_private_key(uint8_t* private_key_out)
763774
{
764775
chunk_1_t chunk = {0};

src/memory/memory.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ USE_RESULT bool memory_bootloader_set_flags(auto_enter_t auto_enter, upside_down
219219
*/
220220
USE_RESULT bool memory_get_salt_root(uint8_t* salt_root_out);
221221

222+
#ifdef TESTING
223+
USE_RESULT bool memory_set_salt_root(const uint8_t* salt_root);
224+
#endif
225+
222226
/**
223227
* @param[out] private_key_out must be 32 bytes.
224228
* @return false if the key has not been initialized (memory_setup() has not

src/rust/Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/rust/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ resolver = "2"
3030

3131
[workspace.dependencies]
3232
bech32 = { version = "0.11.0", default-features = false }
33-
bitcoin = { version = "0.32.2", default-features = false }
33+
# The secp-recovery feature is currently only needed in tests to make use of `RecoverableSignature`.
34+
# Attempting to enable it conditionally only for tests somehow leads to linking errors (duplicate secp256k1 symbols).
35+
bitcoin = { version = "0.32.2", default-features = false, features = ["secp-recovery"] }
3436
hex = { version = "0.4", default-features = false, features = ["alloc"] }
3537
num-bigint = { version = "0.4.6", default-features = false }
3638
# force-soft-compact reduces the binary size by ~3kB. In future versions of sha2 this will change to

src/rust/bitbox02-rust/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ version = "0.13.1"
6363
default-features = false
6464
features = ["derive"]
6565

66+
[dev-dependencies]
67+
bitbox-aes = { path = "../bitbox-aes", features = ["use-wally-sha512"] }
68+
6669
[features]
6770
ed25519 = [
6871
"dep:bip32-ed25519",

src/rust/bitbox02-sys/build.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const ALLOWLIST_VARS: &[&str] = &[
4242
"MEMORY_PLATFORM_BITBOX02_PLUS",
4343
"MEMORY_SECURECHIP_TYPE_ATECC",
4444
"MEMORY_SECURECHIP_TYPE_OPTIGA",
45+
"MAX_UNLOCK_ATTEMPTS",
4546
];
4647

4748
const ALLOWLIST_TYPES: &[&str] = &[
@@ -84,10 +85,13 @@ const ALLOWLIST_FNS: &[&str] = &[
8485
"keystore_unlock",
8586
"keystore_unlock_bip39",
8687
"keystore_bip39_mnemonic_from_seed",
88+
"keystore_test_get_retained_seed_encrypted",
89+
"keystore_test_get_retained_bip39_seed_encrypted",
8790
"label_create",
8891
"localtime",
8992
"lock_animation_start",
9093
"lock_animation_stop",
94+
"memory_set_salt_root",
9195
"memory_add_noise_remote_static_pubkey",
9296
"memory_bootloader_hash",
9397
"memory_check_noise_remote_static_pubkey",
@@ -142,6 +146,8 @@ const ALLOWLIST_FNS: &[&str] = &[
142146
"securechip_model",
143147
"securechip_monotonic_increments_remaining",
144148
"securechip_u2f_counter_set",
149+
"smarteeprom_is_enabled",
150+
"smarteeprom_disable",
145151
"smarteeprom_bb02_config",
146152
"status_create",
147153
"trinary_choice_create",

src/rust/bitbox02/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ hex = { workspace = true }
3030

3131
[dev-dependencies]
3232
hex = { workspace = true }
33+
bitbox-aes = { path = "../bitbox-aes" }
3334

3435
[features]
3536
# Only to be enabled in unit tests.

src/rust/bitbox02/src/keystore.rs

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,10 +356,51 @@ pub fn secp256k1_schnorr_sign(
356356
#[cfg(test)]
357357
mod tests {
358358
use super::*;
359-
use crate::testing::{mock_memory, mock_unlocked_using_mnemonic};
359+
use bitcoin::secp256k1;
360+
361+
use crate::testing::{mock_memory, mock_unlocked, mock_unlocked_using_mnemonic};
360362
use alloc::string::ToString;
361363
use util::bip32::HARDENED;
362364

365+
#[test]
366+
fn test_secp256k1_sign() {
367+
lock();
368+
let keypath = [44 + HARDENED, 0 + HARDENED, 0 + HARDENED, 0, 5];
369+
let msg = [0x88u8; 32];
370+
let host_nonce = [0x56u8; 32];
371+
372+
// Fails because keystore is locked.
373+
assert!(secp256k1_sign(&keypath, &msg, &host_nonce).is_err());
374+
375+
mock_unlocked();
376+
let sign_result = secp256k1_sign(&keypath, &msg, &host_nonce).unwrap();
377+
// Verify signature against expected pubkey.
378+
379+
let secp = secp256k1::Secp256k1::new();
380+
let expected_pubkey = {
381+
let pubkey =
382+
hex::decode("023ffb4a4e41444d40e4e1e4c6cc329bcba2be50d0ef380aea19d490c373be58fb")
383+
.unwrap();
384+
secp256k1::PublicKey::from_slice(&pubkey).unwrap()
385+
};
386+
let msg = secp256k1::Message::from_digest_slice(&msg).unwrap();
387+
// Test recid by recovering the public key from the signature and checking against the
388+
// expected public key.
389+
let recoverable_sig = secp256k1::ecdsa::RecoverableSignature::from_compact(
390+
&sign_result.signature,
391+
secp256k1::ecdsa::RecoveryId::from_i32(sign_result.recid as i32).unwrap(),
392+
)
393+
.unwrap();
394+
395+
let recovered_pubkey = secp.recover_ecdsa(&msg, &recoverable_sig).unwrap();
396+
assert_eq!(recovered_pubkey, expected_pubkey);
397+
398+
// Verify signature.
399+
assert!(secp
400+
.verify_ecdsa(&msg, &recoverable_sig.to_standard(), &expected_pubkey)
401+
.is_ok());
402+
}
403+
363404
#[test]
364405
fn test_bip39_mnemonic_to_seed() {
365406
assert!(bip39_mnemonic_to_seed("invalid").is_err());
@@ -509,6 +550,123 @@ mod tests {
509550
assert!(bip39_mnemonic_from_seed(b"foo").is_err());
510551
}
511552

553+
#[test]
554+
fn test_unlock() {
555+
mock_memory();
556+
lock();
557+
558+
assert!(matches!(unlock("password"), Err(Error::Unseeded)));
559+
560+
let seed = hex::decode("cb33c20cea62a5c277527e2002da82e6e2b37450a755143a540a54cea8da9044")
561+
.unwrap();
562+
563+
let mock_salt_root =
564+
hex::decode("3333333333333333444444444444444411111111111111112222222222222222")
565+
.unwrap();
566+
crate::memory::set_salt_root(mock_salt_root.as_slice().try_into().unwrap()).unwrap();
567+
568+
assert!(encrypt_and_store_seed(&seed, "password").is_ok());
569+
// Loop to check that unlocking works while unlocked.
570+
for _ in 0..3 {
571+
assert!(unlock("password").is_ok());
572+
}
573+
574+
// Also check that the retained seed was encrypted with the expected encryption key.
575+
let decrypted = {
576+
let retained_seed_encrypted: &[u8] = unsafe {
577+
let mut len = 0usize;
578+
let ptr = bitbox02_sys::keystore_test_get_retained_seed_encrypted(&mut len);
579+
core::slice::from_raw_parts(ptr, len)
580+
};
581+
let expected_retained_seed_secret =
582+
hex::decode("b156be416530c6fc00018844161774a3546a53ac6dd4a0462608838e216008f7")
583+
.unwrap();
584+
bitbox_aes::decrypt_with_hmac(&expected_retained_seed_secret, retained_seed_encrypted)
585+
.unwrap()
586+
};
587+
assert_eq!(decrypted.as_slice(), seed.as_slice());
588+
589+
// First 9 wrong attempts.
590+
for i in 1..bitbox02_sys::MAX_UNLOCK_ATTEMPTS {
591+
assert!(matches!(
592+
unlock("invalid password"),
593+
Err(Error::IncorrectPassword { remaining_attempts }) if remaining_attempts
594+
== (bitbox02_sys::MAX_UNLOCK_ATTEMPTS - i) as u8
595+
));
596+
// Still seeded.
597+
assert!(crate::memory::is_seeded());
598+
// Wrong password does not lock the keystore again if already unlocked.
599+
assert!(copy_seed().is_ok());
600+
}
601+
// Last attempt, triggers reset.
602+
assert!(matches!(
603+
unlock("invalid password"),
604+
Err(Error::MaxAttemptsExceeded),
605+
));
606+
// Last wrong attempt locks & resets. There is no more seed.
607+
assert!(!crate::memory::is_seeded());
608+
assert!(copy_seed().is_err());
609+
assert!(matches!(unlock("password"), Err(Error::Unseeded)));
610+
}
611+
612+
#[test]
613+
fn test_unlock_bip39() {
614+
mock_memory();
615+
lock();
616+
617+
let seed = hex::decode("1111111111111111222222222222222233333333333333334444444444444444")
618+
.unwrap();
619+
620+
let mock_salt_root =
621+
hex::decode("3333333333333333444444444444444411111111111111112222222222222222")
622+
.unwrap();
623+
crate::memory::set_salt_root(mock_salt_root.as_slice().try_into().unwrap()).unwrap();
624+
625+
assert!(encrypt_and_store_seed(&seed, "password").is_ok());
626+
assert!(unlock("password").is_ok());
627+
assert!(is_locked()); // still locked, it is only unlocked after unlock_bip39.
628+
assert!(unlock_bip39("foo").is_ok());
629+
630+
// Check that the retained bip39 seed was encrypted with the expected encryption key.
631+
let decrypted = {
632+
let retained_bip39_seed_encrypted: &[u8] = unsafe {
633+
let mut len = 0usize;
634+
let ptr = bitbox02_sys::keystore_test_get_retained_bip39_seed_encrypted(&mut len);
635+
core::slice::from_raw_parts(ptr, len)
636+
};
637+
let expected_retained_bip39_seed_secret =
638+
hex::decode("856d9a8c1ea42a69ae76324244ace674397ff1360a4ba4c85ffbd42cee8a7f29")
639+
.unwrap();
640+
bitbox_aes::decrypt_with_hmac(
641+
&expected_retained_bip39_seed_secret,
642+
retained_bip39_seed_encrypted,
643+
)
644+
.unwrap()
645+
};
646+
let expected_bip39_seed = hex::decode("2b3c63de86f0f2b13cc6a36c1ba2314fbc1b40c77ab9cb64e96ba4d5c62fc204748ca6626a9f035e7d431bce8c9210ec0bdffc2e7db873dee56c8ac2153eee9a").unwrap();
647+
assert_eq!(decrypted.as_slice(), expected_bip39_seed.as_slice());
648+
}
649+
650+
// This tests that you can create a keystore, unlock it, and then do this again. This is an
651+
// expected workflow for when the wallet setup process is restarted after seeding and unlocking,
652+
// but before creating a backup, in which case a new seed is created.
653+
#[test]
654+
fn test_create_and_unlock_twice() {
655+
mock_memory();
656+
lock();
657+
658+
let seed = hex::decode("cb33c20cea62a5c277527e2002da82e6e2b37450a755143a540a54cea8da9044")
659+
.unwrap();
660+
let seed2 = hex::decode("c28135734876aff9ccf4f1d60df8d19a0a38fd02085883f65fc608eb769a635d")
661+
.unwrap();
662+
assert!(encrypt_and_store_seed(&seed, "password").is_ok());
663+
assert!(unlock("password").is_ok());
664+
// Create new (different) seed.
665+
assert!(encrypt_and_store_seed(&seed2, "password").is_ok());
666+
assert!(unlock("password").is_ok());
667+
assert_eq!(copy_seed().unwrap().as_slice(), &seed2);
668+
}
669+
512670
// Functional test to store seeds, unlock, retrieve seed.
513671
#[test]
514672
fn test_seeds() {

src/rust/bitbox02/src/memory.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,14 @@ pub fn ble_enable(enable: bool) -> Result<(), ()> {
217217
}
218218
}
219219

220+
#[cfg(feature = "testing")]
221+
pub fn set_salt_root(salt_root: &[u8; 32]) -> Result<(), ()> {
222+
match unsafe { bitbox02_sys::memory_set_salt_root(salt_root.as_ptr()) } {
223+
true => Ok(()),
224+
false => Err(()),
225+
}
226+
}
227+
220228
#[cfg(test)]
221229
mod tests {
222230
use super::*;

0 commit comments

Comments
 (0)