From 424a68a3a9038b3ca0317bccf07fcadce6d5e49f Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Tue, 19 Mar 2024 17:48:25 +0100 Subject: [PATCH] memory: persist attestation bootloader hash And use it in the attestation check. `bootloader_hash_set` is added just in case the memory is unexpectedly not `0xFF` everywhere (the default value for unused/erased memory). --- src/CMakeLists.txt | 1 + src/factorysetup.c | 4 ++ src/memory/memory.c | 60 ++++++++++++++++++- src/memory/memory.h | 11 ++++ src/rust/bitbox02-rust/src/attestation.rs | 2 +- src/rust/bitbox02/src/memory.rs | 25 ++++++-- test/simulator/framework/mock_memory.c | 23 +++++-- .../framework/includes/mock_memory.h | 2 + test/unit-test/framework/mock_memory.c | 26 ++++++-- test/unit-test/test_memory.c | 4 +- test/unit-test/test_memory_functional.c | 49 ++++++++++++++- 11 files changed, 183 insertions(+), 24 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5a30677fe..52d433d50 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -298,6 +298,7 @@ add_custom_target(rust-bindgen --allowlist-function memory_is_seeded --allowlist-function memory_is_mnemonic_passphrase_enabled --allowlist-function memory_get_attestation_pubkey_and_certificate + --allowlist-function memory_get_attestation_bootloader_hash --allowlist-function memory_bootloader_hash --allowlist-function memory_get_noise_static_private_key --allowlist-function memory_check_noise_remote_static_pubkey diff --git a/src/factorysetup.c b/src/factorysetup.c index 5cc5b215c..9e0fe30d1 100644 --- a/src/factorysetup.c +++ b/src/factorysetup.c @@ -147,6 +147,10 @@ static void _api_msg(const Packet* in_packet, Packet* out_packet, const size_t m switch (input[0]) { case OP_BOOTLOADER_HASH: memory_bootloader_hash(output + 2); + // This is the hash that will be used for the attestation, persist for later use. + if (!memory_set_attestation_bootloader_hash()) { + screen_print_debug("setting attestation bootloader hash failed", 0); + } out_len = 2 + 32; break; case OP_GENKEY: { diff --git a/src/memory/memory.c b/src/memory/memory.c index 09a60afad..8dc9e9495 100644 --- a/src/memory/memory.c +++ b/src/memory/memory.c @@ -120,6 +120,26 @@ typedef union { static_assert(sizeof(((chunk_2_t*)0)->fields) <= (size_t)CHUNK_SIZE, "chunk too large"); +#if FLASH_APPDATA_LEN / CHUNK_SIZE != 8 +#error \ + "We expect 8 chunks in app data. This check is to ensure that chunk_7_t below is the last chunk, so it is not erased during reset." +#endif + +// CHUNK_7: A chunk that survives device resets (is not erased during `memory_reset_hww()`). +#define CHUNK_7_PERMANENT (7) +typedef union { + struct __attribute__((__packed__)) { + // Hash of bootloader used in the attestation sighash. + // If not set, the actual bootloader area is hashed. + secbool_u8 attestation_bootloader_hash_set; + uint8_t reserved[3]; + uint8_t attestation_bootloader_hash[32]; + } fields; + uint8_t bytes[CHUNK_SIZE]; +} chunk_7_t; + +static_assert(sizeof(((chunk_7_t*)0)->fields) <= (size_t)CHUNK_SIZE, "chunk too large"); + #pragma GCC diagnostic pop #define BITMASK_SEEDED ((uint8_t)(1u << 0u)) @@ -330,8 +350,8 @@ bool memory_cleanup_smarteeprom(void) bool memory_reset_hww(void) { - // Erase all app data chunks expect the first one, which is permanent. - for (uint32_t chunk = CHUNK_1; chunk < FLASH_APPDATA_LEN / CHUNK_SIZE; chunk++) { + // Erase all app data chunks expect the first and the last one, which is permanent. + for (uint32_t chunk = CHUNK_1; chunk < (FLASH_APPDATA_LEN / CHUNK_SIZE) - 1; chunk++) { if (!_write_chunk(chunk, NULL)) { return false; } @@ -557,6 +577,38 @@ static bool _is_attestation_setup_done(void) return !MEMEQ(chunk.fields.attestation.certificate, empty, 64); } +bool memory_set_attestation_bootloader_hash(void) +{ + chunk_7_t chunk = {0}; + CLEANUP_CHUNK(chunk); + _read_chunk(CHUNK_7_PERMANENT, chunk_bytes); + uint8_t empty[32]; + memset(empty, 0xff, sizeof(empty)); + if (chunk.fields.attestation_bootloader_hash_set != sectrue_u8 || + MEMEQ(chunk.fields.attestation_bootloader_hash, empty, sizeof(empty))) { + chunk.fields.attestation_bootloader_hash_set = sectrue_u8; + memory_bootloader_hash(chunk.fields.attestation_bootloader_hash); + return _write_chunk(CHUNK_7_PERMANENT, chunk.bytes); + } + + return true; +} + +void memory_get_attestation_bootloader_hash(uint8_t* hash_out) +{ + chunk_7_t chunk = {0}; + CLEANUP_CHUNK(chunk); + _read_chunk(CHUNK_7_PERMANENT, chunk_bytes); + uint8_t empty[32]; + memset(empty, 0xff, sizeof(empty)); + if (chunk.fields.attestation_bootloader_hash_set != sectrue_u8 || + MEMEQ(chunk.fields.attestation_bootloader_hash, empty, sizeof(empty))) { + memory_bootloader_hash(hash_out); + return; + } + memcpy(hash_out, chunk.fields.attestation_bootloader_hash, 32); +} + bool memory_set_attestation_device_pubkey(const uint8_t* attestation_device_pubkey) { chunk_0_t chunk = {0}; @@ -610,9 +662,13 @@ bool memory_get_attestation_pubkey_and_certificate( void memory_bootloader_hash(uint8_t* hash_out) { +#ifdef TESTING + memory_bootloader_hash_mock(hash_out); +#else uint8_t* bootloader = FLASH_BOOT_START; size_t len = FLASH_BOOT_LEN - 32; // 32 bytes are random rust_sha256(bootloader, len, hash_out); +#endif } bool memory_bootloader_set_flags(auto_enter_t auto_enter, upside_down_t upside_down) diff --git a/src/memory/memory.h b/src/memory/memory.h index 5f36b9ac1..3146b29c4 100644 --- a/src/memory/memory.h +++ b/src/memory/memory.h @@ -146,6 +146,17 @@ void memory_get_io_protection_key(uint8_t* key_out); void memory_get_authorization_key(uint8_t* key_out); void memory_get_encryption_key(uint8_t* key_out); +/** + * Persists the current bootloader hash that, which is part of the attestation sighash. + */ +USE_RESULT bool memory_set_attestation_bootloader_hash(void); + +/** + * Retreives the bootloader hash that is part of the attestation sighash. + * @param[out] hash_out must be 32 bytes and will contain the result. + */ +void memory_get_attestation_bootloader_hash(uint8_t* hash_out); + /** * Persists the given attestation device pubkey. * @param[in] attestation_device_pubkey P256 NIST ECC pubkey (X and Y coordinates). diff --git a/src/rust/bitbox02-rust/src/attestation.rs b/src/rust/bitbox02-rust/src/attestation.rs index 9a83a4977..abeb39e28 100644 --- a/src/rust/bitbox02-rust/src/attestation.rs +++ b/src/rust/bitbox02-rust/src/attestation.rs @@ -36,7 +36,7 @@ pub fn perform(host_challenge: [u8; 32]) -> Result { &mut result.root_pubkey_identifier, )?; let hash: [u8; 32] = Sha256::digest(host_challenge).into(); - bitbox02::memory::bootloader_hash(&mut result.bootloader_hash); + result.bootloader_hash = bitbox02::memory::get_attestation_bootloader_hash(); bitbox02::securechip::attestation_sign(&hash, &mut result.challenge_signature)?; Ok(result) } diff --git a/src/rust/bitbox02/src/memory.rs b/src/rust/bitbox02/src/memory.rs index fab04baed..2a43281b5 100644 --- a/src/rust/bitbox02/src/memory.rs +++ b/src/rust/bitbox02/src/memory.rs @@ -65,6 +65,14 @@ pub fn is_mnemonic_passphrase_enabled() -> bool { unsafe { bitbox02_sys::memory_is_mnemonic_passphrase_enabled() } } +pub fn get_attestation_bootloader_hash() -> [u8; 32] { + let mut hash = [0u8; 32]; + unsafe { + bitbox02_sys::memory_get_attestation_bootloader_hash(hash.as_mut_ptr()); + } + hash +} + pub fn get_attestation_pubkey_and_certificate( device_pubkey: &mut [u8; 64], certificate: &mut [u8; 64], @@ -82,12 +90,6 @@ pub fn get_attestation_pubkey_and_certificate( } } -pub fn bootloader_hash(out: &mut [u8; 32]) { - unsafe { - bitbox02_sys::memory_bootloader_hash(out.as_mut_ptr()); - } -} - pub fn get_noise_static_private_key() -> Result, ()> { let mut out = zeroize::Zeroizing::new([0u8; 32]); match unsafe { bitbox02_sys::memory_get_noise_static_private_key(out.as_mut_ptr()) } { @@ -157,3 +159,14 @@ pub fn multisig_get_by_hash(hash: &[u8]) -> Option { false => None, } } + +#[cfg(test)] +mod tests { + use super::*; + + #[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"; + assert_eq!(get_attestation_bootloader_hash(), expected); + } +} diff --git a/test/simulator/framework/mock_memory.c b/test/simulator/framework/mock_memory.c index 63e14bc4c..2395ee291 100644 --- a/test/simulator/framework/mock_memory.c +++ b/test/simulator/framework/mock_memory.c @@ -51,24 +51,35 @@ static uint8_t* _get_memory(uint32_t base) bool memory_write_to_address_mock(uint32_t base, uint32_t addr, const uint8_t* chunk) { if (chunk == NULL) { - memset(_get_memory(base) + addr, 0xff, CHUNK_SIZE); + memset(_get_memory(base) + addr, 0xff, (size_t)CHUNK_SIZE); } else { - memcpy(_get_memory(base) + addr, chunk, CHUNK_SIZE); + memcpy(_get_memory(base) + addr, chunk, (size_t)CHUNK_SIZE); } return true; } bool memory_write_chunk_mock(uint32_t chunk_num, const uint8_t* chunk) { - return memory_write_to_address_mock(FLASH_APPDATA_START, chunk_num * CHUNK_SIZE, chunk); + return memory_write_to_address_mock(FLASH_APPDATA_START, chunk_num * (size_t)CHUNK_SIZE, chunk); } void memory_read_chunk_mock(uint32_t chunk_num, uint8_t* chunk_out) { - memcpy(chunk_out, _memory_app_data + chunk_num * CHUNK_SIZE, CHUNK_SIZE); + memcpy(chunk_out, _memory_app_data + chunk_num * (size_t)CHUNK_SIZE, (size_t)CHUNK_SIZE); } void memory_read_shared_bootdata_mock(uint8_t* chunk_out) { - memcpy(chunk_out, _memory_shared_data, FLASH_SHARED_DATA_LEN); -} \ No newline at end of file + memcpy(chunk_out, _memory_shared_data, (size_t)FLASH_SHARED_DATA_LEN); +} + +// Arbitrary value. +static uint8_t _bootloader_hash[32] = + "\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"; + +void memory_bootloader_hash_mock(uint8_t* hash_out) +{ + // NOLINTNEXTLINE(bugprone-not-null-terminated-result) + memcpy(hash_out, _bootloader_hash, 32); +} diff --git a/test/unit-test/framework/includes/mock_memory.h b/test/unit-test/framework/includes/mock_memory.h index 51e677066..b050aadcb 100644 --- a/test/unit-test/framework/includes/mock_memory.h +++ b/test/unit-test/framework/includes/mock_memory.h @@ -27,4 +27,6 @@ void memory_read_chunk_mock(uint32_t chunk_num, uint8_t* chunk_out); // Size: `FLASH_SHARED_DATA_LEN`. void memory_read_shared_bootdata_mock(uint8_t* chunk_out); void mock_memory_set_salt_root(const uint8_t* salt_root); +void memory_bootloader_hash_mock(uint8_t* hash_out); +void memory_set_bootloader_hash_mock(const uint8_t* mock_hash); #endif diff --git a/test/unit-test/framework/mock_memory.c b/test/unit-test/framework/mock_memory.c index cc9ac1f93..559c6f9d8 100644 --- a/test/unit-test/framework/mock_memory.c +++ b/test/unit-test/framework/mock_memory.c @@ -52,26 +52,26 @@ static uint8_t* _get_memory(uint32_t base) bool memory_write_to_address_mock(uint32_t base, uint32_t addr, const uint8_t* chunk) { if (chunk == NULL) { - memset(_get_memory(base) + addr, 0xff, CHUNK_SIZE); + memset(_get_memory(base) + addr, 0xff, (size_t)CHUNK_SIZE); } else { - memcpy(_get_memory(base) + addr, chunk, CHUNK_SIZE); + memcpy(_get_memory(base) + addr, chunk, (size_t)CHUNK_SIZE); } return true; } bool memory_write_chunk_mock(uint32_t chunk_num, const uint8_t* chunk) { - return memory_write_to_address_mock(FLASH_APPDATA_START, chunk_num * CHUNK_SIZE, chunk); + return memory_write_to_address_mock(FLASH_APPDATA_START, chunk_num * (size_t)CHUNK_SIZE, chunk); } void memory_read_chunk_mock(uint32_t chunk_num, uint8_t* chunk_out) { - memcpy(chunk_out, _memory_app_data + chunk_num * CHUNK_SIZE, CHUNK_SIZE); + memcpy(chunk_out, _memory_app_data + chunk_num * (size_t)CHUNK_SIZE, (size_t)CHUNK_SIZE); } void memory_read_shared_bootdata_mock(uint8_t* chunk_out) { - memcpy(chunk_out, _memory_shared_data, FLASH_SHARED_DATA_LEN); + memcpy(chunk_out, _memory_shared_data, (size_t)FLASH_SHARED_DATA_LEN); } bool __wrap_memory_is_initialized(void) @@ -132,3 +132,19 @@ bool __wrap_memory_get_salt_root(uint8_t* salt_root_out) memcpy(salt_root_out, _salt_root, 32); return true; } + +// Arbitrary value. +static uint8_t _bootloader_hash[32] = + "\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"; + +void memory_bootloader_hash_mock(uint8_t* hash_out) +{ + memcpy(hash_out, _bootloader_hash, 32); +} + +void memory_set_bootloader_hash_mock(const uint8_t* mock_hash) +{ + // NOLINTNEXTLINE(bugprone-not-null-terminated-result) + memcpy(_bootloader_hash, mock_hash, sizeof(_bootloader_hash)); +} diff --git a/test/unit-test/test_memory.c b/test/unit-test/test_memory.c index e53ef9452..bc4299df5 100644 --- a/test/unit-test/test_memory.c +++ b/test/unit-test/test_memory.c @@ -114,8 +114,8 @@ static const memory_interface_functions_t _ifs = { static void _expect_reset(uint8_t* empty_chunk1, uint8_t* empty_chunk2) { - // Reset all - for (uint32_t write_calls = 0; write_calls < FLASH_APP_DATA_LEN / CHUNK_SIZE - 1; + // Reset all except first and last chunk. + for (uint32_t write_calls = 0; write_calls < FLASH_APP_DATA_LEN / CHUNK_SIZE - 2; write_calls++) { expect_value(__wrap_memory_write_chunk_mock, chunk_num, write_calls + 1); expect_value(__wrap_memory_write_chunk_mock, chunk, NULL); diff --git a/test/unit-test/test_memory_functional.c b/test/unit-test/test_memory_functional.c index 206091051..92072ed0c 100644 --- a/test/unit-test/test_memory_functional.c +++ b/test/unit-test/test_memory_functional.c @@ -19,6 +19,7 @@ #include #include +#include static void _test_memory_multisig(void** state) { @@ -141,7 +142,7 @@ static void _test_memory_attestation(void** state) root_pubkey_identifier, expected_root_pubkey_identifier, sizeof(root_pubkey_identifier)); } -void _memory_setup_rand_mock(uint8_t* buf_out) +void _memory_setup_rand_mock_test_functional(uint8_t* buf_out) { static uint8_t ctr = 0; static uint8_t fixtures[][32] = { @@ -177,7 +178,7 @@ static void _test_functional(void** state) mock_memory_factoryreset(); memory_interface_functions_t ifs = { - .random_32_bytes = _memory_setup_rand_mock, + .random_32_bytes = _memory_setup_rand_mock_test_functional, }; assert_true(memory_setup(&ifs)); @@ -214,6 +215,49 @@ static void _test_functional(void** state) assert_memory_equal(encryption_key, expected_encryption_key, sizeof(encryption_key)); } +static void _test_attestation_bootloader_hash(void** state) +{ + mock_memory_factoryreset(); + + memory_interface_functions_t ifs = { + .random_32_bytes = random_32_bytes_mcu, + }; + assert_true(memory_setup(&ifs)); + + const uint8_t mock1[32] = + "\x03\x22\xb3\x19\x1a\xab\x5b\xc4\x15\xc5\xba\xfa\xc5\x33\x34\x45\x17\x5b\xe2\xfa\xa8\x33" + "\x3a\xc3\xab\xee\x4c\xd1\x7e\x49\x08\x2a"; + memory_set_bootloader_hash_mock(mock1); + uint8_t hash[32]; + memory_bootloader_hash(hash); + memory_get_attestation_bootloader_hash(hash); + assert_memory_equal(hash, mock1, sizeof(hash)); + + assert_true(memory_set_attestation_bootloader_hash()); + memset(hash, 0x00, sizeof(hash)); + memory_get_attestation_bootloader_hash(hash); + assert_memory_equal(hash, mock1, sizeof(hash)); + + const uint8_t mock2[32] = + "\x6c\xad\x6a\xbc\x3f\xd4\x47\xa5\x8d\x7a\x26\x2d\x76\x06\xa0\x40\xe4\x9e\x82\xb0\x06\x48" + "\x62\x36\x25\x88\x3e\x9f\xc0\xfa\xa8\xad"; + memory_set_bootloader_hash_mock(mock2); + + memset(hash, 0x00, sizeof(hash)); + memory_bootloader_hash(hash); + assert_memory_equal(hash, mock2, sizeof(hash)); + + memset(hash, 0x00, sizeof(hash)); + memory_get_attestation_bootloader_hash(hash); + assert_memory_equal(hash, mock1, sizeof(hash)); + + assert_true(memory_reset_hww()); + + memset(hash, 0x00, sizeof(hash)); + memory_get_attestation_bootloader_hash(hash); + assert_memory_equal(hash, mock1, sizeof(hash)); +} + int main(void) { const struct CMUnitTest tests[] = { @@ -222,6 +266,7 @@ int main(void) cmocka_unit_test(_test_memory_multisig_full), cmocka_unit_test(_test_memory_attestation), cmocka_unit_test(_test_functional), + cmocka_unit_test(_test_attestation_bootloader_hash), }; return cmocka_run_group_tests(tests, NULL, NULL); }