From 00cf5233d0f196f8c845dad910eb8b085330ba8f Mon Sep 17 00:00:00 2001 From: benma's agent Date: Fri, 17 Oct 2025 22:08:10 +0200 Subject: [PATCH 1/4] rust: expose memory_get_salt_root to Rust --- src/rust/Cargo.lock | 1 + src/rust/bitbox02-sys/build.rs | 1 + src/rust/bitbox02/Cargo.toml | 1 + src/rust/bitbox02/src/memory.rs | 32 +++++++++++++++++++++++++++++++- 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index 08fa1bae23..3e9e57d5e3 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -107,6 +107,7 @@ dependencies = [ "bitbox02-sys", "bitcoin", "hex", + "hex_lit", "util", "zeroize", ] diff --git a/src/rust/bitbox02-sys/build.rs b/src/rust/bitbox02-sys/build.rs index 78c7597d9d..9b52cd7d0a 100644 --- a/src/rust/bitbox02-sys/build.rs +++ b/src/rust/bitbox02-sys/build.rs @@ -99,6 +99,7 @@ const ALLOWLIST_FNS: &[&str] = &[ "memory_is_initialized", "memory_is_mnemonic_passphrase_enabled", "memory_is_seeded", + "memory_get_salt_root", "memory_multisig_get_by_hash", "memory_multisig_set_by_hash", "memory_set_device_name", diff --git a/src/rust/bitbox02/Cargo.toml b/src/rust/bitbox02/Cargo.toml index b99296fd12..25f87d0b3c 100644 --- a/src/rust/bitbox02/Cargo.toml +++ b/src/rust/bitbox02/Cargo.toml @@ -33,6 +33,7 @@ hex = { workspace = true } hex = { workspace = true } bitbox-aes = { path = "../bitbox-aes" } bitbox02-rust = { path = "../bitbox02-rust" } +hex_lit = { workspace = true } [features] # Only to be enabled in unit tests and simulators diff --git a/src/rust/bitbox02/src/memory.rs b/src/rust/bitbox02/src/memory.rs index e3b8ee1ae0..fa21ab8b06 100644 --- a/src/rust/bitbox02/src/memory.rs +++ b/src/rust/bitbox02/src/memory.rs @@ -15,6 +15,7 @@ extern crate alloc; use alloc::string::String; +use alloc::vec::Vec; // deduct one for the null terminator. pub const DEVICE_NAME_MAX_LEN: usize = bitbox02_sys::MEMORY_DEVICE_NAME_MAX_LEN as usize - 1; @@ -232,6 +233,15 @@ pub fn ble_enable(enable: bool) -> Result<(), ()> { if res { Ok(()) } else { Err(()) } } +pub fn get_salt_root() -> Result>, ()> { + let mut salt_root = zeroize::Zeroizing::new(vec![0u8; 32]); + if unsafe { bitbox02_sys::memory_get_salt_root(salt_root.as_mut_ptr()) } { + Ok(salt_root) + } else { + Err(()) + } +} + #[cfg(feature = "testing")] pub fn set_salt_root(salt_root: &[u8; 32]) -> Result<(), ()> { match unsafe { bitbox02_sys::memory_set_salt_root(salt_root.as_ptr()) } { @@ -244,9 +254,29 @@ pub fn set_salt_root(salt_root: &[u8; 32]) -> Result<(), ()> { mod tests { use super::*; + use hex_lit::hex; + #[test] fn test_get_attestation_bootloader_hash() { - let expected: [u8; 32] = *b"\x71\x3d\xf0\xd5\x8c\x71\x7d\x40\x31\x78\x7c\xdc\x8f\xa3\x5b\x90\x25\x82\xbe\x6a\xb6\xa2\x2e\x09\xde\x44\x77\xd3\x0e\x22\x30\xfc"; + let expected: [u8; 32] = + hex!("713df0d58c717d4031787cdc8fa35b902582be6ab6a22e09de4477d30e2230fc"); assert_eq!(get_attestation_bootloader_hash(), expected); } + + #[test] + fn test_get_salt_root_roundtrip() { + let original = get_salt_root().unwrap(); + + let expected = hex!("00112233445566778899aabbccddeefffeeddccbbaa998877665544332211000"); + + set_salt_root(expected.as_slice().try_into().unwrap()).unwrap(); + let salt_root = get_salt_root().unwrap(); + assert_eq!(salt_root.as_slice(), &expected); + + let erased = [0xffu8; 32]; + set_salt_root(&erased).unwrap(); + assert!(get_salt_root().is_err()); + + set_salt_root(original.as_slice().try_into().unwrap()).unwrap(); + } } From c3aad65b5091a8550082e4fe4cd0f6203b119266 Mon Sep 17 00:00:00 2001 From: benma's agent Date: Fri, 17 Oct 2025 22:36:37 +0200 Subject: [PATCH 2/4] port salt.c to Rust test_salt.c unit tests is replicated in Rust. The C function body calls the Rust function so we can also verify the C unit tests still pass (to be removed in the next commit). --- src/rust/bitbox02-rust/src/lib.rs | 1 + src/rust/bitbox02-rust/src/salt.rs | 131 +++++++++++++++++++++++++++++ src/salt.c | 22 +---- 3 files changed, 135 insertions(+), 19 deletions(-) create mode 100644 src/rust/bitbox02-rust/src/salt.rs diff --git a/src/rust/bitbox02-rust/src/lib.rs b/src/rust/bitbox02-rust/src/lib.rs index 479d0af4d6..2a685974a8 100644 --- a/src/rust/bitbox02-rust/src/lib.rs +++ b/src/rust/bitbox02-rust/src/lib.rs @@ -36,6 +36,7 @@ pub mod hal; pub mod hash; pub mod hww; pub mod keystore; +pub mod salt; pub mod secp256k1; #[cfg(feature = "app-u2f")] mod u2f; diff --git a/src/rust/bitbox02-rust/src/salt.rs b/src/rust/bitbox02-rust/src/salt.rs new file mode 100644 index 0000000000..6166481f13 --- /dev/null +++ b/src/rust/bitbox02-rust/src/salt.rs @@ -0,0 +1,131 @@ +// Copyright 2025 Shift Crypto AG +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use alloc::vec::Vec; +use core::ffi::c_char; + +use bitbox02::memory; +use sha2::Digest; +use util::bytes::{Bytes, BytesMut}; +use zeroize::Zeroizing; + +/// Creates `SHA256(salt_root || purpose || data)`, where `salt_root` is a persisted value that +/// remains unchanged until the device is reset. The `purpose` string namespaces individual uses of +/// the salt, and the provided `data` slice is hashed alongside it. +/// +/// Returns `Err(())` if the salt root cannot be retrieved from persistent storage. +pub fn hash_data(data: &[u8], purpose: &str) -> Result>, ()> { + let salt_root = memory::get_salt_root()?; + + let mut hasher = sha2::Sha256::new(); + hasher.update(salt_root.as_slice()); + hasher.update(purpose.as_bytes()); + hasher.update(data); + + Ok(Zeroizing::new(hasher.finalize().to_vec())) +} + +/// # Safety +/// +/// `purpose` must be a valid, null-terminated UTF-8 string pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rust_salt_hash_data( + data: Bytes, + purpose: *const c_char, + mut hash_out: BytesMut, +) -> bool { + let purpose_str = match unsafe { bitbox02::util::str_from_null_terminated_ptr(purpose) } { + Ok(purpose) => purpose, + Err(()) => return false, + }; + match hash_data(data.as_ref(), purpose_str) { + Ok(hash) => { + hash_out.as_mut()[..32].copy_from_slice(&hash); + true + } + Err(()) => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bitbox02::testing::mock_memory; + use core::convert::TryInto; + use core::ptr; + use hex_lit::hex; + + const MOCK_SALT_ROOT: [u8; 32] = + hex!("0000000000000000111111111111111122222222222222223333333333333333"); + + #[test] + fn test_hash_data() { + mock_memory(); + memory::set_salt_root(&MOCK_SALT_ROOT).unwrap(); + + let data = hex!("001122334455667788"); + let expected = hex!("62db8dcd47ddf8e81809c377ed96643855d3052bb73237100ca81f0f5a7611e6"); + + let hash = hash_data(&data, "test purpose").unwrap(); + assert_eq!(hash.as_slice(), &expected); + } + + #[test] + fn test_hash_data_empty_inputs() { + mock_memory(); + memory::set_salt_root(&MOCK_SALT_ROOT).unwrap(); + + let expected = hex!("2dbb05dd73d94edba6946611aaca367f76c809e96f20499ad674e596050f9833"); + + let hash = hash_data(&[], "").unwrap(); + assert_eq!(hash.as_slice(), &expected); + } + + #[test] + fn test_rust_salt_hash_data() { + mock_memory(); + memory::set_salt_root(&MOCK_SALT_ROOT).unwrap(); + + let data = hex!("001122334455667788"); + let expected = hex!("62db8dcd47ddf8e81809c377ed96643855d3052bb73237100ca81f0f5a7611e6"); + + let mut hash_out = [0u8; 32]; + let purpose = c"test purpose"; + assert!(unsafe { + rust_salt_hash_data( + util::bytes::rust_util_bytes(data.as_ptr(), data.len()), + purpose.as_ptr(), + util::bytes::rust_util_bytes_mut(hash_out.as_mut_ptr(), hash_out.len()), + ) + }); + assert_eq!(hash_out, expected); + } + + #[test] + fn test_rust_salt_hash_data_empty_inputs() { + mock_memory(); + memory::set_salt_root(&MOCK_SALT_ROOT).unwrap(); + + let expected = hex!("2dbb05dd73d94edba6946611aaca367f76c809e96f20499ad674e596050f9833"); + let mut hash_out = [0u8; 32]; + assert!(unsafe { + rust_salt_hash_data( + util::bytes::rust_util_bytes(ptr::null(), 0), + c"".as_ptr(), + util::bytes::rust_util_bytes_mut(hash_out.as_mut_ptr(), hash_out.len()), + ) + }); + assert_eq!(hash_out, expected); + } +} diff --git a/src/salt.c b/src/salt.c index 2570cf46d0..912c6c38b2 100644 --- a/src/salt.c +++ b/src/salt.c @@ -21,25 +21,9 @@ bool salt_hash_data(const uint8_t* data, size_t data_len, const char* purpose, uint8_t* hash_out) { - if (data_len > 0 && data == NULL) { + if ((data_len > 0 && data == NULL) || purpose == NULL || hash_out == NULL) { return false; } - if (!purpose || !hash_out) { - return false; - } - - uint8_t salt_root[32]; - UTIL_CLEANUP_32(salt_root); - if (!memory_get_salt_root(salt_root)) { - return false; - } - - void* ctx = rust_sha256_new(); - rust_sha256_update(ctx, salt_root, sizeof(salt_root)); - rust_sha256_update(ctx, purpose, strlen(purpose)); - if (data != NULL) { - rust_sha256_update(ctx, data, data_len); - } - rust_sha256_finish(&ctx, hash_out); - return true; + return rust_salt_hash_data( + rust_util_bytes(data, data_len), purpose, rust_util_bytes_mut(hash_out, 32)); } From 76079fba45b263c10b34b0c5f1ee950dc427441e Mon Sep 17 00:00:00 2001 From: benma's agent Date: Fri, 17 Oct 2025 22:59:10 +0200 Subject: [PATCH 3/4] Remove C salt unit test now covered by Rust --- test/unit-test/CMakeLists.txt | 2 -- test/unit-test/test_salt.c | 64 ----------------------------------- 2 files changed, 66 deletions(-) delete mode 100644 test/unit-test/test_salt.c diff --git a/test/unit-test/CMakeLists.txt b/test/unit-test/CMakeLists.txt index efda070a3c..569b4bea22 100644 --- a/test/unit-test/CMakeLists.txt +++ b/test/unit-test/CMakeLists.txt @@ -66,8 +66,6 @@ set(TEST_LIST "-Wl,--wrap=memory_read_chunk_fake,--wrap=memory_write_chunk_fake,--wrap=rust_noise_generate_static_private_key,--wrap=memory_read_shared_bootdata_fake,--wrap=memory_write_to_address_fake,--wrap=random_32_bytes_mcu" memory_functional "" - salt - "-Wl,--wrap=memory_get_salt_root" cipher "-Wl,--wrap=cipher_fake_iv" util diff --git a/test/unit-test/test_salt.c b/test/unit-test/test_salt.c deleted file mode 100644 index fd5260944f..0000000000 --- a/test/unit-test/test_salt.c +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2019 Shift Cryptosecurity AG -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include -#include -#include -#include - -#include - -#include -#include - -static void _test_salt_hash_data(void** state) -{ - uint8_t data[9] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}; - uint8_t hash[32]; - assert_true(salt_hash_data(data, sizeof(data), "test purpose", hash)); - uint8_t expected_result[32] = { - 0x62, 0xdb, 0x8d, 0xcd, 0x47, 0xdd, 0xf8, 0xe8, 0x18, 0x09, 0xc3, - 0x77, 0xed, 0x96, 0x64, 0x38, 0x55, 0xd3, 0x05, 0x2b, 0xb7, 0x32, - 0x37, 0x10, 0x0c, 0xa8, 0x1f, 0x0f, 0x5a, 0x76, 0x11, 0xe6, - }; - assert_memory_equal(hash, expected_result, 32); -} - -static void _test_salt_hash_data_empty(void** state) -{ - const char* data = ""; - uint8_t hash[32]; - uint8_t expected_result[32] = { - 0x2d, 0xbb, 0x05, 0xdd, 0x73, 0xd9, 0x4e, 0xdb, 0xa6, 0x94, 0x66, - 0x11, 0xaa, 0xca, 0x36, 0x7f, 0x76, 0xc8, 0x09, 0xe9, 0x6f, 0x20, - 0x49, 0x9a, 0xd6, 0x74, 0xe5, 0x96, 0x05, 0x0f, 0x98, 0x33, - }; - - assert_true(salt_hash_data((const uint8_t*)data, 0, "", hash)); - assert_memory_equal(hash, expected_result, 32); - - assert_true(salt_hash_data(NULL, 0, "", hash)); - assert_memory_equal(hash, expected_result, 32); - - assert_false(salt_hash_data(NULL, 1, "", hash)); -} - -int main(void) -{ - const struct CMUnitTest tests[] = { - cmocka_unit_test(_test_salt_hash_data), - cmocka_unit_test(_test_salt_hash_data_empty), - }; - return cmocka_run_group_tests(tests, NULL, NULL); -} From cfa4c4d0c424d818f62a3b4595ca8477cc127071 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Fri, 17 Oct 2025 23:18:58 +0200 Subject: [PATCH 4/4] test: remove unused mock_memory.c --- test/unit-test/CMakeLists.txt | 1 - test/unit-test/framework/src/mock_memory.c | 76 ---------------------- 2 files changed, 77 deletions(-) delete mode 100644 test/unit-test/framework/src/mock_memory.c diff --git a/test/unit-test/CMakeLists.txt b/test/unit-test/CMakeLists.txt index 569b4bea22..1cebf04a3c 100644 --- a/test/unit-test/CMakeLists.txt +++ b/test/unit-test/CMakeLists.txt @@ -34,7 +34,6 @@ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-unused-parameter -Wno-missing-prototype add_library(mocks STATIC EXCLUDE_FROM_ALL framework/src/mock_gestures.c framework/src/mock_screen_stack.c - framework/src/mock_memory.c framework/src/mock_qtouch.c ) target_link_libraries(mocks PUBLIC c-unit-tests_rust_c ${CMOCKA_LDFLAGS}) diff --git a/test/unit-test/framework/src/mock_memory.c b/test/unit-test/framework/src/mock_memory.c deleted file mode 100644 index fc23ca262e..0000000000 --- a/test/unit-test/framework/src/mock_memory.c +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2019 Shift Cryptosecurity AG -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -bool __wrap_memory_is_initialized(void) -{ - return mock(); -} - -bool __wrap_memory_is_seeded(void) -{ - return mock(); -} - -static uint8_t _encrypted_seed_and_hmac[96]; -static uint8_t _encrypted_seed_and_hmac_len; - -bool __wrap_memory_set_encrypted_seed_and_hmac(uint8_t* encrypted_seed_and_hmac, uint8_t len) -{ - memcpy(_encrypted_seed_and_hmac, encrypted_seed_and_hmac, len); - _encrypted_seed_and_hmac_len = len; - return true; -} - -bool __wrap_memory_get_encrypted_seed_and_hmac( - uint8_t* encrypted_seed_and_hmac_out, - uint8_t* len_out) -{ - *len_out = _encrypted_seed_and_hmac_len; - memcpy(encrypted_seed_and_hmac_out, _encrypted_seed_and_hmac, *len_out); - return true; -} - -void __wrap_memory_get_device_name(char* name_out) -{ - snprintf(name_out, MEMORY_DEVICE_NAME_MAX_LEN, "%s", (const char*)mock()); -} - -bool __wrap_memory_set_device_name(const char* name) -{ - return mock(); -} - -bool __wrap_memory_set_mnemonic_passphrase_enabled(bool enabled) -{ - check_expected(enabled); - return mock(); -} - -bool __wrap_memory_get_salt_root(uint8_t* salt_root_out) -{ - memcpy(salt_root_out, fake_memory_get_salt_root(), 32); - return true; -}