Skip to content

Commit 013cafc

Browse files
committed
keystore: port _test_keystore_unlock to Rust
The smarteeprom disable is also ported, though not strictly needed in unit tests - without it there is a memory leak due to the smarteeprom mock using malloc.
1 parent a6aceeb commit 013cafc

File tree

10 files changed

+102
-107
lines changed

10 files changed

+102
-107
lines changed

src/keystore.c

Lines changed: 10 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(

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: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/rust/bitbox02-sys/build.rs

Lines changed: 5 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,12 @@ const ALLOWLIST_FNS: &[&str] = &[
8485
"keystore_unlock",
8586
"keystore_unlock_bip39",
8687
"keystore_bip39_mnemonic_from_seed",
88+
"keystore_test_get_retained_seed_encrypted",
8789
"label_create",
8890
"localtime",
8991
"lock_animation_start",
9092
"lock_animation_stop",
93+
"memory_set_salt_root",
9194
"memory_add_noise_remote_static_pubkey",
9295
"memory_bootloader_hash",
9396
"memory_check_noise_remote_static_pubkey",
@@ -142,6 +145,8 @@ const ALLOWLIST_FNS: &[&str] = &[
142145
"securechip_model",
143146
"securechip_monotonic_increments_remaining",
144147
"securechip_u2f_counter_set",
148+
"smarteeprom_is_enabled",
149+
"smarteeprom_disable",
145150
"smarteeprom_bb02_config",
146151
"status_create",
147152
"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: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,65 @@ mod tests {
509509
assert!(bip39_mnemonic_from_seed(b"foo").is_err());
510510
}
511511

512+
#[test]
513+
fn test_unlock() {
514+
mock_memory();
515+
lock();
516+
517+
assert!(matches!(unlock("password"), Err(Error::Unseeded)));
518+
519+
let seed = hex::decode("cb33c20cea62a5c277527e2002da82e6e2b37450a755143a540a54cea8da9044")
520+
.unwrap();
521+
522+
let mock_salt_root =
523+
hex::decode("3333333333333333444444444444444411111111111111112222222222222222")
524+
.unwrap();
525+
crate::memory::set_salt_root(mock_salt_root.as_slice().try_into().unwrap()).unwrap();
526+
527+
assert!(encrypt_and_store_seed(&seed, "password").is_ok());
528+
// Loop to check that unlocking works while unlocked.
529+
for _ in 0..3 {
530+
assert!(unlock("password").is_ok());
531+
}
532+
533+
// Also check that the retained seed was encrypted with the expected encryption key.
534+
let decrypted = {
535+
let retained_seed_encrypted: &[u8] = unsafe {
536+
let mut len = 0usize;
537+
let ptr = bitbox02_sys::keystore_test_get_retained_seed_encrypted(&mut len);
538+
core::slice::from_raw_parts(ptr, len)
539+
};
540+
let expected_retained_seed_secret =
541+
hex::decode("b156be416530c6fc00018844161774a3546a53ac6dd4a0462608838e216008f7")
542+
.unwrap();
543+
bitbox_aes::decrypt_with_hmac(&expected_retained_seed_secret, retained_seed_encrypted)
544+
.unwrap()
545+
};
546+
assert_eq!(decrypted.as_slice(), seed.as_slice());
547+
548+
// First 9 wrong attempts.
549+
for i in 1..bitbox02_sys::MAX_UNLOCK_ATTEMPTS {
550+
assert!(matches!(
551+
unlock("invalid password"),
552+
Err(Error::IncorrectPassword { remaining_attempts }) if remaining_attempts
553+
== (bitbox02_sys::MAX_UNLOCK_ATTEMPTS - i) as u8
554+
));
555+
// Still seeded.
556+
assert!(crate::memory::is_seeded());
557+
// Wrong password does not lock the keystore again if already unlocked.
558+
assert!(copy_seed().is_ok());
559+
}
560+
// Last attempt, triggers reset.
561+
assert!(matches!(
562+
unlock("invalid password"),
563+
Err(Error::MaxAttemptsExceeded),
564+
));
565+
// Last wrong attempt locks & resets. There is no more seed.
566+
assert!(!crate::memory::is_seeded());
567+
assert!(copy_seed().is_err());
568+
assert!(matches!(unlock("password"), Err(Error::Unseeded)));
569+
}
570+
512571
// This tests that you can create a keystore, unlock it, and then do this again. This is an
513572
// expected workflow for when the wallet setup process is restarted after seeding and unlocking,
514573
// but before creating a backup, in which case a new seed is created.

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::*;

src/rust/bitbox02/src/testing.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ pub fn mock_memory() {
5151

5252
assert!(bitbox02_sys::memory_setup(&MEMORY_IFS));
5353

54+
if bitbox02_sys::smarteeprom_is_enabled() {
55+
bitbox02_sys::smarteeprom_disable();
56+
}
5457
bitbox02_sys::smarteeprom_bb02_config();
5558
bitbox02_sys::bitbox02_smarteeprom_init();
5659
bitbox02_sys::spi_mem_full_erase();

test/unit-test/test_keystore.c

Lines changed: 0 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,6 @@ static uint8_t _mock_bip39_seed[64] = {
5252
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
5353
};
5454

55-
static uint8_t _unstretched_retained_seed_encryption_key[32] =
56-
"\xfe\x09\x76\x01\x14\x52\xa7\x22\x12\xe4\xb8\xbd\x57\x2b\x5b\xe3\x01\x41\xa3\x56\xf1\x13\x37"
57-
"\xd2\x9d\x35\xea\x8f\xf9\x97\xbe\xfc";
58-
5955
static uint8_t _unstretched_retained_bip39_seed_encryption_key[32] =
6056
"\x9b\x44\xc7\x04\x88\x93\xfa\xaf\x6e\x2d\x76\x25\xd1\x3d\x8f\x1c\xab\x07\x65\xfd\x61\xf1\x59"
6157
"\xd9\x71\x3e\x08\x15\x5d\x06\x71\x7c";
@@ -73,10 +69,6 @@ static const uint8_t _expected_seckey[32] = {
7369
0x58, 0x92, 0x32, 0x9d, 0x67, 0xdf, 0xd4, 0xad, 0x05, 0xe9, 0xc3, 0xd0, 0x6e, 0xdf, 0x74, 0xfb,
7470
};
7571

76-
static uint8_t _expected_retained_seed_secret[32] =
77-
"\xb1\x56\xbe\x41\x65\x30\xc6\xfc\x00\x01\x88\x44\x16\x17\x74\xa3\x54\x6a\x53\xac\x6d\xd4\xa0"
78-
"\x46\x26\x08\x83\x8e\x21\x60\x08\xf7";
79-
8072
const uint8_t _expected_retained_bip39_seed_secret[32] =
8173
"\x85\x6d\x9a\x8c\x1e\xa4\x2a\x69\xae\x76\x32\x42\x44\xac\xe6\x74\x39\x7f\xf1\x36\x0a\x4b\xa4"
8274
"\xc8\x5f\xfb\xd4\x2c\xee\x8a\x7f\x29";
@@ -117,16 +109,6 @@ int __wrap_secp256k1_anti_exfil_sign(
117109
return __real_secp256k1_anti_exfil_sign(ctx, sig, msg32, seckey, host_data32, recid);
118110
}
119111

120-
/** Reset the SmartEEPROM configuration. */
121-
static void _smarteeprom_reset(void)
122-
{
123-
if (smarteeprom_is_enabled()) {
124-
smarteeprom_disable();
125-
}
126-
smarteeprom_bb02_config();
127-
bitbox02_smarteeprom_init();
128-
}
129-
130112
static bool _reset_reset_called = false;
131113
void __wrap_reset_reset(void)
132114
{
@@ -138,21 +120,13 @@ void __wrap_random_32_bytes(uint8_t* buf)
138120
memcpy(buf, (const void*)mock(), 32);
139121
}
140122

141-
static void _expect_retain_seed(void)
142-
{
143-
will_return(__wrap_random_32_bytes, _unstretched_retained_seed_encryption_key);
144-
}
145-
146123
static void _expect_retain_bip39_seed(void)
147124
{
148125
will_return(__wrap_random_32_bytes, _unstretched_retained_bip39_seed_encryption_key);
149126
}
150127

151128
void _mock_unlocked(const uint8_t* seed, size_t seed_len, const uint8_t* bip39_seed)
152129
{
153-
if (seed != NULL) {
154-
_expect_retain_seed();
155-
}
156130
if (bip39_seed != NULL) {
157131
_expect_retain_bip39_seed();
158132
}
@@ -262,86 +236,6 @@ static void _expect_encrypt_and_store_seed(void)
262236
will_return(__wrap_memory_is_initialized, false);
263237
}
264238

265-
static void _expect_seeded(bool seeded)
266-
{
267-
uint8_t seed[KEYSTORE_MAX_SEED_LENGTH];
268-
size_t len;
269-
assert_int_equal(seeded, keystore_copy_seed(seed, &len));
270-
if (seeded) {
271-
assert_memory_equal(seed, _mock_seed, sizeof(_mock_seed));
272-
// Also check that the retained seed was encrypted with the expected encryption key.
273-
size_t encrypted_len = 0;
274-
const uint8_t* retained_seed_encrypted =
275-
keystore_test_get_retained_seed_encrypted(&encrypted_len);
276-
size_t decrypted_len = encrypted_len - 48;
277-
uint8_t out[decrypted_len];
278-
assert_true(cipher_aes_hmac_decrypt(
279-
retained_seed_encrypted,
280-
encrypted_len,
281-
out,
282-
&decrypted_len,
283-
_expected_retained_seed_secret));
284-
assert_int_equal(decrypted_len, 32);
285-
assert_memory_equal(out, _mock_seed, decrypted_len);
286-
}
287-
}
288-
289-
static void _perform_some_unlocks(void)
290-
{
291-
uint8_t remaining_attempts;
292-
// Loop to check that unlocking unlocked works while unlocked.
293-
for (int i = 0; i < 3; i++) {
294-
_reset_reset_called = false;
295-
will_return(__wrap_memory_is_seeded, true);
296-
if (i == 0) {
297-
_expect_retain_seed();
298-
}
299-
assert_int_equal(KEYSTORE_OK, keystore_unlock(PASSWORD, &remaining_attempts, NULL));
300-
assert_int_equal(remaining_attempts, MAX_UNLOCK_ATTEMPTS);
301-
assert_false(_reset_reset_called);
302-
_expect_seeded(true);
303-
}
304-
}
305-
306-
static void _test_keystore_unlock(void** state)
307-
{
308-
_smarteeprom_reset();
309-
_mock_unlocked(NULL, 0, NULL); // reset to locked
310-
311-
uint8_t remaining_attempts;
312-
313-
will_return(__wrap_memory_is_seeded, false);
314-
assert_int_equal(KEYSTORE_ERR_UNSEEDED, keystore_unlock(PASSWORD, &remaining_attempts, NULL));
315-
_expect_encrypt_and_store_seed();
316-
assert_int_equal(keystore_encrypt_and_store_seed(_mock_seed, 32, PASSWORD), KEYSTORE_OK);
317-
_expect_seeded(false);
318-
319-
_perform_some_unlocks();
320-
321-
// Invalid passwords until we run out of attempts.
322-
for (int i = 1; i <= MAX_UNLOCK_ATTEMPTS; i++) {
323-
_reset_reset_called = false;
324-
will_return(__wrap_memory_is_seeded, true);
325-
assert_int_equal(
326-
i >= MAX_UNLOCK_ATTEMPTS ? KEYSTORE_ERR_MAX_ATTEMPTS_EXCEEDED
327-
: KEYSTORE_ERR_INCORRECT_PASSWORD,
328-
keystore_unlock("invalid password", &remaining_attempts, NULL));
329-
assert_int_equal(remaining_attempts, MAX_UNLOCK_ATTEMPTS - i);
330-
// Wrong password does not lock the keystore again if already unlocked.
331-
_expect_seeded(true);
332-
// reset_reset() called in last attempt
333-
assert_int_equal(i == MAX_UNLOCK_ATTEMPTS, _reset_reset_called);
334-
}
335-
336-
// Trying again after max attempts is blocked immediately.
337-
_reset_reset_called = false;
338-
will_return(__wrap_memory_is_seeded, true);
339-
assert_int_equal(
340-
KEYSTORE_ERR_MAX_ATTEMPTS_EXCEEDED, keystore_unlock(PASSWORD, &remaining_attempts, NULL));
341-
assert_int_equal(remaining_attempts, 0);
342-
assert_true(_reset_reset_called);
343-
}
344-
345239
static void _test_keystore_unlock_bip39(void** state)
346240
{
347241
keystore_lock();
@@ -567,7 +461,6 @@ int main(void)
567461
const struct CMUnitTest tests[] = {
568462
cmocka_unit_test(_test_keystore_secp256k1_nonce_commit),
569463
cmocka_unit_test(_test_keystore_secp256k1_sign),
570-
cmocka_unit_test(_test_keystore_unlock),
571464
cmocka_unit_test(_test_keystore_unlock_bip39),
572465
cmocka_unit_test(_test_keystore_lock),
573466
cmocka_unit_test(_test_keystore_bip39_mnemonic_from_seed),

0 commit comments

Comments
 (0)