From c35b657a17915789b351682d07591ae930dc6e26 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Sun, 19 Apr 2026 09:34:24 +0000 Subject: [PATCH 1/4] atecc: make securechip ATECC Hal functions asynchronous Same async machinery as in optiga/ops.rs. --- src/CMakeLists.txt | 1 + src/atecc/atecc.c | 393 +-------- src/atecc/atecc.h | 94 ++- src/atecc/atecc_ops.c | 765 ++++++++++++++++++ src/common_main.c | 9 +- src/rust/bitbox-hal/src/memory.rs | 2 + src/rust/bitbox-hal/src/securechip.rs | 34 +- src/rust/bitbox-platform-host/src/memory.rs | 12 +- .../bitbox-platform-host/src/securechip.rs | 42 +- src/rust/bitbox-securechip-sys/build.rs | 47 +- src/rust/bitbox-securechip-sys/wrapper.h | 1 + src/rust/bitbox-securechip/src/atecc.rs | 555 +++++++++++-- src/rust/bitbox-securechip/src/atecc/ops.rs | 399 +++++++++ .../bitbox-securechip/src/atecc/ops_fake.rs | 268 ++++++ src/rust/bitbox-securechip/src/optiga.rs | 2 +- src/rust/bitbox02-rust/src/attestation.rs | 11 +- .../bitbox02-rust/src/hww/api/device_info.rs | 2 +- src/rust/bitbox02-rust/src/hww/api/restore.rs | 22 +- src/rust/bitbox02-rust/src/keystore.rs | 10 +- src/rust/bitbox02-rust/src/reset.rs | 19 +- src/rust/bitbox02/src/hal.rs | 6 +- src/rust/bitbox02/src/hal/memory.rs | 8 + src/rust/bitbox02/src/hal/securechip.rs | 64 +- src/rust/bitbox02/src/memory.rs | 18 +- src/rust/bitbox02/src/securechip/imp.rs | 112 ++- src/rust/bitbox02/src/securechip/imp_fake.rs | 43 +- src/rust/bitbox03/src/memory.rs | 8 + src/rust/bitbox03/src/securechip.rs | 17 +- src/securechip/securechip.h | 17 - 29 files changed, 2378 insertions(+), 603 deletions(-) create mode 100644 src/atecc/atecc_ops.c create mode 100644 src/rust/bitbox-securechip/src/atecc/ops.rs create mode 100644 src/rust/bitbox-securechip/src/atecc/ops_fake.rs diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7662f0095a..2ee74a8b09 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -142,6 +142,7 @@ set(PLATFORM-BITBOX02-SOURCES ${PLATFORM-BITBOX02-SOURCES} PARENT_SCOPE) set(SECURECHIP-SOURCES ${CMAKE_SOURCE_DIR}/src/atecc/atecc.c + ${CMAKE_SOURCE_DIR}/src/atecc/atecc_ops.c ${CMAKE_SOURCE_DIR}/src/optiga/pal/pal.c ${CMAKE_SOURCE_DIR}/src/optiga/pal/pal_gpio.c ${CMAKE_SOURCE_DIR}/src/optiga/pal/pal_i2c.c diff --git a/src/atecc/atecc.c b/src/atecc/atecc.c index 5698198d7c..ed71bf0063 100644 --- a/src/atecc/atecc.c +++ b/src/atecc/atecc.c @@ -1,10 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include "atecc.h" -#include "hardfault.h" -#include "securechip/securechip.h" #include -#include #include // disabling some warnings, as it's an external library. @@ -12,23 +9,12 @@ #pragma GCC diagnostic ignored "-Wint-conversion" #pragma GCC diagnostic ignored "-Wpedantic" #pragma GCC diagnostic ignored "-Wunused-parameter" +// clang-format off #include #include +// clang-format on #pragma GCC diagnostic pop -typedef enum { - ATECC_SLOT_IO_PROTECTION_KEY = 0, - ATECC_SLOT_AUTHKEY = 1, - ATECC_SLOT_ENCRYPTION_KEY = 2, - ATECC_SLOT_ROLLKEY = 3, - ATECC_SLOT_KDF = 4, - ATECC_SLOT_ATTESTATION = 5, - // Deprecated as the equivalent does not exist in the Optiga chip. - ATECC_SLOT_ECC_UNSAFE_SIGN_DEPRECATED = 6, - ATECC_SLOT_DATA0 = 9, - // The other slots are currently not in use. -} atecc_slot_t; - // Chip Configuration, generated with "make generate-atecc608-config" // The first 16 bytes, as well as the LockValue/LockConfig can't be changed and are ignored when // writing the configuration to the device. Locking is performed via the Lock command during setup, @@ -64,31 +50,19 @@ static uint8_t _configuration[ATCA_ECC_CONFIG_SIZE] = { // Number of times the first kdf slot can be used. #define MONOTONIC_COUNTER_MAX_USE (730500) -// This number of KDF iterations on the 2nd kdf slot when stretching the device -// password. -#define KDF_NUM_ITERATIONS (2) - -// The total individual size of the public key data slots (slots 9-15) is 72 bytes. Using encrypted -// read/write it is only possible to transmit 32 bytes. The last block is therefore 8 (72 = -// 32+32+8). -#define DATA_PUBLIC_KEY_SLOT_BLOCK_SIZE 32 - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpacked" -#pragma GCC diagnostic ignored "-Wattributes" -typedef union { - struct __attribute__((__packed__)) { - uint32_t u2f_counter; - } fields; - uint8_t bytes[DATA_PUBLIC_KEY_SLOT_BLOCK_SIZE]; -} data_9_0_t; - -#pragma GCC diagnostic pop - -static const securechip_interface_functions_t* _interface_functions = NULL; static uint8_t _serial_number[ATCA_SERIAL_NUM_SIZE] = {0}; static bool _serial_number_cached = false; +const uint8_t* atecc_serial_number(void) +{ + return _serial_number; +} + +bool atecc_serial_number_is_cached(void) +{ + return _serial_number_cached; +} + /** \brief initialize an I2C interface using given config. * \param[in] hal - opaque ptr to HAL data * \param[in] cfg - interface configuration @@ -226,11 +200,11 @@ static ATCA_STATUS _lock_slot(atecc_slot_t slot) return ATCA_SUCCESS; } -static ATCA_STATUS _factory_setup(void) +static ATCA_STATUS _factory_setup( + const uint8_t* io_protection_key, + const uint8_t* auth_key, + const uint8_t* encryption_key) { - if (_interface_functions == NULL) { - return (ATCA_STATUS)SC_ERR_IFS; - } bool is_config_locked = false; ATCA_STATUS result = atcab_is_locked(LOCK_ZONE_CONFIG, &is_config_locked); if (result != ATCA_SUCCESS) { @@ -271,26 +245,17 @@ static ATCA_STATUS _factory_setup(void) } if (is_config_locked && !is_data_locked) { // Write IO protection key. - uint8_t io_protection_key[32] = {0}; - UTIL_CLEANUP_32(io_protection_key); - _interface_functions->get_io_protection_key(io_protection_key); result = atcab_write_zone( ATCA_ZONE_DATA, ATECC_SLOT_IO_PROTECTION_KEY, 0, 0, io_protection_key, 32); if (result != ATCA_SUCCESS) { return result; } // Write auth key. - uint8_t auth_key[32] = {0}; - UTIL_CLEANUP_32(auth_key); - _interface_functions->get_auth_key(auth_key); result = atcab_write_zone(ATCA_ZONE_DATA, ATECC_SLOT_AUTHKEY, 0, 0, auth_key, 32); if (result != ATCA_SUCCESS) { return result; } // Write encryption key. - uint8_t encryption_key[32] = {0}; - UTIL_CLEANUP_32(encryption_key); - _interface_functions->get_encryption_key(encryption_key); result = atcab_write_zone(ATCA_ZONE_DATA, ATECC_SLOT_ENCRYPTION_KEY, 0, 0, encryption_key, 32); if (result != ATCA_SUCCESS) { @@ -375,19 +340,21 @@ static int _verify_config(void) return ATCA_SUCCESS; } -int atecc_setup(const securechip_interface_functions_t* ifs) +int atecc_setup( + const uint8_t* io_protection_key, + const uint8_t* auth_key, + const uint8_t* encryption_key) { - if (ifs == NULL) { + if (io_protection_key == NULL || auth_key == NULL || encryption_key == NULL) { return SC_ERR_IFS; } - _interface_functions = ifs; ATCA_STATUS result = atcab_init(&cfg); if (result != ATCA_SUCCESS) { return result; } #if FACTORYSETUP == 1 - result = _factory_setup(); + result = _factory_setup(io_protection_key, auth_key, encryption_key); if (result != ATCA_SUCCESS) { return result; } @@ -410,7 +377,7 @@ int atecc_setup(const securechip_interface_functions_t* ifs) * be called before using any slot requiring auth and whose KeyConfig.AuthKey is * ATECC_SLOT_AUTHKEY. */ -static ATCA_STATUS _authorize_key(void) +static ATCA_STATUS _authorize_key(const uint8_t* auth_key) { uint8_t num_in[NONCE_NUMIN_SIZE] = {0}; uint8_t rand_out[32] = {0}; @@ -440,9 +407,6 @@ static ATCA_STATUS _authorize_key(void) return ATCA_NOT_INITIALIZED; } - uint8_t auth_key[32] = {0}; - UTIL_CLEANUP_32(auth_key); - _interface_functions->get_auth_key(auth_key); atca_check_mac_in_out_t checkmac_params = { // First SHA block from slot key, Second SHA block from TempKey. .mode = CHECKMAC_MODE_BLOCK2_TEMPKEY, @@ -470,318 +434,11 @@ static ATCA_STATUS _authorize_key(void) checkmac_params.other_data); } -/** - * Performs a roll-key operation on a ATECC_SLOT_ROLLKEY. - * @return ATCA_SUCCESS on success. - */ -static ATCA_STATUS _rollkey(void) -{ - ATCA_STATUS result = _authorize_key(); - if (result != ATCA_SUCCESS) { - return result; - } - - uint8_t num_in[NONCE_NUMIN_SIZE] = {0}; - result = atcab_nonce_rand(num_in, NULL); - if (result != ATCA_SUCCESS) { - return result; - } - return atcab_derivekey(0, ATECC_SLOT_ROLLKEY, NULL); -} - -/** - * Writes a new random key to ATECC_SLOT_KDF. - * @return ATCA_SUCCESS on success. - */ -static ATCA_STATUS _update_kdf_key(void) -{ - uint8_t new_key[32] = {0}; - UTIL_CLEANUP_32(new_key); - _interface_functions->random_32_bytes(new_key); - uint8_t encryption_key[32] = {0}; - UTIL_CLEANUP_32(encryption_key); - _interface_functions->get_encryption_key(encryption_key); - - uint8_t nonce_contribution[32] = {0}; - UTIL_CLEANUP_32(nonce_contribution); - _interface_functions->random_32_bytes(nonce_contribution); -#if NONCE_NUMIN_SIZE > 32 - #error "size mismatch" -#endif - - ATCA_STATUS result = _authorize_key(); - if (result != ATCA_SUCCESS) { - return result; - } - - return atcab_write_enc( - ATECC_SLOT_KDF, 0, new_key, encryption_key, ATECC_SLOT_ENCRYPTION_KEY, nonce_contribution); -} - -static int _atecc_kdf(atecc_slot_t slot, const uint8_t* msg, size_t len, uint8_t* kdf_out) +bool atecc_gen_attestation_key(const uint8_t* auth_key, uint8_t* pubkey_out) { - if (len > 127 || (slot != ATECC_SLOT_ROLLKEY && slot != ATECC_SLOT_KDF)) { - return SC_ERR_INVALID_ARGS; - } - if (msg == kdf_out) { - return SC_ERR_INVALID_ARGS; - } - - ATCA_STATUS result = _authorize_key(); - if (result != ATCA_SUCCESS) { - return result; - } - - uint8_t nonce_out[32] = {0}; - - // The result is hkdf_extract with the msg as ikm (input key material) and - // the slot key as the salt. hkdf info does not apply, as it is part of - // hkdf_expand, which is not performed. hkdf_extract is simply hmac. - // Python equivalent: - // import hmac, hashlib; hmac.new(slot_key, msg, hashlib.sha256).digest() - result = atcab_kdf( - KDF_MODE_SOURCE_SLOT | KDF_MODE_TARGET_OUTPUT_ENC | KDF_MODE_ALG_HKDF, - slot, - KDF_DETAILS_HKDF_MSG_LOC_INPUT | (len << 24), // << 24, not << 25 as - // described in the data - // sheet. - msg, - kdf_out, - nonce_out); - - // For PRF instead of HKDF, the Python equivalent is (msg = label+seed): - // from scapy.layers.tls.crypto.prf import _tls12_SHA256PRF - // _tls12_SHA256PRF(slot_key, msg, '', 32) - /* result = atcab_kdf( */ - /* KDF_MODE_SOURCE_SLOT | KDF_MODE_TARGET_OUTPUT | KDF_MODE_ALG_PRF, */ - /* slot, */ - /* KDF_DETAILS_PRF_KEY_LEN_32 | (len << 24), */ - /* msg, */ - /* kdf_out, */ - /* nonce_out); */ - if (result != ATCA_SUCCESS) { - return result; - } - // Output is encrypted with the io protection key. - uint8_t io_protection_key[32] = {0}; - UTIL_CLEANUP_32(io_protection_key); - _interface_functions->get_io_protection_key(io_protection_key); - atca_io_decrypt_in_out_t io_dec_params = { - .io_key = io_protection_key, - .out_nonce = nonce_out, - .data = kdf_out, - .data_size = 32, - }; - return atcah_io_decrypt(&io_dec_params); -} - -int atecc_kdf(const uint8_t* msg, size_t len, uint8_t* kdf_out) -{ - return _atecc_kdf(ATECC_SLOT_KDF, msg, len, kdf_out); -} - -int atecc_init_new_password( - const char* password, - securechip_password_stretch_algo_t password_stretch_algo, - uint8_t* stretched_out) -{ - (void)password; - if (password_stretch_algo != SECURECHIP_PASSWORD_STRETCH_ALGO_V0) { - return SC_ERR_INVALID_PASSWORD_STRETCH_ALGO; - } - if (!atecc_reset_keys()) { - return SC_ATECC_ERR_RESET_KEYS; - } - return atecc_stretch_password(password, password_stretch_algo, stretched_out); -} - -int atecc_stretch_password( - const char* password, - securechip_password_stretch_algo_t password_stretch_algo, - uint8_t* stretched_out) -{ - if (password_stretch_algo != SECURECHIP_PASSWORD_STRETCH_ALGO_V0) { - return SC_ERR_INVALID_PASSWORD_STRETCH_ALGO; - } - - uint8_t password_salted_hashed[32] = {0}; - UTIL_CLEANUP_32(password_salted_hashed); - if (!rust_salt_hash_data( - rust_util_bytes((const uint8_t*)password, strlen(password)), - "keystore_seed_access_in", - rust_util_bytes_mut(password_salted_hashed, sizeof(password_salted_hashed)))) { - return SC_ERR_SALT; - } - - uint8_t kdf_in[32] = {0}; - UTIL_CLEANUP_32(kdf_in); - memcpy(kdf_in, password_salted_hashed, 32); - - // First KDF on rollkey increments the monotonic counter. Call only once! - int securechip_result = _atecc_kdf(ATECC_SLOT_ROLLKEY, kdf_in, 32, stretched_out); - if (securechip_result) { - return securechip_result; - } - // Second KDF does not use the counter and we call it multiple times. - for (int i = 0; i < KDF_NUM_ITERATIONS; i++) { - memcpy(kdf_in, stretched_out, 32); - securechip_result = atecc_kdf(kdf_in, 32, stretched_out); - if (securechip_result) { - return securechip_result; - } - } - - if (!rust_salt_hash_data( - rust_util_bytes((const uint8_t*)password, strlen(password)), - "keystore_seed_access_out", - rust_util_bytes_mut(password_salted_hashed, sizeof(password_salted_hashed)))) { - return SC_ERR_SALT; - } - rust_hmac_sha256( - password_salted_hashed, sizeof(password_salted_hashed), stretched_out, 32, stretched_out); - return 0; -} - -bool atecc_reset_keys(void) -{ - if (_rollkey() != ATCA_SUCCESS) { - return false; - } - return _update_kdf_key() == ATCA_SUCCESS; -} - -bool atecc_gen_attestation_key(uint8_t* pubkey_out) -{ - ATCA_STATUS result = _authorize_key(); + ATCA_STATUS result = _authorize_key(auth_key); if (result != ATCA_SUCCESS) { return false; } return atcab_genkey(ATECC_SLOT_ATTESTATION, pubkey_out) == ATCA_SUCCESS; } - -bool atecc_attestation_sign(const uint8_t* msg, uint8_t* signature_out) -{ - ATCA_STATUS result = _authorize_key(); - if (result != ATCA_SUCCESS) { - return false; - } - return atcab_sign(ATECC_SLOT_ATTESTATION, msg, signature_out) == ATCA_SUCCESS; -} - -bool atecc_monotonic_increments_remaining(uint32_t* remaining_out) -{ - uint32_t counter; - if (atcab_counter_read(0, &counter) != ATCA_SUCCESS) { - return false; - } - if (COUNTER_MAX_VALUE < counter) { - Abort("ATECC returned an invalid value"); - } - *remaining_out = COUNTER_MAX_VALUE - counter; - return true; -} - -int atecc_random(uint8_t* rand_out) -{ - ATCA_STATUS result = ATCA_GEN_FAIL; - for (int retries = 0; retries < 5; retries++) { - result = atcab_random(rand_out); - if (result == ATCA_SUCCESS) { - return 0; - } - } - return result; -} - -#if APP_U2F == 1 || FACTORYSETUP == 1 -// Read a "standard" sized block from a data slot (must be 32 bytes) -static bool _read_data_slot_block(uint8_t* bytes, uint16_t slot, uint8_t block) -{ - uint8_t encryption_key[32] = {0}; - UTIL_CLEANUP_32(encryption_key); - _interface_functions->get_encryption_key(encryption_key); - - uint8_t nonce_contribution[32] = {0}; - UTIL_CLEANUP_32(nonce_contribution); - _interface_functions->random_32_bytes(nonce_contribution); - #if NONCE_NUMIN_SIZE > 32 - #error "size mismatch" - #endif - - ATCA_STATUS result = _authorize_key(); - if (result != ATCA_SUCCESS) { - return false; - } - return atcab_read_enc( - slot, block, bytes, encryption_key, ATECC_SLOT_ENCRYPTION_KEY, nonce_contribution) == - ATCA_SUCCESS; -} - -// Write a "standard" sized block from a data slot (must be 32 bytes) -static bool _write_data_slot_block(uint8_t* bytes, uint16_t slot, uint8_t block) -{ - uint8_t encryption_key[32] = {0}; - UTIL_CLEANUP_32(encryption_key); - _interface_functions->get_encryption_key(encryption_key); - - uint8_t nonce_contribution[32] = {0}; - UTIL_CLEANUP_32(nonce_contribution); - _interface_functions->random_32_bytes(nonce_contribution); - #if NONCE_NUMIN_SIZE > 32 - #error "size mismatch" - #endif - - ATCA_STATUS result = _authorize_key(); - if (result != ATCA_SUCCESS) { - return false; - } - result = atcab_write_enc( - slot, block, bytes, encryption_key, ATECC_SLOT_ENCRYPTION_KEY, nonce_contribution); - if (result != ATCA_SUCCESS) { - return false; - } - // Double-check by reading it back and comparing. - uint8_t written_bytes[32] = {0}; - if (!_read_data_slot_block(written_bytes, slot, block)) { - return false; - } - return MEMEQ(written_bytes, bytes, sizeof(written_bytes)); -} - -bool atecc_u2f_counter_set(uint32_t counter) -{ - data_9_0_t data = {0}; - if (!_read_data_slot_block(&data.bytes[0], ATECC_SLOT_DATA0, 0)) { - return false; - } - - data.fields.u2f_counter = counter; - - return _write_data_slot_block(&data.bytes[0], ATECC_SLOT_DATA0, 0); -} -#endif - -#if APP_U2F == 1 -bool atecc_u2f_counter_inc(uint32_t* counter) -{ - data_9_0_t data = {0}; - if (!_read_data_slot_block(&data.bytes[0], ATECC_SLOT_DATA0, 0)) { - return false; - } - - data.fields.u2f_counter += 1; - *counter = data.fields.u2f_counter; - - return _write_data_slot_block(&data.bytes[0], ATECC_SLOT_DATA0, 0); -} -#endif - -bool atecc_model(securechip_model_t* model_out) -{ - uint8_t revision[4] = {0}; - if (atcab_info(revision) != ATCA_SUCCESS) { - return false; - } - *model_out = revision[3] >= 0x03 ? ATECC_ATECC608B : ATECC_ATECC608A; - return true; -} diff --git a/src/atecc/atecc.h b/src/atecc/atecc.h index a3a5ec810b..e8a3aa0783 100644 --- a/src/atecc/atecc.h +++ b/src/atecc/atecc.h @@ -12,27 +12,77 @@ #include #include -USE_RESULT int atecc_setup(const securechip_interface_functions_t* ifs); -USE_RESULT int atecc_kdf(const uint8_t* msg, size_t len, uint8_t* kdf_out); -USE_RESULT int atecc_init_new_password( - const char* password, - securechip_password_stretch_algo_t password_stretch_algo, - uint8_t* stretched_out); -USE_RESULT int atecc_stretch_password( - const char* password, - securechip_password_stretch_algo_t password_stretch_algo, - uint8_t* stretched_out); -USE_RESULT bool atecc_reset_keys(void); -USE_RESULT bool atecc_gen_attestation_key(uint8_t* pubkey_out); -USE_RESULT bool atecc_attestation_sign(const uint8_t* challenge, uint8_t* signature_out); -USE_RESULT bool atecc_monotonic_increments_remaining(uint32_t* remaining_out); -USE_RESULT int atecc_random(uint8_t* rand_out); -#if APP_U2F == 1 || FACTORYSETUP == 1 -USE_RESULT bool atecc_u2f_counter_set(uint32_t counter); -#endif -#if APP_U2F == 1 -USE_RESULT bool atecc_u2f_counter_inc(uint32_t* counter); -#endif -USE_RESULT bool atecc_model(securechip_model_t* model_out); +typedef enum { + ATECC_SLOT_IO_PROTECTION_KEY = 0, + ATECC_SLOT_AUTHKEY = 1, + ATECC_SLOT_ENCRYPTION_KEY = 2, + ATECC_SLOT_ROLLKEY = 3, + ATECC_SLOT_KDF = 4, + ATECC_SLOT_ATTESTATION = 5, + // Deprecated as the equivalent does not exist in the Optiga chip. + ATECC_SLOT_ECC_UNSAFE_SIGN_DEPRECATED = 6, + ATECC_SLOT_DATA0 = 9, + // The other slots are currently not in use. +} atecc_slot_t; + +USE_RESULT int atecc_setup( + const uint8_t* io_protection_key, + const uint8_t* auth_key, + const uint8_t* encryption_key); +USE_RESULT bool atecc_gen_attestation_key(const uint8_t* auth_key, uint8_t* pubkey_out); + +const uint8_t* atecc_serial_number(void); +bool atecc_serial_number_is_cached(void); + +#define ATECC_OPS_STATUS_BUSY 0x100 + +int atecc_ops_get_status(void); +uint32_t atecc_ops_get_poll_delay_ms(void); +void atecc_ops_poll(void); + +USE_RESULT int atecc_cmd_start_nonce_rand(const uint8_t* num_in); +USE_RESULT int atecc_cmd_start_checkmac(const uint8_t* response); +USE_RESULT int atecc_cmd_start_random(void); +USE_RESULT int atecc_cmd_start_counter_read(void); +USE_RESULT int atecc_cmd_start_info_revision(void); +USE_RESULT int atecc_cmd_start_kdf(atecc_slot_t slot, const uint8_t* msg, size_t len); +USE_RESULT int atecc_cmd_start_derivekey_rollkey(void); +USE_RESULT int atecc_cmd_start_nonce_load_msgdigest(const uint8_t* msg); +USE_RESULT int atecc_cmd_start_sign_attestation(void); +USE_RESULT int atecc_cmd_start_gendig_encryption_key(void); +USE_RESULT int atecc_cmd_start_read_block(uint16_t slot, uint8_t block); +USE_RESULT int atecc_cmd_start_write_encrypted_block( + uint16_t slot, + uint8_t block, + const uint8_t* value, + const uint8_t* mac); + +USE_RESULT int atecc_cmd_read_random_response(uint8_t* out); +USE_RESULT int atecc_cmd_read_counter_response(uint32_t* counter_out); +USE_RESULT int atecc_cmd_read_info_response(uint8_t* out); +USE_RESULT int atecc_cmd_read_kdf_response(uint8_t* out_data, uint8_t* out_nonce); +USE_RESULT int atecc_cmd_read_sign_response(uint8_t* signature_out); +USE_RESULT int atecc_cmd_read_block_response(uint8_t* out); + +USE_RESULT int atecc_auth_compute_response( + const uint8_t* num_in, + const uint8_t* rand_out, + const uint8_t* auth_key, + uint8_t* response_out); +USE_RESULT int atecc_kdf_decrypt( + const uint8_t* io_protection_key, + const uint8_t* nonce_out, + uint8_t* data, + size_t data_size); +void atecc_io_clear_tempkey(void); +USE_RESULT int atecc_io_prepare_tempkey(const uint8_t* num_in, const uint8_t* rand_out); +USE_RESULT int atecc_io_apply_gendig(const uint8_t* encryption_key); +USE_RESULT int atecc_io_prepare_encrypted_write( + uint16_t key_id, + uint8_t block, + const uint8_t* input_data, + uint8_t* encrypted_out, + uint8_t* mac_out); +USE_RESULT int atecc_io_decrypt_block(uint8_t* data, size_t len); #endif diff --git a/src/atecc/atecc_ops.c b/src/atecc/atecc_ops.c new file mode 100644 index 0000000000..46484cddca --- /dev/null +++ b/src/atecc/atecc_ops.c @@ -0,0 +1,765 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "atecc.h" + +#include +#include + +// disabling some warnings, as it's an external library. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wint-conversion" +#pragma GCC diagnostic ignored "-Wpedantic" +#pragma GCC diagnostic ignored "-Wunused-parameter" +// clang-format off +#include +#include +#include +#include +// clang-format on +#pragma GCC diagnostic pop + +// NOLINTBEGIN(bugprone-assignment-in-if-condition) + +typedef struct { + volatile int status; + struct { + bool active; + ATCADevice device; + uint32_t max_delay_count; + uint32_t next_poll_delay_ms; + } command; + ATCAPacket packet; + atca_temp_key_t io_temp_key; + uint8_t io_other_data[4]; +} atecc_async_ctx_t; + +static atecc_async_ctx_t _async = { + .status = ATCA_SUCCESS, +}; + +static void _atecc_async_reset_command(void) +{ + memset(&_async.command, 0, sizeof(_async.command)); + memset(&_async.packet, 0, sizeof(_async.packet)); + _async.status = ATECC_OPS_STATUS_BUSY; +} + +static void _atecc_async_finish(int status) +{ + _async.command.active = false; + _async.command.next_poll_delay_ms = 0; + _async.status = status; +} + +static ATCA_STATUS _atecc_async_init_command_device(void) +{ + _async.command.device = atcab_get_device(); + if (_async.command.device == NULL) { + return ATCA_NOT_INITIALIZED; + } + return ATCA_SUCCESS; +} + +static void _atecc_async_complete_command(ATCA_STATUS status) +{ + if (_async.command.device != NULL) { + atidle(_async.command.device->mIface); + } + _atecc_async_finish(status); +} + +// Async counterpart to the wake/send half of cryptoauthlib's calib_execute_command(). +static ATCA_STATUS _atecc_async_start_command(void) +{ + // Ignored by our ATCA_CUSTOM_IFACE HAL; set only for cryptoauthlib's native I2C iface below. + uint8_t word_address = 0xFF; + ATCA_STATUS status; + + _async.command.device = atcab_get_device(); + if (_async.command.device == NULL) { + return ATCA_NOT_INITIALIZED; + } + +#ifdef ATCA_NO_POLL + if ((status = calib_get_execution_time( + _async.packet.opcode, _async.command.device->mCommands)) != ATCA_SUCCESS) { + return status; + } + _async.command.max_delay_count = 0; + _async.command.next_poll_delay_ms = _async.command.device->mCommands->execution_time_msec; +#else + _async.command.max_delay_count = ATCA_POLLING_MAX_TIME_MSEC / ATCA_POLLING_FREQUENCY_TIME_MSEC; + _async.command.next_poll_delay_ms = ATCA_POLLING_INIT_TIME_MSEC; +#endif + + _async.command.active = true; + + if ((status = atwake(_async.command.device->mIface)) != ATCA_SUCCESS) { + _atecc_async_complete_command(status); + return status; + } + + if (ATCA_I2C_IFACE == _async.command.device->mIface->mIfaceCFG->iface_type) { + word_address = 0x03; + } + if ((status = atsend( + _async.command.device->mIface, + word_address, + (uint8_t*)&_async.packet, + _async.packet.txsize)) != ATCA_SUCCESS) { + _atecc_async_complete_command(status); + return status; + } + + return ATCA_SUCCESS; +} + +// Async counterpart to the receive/poll half of cryptoauthlib's calib_execute_command(). +static void _atecc_async_poll_command(void) +{ + ATCA_STATUS status; + uint16_t rxsize; + + if (_async.status != ATECC_OPS_STATUS_BUSY || !_async.command.active) { + return; + } + + memset(_async.packet.data, 0, sizeof(_async.packet.data)); + rxsize = sizeof(_async.packet.data); + status = atreceive(_async.command.device->mIface, 0, _async.packet.data, &rxsize); + if (status == ATCA_SUCCESS) { + if (rxsize < 4) { + status = rxsize > 0 ? ATCA_RX_FAIL : ATCA_RX_NO_RESPONSE; + } else if ((status = atCheckCrc(_async.packet.data)) == ATCA_SUCCESS) { + status = isATCAError(_async.packet.data); + } + _atecc_async_complete_command(status); + return; + } + +#ifndef ATCA_NO_POLL + if (_async.command.max_delay_count-- > 0) { + _async.command.next_poll_delay_ms = ATCA_POLLING_FREQUENCY_TIME_MSEC; + return; + } +#endif + + _atecc_async_complete_command(status); +} + +static int _atecc_async_launch(int status) +{ + if (status != ATCA_SUCCESS && _async.status == ATECC_OPS_STATUS_BUSY) { + _atecc_async_finish(status); + } + return status; +} + +// Ported from calib_nonce_rand() / calib_nonce_base() in cryptoauthlib/lib/calib/calib_nonce.c. +static ATCA_STATUS _atecc_async_start_nonce_rand(const uint8_t* num_in) +{ + ATCA_STATUS status; + + if ((status = _atecc_async_init_command_device()) != ATCA_SUCCESS) { + return status; + } + _async.packet.param1 = NONCE_MODE_SEED_UPDATE; + _async.packet.param2 = 0; + memcpy(_async.packet.data, num_in, NONCE_NUMIN_SIZE); + if ((status = atNonce(_async.command.device->mCommands, &_async.packet)) != ATCA_SUCCESS) { + return status; + } + return _atecc_async_start_command(); +} + +// Ported from calib_nonce_load() / calib_nonce_base() in cryptoauthlib/lib/calib/calib_nonce.c. +static ATCA_STATUS _atecc_async_start_nonce_load( + uint8_t target, + const uint8_t* num_in, + uint16_t num_in_size) +{ + uint8_t mode = NONCE_MODE_PASSTHROUGH | (NONCE_MODE_TARGET_MASK & target); + ATCA_STATUS status; + + if ((status = _atecc_async_init_command_device()) != ATCA_SUCCESS) { + return status; + } + if (num_in_size == 32) { + mode |= NONCE_MODE_INPUT_LEN_32; + } else if (num_in_size == 64) { + mode |= NONCE_MODE_INPUT_LEN_64; + } else { + return ATCA_BAD_PARAM; + } + + _async.packet.param1 = mode; + _async.packet.param2 = 0; + memcpy(_async.packet.data, num_in, num_in_size); + if ((status = atNonce(_async.command.device->mCommands, &_async.packet)) != ATCA_SUCCESS) { + return status; + } + return _atecc_async_start_command(); +} + +// Ported from calib_checkmac() in cryptoauthlib/lib/calib/calib_checkmac.c. +static ATCA_STATUS _atecc_async_start_checkmac(const uint8_t* response) +{ + static const uint8_t other_data[13] = {0}; + ATCA_STATUS status; + + if ((status = _atecc_async_init_command_device()) != ATCA_SUCCESS) { + return status; + } + _async.packet.param1 = CHECKMAC_MODE_BLOCK2_TEMPKEY; + _async.packet.param2 = ATECC_SLOT_AUTHKEY; + memset(&_async.packet.data[0], 0, CHECKMAC_CLIENT_CHALLENGE_SIZE); + memcpy(&_async.packet.data[32], response, CHECKMAC_CLIENT_RESPONSE_SIZE); + memcpy(&_async.packet.data[64], other_data, CHECKMAC_OTHER_DATA_SIZE); + if ((status = atCheckMAC(_async.command.device->mCommands, &_async.packet)) != ATCA_SUCCESS) { + return status; + } + return _atecc_async_start_command(); +} + +// Ported from calib_random() in cryptoauthlib/lib/calib/calib_random.c. +static ATCA_STATUS _atecc_async_start_random_command(void) +{ + ATCA_STATUS status; + + if ((status = _atecc_async_init_command_device()) != ATCA_SUCCESS) { + return status; + } + _async.packet.param1 = RANDOM_SEED_UPDATE; + _async.packet.param2 = 0; + if ((status = atRandom(_async.command.device->mCommands, &_async.packet)) != ATCA_SUCCESS) { + return status; + } + return _atecc_async_start_command(); +} + +// Ported from calib_counter_read() / calib_counter() in cryptoauthlib/lib/calib/calib_counter.c. +static ATCA_STATUS _atecc_async_start_counter_read_command(uint16_t counter_id) +{ + ATCA_STATUS status; + + if ((status = _atecc_async_init_command_device()) != ATCA_SUCCESS) { + return status; + } + _async.packet.param1 = COUNTER_MODE_READ; + _async.packet.param2 = counter_id; + if ((status = atCounter(_async.command.device->mCommands, &_async.packet)) != ATCA_SUCCESS) { + return status; + } + return _atecc_async_start_command(); +} + +// Ported from calib_info() / calib_info_base() in cryptoauthlib/lib/calib/calib_info.c. +static ATCA_STATUS _atecc_async_start_info_revision_command(void) +{ + ATCA_STATUS status; + + if ((status = _atecc_async_init_command_device()) != ATCA_SUCCESS) { + return status; + } + _async.packet.param1 = INFO_MODE_REVISION; + _async.packet.param2 = 0; + if ((status = atInfo(_async.command.device->mCommands, &_async.packet)) != ATCA_SUCCESS) { + return status; + } + return _atecc_async_start_command(); +} + +// Ported from calib_kdf() in cryptoauthlib/lib/calib/calib_kdf.c. +static ATCA_STATUS _atecc_async_start_kdf_command(atecc_slot_t slot, const uint8_t* msg, size_t len) +{ + ATCA_STATUS status; + + if ((status = _atecc_async_init_command_device()) != ATCA_SUCCESS) { + return status; + } + _async.packet.param1 = KDF_MODE_SOURCE_SLOT | KDF_MODE_TARGET_OUTPUT_ENC | KDF_MODE_ALG_HKDF; + _async.packet.param2 = slot; + _async.packet.data[0] = KDF_DETAILS_HKDF_MSG_LOC_INPUT; + _async.packet.data[1] = 0; + _async.packet.data[2] = 0; + _async.packet.data[3] = (uint8_t)len; + memcpy(&_async.packet.data[KDF_DETAILS_SIZE], msg, len); + if ((status = atKDF(_async.command.device->mCommands, &_async.packet)) != ATCA_SUCCESS) { + return status; + } + return _atecc_async_start_command(); +} + +// Ported from calib_derivekey() in cryptoauthlib/lib/calib/calib_derivekey.c. +static ATCA_STATUS _atecc_async_start_derivekey_command(uint8_t mode, uint16_t target_key) +{ + ATCA_STATUS status; + + if ((status = _atecc_async_init_command_device()) != ATCA_SUCCESS) { + return status; + } + _async.packet.param1 = mode; + _async.packet.param2 = target_key; + if ((status = atDeriveKey(_async.command.device->mCommands, &_async.packet, false)) != + ATCA_SUCCESS) { + return status; + } + return _atecc_async_start_command(); +} + +// Ported from calib_gendig() in cryptoauthlib/lib/calib/calib_gendig.c. +static ATCA_STATUS _atecc_async_start_gendig_command( + uint8_t zone, + uint16_t key_id, + const uint8_t* other_data, + uint8_t other_data_size) +{ + ATCA_STATUS status; + bool is_no_mac_key = false; + + if ((status = _atecc_async_init_command_device()) != ATCA_SUCCESS) { + return status; + } + _async.packet.param1 = zone; + _async.packet.param2 = key_id; + if (_async.packet.param1 == GENDIG_ZONE_SHARED_NONCE && other_data_size >= ATCA_BLOCK_SIZE) { + memcpy(&_async.packet.data[0], &other_data[0], ATCA_BLOCK_SIZE); + } else if (_async.packet.param1 == GENDIG_ZONE_DATA && other_data_size >= ATCA_WORD_SIZE) { + memcpy(&_async.packet.data[0], &other_data[0], ATCA_WORD_SIZE); + is_no_mac_key = true; + } + if ((status = atGenDig(_async.command.device->mCommands, &_async.packet, is_no_mac_key)) != + ATCA_SUCCESS) { + return status; + } + return _atecc_async_start_command(); +} + +// Ported from calib_read_zone() in cryptoauthlib/lib/calib/calib_read.c. +static ATCA_STATUS _atecc_async_start_read_zone_command( + uint8_t zone, + uint16_t slot, + uint8_t block, + uint8_t offset, + uint8_t len) +{ + ATCA_STATUS status; + uint16_t addr; + uint8_t addr_zone = zone & ~ATCA_ZONE_READWRITE_32; + + if ((status = _atecc_async_init_command_device()) != ATCA_SUCCESS) { + return status; + } + if ((status = calib_get_addr(addr_zone, slot, block, offset, &addr)) != ATCA_SUCCESS) { + return status; + } + if (len == ATCA_BLOCK_SIZE) { + zone |= ATCA_ZONE_READWRITE_32; + } + _async.packet.param1 = zone; + _async.packet.param2 = addr; + if ((status = atRead(_async.command.device->mCommands, &_async.packet)) != ATCA_SUCCESS) { + return status; + } + return _atecc_async_start_command(); +} + +// Ported from calib_write() in cryptoauthlib/lib/calib/calib_write.c. +static ATCA_STATUS _atecc_async_start_write_command( + uint8_t zone, + uint16_t address, + const uint8_t* value, + const uint8_t* mac) +{ + ATCA_STATUS status; + + if ((status = _atecc_async_init_command_device()) != ATCA_SUCCESS) { + return status; + } + _async.packet.param1 = zone; + _async.packet.param2 = address; + if (zone & ATCA_ZONE_READWRITE_32) { + memcpy(_async.packet.data, value, 32); + if (mac != NULL) { + memcpy(&_async.packet.data[32], mac, 32); + } + } else { + memcpy(_async.packet.data, value, 4); + } + if ((status = atWrite( + _async.command.device->mCommands, + &_async.packet, + mac && (zone & ATCA_ZONE_READWRITE_32))) != ATCA_SUCCESS) { + return status; + } + return _atecc_async_start_command(); +} + +// Ported from calib_sign_base() in cryptoauthlib/lib/calib/calib_sign.c. +static ATCA_STATUS _atecc_async_start_sign_base_command(uint8_t mode, uint16_t key_id) +{ + ATCA_STATUS status; + + if ((status = _atecc_async_init_command_device()) != ATCA_SUCCESS) { + return status; + } + _async.packet.param1 = mode; + _async.packet.param2 = key_id; + if ((status = atSign(_async.command.device->mCommands, &_async.packet)) != ATCA_SUCCESS) { + return status; + } + return _atecc_async_start_command(); +} + +// Ported from the write-command portion of calib_write_enc() in +// cryptoauthlib/lib/calib/calib_write.c. +static ATCA_STATUS _atecc_async_start_write_encrypted_data_slot_command( + uint16_t slot, + uint8_t block, + const uint8_t* value, + const uint8_t* mac) +{ + uint16_t addr; + ATCA_STATUS status; + + if ((status = calib_get_addr(ATCA_ZONE_DATA, slot, block, 0, &addr)) != ATCA_SUCCESS) { + return status; + } + return _atecc_async_start_write_command( + ATCA_ZONE_DATA | ATCA_ZONE_READWRITE_32 | ATCA_ZONE_ENCRYPTED, addr, value, mac); +} + +static int _atecc_async_extract_random_response(uint8_t* out) +{ + if (_async.packet.data[ATCA_COUNT_IDX] != RANDOM_RSP_SIZE) { + return ATCA_RX_FAIL; + } + if (out != NULL) { + memcpy(out, &_async.packet.data[ATCA_RSP_DATA_IDX], RANDOM_NUM_SIZE); + } + return ATCA_SUCCESS; +} + +static int _atecc_async_extract_counter_response(uint32_t* counter_out) +{ + if (_async.packet.data[ATCA_COUNT_IDX] != 7) { + return ATCA_RX_FAIL; + } + *counter_out = ((uint32_t)_async.packet.data[ATCA_RSP_DATA_IDX + 0] << 0) | + ((uint32_t)_async.packet.data[ATCA_RSP_DATA_IDX + 1] << 8) | + ((uint32_t)_async.packet.data[ATCA_RSP_DATA_IDX + 2] << 16) | + ((uint32_t)_async.packet.data[ATCA_RSP_DATA_IDX + 3] << 24); + return ATCA_SUCCESS; +} + +static int _atecc_async_extract_info_response(uint8_t* out) +{ + if (_async.packet.data[ATCA_COUNT_IDX] < 7) { + return ATCA_RX_FAIL; + } + memcpy(out, &_async.packet.data[ATCA_RSP_DATA_IDX], 4); + return ATCA_SUCCESS; +} + +static int _atecc_async_extract_kdf_response(uint8_t* out_data, uint8_t* out_nonce) +{ + if (_async.packet.data[ATCA_COUNT_IDX] < (ATCA_PACKET_OVERHEAD + 64)) { + return ATCA_RX_FAIL; + } + memcpy(out_data, &_async.packet.data[ATCA_RSP_DATA_IDX], 32); + memcpy(out_nonce, &_async.packet.data[ATCA_RSP_DATA_IDX + 32], 32); + return ATCA_SUCCESS; +} + +static int _atecc_async_extract_sign_response(uint8_t* signature_out) +{ + if (_async.packet.data[ATCA_COUNT_IDX] != (ATCA_SIG_SIZE + ATCA_PACKET_OVERHEAD)) { + return ATCA_RX_FAIL; + } + memcpy(signature_out, &_async.packet.data[ATCA_RSP_DATA_IDX], ATCA_SIG_SIZE); + return ATCA_SUCCESS; +} + +static int _atecc_async_extract_read_response(uint8_t* out, size_t len) +{ + if (_async.packet.data[ATCA_COUNT_IDX] < (ATCA_PACKET_OVERHEAD + len)) { + return ATCA_RX_FAIL; + } + memcpy(out, &_async.packet.data[ATCA_RSP_DATA_IDX], len); + return ATCA_SUCCESS; +} + +int atecc_auth_compute_response( + const uint8_t* num_in, + const uint8_t* rand_out, + const uint8_t* auth_key, + uint8_t* response_out) +{ + static const uint8_t other_data[13] = {0}; + atca_temp_key_t temp_key = {0}; + atca_nonce_in_out_t nonce_params = { + .mode = NONCE_MODE_SEED_UPDATE, + .zero = 0, + .num_in = num_in, + .rand_out = rand_out, + .temp_key = &temp_key, + }; + atca_check_mac_in_out_t checkmac_params; + ATCA_STATUS status; + + if (!atecc_serial_number_is_cached()) { + return ATCA_NOT_INITIALIZED; + } + if ((status = atcah_nonce(&nonce_params)) != ATCA_SUCCESS) { + return status; + } + + memset(&checkmac_params, 0, sizeof(checkmac_params)); + checkmac_params.mode = CHECKMAC_MODE_BLOCK2_TEMPKEY; + checkmac_params.key_id = ATECC_SLOT_AUTHKEY; + checkmac_params.sn = atecc_serial_number(); + checkmac_params.client_resp = response_out; + checkmac_params.other_data = other_data; + checkmac_params.slot_key = auth_key; + checkmac_params.temp_key = &temp_key; + return atcah_check_mac(&checkmac_params); +} + +int atecc_kdf_decrypt( + const uint8_t* io_protection_key, + const uint8_t* nonce_out, + uint8_t* data, + size_t data_size) +{ + atca_io_decrypt_in_out_t io_dec_params; + + if (data_size != ATCA_BLOCK_SIZE && data_size != 2 * (size_t)ATCA_BLOCK_SIZE) { + return SC_ERR_INVALID_ARGS; + } + memset(&io_dec_params, 0, sizeof(io_dec_params)); + io_dec_params.io_key = io_protection_key; + io_dec_params.out_nonce = nonce_out; + io_dec_params.data = data; + io_dec_params.data_size = data_size; + return atcah_io_decrypt(&io_dec_params); +} + +void atecc_io_clear_tempkey(void) +{ + util_zero(&_async.io_temp_key, sizeof(_async.io_temp_key)); +} + +int atecc_io_prepare_tempkey(const uint8_t* num_in, const uint8_t* rand_out) +{ + atca_nonce_in_out_t nonce_params = { + .mode = NONCE_MODE_SEED_UPDATE, + .zero = 0, + .num_in = num_in, + .rand_out = rand_out, + .temp_key = &_async.io_temp_key, + }; + ATCA_STATUS status; + + atecc_io_clear_tempkey(); + if ((status = atcah_nonce(&nonce_params)) != ATCA_SUCCESS) { + return status; + } + + _async.io_other_data[0] = ATCA_GENDIG; + _async.io_other_data[1] = GENDIG_ZONE_DATA; + _async.io_other_data[2] = (uint8_t)ATECC_SLOT_ENCRYPTION_KEY; + _async.io_other_data[3] = (uint8_t)(ATECC_SLOT_ENCRYPTION_KEY >> 8); + return ATCA_SUCCESS; +} + +int atecc_io_apply_gendig(const uint8_t* encryption_key) +{ + atca_gen_dig_in_out_t gen_dig_param; + + if (!atecc_serial_number_is_cached()) { + return ATCA_NOT_INITIALIZED; + } + memset(&gen_dig_param, 0, sizeof(gen_dig_param)); + gen_dig_param.key_id = ATECC_SLOT_ENCRYPTION_KEY; + gen_dig_param.is_key_nomac = false; + gen_dig_param.sn = atecc_serial_number(); + gen_dig_param.stored_value = encryption_key; + gen_dig_param.zone = GENDIG_ZONE_DATA; + gen_dig_param.other_data = _async.io_other_data; + gen_dig_param.temp_key = &_async.io_temp_key; + return atcah_gen_dig(&gen_dig_param); +} + +int atecc_io_prepare_encrypted_write( + uint16_t key_id, + uint8_t block, + const uint8_t* input_data, + uint8_t* encrypted_out, + uint8_t* mac_out) +{ + uint16_t addr; + atca_write_mac_in_out_t write_mac_param; + ATCA_STATUS status; + + if (!atecc_serial_number_is_cached()) { + return ATCA_NOT_INITIALIZED; + } + if ((status = calib_get_addr(ATCA_ZONE_DATA, key_id, block, 0, &addr)) != ATCA_SUCCESS) { + return status; + } + memset(&write_mac_param, 0, sizeof(write_mac_param)); + write_mac_param.zone = ATCA_ZONE_DATA | ATCA_ZONE_READWRITE_32 | ATCA_ZONE_ENCRYPTED; + write_mac_param.key_id = addr; + write_mac_param.sn = atecc_serial_number(); + write_mac_param.input_data = input_data; + write_mac_param.encrypted_data = encrypted_out; + write_mac_param.auth_mac = mac_out; + write_mac_param.temp_key = &_async.io_temp_key; + return atcah_write_auth_mac(&write_mac_param); +} + +int atecc_io_decrypt_block(uint8_t* data, size_t len) +{ + if (len > ATCA_BLOCK_SIZE) { + return SC_ERR_INVALID_ARGS; + } + for (size_t i = 0; i < len; i++) { + data[i] ^= _async.io_temp_key.value[i]; + } + return ATCA_SUCCESS; +} + +int atecc_ops_get_status(void) +{ + return _async.status; +} + +uint32_t atecc_ops_get_poll_delay_ms(void) +{ + return _async.command.next_poll_delay_ms; +} + +void atecc_ops_poll(void) +{ + _atecc_async_poll_command(); +} + +int atecc_cmd_start_nonce_rand(const uint8_t* num_in) +{ + _atecc_async_reset_command(); + return _atecc_async_launch(_atecc_async_start_nonce_rand(num_in)); +} + +int atecc_cmd_start_checkmac(const uint8_t* response) +{ + _atecc_async_reset_command(); + return _atecc_async_launch(_atecc_async_start_checkmac(response)); +} + +int atecc_cmd_start_random(void) +{ + _atecc_async_reset_command(); + return _atecc_async_launch(_atecc_async_start_random_command()); +} + +int atecc_cmd_start_counter_read(void) +{ + _atecc_async_reset_command(); + return _atecc_async_launch(_atecc_async_start_counter_read_command(0)); +} + +int atecc_cmd_start_info_revision(void) +{ + _atecc_async_reset_command(); + return _atecc_async_launch(_atecc_async_start_info_revision_command()); +} + +int atecc_cmd_start_kdf(atecc_slot_t slot, const uint8_t* msg, size_t len) +{ + _atecc_async_reset_command(); + if (len > 127 || (slot != ATECC_SLOT_ROLLKEY && slot != ATECC_SLOT_KDF)) { + return _atecc_async_launch(SC_ERR_INVALID_ARGS); + } + // The KDF command encodes the HKDF input length in one details byte, so len must be <=127 + // before launching the async command. This mirrors the old atcab_kdf() wrapper. + return _atecc_async_launch(_atecc_async_start_kdf_command(slot, msg, len)); +} + +int atecc_cmd_start_derivekey_rollkey(void) +{ + _atecc_async_reset_command(); + return _atecc_async_launch(_atecc_async_start_derivekey_command(0, ATECC_SLOT_ROLLKEY)); +} + +int atecc_cmd_start_nonce_load_msgdigest(const uint8_t* msg) +{ + _atecc_async_reset_command(); + return _atecc_async_launch(_atecc_async_start_nonce_load(NONCE_MODE_TARGET_MSGDIGBUF, msg, 32)); +} + +int atecc_cmd_start_sign_attestation(void) +{ + _atecc_async_reset_command(); + return _atecc_async_launch(_atecc_async_start_sign_base_command( + SIGN_MODE_EXTERNAL | SIGN_MODE_SOURCE_MSGDIGBUF, ATECC_SLOT_ATTESTATION)); +} + +int atecc_cmd_start_gendig_encryption_key(void) +{ + _atecc_async_reset_command(); + return _atecc_async_launch(_atecc_async_start_gendig_command( + GENDIG_ZONE_DATA, + ATECC_SLOT_ENCRYPTION_KEY, + _async.io_other_data, + sizeof(_async.io_other_data))); +} + +int atecc_cmd_start_read_block(uint16_t slot, uint8_t block) +{ + _atecc_async_reset_command(); + return _atecc_async_launch(_atecc_async_start_read_zone_command( + ATCA_ZONE_DATA | ATCA_ZONE_READWRITE_32, slot, block, 0, ATCA_BLOCK_SIZE)); +} + +int atecc_cmd_start_write_encrypted_block( + uint16_t slot, + uint8_t block, + const uint8_t* value, + const uint8_t* mac) +{ + _atecc_async_reset_command(); + return _atecc_async_launch( + _atecc_async_start_write_encrypted_data_slot_command(slot, block, value, mac)); +} + +int atecc_cmd_read_random_response(uint8_t* out) +{ + return _atecc_async_extract_random_response(out); +} + +int atecc_cmd_read_counter_response(uint32_t* counter_out) +{ + return _atecc_async_extract_counter_response(counter_out); +} + +int atecc_cmd_read_info_response(uint8_t* out) +{ + return _atecc_async_extract_info_response(out); +} + +int atecc_cmd_read_kdf_response(uint8_t* out_data, uint8_t* out_nonce) +{ + return _atecc_async_extract_kdf_response(out_data, out_nonce); +} + +int atecc_cmd_read_sign_response(uint8_t* signature_out) +{ + return _atecc_async_extract_sign_response(signature_out); +} + +int atecc_cmd_read_block_response(uint8_t* out) +{ + return _atecc_async_extract_read_response(out, ATCA_BLOCK_SIZE); +} + +// NOLINTEND(bugprone-assignment-in-if-condition) diff --git a/src/common_main.c b/src/common_main.c index a154492241..c223454537 100644 --- a/src/common_main.c +++ b/src/common_main.c @@ -34,13 +34,6 @@ static const memory_interface_functions_t _memory_interface_functions = { .random_32_bytes = random_32_bytes_mcu, }; -static const securechip_interface_functions_t _securechip_interface_functions = { - .get_auth_key = memory_get_authorization_key, - .get_io_protection_key = memory_get_io_protection_key, - .get_encryption_key = memory_get_encryption_key, - .random_32_bytes = random_32_bytes, -}; - void common_main(void) { mpu_bitbox02_init(); @@ -57,7 +50,7 @@ void common_main(void) } // rust_securechip_setup must come after memory_setup, so the io/auth keys to be // used are already initialized. - int securechip_result = rust_securechip_setup(&_securechip_interface_functions); + int securechip_result = rust_securechip_setup(); if (securechip_result) { char errmsg[100] = {0}; snprintf( diff --git a/src/rust/bitbox-hal/src/memory.rs b/src/rust/bitbox-hal/src/memory.rs index e66ee6fedd..75ad7bf59b 100644 --- a/src/rust/bitbox-hal/src/memory.rs +++ b/src/rust/bitbox-hal/src/memory.rs @@ -98,7 +98,9 @@ pub trait Memory { fn get_noise_static_private_key(&mut self) -> Result, ()>; fn check_noise_remote_static_pubkey(&mut self, pubkey: &[u8; 32]) -> bool; fn add_noise_remote_static_pubkey(&mut self, pubkey: &[u8; 32]) -> Result<(), ()>; + fn get_auth_key(&mut self, out: &mut [u8; 32]); fn get_io_protection_key(&mut self, out: &mut [u8; 32]); + fn get_encryption_key(&mut self, out: &mut [u8; 32]); fn get_salt_root(&mut self) -> Result>, ()>; fn get_attestation_pubkey_and_certificate( &mut self, diff --git a/src/rust/bitbox-hal/src/securechip.rs b/src/rust/bitbox-hal/src/securechip.rs index d87684b834..7d6f14c422 100644 --- a/src/rust/bitbox-hal/src/securechip.rs +++ b/src/rust/bitbox-hal/src/securechip.rs @@ -84,13 +84,22 @@ pub trait SecureChip { /// /// This must not increment a monotonic counter. /// + /// `memory` is used for persistent secrets needed by some secure-chip backends. + /// /// `msg` must be 32 bytes long. - async fn kdf(&mut self, msg: &[u8; 32]) -> Result>, Error>; + async fn kdf( + &mut self, + memory: &mut impl super::memory::Memory, + msg: &[u8; 32], + ) -> Result>, Error>; /// Signs a 32-byte attestation challenge and writes the raw 64-byte P-256 signature to /// `signature`. + /// + /// `memory` is used for persistent secrets needed by some secure-chip backends. async fn attestation_sign( &mut self, + memory: &mut impl super::memory::Memory, challenge: &[u8; 32], signature: &mut [u8; 64], ) -> Result<(), ()>; @@ -99,7 +108,7 @@ pub trait SecureChip { async fn monotonic_increments_remaining(&mut self) -> Result; /// Returns the detected secure-chip model. - fn model(&mut self) -> Result; + async fn model(&mut self) -> Result; /// Resets the secure-chip objects involved in password stretching. async fn reset_keys( @@ -111,10 +120,27 @@ pub trait SecureChip { #[cfg(feature = "app-u2f")] /// Sets the U2F counter to `counter`. /// + /// `random` is used by some secure-chip backends for authenticated encrypted writes. + /// + /// `memory` is used for persistent secrets needed by some secure-chip backends. + /// /// This is intended for initialization only. - async fn u2f_counter_set(&mut self, counter: u32) -> Result<(), ()>; + async fn u2f_counter_set( + &mut self, + random: &mut impl super::random::Random, + memory: &mut impl super::memory::Memory, + counter: u32, + ) -> Result<(), ()>; #[cfg(feature = "app-u2f")] /// Increments the U2F counter and returns the new value. - async fn u2f_counter_inc(&mut self) -> Result; + /// + /// `random` is used by some secure-chip backends for authenticated encrypted writes. + /// + /// `memory` is used for persistent secrets needed by some secure-chip backends. + async fn u2f_counter_inc( + &mut self, + random: &mut impl super::random::Random, + memory: &mut impl super::memory::Memory, + ) -> Result; } diff --git a/src/rust/bitbox-platform-host/src/memory.rs b/src/rust/bitbox-platform-host/src/memory.rs index 7758772ffc..51ce273a8c 100644 --- a/src/rust/bitbox-platform-host/src/memory.rs +++ b/src/rust/bitbox-platform-host/src/memory.rs @@ -283,8 +283,16 @@ impl bitbox_hal::Memory for FakeMemory { Ok(()) } - fn get_io_protection_key(&mut self, _out: &mut [u8; 32]) { - panic!("unused") + fn get_auth_key(&mut self, out: &mut [u8; 32]) { + *out = [0; 32]; + } + + fn get_io_protection_key(&mut self, out: &mut [u8; 32]) { + *out = [0; 32]; + } + + fn get_encryption_key(&mut self, out: &mut [u8; 32]) { + *out = [0; 32]; } fn get_salt_root(&mut self) -> Result>, ()> { diff --git a/src/rust/bitbox-platform-host/src/securechip.rs b/src/rust/bitbox-platform-host/src/securechip.rs index 11c445cb45..1c86cb2970 100644 --- a/src/rust/bitbox-platform-host/src/securechip.rs +++ b/src/rust/bitbox-platform-host/src/securechip.rs @@ -125,7 +125,11 @@ impl bitbox_hal::SecureChip for FakeSecureChip { ))) } - async fn kdf(&mut self, msg: &[u8; 32]) -> Result>, Error> { + async fn kdf( + &mut self, + _memory: &mut impl bitbox_hal::Memory, + msg: &[u8; 32], + ) -> Result>, Error> { self.event_counter += 1; use bitcoin::hashes::{HashEngine, Hmac, HmacEngine, sha256}; @@ -147,6 +151,7 @@ impl bitbox_hal::SecureChip for FakeSecureChip { async fn attestation_sign( &mut self, + _memory: &mut impl bitbox_hal::Memory, challenge: &[u8; 32], signature: &mut [u8; 64], ) -> Result<(), ()> { @@ -160,7 +165,7 @@ impl bitbox_hal::SecureChip for FakeSecureChip { Ok(1) } - fn model(&mut self) -> Result { + async fn model(&mut self) -> Result { Ok(Model::Atecc608B) } @@ -179,13 +184,22 @@ impl bitbox_hal::SecureChip for FakeSecureChip { } #[cfg(feature = "app-u2f")] - async fn u2f_counter_set(&mut self, counter: u32) -> Result<(), ()> { + async fn u2f_counter_set( + &mut self, + _random: &mut impl bitbox_hal::Random, + _memory: &mut impl bitbox_hal::Memory, + counter: u32, + ) -> Result<(), ()> { self.u2f_counter = counter; Ok(()) } #[cfg(feature = "app-u2f")] - async fn u2f_counter_inc(&mut self) -> Result { + async fn u2f_counter_inc( + &mut self, + _random: &mut impl bitbox_hal::Random, + _memory: &mut impl bitbox_hal::Memory, + ) -> Result { self.u2f_counter = self.u2f_counter.wrapping_add(1); Ok(self.u2f_counter) } @@ -194,6 +208,8 @@ impl bitbox_hal::SecureChip for FakeSecureChip { #[cfg(test)] mod tests { use super::*; + use crate::memory::FakeMemory; + use crate::random::TestingRandom; use bitbox_hal::SecureChip; use hex_lit::hex; @@ -212,10 +228,24 @@ mod tests { #[async_test::test] async fn test_u2f_counter_inc() { let mut securechip = FakeSecureChip::new(); + let mut random = TestingRandom::new(); + let mut memory = FakeMemory::new(); assert_eq!(securechip.get_u2f_counter(), 0); - assert_eq!(securechip.u2f_counter_inc().await.unwrap(), 1); + assert_eq!( + securechip + .u2f_counter_inc(&mut random, &mut memory) + .await + .unwrap(), + 1 + ); assert_eq!(securechip.get_u2f_counter(), 1); - assert_eq!(securechip.u2f_counter_inc().await.unwrap(), 2); + assert_eq!( + securechip + .u2f_counter_inc(&mut random, &mut memory) + .await + .unwrap(), + 2 + ); assert_eq!(securechip.get_u2f_counter(), 2); } } diff --git a/src/rust/bitbox-securechip-sys/build.rs b/src/rust/bitbox-securechip-sys/build.rs index dcac2a66ce..a8b3afca99 100644 --- a/src/rust/bitbox-securechip-sys/build.rs +++ b/src/rust/bitbox-securechip-sys/build.rs @@ -6,6 +6,7 @@ use std::path::PathBuf; use std::process::{Command, Output}; const ALLOWLIST_TYPES: &[&str] = &[ + "atecc_slot_t", "bool_t", "optiga_crypt_t", "optiga_hmac_type_t", @@ -17,24 +18,41 @@ const ALLOWLIST_TYPES: &[&str] = &[ "optiga_symmetric_key_type_t", "optiga_util_t", "securechip_error_t", - "securechip_interface_functions_t", "securechip_model_t", "securechip_password_stretch_algo_t", ]; const ALLOWLIST_FNS: &[&str] = &[ - "atecc_attestation_sign", + "atecc_auth_compute_response", + "atecc_cmd_read_block_response", + "atecc_cmd_read_counter_response", + "atecc_cmd_read_info_response", + "atecc_cmd_read_kdf_response", + "atecc_cmd_read_random_response", + "atecc_cmd_read_sign_response", + "atecc_cmd_start_checkmac", + "atecc_cmd_start_counter_read", + "atecc_cmd_start_derivekey_rollkey", + "atecc_cmd_start_gendig_encryption_key", + "atecc_cmd_start_info_revision", + "atecc_cmd_start_kdf", + "atecc_cmd_start_nonce_load_msgdigest", + "atecc_cmd_start_nonce_rand", + "atecc_cmd_start_random", + "atecc_cmd_start_read_block", + "atecc_cmd_start_sign_attestation", + "atecc_cmd_start_write_encrypted_block", "atecc_gen_attestation_key", - "atecc_init_new_password", - "atecc_kdf", - "atecc_model", - "atecc_monotonic_increments_remaining", - "atecc_random", - "atecc_reset_keys", + "atecc_io_apply_gendig", + "atecc_io_clear_tempkey", + "atecc_io_decrypt_block", + "atecc_io_prepare_encrypted_write", + "atecc_io_prepare_tempkey", + "atecc_kdf_decrypt", + "atecc_ops_get_status", + "atecc_ops_get_poll_delay_ms", + "atecc_ops_poll", "atecc_setup", - "atecc_stretch_password", - "atecc_u2f_counter_inc", - "atecc_u2f_counter_set", "optiga_crypt_clear_auto_state", "optiga_crypt_ecdsa_sign", "optiga_crypt_generate_auth_code", @@ -54,8 +72,10 @@ const ALLOWLIST_FNS: &[&str] = &[ ]; const ALLOWLIST_VARS: &[&str] = &[ + "ATECC_OPS_STATUS_BUSY", "ARBITRARY_DATA_OBJECT_TYPE_3_MAX_SIZE", "MONOTONIC_COUNTER_MAX_USE", + "NONCE_NUMIN_SIZE", "OID_AES_SYMKEY", "OID_ARBITRARY_DATA", "OID_COUNTER", @@ -79,6 +99,7 @@ const ALLOWLIST_VARS: &[&str] = &[ ]; const RUSTIFIED_ENUMS: &[&str] = &[ + "atecc_slot_t", "optiga_hmac_type", "optiga_hmac_type_t", "optiga_key_id", @@ -170,6 +191,10 @@ pub fn main() -> BuildResult<()> { .args(&definitions) .arg(format!("-I{}", src_dir.display())) .arg(format!("-I{}", external_dir.display())) + .arg(format!( + "-I{}", + external_dir.join("cryptoauthlib/lib").display() + )) .arg(format!( "-I{}", external_dir.join("optiga-trust-m/config").display() diff --git a/src/rust/bitbox-securechip-sys/wrapper.h b/src/rust/bitbox-securechip-sys/wrapper.h index da3362e828..eab0162f39 100644 --- a/src/rust/bitbox-securechip-sys/wrapper.h +++ b/src/rust/bitbox-securechip-sys/wrapper.h @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include #include diff --git a/src/rust/bitbox-securechip/src/atecc.rs b/src/rust/bitbox-securechip/src/atecc.rs index f6935c12f4..16909c15bb 100644 --- a/src/rust/bitbox-securechip/src/atecc.rs +++ b/src/rust/bitbox-securechip/src/atecc.rs @@ -2,118 +2,515 @@ use crate::{Error, Model, PasswordStretchAlgo, SecureChipError}; use alloc::boxed::Box; -use bitbox_hal::Memory; +use bitbox_hal::{Memory, Random, timer::Timer}; +use bitbox_securechip_sys::atecc_slot_t as Slot; +use core::sync::atomic::{AtomicBool, Ordering}; +use util::sha2::hmac_sha256_overwrite; use zeroize::Zeroizing; -pub async fn attestation_sign(challenge: &[u8; 32], signature: &mut [u8; 64]) -> Result<(), ()> { - match unsafe { - bitbox_securechip_sys::atecc_attestation_sign(challenge.as_ptr(), signature.as_mut_ptr()) - } { - true => Ok(()), - false => Err(()), +#[cfg(not(test))] +#[path = "atecc/ops.rs"] +mod ops; +#[cfg(test)] +#[path = "atecc/ops_fake.rs"] +mod ops; + +const ATECC_OPS_STATUS_BUSY: i32 = bitbox_securechip_sys::ATECC_OPS_STATUS_BUSY as i32; +const KDF_LEN: usize = 32; +// This number of KDF iterations on the 2nd kdf slot when stretching the device password. +const KDF_NUM_ITERATIONS: usize = 2; +const RANDOM_RETRIES: usize = 5; +// ATECC encrypted read/write operates on 32-byte blocks. These helpers only support the standard +// full block size. +#[cfg(any(feature = "app-u2f", feature = "factory-setup"))] +const BLOCK_SIZE: usize = 32; +const COUNTER_MAX_VALUE: u32 = 2_097_151; +const SLOT_ROLLKEY: Slot = Slot::ATECC_SLOT_ROLLKEY; +const SLOT_KDF: Slot = Slot::ATECC_SLOT_KDF; +#[cfg(any(feature = "app-u2f", feature = "factory-setup"))] +const SLOT_DATA0: Slot = Slot::ATECC_SLOT_DATA0; + +// Several ATECC flows derive TempKey-like state across multiple chip commands and host helpers, so +// public operations must not interleave even though the low-level async machine is per-command. +static OP_IN_FLIGHT: AtomicBool = AtomicBool::new(false); + +struct OpGuard; + +impl Drop for OpGuard { + fn drop(&mut self) { + ops::clear_io_temp_key(); + OP_IN_FLIGHT.store(false, Ordering::Release); } } -pub fn random() -> Result>, Error> { - let mut result = Box::new(Zeroizing::new([0u8; 32])); - let status = unsafe { bitbox_securechip_sys::atecc_random(result.as_mut_ptr()) }; - if status == 0 { - Ok(result) +// Some ATECC operations derive host/device state across multiple awaited chip commands, e.g. +// authorize+derivekey, nonce+gendig+encrypted read/write, or nonce-load+sign. Those sequences must +// not interleave with another high-level ATECC call, even though `ops.rs` only serializes one +// low-level command at a time. +fn begin_high_level_op() -> Result { + if OP_IN_FLIGHT.swap(true, Ordering::AcqRel) { + Err(Error::from_status(ATECC_OPS_STATUS_BUSY)) } else { - Err(Error::from_status(status)) + Ok(OpGuard) + } +} + +fn zeroed_secret() -> Box> { + Box::new(Zeroizing::new([0; N])) +} + +fn status_from_error(err: Error) -> i32 { + match err { + Error::SecureChip(err) => err as i32, + Error::Status(status) => status, } } -pub fn monotonic_increments_remaining() -> Result { - let mut result = 0u32; - match unsafe { bitbox_securechip_sys::atecc_monotonic_increments_remaining(&mut result) } { - true => Ok(result), - false => Err(()), + +async fn random_32_bytes( + random: &mut impl Random, +) -> Result>, i32> { + let securechip_random = random_inner::().await.map_err(status_from_error)?; + Ok(bitbox_core_utils::random::random_32_bytes_with_mixin( + random, + securechip_random.as_ref(), + )) +} + +fn load_auth_key(memory: &mut impl Memory) -> Box> { + let mut auth_key = zeroed_secret(); + memory.get_auth_key(auth_key.as_mut()); + auth_key +} + +fn load_io_protection_key(memory: &mut impl Memory) -> Box> { + let mut io_protection_key = zeroed_secret(); + memory.get_io_protection_key(io_protection_key.as_mut()); + io_protection_key +} + +fn load_encryption_key(memory: &mut impl Memory) -> Box> { + let mut encryption_key = zeroed_secret(); + memory.get_encryption_key(encryption_key.as_mut()); + encryption_key +} + +/** + * This performs the CheckMac command on ATECC_SLOT_AUTHKEY. This needs to + * be called before using any slot requiring auth and whose KeyConfig.AuthKey is + * ATECC_SLOT_AUTHKEY. + */ +async fn authorize_key(memory: &mut impl Memory) -> Result<(), i32> { + let num_in = Zeroizing::new([0u8; ops::NONCE_NUMIN_SIZE]); + let rand_out = ops::chip_nonce_rand::(&num_in).await?; + let auth_key = load_auth_key(memory); + let response = ops::host_check_mac(&num_in, &rand_out, auth_key.as_ref())?; + ops::chip_checkmac::(&response).await +} + +/** + * Performs a roll-key operation on a ATECC_SLOT_ROLLKEY. + * @return ATCA_SUCCESS on success. + */ +async fn rollkey(memory: &mut impl Memory) -> Result<(), i32> { + authorize_key::(memory).await?; + + let num_in = Zeroizing::new([0u8; ops::NONCE_NUMIN_SIZE]); + let _rand_out = ops::chip_nonce_rand::(&num_in).await?; + ops::chip_derivekey_rollkey::().await +} + +/** + * Writes a new random key to ATECC_SLOT_KDF. + * @return ATCA_SUCCESS on success. + */ +async fn update_kdf_key( + random: &mut impl Random, + memory: &mut impl Memory, +) -> Result<(), i32> { + let new_key = random_32_bytes::(random).await?; + let nonce_contribution = random_32_bytes::(random).await?; + + authorize_key::(memory).await?; + + let mut num_in = Zeroizing::new([0u8; ops::NONCE_NUMIN_SIZE]); + (*num_in).copy_from_slice(&nonce_contribution[..ops::NONCE_NUMIN_SIZE]); + let rand_out = ops::chip_nonce_rand::(&num_in).await?; + ops::host_nonce(&num_in, &rand_out)?; + ops::chip_gendig_encryption_key::().await?; + let encryption_key = load_encryption_key(memory); + ops::host_gendig(encryption_key.as_ref())?; + + let (cipher, mac) = ops::host_write_auth_mac(SLOT_KDF, 0, new_key.as_ref())?; + ops::chip_write_encrypted_block::(SLOT_KDF, 0, &cipher, &mac).await +} + +async fn atecc_kdf( + memory: &mut impl Memory, + slot: Slot, + msg: &[u8; 32], +) -> Result>, Error> { + authorize_key::(memory) + .await + .map_err(Error::from_status)?; + + // The result is hkdf_extract with the msg as ikm (input key material) and + // the slot key as the salt. hkdf info does not apply, as it is part of + // hkdf_expand, which is not performed. hkdf_extract is simply hmac. + // Python equivalent: + // import hmac, hashlib; hmac.new(slot_key, msg, hashlib.sha256).digest() + let (mut kdf_out, nonce_out) = ops::chip_kdf::(slot, msg) + .await + .map_err(Error::from_status)?; + + // The chip output is encrypted with the io protection key. + let io_protection_key = load_io_protection_key(memory); + ops::host_kdf_decrypt(io_protection_key.as_ref(), &nonce_out, &mut kdf_out) + .map_err(Error::from_status)?; + Ok(Box::new(kdf_out)) +} + +// Read a "standard" sized block from a data slot (must be 32 bytes) +#[cfg(any(feature = "app-u2f", feature = "factory-setup"))] +async fn read_data_slot_block( + random: &mut impl Random, + memory: &mut impl Memory, + slot: Slot, + block: u8, +) -> Result, i32> { + let nonce_contribution = random_32_bytes::(random).await?; + + authorize_key::(memory).await?; + + let mut num_in = Zeroizing::new([0u8; ops::NONCE_NUMIN_SIZE]); + (*num_in).copy_from_slice(&nonce_contribution[..ops::NONCE_NUMIN_SIZE]); + let rand_out = ops::chip_nonce_rand::(&num_in).await?; + ops::host_nonce(&num_in, &rand_out)?; + ops::chip_gendig_encryption_key::().await?; + let encryption_key = load_encryption_key(memory); + ops::host_gendig(encryption_key.as_ref())?; + + let mut data = ops::chip_read_block::(slot, block).await?; + ops::host_io_decrypt(&mut data)?; + Ok(data) +} + +// Write a "standard" sized block from a data slot (must be 32 bytes) +#[cfg(any(feature = "app-u2f", feature = "factory-setup"))] +async fn write_data_slot_block( + random: &mut impl Random, + memory: &mut impl Memory, + bytes: &Zeroizing<[u8; BLOCK_SIZE]>, + slot: Slot, + block: u8, +) -> Result<(), i32> { + let nonce_contribution = random_32_bytes::(random).await?; + + authorize_key::(memory).await?; + + let mut num_in = Zeroizing::new([0u8; ops::NONCE_NUMIN_SIZE]); + (*num_in).copy_from_slice(&nonce_contribution[..ops::NONCE_NUMIN_SIZE]); + let rand_out = ops::chip_nonce_rand::(&num_in).await?; + ops::host_nonce(&num_in, &rand_out)?; + ops::chip_gendig_encryption_key::().await?; + let encryption_key = load_encryption_key(memory); + ops::host_gendig(encryption_key.as_ref())?; + + let (cipher, mac) = ops::host_write_auth_mac(slot, block, bytes)?; + ops::chip_write_encrypted_block::(slot, block, &cipher, &mac).await?; + + // Double-check by reading it back and comparing. + let written_bytes = read_data_slot_block::(random, memory, slot, block).await?; + if written_bytes[..BLOCK_SIZE] == bytes[..BLOCK_SIZE] { + Ok(()) + } else { + Err(-1) } } -pub fn reset_keys() -> Result<(), ()> { - match unsafe { bitbox_securechip_sys::atecc_reset_keys() } { - true => Ok(()), - false => Err(()), +async fn random_inner() -> Result>, Error> { + let mut last_err = 0; + for _ in 0..RANDOM_RETRIES { + match ops::chip_random::().await { + Ok(random) => return Ok(Box::new(random)), + Err(err) => last_err = err, + } } + Err(Error::from_status(last_err)) } -pub fn init_new_password( - _memory: &mut impl Memory, +async fn reset_keys_inner( + random: &mut impl Random, + memory: &mut impl Memory, +) -> Result<(), ()> { + rollkey::(memory).await.map_err(|_| ())?; + update_kdf_key::(random, memory).await.map_err(|_| ()) +} + +async fn stretch_password_inner( + memory: &mut impl Memory, password: &str, - password_stretch_algo: PasswordStretchAlgo, ) -> Result>, Error> { - let password = util::strings::str_to_cstr_vec_zeroizing(password) - .map_err(|_| Error::SecureChip(SecureChipError::SC_ERR_INVALID_ARGS))?; - let mut stretched = Box::new(Zeroizing::new([0u8; 32])); - let status = unsafe { - bitbox_securechip_sys::atecc_init_new_password( - password.as_ptr().cast(), - password_stretch_algo, - stretched.as_mut_ptr(), - ) - }; - if status == 0 { - Ok(stretched) - } else { - Err(Error::from_status(status)) + let password_salted_hashed = + bitbox_core_utils::salt::hash_data(memory, password.as_bytes(), "keystore_seed_access_in") + .map_err(|_| Error::SecureChip(SecureChipError::SC_ERR_SALT))?; + + let mut kdf_in = zeroed_secret::(); + (*kdf_in).copy_from_slice(password_salted_hashed.as_slice()); + + // First KDF on rollkey increments the monotonic counter. Call only once! + let mut stretched = atecc_kdf::(memory, SLOT_ROLLKEY, kdf_in.as_ref()).await?; + // Second KDF does not use the counter and we call it multiple times. + for _ in 0..KDF_NUM_ITERATIONS { + (*kdf_in).copy_from_slice(stretched.as_slice()); + stretched = atecc_kdf::(memory, SLOT_KDF, kdf_in.as_ref()).await?; } + + let password_salted_hashed = + bitbox_core_utils::salt::hash_data(memory, password.as_bytes(), "keystore_seed_access_out") + .map_err(|_| Error::SecureChip(SecureChipError::SC_ERR_SALT))?; + hmac_sha256_overwrite(password_salted_hashed.as_slice(), &mut stretched); + Ok(stretched) +} + +pub async fn attestation_sign( + memory: &mut impl Memory, + challenge: &[u8; 32], + signature: &mut [u8; 64], +) -> Result<(), ()> { + let _guard = begin_high_level_op().map_err(|_| ())?; + authorize_key::(memory).await.map_err(|_| ())?; + + // Keep the device RNG seeded before loading the external digest, matching calib_sign(). + let _throwaway = ops::chip_random::().await.map_err(|_| ())?; + ops::chip_nonce_load_msgdigest::(challenge) + .await + .map_err(|_| ())?; + let chip_signature = ops::chip_sign_attestation::().await.map_err(|_| ())?; + signature.copy_from_slice(&chip_signature); + Ok(()) +} + +pub async fn random() -> Result>, Error> { + let _guard = begin_high_level_op()?; + random_inner::().await +} + +pub async fn monotonic_increments_remaining() -> Result { + let _guard = begin_high_level_op().map_err(|_| ())?; + let counter = ops::chip_counter_read::().await.map_err(|_| ())?; + if COUNTER_MAX_VALUE < counter { + panic!("ATECC returned an invalid value"); + } + Ok(COUNTER_MAX_VALUE.wrapping_sub(counter)) +} + +pub async fn reset_keys( + random: &mut impl Random, + memory: &mut impl Memory, +) -> Result<(), ()> { + let _guard = begin_high_level_op().map_err(|_| ())?; + reset_keys_inner::(random, memory).await } -pub fn stretch_password( - _memory: &mut impl Memory, +pub async fn init_new_password( + random: &mut impl Random, + memory: &mut impl Memory, password: &str, password_stretch_algo: PasswordStretchAlgo, ) -> Result>, Error> { - let password = util::strings::str_to_cstr_vec_zeroizing(password) - .map_err(|_| Error::SecureChip(SecureChipError::SC_ERR_INVALID_ARGS))?; - let mut stretched = Box::new(Zeroizing::new([0u8; 32])); - let status = unsafe { - bitbox_securechip_sys::atecc_stretch_password( - password.as_ptr().cast(), - password_stretch_algo, - stretched.as_mut_ptr(), - ) - }; - if status == 0 { - Ok(stretched) - } else { - Err(Error::from_status(status)) + if password_stretch_algo != PasswordStretchAlgo::SECURECHIP_PASSWORD_STRETCH_ALGO_V0 { + return Err(Error::SecureChip( + SecureChipError::SC_ERR_INVALID_PASSWORD_STRETCH_ALGO, + )); } + + let _guard = begin_high_level_op()?; + reset_keys_inner::(random, memory) + .await + .map_err(|_| Error::SecureChip(SecureChipError::SC_ATECC_ERR_RESET_KEYS))?; + stretch_password_inner::(memory, password).await } -pub fn kdf(msg: &[u8; 32]) -> Result>, Error> { - let mut result = Box::new(Zeroizing::new([0u8; 32])); - let status = - unsafe { bitbox_securechip_sys::atecc_kdf(msg.as_ptr(), msg.len(), result.as_mut_ptr()) }; - if status == 0 { - Ok(result) - } else { - Err(Error::from_status(status)) +pub async fn stretch_password( + memory: &mut impl Memory, + password: &str, + password_stretch_algo: PasswordStretchAlgo, +) -> Result>, Error> { + if password_stretch_algo != PasswordStretchAlgo::SECURECHIP_PASSWORD_STRETCH_ALGO_V0 { + return Err(Error::SecureChip( + SecureChipError::SC_ERR_INVALID_PASSWORD_STRETCH_ALGO, + )); } + + let _guard = begin_high_level_op()?; + stretch_password_inner::(memory, password).await +} + +pub async fn kdf( + memory: &mut impl Memory, + msg: &[u8; 32], +) -> Result>, Error> { + let _guard = begin_high_level_op()?; + atecc_kdf::(memory, SLOT_KDF, msg).await } #[cfg(any(feature = "app-u2f", feature = "factory-setup"))] -pub fn u2f_counter_set(counter: u32) -> Result<(), ()> { - match unsafe { bitbox_securechip_sys::atecc_u2f_counter_set(counter) } { - true => Ok(()), - false => Err(()), - } +pub async fn u2f_counter_set( + random: &mut impl Random, + memory: &mut impl Memory, + counter: u32, +) -> Result<(), ()> { + let _guard = begin_high_level_op().map_err(|_| ())?; + let mut data = read_data_slot_block::(random, memory, SLOT_DATA0, 0) + .await + .map_err(|_| ())?; + (*data)[..4].copy_from_slice(&counter.to_le_bytes()); + write_data_slot_block::(random, memory, &data, SLOT_DATA0, 0) + .await + .map_err(|_| ()) } #[cfg(feature = "app-u2f")] -pub fn u2f_counter_inc() -> Result { - let mut counter = 0; - match unsafe { bitbox_securechip_sys::atecc_u2f_counter_inc(&mut counter) } { - true => Ok(counter), - false => Err(()), - } +pub async fn u2f_counter_inc( + random: &mut impl Random, + memory: &mut impl Memory, +) -> Result { + let _guard = begin_high_level_op().map_err(|_| ())?; + let mut data = read_data_slot_block::(random, memory, SLOT_DATA0, 0) + .await + .map_err(|_| ())?; + let current = u32::from_le_bytes(data[..4].try_into().unwrap()).wrapping_add(1); + (*data)[..4].copy_from_slice(¤t.to_le_bytes()); + write_data_slot_block::(random, memory, &data, SLOT_DATA0, 0) + .await + .map_err(|_| ())?; + Ok(current) +} + +pub async fn model() -> Result { + let _guard = begin_high_level_op().map_err(|_| ())?; + let revision = ops::chip_info_revision::().await.map_err(|_| ())?; + Ok(if revision[3] >= 0x03 { + Model::ATECC_ATECC608B + } else { + Model::ATECC_ATECC608A + }) } -pub fn model() -> Result { - let mut model = core::mem::MaybeUninit::uninit(); - match unsafe { bitbox_securechip_sys::atecc_model(model.as_mut_ptr()) } { - true => Ok(unsafe { model.assume_init() }), - false => Err(()), +#[cfg(test)] +mod tests { + use super::*; + use bitbox_platform_host::memory::FakeMemory; + use bitbox_platform_host::random::TestingRandom; + use core::{task::Poll, time::Duration}; + use hex_lit::hex; + + const SALT_ROOT_FIXED: [u8; 32] = [0x42; 32]; + const EXPECTED_STRETCHED_OUT_V0: [u8; 32] = + hex!("868f9f41fe3122cdcd0be180779401d7f68e1fa8744a5bbd4f3f117a316da1d5"); + + struct TestTimer; + + impl Timer for TestTimer { + async fn delay_for(_duration: Duration) {} + } + + fn setup_test() -> ( + std::sync::MutexGuard<'static, ()>, + FakeMemory, + TestingRandom, + ) { + let guard = ops::test_lock(); + ops::test_reset(); + let mut memory = FakeMemory::new(); + memory.set_salt_root(&SALT_ROOT_FIXED); + (guard, memory, TestingRandom::new()) + } + + #[async_test::test] + #[allow(clippy::await_holding_lock)] + async fn test_reset_keys() { + let (_guard, mut memory, mut random) = setup_test(); + reset_keys::(&mut random, &mut memory) + .await + .unwrap(); + assert_eq!(ops::test_get_derivekey_rollkey_calls(), 1); + assert_eq!(ops::test_get_kdf_key_write_calls(), 1); + assert!(!ops::test_io_temp_key_active()); + } + + #[async_test::test] + #[allow(clippy::await_holding_lock)] + async fn test_stretch_password() { + let (_guard, mut memory, _random) = setup_test(); + let stretched = stretch_password::( + &mut memory, + "pw", + PasswordStretchAlgo::SECURECHIP_PASSWORD_STRETCH_ALGO_V0, + ) + .await + .unwrap(); + assert_eq!(stretched.as_slice(), EXPECTED_STRETCHED_OUT_V0.as_slice(),); + assert_eq!(ops::test_get_rollkey_kdf_calls(), 1); + assert_eq!(ops::test_get_kdf_calls(), KDF_NUM_ITERATIONS); + } + + #[test] + fn test_random_busy() { + let (_guard, _memory, _random) = setup_test(); + ops::test_block_next_chip_command(); + let mut first: util::bb02_async::Task<_> = alloc::boxed::Box::pin(random::()); + assert!(matches!(util::bb02_async::spin(&mut first), Poll::Pending)); + + assert_eq!( + util::bb02_async::block_on(random::()), + Err(Error::Status(ATECC_OPS_STATUS_BUSY)), + ); + + ops::test_unblock_chip_command(); + drop(util::bb02_async::block_on(first).unwrap()); + } + + #[test] + fn test_model_busy() { + let (_guard, _memory, _random) = setup_test(); + ops::test_block_next_chip_command(); + let mut first: util::bb02_async::Task<_> = alloc::boxed::Box::pin(random::()); + assert!(matches!(util::bb02_async::spin(&mut first), Poll::Pending)); + + assert_eq!(util::bb02_async::block_on(model::()), Err(())); + + ops::test_unblock_chip_command(); + drop(util::bb02_async::block_on(first).unwrap()); + } + + #[test] + fn test_drop_releases_high_level_guard() { + let (_guard, _memory, _random) = setup_test(); + ops::test_block_next_chip_command(); + let mut first: util::bb02_async::Task<_> = alloc::boxed::Box::pin(random::()); + assert!(matches!(util::bb02_async::spin(&mut first), Poll::Pending)); + + drop(first); + + assert!(util::bb02_async::block_on(random::()).is_ok()); + } + + #[test] + fn test_drop_clears_io_temp_key() { + let (_guard, _memory, _random) = setup_test(); + let mut task: util::bb02_async::Task<_> = alloc::boxed::Box::pin(async { + let _guard = begin_high_level_op().unwrap(); + let num_in = Zeroizing::new([0u8; ops::NONCE_NUMIN_SIZE]); + let rand_out = Zeroizing::new([0u8; 32]); + ops::host_nonce(&num_in, &rand_out).unwrap(); + core::future::pending::<()>().await; + }); + assert!(matches!(util::bb02_async::spin(&mut task), Poll::Pending)); + assert!(ops::test_io_temp_key_active()); + + drop(task); + + assert!(!ops::test_io_temp_key_active()); } } diff --git a/src/rust/bitbox-securechip/src/atecc/ops.rs b/src/rust/bitbox-securechip/src/atecc/ops.rs new file mode 100644 index 0000000000..5c4e363c57 --- /dev/null +++ b/src/rust/bitbox-securechip/src/atecc/ops.rs @@ -0,0 +1,399 @@ +// SPDX-License-Identifier: Apache-2.0 + +use bitbox_hal::timer::Timer; +use bitbox_securechip_sys::atecc_slot_t as Slot; +use core::time::Duration; +use util::cell::SyncCell; +use zeroize::Zeroizing; + +const ATECC_OPS_STATUS_BUSY: i32 = bitbox_securechip_sys::ATECC_OPS_STATUS_BUSY as i32; +pub(super) const NONCE_NUMIN_SIZE: usize = bitbox_securechip_sys::NONCE_NUMIN_SIZE as usize; +const BLOCK_SIZE: usize = 32; +const SIGNATURE_SIZE: usize = 64; + +#[derive(Copy, Clone, Eq, PartialEq)] +enum AsyncOpState { + // No async Rust wrapper owns the shared ATECC command context. + Idle, + // One live Rust future launched an ATECC command and still needs to observe the result. + Running, + // The Rust future was dropped after launching the command, but the C command has not + // completed yet. The old command may still be using the shared command context, so a new call + // must not reset it yet. + Detached, +} + +// The ATECC C API exposes only one shared async command context and status variable, so the Rust +// wrappers can support only one in-flight async operation at a time. The extra Detached state is +// needed because cancellation must not free the slot until polling has really observed completion +// of the old command. +static STATE: SyncCell = SyncCell::new(AsyncOpState::Idle); + +struct AsyncOpGuard { + armed: bool, +} + +impl AsyncOpGuard { + fn new() -> Self { + Self { armed: true } + } + + fn disarm(&mut self) { + self.armed = false; + } +} + +impl Drop for AsyncOpGuard { + fn drop(&mut self) { + if !self.armed { + return; + } + + if poll_status() == ATECC_OPS_STATUS_BUSY { + STATE.write(AsyncOpState::Detached); + } else { + STATE.write(AsyncOpState::Idle); + } + } +} + +fn poll_status() -> i32 { + unsafe { bitbox_securechip_sys::atecc_ops_get_status() } +} + +fn ensure_success(status: i32) -> Result<(), i32> { + if status == 0 { Ok(()) } else { Err(status) } +} + +pub(super) fn clear_io_temp_key() { + unsafe { + bitbox_securechip_sys::atecc_io_clear_tempkey(); + } +} + +async fn poll_once() { + let delay_ms = unsafe { bitbox_securechip_sys::atecc_ops_get_poll_delay_ms() } as u64; + if delay_ms > 0 { + T::delay_for(Duration::from_millis(delay_ms)).await; + } + unsafe { + bitbox_securechip_sys::atecc_ops_poll(); + } +} + +async fn reclaim_detached_op() { + match STATE.read() { + AsyncOpState::Idle => { + // Another path already observed completion and released the slot, so there is nothing + // left to reclaim here. + return; + } + AsyncOpState::Running => { + // Sequential callers are required for this wrapper. Reaching this arm means some other + // live future is still using the shared ATECC command context. + panic!("concurrent async atecc operation not supported"); + } + AsyncOpState::Detached => { + // Detached means the old future is gone. Under the sequential-caller assumption, this + // recovery future is now the only one that can wait until polling observes completion + // and then release the shared command context for reuse. + } + } + + let mut guard = AsyncOpGuard::new(); + let _ = wait_until_not_busy::().await; + guard.disarm(); + STATE.write(AsyncOpState::Idle); +} + +async fn begin_async_op() -> AsyncOpGuard { + loop { + match STATE.read() { + AsyncOpState::Idle => { + // We are the only Rust future touching the shared ATECC command context. It is + // safe to launch a new command unless the C status still reports a busy operation, + // in which case a start helper would reset a command that may still be active. + if poll_status() == ATECC_OPS_STATUS_BUSY { + STATE.write(AsyncOpState::Detached); + reclaim_detached_op::().await; + continue; + } + STATE.write(AsyncOpState::Running); + return AsyncOpGuard::new(); + } + AsyncOpState::Running => { + // Sequential callers are required. If we are asked to start another operation while + // one live future still owns the shared command context, something above this + // wrapper broke that invariant. + panic!("concurrent async atecc operation not supported"); + } + AsyncOpState::Detached => { + // A previous future was cancelled after launching the command. Wait until polling + // has really observed completion before reusing the shared command context. + reclaim_detached_op::().await; + } + } + } +} + +fn end_async_op() { + STATE.write(AsyncOpState::Idle); +} + +async fn wait_with_cleanup( + mut guard: AsyncOpGuard, + initial_status: i32, +) -> Result<(), i32> { + let result = wait::(initial_status).await; + guard.disarm(); + end_async_op(); + result +} + +async fn wait_until_not_busy() -> i32 { + loop { + match poll_status() { + ATECC_OPS_STATUS_BUSY => poll_once::().await, + status => return status, + } + } +} + +async fn wait(initial_status: i32) -> Result<(), i32> { + if initial_status != 0 { + return Err(initial_status); + } + + match wait_until_not_busy::().await { + 0 => Ok(()), + status => Err(status), + } +} + +pub(super) async fn chip_nonce_rand( + num_in: &[u8; NONCE_NUMIN_SIZE], +) -> Result, i32> { + let mut rand_out = Zeroizing::new([0u8; 32]); + let guard = begin_async_op::().await; + wait_with_cleanup::(guard, unsafe { + bitbox_securechip_sys::atecc_cmd_start_nonce_rand(num_in.as_ptr()) + }) + .await?; + ensure_success(unsafe { + bitbox_securechip_sys::atecc_cmd_read_random_response((*rand_out).as_mut_ptr()) + })?; + Ok(rand_out) +} + +pub(super) async fn chip_checkmac(response: &Zeroizing<[u8; 32]>) -> Result<(), i32> { + let guard = begin_async_op::().await; + wait_with_cleanup::(guard, unsafe { + bitbox_securechip_sys::atecc_cmd_start_checkmac(response.as_ptr()) + }) + .await +} + +pub(super) async fn chip_random() -> Result, i32> { + let mut rand_out = Zeroizing::new([0u8; 32]); + let guard = begin_async_op::().await; + wait_with_cleanup::(guard, unsafe { + bitbox_securechip_sys::atecc_cmd_start_random() + }) + .await?; + ensure_success(unsafe { + bitbox_securechip_sys::atecc_cmd_read_random_response((*rand_out).as_mut_ptr()) + })?; + Ok(rand_out) +} + +pub(super) async fn chip_counter_read() -> Result { + let mut counter = 0u32; + let guard = begin_async_op::().await; + wait_with_cleanup::(guard, unsafe { + bitbox_securechip_sys::atecc_cmd_start_counter_read() + }) + .await?; + ensure_success(unsafe { + bitbox_securechip_sys::atecc_cmd_read_counter_response(&mut counter) + })?; + Ok(counter) +} + +pub(super) async fn chip_info_revision() -> Result<[u8; 4], i32> { + let mut revision = [0u8; 4]; + let guard = begin_async_op::().await; + wait_with_cleanup::(guard, unsafe { + bitbox_securechip_sys::atecc_cmd_start_info_revision() + }) + .await?; + ensure_success(unsafe { + bitbox_securechip_sys::atecc_cmd_read_info_response(revision.as_mut_ptr()) + })?; + Ok(revision) +} + +pub(super) async fn chip_kdf( + slot: Slot, + msg: &[u8; 32], +) -> Result<(Zeroizing<[u8; 32]>, Zeroizing<[u8; 32]>), i32> { + let mut kdf_out = Zeroizing::new([0u8; 32]); + let mut nonce_out = Zeroizing::new([0u8; 32]); + let guard = begin_async_op::().await; + wait_with_cleanup::(guard, unsafe { + bitbox_securechip_sys::atecc_cmd_start_kdf(slot, msg.as_ptr(), msg.len()) + }) + .await?; + ensure_success(unsafe { + bitbox_securechip_sys::atecc_cmd_read_kdf_response( + (*kdf_out).as_mut_ptr(), + (*nonce_out).as_mut_ptr(), + ) + })?; + Ok((kdf_out, nonce_out)) +} + +pub(super) async fn chip_derivekey_rollkey() -> Result<(), i32> { + let guard = begin_async_op::().await; + wait_with_cleanup::(guard, unsafe { + bitbox_securechip_sys::atecc_cmd_start_derivekey_rollkey() + }) + .await +} + +pub(super) async fn chip_nonce_load_msgdigest(msg: &[u8; 32]) -> Result<(), i32> { + let guard = begin_async_op::().await; + wait_with_cleanup::(guard, unsafe { + bitbox_securechip_sys::atecc_cmd_start_nonce_load_msgdigest(msg.as_ptr()) + }) + .await +} + +pub(super) async fn chip_sign_attestation() -> Result<[u8; SIGNATURE_SIZE], i32> { + let mut signature = [0u8; SIGNATURE_SIZE]; + let guard = begin_async_op::().await; + wait_with_cleanup::(guard, unsafe { + bitbox_securechip_sys::atecc_cmd_start_sign_attestation() + }) + .await?; + ensure_success(unsafe { + bitbox_securechip_sys::atecc_cmd_read_sign_response(signature.as_mut_ptr()) + })?; + Ok(signature) +} + +pub(super) async fn chip_gendig_encryption_key() -> Result<(), i32> { + let guard = begin_async_op::().await; + wait_with_cleanup::(guard, unsafe { + bitbox_securechip_sys::atecc_cmd_start_gendig_encryption_key() + }) + .await +} + +#[cfg(any(feature = "app-u2f", feature = "factory-setup"))] +pub(super) async fn chip_read_block( + slot: Slot, + block: u8, +) -> Result, i32> { + let mut data = Zeroizing::new([0u8; BLOCK_SIZE]); + let guard = begin_async_op::().await; + wait_with_cleanup::(guard, unsafe { + bitbox_securechip_sys::atecc_cmd_start_read_block(slot as u16, block) + }) + .await?; + ensure_success(unsafe { + bitbox_securechip_sys::atecc_cmd_read_block_response((*data).as_mut_ptr()) + })?; + Ok(data) +} + +pub(super) async fn chip_write_encrypted_block( + slot: Slot, + block: u8, + value: &Zeroizing<[u8; BLOCK_SIZE]>, + mac: &Zeroizing<[u8; BLOCK_SIZE]>, +) -> Result<(), i32> { + let guard = begin_async_op::().await; + wait_with_cleanup::(guard, unsafe { + bitbox_securechip_sys::atecc_cmd_start_write_encrypted_block( + slot as u16, + block, + value.as_ptr(), + mac.as_ptr(), + ) + }) + .await +} + +// The C helper first calculates the contents of TempKey with atcah_nonce(), then computes the +// client response with atcah_check_mac(). In this CheckMac mode, the first SHA block comes from +// the slot key and the second from TempKey, so Rust keeps those two host steps bundled here. +pub(super) fn host_check_mac( + num_in: &[u8; NONCE_NUMIN_SIZE], + rand_out: &Zeroizing<[u8; 32]>, + auth_key: &[u8; 32], +) -> Result, i32> { + let mut response = Zeroizing::new([0u8; 32]); + ensure_success(unsafe { + bitbox_securechip_sys::atecc_auth_compute_response( + num_in.as_ptr(), + rand_out.as_ptr(), + auth_key.as_ptr(), + (*response).as_mut_ptr(), + ) + })?; + Ok(response) +} + +pub(super) fn host_kdf_decrypt( + io_protection_key: &[u8; 32], + nonce_out: &Zeroizing<[u8; 32]>, + data: &mut Zeroizing<[u8; 32]>, +) -> Result<(), i32> { + ensure_success(unsafe { + bitbox_securechip_sys::atecc_kdf_decrypt( + io_protection_key.as_ptr(), + nonce_out.as_ptr(), + (*data).as_mut_ptr(), + BLOCK_SIZE, + ) + }) +} + +pub(super) fn host_nonce( + num_in: &[u8; NONCE_NUMIN_SIZE], + rand_out: &Zeroizing<[u8; 32]>, +) -> Result<(), i32> { + ensure_success(unsafe { + bitbox_securechip_sys::atecc_io_prepare_tempkey(num_in.as_ptr(), rand_out.as_ptr()) + }) +} + +pub(super) fn host_gendig(encryption_key: &[u8; 32]) -> Result<(), i32> { + ensure_success(unsafe { bitbox_securechip_sys::atecc_io_apply_gendig(encryption_key.as_ptr()) }) +} + +pub(super) fn host_write_auth_mac( + slot: Slot, + block: u8, + input_data: &Zeroizing<[u8; BLOCK_SIZE]>, +) -> Result<(Zeroizing<[u8; BLOCK_SIZE]>, Zeroizing<[u8; BLOCK_SIZE]>), i32> { + let mut encrypted = Zeroizing::new([0u8; BLOCK_SIZE]); + let mut mac = Zeroizing::new([0u8; BLOCK_SIZE]); + ensure_success(unsafe { + bitbox_securechip_sys::atecc_io_prepare_encrypted_write( + slot as u16, + block, + input_data.as_ptr(), + (*encrypted).as_mut_ptr(), + (*mac).as_mut_ptr(), + ) + })?; + Ok((encrypted, mac)) +} + +#[cfg(any(feature = "app-u2f", feature = "factory-setup"))] +pub(super) fn host_io_decrypt(data: &mut Zeroizing<[u8; BLOCK_SIZE]>) -> Result<(), i32> { + ensure_success(unsafe { + bitbox_securechip_sys::atecc_io_decrypt_block((*data).as_mut_ptr(), BLOCK_SIZE) + }) +} diff --git a/src/rust/bitbox-securechip/src/atecc/ops_fake.rs b/src/rust/bitbox-securechip/src/atecc/ops_fake.rs new file mode 100644 index 0000000000..bf5ca8eb6b --- /dev/null +++ b/src/rust/bitbox-securechip/src/atecc/ops_fake.rs @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: Apache-2.0 + +#![allow(clippy::extra_unused_type_parameters)] + +use bitbox_hal::timer::Timer; +use bitbox_securechip_sys::atecc_slot_t as Slot; +use core::future::poll_fn; +use core::task::Poll; +use std::sync::{LazyLock, Mutex, MutexGuard}; +use util::sha2::hmac_sha256 as util_hmac_sha256; +use zeroize::Zeroizing; + +pub(super) const NONCE_NUMIN_SIZE: usize = bitbox_securechip_sys::NONCE_NUMIN_SIZE as usize; +const BLOCK_SIZE: usize = 32; +const SLOT_ROLLKEY: Slot = Slot::ATECC_SLOT_ROLLKEY; +const SLOT_KDF: Slot = Slot::ATECC_SLOT_KDF; +const SLOT_DATA0: Slot = Slot::ATECC_SLOT_DATA0; + +#[derive(Clone)] +struct FakeState { + data0_block0: [u8; BLOCK_SIZE], + block_next_chip_command: bool, + blocked_chip_command_active: bool, + derivekey_rollkey_calls: usize, + rollkey_kdf_calls: usize, + kdf_calls: usize, + kdf_key_write_calls: usize, + io_temp_key_active: bool, +} + +impl Default for FakeState { + fn default() -> Self { + Self { + data0_block0: [0; BLOCK_SIZE], + block_next_chip_command: false, + blocked_chip_command_active: false, + derivekey_rollkey_calls: 0, + rollkey_kdf_calls: 0, + kdf_calls: 0, + kdf_key_write_calls: 0, + io_temp_key_active: false, + } + } +} + +static TEST_LOCK: Mutex<()> = Mutex::new(()); +static STATE: LazyLock> = LazyLock::new(|| Mutex::new(FakeState::default())); + +fn lock_state() -> MutexGuard<'static, FakeState> { + STATE.lock().unwrap() +} + +fn fake_hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] { + let mut result = [0u8; 32]; + util_hmac_sha256(key, data, &mut result); + result +} + +async fn maybe_block_next_chip_command() { + let should_block = { + let mut state = lock_state(); + if state.block_next_chip_command { + state.block_next_chip_command = false; + state.blocked_chip_command_active = true; + true + } else { + false + } + }; + + if !should_block { + return; + } + + poll_fn(|_| { + if lock_state().blocked_chip_command_active { + Poll::Pending + } else { + Poll::Ready(()) + } + }) + .await; +} + +pub(super) fn test_lock() -> MutexGuard<'static, ()> { + TEST_LOCK.lock().unwrap() +} + +pub(super) fn test_reset() { + *lock_state() = FakeState::default(); +} + +pub(super) fn test_block_next_chip_command() { + let mut state = lock_state(); + state.block_next_chip_command = true; +} + +pub(super) fn test_unblock_chip_command() { + let mut state = lock_state(); + state.blocked_chip_command_active = false; +} + +pub(super) fn test_get_derivekey_rollkey_calls() -> usize { + lock_state().derivekey_rollkey_calls +} + +pub(super) fn test_get_rollkey_kdf_calls() -> usize { + lock_state().rollkey_kdf_calls +} + +pub(super) fn test_get_kdf_calls() -> usize { + lock_state().kdf_calls +} + +pub(super) fn test_get_kdf_key_write_calls() -> usize { + lock_state().kdf_key_write_calls +} + +pub(super) fn test_io_temp_key_active() -> bool { + lock_state().io_temp_key_active +} + +pub(super) fn clear_io_temp_key() { + lock_state().io_temp_key_active = false; +} + +pub(super) async fn chip_nonce_rand( + _num_in: &[u8; NONCE_NUMIN_SIZE], +) -> Result, i32> { + maybe_block_next_chip_command().await; + Ok(Zeroizing::new([0u8; 32])) +} + +pub(super) async fn chip_checkmac(_response: &Zeroizing<[u8; 32]>) -> Result<(), i32> { + maybe_block_next_chip_command().await; + Ok(()) +} + +pub(super) async fn chip_random() -> Result, i32> { + maybe_block_next_chip_command().await; + Ok(Zeroizing::new([0u8; 32])) +} + +pub(super) async fn chip_counter_read() -> Result { + maybe_block_next_chip_command().await; + Ok(2_097_150) +} + +pub(super) async fn chip_info_revision() -> Result<[u8; 4], i32> { + maybe_block_next_chip_command().await; + Ok([0, 0, 0, 0x03]) +} + +pub(super) async fn chip_kdf( + slot: Slot, + msg: &[u8; 32], +) -> Result<(Zeroizing<[u8; 32]>, Zeroizing<[u8; 32]>), i32> { + maybe_block_next_chip_command().await; + + let key = if slot == SLOT_ROLLKEY { + let mut state = lock_state(); + state.rollkey_kdf_calls += 1; + b"atecc-rollkey".as_slice() + } else { + let mut state = lock_state(); + state.kdf_calls += 1; + b"atecc-kdf".as_slice() + }; + Ok(( + Zeroizing::new(fake_hmac_sha256(key, msg)), + Zeroizing::new([0u8; 32]), + )) +} + +pub(super) async fn chip_derivekey_rollkey() -> Result<(), i32> { + maybe_block_next_chip_command().await; + let mut state = lock_state(); + state.derivekey_rollkey_calls += 1; + Ok(()) +} + +pub(super) async fn chip_nonce_load_msgdigest(_msg: &[u8; 32]) -> Result<(), i32> { + maybe_block_next_chip_command().await; + Ok(()) +} + +pub(super) async fn chip_sign_attestation() -> Result<[u8; 64], i32> { + maybe_block_next_chip_command().await; + Err(-1) +} + +pub(super) async fn chip_gendig_encryption_key() -> Result<(), i32> { + maybe_block_next_chip_command().await; + Ok(()) +} + +pub(super) async fn chip_read_block( + slot: Slot, + block: u8, +) -> Result, i32> { + maybe_block_next_chip_command().await; + if slot == SLOT_DATA0 && block == 0 { + Ok(Zeroizing::new(lock_state().data0_block0)) + } else { + Ok(Zeroizing::new([0u8; BLOCK_SIZE])) + } +} + +pub(super) async fn chip_write_encrypted_block( + slot: Slot, + block: u8, + value: &Zeroizing<[u8; BLOCK_SIZE]>, + _mac: &Zeroizing<[u8; BLOCK_SIZE]>, +) -> Result<(), i32> { + maybe_block_next_chip_command().await; + if slot == SLOT_DATA0 && block == 0 { + let mut state = lock_state(); + state.data0_block0 = **value; + } + Ok(()) +} + +pub(super) fn host_check_mac( + _num_in: &[u8; NONCE_NUMIN_SIZE], + _rand_out: &Zeroizing<[u8; 32]>, + _auth_key: &[u8; 32], +) -> Result, i32> { + Ok(Zeroizing::new([0u8; 32])) +} + +pub(super) fn host_kdf_decrypt( + _io_protection_key: &[u8; 32], + _nonce_out: &Zeroizing<[u8; 32]>, + _data: &mut Zeroizing<[u8; 32]>, +) -> Result<(), i32> { + Ok(()) +} + +pub(super) fn host_nonce( + _num_in: &[u8; NONCE_NUMIN_SIZE], + _rand_out: &Zeroizing<[u8; 32]>, +) -> Result<(), i32> { + lock_state().io_temp_key_active = true; + Ok(()) +} + +pub(super) fn host_gendig(_encryption_key: &[u8; 32]) -> Result<(), i32> { + Ok(()) +} + +pub(super) fn host_write_auth_mac( + slot: Slot, + _block: u8, + input_data: &Zeroizing<[u8; BLOCK_SIZE]>, +) -> Result<(Zeroizing<[u8; BLOCK_SIZE]>, Zeroizing<[u8; BLOCK_SIZE]>), i32> { + if slot == SLOT_KDF { + let mut state = lock_state(); + state.kdf_key_write_calls += 1; + } + Ok(( + Zeroizing::new(**input_data), + Zeroizing::new([0u8; BLOCK_SIZE]), + )) +} + +pub(super) fn host_io_decrypt(_data: &mut Zeroizing<[u8; BLOCK_SIZE]>) -> Result<(), i32> { + Ok(()) +} diff --git a/src/rust/bitbox-securechip/src/optiga.rs b/src/rust/bitbox-securechip/src/optiga.rs index 9d599d6ece..0cab168a2e 100644 --- a/src/rust/bitbox-securechip/src/optiga.rs +++ b/src/rust/bitbox-securechip/src/optiga.rs @@ -535,7 +535,7 @@ pub async fn u2f_counter_inc() -> Result { Ok(counter) } -pub fn model() -> Result { +pub async fn model() -> Result { Ok(Model::OPTIGA_TRUST_M_V3) } diff --git a/src/rust/bitbox02-rust/src/attestation.rs b/src/rust/bitbox02-rust/src/attestation.rs index b2bad6125a..f465b5277d 100644 --- a/src/rust/bitbox02-rust/src/attestation.rs +++ b/src/rust/bitbox02-rust/src/attestation.rs @@ -26,9 +26,14 @@ pub async fn perform(hal: &mut impl crate::hal::Hal, host_challenge: [u8; 32]) - )?; result.bootloader_hash = hal.memory().get_attestation_bootloader_hash(); let hash: [u8; 32] = Sha256::digest(host_challenge).into(); - hal.securechip() - .attestation_sign(&hash, &mut result.challenge_signature) - .await?; + { + let crate::hal::HalSubsystems { + securechip, memory, .. + } = hal.as_mut(); + securechip + .attestation_sign(memory, &hash, &mut result.challenge_signature) + .await?; + } Ok(result) } diff --git a/src/rust/bitbox02-rust/src/hww/api/device_info.rs b/src/rust/bitbox02-rust/src/hww/api/device_info.rs index 7252821a1b..642c6455c9 100644 --- a/src/rust/bitbox02-rust/src/hww/api/device_info.rs +++ b/src/rust/bitbox02-rust/src/hww/api/device_info.rs @@ -38,7 +38,7 @@ pub async fn process(hal: &mut impl crate::hal::Hal) -> Result version: crate::version::FIRMWARE_VERSION_SHORT.into(), mnemonic_passphrase_enabled: hal.memory().is_mnemonic_passphrase_enabled(), monotonic_increments_remaining: hal.securechip().monotonic_increments_remaining().await?, - securechip_model: match hal.securechip().model()? { + securechip_model: match hal.securechip().model().await? { securechip::Model::Atecc608A => "ATECC608A".into(), securechip::Model::Atecc608B => "ATECC608B".into(), securechip::Model::OptigaTrustM3 => "OPTIGA_TRUST_M_V3".into(), diff --git a/src/rust/bitbox02-rust/src/hww/api/restore.rs b/src/rust/bitbox02-rust/src/hww/api/restore.rs index ddd5e91c25..a5c5e4a793 100644 --- a/src/rust/bitbox02-rust/src/hww/api/restore.rs +++ b/src/rust/bitbox02-rust/src/hww/api/restore.rs @@ -79,7 +79,17 @@ pub async fn from_file( { // Ignore error - the U2f counter not being set can lead to problems with U2F, but it should // not fail the recovery, so the user can access their coins. - let _ = hal.securechip().u2f_counter_set(request.timestamp).await; + let _ = { + let crate::hal::HalSubsystems { + securechip, + random, + memory, + .. + } = hal.as_mut(); + securechip + .u2f_counter_set(random, memory, request.timestamp) + .await + }; } hal.memory().set_initialized().or(Err(Error::Memory))?; @@ -160,7 +170,15 @@ pub async fn from_mnemonic( { // Ignore error - the U2f counter not being set can lead to problems with U2F, but it should // not fail the recovery, so the user can access their coins. - let _ = hal.securechip().u2f_counter_set(timestamp).await; + let _ = { + let crate::hal::HalSubsystems { + securechip, + random, + memory, + .. + } = hal.as_mut(); + securechip.u2f_counter_set(random, memory, timestamp).await + }; } hal.memory().set_initialized().or(Err(Error::Memory))?; diff --git a/src/rust/bitbox02-rust/src/keystore.rs b/src/rust/bitbox02-rust/src/keystore.rs index 3d63094382..9550cb2f20 100644 --- a/src/rust/bitbox02-rust/src/keystore.rs +++ b/src/rust/bitbox02-rust/src/keystore.rs @@ -36,6 +36,7 @@ pub trait KeystoreHal { fn memory(&mut self) -> &mut Self::Memory; fn random(&mut self) -> &mut Self::Random; fn securechip(&mut self) -> &mut Self::SecureChip; + fn memory_and_securechip(&mut self) -> (&mut Self::Memory, &mut Self::SecureChip); fn random_and_securechip(&mut self) -> (&mut Self::Random, &mut Self::SecureChip); } @@ -100,6 +101,10 @@ impl KeystoreHal self.securechip } + fn memory_and_securechip(&mut self) -> (&mut Self::Memory, &mut Self::SecureChip) { + (self.memory, self.securechip) + } + fn random_and_securechip(&mut self) -> (&mut Self::Random, &mut Self::SecureChip) { (self.random, self.securechip) } @@ -753,7 +758,10 @@ pub async fn stretch_retained_seed_encryption_key( let salted_in = bitbox_core_utils::salt::hash_data(hal.memory(), encryption_key, purpose_in) .map_err(|_| Error::Salt)?; - let kdf = hal.securechip().kdf(&salted_in).await?; + let kdf = { + let (memory, securechip) = hal.memory_and_securechip(); + securechip.kdf(memory, &salted_in).await? + }; let salted_out = bitbox_core_utils::salt::hash_data(hal.memory(), encryption_key, purpose_out) .map_err(|_| Error::Salt)?; diff --git a/src/rust/bitbox02-rust/src/reset.rs b/src/rust/bitbox02-rust/src/reset.rs index 0746034d1d..492e0dced5 100644 --- a/src/rust/bitbox02-rust/src/reset.rs +++ b/src/rust/bitbox02-rust/src/reset.rs @@ -43,7 +43,14 @@ pub(crate) async fn reset(hal: &mut impl crate::hal::Hal, status: bool) { { let mut u2f_ok = false; for _ in 0..5 { - if hal.securechip().u2f_counter_set(0).await.is_ok() { + let result = { + let subsystems = hal.as_mut(); + subsystems + .securechip + .u2f_counter_set(subsystems.random, subsystems.memory, 0) + .await + }; + if result.is_ok() { u2f_ok = true; break; } @@ -98,7 +105,15 @@ mod tests { hal.securechip.mock_reset_keys_fails(); // Simulate a non-zero U2F counter before reset. - hal.securechip.u2f_counter_set(42).await.unwrap(); + { + let securechip = &mut hal.securechip; + let random = &mut hal.random; + let memory = &mut hal.memory; + securechip + .u2f_counter_set(random, memory, 42) + .await + .unwrap(); + } hal.securechip.event_counter_reset(); reset(&mut hal, true).await; diff --git a/src/rust/bitbox02/src/hal.rs b/src/rust/bitbox02/src/hal.rs index abb99dd8af..3bb29509b5 100644 --- a/src/rust/bitbox02/src/hal.rs +++ b/src/rust/bitbox02/src/hal.rs @@ -15,7 +15,7 @@ pub struct BitBox02Hal { ui: ui::BitBox02Ui, sd: sd::BitBox02Sd, random: random::BitBox02Random, - securechip: securechip::BitBox02SecureChip, + securechip: securechip::BitBox02SecureChip, memory: memory::BitBox02Memory, eeprom: eeprom::BitBox02Eeprom, system: system::BitBox02System, @@ -31,7 +31,7 @@ impl BitBox02Hal { ui: ui::BitBox02Ui::new(), sd: sd::BitBox02Sd, random: random::BitBox02Random, - securechip: securechip::BitBox02SecureChip, + securechip: securechip::BitBox02SecureChip::new(), memory: memory::BitBox02Memory, eeprom: eeprom::BitBox02Eeprom, system: system::BitBox02System::new(), @@ -49,7 +49,7 @@ impl Hal for BitBox02Hal { type Ui = ui::BitBox02Ui; type Random = random::BitBox02Random; type Sd = sd::BitBox02Sd; - type SecureChip = securechip::BitBox02SecureChip; + type SecureChip = securechip::BitBox02SecureChip; type Memory = memory::BitBox02Memory; type Eeprom = eeprom::BitBox02Eeprom; type System = system::BitBox02System; diff --git a/src/rust/bitbox02/src/hal/memory.rs b/src/rust/bitbox02/src/hal/memory.rs index 145aef6b44..f75a8410b0 100644 --- a/src/rust/bitbox02/src/hal/memory.rs +++ b/src/rust/bitbox02/src/hal/memory.rs @@ -227,10 +227,18 @@ impl Memory for BitBox02Memory { crate::memory::add_noise_remote_static_pubkey(pubkey) } + fn get_auth_key(&mut self, out: &mut [u8; 32]) { + crate::memory::get_auth_key(out) + } + fn get_io_protection_key(&mut self, out: &mut [u8; 32]) { crate::memory::get_io_protection_key(out) } + fn get_encryption_key(&mut self, out: &mut [u8; 32]) { + crate::memory::get_encryption_key(out) + } + fn get_salt_root(&mut self) -> Result>, ()> { crate::memory::get_salt_root() } diff --git a/src/rust/bitbox02/src/hal/securechip.rs b/src/rust/bitbox02/src/hal/securechip.rs index 8fa5b3eb1a..b192e549c1 100644 --- a/src/rust/bitbox02/src/hal/securechip.rs +++ b/src/rust/bitbox02/src/hal/securechip.rs @@ -1,12 +1,19 @@ // SPDX-License-Identifier: Apache-2.0 use alloc::boxed::Box; +use core::marker::PhantomData; use bitbox_hal::SecureChip; use bitbox_hal::memory::PasswordStretchAlgo; use bitbox_hal::securechip::{Error, Model, SecureChipError}; -pub struct BitBox02SecureChip; +pub struct BitBox02SecureChip(PhantomData); + +impl BitBox02SecureChip { + pub const fn new() -> Self { + Self(PhantomData) + } +} fn to_hal_model(model: bitbox_securechip::Model) -> Model { match model { @@ -76,9 +83,11 @@ fn to_c_password_stretch_algo(algo: PasswordStretchAlgo) -> bitbox_securechip::P } } -impl SecureChip for BitBox02SecureChip { +impl SecureChip for BitBox02SecureChip { async fn random(&mut self) -> Result>, Error> { - crate::securechip::random().await.map_err(to_hal_error) + crate::securechip::random::() + .await + .map_err(to_hal_error) } async fn init_new_password( @@ -88,7 +97,7 @@ impl SecureChip for BitBox02SecureChip { password: &str, password_stretch_algo: PasswordStretchAlgo, ) -> Result>, Error> { - crate::securechip::init_new_password( + crate::securechip::init_new_password::( random, memory, password, @@ -104,7 +113,7 @@ impl SecureChip for BitBox02SecureChip { password: &str, password_stretch_algo: PasswordStretchAlgo, ) -> Result>, Error> { - crate::securechip::stretch_password( + crate::securechip::stretch_password::( memory, password, to_c_password_stretch_algo(password_stretch_algo), @@ -113,24 +122,31 @@ impl SecureChip for BitBox02SecureChip { .map_err(to_hal_error) } - async fn kdf(&mut self, msg: &[u8; 32]) -> Result>, Error> { - crate::securechip::kdf(msg).await.map_err(to_hal_error) + async fn kdf( + &mut self, + memory: &mut impl bitbox_hal::Memory, + msg: &[u8; 32], + ) -> Result>, Error> { + crate::securechip::kdf::(memory, msg) + .await + .map_err(to_hal_error) } async fn attestation_sign( &mut self, + memory: &mut impl bitbox_hal::Memory, challenge: &[u8; 32], signature: &mut [u8; 64], ) -> Result<(), ()> { - crate::securechip::attestation_sign(challenge, signature).await + crate::securechip::attestation_sign::(memory, challenge, signature).await } async fn monotonic_increments_remaining(&mut self) -> Result { - crate::securechip::monotonic_increments_remaining().await + crate::securechip::monotonic_increments_remaining::().await } - fn model(&mut self) -> Result { - crate::securechip::model().map(to_hal_model) + async fn model(&mut self) -> Result { + crate::securechip::model::().await.map(to_hal_model) } async fn reset_keys( @@ -138,17 +154,26 @@ impl SecureChip for BitBox02SecureChip { random: &mut impl bitbox_hal::Random, memory: &mut impl bitbox_hal::Memory, ) -> Result<(), ()> { - crate::securechip::reset_keys(random, memory).await + crate::securechip::reset_keys::(random, memory).await } #[cfg(feature = "app-u2f")] - async fn u2f_counter_set(&mut self, counter: u32) -> Result<(), ()> { - crate::securechip::u2f_counter_set(counter).await + async fn u2f_counter_set( + &mut self, + random: &mut impl bitbox_hal::Random, + memory: &mut impl bitbox_hal::Memory, + counter: u32, + ) -> Result<(), ()> { + crate::securechip::u2f_counter_set::(random, memory, counter).await } #[cfg(feature = "app-u2f")] - async fn u2f_counter_inc(&mut self) -> Result { - crate::securechip::u2f_counter_inc().await + async fn u2f_counter_inc( + &mut self, + random: &mut impl bitbox_hal::Random, + memory: &mut impl bitbox_hal::Memory, + ) -> Result { + crate::securechip::u2f_counter_inc::(random, memory).await } } @@ -288,16 +313,17 @@ mod tests { #[async_test::test] async fn test_kdf() { - let mut securechip = BitBox02SecureChip; + let mut securechip = BitBox02SecureChip::::new(); + let mut memory = crate::hal::memory::BitBox02Memory; let msg = [0u8; 32]; - let result = securechip.kdf(&msg).await.unwrap(); + let result = securechip.kdf(&mut memory, &msg).await.unwrap(); let expected = hex!("1c723ccd9597e76deb55f9fd6808014007bcb3d67fc060f1149aefb9be88f423"); assert_eq!(result.as_slice(), expected.as_slice()); } #[async_test::test] async fn test_init_new_password_invalid_password_stretch_algo() { - let mut securechip = BitBox02SecureChip; + let mut securechip = BitBox02SecureChip::::new(); let mut random = TestRandom; let mut memory = crate::hal::memory::BitBox02Memory; assert_eq!( diff --git a/src/rust/bitbox02/src/memory.rs b/src/rust/bitbox02/src/memory.rs index 415eea867d..ecfd6631f2 100644 --- a/src/rust/bitbox02/src/memory.rs +++ b/src/rust/bitbox02/src/memory.rs @@ -323,13 +323,11 @@ pub(crate) fn get_io_protection_key(out: &mut [u8; 32]) { unsafe { bitbox02_sys::memory_get_io_protection_key(out.as_mut_ptr()) } } -#[cfg(test)] -fn get_authorization_key(out: &mut [u8; 32]) { +pub(crate) fn get_auth_key(out: &mut [u8; 32]) { unsafe { bitbox02_sys::memory_get_authorization_key(out.as_mut_ptr()) } } -#[cfg(test)] -fn get_encryption_key(out: &mut [u8; 32]) { +pub(crate) fn get_encryption_key(out: &mut [u8; 32]) { unsafe { bitbox02_sys::memory_get_encryption_key(out.as_mut_ptr()) } } @@ -764,22 +762,22 @@ mod tests { assert!(memory_setup(memory_setup_rand_mock_test_functional)); let mut io_protection_key = [0u8; 32]; - let mut authorization_key = [0u8; 32]; + let mut auth_key = [0u8; 32]; let mut encryption_key = [0u8; 32]; get_io_protection_key(&mut io_protection_key); - get_authorization_key(&mut authorization_key); + get_auth_key(&mut auth_key); get_encryption_key(&mut encryption_key); let expected_io_protection_key = hex!("866b7a17a54a694eb6ea57d6754f76bc257cdd6bffc392813bbbd9fbedd5b145"); - let expected_authorization_key = + let expected_auth_key = hex!("42b36c8396572dcde005d737ddad0036deb3f02351f460c43901fa520530b7d2"); let expected_encryption_key = hex!("f4ee396cee35c5eccff2c9c7e9f0decbd1c82324f4b61c8ceeab819129abaf9c"); assert_eq!(io_protection_key, expected_io_protection_key); - assert_eq!(authorization_key, expected_authorization_key); + assert_eq!(auth_key, expected_auth_key); assert_eq!(encryption_key, expected_encryption_key); assert!(memory_setup(memory_setup_rand_mock_test_functional)); @@ -789,11 +787,11 @@ mod tests { assert!(bootloader_set_flags(bitbox02_sys::secfalse_u8 as u8, true)); get_io_protection_key(&mut io_protection_key); - get_authorization_key(&mut authorization_key); + get_auth_key(&mut auth_key); get_encryption_key(&mut encryption_key); assert_eq!(io_protection_key, expected_io_protection_key); - assert_eq!(authorization_key, expected_authorization_key); + assert_eq!(auth_key, expected_auth_key); assert_eq!(encryption_key, expected_encryption_key); } diff --git a/src/rust/bitbox02/src/securechip/imp.rs b/src/rust/bitbox02/src/securechip/imp.rs index 79bd31385f..1e1290c319 100644 --- a/src/rust/bitbox02/src/securechip/imp.rs +++ b/src/rust/bitbox02/src/securechip/imp.rs @@ -19,88 +19,111 @@ fn backend() -> Backend { BACKEND.read().unwrap() } -pub async fn attestation_sign(challenge: &[u8; 32], signature: &mut [u8; 64]) -> Result<(), ()> { +type DefaultTimer = crate::hal::timer::BitBox02Timer; + +pub async fn attestation_sign( + memory: &mut impl Memory, + challenge: &[u8; 32], + signature: &mut [u8; 64], +) -> Result<(), ()> { match backend() { - Backend::Atecc => atecc::attestation_sign(challenge, signature).await, + Backend::Atecc => atecc::attestation_sign::(memory, challenge, signature).await, Backend::Optiga => optiga::attestation_sign(challenge, signature).await, } } -pub async fn random() -> Result>, Error> { +pub async fn random() -> Result>, Error> { match backend() { - Backend::Atecc => atecc::random(), + Backend::Atecc => atecc::random::().await, Backend::Optiga => optiga::random().await, } } -pub async fn monotonic_increments_remaining() -> Result { +pub async fn monotonic_increments_remaining() -> Result { match backend() { - Backend::Atecc => atecc::monotonic_increments_remaining(), + Backend::Atecc => atecc::monotonic_increments_remaining::().await, Backend::Optiga => optiga::monotonic_increments_remaining().await, } } -pub async fn reset_keys(random: &mut impl Random, memory: &mut impl Memory) -> Result<(), ()> { +pub async fn reset_keys( + random: &mut impl Random, + memory: &mut impl Memory, +) -> Result<(), ()> { match backend() { - Backend::Atecc => atecc::reset_keys(), + Backend::Atecc => atecc::reset_keys::(random, memory).await, Backend::Optiga => optiga::reset_keys(random, memory).await, } } -pub async fn init_new_password( +pub async fn init_new_password( random: &mut impl Random, memory: &mut impl Memory, password: &str, password_stretch_algo: PasswordStretchAlgo, ) -> Result>, Error> { match backend() { - Backend::Atecc => atecc::init_new_password(memory, password, password_stretch_algo), + Backend::Atecc => { + atecc::init_new_password::(random, memory, password, password_stretch_algo).await + } Backend::Optiga => { optiga::init_new_password(random, memory, password, password_stretch_algo).await } } } -pub async fn stretch_password( +pub async fn stretch_password( memory: &mut impl Memory, password: &str, password_stretch_algo: PasswordStretchAlgo, ) -> Result>, Error> { match backend() { - Backend::Atecc => atecc::stretch_password(memory, password, password_stretch_algo), + Backend::Atecc => { + atecc::stretch_password::(memory, password, password_stretch_algo).await + } Backend::Optiga => optiga::stretch_password(memory, password, password_stretch_algo).await, } } /// Perform the secure chip KDF with the message in `msg` and return the zeroizing 32-byte /// result. -pub async fn kdf(msg: &[u8; 32]) -> Result>, Error> { +pub async fn kdf( + memory: &mut impl Memory, + msg: &[u8; 32], +) -> Result>, Error> { match backend() { - Backend::Atecc => atecc::kdf(msg), + Backend::Atecc => atecc::kdf::(memory, msg).await, Backend::Optiga => optiga::kdf(msg).await, } } #[cfg(any(feature = "app-u2f", feature = "factory-setup"))] -pub async fn u2f_counter_set(counter: u32) -> Result<(), ()> { +pub async fn u2f_counter_set( + random: &mut impl Random, + memory: &mut impl Memory, + counter: u32, +) -> Result<(), ()> { match backend() { - Backend::Atecc => atecc::u2f_counter_set(counter), + Backend::Atecc => atecc::u2f_counter_set::(random, memory, counter).await, Backend::Optiga => optiga::u2f_counter_set(counter).await, } } #[cfg(feature = "app-u2f")] -pub async fn u2f_counter_inc() -> Result { +pub async fn u2f_counter_inc( + random: &mut impl Random, + memory: &mut impl Memory, +) -> Result { match backend() { - Backend::Atecc => atecc::u2f_counter_inc(), + Backend::Atecc => atecc::u2f_counter_inc::(random, memory).await, Backend::Optiga => optiga::u2f_counter_inc().await, } } -pub fn model() -> Result { +pub async fn model() -> Result { match backend() { - Backend::Atecc => atecc::model(), - Backend::Optiga => optiga::model(), + Backend::Atecc => atecc::model::().await, + Backend::Optiga => optiga::model().await, } } @@ -125,11 +148,24 @@ pub extern "C" fn rust_securechip_init() -> bool { /// Returns `0` on success. Negative values are [`SecureChipError`] codes. Positive values are /// backend-specific status codes from CryptoAuthLib or the Optiga library. #[unsafe(no_mangle)] -pub unsafe extern "C" fn rust_securechip_setup( - ifs: *const bitbox_securechip_sys::securechip_interface_functions_t, -) -> c_int { +pub unsafe extern "C" fn rust_securechip_setup() -> c_int { match backend() { - Backend::Atecc => unsafe { bitbox_securechip_sys::atecc_setup(ifs) }, + Backend::Atecc => { + let mut memory = crate::hal::memory::BitBox02Memory; + let mut io_protection_key = Box::new(Zeroizing::new([0u8; 32])); + let mut auth_key = Box::new(Zeroizing::new([0u8; 32])); + let mut encryption_key = Box::new(Zeroizing::new([0u8; 32])); + memory.get_io_protection_key(io_protection_key.as_mut()); + memory.get_auth_key(auth_key.as_mut()); + memory.get_encryption_key(encryption_key.as_mut()); + unsafe { + bitbox_securechip_sys::atecc_setup( + io_protection_key.as_ptr(), + auth_key.as_ptr(), + encryption_key.as_ptr(), + ) + } + } Backend::Optiga => unsafe { bitbox_securechip_sys::optiga_setup() }, } } @@ -139,14 +175,21 @@ pub unsafe extern "C" fn rust_securechip_setup( pub extern "C" fn rust_securechip_reset_keys() -> bool { let mut random = crate::hal::random::BitBox02Random; let mut memory = crate::hal::memory::BitBox02Memory; - util::bb02_async::block_on(reset_keys(&mut random, &mut memory)).is_ok() + util::bb02_async::block_on(reset_keys::(&mut random, &mut memory)).is_ok() } /// Generates a new device attestation key and writes the public key to `pubkey_out`. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_securechip_gen_attestation_key(pubkey_out: *mut u8) -> bool { match backend() { - Backend::Atecc => unsafe { bitbox_securechip_sys::atecc_gen_attestation_key(pubkey_out) }, + Backend::Atecc => { + let mut memory = crate::hal::memory::BitBox02Memory; + let mut auth_key = Box::new(Zeroizing::new([0u8; 32])); + memory.get_auth_key(auth_key.as_mut()); + unsafe { + bitbox_securechip_sys::atecc_gen_attestation_key(auth_key.as_ptr(), pubkey_out) + } + } Backend::Optiga => unsafe { bitbox_securechip_sys::optiga_gen_attestation_key(pubkey_out) }, } } @@ -154,7 +197,7 @@ pub unsafe extern "C" fn rust_securechip_gen_attestation_key(pubkey_out: *mut u8 /// Fills `rand_out` with 32 bytes of randomness from the secure chip. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_securechip_random(rand_out: *mut u8) -> bool { - match util::bb02_async::block_on(random()) { + match util::bb02_async::block_on(random::()) { Ok(random) => { unsafe { core::ptr::copy_nonoverlapping(random.as_ptr(), rand_out, 32); @@ -171,7 +214,14 @@ pub unsafe extern "C" fn rust_securechip_random(rand_out: *mut u8) -> bool { #[cfg(any(feature = "app-u2f", feature = "factory-setup"))] #[unsafe(no_mangle)] pub extern "C" fn rust_securechip_u2f_counter_set(counter: u32) -> bool { - util::bb02_async::block_on(u2f_counter_set(counter)).is_ok() + let mut random = crate::hal::random::BitBox02Random; + let mut memory = crate::hal::memory::BitBox02Memory; + util::bb02_async::block_on(u2f_counter_set::( + &mut random, + &mut memory, + counter, + )) + .is_ok() } #[cfg(feature = "app-u2f")] @@ -179,7 +229,9 @@ pub extern "C" fn rust_securechip_u2f_counter_set(counter: u32) -> bool { #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_securechip_u2f_counter_inc(counter: *mut u32) -> bool { assert!(!counter.is_null()); - match util::bb02_async::block_on(u2f_counter_inc()) { + let mut random = crate::hal::random::BitBox02Random; + let mut memory = crate::hal::memory::BitBox02Memory; + match util::bb02_async::block_on(u2f_counter_inc::(&mut random, &mut memory)) { Ok(current) => { unsafe { *counter = current; diff --git a/src/rust/bitbox02/src/securechip/imp_fake.rs b/src/rust/bitbox02/src/securechip/imp_fake.rs index d921679283..7bb5cbcadc 100644 --- a/src/rust/bitbox02/src/securechip/imp_fake.rs +++ b/src/rust/bitbox02/src/securechip/imp_fake.rs @@ -1,5 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 +#![allow(clippy::extra_unused_type_parameters)] + use alloc::boxed::Box; use bitbox_securechip::{Error, Model, PasswordStretchAlgo, SecureChipError}; use hex_lit::hex; @@ -24,26 +26,30 @@ fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] { out } -pub async fn attestation_sign(_challenge: &[u8; 32], _signature: &mut [u8; 64]) -> Result<(), ()> { +pub async fn attestation_sign( + _memory: &mut impl bitbox_hal::Memory, + _challenge: &[u8; 32], + _signature: &mut [u8; 64], +) -> Result<(), ()> { Err(()) } -pub async fn random() -> Result>, Error> { +pub async fn random() -> Result>, Error> { Ok(Box::new(Zeroizing::new([0u8; 32]))) } -pub async fn monotonic_increments_remaining() -> Result { +pub async fn monotonic_increments_remaining() -> Result { Ok(1) } -pub async fn reset_keys( +pub async fn reset_keys( _random: &mut impl bitbox_hal::Random, _memory: &mut impl bitbox_hal::Memory, ) -> Result<(), ()> { Ok(()) } -pub async fn init_new_password( +pub async fn init_new_password( _random: &mut impl bitbox_hal::Random, _memory: &mut impl bitbox_hal::Memory, password: &str, @@ -60,7 +66,7 @@ pub async fn init_new_password( )))) } -pub async fn stretch_password( +pub async fn stretch_password( _memory: &mut impl bitbox_hal::Memory, password: &str, _password_stretch_algo: PasswordStretchAlgo, @@ -73,24 +79,34 @@ pub async fn stretch_password( /// Perform the secure chip KDF with the message in `msg` and return the zeroizing 32-byte /// result. -pub async fn kdf(msg: &[u8; 32]) -> Result>, Error> { +pub async fn kdf( + _memory: &mut impl bitbox_hal::Memory, + msg: &[u8; 32], +) -> Result>, Error> { Ok(Box::new(Zeroizing::new(hmac_sha256(&KDF_KEY, msg)))) } #[cfg(any(feature = "app-u2f", feature = "factory-setup"))] -pub async fn u2f_counter_set(counter: u32) -> Result<(), ()> { +pub async fn u2f_counter_set( + _random: &mut impl bitbox_hal::Random, + _memory: &mut impl bitbox_hal::Memory, + counter: u32, +) -> Result<(), ()> { U2F_COUNTER.write(counter); Ok(()) } #[cfg(feature = "app-u2f")] -pub async fn u2f_counter_inc() -> Result { +pub async fn u2f_counter_inc( + _random: &mut impl bitbox_hal::Random, + _memory: &mut impl bitbox_hal::Memory, +) -> Result { let current = U2F_COUNTER.read().wrapping_add(1); U2F_COUNTER.write(current); Ok(current) } -pub fn model() -> Result { +pub async fn model() -> Result { Ok(Model::ATECC_ATECC608B) } @@ -99,7 +115,12 @@ pub fn model() -> Result { #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_securechip_u2f_counter_inc(counter: *mut u32) -> bool { assert!(!counter.is_null()); - match util::bb02_async::block_on(u2f_counter_inc()) { + let mut random = crate::hal::random::BitBox02Random; + let mut memory = crate::hal::memory::BitBox02Memory; + match util::bb02_async::block_on(u2f_counter_inc::( + &mut random, + &mut memory, + )) { Ok(current) => { unsafe { *counter = current; diff --git a/src/rust/bitbox03/src/memory.rs b/src/rust/bitbox03/src/memory.rs index 68f9ce3f56..cde7fc1eeb 100644 --- a/src/rust/bitbox03/src/memory.rs +++ b/src/rust/bitbox03/src/memory.rs @@ -153,7 +153,15 @@ impl hal::memory::Memory for BitBox03Memory { todo!() } + fn get_auth_key(&mut self, _out: &mut [u8; 32]) { + todo!() + } + fn get_io_protection_key(&mut self, _out: &mut [u8; 32]) { todo!() } + + fn get_encryption_key(&mut self, _out: &mut [u8; 32]) { + todo!() + } } diff --git a/src/rust/bitbox03/src/securechip.rs b/src/rust/bitbox03/src/securechip.rs index 6ef88f043a..1dc76fa630 100644 --- a/src/rust/bitbox03/src/securechip.rs +++ b/src/rust/bitbox03/src/securechip.rs @@ -33,6 +33,7 @@ impl hal::securechip::SecureChip for BitBox03SecureChip { async fn kdf( &mut self, + _memory: &mut impl bitbox_hal::Memory, _msg: &[u8; 32], ) -> Result>, bitbox_hal::securechip::Error> { @@ -41,6 +42,7 @@ impl hal::securechip::SecureChip for BitBox03SecureChip { async fn attestation_sign( &mut self, + _memory: &mut impl bitbox_hal::Memory, _challenge: &[u8; 32], _signature: &mut [u8; 64], ) -> Result<(), ()> { @@ -51,7 +53,7 @@ impl hal::securechip::SecureChip for BitBox03SecureChip { todo!() } - fn model(&mut self) -> Result { + async fn model(&mut self) -> Result { todo!() } @@ -64,12 +66,21 @@ impl hal::securechip::SecureChip for BitBox03SecureChip { } #[cfg(feature = "app-u2f")] - async fn u2f_counter_set(&mut self, _counter: u32) -> Result<(), ()> { + async fn u2f_counter_set( + &mut self, + _random: &mut impl bitbox_hal::Random, + _memory: &mut impl bitbox_hal::Memory, + _counter: u32, + ) -> Result<(), ()> { todo!() } #[cfg(feature = "app-u2f")] - async fn u2f_counter_inc(&mut self) -> Result { + async fn u2f_counter_inc( + &mut self, + _random: &mut impl bitbox_hal::Random, + _memory: &mut impl bitbox_hal::Memory, + ) -> Result { todo!() } } diff --git a/src/securechip/securechip.h b/src/securechip/securechip.h index 924a155502..f847614d50 100644 --- a/src/securechip/securechip.h +++ b/src/securechip/securechip.h @@ -47,23 +47,6 @@ typedef enum { SECURECHIP_PASSWORD_STRETCH_ALGO_V1, } securechip_password_stretch_algo_t; -typedef struct { - /** - * @param[out] key_out must be of size 32 - */ - void (*const get_auth_key)(uint8_t* key_out); - /** - * @param[out] key_out must be of size 32 - */ - void (*const get_io_protection_key)(uint8_t* key_out); - /** - * @param[out] key_out must be of size 32 - */ - void (*const get_encryption_key)(uint8_t* key_out); - - void (*const random_32_bytes)(uint8_t* buf); -} securechip_interface_functions_t; - /* The common securechip ABI is implemented in Rust and declared in rust/rust.h. */ typedef enum { From 838fc9ffeea03f0607eaaf93c191eab7273f9b2a Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Thu, 23 Apr 2026 13:12:38 +0000 Subject: [PATCH 2/4] manual test --- src/rust/Cargo.lock | 1 + src/rust/bitbox-securechip/Cargo.toml | 1 + src/rust/bitbox-securechip/src/atecc.rs | 197 ++++++++++++++++++++++- src/rust/bitbox02/src/hal/system.rs | 37 +++++ test/simulator-graphical-bb03/Cargo.lock | 128 +++++++++++++++ test/simulator-graphical/Cargo.lock | 128 +++++++++++++++ 6 files changed, 491 insertions(+), 1 deletion(-) diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index 8031b049b5..2776ff49da 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -241,6 +241,7 @@ dependencies = [ "der", "grounded", "hex_lit", + "p256", "util", "zeroize", ] diff --git a/src/rust/bitbox-securechip/Cargo.toml b/src/rust/bitbox-securechip/Cargo.toml index d5589be139..0be74cbd90 100644 --- a/src/rust/bitbox-securechip/Cargo.toml +++ b/src/rust/bitbox-securechip/Cargo.toml @@ -16,6 +16,7 @@ critical-section = { workspace = true } grounded = { workspace = true } util = { path = "../util", features = ["sha2"] } der = { version = "0.7.9", default-features = false } +p256 = { version = "0.13.2", default-features = false, features = ["arithmetic", "ecdsa"] } zeroize = { workspace = true } [features] diff --git a/src/rust/bitbox-securechip/src/atecc.rs b/src/rust/bitbox-securechip/src/atecc.rs index 16909c15bb..92ddfbfdf7 100644 --- a/src/rust/bitbox-securechip/src/atecc.rs +++ b/src/rust/bitbox-securechip/src/atecc.rs @@ -1,9 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{Error, Model, PasswordStretchAlgo, SecureChipError}; -use alloc::boxed::Box; +use alloc::{boxed::Box, format, string::String}; use bitbox_hal::{Memory, Random, timer::Timer}; use bitbox_securechip_sys::atecc_slot_t as Slot; +use core::fmt::Write; use core::sync::atomic::{AtomicBool, Ordering}; use util::sha2::hmac_sha256_overwrite; use zeroize::Zeroizing; @@ -397,6 +398,200 @@ pub async fn model() -> Result { }) } +fn manual_selftest_print_hex(print: &mut impl FnMut(&str), label: &str, bytes: &[u8]) { + for (chunk_index, chunk) in bytes.chunks(32).enumerate() { + let mut msg = String::new(); + if bytes.len() <= 32 { + writeln!(&mut msg, "{label} ok").unwrap(); + } else { + writeln!(&mut msg, "{label}[{chunk_index}] ok").unwrap(); + } + for byte in chunk { + write!(&mut msg, "{byte:02x}").unwrap(); + } + print(&msg); + } +} + +fn manual_selftest_print_unit(print: &mut impl FnMut(&str), label: &str, result: Result<(), ()>) { + match result { + Ok(()) => print(&format!("{label}\nok")), + Err(()) => print(&format!("{label}\nerr")), + } +} + +fn manual_selftest_model_name(model: Model) -> &'static str { + match model { + Model::ATECC_ATECC608A => "ATECC608A", + Model::ATECC_ATECC608B => "ATECC608B", + Model::OPTIGA_TRUST_M_V3 => "optiga?", + } +} + +fn manual_selftest_generate_attestation_key( + memory: &mut impl Memory, + print: &mut impl FnMut(&str), +) -> Option<[u8; 64]> { + let auth_key = load_auth_key(memory); + let mut pubkey = [0u8; 64]; + if unsafe { + bitbox_securechip_sys::atecc_gen_attestation_key(auth_key.as_ptr(), pubkey.as_mut_ptr()) + } { + manual_selftest_print_hex(print, "attest_pubkey", &pubkey); + Some(pubkey) + } else { + print("attest_gen_key\nerr"); + None + } +} + +fn manual_selftest_verify_attestation_signature( + pubkey: &[u8; 64], + challenge: &[u8; 32], + signature: &[u8; 64], +) -> Result<(), ()> { + use p256::ecdsa::signature::hazmat::PrehashVerifier; + + let mut sec1_pubkey = [0u8; 65]; + sec1_pubkey[0] = 0x04; + sec1_pubkey[1..].copy_from_slice(pubkey); + let verifying_key = p256::ecdsa::VerifyingKey::from_sec1_bytes(&sec1_pubkey).map_err(|_| ())?; + let signature = p256::ecdsa::Signature::from_slice(signature).map_err(|_| ())?; + verifying_key + .verify_prehash(challenge, &signature) + .map_err(|_| ()) +} + +/// Runs a manual public ATECC API smoke-test and reports every result through `print`. +/// +/// This is intended for temporary on-device diagnostics. The key-reset and password-init steps +/// mutate the secure-chip password-stretching keys, and the U2F steps mutate the U2F counter. +pub async fn manual_selftest( + random: &mut impl Random, + memory: &mut impl Memory, + mut print: impl FnMut(&str), +) { + print("ATECC self-test\nstart"); + + match model::().await { + Ok(model) => print(&format!("model ok\n{}", manual_selftest_model_name(model))), + Err(()) => print("model\nerr"), + } + + match self::random::().await { + Ok(random) => manual_selftest_print_hex(&mut print, "random", random.as_slice()), + Err(err) => print(&format!("random\nerr {}", status_from_error(err))), + } + + match monotonic_increments_remaining::().await { + Ok(remaining) => print(&format!("mono_remaining ok\n{remaining}")), + Err(()) => print("mono_remaining\nerr"), + } + + let kdf_msg = [0x11; 32]; + manual_selftest_print_hex(&mut print, "kdf_msg", &kdf_msg); + match kdf::(memory, &kdf_msg).await { + Ok(kdf_out) => manual_selftest_print_hex(&mut print, "kdf", kdf_out.as_slice()), + Err(err) => print(&format!("kdf\nerr {}", status_from_error(err))), + } + + let challenge = [0x22; 32]; + let mut signature = [0u8; 64]; + if let Some(attestation_pubkey) = manual_selftest_generate_attestation_key(memory, &mut print) { + manual_selftest_print_hex(&mut print, "attest_msg", &challenge); + match attestation_sign::(memory, &challenge, &mut signature).await { + Ok(()) => { + manual_selftest_print_hex(&mut print, "attest_sig", &signature); + manual_selftest_print_unit( + &mut print, + "attest_verify", + manual_selftest_verify_attestation_signature( + &attestation_pubkey, + &challenge, + &signature, + ), + ); + } + Err(()) => print("attest_sign\nerr"), + } + } + + print("reset_keys\nmutates keys"); + manual_selftest_print_unit( + &mut print, + "reset_keys", + reset_keys::(random, memory).await, + ); + + let password = "atecc-selftest"; + print("init_new_password\nmutates keys"); + let initialized_password = match init_new_password::( + random, + memory, + password, + PasswordStretchAlgo::SECURECHIP_PASSWORD_STRETCH_ALGO_V0, + ) + .await + { + Ok(stretched) => { + manual_selftest_print_hex(&mut print, "init_new_pw", stretched.as_slice()); + Some(stretched) + } + Err(err) => { + print(&format!("init_new_pw\nerr {}", status_from_error(err))); + None + } + }; + + match stretch_password::( + memory, + password, + PasswordStretchAlgo::SECURECHIP_PASSWORD_STRETCH_ALGO_V0, + ) + .await + { + Ok(stretched) => { + manual_selftest_print_hex(&mut print, "stretch_pw", stretched.as_slice()); + if let Some(initialized_password) = initialized_password { + if initialized_password.as_slice() == stretched.as_slice() { + print("stretch_cmp\nmatch"); + } else { + print("stretch_cmp\nmismatch"); + } + } + } + Err(err) => print(&format!("stretch_pw\nerr {}", status_from_error(err))), + } + + #[cfg(any(feature = "app-u2f", feature = "factory-setup"))] + { + print("u2f_counter_set\nmutates counter"); + manual_selftest_print_unit( + &mut print, + "u2f_counter_set", + u2f_counter_set::(random, memory, 1).await, + ); + } + + #[cfg(feature = "app-u2f")] + match u2f_counter_inc::(random, memory).await { + Ok(counter) => print(&format!("u2f_counter_inc ok\n{counter}")), + Err(()) => print("u2f_counter_inc\nerr"), + } + + #[cfg(not(any(feature = "app-u2f", feature = "factory-setup")))] + { + print("u2f_counter_set\nskip: feature off"); + } + + #[cfg(not(feature = "app-u2f"))] + { + print("u2f_counter_inc\nskip: feature off"); + } + + print("ATECC self-test\ndone"); +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/rust/bitbox02/src/hal/system.rs b/src/rust/bitbox02/src/hal/system.rs index 6cf11c5b14..5d308fb561 100644 --- a/src/rust/bitbox02/src/hal/system.rs +++ b/src/rust/bitbox02/src/hal/system.rs @@ -1,6 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 use bitbox_hal::System; +#[cfg(not(any( + feature = "testing", + feature = "c-unit-testing", + feature = "simulator-graphical" +)))] +use bitbox_hal::{Memory, memory::SecurechipType}; use core::marker::PhantomData; use core::time::Duration; @@ -22,6 +28,30 @@ impl Default for BitBox02System { } } +#[cfg(not(any( + feature = "testing", + feature = "c-unit-testing", + feature = "simulator-graphical" +)))] +async fn manual_atecc_selftest() { + let mut memory = super::memory::BitBox02Memory; + match memory.get_securechip_type() { + Ok(SecurechipType::Atecc) => { + let mut random = super::random::BitBox02Random; + bitbox_securechip::atecc::manual_selftest::( + &mut random, + &mut memory, + |message| crate::screen_print_debug(message, 5000), + ) + .await; + } + Ok(SecurechipType::Optiga) => { + crate::screen_print_debug("ATECC self-test\nskip: optiga", 5000) + } + Err(()) => crate::screen_print_debug("ATECC self-test\nsecurechip type err", 5000), + } +} + impl System for BitBox02System { async fn startup() { let upside_down = crate::ui::choose_orientation().await; @@ -29,6 +59,13 @@ impl System for BitBox02System { crate::screen_rotate(); } + #[cfg(not(any( + feature = "testing", + feature = "c-unit-testing", + feature = "simulator-graphical" + )))] + manual_atecc_selftest::().await; + // During this delay the bb02 logotype is shown. Timer::delay_for(Duration::from_millis(1300)).await; diff --git a/test/simulator-graphical-bb03/Cargo.lock b/test/simulator-graphical-bb03/Cargo.lock index 0ce5fd42dd..1ff5ccbde7 100644 --- a/test/simulator-graphical-bb03/Cargo.lock +++ b/test/simulator-graphical-bb03/Cargo.lock @@ -271,6 +271,12 @@ dependencies = [ "rustc_version 0.2.3", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base58ck" version = "0.1.0" @@ -440,6 +446,7 @@ dependencies = [ "critical-section", "der", "grounded", + "p256", "util", "zeroize", ] @@ -870,6 +877,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation" version = "0.9.4" @@ -1011,6 +1024,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -1068,6 +1093,10 @@ name = "der" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] [[package]] name = "deranged" @@ -1085,6 +1114,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -1126,6 +1156,19 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -1155,6 +1198,24 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "embedded-hal" version = "0.2.7" @@ -1284,6 +1345,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1382,6 +1453,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1514,6 +1586,17 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "half" version = "2.7.1" @@ -2366,6 +2449,18 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "paste" version = "1.0.15" @@ -2495,6 +2590,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -2749,6 +2853,16 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rgb" version = "0.8.52" @@ -2854,6 +2968,19 @@ dependencies = [ "tiny-skia", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + [[package]] name = "secp256k1" version = "0.29.1" @@ -2979,6 +3106,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", + "rand_core 0.6.4", ] [[package]] diff --git a/test/simulator-graphical/Cargo.lock b/test/simulator-graphical/Cargo.lock index 36e70dc342..dd914b52ad 100644 --- a/test/simulator-graphical/Cargo.lock +++ b/test/simulator-graphical/Cargo.lock @@ -233,6 +233,12 @@ dependencies = [ "rustc_version 0.2.3", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base58ck" version = "0.1.0" @@ -385,6 +391,7 @@ dependencies = [ "critical-section", "der", "grounded", + "p256", "util", "zeroize", ] @@ -831,6 +838,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation" version = "0.9.4" @@ -963,6 +976,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1020,6 +1045,10 @@ name = "der" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] [[package]] name = "deranged" @@ -1037,6 +1066,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -1078,6 +1108,19 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -1107,6 +1150,24 @@ version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "embedded-hal" version = "0.2.7" @@ -1236,6 +1297,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.3" @@ -1309,6 +1380,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1452,6 +1524,17 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "half" version = "2.6.0" @@ -2317,6 +2400,18 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "paste" version = "1.0.15" @@ -2423,6 +2518,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -2672,6 +2776,16 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rgb" version = "0.8.52" @@ -2777,6 +2891,19 @@ dependencies = [ "tiny-skia", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + [[package]] name = "secp256k1" version = "0.29.0" @@ -2911,6 +3038,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", + "rand_core", ] [[package]] From d47627819b4c64af718aece79c9b15f8ea0543bb Mon Sep 17 00:00:00 2001 From: benma's agent Date: Wed, 15 Jul 2026 20:28:57 +0000 Subject: [PATCH 3/4] securechip: test production async engines Move ATECC and Optiga test doubles below their production async operation engines, leaving only raw device calls faked. Exercise delayed completion, cancellation recovery, callback wakeups, error cleanup, and length validation using the real state machines. --- src/rust/bitbox-securechip/Cargo.toml | 1 + src/rust/bitbox-securechip/src/atecc.rs | 106 ++- src/rust/bitbox-securechip/src/atecc/ops.rs | 169 ++--- .../bitbox-securechip/src/atecc/ops_fake.rs | 268 -------- .../bitbox-securechip/src/atecc/raw_fake.rs | 374 +++++++++++ .../bitbox-securechip/src/atecc/raw_ffi.rs | 180 ++++++ .../bitbox-securechip/src/atecc/testing.rs | 58 ++ src/rust/bitbox-securechip/src/optiga.rs | 260 ++++++-- src/rust/bitbox-securechip/src/optiga/ops.rs | 79 ++- .../bitbox-securechip/src/optiga/ops_fake.rs | 452 ------------- .../bitbox-securechip/src/optiga/raw_fake.rs | 609 ++++++++++++++++++ .../bitbox-securechip/src/optiga/raw_ffi.rs | 181 ++++++ .../bitbox-securechip/src/optiga/testing.rs | 59 ++ 13 files changed, 1817 insertions(+), 979 deletions(-) delete mode 100644 src/rust/bitbox-securechip/src/atecc/ops_fake.rs create mode 100644 src/rust/bitbox-securechip/src/atecc/raw_fake.rs create mode 100644 src/rust/bitbox-securechip/src/atecc/raw_ffi.rs create mode 100644 src/rust/bitbox-securechip/src/atecc/testing.rs delete mode 100644 src/rust/bitbox-securechip/src/optiga/ops_fake.rs create mode 100644 src/rust/bitbox-securechip/src/optiga/raw_fake.rs create mode 100644 src/rust/bitbox-securechip/src/optiga/raw_ffi.rs create mode 100644 src/rust/bitbox-securechip/src/optiga/testing.rs diff --git a/src/rust/bitbox-securechip/Cargo.toml b/src/rust/bitbox-securechip/Cargo.toml index 0be74cbd90..cf17741731 100644 --- a/src/rust/bitbox-securechip/Cargo.toml +++ b/src/rust/bitbox-securechip/Cargo.toml @@ -26,4 +26,5 @@ factory-setup = [] [dev-dependencies] async_test = { path = "../async_test" } bitbox-platform-host = { path = "../bitbox-platform-host" } +critical-section = { workspace = true, features = ["std"] } hex_lit = { workspace = true, features = ["rust_v_1_46"] } diff --git a/src/rust/bitbox-securechip/src/atecc.rs b/src/rust/bitbox-securechip/src/atecc.rs index 92ddfbfdf7..ed87bb3d32 100644 --- a/src/rust/bitbox-securechip/src/atecc.rs +++ b/src/rust/bitbox-securechip/src/atecc.rs @@ -9,12 +9,8 @@ use core::sync::atomic::{AtomicBool, Ordering}; use util::sha2::hmac_sha256_overwrite; use zeroize::Zeroizing; -#[cfg(not(test))] #[path = "atecc/ops.rs"] mod ops; -#[cfg(test)] -#[path = "atecc/ops_fake.rs"] -mod ops; const ATECC_OPS_STATUS_BUSY: i32 = bitbox_securechip_sys::ATECC_OPS_STATUS_BUSY as i32; const KDF_LEN: usize = 32; @@ -607,7 +603,21 @@ mod tests { struct TestTimer; impl Timer for TestTimer { - async fn delay_for(_duration: Duration) {} + async fn delay_for(_duration: Duration) { + // Yield once so the command remains busy for one poll, as it would while a real timer + // is running, before the fake backend is polled for completion. + let mut first_poll = true; + core::future::poll_fn(move |cx| { + if first_poll { + first_poll = false; + cx.waker().wake_by_ref(); + Poll::Pending + } else { + Poll::Ready(()) + } + }) + .await; + } } fn setup_test() -> ( @@ -615,8 +625,8 @@ mod tests { FakeMemory, TestingRandom, ) { - let guard = ops::test_lock(); - ops::test_reset(); + let guard = ops::testing::lock(); + ops::testing::reset(); let mut memory = FakeMemory::new(); memory.set_salt_root(&SALT_ROOT_FIXED); (guard, memory, TestingRandom::new()) @@ -629,9 +639,9 @@ mod tests { reset_keys::(&mut random, &mut memory) .await .unwrap(); - assert_eq!(ops::test_get_derivekey_rollkey_calls(), 1); - assert_eq!(ops::test_get_kdf_key_write_calls(), 1); - assert!(!ops::test_io_temp_key_active()); + assert_eq!(ops::testing::derivekey_rollkey_calls(), 1); + assert_eq!(ops::testing::kdf_key_write_calls(), 1); + assert!(!ops::testing::io_temp_key_active()); } #[async_test::test] @@ -646,53 +656,66 @@ mod tests { .await .unwrap(); assert_eq!(stretched.as_slice(), EXPECTED_STRETCHED_OUT_V0.as_slice(),); - assert_eq!(ops::test_get_rollkey_kdf_calls(), 1); - assert_eq!(ops::test_get_kdf_calls(), KDF_NUM_ITERATIONS); + assert_eq!(ops::testing::rollkey_kdf_calls(), 1); + assert_eq!(ops::testing::kdf_calls(), KDF_NUM_ITERATIONS); } - #[test] - fn test_random_busy() { + #[async_test::test] + #[allow(clippy::await_holding_lock)] + async fn test_random_busy() { let (_guard, _memory, _random) = setup_test(); - ops::test_block_next_chip_command(); + ops::testing::block_next_chip_command(); let mut first: util::bb02_async::Task<_> = alloc::boxed::Box::pin(random::()); assert!(matches!(util::bb02_async::spin(&mut first), Poll::Pending)); assert_eq!( - util::bb02_async::block_on(random::()), + random::().await, Err(Error::Status(ATECC_OPS_STATUS_BUSY)), ); - ops::test_unblock_chip_command(); - drop(util::bb02_async::block_on(first).unwrap()); + ops::testing::unblock_chip_command(); + drop(first.await.unwrap()); } - #[test] - fn test_model_busy() { + #[async_test::test] + #[allow(clippy::await_holding_lock)] + async fn test_model_busy() { let (_guard, _memory, _random) = setup_test(); - ops::test_block_next_chip_command(); + ops::testing::block_next_chip_command(); let mut first: util::bb02_async::Task<_> = alloc::boxed::Box::pin(random::()); assert!(matches!(util::bb02_async::spin(&mut first), Poll::Pending)); - assert_eq!(util::bb02_async::block_on(model::()), Err(())); + assert_eq!(model::().await, Err(())); - ops::test_unblock_chip_command(); - drop(util::bb02_async::block_on(first).unwrap()); + ops::testing::unblock_chip_command(); + drop(first.await.unwrap()); } - #[test] - fn test_drop_releases_high_level_guard() { + /// Dropping an in-flight operation must keep the command context reserved until the detached + /// command completes, after which the next operation may reuse it. + #[async_test::test] + #[allow(clippy::await_holding_lock)] + async fn test_drop_releases_high_level_guard() { let (_guard, _memory, _random) = setup_test(); - ops::test_block_next_chip_command(); + ops::testing::block_next_chip_command(); let mut first: util::bb02_async::Task<_> = alloc::boxed::Box::pin(random::()); assert!(matches!(util::bb02_async::spin(&mut first), Poll::Pending)); + assert_eq!(ops::testing::started_commands(), 1); drop(first); - assert!(util::bb02_async::block_on(random::()).is_ok()); + let mut second: util::bb02_async::Task<_> = alloc::boxed::Box::pin(random::()); + assert!(matches!(util::bb02_async::spin(&mut second), Poll::Pending)); + assert_eq!(ops::testing::started_commands(), 1); + + ops::testing::unblock_chip_command(); + assert!(second.await.is_ok()); + assert_eq!(ops::testing::started_commands(), 2); } - #[test] - fn test_drop_clears_io_temp_key() { + #[async_test::test] + #[allow(clippy::await_holding_lock)] + async fn test_drop_clears_io_temp_key() { let (_guard, _memory, _random) = setup_test(); let mut task: util::bb02_async::Task<_> = alloc::boxed::Box::pin(async { let _guard = begin_high_level_op().unwrap(); @@ -702,10 +725,29 @@ mod tests { core::future::pending::<()>().await; }); assert!(matches!(util::bb02_async::spin(&mut task), Poll::Pending)); - assert!(ops::test_io_temp_key_active()); + assert!(ops::testing::io_temp_key_active()); drop(task); - assert!(!ops::test_io_temp_key_active()); + assert!(!ops::testing::io_temp_key_active()); + } + + /// Start, completion, and response errors must all release the async engine so a subsequent + /// command can succeed. + #[async_test::test] + #[allow(clippy::await_holding_lock)] + async fn test_chip_random_errors_release_engine() { + let (_guard, _memory, _random) = setup_test(); + + ops::testing::fail_next_start(21); + assert_eq!(ops::chip_random::().await, Err(21)); + + ops::testing::fail_next_completion(22); + assert_eq!(ops::chip_random::().await, Err(22)); + + ops::testing::fail_next_response(23); + assert_eq!(ops::chip_random::().await, Err(23)); + + assert!(ops::chip_random::().await.is_ok()); } } diff --git a/src/rust/bitbox-securechip/src/atecc/ops.rs b/src/rust/bitbox-securechip/src/atecc/ops.rs index 5c4e363c57..794942b053 100644 --- a/src/rust/bitbox-securechip/src/atecc/ops.rs +++ b/src/rust/bitbox-securechip/src/atecc/ops.rs @@ -6,6 +6,16 @@ use core::time::Duration; use util::cell::SyncCell; use zeroize::Zeroizing; +#[cfg(not(test))] +#[path = "raw_ffi.rs"] +mod raw; +#[cfg(test)] +#[path = "raw_fake.rs"] +mod raw; +#[cfg(test)] +#[path = "testing.rs"] +pub(super) mod testing; + const ATECC_OPS_STATUS_BUSY: i32 = bitbox_securechip_sys::ATECC_OPS_STATUS_BUSY as i32; pub(super) const NONCE_NUMIN_SIZE: usize = bitbox_securechip_sys::NONCE_NUMIN_SIZE as usize; const BLOCK_SIZE: usize = 32; @@ -58,7 +68,7 @@ impl Drop for AsyncOpGuard { } fn poll_status() -> i32 { - unsafe { bitbox_securechip_sys::atecc_ops_get_status() } + raw::status() } fn ensure_success(status: i32) -> Result<(), i32> { @@ -66,19 +76,15 @@ fn ensure_success(status: i32) -> Result<(), i32> { } pub(super) fn clear_io_temp_key() { - unsafe { - bitbox_securechip_sys::atecc_io_clear_tempkey(); - } + raw::clear_io_temp_key(); } async fn poll_once() { - let delay_ms = unsafe { bitbox_securechip_sys::atecc_ops_get_poll_delay_ms() } as u64; + let delay_ms = raw::poll_delay_ms() as u64; if delay_ms > 0 { T::delay_for(Duration::from_millis(delay_ms)).await; } - unsafe { - bitbox_securechip_sys::atecc_ops_poll(); - } + raw::poll(); } async fn reclaim_detached_op() { @@ -175,60 +181,37 @@ pub(super) async fn chip_nonce_rand( ) -> Result, i32> { let mut rand_out = Zeroizing::new([0u8; 32]); let guard = begin_async_op::().await; - wait_with_cleanup::(guard, unsafe { - bitbox_securechip_sys::atecc_cmd_start_nonce_rand(num_in.as_ptr()) - }) - .await?; - ensure_success(unsafe { - bitbox_securechip_sys::atecc_cmd_read_random_response((*rand_out).as_mut_ptr()) - })?; + wait_with_cleanup::(guard, raw::start_nonce_rand(num_in)).await?; + ensure_success(raw::read_random_response(&mut rand_out))?; Ok(rand_out) } pub(super) async fn chip_checkmac(response: &Zeroizing<[u8; 32]>) -> Result<(), i32> { let guard = begin_async_op::().await; - wait_with_cleanup::(guard, unsafe { - bitbox_securechip_sys::atecc_cmd_start_checkmac(response.as_ptr()) - }) - .await + wait_with_cleanup::(guard, raw::start_checkmac(response)).await } pub(super) async fn chip_random() -> Result, i32> { let mut rand_out = Zeroizing::new([0u8; 32]); let guard = begin_async_op::().await; - wait_with_cleanup::(guard, unsafe { - bitbox_securechip_sys::atecc_cmd_start_random() - }) - .await?; - ensure_success(unsafe { - bitbox_securechip_sys::atecc_cmd_read_random_response((*rand_out).as_mut_ptr()) - })?; + wait_with_cleanup::(guard, raw::start_random()).await?; + ensure_success(raw::read_random_response(&mut rand_out))?; Ok(rand_out) } pub(super) async fn chip_counter_read() -> Result { let mut counter = 0u32; let guard = begin_async_op::().await; - wait_with_cleanup::(guard, unsafe { - bitbox_securechip_sys::atecc_cmd_start_counter_read() - }) - .await?; - ensure_success(unsafe { - bitbox_securechip_sys::atecc_cmd_read_counter_response(&mut counter) - })?; + wait_with_cleanup::(guard, raw::start_counter_read()).await?; + ensure_success(raw::read_counter_response(&mut counter))?; Ok(counter) } pub(super) async fn chip_info_revision() -> Result<[u8; 4], i32> { let mut revision = [0u8; 4]; let guard = begin_async_op::().await; - wait_with_cleanup::(guard, unsafe { - bitbox_securechip_sys::atecc_cmd_start_info_revision() - }) - .await?; - ensure_success(unsafe { - bitbox_securechip_sys::atecc_cmd_read_info_response(revision.as_mut_ptr()) - })?; + wait_with_cleanup::(guard, raw::start_info_revision()).await?; + ensure_success(raw::read_info_response(&mut revision))?; Ok(revision) } @@ -239,54 +222,32 @@ pub(super) async fn chip_kdf( let mut kdf_out = Zeroizing::new([0u8; 32]); let mut nonce_out = Zeroizing::new([0u8; 32]); let guard = begin_async_op::().await; - wait_with_cleanup::(guard, unsafe { - bitbox_securechip_sys::atecc_cmd_start_kdf(slot, msg.as_ptr(), msg.len()) - }) - .await?; - ensure_success(unsafe { - bitbox_securechip_sys::atecc_cmd_read_kdf_response( - (*kdf_out).as_mut_ptr(), - (*nonce_out).as_mut_ptr(), - ) - })?; + wait_with_cleanup::(guard, raw::start_kdf(slot, msg)).await?; + ensure_success(raw::read_kdf_response(&mut kdf_out, &mut nonce_out))?; Ok((kdf_out, nonce_out)) } pub(super) async fn chip_derivekey_rollkey() -> Result<(), i32> { let guard = begin_async_op::().await; - wait_with_cleanup::(guard, unsafe { - bitbox_securechip_sys::atecc_cmd_start_derivekey_rollkey() - }) - .await + wait_with_cleanup::(guard, raw::start_derivekey_rollkey()).await } pub(super) async fn chip_nonce_load_msgdigest(msg: &[u8; 32]) -> Result<(), i32> { let guard = begin_async_op::().await; - wait_with_cleanup::(guard, unsafe { - bitbox_securechip_sys::atecc_cmd_start_nonce_load_msgdigest(msg.as_ptr()) - }) - .await + wait_with_cleanup::(guard, raw::start_nonce_load_msgdigest(msg)).await } pub(super) async fn chip_sign_attestation() -> Result<[u8; SIGNATURE_SIZE], i32> { let mut signature = [0u8; SIGNATURE_SIZE]; let guard = begin_async_op::().await; - wait_with_cleanup::(guard, unsafe { - bitbox_securechip_sys::atecc_cmd_start_sign_attestation() - }) - .await?; - ensure_success(unsafe { - bitbox_securechip_sys::atecc_cmd_read_sign_response(signature.as_mut_ptr()) - })?; + wait_with_cleanup::(guard, raw::start_sign_attestation()).await?; + ensure_success(raw::read_sign_response(&mut signature))?; Ok(signature) } pub(super) async fn chip_gendig_encryption_key() -> Result<(), i32> { let guard = begin_async_op::().await; - wait_with_cleanup::(guard, unsafe { - bitbox_securechip_sys::atecc_cmd_start_gendig_encryption_key() - }) - .await + wait_with_cleanup::(guard, raw::start_gendig_encryption_key()).await } #[cfg(any(feature = "app-u2f", feature = "factory-setup"))] @@ -296,13 +257,8 @@ pub(super) async fn chip_read_block( ) -> Result, i32> { let mut data = Zeroizing::new([0u8; BLOCK_SIZE]); let guard = begin_async_op::().await; - wait_with_cleanup::(guard, unsafe { - bitbox_securechip_sys::atecc_cmd_start_read_block(slot as u16, block) - }) - .await?; - ensure_success(unsafe { - bitbox_securechip_sys::atecc_cmd_read_block_response((*data).as_mut_ptr()) - })?; + wait_with_cleanup::(guard, raw::start_read_block(slot, block)).await?; + ensure_success(raw::read_block_response(&mut data))?; Ok(data) } @@ -313,14 +269,10 @@ pub(super) async fn chip_write_encrypted_block( mac: &Zeroizing<[u8; BLOCK_SIZE]>, ) -> Result<(), i32> { let guard = begin_async_op::().await; - wait_with_cleanup::(guard, unsafe { - bitbox_securechip_sys::atecc_cmd_start_write_encrypted_block( - slot as u16, - block, - value.as_ptr(), - mac.as_ptr(), - ) - }) + wait_with_cleanup::( + guard, + raw::start_write_encrypted_block(slot, block, value, mac), + ) .await } @@ -333,14 +285,12 @@ pub(super) fn host_check_mac( auth_key: &[u8; 32], ) -> Result, i32> { let mut response = Zeroizing::new([0u8; 32]); - ensure_success(unsafe { - bitbox_securechip_sys::atecc_auth_compute_response( - num_in.as_ptr(), - rand_out.as_ptr(), - auth_key.as_ptr(), - (*response).as_mut_ptr(), - ) - })?; + ensure_success(raw::auth_compute_response( + num_in, + rand_out, + auth_key, + &mut response, + ))?; Ok(response) } @@ -349,27 +299,18 @@ pub(super) fn host_kdf_decrypt( nonce_out: &Zeroizing<[u8; 32]>, data: &mut Zeroizing<[u8; 32]>, ) -> Result<(), i32> { - ensure_success(unsafe { - bitbox_securechip_sys::atecc_kdf_decrypt( - io_protection_key.as_ptr(), - nonce_out.as_ptr(), - (*data).as_mut_ptr(), - BLOCK_SIZE, - ) - }) + ensure_success(raw::kdf_decrypt(io_protection_key, nonce_out, data)) } pub(super) fn host_nonce( num_in: &[u8; NONCE_NUMIN_SIZE], rand_out: &Zeroizing<[u8; 32]>, ) -> Result<(), i32> { - ensure_success(unsafe { - bitbox_securechip_sys::atecc_io_prepare_tempkey(num_in.as_ptr(), rand_out.as_ptr()) - }) + ensure_success(raw::io_prepare_tempkey(num_in, rand_out)) } pub(super) fn host_gendig(encryption_key: &[u8; 32]) -> Result<(), i32> { - ensure_success(unsafe { bitbox_securechip_sys::atecc_io_apply_gendig(encryption_key.as_ptr()) }) + ensure_success(raw::io_apply_gendig(encryption_key)) } pub(super) fn host_write_auth_mac( @@ -379,21 +320,17 @@ pub(super) fn host_write_auth_mac( ) -> Result<(Zeroizing<[u8; BLOCK_SIZE]>, Zeroizing<[u8; BLOCK_SIZE]>), i32> { let mut encrypted = Zeroizing::new([0u8; BLOCK_SIZE]); let mut mac = Zeroizing::new([0u8; BLOCK_SIZE]); - ensure_success(unsafe { - bitbox_securechip_sys::atecc_io_prepare_encrypted_write( - slot as u16, - block, - input_data.as_ptr(), - (*encrypted).as_mut_ptr(), - (*mac).as_mut_ptr(), - ) - })?; + ensure_success(raw::io_prepare_encrypted_write( + slot, + block, + input_data, + &mut encrypted, + &mut mac, + ))?; Ok((encrypted, mac)) } #[cfg(any(feature = "app-u2f", feature = "factory-setup"))] pub(super) fn host_io_decrypt(data: &mut Zeroizing<[u8; BLOCK_SIZE]>) -> Result<(), i32> { - ensure_success(unsafe { - bitbox_securechip_sys::atecc_io_decrypt_block((*data).as_mut_ptr(), BLOCK_SIZE) - }) + ensure_success(raw::io_decrypt_block(data)) } diff --git a/src/rust/bitbox-securechip/src/atecc/ops_fake.rs b/src/rust/bitbox-securechip/src/atecc/ops_fake.rs deleted file mode 100644 index bf5ca8eb6b..0000000000 --- a/src/rust/bitbox-securechip/src/atecc/ops_fake.rs +++ /dev/null @@ -1,268 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#![allow(clippy::extra_unused_type_parameters)] - -use bitbox_hal::timer::Timer; -use bitbox_securechip_sys::atecc_slot_t as Slot; -use core::future::poll_fn; -use core::task::Poll; -use std::sync::{LazyLock, Mutex, MutexGuard}; -use util::sha2::hmac_sha256 as util_hmac_sha256; -use zeroize::Zeroizing; - -pub(super) const NONCE_NUMIN_SIZE: usize = bitbox_securechip_sys::NONCE_NUMIN_SIZE as usize; -const BLOCK_SIZE: usize = 32; -const SLOT_ROLLKEY: Slot = Slot::ATECC_SLOT_ROLLKEY; -const SLOT_KDF: Slot = Slot::ATECC_SLOT_KDF; -const SLOT_DATA0: Slot = Slot::ATECC_SLOT_DATA0; - -#[derive(Clone)] -struct FakeState { - data0_block0: [u8; BLOCK_SIZE], - block_next_chip_command: bool, - blocked_chip_command_active: bool, - derivekey_rollkey_calls: usize, - rollkey_kdf_calls: usize, - kdf_calls: usize, - kdf_key_write_calls: usize, - io_temp_key_active: bool, -} - -impl Default for FakeState { - fn default() -> Self { - Self { - data0_block0: [0; BLOCK_SIZE], - block_next_chip_command: false, - blocked_chip_command_active: false, - derivekey_rollkey_calls: 0, - rollkey_kdf_calls: 0, - kdf_calls: 0, - kdf_key_write_calls: 0, - io_temp_key_active: false, - } - } -} - -static TEST_LOCK: Mutex<()> = Mutex::new(()); -static STATE: LazyLock> = LazyLock::new(|| Mutex::new(FakeState::default())); - -fn lock_state() -> MutexGuard<'static, FakeState> { - STATE.lock().unwrap() -} - -fn fake_hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] { - let mut result = [0u8; 32]; - util_hmac_sha256(key, data, &mut result); - result -} - -async fn maybe_block_next_chip_command() { - let should_block = { - let mut state = lock_state(); - if state.block_next_chip_command { - state.block_next_chip_command = false; - state.blocked_chip_command_active = true; - true - } else { - false - } - }; - - if !should_block { - return; - } - - poll_fn(|_| { - if lock_state().blocked_chip_command_active { - Poll::Pending - } else { - Poll::Ready(()) - } - }) - .await; -} - -pub(super) fn test_lock() -> MutexGuard<'static, ()> { - TEST_LOCK.lock().unwrap() -} - -pub(super) fn test_reset() { - *lock_state() = FakeState::default(); -} - -pub(super) fn test_block_next_chip_command() { - let mut state = lock_state(); - state.block_next_chip_command = true; -} - -pub(super) fn test_unblock_chip_command() { - let mut state = lock_state(); - state.blocked_chip_command_active = false; -} - -pub(super) fn test_get_derivekey_rollkey_calls() -> usize { - lock_state().derivekey_rollkey_calls -} - -pub(super) fn test_get_rollkey_kdf_calls() -> usize { - lock_state().rollkey_kdf_calls -} - -pub(super) fn test_get_kdf_calls() -> usize { - lock_state().kdf_calls -} - -pub(super) fn test_get_kdf_key_write_calls() -> usize { - lock_state().kdf_key_write_calls -} - -pub(super) fn test_io_temp_key_active() -> bool { - lock_state().io_temp_key_active -} - -pub(super) fn clear_io_temp_key() { - lock_state().io_temp_key_active = false; -} - -pub(super) async fn chip_nonce_rand( - _num_in: &[u8; NONCE_NUMIN_SIZE], -) -> Result, i32> { - maybe_block_next_chip_command().await; - Ok(Zeroizing::new([0u8; 32])) -} - -pub(super) async fn chip_checkmac(_response: &Zeroizing<[u8; 32]>) -> Result<(), i32> { - maybe_block_next_chip_command().await; - Ok(()) -} - -pub(super) async fn chip_random() -> Result, i32> { - maybe_block_next_chip_command().await; - Ok(Zeroizing::new([0u8; 32])) -} - -pub(super) async fn chip_counter_read() -> Result { - maybe_block_next_chip_command().await; - Ok(2_097_150) -} - -pub(super) async fn chip_info_revision() -> Result<[u8; 4], i32> { - maybe_block_next_chip_command().await; - Ok([0, 0, 0, 0x03]) -} - -pub(super) async fn chip_kdf( - slot: Slot, - msg: &[u8; 32], -) -> Result<(Zeroizing<[u8; 32]>, Zeroizing<[u8; 32]>), i32> { - maybe_block_next_chip_command().await; - - let key = if slot == SLOT_ROLLKEY { - let mut state = lock_state(); - state.rollkey_kdf_calls += 1; - b"atecc-rollkey".as_slice() - } else { - let mut state = lock_state(); - state.kdf_calls += 1; - b"atecc-kdf".as_slice() - }; - Ok(( - Zeroizing::new(fake_hmac_sha256(key, msg)), - Zeroizing::new([0u8; 32]), - )) -} - -pub(super) async fn chip_derivekey_rollkey() -> Result<(), i32> { - maybe_block_next_chip_command().await; - let mut state = lock_state(); - state.derivekey_rollkey_calls += 1; - Ok(()) -} - -pub(super) async fn chip_nonce_load_msgdigest(_msg: &[u8; 32]) -> Result<(), i32> { - maybe_block_next_chip_command().await; - Ok(()) -} - -pub(super) async fn chip_sign_attestation() -> Result<[u8; 64], i32> { - maybe_block_next_chip_command().await; - Err(-1) -} - -pub(super) async fn chip_gendig_encryption_key() -> Result<(), i32> { - maybe_block_next_chip_command().await; - Ok(()) -} - -pub(super) async fn chip_read_block( - slot: Slot, - block: u8, -) -> Result, i32> { - maybe_block_next_chip_command().await; - if slot == SLOT_DATA0 && block == 0 { - Ok(Zeroizing::new(lock_state().data0_block0)) - } else { - Ok(Zeroizing::new([0u8; BLOCK_SIZE])) - } -} - -pub(super) async fn chip_write_encrypted_block( - slot: Slot, - block: u8, - value: &Zeroizing<[u8; BLOCK_SIZE]>, - _mac: &Zeroizing<[u8; BLOCK_SIZE]>, -) -> Result<(), i32> { - maybe_block_next_chip_command().await; - if slot == SLOT_DATA0 && block == 0 { - let mut state = lock_state(); - state.data0_block0 = **value; - } - Ok(()) -} - -pub(super) fn host_check_mac( - _num_in: &[u8; NONCE_NUMIN_SIZE], - _rand_out: &Zeroizing<[u8; 32]>, - _auth_key: &[u8; 32], -) -> Result, i32> { - Ok(Zeroizing::new([0u8; 32])) -} - -pub(super) fn host_kdf_decrypt( - _io_protection_key: &[u8; 32], - _nonce_out: &Zeroizing<[u8; 32]>, - _data: &mut Zeroizing<[u8; 32]>, -) -> Result<(), i32> { - Ok(()) -} - -pub(super) fn host_nonce( - _num_in: &[u8; NONCE_NUMIN_SIZE], - _rand_out: &Zeroizing<[u8; 32]>, -) -> Result<(), i32> { - lock_state().io_temp_key_active = true; - Ok(()) -} - -pub(super) fn host_gendig(_encryption_key: &[u8; 32]) -> Result<(), i32> { - Ok(()) -} - -pub(super) fn host_write_auth_mac( - slot: Slot, - _block: u8, - input_data: &Zeroizing<[u8; BLOCK_SIZE]>, -) -> Result<(Zeroizing<[u8; BLOCK_SIZE]>, Zeroizing<[u8; BLOCK_SIZE]>), i32> { - if slot == SLOT_KDF { - let mut state = lock_state(); - state.kdf_key_write_calls += 1; - } - Ok(( - Zeroizing::new(**input_data), - Zeroizing::new([0u8; BLOCK_SIZE]), - )) -} - -pub(super) fn host_io_decrypt(_data: &mut Zeroizing<[u8; BLOCK_SIZE]>) -> Result<(), i32> { - Ok(()) -} diff --git a/src/rust/bitbox-securechip/src/atecc/raw_fake.rs b/src/rust/bitbox-securechip/src/atecc/raw_fake.rs new file mode 100644 index 0000000000..011acbf8a1 --- /dev/null +++ b/src/rust/bitbox-securechip/src/atecc/raw_fake.rs @@ -0,0 +1,374 @@ +// SPDX-License-Identifier: Apache-2.0 + +use super::{ATECC_OPS_STATUS_BUSY, BLOCK_SIZE, NONCE_NUMIN_SIZE, SIGNATURE_SIZE, Slot}; +use std::sync::{LazyLock, Mutex, MutexGuard}; +use util::sha2::hmac_sha256; +use zeroize::Zeroizing; + +const SLOT_ROLLKEY: Slot = Slot::ATECC_SLOT_ROLLKEY; +const SLOT_KDF: Slot = Slot::ATECC_SLOT_KDF; +const SLOT_DATA0: Slot = Slot::ATECC_SLOT_DATA0; + +enum Command { + NonceRand, + Checkmac, + Random, + CounterRead, + InfoRevision, + Kdf(Slot, [u8; 32]), + DerivekeyRollkey, + NonceLoadMsgdigest, + SignAttestation, + GendigEncryptionKey, + ReadBlock(Slot, u8), + WriteEncryptedBlock(Slot, u8, [u8; BLOCK_SIZE]), +} + +/// Models the raw ATECC command context, responses, and test-controlled completion behavior used +/// to exercise the production async engine without hardware. +struct FakeState { + status: i32, + pending: Option, + block_next_command: bool, + blocked_command_active: bool, + next_start_error: Option, + next_completion_error: Option, + next_response_error: Option, + started_commands: usize, + data0_block0: [u8; BLOCK_SIZE], + random_response: [u8; 32], + counter_response: u32, + info_response: [u8; 4], + kdf_response: [u8; 32], + kdf_nonce_response: [u8; 32], + read_block_response: [u8; BLOCK_SIZE], + derivekey_rollkey_calls: usize, + rollkey_kdf_calls: usize, + kdf_calls: usize, + kdf_key_write_calls: usize, + io_temp_key_active: bool, +} + +impl Default for FakeState { + fn default() -> Self { + Self { + status: 0, + pending: None, + block_next_command: false, + blocked_command_active: false, + next_start_error: None, + next_completion_error: None, + next_response_error: None, + started_commands: 0, + data0_block0: [0; BLOCK_SIZE], + random_response: [0; 32], + counter_response: 2_097_150, + info_response: [0, 0, 0, 0x03], + kdf_response: [0; 32], + kdf_nonce_response: [0; 32], + read_block_response: [0; BLOCK_SIZE], + derivekey_rollkey_calls: 0, + rollkey_kdf_calls: 0, + kdf_calls: 0, + kdf_key_write_calls: 0, + io_temp_key_active: false, + } + } +} + +static TEST_LOCK: Mutex<()> = Mutex::new(()); +static STATE: LazyLock> = LazyLock::new(|| Mutex::new(FakeState::default())); + +fn lock_state() -> MutexGuard<'static, FakeState> { + STATE.lock().unwrap() +} + +fn start(command: Command) -> i32 { + let mut state = lock_state(); + state.started_commands += 1; + if let Some(error) = state.next_start_error.take() { + state.status = error; + return error; + } + assert!(state.pending.is_none()); + state.status = ATECC_OPS_STATUS_BUSY; + state.blocked_command_active = state.block_next_command; + state.block_next_command = false; + state.pending = Some(command); + 0 +} + +fn complete(state: &mut FakeState, command: Command) { + match command { + Command::Kdf(slot, msg) => { + let key = if slot == SLOT_ROLLKEY { + state.rollkey_kdf_calls += 1; + b"atecc-rollkey".as_slice() + } else { + state.kdf_calls += 1; + b"atecc-kdf".as_slice() + }; + hmac_sha256(key, &msg, &mut state.kdf_response); + state.kdf_nonce_response.fill(0); + } + Command::DerivekeyRollkey => state.derivekey_rollkey_calls += 1, + Command::ReadBlock(slot, block) => { + state.read_block_response = if slot == SLOT_DATA0 && block == 0 { + state.data0_block0 + } else { + [0; BLOCK_SIZE] + }; + } + Command::WriteEncryptedBlock(slot, block, value) => { + if slot == SLOT_DATA0 && block == 0 { + state.data0_block0 = value; + } + } + Command::NonceRand + | Command::Checkmac + | Command::Random + | Command::CounterRead + | Command::InfoRevision + | Command::NonceLoadMsgdigest + | Command::SignAttestation + | Command::GendigEncryptionKey => {} + } +} + +fn response_status(state: &mut FakeState) -> i32 { + state.next_response_error.take().unwrap_or(0) +} + +pub(super) fn test_lock() -> MutexGuard<'static, ()> { + TEST_LOCK.lock().unwrap() +} + +pub(super) fn test_reset() { + *lock_state() = FakeState::default(); +} + +pub(super) fn test_block_next_chip_command() { + lock_state().block_next_command = true; +} + +pub(super) fn test_unblock_chip_command() { + lock_state().blocked_command_active = false; +} + +pub(super) fn test_fail_next_start(error: i32) { + lock_state().next_start_error = Some(error); +} + +pub(super) fn test_fail_next_completion(error: i32) { + lock_state().next_completion_error = Some(error); +} + +pub(super) fn test_fail_next_response(error: i32) { + lock_state().next_response_error = Some(error); +} + +pub(super) fn test_started_commands() -> usize { + lock_state().started_commands +} + +pub(super) fn test_get_derivekey_rollkey_calls() -> usize { + lock_state().derivekey_rollkey_calls +} + +pub(super) fn test_get_rollkey_kdf_calls() -> usize { + lock_state().rollkey_kdf_calls +} + +pub(super) fn test_get_kdf_calls() -> usize { + lock_state().kdf_calls +} + +pub(super) fn test_get_kdf_key_write_calls() -> usize { + lock_state().kdf_key_write_calls +} + +pub(super) fn test_io_temp_key_active() -> bool { + lock_state().io_temp_key_active +} + +pub(super) fn status() -> i32 { + lock_state().status +} + +pub(super) fn poll_delay_ms() -> u32 { + 1 +} + +pub(super) fn poll() { + let mut state = lock_state(); + if state.status != ATECC_OPS_STATUS_BUSY || state.blocked_command_active { + return; + } + let command = state.pending.take().unwrap(); + if let Some(error) = state.next_completion_error.take() { + state.status = error; + return; + } + complete(&mut state, command); + state.status = 0; +} + +pub(super) fn clear_io_temp_key() { + lock_state().io_temp_key_active = false; +} + +pub(super) fn start_nonce_rand(_num_in: &[u8; NONCE_NUMIN_SIZE]) -> i32 { + start(Command::NonceRand) +} + +pub(super) fn start_checkmac(_response: &Zeroizing<[u8; 32]>) -> i32 { + start(Command::Checkmac) +} + +pub(super) fn start_random() -> i32 { + start(Command::Random) +} + +pub(super) fn start_counter_read() -> i32 { + start(Command::CounterRead) +} + +pub(super) fn start_info_revision() -> i32 { + start(Command::InfoRevision) +} + +pub(super) fn start_kdf(slot: Slot, msg: &[u8; 32]) -> i32 { + start(Command::Kdf(slot, *msg)) +} + +pub(super) fn start_derivekey_rollkey() -> i32 { + start(Command::DerivekeyRollkey) +} + +pub(super) fn start_nonce_load_msgdigest(_msg: &[u8; 32]) -> i32 { + start(Command::NonceLoadMsgdigest) +} + +pub(super) fn start_sign_attestation() -> i32 { + start(Command::SignAttestation) +} + +pub(super) fn start_gendig_encryption_key() -> i32 { + start(Command::GendigEncryptionKey) +} + +pub(super) fn start_read_block(slot: Slot, block: u8) -> i32 { + start(Command::ReadBlock(slot, block)) +} + +pub(super) fn start_write_encrypted_block( + slot: Slot, + block: u8, + value: &Zeroizing<[u8; BLOCK_SIZE]>, + _mac: &Zeroizing<[u8; BLOCK_SIZE]>, +) -> i32 { + start(Command::WriteEncryptedBlock(slot, block, **value)) +} + +pub(super) fn read_random_response(out: &mut Zeroizing<[u8; 32]>) -> i32 { + let mut state = lock_state(); + let status = response_status(&mut state); + if status == 0 { + **out = state.random_response; + } + status +} + +pub(super) fn read_counter_response(out: &mut u32) -> i32 { + let mut state = lock_state(); + let status = response_status(&mut state); + if status == 0 { + *out = state.counter_response; + } + status +} + +pub(super) fn read_info_response(out: &mut [u8; 4]) -> i32 { + let mut state = lock_state(); + let status = response_status(&mut state); + if status == 0 { + *out = state.info_response; + } + status +} + +pub(super) fn read_kdf_response( + data: &mut Zeroizing<[u8; 32]>, + nonce: &mut Zeroizing<[u8; 32]>, +) -> i32 { + let mut state = lock_state(); + let status = response_status(&mut state); + if status == 0 { + **data = state.kdf_response; + **nonce = state.kdf_nonce_response; + } + status +} + +pub(super) fn read_sign_response(_out: &mut [u8; SIGNATURE_SIZE]) -> i32 { + let mut state = lock_state(); + state.next_response_error.take().unwrap_or(-1) +} + +pub(super) fn read_block_response(out: &mut Zeroizing<[u8; BLOCK_SIZE]>) -> i32 { + let mut state = lock_state(); + let status = response_status(&mut state); + if status == 0 { + **out = state.read_block_response; + } + status +} + +pub(super) fn auth_compute_response( + _num_in: &[u8; NONCE_NUMIN_SIZE], + _rand_out: &Zeroizing<[u8; 32]>, + _auth_key: &[u8; 32], + response: &mut Zeroizing<[u8; 32]>, +) -> i32 { + response.fill(0); + 0 +} + +pub(super) fn kdf_decrypt( + _io_protection_key: &[u8; 32], + _nonce_out: &Zeroizing<[u8; 32]>, + _data: &mut Zeroizing<[u8; 32]>, +) -> i32 { + 0 +} + +pub(super) fn io_prepare_tempkey( + _num_in: &[u8; NONCE_NUMIN_SIZE], + _rand_out: &Zeroizing<[u8; 32]>, +) -> i32 { + lock_state().io_temp_key_active = true; + 0 +} + +pub(super) fn io_apply_gendig(_encryption_key: &[u8; 32]) -> i32 { + 0 +} + +pub(super) fn io_prepare_encrypted_write( + slot: Slot, + _block: u8, + input: &Zeroizing<[u8; BLOCK_SIZE]>, + encrypted: &mut Zeroizing<[u8; BLOCK_SIZE]>, + mac: &mut Zeroizing<[u8; BLOCK_SIZE]>, +) -> i32 { + if slot == SLOT_KDF { + lock_state().kdf_key_write_calls += 1; + } + **encrypted = **input; + mac.fill(0); + 0 +} + +pub(super) fn io_decrypt_block(_data: &mut Zeroizing<[u8; BLOCK_SIZE]>) -> i32 { + 0 +} diff --git a/src/rust/bitbox-securechip/src/atecc/raw_ffi.rs b/src/rust/bitbox-securechip/src/atecc/raw_ffi.rs new file mode 100644 index 0000000000..8980e43e73 --- /dev/null +++ b/src/rust/bitbox-securechip/src/atecc/raw_ffi.rs @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: Apache-2.0 + +use super::{BLOCK_SIZE, NONCE_NUMIN_SIZE, SIGNATURE_SIZE, Slot}; +use zeroize::Zeroizing; + +pub(super) fn status() -> i32 { + unsafe { bitbox_securechip_sys::atecc_ops_get_status() } +} + +pub(super) fn poll_delay_ms() -> u32 { + unsafe { bitbox_securechip_sys::atecc_ops_get_poll_delay_ms() } +} + +pub(super) fn poll() { + unsafe { + bitbox_securechip_sys::atecc_ops_poll(); + } +} + +pub(super) fn clear_io_temp_key() { + unsafe { + bitbox_securechip_sys::atecc_io_clear_tempkey(); + } +} + +pub(super) fn start_nonce_rand(num_in: &[u8; NONCE_NUMIN_SIZE]) -> i32 { + unsafe { bitbox_securechip_sys::atecc_cmd_start_nonce_rand(num_in.as_ptr()) } +} + +pub(super) fn start_checkmac(response: &Zeroizing<[u8; 32]>) -> i32 { + unsafe { bitbox_securechip_sys::atecc_cmd_start_checkmac(response.as_ptr()) } +} + +pub(super) fn start_random() -> i32 { + unsafe { bitbox_securechip_sys::atecc_cmd_start_random() } +} + +pub(super) fn start_counter_read() -> i32 { + unsafe { bitbox_securechip_sys::atecc_cmd_start_counter_read() } +} + +pub(super) fn start_info_revision() -> i32 { + unsafe { bitbox_securechip_sys::atecc_cmd_start_info_revision() } +} + +pub(super) fn start_kdf(slot: Slot, msg: &[u8; 32]) -> i32 { + unsafe { bitbox_securechip_sys::atecc_cmd_start_kdf(slot, msg.as_ptr(), msg.len()) } +} + +pub(super) fn start_derivekey_rollkey() -> i32 { + unsafe { bitbox_securechip_sys::atecc_cmd_start_derivekey_rollkey() } +} + +pub(super) fn start_nonce_load_msgdigest(msg: &[u8; 32]) -> i32 { + unsafe { bitbox_securechip_sys::atecc_cmd_start_nonce_load_msgdigest(msg.as_ptr()) } +} + +pub(super) fn start_sign_attestation() -> i32 { + unsafe { bitbox_securechip_sys::atecc_cmd_start_sign_attestation() } +} + +pub(super) fn start_gendig_encryption_key() -> i32 { + unsafe { bitbox_securechip_sys::atecc_cmd_start_gendig_encryption_key() } +} + +#[cfg(any(feature = "app-u2f", feature = "factory-setup"))] +pub(super) fn start_read_block(slot: Slot, block: u8) -> i32 { + unsafe { bitbox_securechip_sys::atecc_cmd_start_read_block(slot as u16, block) } +} + +pub(super) fn start_write_encrypted_block( + slot: Slot, + block: u8, + value: &Zeroizing<[u8; BLOCK_SIZE]>, + mac: &Zeroizing<[u8; BLOCK_SIZE]>, +) -> i32 { + unsafe { + bitbox_securechip_sys::atecc_cmd_start_write_encrypted_block( + slot as u16, + block, + value.as_ptr(), + mac.as_ptr(), + ) + } +} + +pub(super) fn read_random_response(out: &mut Zeroizing<[u8; 32]>) -> i32 { + unsafe { bitbox_securechip_sys::atecc_cmd_read_random_response(out.as_mut_ptr()) } +} + +pub(super) fn read_counter_response(out: &mut u32) -> i32 { + unsafe { bitbox_securechip_sys::atecc_cmd_read_counter_response(out) } +} + +pub(super) fn read_info_response(out: &mut [u8; 4]) -> i32 { + unsafe { bitbox_securechip_sys::atecc_cmd_read_info_response(out.as_mut_ptr()) } +} + +pub(super) fn read_kdf_response( + data: &mut Zeroizing<[u8; 32]>, + nonce: &mut Zeroizing<[u8; 32]>, +) -> i32 { + unsafe { + bitbox_securechip_sys::atecc_cmd_read_kdf_response(data.as_mut_ptr(), nonce.as_mut_ptr()) + } +} + +pub(super) fn read_sign_response(out: &mut [u8; SIGNATURE_SIZE]) -> i32 { + unsafe { bitbox_securechip_sys::atecc_cmd_read_sign_response(out.as_mut_ptr()) } +} + +#[cfg(any(feature = "app-u2f", feature = "factory-setup"))] +pub(super) fn read_block_response(out: &mut Zeroizing<[u8; BLOCK_SIZE]>) -> i32 { + unsafe { bitbox_securechip_sys::atecc_cmd_read_block_response(out.as_mut_ptr()) } +} + +pub(super) fn auth_compute_response( + num_in: &[u8; NONCE_NUMIN_SIZE], + rand_out: &Zeroizing<[u8; 32]>, + auth_key: &[u8; 32], + response: &mut Zeroizing<[u8; 32]>, +) -> i32 { + unsafe { + bitbox_securechip_sys::atecc_auth_compute_response( + num_in.as_ptr(), + rand_out.as_ptr(), + auth_key.as_ptr(), + response.as_mut_ptr(), + ) + } +} + +pub(super) fn kdf_decrypt( + io_protection_key: &[u8; 32], + nonce_out: &Zeroizing<[u8; 32]>, + data: &mut Zeroizing<[u8; 32]>, +) -> i32 { + unsafe { + bitbox_securechip_sys::atecc_kdf_decrypt( + io_protection_key.as_ptr(), + nonce_out.as_ptr(), + data.as_mut_ptr(), + BLOCK_SIZE, + ) + } +} + +pub(super) fn io_prepare_tempkey( + num_in: &[u8; NONCE_NUMIN_SIZE], + rand_out: &Zeroizing<[u8; 32]>, +) -> i32 { + unsafe { bitbox_securechip_sys::atecc_io_prepare_tempkey(num_in.as_ptr(), rand_out.as_ptr()) } +} + +pub(super) fn io_apply_gendig(encryption_key: &[u8; 32]) -> i32 { + unsafe { bitbox_securechip_sys::atecc_io_apply_gendig(encryption_key.as_ptr()) } +} + +pub(super) fn io_prepare_encrypted_write( + slot: Slot, + block: u8, + input: &Zeroizing<[u8; BLOCK_SIZE]>, + encrypted: &mut Zeroizing<[u8; BLOCK_SIZE]>, + mac: &mut Zeroizing<[u8; BLOCK_SIZE]>, +) -> i32 { + unsafe { + bitbox_securechip_sys::atecc_io_prepare_encrypted_write( + slot as u16, + block, + input.as_ptr(), + encrypted.as_mut_ptr(), + mac.as_mut_ptr(), + ) + } +} + +#[cfg(any(feature = "app-u2f", feature = "factory-setup"))] +pub(super) fn io_decrypt_block(data: &mut Zeroizing<[u8; BLOCK_SIZE]>) -> i32 { + unsafe { bitbox_securechip_sys::atecc_io_decrypt_block(data.as_mut_ptr(), BLOCK_SIZE) } +} diff --git a/src/rust/bitbox-securechip/src/atecc/testing.rs b/src/rust/bitbox-securechip/src/atecc/testing.rs new file mode 100644 index 0000000000..e709a20de4 --- /dev/null +++ b/src/rust/bitbox-securechip/src/atecc/testing.rs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Test controls for the ATECC raw backend and production async engine. + +use super::{AsyncOpState, STATE, raw}; + +pub(in crate::atecc) fn lock() -> std::sync::MutexGuard<'static, ()> { + raw::test_lock() +} + +pub(in crate::atecc) fn reset() { + STATE.write(AsyncOpState::Idle); + raw::test_reset(); +} + +pub(in crate::atecc) fn block_next_chip_command() { + raw::test_block_next_chip_command(); +} + +pub(in crate::atecc) fn unblock_chip_command() { + raw::test_unblock_chip_command(); +} + +pub(in crate::atecc) fn fail_next_start(error: i32) { + raw::test_fail_next_start(error); +} + +pub(in crate::atecc) fn fail_next_completion(error: i32) { + raw::test_fail_next_completion(error); +} + +pub(in crate::atecc) fn fail_next_response(error: i32) { + raw::test_fail_next_response(error); +} + +pub(in crate::atecc) fn started_commands() -> usize { + raw::test_started_commands() +} + +pub(in crate::atecc) fn derivekey_rollkey_calls() -> usize { + raw::test_get_derivekey_rollkey_calls() +} + +pub(in crate::atecc) fn rollkey_kdf_calls() -> usize { + raw::test_get_rollkey_kdf_calls() +} + +pub(in crate::atecc) fn kdf_calls() -> usize { + raw::test_get_kdf_calls() +} + +pub(in crate::atecc) fn kdf_key_write_calls() -> usize { + raw::test_get_kdf_key_write_calls() +} + +pub(in crate::atecc) fn io_temp_key_active() -> bool { + raw::test_io_temp_key_active() +} diff --git a/src/rust/bitbox-securechip/src/optiga.rs b/src/rust/bitbox-securechip/src/optiga.rs index 0cab168a2e..dc5b78667a 100644 --- a/src/rust/bitbox-securechip/src/optiga.rs +++ b/src/rust/bitbox-securechip/src/optiga.rs @@ -8,12 +8,8 @@ use zeroize::Zeroizing; mod der; -#[cfg(not(test))] #[path = "optiga/ops.rs"] mod ops; -#[cfg(test)] -#[path = "optiga/ops_fake.rs"] -mod ops; const OID_AES_SYMKEY: u16 = bitbox_securechip_sys::OID_AES_SYMKEY as u16; #[cfg(any(feature = "app-u2f", feature = "factory-setup"))] @@ -542,31 +538,32 @@ pub async fn model() -> Result { #[cfg(test)] mod tests { use super::*; - use bitbox_platform_host::memory::FakeMemory; + use bitbox_platform_host::{memory::FakeMemory, random::TestingRandom}; + use core::task::Poll; use hex_lit::hex; //------------------------------------------------------------------------------ // Fixed test vectors / keys (deterministic fakes). const SALT_ROOT_FIXED: [u8; 32] = [0x42; 32]; - - struct TestRandom; - - // Unused in these unit tests. The fake `ops::random_32_bytes()` implementation ignores the - // HAL RNG and returns fixed test vectors instead. - impl bitbox_hal::Random for TestRandom { - fn factory_randomness(&mut self) -> &'static [u8; 32] { - unreachable!("unused in optiga unit tests") - } - - fn mcu_32_bytes(&mut self, _out: &mut [u8; 32]) { - unreachable!("unused in optiga unit tests") - } + const MCU_RANDOM_HMAC_KEY: [u8; 32] = + hex!("b40711a88c7039756fb8a73827eabe2c0fe5a0346ca7e0a104adc0fc764f528d"); + const MCU_RANDOM_PASSWORD_SECRET: [u8; 32] = + hex!("433ebf5bc03dffa38536673207a21281612cef5faa9bc7a4d5b9be2fdb12cf1a"); + const MCU_RANDOM_HMAC_WRITEPROTECTED_KEY: [u8; 32] = + hex!("88185d128d9922e0e6bcd32b07b6c7f20f27968eab447a1d8d1cdf250f79f7d3"); + + fn testing_random() -> TestingRandom { + let mut random = TestingRandom::new(); + random.mock_next(MCU_RANDOM_HMAC_KEY); + random.mock_next(MCU_RANDOM_PASSWORD_SECRET); + random.mock_next(MCU_RANDOM_HMAC_WRITEPROTECTED_KEY); + random } fn setup_test() -> (std::sync::MutexGuard<'static, ()>, FakeMemory) { - let guard = ops::test_lock(); - ops::test_reset(); + let guard = ops::testing::lock(); + ops::testing::reset(); let mut memory = FakeMemory::new(); // Provides a fixed salt root for deterministic hash_data() results. memory.set_salt_root(&SALT_ROOT_FIXED); @@ -588,6 +585,80 @@ mod tests { ); } + /// A dropped future must leave its retained buffer pointer valid until the late callback, and + /// a second operation must not start before that callback completes. + #[async_test::test] + #[allow(clippy::await_holding_lock)] + async fn test_async_op_drop_waits_for_callback() { + let (_guard, _memory) = setup_test(); + ops::testing::block_next_operation(); + + let mut first_out = [0xAAu8; 32]; + let mut first: util::bb02_async::Task<_> = + alloc::boxed::Box::pin(ops::crypt_random(OPTIGA_RNG_TYPE_TRNG, &mut first_out)); + assert!(matches!(util::bb02_async::spin(&mut first), Poll::Pending)); + assert_eq!(ops::testing::started_operations(), 1); + assert_eq!(ops::testing::completed_pointer_accesses(), 0); + drop(first); + assert_eq!(first_out, [0xAA; 32]); + + let mut second_out = [0u8; 32]; + let mut second: util::bb02_async::Task<_> = + alloc::boxed::Box::pin(ops::crypt_random(OPTIGA_RNG_TYPE_TRNG, &mut second_out)); + assert!(matches!(util::bb02_async::spin(&mut second), Poll::Pending)); + assert_eq!(ops::testing::started_operations(), 1); + + ops::testing::complete_blocked_operation(); + assert_eq!(ops::testing::completed_pointer_accesses(), 1); + assert_eq!(first_out, [0xAA; 32]); + second.await.unwrap(); + assert_eq!(ops::testing::started_operations(), 2); + } + + /// Immediate start failures and asynchronous completion failures must both release the engine + /// so it remains usable by later operations. + #[async_test::test] + #[allow(clippy::await_holding_lock)] + async fn test_async_op_errors_release_engine() { + let (_guard, _memory) = setup_test(); + let mut out = [0u8; 32]; + + ops::testing::fail_next_start(0x1234); + assert_eq!( + ops::crypt_random(OPTIGA_RNG_TYPE_TRNG, &mut out).await, + Err(Error::Status(0x1234)), + ); + + ops::testing::fail_next_completion(0x1235); + assert_eq!( + ops::crypt_random(OPTIGA_RNG_TYPE_TRNG, &mut out).await, + Err(Error::Status(0x1235)), + ); + + ops::crypt_random(OPTIGA_RNG_TYPE_TRNG, &mut out) + .await + .unwrap(); + } + + /// A successful read with a device-reported length different from the requested length must be + /// rejected without leaving the async engine stuck. + #[async_test::test] + #[allow(clippy::await_holding_lock)] + async fn test_async_read_rejects_unexpected_length() { + let (_guard, _memory) = setup_test(); + let mut out = [0u8; 4]; + + ops::testing::set_next_read_len(0); + assert_eq!( + ops::util_read_data(OID_COUNTER, 0, &mut out).await, + Err(Error::SecureChip( + SecureChipError::SC_OPTIGA_ERR_UNEXPECTED_LEN, + )), + ); + + ops::util_read_data(OID_COUNTER, 0, &mut out).await.unwrap(); + } + #[cfg(feature = "app-u2f")] #[async_test::test] #[allow(clippy::await_holding_lock)] @@ -607,8 +678,8 @@ mod tests { assert_eq!(read_arbitrary_data().await.unwrap().u2f_counter(), 43); } - // Expected stretched_out for password "pw" for the V0 algorithm given the deterministic fake - // constants in ops_fake.rs. + // Expected stretched_out for password "pw" for the V0 algorithm given TestingRandom and the + // deterministic raw fake. // // Repro script (mirrors stretch_password() with the unit test fakes): // ```python @@ -631,10 +702,33 @@ mod tests { // # crypt_hmac_sync fake: HMAC-SHA256(hmac_key, msg) // return hmac_sha256(hmac_key, msg) // + // def random_32_bytes(mcu_random: bytes) -> bytes: + // # random_32_bytes_with_mixin(), with the raw fake's all-zero chip random value. + // factory_randomness = bytes.fromhex( + // "f71df5932e61dbaab9b9eca90e59c4b45ec91fadf803db15578c260c608eb46b" + // ) + // chip_random = bytes(32) + // mixed = bytes( + // m ^ c ^ f for m, c, f in zip(mcu_random, chip_random, factory_randomness) + // ) + // return sha256(mixed) + // // salt_root = bytes([0x42]) * 32 // cmac_key = bytes([0xA0]) * 32 - // hmac_key = bytes([0xB0]) * 32 - // password_secret = bytes([0x99]) * 32 + // mcu_hmac_key = bytes.fromhex( + // "b40711a88c7039756fb8a73827eabe2c0fe5a0346ca7e0a104adc0fc764f528d" + // ) + // mcu_password_secret = bytes.fromhex( + // "433ebf5bc03dffa38536673207a21281612cef5faa9bc7a4d5b9be2fdb12cf1a" + // ) + // hmac_key = random_32_bytes(mcu_hmac_key) + // password_secret = random_32_bytes(mcu_password_secret) + // assert hmac_key.hex() == ( + // "39dfec3e1c0088b4dadc06ee8f5e0187fb2b93a957b0fc9fa7b80e303ab2f3c5" + // ) + // assert password_secret.hex() == ( + // "0abf2413d2f222b1c1b3ff60ff8392684bb5a33a1e3f7e94f45291172602b25b" + // ) // password = b"pw" // // kdf_in = salt_hash_data(password, b"optiga_password_stretch_in", salt_root) @@ -647,10 +741,10 @@ mod tests { // print(stretched.hex()) // ``` const EXPECTED_STRETCHED_OUT_V0: [u8; 32] = - hex!("c41f87b7c9f3169c14f3f26287093c311819067776f6163b8a0fdf3dfb8b8ebb"); + hex!("bcd3fdd35ca51ddc3005dc4fc662bfe143210ebd27c9acfdba289b6ccd4b0c4f"); - // Expected stretched_out for password "pw" for the V1 algorithm given the deterministic fake - // constants in ops_fake.rs. + // Expected stretched_out for password "pw" for the V1 algorithm given TestingRandom and the + // deterministic raw fake. // // Repro script (mirrors stretch_password() with the unit test fakes): // ```python @@ -673,10 +767,40 @@ mod tests { // # crypt_hmac_sync fake: HMAC-SHA256(hmac_key, msg) // return hmac_sha256(hmac_key, msg) // + // def random_32_bytes(mcu_random: bytes) -> bytes: + // # random_32_bytes_with_mixin(), with the raw fake's all-zero chip random value. + // factory_randomness = bytes.fromhex( + // "f71df5932e61dbaab9b9eca90e59c4b45ec91fadf803db15578c260c608eb46b" + // ) + // chip_random = bytes(32) + // mixed = bytes( + // m ^ c ^ f for m, c, f in zip(mcu_random, chip_random, factory_randomness) + // ) + // return sha256(mixed) + // // salt_root = bytes([0x42]) * 32 // cmac_key = bytes([0xA0]) * 32 - // hmac_writeprotected_key = bytes([0xC0]) * 32 - // password_secret = bytes([0x99]) * 32 + // mcu_hmac_key = bytes.fromhex( + // "b40711a88c7039756fb8a73827eabe2c0fe5a0346ca7e0a104adc0fc764f528d" + // ) + // mcu_password_secret = bytes.fromhex( + // "433ebf5bc03dffa38536673207a21281612cef5faa9bc7a4d5b9be2fdb12cf1a" + // ) + // mcu_hmac_writeprotected_key = bytes.fromhex( + // "88185d128d9922e0e6bcd32b07b6c7f20f27968eab447a1d8d1cdf250f79f7d3" + // ) + // hmac_key = random_32_bytes(mcu_hmac_key) + // password_secret = random_32_bytes(mcu_password_secret) + // hmac_writeprotected_key = random_32_bytes(mcu_hmac_writeprotected_key) + // assert hmac_key.hex() == ( + // "39dfec3e1c0088b4dadc06ee8f5e0187fb2b93a957b0fc9fa7b80e303ab2f3c5" + // ) + // assert password_secret.hex() == ( + // "0abf2413d2f222b1c1b3ff60ff8392684bb5a33a1e3f7e94f45291172602b25b" + // ) + // assert hmac_writeprotected_key.hex() == ( + // "cb47020ccd1aaa6d7fc64ab812f83ff1996be6987c83d39cbdb9720f3501ce99" + // ) // password = b"pw" // // kdf_in = salt_hash_data(password, b"optiga_password_stretch_in", salt_root) @@ -688,7 +812,7 @@ mod tests { // print(stretched.hex()) // ``` const EXPECTED_STRETCHED_OUT_V1: [u8; 32] = - hex!("c59ec3c3b1c45f7e7639a629f5b34d1e4dc508f3b5b9577dd9dd57eecf496751"); + hex!("c25485dda75fdd0bc0e30f4a231594b74767af40cb5042832a206bd45eb99b1e"); fn seed_v0_password(memory: &mut FakeMemory, password: &str) { // Seed the OID_PASSWORD and OID_PASSWORD_COUNTER objects as if they were @@ -696,8 +820,8 @@ mod tests { let oid_password = bitbox_core_utils::salt::hash_data(memory, password.as_bytes(), "optiga_password") .unwrap(); - ops::test_seed_oid_password(&oid_password); - ops::test_set_counter(OID_COUNTER_PASSWORD, 0, SMALL_MONOTONIC_COUNTER_MAX_USE); + ops::testing::seed_oid_password(&oid_password); + ops::testing::set_counter(OID_COUNTER_PASSWORD, 0, SMALL_MONOTONIC_COUNTER_MAX_USE); } #[async_test::test] @@ -719,9 +843,9 @@ mod tests { EXPECTED_STRETCHED_OUT_V0.as_slice() ); // Successful password verification resets the small monotonic counter/threshold. - assert_eq!(ops::test_get_counter(OID_COUNTER_PASSWORD), 0); + assert_eq!(ops::testing::counter(OID_COUNTER_PASSWORD), 0); assert_eq!( - ops::test_get_threshold(OID_COUNTER_PASSWORD), + ops::testing::threshold(OID_COUNTER_PASSWORD), SMALL_MONOTONIC_COUNTER_MAX_USE, ); } @@ -743,9 +867,9 @@ mod tests { SecureChipError::SC_ERR_INCORRECT_PASSWORD, )), ); - assert_eq!(ops::test_get_counter(OID_COUNTER_PASSWORD), 1); + assert_eq!(ops::testing::counter(OID_COUNTER_PASSWORD), 1); assert_eq!( - ops::test_get_threshold(OID_COUNTER_PASSWORD), + ops::testing::threshold(OID_COUNTER_PASSWORD), SMALL_MONOTONIC_COUNTER_MAX_USE, ); @@ -760,9 +884,9 @@ mod tests { SecureChipError::SC_ERR_INCORRECT_PASSWORD, )), ); - assert_eq!(ops::test_get_counter(OID_COUNTER_PASSWORD), 2); + assert_eq!(ops::testing::counter(OID_COUNTER_PASSWORD), 2); assert_eq!( - ops::test_get_threshold(OID_COUNTER_PASSWORD), + ops::testing::threshold(OID_COUNTER_PASSWORD), SMALL_MONOTONIC_COUNTER_MAX_USE, ); @@ -773,9 +897,9 @@ mod tests { ) .await .unwrap(); - assert_eq!(ops::test_get_counter(OID_COUNTER_PASSWORD), 0); + assert_eq!(ops::testing::counter(OID_COUNTER_PASSWORD), 0); assert_eq!( - ops::test_get_threshold(OID_COUNTER_PASSWORD), + ops::testing::threshold(OID_COUNTER_PASSWORD), SMALL_MONOTONIC_COUNTER_MAX_USE, ); @@ -793,7 +917,7 @@ mod tests { ); } assert_eq!( - ops::test_get_counter(OID_COUNTER_PASSWORD), + ops::testing::counter(OID_COUNTER_PASSWORD), SMALL_MONOTONIC_COUNTER_MAX_USE, ); @@ -810,7 +934,7 @@ mod tests { )), ); assert_eq!( - ops::test_get_counter(OID_COUNTER_PASSWORD), + ops::testing::counter(OID_COUNTER_PASSWORD), SMALL_MONOTONIC_COUNTER_MAX_USE, ); } @@ -822,7 +946,7 @@ mod tests { // Attempts after init are special because the PASSWORD_COUNTER init/threshold are offset by 1. async fn test_optiga_password_v1_stretch_exhaust_fails_after_init() { let (_guard, mut memory) = setup_test(); - let mut random = TestRandom; + let mut random = testing_random(); let stretched = init_new_password( &mut random, @@ -836,15 +960,15 @@ mod tests { // Counter & threshold of password counter. After init, it is at 1, but the threshold is // increased by 1, so the number of attempts is still 10. - assert_eq!(ops::test_get_counter(OID_COUNTER_PASSWORD), 1); + assert_eq!(ops::testing::counter(OID_COUNTER_PASSWORD), 1); assert_eq!( - ops::test_get_threshold(OID_COUNTER_PASSWORD), + ops::testing::threshold(OID_COUNTER_PASSWORD), SMALL_MONOTONIC_COUNTER_MAX_USE + 1, ); // Counter & threshold of hmac_writeprotected counter. - assert_eq!(ops::test_get_counter(OID_COUNTER_HMAC_WRITEPROTECTED), 0); + assert_eq!(ops::testing::counter(OID_COUNTER_HMAC_WRITEPROTECTED), 0); assert_eq!( - ops::test_get_threshold(OID_COUNTER_HMAC_WRITEPROTECTED), + ops::testing::threshold(OID_COUNTER_HMAC_WRITEPROTECTED), SMALL_MONOTONIC_COUNTER_MAX_USE, ); @@ -863,15 +987,15 @@ mod tests { ); // Counter & threshold of password counter. - assert_eq!(ops::test_get_counter(OID_COUNTER_PASSWORD), 1 + i); + assert_eq!(ops::testing::counter(OID_COUNTER_PASSWORD), 1 + i); assert_eq!( - ops::test_get_threshold(OID_COUNTER_PASSWORD), + ops::testing::threshold(OID_COUNTER_PASSWORD), SMALL_MONOTONIC_COUNTER_MAX_USE + 1, ); // Counter & threshold of hmac_writeprotected counter. - assert_eq!(ops::test_get_counter(OID_COUNTER_HMAC_WRITEPROTECTED), i); + assert_eq!(ops::testing::counter(OID_COUNTER_HMAC_WRITEPROTECTED), i); assert_eq!( - ops::test_get_threshold(OID_COUNTER_HMAC_WRITEPROTECTED), + ops::testing::threshold(OID_COUNTER_HMAC_WRITEPROTECTED), SMALL_MONOTONIC_COUNTER_MAX_USE, ); } @@ -898,7 +1022,7 @@ mod tests { // that doing a correct attempt resets the counters. async fn test_optiga_password_v1() { let (_guard, mut memory) = setup_test(); - let mut random = TestRandom; + let mut random = testing_random(); let stretched = init_new_password( &mut random, @@ -912,15 +1036,15 @@ mod tests { // Counter & threshold of password counter. After init, it is at 1, but the threshold is // increased by 1, so the number of attempts is still 10. - assert_eq!(ops::test_get_counter(OID_COUNTER_PASSWORD), 1); + assert_eq!(ops::testing::counter(OID_COUNTER_PASSWORD), 1); assert_eq!( - ops::test_get_threshold(OID_COUNTER_PASSWORD), + ops::testing::threshold(OID_COUNTER_PASSWORD), SMALL_MONOTONIC_COUNTER_MAX_USE + 1, ); // Counter & threshold of hmac_writeprotected counter. - assert_eq!(ops::test_get_counter(OID_COUNTER_HMAC_WRITEPROTECTED), 0); + assert_eq!(ops::testing::counter(OID_COUNTER_HMAC_WRITEPROTECTED), 0); assert_eq!( - ops::test_get_threshold(OID_COUNTER_HMAC_WRITEPROTECTED), + ops::testing::threshold(OID_COUNTER_HMAC_WRITEPROTECTED), SMALL_MONOTONIC_COUNTER_MAX_USE, ); @@ -939,15 +1063,15 @@ mod tests { ); // Counter & threshold of password counter. - assert_eq!(ops::test_get_counter(OID_COUNTER_PASSWORD), 1 + i); + assert_eq!(ops::testing::counter(OID_COUNTER_PASSWORD), 1 + i); assert_eq!( - ops::test_get_threshold(OID_COUNTER_PASSWORD), + ops::testing::threshold(OID_COUNTER_PASSWORD), SMALL_MONOTONIC_COUNTER_MAX_USE + 1, ); // Counter & threshold of hmac_writeprotected counter. - assert_eq!(ops::test_get_counter(OID_COUNTER_HMAC_WRITEPROTECTED), i); + assert_eq!(ops::testing::counter(OID_COUNTER_HMAC_WRITEPROTECTED), i); assert_eq!( - ops::test_get_threshold(OID_COUNTER_HMAC_WRITEPROTECTED), + ops::testing::threshold(OID_COUNTER_HMAC_WRITEPROTECTED), SMALL_MONOTONIC_COUNTER_MAX_USE, ); } @@ -962,15 +1086,15 @@ mod tests { .unwrap(); assert_eq!(stretched.as_slice(), EXPECTED_STRETCHED_OUT_V1.as_slice()); // Counter & threshold of password counter. - assert_eq!(ops::test_get_counter(OID_COUNTER_PASSWORD), 0); + assert_eq!(ops::testing::counter(OID_COUNTER_PASSWORD), 0); assert_eq!( - ops::test_get_threshold(OID_COUNTER_PASSWORD), + ops::testing::threshold(OID_COUNTER_PASSWORD), SMALL_MONOTONIC_COUNTER_MAX_USE, ); // Counter & threshold of hmac_writeprotected counter. - assert_eq!(ops::test_get_counter(OID_COUNTER_HMAC_WRITEPROTECTED), 0); + assert_eq!(ops::testing::counter(OID_COUNTER_HMAC_WRITEPROTECTED), 0); assert_eq!( - ops::test_get_threshold(OID_COUNTER_HMAC_WRITEPROTECTED), + ops::testing::threshold(OID_COUNTER_HMAC_WRITEPROTECTED), SMALL_MONOTONIC_COUNTER_MAX_USE, ); @@ -990,15 +1114,15 @@ mod tests { ); // Counter & threshold of password counter. - assert_eq!(ops::test_get_counter(OID_COUNTER_PASSWORD), i); + assert_eq!(ops::testing::counter(OID_COUNTER_PASSWORD), i); assert_eq!( - ops::test_get_threshold(OID_COUNTER_PASSWORD), + ops::testing::threshold(OID_COUNTER_PASSWORD), SMALL_MONOTONIC_COUNTER_MAX_USE, ); // Counter & threshold of hmac_writeprotected counter. - assert_eq!(ops::test_get_counter(OID_COUNTER_HMAC_WRITEPROTECTED), i); + assert_eq!(ops::testing::counter(OID_COUNTER_HMAC_WRITEPROTECTED), i); assert_eq!( - ops::test_get_threshold(OID_COUNTER_HMAC_WRITEPROTECTED), + ops::testing::threshold(OID_COUNTER_HMAC_WRITEPROTECTED), SMALL_MONOTONIC_COUNTER_MAX_USE, ); } diff --git a/src/rust/bitbox-securechip/src/optiga/ops.rs b/src/rust/bitbox-securechip/src/optiga/ops.rs index 23618fb222..dc6abb33a6 100644 --- a/src/rust/bitbox-securechip/src/optiga/ops.rs +++ b/src/rust/bitbox-securechip/src/optiga/ops.rs @@ -9,6 +9,16 @@ use grounded::uninit::{GroundedArrayCell, GroundedCell}; use util::cell::SyncCell; use zeroize::{Zeroize, Zeroizing}; +#[cfg(not(test))] +#[path = "raw_ffi.rs"] +mod raw; +#[cfg(test)] +#[path = "raw_fake.rs"] +mod raw; +#[cfg(test)] +#[path = "testing.rs"] +pub(super) mod testing; + const ARBITRARY_DATA_OBJECT_TYPE_3_MAX_SIZE: usize = bitbox_securechip_sys::ARBITRARY_DATA_OBJECT_TYPE_3_MAX_SIZE as usize; @@ -131,7 +141,7 @@ impl Drop for AsyncOpGuard { } WAKER.clear(); - let status = unsafe { bitbox_securechip_sys::optiga_ops_get_status() }; + let status = raw::status(); if status == bitbox_securechip_sys::OPTIGA_LIB_BUSY as _ { STATE.write(AsyncOpState::Detached); } else { @@ -175,9 +185,7 @@ async fn begin_async_op() -> AsyncOpGuard { // to hand their addresses to the C library and reuse the global waker slot. STATE.write(AsyncOpState::Running); WAKER.clear(); - unsafe { - bitbox_securechip_sys::optiga_ops_set_status_busy(); - } + raw::set_status_busy(); return AsyncOpGuard::new(); } AsyncOpState::Running => { @@ -212,12 +220,12 @@ async fn wait_with_cleanup( async fn wait_until_not_busy() -> bitbox_securechip_sys::optiga_lib_status_t { poll_fn(|cx| { - let status = unsafe { bitbox_securechip_sys::optiga_ops_get_status() }; + let status = raw::status(); if status == bitbox_securechip_sys::OPTIGA_LIB_BUSY as _ { // Register first, then re-check status to avoid missing a callback that fires between // the initial busy check and storing the waker. WAKER.register(cx.waker()); - let status = unsafe { bitbox_securechip_sys::optiga_ops_get_status() }; + let status = raw::status(); if status == bitbox_securechip_sys::OPTIGA_LIB_BUSY as _ { Poll::Pending } else { @@ -255,7 +263,7 @@ pub(super) async fn util_read_data(oid: u16, offset: u16, out: &mut [u8]) -> Res } let requested_len: u16 = out.len().try_into().unwrap(); - let util = unsafe { bitbox_securechip_sys::optiga_util_instance() }; + let util = raw::util_instance(); let guard = begin_async_op().await; BUF.clear(); @@ -263,7 +271,7 @@ pub(super) async fn util_read_data(oid: u16, offset: u16, out: &mut [u8]) -> Res LEN.get().write(requested_len); } let result = wait_with_cleanup(guard, unsafe { - bitbox_securechip_sys::optiga_util_read_data(util, oid, offset, BUF.as_mut_ptr(), LEN.get()) + raw::util_read_data(util, oid, offset, BUF.as_mut_ptr(), LEN.get()) }) .await .map_err(|status| Error::from_status(status as i32)); @@ -295,7 +303,7 @@ pub(super) async fn crypt_hmac( static MAC: StaticBytes<{ super::KDF_LEN }> = StaticBytes::const_init(); static MAC_LEN: GroundedCell = GroundedCell::const_init(); - let crypt = unsafe { bitbox_securechip_sys::optiga_crypt_instance() }; + let crypt = raw::crypt_instance(); let guard = begin_async_op().await; INPUT.copy_from_slice(msg); @@ -304,7 +312,7 @@ pub(super) async fn crypt_hmac( MAC_LEN.get().write(super::KDF_LEN as u32); } let result = wait_with_cleanup(guard, unsafe { - bitbox_securechip_sys::optiga_crypt_hmac( + raw::crypt_hmac( crypt, hmac_type, secret, @@ -349,19 +357,12 @@ pub(super) async fn util_write_data( panic!("optiga async write larger than max supported size"); } let input_len: u16 = buffer.len().try_into().unwrap(); - let util = unsafe { bitbox_securechip_sys::optiga_util_instance() }; + let util = raw::util_instance(); let guard = begin_async_op().await; INPUT.copy_from_slice(buffer); let result = wait_with_cleanup(guard, unsafe { - bitbox_securechip_sys::optiga_util_write_data( - util, - oid, - write_type, - offset, - INPUT.as_mut_ptr(), - input_len, - ) + raw::util_write_data(util, oid, write_type, offset, INPUT.as_mut_ptr(), input_len) }) .await .map_err(|status| Error::from_status(status as i32)); @@ -381,7 +382,7 @@ pub(super) async fn crypt_symmetric_encrypt( static OUTPUT: StaticBytes<16> = StaticBytes::const_init(); static OUTPUT_LEN: GroundedCell = GroundedCell::const_init(); - let crypt = unsafe { bitbox_securechip_sys::optiga_crypt_instance() }; + let crypt = raw::crypt_instance(); let input_len = super::KDF_LEN as u32; let guard = begin_async_op().await; @@ -391,7 +392,7 @@ pub(super) async fn crypt_symmetric_encrypt( OUTPUT_LEN.get().write(16); } let result = wait_with_cleanup(guard, unsafe { - bitbox_securechip_sys::optiga_crypt_symmetric_encrypt( + raw::crypt_symmetric_encrypt( crypt, encryption_mode, symmetric_key_oid, @@ -434,12 +435,12 @@ pub(super) async fn crypt_generate_auth_code( // callback completes, and the Rust future may be dropped before that happens. static RANDOM: StaticBytes<32> = StaticBytes::const_init(); - let crypt = unsafe { bitbox_securechip_sys::optiga_crypt_instance() }; + let crypt = raw::crypt_instance(); let guard = begin_async_op().await; RANDOM.clear(); let result = wait_with_cleanup(guard, unsafe { - bitbox_securechip_sys::optiga_crypt_generate_auth_code( + raw::crypt_generate_auth_code( crypt, rng_type, core::ptr::null(), @@ -472,7 +473,7 @@ pub(super) async fn crypt_ecdsa_sign( static SIGNATURE: StaticBytes = StaticBytes::const_init(); static SIGNATURE_LEN: GroundedCell = GroundedCell::const_init(); - let crypt = unsafe { bitbox_securechip_sys::optiga_crypt_instance() }; + let crypt = raw::crypt_instance(); let guard = begin_async_op().await; DIGEST.copy_from_slice(digest); @@ -481,7 +482,7 @@ pub(super) async fn crypt_ecdsa_sign( SIGNATURE_LEN.get().write(ECDSA_SIGNATURE_MAX_LEN as u16); } let result = wait_with_cleanup(guard, unsafe { - bitbox_securechip_sys::optiga_crypt_ecdsa_sign( + raw::crypt_ecdsa_sign( crypt, DIGEST.as_mut_ptr(), super::KDF_LEN as u8, @@ -525,13 +526,13 @@ pub(super) async fn crypt_hmac_verify( static INPUT: StaticBytes<{ super::KDF_LEN }> = StaticBytes::const_init(); static HMAC: StaticBytes<{ super::KDF_LEN }> = StaticBytes::const_init(); - let crypt = unsafe { bitbox_securechip_sys::optiga_crypt_instance() }; + let crypt = raw::crypt_instance(); let guard = begin_async_op().await; INPUT.copy_from_slice(input_data); HMAC.copy_from_slice(hmac); let result = wait_with_cleanup(guard, unsafe { - bitbox_securechip_sys::optiga_crypt_hmac_verify( + raw::crypt_hmac_verify( crypt, hmac_type, secret, @@ -556,7 +557,7 @@ pub(super) async fn crypt_symmetric_generate_key( // callback completes, and the Rust future may be dropped before that happens. static KEYID: GroundedCell = GroundedCell::uninit(); - let crypt = unsafe { bitbox_securechip_sys::optiga_crypt_instance() }; + let crypt = raw::crypt_instance(); let guard = begin_async_op().await; unsafe { @@ -565,26 +566,18 @@ pub(super) async fn crypt_symmetric_generate_key( .write(super::key_id_from_oid(super::OID_AES_SYMKEY)); } wait_with_cleanup(guard, unsafe { - bitbox_securechip_sys::optiga_crypt_symmetric_generate_key( - crypt, - key_type, - key_usage as u8, - 0, - KEYID.get().cast(), - ) + raw::crypt_symmetric_generate_key(crypt, key_type, key_usage as u8, 0, KEYID.get().cast()) }) .await .map_err(|status| Error::from_status(status as i32)) } pub(super) async fn crypt_clear_auto_state(secret: u16) -> Result<(), Error> { - let crypt = unsafe { bitbox_securechip_sys::optiga_crypt_instance() }; + let crypt = raw::crypt_instance(); let guard = begin_async_op().await; - wait_with_cleanup(guard, unsafe { - bitbox_securechip_sys::optiga_crypt_clear_auto_state(crypt, secret) - }) - .await - .map_err(|status| Error::from_status(status as i32)) + wait_with_cleanup(guard, unsafe { raw::crypt_clear_auto_state(crypt, secret) }) + .await + .map_err(|status| Error::from_status(status as i32)) } pub(super) async fn crypt_random( @@ -595,12 +588,12 @@ pub(super) async fn crypt_random( // callback completes, and the Rust future may be dropped before that happens. static BUF: StaticBytes<32> = StaticBytes::const_init(); - let crypt = unsafe { bitbox_securechip_sys::optiga_crypt_instance() }; + let crypt = raw::crypt_instance(); let guard = begin_async_op().await; BUF.clear(); let result = wait_with_cleanup(guard, unsafe { - bitbox_securechip_sys::optiga_crypt_random(crypt, rng_type, BUF.as_mut_ptr(), 32) + raw::crypt_random(crypt, rng_type, BUF.as_mut_ptr(), 32) }) .await .map_err(|status| Error::from_status(status as i32)); diff --git a/src/rust/bitbox-securechip/src/optiga/ops_fake.rs b/src/rust/bitbox-securechip/src/optiga/ops_fake.rs deleted file mode 100644 index 7371e5b04b..0000000000 --- a/src/rust/bitbox-securechip/src/optiga/ops_fake.rs +++ /dev/null @@ -1,452 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -use crate::Error; -use alloc::{boxed::Box, vec::Vec}; -use hex_lit::hex; -use std::sync::{LazyLock, Mutex, MutexGuard}; -use zeroize::Zeroizing; - -//------------------------------------------------------------------------------ -// Fixed test vectors / keys (deterministic fakes). - -const KDF_CMAC_KEY_FIXED: [u8; super::KDF_LEN] = [0xA0; super::KDF_LEN]; -const KDF_HMAC_KEY_FIXED: [u8; super::KDF_LEN] = [0xB0; super::KDF_LEN]; -const KDF_HMAC_WRITEPROTECTED_KEY_FIXED: [u8; super::KDF_LEN] = [0xC0; super::KDF_LEN]; -const PASSWORD_SECRET_FIXED: [u8; super::KDF_LEN] = [0x99; super::KDF_LEN]; -const AUTH_CODE_RANDOM_FIXED: [u8; super::KDF_LEN] = [0x77; super::KDF_LEN]; -const OPTIGA_CRYPT_ERROR: i32 = bitbox_securechip_sys::OPTIGA_CRYPT_ERROR as i32; -const OPTIGA_CRYPT_ERROR_INVALID_INPUT: i32 = - bitbox_securechip_sys::OPTIGA_CRYPT_ERROR_INVALID_INPUT as i32; -const OPTIGA_UTIL_ERROR: i32 = bitbox_securechip_sys::OPTIGA_UTIL_ERROR as i32; -const OPTIGA_UTIL_ERROR_INVALID_INPUT: i32 = - bitbox_securechip_sys::OPTIGA_UTIL_ERROR_INVALID_INPUT as i32; -const OPTIGA_UTIL_ERROR_MEMORY_INSUFFICIENT: i32 = - bitbox_securechip_sys::OPTIGA_UTIL_ERROR_MEMORY_INSUFFICIENT as i32; - -#[derive(Clone)] -struct FakeState { - oid_password: [u8; super::KDF_LEN], - oid_password_set: bool, - oid_arbitrary_data: [u8; super::ARBITRARY_DATA_LEN], - oid_counter_password_buf: [u8; 8], - oid_counter_hmac_writeprotected_buf: [u8; 8], - authorized_password: bool, - authorized_password_secret: bool, - random_ctr: usize, -} - -impl Default for FakeState { - fn default() -> Self { - Self { - oid_password: [0; super::KDF_LEN], - oid_password_set: false, - oid_arbitrary_data: [0; super::ARBITRARY_DATA_LEN], - oid_counter_password_buf: [0; 8], - oid_counter_hmac_writeprotected_buf: [0; 8], - authorized_password: false, - authorized_password_secret: false, - random_ctr: 0, - } - } -} - -static TEST_LOCK: Mutex<()> = Mutex::new(()); -static STATE: LazyLock> = LazyLock::new(|| Mutex::new(FakeState::default())); - -fn lock_state() -> MutexGuard<'static, FakeState> { - STATE.lock().unwrap() -} - -fn counter_buf(state: &FakeState, oid: u16) -> &[u8; 8] { - match oid { - super::OID_COUNTER_PASSWORD => &state.oid_counter_password_buf, - super::OID_COUNTER_HMAC_WRITEPROTECTED => &state.oid_counter_hmac_writeprotected_buf, - _ => panic!("unexpected counter oid"), - } -} - -fn counter_buf_mut(state: &mut FakeState, oid: u16) -> &mut [u8; 8] { - match oid { - super::OID_COUNTER_PASSWORD => &mut state.oid_counter_password_buf, - super::OID_COUNTER_HMAC_WRITEPROTECTED => &mut state.oid_counter_hmac_writeprotected_buf, - _ => panic!("unexpected counter oid"), - } -} - -fn get_counter_from_buf(buf: &[u8; 8]) -> u32 { - u32::from_be_bytes(buf[..4].try_into().unwrap()) -} - -fn get_threshold_from_buf(buf: &[u8; 8]) -> u32 { - u32::from_be_bytes(buf[4..].try_into().unwrap()) -} - -fn set_counter_in_buf(buf: &mut [u8; 8], counter: u32) { - buf[..4].copy_from_slice(&counter.to_be_bytes()); -} - -fn set_threshold_in_buf(buf: &mut [u8; 8], threshold: u32) { - buf[4..].copy_from_slice(&threshold.to_be_bytes()); -} - -fn compute_hmac(key: &[u8], data: &[u8]) -> [u8; super::KDF_LEN] { - let mut out = [0u8; super::KDF_LEN]; - super::hmac_sha256(key, data, &mut out); - out -} - -pub(super) fn test_lock() -> MutexGuard<'static, ()> { - TEST_LOCK.lock().unwrap() -} - -pub(super) fn test_reset() { - *lock_state() = FakeState::default(); -} - -pub(super) fn test_seed_oid_password(password_hash: &[u8; super::KDF_LEN]) { - let mut state = lock_state(); - state.oid_password = *password_hash; - state.oid_password_set = true; -} - -pub(super) fn test_set_counter(oid: u16, counter: u32, threshold: u32) { - let mut state = lock_state(); - let buf = counter_buf_mut(&mut state, oid); - set_counter_in_buf(buf, counter); - set_threshold_in_buf(buf, threshold); -} - -pub(super) fn test_get_counter(oid: u16) -> u32 { - let state = lock_state(); - get_counter_from_buf(counter_buf(&state, oid)) -} - -pub(super) fn test_get_threshold(oid: u16) -> u32 { - let state = lock_state(); - get_threshold_from_buf(counter_buf(&state, oid)) -} - -//------------------------------------------------------------------------------ -// Fake optiga_ops API surface (unit-test seam). - -pub(super) async fn util_read_data(oid: u16, offset: u16, out: &mut [u8]) -> Result<(), Error> { - if offset != 0 { - return Err(Error::from_status(OPTIGA_UTIL_ERROR_INVALID_INPUT)); - } - - let state = lock_state(); - match oid { - super::OID_PASSWORD_SECRET => { - if !state.authorized_password { - return Err(Error::from_status(OPTIGA_UTIL_ERROR)); - } - if out.len() != super::KDF_LEN { - return Err(Error::from_status(OPTIGA_UTIL_ERROR_MEMORY_INSUFFICIENT)); - } - out.copy_from_slice(&PASSWORD_SECRET_FIXED); - Ok(()) - } - super::OID_COUNTER => { - if out.len() != 4 { - return Err(Error::from_status(OPTIGA_UTIL_ERROR_MEMORY_INSUFFICIENT)); - } - out.fill(0); - Ok(()) - } - super::OID_ARBITRARY_DATA => { - if out.len() != super::ARBITRARY_DATA_LEN { - return Err(Error::from_status(OPTIGA_UTIL_ERROR_MEMORY_INSUFFICIENT)); - } - out.copy_from_slice(&state.oid_arbitrary_data); - Ok(()) - } - _ => Err(Error::from_status(OPTIGA_UTIL_ERROR_INVALID_INPUT)), - } -} - -pub(super) async fn crypt_hmac( - hmac_type: bitbox_securechip_sys::optiga_hmac_type_t, - secret: u16, - msg: &[u8; super::KDF_LEN], - mac_out: &mut [u8; super::KDF_LEN], -) -> Result<(), Error> { - crypt_hmac_sync(hmac_type, secret, msg, mac_out) -} - -pub(super) async fn util_write_data( - oid: u16, - write_type: u8, - offset: u16, - buffer: &[u8], -) -> Result<(), Error> { - util_write_data_sync(oid, write_type, offset, buffer) -} - -pub(super) async fn crypt_symmetric_encrypt( - encryption_mode: bitbox_securechip_sys::optiga_symmetric_encryption_mode_t, - symmetric_key_oid: bitbox_securechip_sys::optiga_key_id_t, - plain_data: &[u8; super::KDF_LEN], - encrypted_data: &mut [u8; 16], -) -> Result<(), Error> { - crypt_symmetric_encrypt_sync( - encryption_mode, - symmetric_key_oid, - plain_data, - encrypted_data, - ) -} - -pub(super) async fn crypt_generate_auth_code( - rng_type: bitbox_securechip_sys::optiga_rng_type_t, - random_data: &mut [u8; 32], -) -> Result<(), Error> { - crypt_generate_auth_code_sync(rng_type, random_data) -} - -pub(super) async fn crypt_ecdsa_sign( - _digest: &[u8; super::KDF_LEN], - _private_key: bitbox_securechip_sys::optiga_key_id_t, -) -> Result, Error> { - const SIG_DER: [u8; 9] = hex!("02021234020300abcd"); - Ok(SIG_DER.to_vec()) -} - -pub(super) async fn crypt_hmac_verify( - hmac_type: bitbox_securechip_sys::optiga_hmac_type_t, - secret: u16, - input_data: &[u8; super::KDF_LEN], - hmac: &[u8; super::KDF_LEN], -) -> Result<(), Error> { - crypt_hmac_verify_sync(hmac_type, secret, input_data, hmac) -} - -pub(super) async fn crypt_symmetric_generate_key( - key_type: bitbox_securechip_sys::optiga_symmetric_key_type_t, - key_usage: bitbox_securechip_sys::optiga_key_usage_t, -) -> Result<(), Error> { - crypt_symmetric_generate_key_sync(key_type, key_usage) -} - -pub(super) async fn crypt_clear_auto_state(secret: u16) -> Result<(), Error> { - crypt_clear_auto_state_sync(secret) -} - -pub(super) async fn crypt_random( - _rng_type: bitbox_securechip_sys::optiga_rng_type_t, - out: &mut [u8; 32], -) -> Result<(), Error> { - *out = [0u8; 32]; - Ok(()) -} - -pub(super) fn random_32_bytes( - _random: &mut impl bitbox_hal::Random, - _mixin: &[u8; super::KDF_LEN], -) -> Result>, Error> { - let mut state = lock_state(); - let src = match state.random_ctr { - 0 => KDF_HMAC_KEY_FIXED, - 1 => PASSWORD_SECRET_FIXED, - 2 => KDF_HMAC_WRITEPROTECTED_KEY_FIXED, - _ => unreachable!(), - }; - state.random_ctr = (state.random_ctr + 1) % 3; - Ok(Box::new(Zeroizing::new(src))) -} - -pub(super) fn util_write_data_sync( - oid: u16, - _write_type: u8, - offset: u16, - buffer: &[u8], -) -> Result<(), Error> { - if offset != 0 { - return Err(Error::from_status(OPTIGA_UTIL_ERROR_INVALID_INPUT)); - } - - let mut state = lock_state(); - match oid { - super::OID_PASSWORD => { - if !state.authorized_password_secret || buffer.len() != super::KDF_LEN { - return Err(Error::from_status(if !state.authorized_password_secret { - OPTIGA_UTIL_ERROR - } else { - OPTIGA_UTIL_ERROR_INVALID_INPUT - })); - } - state.oid_password.copy_from_slice(buffer); - state.oid_password_set = true; - Ok(()) - } - super::OID_COUNTER_PASSWORD | super::OID_COUNTER_HMAC_WRITEPROTECTED => { - if oid == super::OID_COUNTER_HMAC_WRITEPROTECTED && !state.authorized_password { - return Err(Error::from_status(OPTIGA_UTIL_ERROR)); - } - if buffer.len() != 8 { - return Err(Error::from_status(OPTIGA_UTIL_ERROR_INVALID_INPUT)); - } - counter_buf_mut(&mut state, oid).copy_from_slice(buffer); - Ok(()) - } - super::OID_HMAC => { - if buffer.len() != super::KDF_LEN { - return Err(Error::from_status(OPTIGA_UTIL_ERROR_INVALID_INPUT)); - } - assert_eq!(buffer, KDF_HMAC_KEY_FIXED.as_slice()); - Ok(()) - } - super::OID_HMAC_WRITEPROTECTED => { - if !state.authorized_password || buffer.len() != super::KDF_LEN { - return Err(Error::from_status(if !state.authorized_password { - OPTIGA_UTIL_ERROR - } else { - OPTIGA_UTIL_ERROR_INVALID_INPUT - })); - } - assert_eq!(buffer, KDF_HMAC_WRITEPROTECTED_KEY_FIXED.as_slice()); - Ok(()) - } - super::OID_PASSWORD_SECRET => { - assert_eq!(buffer, PASSWORD_SECRET_FIXED.as_slice()); - Ok(()) - } - super::OID_ARBITRARY_DATA => { - if buffer.len() != super::ARBITRARY_DATA_LEN { - return Err(Error::from_status(OPTIGA_UTIL_ERROR_INVALID_INPUT)); - } - state.oid_arbitrary_data.copy_from_slice(buffer); - Ok(()) - } - // Accept other writes without emulating full semantics (counter reset, hmac key, etc.). - _ => Ok(()), - } -} - -pub(super) fn crypt_hmac_sync( - hmac_type: bitbox_securechip_sys::optiga_hmac_type_t, - secret: u16, - input_data: &[u8; super::KDF_LEN], - mac_out: &mut [u8; super::KDF_LEN], -) -> Result<(), Error> { - // Use hmac_sha256 with a different fixed key and msg as the value. - if hmac_type != super::OPTIGA_HMAC_SHA_256 { - return Err(Error::from_status(OPTIGA_CRYPT_ERROR_INVALID_INPUT)); - } - - let mut state = lock_state(); - let key = match secret { - super::OID_HMAC => &KDF_HMAC_KEY_FIXED, - super::OID_HMAC_WRITEPROTECTED => { - // Emulate the small monotonic counter that is attached to using the - // hmac_writeprotected slot. Stored as {counter_be_u32, threshold_be_u32}. - let buf = counter_buf_mut(&mut state, super::OID_COUNTER_HMAC_WRITEPROTECTED); - let counter = get_counter_from_buf(buf); - let threshold = get_threshold_from_buf(buf); - if counter >= threshold { - return Err(Error::from_status(super::OPTIGA_HMAC_VERIFY_FAIL)); - } - set_counter_in_buf(buf, counter + 1); - &KDF_HMAC_WRITEPROTECTED_KEY_FIXED - } - _ => return Err(Error::from_status(OPTIGA_CRYPT_ERROR)), - }; - - mac_out.copy_from_slice(&compute_hmac(key, input_data)); - Ok(()) -} - -pub(super) fn crypt_symmetric_encrypt_sync( - encryption_mode: bitbox_securechip_sys::optiga_symmetric_encryption_mode_t, - symmetric_key_oid: bitbox_securechip_sys::optiga_key_id_t, - plain_data: &[u8; super::KDF_LEN], - encrypted_data: &mut [u8; 16], -) -> Result<(), Error> { - // Use hmac_sha256 with a fixed key and msg as the value, truncated to 16 bytes. - if encryption_mode != super::OPTIGA_SYMMETRIC_CMAC - || symmetric_key_oid != super::key_id_from_oid(super::OID_AES_SYMKEY) - { - return Err(Error::from_status(OPTIGA_CRYPT_ERROR_INVALID_INPUT)); - } - let out = compute_hmac(&KDF_CMAC_KEY_FIXED, plain_data); - encrypted_data.copy_from_slice(&out[..16]); - Ok(()) -} - -pub(super) fn crypt_symmetric_generate_key_sync( - key_type: bitbox_securechip_sys::optiga_symmetric_key_type_t, - key_usage: bitbox_securechip_sys::optiga_key_usage_t, -) -> Result<(), Error> { - if key_type != super::OPTIGA_SYMMETRIC_AES_256 - || key_usage != super::OPTIGA_KEY_USAGE_ENCRYPTION - { - return Err(Error::from_status(OPTIGA_CRYPT_ERROR_INVALID_INPUT)); - } - // We keep using the fixed cmac key in the tests. - Ok(()) -} - -pub(super) fn crypt_generate_auth_code_sync( - rng_type: bitbox_securechip_sys::optiga_rng_type_t, - random_data: &mut [u8; 32], -) -> Result<(), Error> { - if rng_type != super::OPTIGA_RNG_TYPE_TRNG { - return Err(Error::from_status(OPTIGA_CRYPT_ERROR_INVALID_INPUT)); - } - random_data.copy_from_slice(&AUTH_CODE_RANDOM_FIXED); - Ok(()) -} - -pub(super) fn crypt_hmac_verify_sync( - hmac_type: bitbox_securechip_sys::optiga_hmac_type_t, - secret: u16, - input_data: &[u8; super::KDF_LEN], - hmac: &[u8; super::KDF_LEN], -) -> Result<(), Error> { - if hmac_type != super::OPTIGA_HMAC_SHA_256 { - return Err(Error::from_status(OPTIGA_CRYPT_ERROR_INVALID_INPUT)); - } - - let mut state = lock_state(); - let key = match secret { - super::OID_PASSWORD_SECRET => &PASSWORD_SECRET_FIXED, - super::OID_PASSWORD => { - // Emulate the small monotonic counter that is attached to password authorization. - // Stored as {counter_be_u32, threshold_be_u32}. - let buf = counter_buf_mut(&mut state, super::OID_COUNTER_PASSWORD); - let counter = get_counter_from_buf(buf); - let threshold = get_threshold_from_buf(buf); - if counter >= threshold { - return Err(Error::from_status(super::OPTIGA_HMAC_VERIFY_FAIL)); - } - set_counter_in_buf(buf, counter + 1); - - if !state.oid_password_set { - return Err(Error::from_status(OPTIGA_CRYPT_ERROR)); - } - &state.oid_password - } - _ => return Err(Error::from_status(OPTIGA_CRYPT_ERROR_INVALID_INPUT)), - }; - - let computed = compute_hmac(key, input_data); - if computed.as_slice() != hmac.as_slice() { - return Err(Error::from_status(super::OPTIGA_HMAC_VERIFY_FAIL)); - } - - match secret { - super::OID_PASSWORD => state.authorized_password = true, - super::OID_PASSWORD_SECRET => state.authorized_password_secret = true, - _ => {} - } - Ok(()) -} - -pub(super) fn crypt_clear_auto_state_sync(secret: u16) -> Result<(), Error> { - let mut state = lock_state(); - match secret { - super::OID_PASSWORD => state.authorized_password = false, - super::OID_PASSWORD_SECRET => state.authorized_password_secret = false, - _ => {} - } - Ok(()) -} diff --git a/src/rust/bitbox-securechip/src/optiga/raw_fake.rs b/src/rust/bitbox-securechip/src/optiga/raw_fake.rs new file mode 100644 index 0000000000..41e7de65f3 --- /dev/null +++ b/src/rust/bitbox-securechip/src/optiga/raw_fake.rs @@ -0,0 +1,609 @@ +// SPDX-License-Identifier: Apache-2.0 + +use hex_lit::hex; +use std::sync::{LazyLock, Mutex, MutexGuard}; + +const KDF_LEN: usize = super::super::KDF_LEN; +const KDF_CMAC_KEY_FIXED: [u8; KDF_LEN] = [0xA0; KDF_LEN]; +// Outputs of the production entropy mixer for TestingRandom's first three values and the fake +// chip's all-zero random output. +const KDF_HMAC_KEY_FIXED: [u8; KDF_LEN] = + hex!("39dfec3e1c0088b4dadc06ee8f5e0187fb2b93a957b0fc9fa7b80e303ab2f3c5"); +const KDF_HMAC_WRITEPROTECTED_KEY_FIXED: [u8; KDF_LEN] = + hex!("cb47020ccd1aaa6d7fc64ab812f83ff1996be6987c83d39cbdb9720f3501ce99"); +const PASSWORD_SECRET_FIXED: [u8; KDF_LEN] = + hex!("0abf2413d2f222b1c1b3ff60ff8392684bb5a33a1e3f7e94f45291172602b25b"); +const AUTH_CODE_RANDOM_FIXED: [u8; KDF_LEN] = [0x77; KDF_LEN]; +const OPTIGA_CRYPT_ERROR: u16 = bitbox_securechip_sys::OPTIGA_CRYPT_ERROR as u16; +const OPTIGA_CRYPT_ERROR_INVALID_INPUT: u16 = + bitbox_securechip_sys::OPTIGA_CRYPT_ERROR_INVALID_INPUT as u16; +const OPTIGA_UTIL_ERROR: u16 = bitbox_securechip_sys::OPTIGA_UTIL_ERROR as u16; +const OPTIGA_UTIL_ERROR_INVALID_INPUT: u16 = + bitbox_securechip_sys::OPTIGA_UTIL_ERROR_INVALID_INPUT as u16; +const OPTIGA_UTIL_ERROR_MEMORY_INSUFFICIENT: u16 = + bitbox_securechip_sys::OPTIGA_UTIL_ERROR_MEMORY_INSUFFICIENT as u16; +const OPTIGA_LIB_SUCCESS: u16 = bitbox_securechip_sys::OPTIGA_LIB_SUCCESS as u16; +const OPTIGA_LIB_BUSY: u16 = bitbox_securechip_sys::OPTIGA_LIB_BUSY as u16; + +struct RetainedMutPtr(*mut u8); + +// The production wrapper gives the raw API pointers to static buffers and serializes access to +// them. This test-only wrapper retains such a pointer behind a mutex until fake completion. +unsafe impl Send for RetainedMutPtr {} + +enum BlockedOperation { + Completed(u16), + CryptRandom { + out: RetainedMutPtr, + out_len: u16, + completion_status: u16, + }, +} + +/// Models Optiga object state and the raw asynchronous status/callback behavior needed to exercise +/// the production engine without a device. +struct FakeState { + status: u16, + block_next_operation: bool, + blocked_operation: Option, + next_start_error: Option, + next_completion_error: Option, + next_read_len: Option, + started_operations: usize, + completed_pointer_accesses: usize, + oid_password: [u8; KDF_LEN], + oid_password_set: bool, + #[cfg(any(feature = "app-u2f", feature = "factory-setup"))] + oid_arbitrary_data: [u8; super::super::ARBITRARY_DATA_LEN], + oid_counter_password_buf: [u8; 8], + oid_counter_hmac_writeprotected_buf: [u8; 8], + authorized_password: bool, + authorized_password_secret: bool, +} + +impl Default for FakeState { + fn default() -> Self { + Self { + status: OPTIGA_LIB_SUCCESS, + block_next_operation: false, + blocked_operation: None, + next_start_error: None, + next_completion_error: None, + next_read_len: None, + started_operations: 0, + completed_pointer_accesses: 0, + oid_password: [0; KDF_LEN], + oid_password_set: false, + #[cfg(any(feature = "app-u2f", feature = "factory-setup"))] + oid_arbitrary_data: [0; super::super::ARBITRARY_DATA_LEN], + oid_counter_password_buf: [0; 8], + oid_counter_hmac_writeprotected_buf: [0; 8], + authorized_password: false, + authorized_password_secret: false, + } + } +} + +static TEST_LOCK: Mutex<()> = Mutex::new(()); +static STATE: LazyLock> = LazyLock::new(|| Mutex::new(FakeState::default())); + +fn lock_state() -> MutexGuard<'static, FakeState> { + STATE.lock().unwrap() +} + +fn start(operation: impl FnOnce(&mut FakeState) -> u16) -> u16 { + let mut state = lock_state(); + state.started_operations += 1; + if let Some(error) = state.next_start_error.take() { + return error; + } + + let operation_status = operation(&mut state); + let completion_status = state + .next_completion_error + .take() + .unwrap_or(operation_status); + if state.block_next_operation { + state.block_next_operation = false; + state.blocked_operation = Some(BlockedOperation::Completed(completion_status)); + state.status = OPTIGA_LIB_BUSY; + } else { + state.status = completion_status; + } + OPTIGA_LIB_SUCCESS +} + +fn counter_buf(state: &FakeState, oid: u16) -> &[u8; 8] { + match oid { + super::super::OID_COUNTER_PASSWORD => &state.oid_counter_password_buf, + super::super::OID_COUNTER_HMAC_WRITEPROTECTED => &state.oid_counter_hmac_writeprotected_buf, + _ => panic!("unexpected counter oid"), + } +} + +fn counter_buf_mut(state: &mut FakeState, oid: u16) -> &mut [u8; 8] { + match oid { + super::super::OID_COUNTER_PASSWORD => &mut state.oid_counter_password_buf, + super::super::OID_COUNTER_HMAC_WRITEPROTECTED => { + &mut state.oid_counter_hmac_writeprotected_buf + } + _ => panic!("unexpected counter oid"), + } +} + +fn get_counter_from_buf(buf: &[u8; 8]) -> u32 { + u32::from_be_bytes(buf[..4].try_into().unwrap()) +} + +fn get_threshold_from_buf(buf: &[u8; 8]) -> u32 { + u32::from_be_bytes(buf[4..].try_into().unwrap()) +} + +fn set_counter_in_buf(buf: &mut [u8; 8], counter: u32) { + buf[..4].copy_from_slice(&counter.to_be_bytes()); +} + +fn set_threshold_in_buf(buf: &mut [u8; 8], threshold: u32) { + buf[4..].copy_from_slice(&threshold.to_be_bytes()); +} + +fn compute_hmac(key: &[u8], data: &[u8]) -> [u8; KDF_LEN] { + let mut out = [0u8; KDF_LEN]; + super::super::hmac_sha256(key, data, &mut out); + out +} + +pub(super) fn test_lock() -> MutexGuard<'static, ()> { + TEST_LOCK.lock().unwrap() +} + +pub(super) fn test_reset() { + *lock_state() = FakeState::default(); +} + +pub(super) fn test_block_next_operation() { + lock_state().block_next_operation = true; +} + +pub(super) fn test_complete_blocked_operation() { + let mut state = lock_state(); + let blocked_operation = state.blocked_operation.take().unwrap(); + state.status = match blocked_operation { + BlockedOperation::Completed(status) => status, + BlockedOperation::CryptRandom { + out, + out_len, + completion_status, + } => { + if completion_status == OPTIGA_LIB_SUCCESS { + unsafe { + core::slice::from_raw_parts_mut(out.0, out_len as usize).fill(0); + } + state.completed_pointer_accesses += 1; + } + completion_status + } + }; + drop(state); + super::rust_optiga_callback_wake(); +} + +pub(super) fn test_fail_next_start(error: u16) { + lock_state().next_start_error = Some(error); +} + +pub(super) fn test_fail_next_completion(error: u16) { + lock_state().next_completion_error = Some(error); +} + +pub(super) fn test_set_next_read_len(len: u16) { + lock_state().next_read_len = Some(len); +} + +pub(super) fn test_started_operations() -> usize { + lock_state().started_operations +} + +pub(super) fn test_completed_pointer_accesses() -> usize { + lock_state().completed_pointer_accesses +} + +pub(super) fn test_seed_oid_password(password_hash: &[u8; KDF_LEN]) { + let mut state = lock_state(); + state.oid_password = *password_hash; + state.oid_password_set = true; +} + +pub(super) fn test_set_counter(oid: u16, counter: u32, threshold: u32) { + let mut state = lock_state(); + let buf = counter_buf_mut(&mut state, oid); + set_counter_in_buf(buf, counter); + set_threshold_in_buf(buf, threshold); +} + +pub(super) fn test_get_counter(oid: u16) -> u32 { + let state = lock_state(); + get_counter_from_buf(counter_buf(&state, oid)) +} + +pub(super) fn test_get_threshold(oid: u16) -> u32 { + let state = lock_state(); + get_threshold_from_buf(counter_buf(&state, oid)) +} + +pub(super) fn status() -> bitbox_securechip_sys::optiga_lib_status_t { + lock_state().status +} + +pub(super) fn set_status_busy() { + lock_state().status = OPTIGA_LIB_BUSY; +} + +pub(super) fn util_instance() -> *mut bitbox_securechip_sys::optiga_util_t { + core::ptr::null_mut() +} + +pub(super) fn crypt_instance() -> *mut bitbox_securechip_sys::optiga_crypt_t { + core::ptr::null_mut() +} + +pub(super) unsafe fn util_read_data( + _util: *mut bitbox_securechip_sys::optiga_util_t, + oid: u16, + offset: u16, + out: *mut u8, + out_len: *mut u16, +) -> bitbox_securechip_sys::optiga_lib_status_t { + start(|state| { + if offset != 0 { + return OPTIGA_UTIL_ERROR_INVALID_INPUT; + } + let requested_len = unsafe { out_len.read() } as usize; + let out = unsafe { core::slice::from_raw_parts_mut(out, requested_len) }; + let result = match oid { + super::super::OID_PASSWORD_SECRET => { + if !state.authorized_password { + OPTIGA_UTIL_ERROR + } else if out.len() != KDF_LEN { + OPTIGA_UTIL_ERROR_MEMORY_INSUFFICIENT + } else { + out.copy_from_slice(&PASSWORD_SECRET_FIXED); + OPTIGA_LIB_SUCCESS + } + } + super::super::OID_COUNTER => { + if out.len() != 4 { + OPTIGA_UTIL_ERROR_MEMORY_INSUFFICIENT + } else { + out.fill(0); + OPTIGA_LIB_SUCCESS + } + } + #[cfg(any(feature = "app-u2f", feature = "factory-setup"))] + super::super::OID_ARBITRARY_DATA => { + if out.len() != super::super::ARBITRARY_DATA_LEN { + OPTIGA_UTIL_ERROR_MEMORY_INSUFFICIENT + } else { + out.copy_from_slice(&state.oid_arbitrary_data); + OPTIGA_LIB_SUCCESS + } + } + _ => OPTIGA_UTIL_ERROR_INVALID_INPUT, + }; + if let Some(len) = state.next_read_len.take() { + unsafe { + out_len.write(len); + } + } + result + }) +} + +pub(super) unsafe fn crypt_hmac( + _crypt: *mut bitbox_securechip_sys::optiga_crypt_t, + hmac_type: bitbox_securechip_sys::optiga_hmac_type_t, + secret: u16, + input: *const u8, + input_len: u32, + mac: *mut u8, + mac_len: *mut u32, +) -> bitbox_securechip_sys::optiga_lib_status_t { + start(|state| { + if hmac_type != super::super::OPTIGA_HMAC_SHA_256 + || input_len as usize != KDF_LEN + || unsafe { mac_len.read() } as usize != KDF_LEN + { + return OPTIGA_CRYPT_ERROR_INVALID_INPUT; + } + let input = unsafe { core::slice::from_raw_parts(input, KDF_LEN) }; + let key = match secret { + super::super::OID_HMAC => &KDF_HMAC_KEY_FIXED, + super::super::OID_HMAC_WRITEPROTECTED => { + // Emulate the small monotonic counter that is attached to using the + // hmac_writeprotected slot. Stored as {counter_be_u32, threshold_be_u32}. + let buf = counter_buf_mut(state, super::super::OID_COUNTER_HMAC_WRITEPROTECTED); + let counter = get_counter_from_buf(buf); + let threshold = get_threshold_from_buf(buf); + if counter >= threshold { + return super::super::OPTIGA_HMAC_VERIFY_FAIL as u16; + } + set_counter_in_buf(buf, counter + 1); + &KDF_HMAC_WRITEPROTECTED_KEY_FIXED + } + _ => return OPTIGA_CRYPT_ERROR, + }; + unsafe { + core::ptr::copy_nonoverlapping(compute_hmac(key, input).as_ptr(), mac, KDF_LEN); + } + OPTIGA_LIB_SUCCESS + }) +} + +pub(super) unsafe fn util_write_data( + _util: *mut bitbox_securechip_sys::optiga_util_t, + oid: u16, + _write_type: u8, + offset: u16, + input: *const u8, + input_len: u16, +) -> bitbox_securechip_sys::optiga_lib_status_t { + start(|state| { + if offset != 0 { + return OPTIGA_UTIL_ERROR_INVALID_INPUT; + } + let input = unsafe { core::slice::from_raw_parts(input, input_len as usize) }; + match oid { + super::super::OID_PASSWORD => { + if !state.authorized_password_secret || input.len() != KDF_LEN { + return if !state.authorized_password_secret { + OPTIGA_UTIL_ERROR + } else { + OPTIGA_UTIL_ERROR_INVALID_INPUT + }; + } + state.oid_password.copy_from_slice(input); + state.oid_password_set = true; + OPTIGA_LIB_SUCCESS + } + super::super::OID_COUNTER_PASSWORD | super::super::OID_COUNTER_HMAC_WRITEPROTECTED => { + if oid == super::super::OID_COUNTER_HMAC_WRITEPROTECTED + && !state.authorized_password + { + return OPTIGA_UTIL_ERROR; + } + if input.len() != 8 { + return OPTIGA_UTIL_ERROR_INVALID_INPUT; + } + counter_buf_mut(state, oid).copy_from_slice(input); + OPTIGA_LIB_SUCCESS + } + super::super::OID_HMAC => { + if input.len() != KDF_LEN { + return OPTIGA_UTIL_ERROR_INVALID_INPUT; + } + assert_eq!(input, KDF_HMAC_KEY_FIXED.as_slice()); + OPTIGA_LIB_SUCCESS + } + super::super::OID_HMAC_WRITEPROTECTED => { + if !state.authorized_password || input.len() != KDF_LEN { + return if !state.authorized_password { + OPTIGA_UTIL_ERROR + } else { + OPTIGA_UTIL_ERROR_INVALID_INPUT + }; + } + assert_eq!(input, KDF_HMAC_WRITEPROTECTED_KEY_FIXED.as_slice()); + OPTIGA_LIB_SUCCESS + } + super::super::OID_PASSWORD_SECRET => { + assert_eq!(input, PASSWORD_SECRET_FIXED.as_slice()); + OPTIGA_LIB_SUCCESS + } + #[cfg(any(feature = "app-u2f", feature = "factory-setup"))] + super::super::OID_ARBITRARY_DATA => { + if input.len() != super::super::ARBITRARY_DATA_LEN { + return OPTIGA_UTIL_ERROR_INVALID_INPUT; + } + state.oid_arbitrary_data.copy_from_slice(input); + OPTIGA_LIB_SUCCESS + } + // Accept other writes without emulating full semantics (counter reset, hmac key, etc.). + _ => OPTIGA_LIB_SUCCESS, + } + }) +} + +#[allow(clippy::too_many_arguments)] +pub(super) unsafe fn crypt_symmetric_encrypt( + _crypt: *mut bitbox_securechip_sys::optiga_crypt_t, + encryption_mode: bitbox_securechip_sys::optiga_symmetric_encryption_mode_t, + symmetric_key_oid: bitbox_securechip_sys::optiga_key_id_t, + plain_data: *const u8, + plain_data_len: u32, + _iv: *const u8, + _iv_len: u16, + _associated_data: *const u8, + _associated_data_len: u16, + encrypted_data: *mut u8, + encrypted_data_len: *mut u32, +) -> bitbox_securechip_sys::optiga_lib_status_t { + start(|_| { + if encryption_mode != super::super::OPTIGA_SYMMETRIC_CMAC + || symmetric_key_oid != super::super::key_id_from_oid(super::super::OID_AES_SYMKEY) + || plain_data_len as usize != KDF_LEN + || unsafe { encrypted_data_len.read() } != 16 + { + return OPTIGA_CRYPT_ERROR_INVALID_INPUT; + } + let input = unsafe { core::slice::from_raw_parts(plain_data, KDF_LEN) }; + let out = compute_hmac(&KDF_CMAC_KEY_FIXED, input); + unsafe { + core::ptr::copy_nonoverlapping(out.as_ptr(), encrypted_data, 16); + } + OPTIGA_LIB_SUCCESS + }) +} + +pub(super) unsafe fn crypt_generate_auth_code( + _crypt: *mut bitbox_securechip_sys::optiga_crypt_t, + rng_type: bitbox_securechip_sys::optiga_rng_type_t, + _optional_data: *const u8, + _optional_data_len: u16, + random_data: *mut u8, + random_data_len: u16, +) -> bitbox_securechip_sys::optiga_lib_status_t { + start(|_| { + if rng_type != super::super::OPTIGA_RNG_TYPE_TRNG || random_data_len as usize != KDF_LEN { + return OPTIGA_CRYPT_ERROR_INVALID_INPUT; + } + unsafe { + core::ptr::copy_nonoverlapping(AUTH_CODE_RANDOM_FIXED.as_ptr(), random_data, KDF_LEN); + } + OPTIGA_LIB_SUCCESS + }) +} + +pub(super) unsafe fn crypt_ecdsa_sign( + _crypt: *mut bitbox_securechip_sys::optiga_crypt_t, + _digest: *const u8, + digest_len: u8, + _private_key: bitbox_securechip_sys::optiga_key_id_t, + signature: *mut u8, + signature_len: *mut u16, +) -> bitbox_securechip_sys::optiga_lib_status_t { + const SIG_DER: [u8; 9] = hex!("02021234020300abcd"); + start(|_| { + if digest_len as usize != KDF_LEN || unsafe { signature_len.read() } < SIG_DER.len() as u16 + { + return OPTIGA_CRYPT_ERROR_INVALID_INPUT; + } + unsafe { + core::ptr::copy_nonoverlapping(SIG_DER.as_ptr(), signature, SIG_DER.len()); + signature_len.write(SIG_DER.len() as u16); + } + OPTIGA_LIB_SUCCESS + }) +} + +pub(super) unsafe fn crypt_hmac_verify( + _crypt: *mut bitbox_securechip_sys::optiga_crypt_t, + hmac_type: bitbox_securechip_sys::optiga_hmac_type_t, + secret: u16, + input: *const u8, + input_len: u32, + hmac: *const u8, + hmac_len: u32, +) -> bitbox_securechip_sys::optiga_lib_status_t { + start(|state| { + if hmac_type != super::super::OPTIGA_HMAC_SHA_256 + || input_len as usize != KDF_LEN + || hmac_len as usize != KDF_LEN + { + return OPTIGA_CRYPT_ERROR_INVALID_INPUT; + } + let input = unsafe { core::slice::from_raw_parts(input, KDF_LEN) }; + let hmac = unsafe { core::slice::from_raw_parts(hmac, KDF_LEN) }; + let key = match secret { + super::super::OID_PASSWORD_SECRET => &PASSWORD_SECRET_FIXED, + super::super::OID_PASSWORD => { + // Emulate the small monotonic counter that is attached to password authorization. + // Stored as {counter_be_u32, threshold_be_u32}. + let buf = counter_buf_mut(state, super::super::OID_COUNTER_PASSWORD); + let counter = get_counter_from_buf(buf); + let threshold = get_threshold_from_buf(buf); + if counter >= threshold { + return super::super::OPTIGA_HMAC_VERIFY_FAIL as u16; + } + set_counter_in_buf(buf, counter + 1); + + if !state.oid_password_set { + return OPTIGA_CRYPT_ERROR; + } + &state.oid_password + } + _ => return OPTIGA_CRYPT_ERROR_INVALID_INPUT, + }; + + if compute_hmac(key, input).as_slice() != hmac { + return super::super::OPTIGA_HMAC_VERIFY_FAIL as u16; + } + match secret { + super::super::OID_PASSWORD => state.authorized_password = true, + super::super::OID_PASSWORD_SECRET => state.authorized_password_secret = true, + _ => {} + } + OPTIGA_LIB_SUCCESS + }) +} + +pub(super) unsafe fn crypt_symmetric_generate_key( + _crypt: *mut bitbox_securechip_sys::optiga_crypt_t, + key_type: bitbox_securechip_sys::optiga_symmetric_key_type_t, + key_usage: u8, + _export_symmetric_key: u8, + symmetric_key: *mut core::ffi::c_void, +) -> bitbox_securechip_sys::optiga_lib_status_t { + start(|_| { + if key_type != super::super::OPTIGA_SYMMETRIC_AES_256 + || key_usage != super::super::OPTIGA_KEY_USAGE_ENCRYPTION as u8 + || unsafe { + symmetric_key + .cast::() + .read() + } != super::super::key_id_from_oid(super::super::OID_AES_SYMKEY) + { + return OPTIGA_CRYPT_ERROR_INVALID_INPUT; + } + // We keep using the fixed cmac key in the tests. + OPTIGA_LIB_SUCCESS + }) +} + +pub(super) unsafe fn crypt_clear_auto_state( + _crypt: *mut bitbox_securechip_sys::optiga_crypt_t, + secret: u16, +) -> bitbox_securechip_sys::optiga_lib_status_t { + start(|state| { + match secret { + super::super::OID_PASSWORD => state.authorized_password = false, + super::super::OID_PASSWORD_SECRET => state.authorized_password_secret = false, + _ => {} + } + OPTIGA_LIB_SUCCESS + }) +} + +pub(super) unsafe fn crypt_random( + _crypt: *mut bitbox_securechip_sys::optiga_crypt_t, + _rng_type: bitbox_securechip_sys::optiga_rng_type_t, + out: *mut u8, + out_len: u16, +) -> bitbox_securechip_sys::optiga_lib_status_t { + let mut state = lock_state(); + state.started_operations += 1; + if let Some(error) = state.next_start_error.take() { + return error; + } + + let completion_status = state + .next_completion_error + .take() + .unwrap_or(OPTIGA_LIB_SUCCESS); + if state.block_next_operation { + state.block_next_operation = false; + state.blocked_operation = Some(BlockedOperation::CryptRandom { + out: RetainedMutPtr(out), + out_len, + completion_status, + }); + state.status = OPTIGA_LIB_BUSY; + } else { + if completion_status == OPTIGA_LIB_SUCCESS { + unsafe { + core::slice::from_raw_parts_mut(out, out_len as usize).fill(0); + } + } + state.status = completion_status; + } + OPTIGA_LIB_SUCCESS +} diff --git a/src/rust/bitbox-securechip/src/optiga/raw_ffi.rs b/src/rust/bitbox-securechip/src/optiga/raw_ffi.rs new file mode 100644 index 0000000000..5d9ff01384 --- /dev/null +++ b/src/rust/bitbox-securechip/src/optiga/raw_ffi.rs @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: Apache-2.0 + +pub(super) fn status() -> bitbox_securechip_sys::optiga_lib_status_t { + unsafe { bitbox_securechip_sys::optiga_ops_get_status() } +} + +pub(super) fn set_status_busy() { + unsafe { + bitbox_securechip_sys::optiga_ops_set_status_busy(); + } +} + +pub(super) fn util_instance() -> *mut bitbox_securechip_sys::optiga_util_t { + unsafe { bitbox_securechip_sys::optiga_util_instance() } +} + +pub(super) fn crypt_instance() -> *mut bitbox_securechip_sys::optiga_crypt_t { + unsafe { bitbox_securechip_sys::optiga_crypt_instance() } +} + +pub(super) unsafe fn util_read_data( + util: *mut bitbox_securechip_sys::optiga_util_t, + oid: u16, + offset: u16, + out: *mut u8, + out_len: *mut u16, +) -> bitbox_securechip_sys::optiga_lib_status_t { + unsafe { bitbox_securechip_sys::optiga_util_read_data(util, oid, offset, out, out_len) } +} + +pub(super) unsafe fn crypt_hmac( + crypt: *mut bitbox_securechip_sys::optiga_crypt_t, + hmac_type: bitbox_securechip_sys::optiga_hmac_type_t, + secret: u16, + input: *const u8, + input_len: u32, + mac: *mut u8, + mac_len: *mut u32, +) -> bitbox_securechip_sys::optiga_lib_status_t { + unsafe { + bitbox_securechip_sys::optiga_crypt_hmac( + crypt, hmac_type, secret, input, input_len, mac, mac_len, + ) + } +} + +pub(super) unsafe fn util_write_data( + util: *mut bitbox_securechip_sys::optiga_util_t, + oid: u16, + write_type: u8, + offset: u16, + input: *const u8, + input_len: u16, +) -> bitbox_securechip_sys::optiga_lib_status_t { + unsafe { + bitbox_securechip_sys::optiga_util_write_data( + util, oid, write_type, offset, input, input_len, + ) + } +} + +#[allow(clippy::too_many_arguments)] +pub(super) unsafe fn crypt_symmetric_encrypt( + crypt: *mut bitbox_securechip_sys::optiga_crypt_t, + encryption_mode: bitbox_securechip_sys::optiga_symmetric_encryption_mode_t, + symmetric_key_oid: bitbox_securechip_sys::optiga_key_id_t, + plain_data: *const u8, + plain_data_len: u32, + iv: *const u8, + iv_len: u16, + associated_data: *const u8, + associated_data_len: u16, + encrypted_data: *mut u8, + encrypted_data_len: *mut u32, +) -> bitbox_securechip_sys::optiga_lib_status_t { + unsafe { + bitbox_securechip_sys::optiga_crypt_symmetric_encrypt( + crypt, + encryption_mode, + symmetric_key_oid, + plain_data, + plain_data_len, + iv, + iv_len, + associated_data, + associated_data_len, + encrypted_data, + encrypted_data_len, + ) + } +} + +pub(super) unsafe fn crypt_generate_auth_code( + crypt: *mut bitbox_securechip_sys::optiga_crypt_t, + rng_type: bitbox_securechip_sys::optiga_rng_type_t, + optional_data: *const u8, + optional_data_len: u16, + random_data: *mut u8, + random_data_len: u16, +) -> bitbox_securechip_sys::optiga_lib_status_t { + unsafe { + bitbox_securechip_sys::optiga_crypt_generate_auth_code( + crypt, + rng_type, + optional_data, + optional_data_len, + random_data, + random_data_len, + ) + } +} + +pub(super) unsafe fn crypt_ecdsa_sign( + crypt: *mut bitbox_securechip_sys::optiga_crypt_t, + digest: *const u8, + digest_len: u8, + private_key: bitbox_securechip_sys::optiga_key_id_t, + signature: *mut u8, + signature_len: *mut u16, +) -> bitbox_securechip_sys::optiga_lib_status_t { + unsafe { + bitbox_securechip_sys::optiga_crypt_ecdsa_sign( + crypt, + digest, + digest_len, + private_key, + signature, + signature_len, + ) + } +} + +pub(super) unsafe fn crypt_hmac_verify( + crypt: *mut bitbox_securechip_sys::optiga_crypt_t, + hmac_type: bitbox_securechip_sys::optiga_hmac_type_t, + secret: u16, + input: *const u8, + input_len: u32, + hmac: *const u8, + hmac_len: u32, +) -> bitbox_securechip_sys::optiga_lib_status_t { + unsafe { + bitbox_securechip_sys::optiga_crypt_hmac_verify( + crypt, hmac_type, secret, input, input_len, hmac, hmac_len, + ) + } +} + +pub(super) unsafe fn crypt_symmetric_generate_key( + crypt: *mut bitbox_securechip_sys::optiga_crypt_t, + key_type: bitbox_securechip_sys::optiga_symmetric_key_type_t, + key_usage: u8, + export_symmetric_key: u8, + symmetric_key: *mut core::ffi::c_void, +) -> bitbox_securechip_sys::optiga_lib_status_t { + unsafe { + bitbox_securechip_sys::optiga_crypt_symmetric_generate_key( + crypt, + key_type, + key_usage, + export_symmetric_key, + symmetric_key, + ) + } +} + +pub(super) unsafe fn crypt_clear_auto_state( + crypt: *mut bitbox_securechip_sys::optiga_crypt_t, + secret: u16, +) -> bitbox_securechip_sys::optiga_lib_status_t { + unsafe { bitbox_securechip_sys::optiga_crypt_clear_auto_state(crypt, secret) } +} + +pub(super) unsafe fn crypt_random( + crypt: *mut bitbox_securechip_sys::optiga_crypt_t, + rng_type: bitbox_securechip_sys::optiga_rng_type_t, + out: *mut u8, + out_len: u16, +) -> bitbox_securechip_sys::optiga_lib_status_t { + unsafe { bitbox_securechip_sys::optiga_crypt_random(crypt, rng_type, out, out_len) } +} diff --git a/src/rust/bitbox-securechip/src/optiga/testing.rs b/src/rust/bitbox-securechip/src/optiga/testing.rs new file mode 100644 index 0000000000..129e47d3df --- /dev/null +++ b/src/rust/bitbox-securechip/src/optiga/testing.rs @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Test controls for the Optiga raw backend and production async engine. + +use super::{AsyncOpState, STATE, WAKER, raw}; + +pub(in crate::optiga) fn lock() -> std::sync::MutexGuard<'static, ()> { + raw::test_lock() +} + +pub(in crate::optiga) fn reset() { + STATE.write(AsyncOpState::Idle); + WAKER.clear(); + raw::test_reset(); +} + +pub(in crate::optiga) fn block_next_operation() { + raw::test_block_next_operation(); +} + +pub(in crate::optiga) fn complete_blocked_operation() { + raw::test_complete_blocked_operation(); +} + +pub(in crate::optiga) fn fail_next_start(error: u16) { + raw::test_fail_next_start(error); +} + +pub(in crate::optiga) fn fail_next_completion(error: u16) { + raw::test_fail_next_completion(error); +} + +pub(in crate::optiga) fn set_next_read_len(len: u16) { + raw::test_set_next_read_len(len); +} + +pub(in crate::optiga) fn started_operations() -> usize { + raw::test_started_operations() +} + +pub(in crate::optiga) fn completed_pointer_accesses() -> usize { + raw::test_completed_pointer_accesses() +} + +pub(in crate::optiga) fn seed_oid_password(password_hash: &[u8; super::super::KDF_LEN]) { + raw::test_seed_oid_password(password_hash); +} + +pub(in crate::optiga) fn set_counter(oid: u16, counter: u32, threshold: u32) { + raw::test_set_counter(oid, counter, threshold); +} + +pub(in crate::optiga) fn counter(oid: u16) -> u32 { + raw::test_get_counter(oid) +} + +pub(in crate::optiga) fn threshold(oid: u16) -> u32 { + raw::test_get_threshold(oid) +} From c1fc6ddf1cafcc8579262e137e3025cc731495f7 Mon Sep 17 00:00:00 2001 From: benma's agent Date: Wed, 15 Jul 2026 22:00:31 +0000 Subject: [PATCH 4/4] atecc: test async C operations Compile the production ATECC operation state machine in the C unit suite and fake only device transport calls. Compare every generated command packet and cover delayed completion, device errors, and transport start failures. --- test/unit-test/CMakeLists.txt | 19 ++ test/unit-test/test_atecc_ops.c | 397 ++++++++++++++++++++++++++++++++ 2 files changed, 416 insertions(+) create mode 100644 test/unit-test/test_atecc_ops.c diff --git a/test/unit-test/CMakeLists.txt b/test/unit-test/CMakeLists.txt index a7ce11d230..1a24a922ba 100644 --- a/test/unit-test/CMakeLists.txt +++ b/test/unit-test/CMakeLists.txt @@ -53,6 +53,8 @@ else() "" bootloader_upgrade_check "" + atecc_ops + "-Wl,--gc-sections" util "" ugui @@ -106,6 +108,23 @@ else() ) target_compile_definitions(${EXE} PRIVATE PRODUCT_BITBOX_MULTI=1) endif() + if(TEST_NAME STREQUAL "atecc_ops") + target_sources(${EXE} PRIVATE + ${CMAKE_SOURCE_DIR}/src/atecc/atecc_ops.c + ${CMAKE_SOURCE_DIR}/external/cryptoauthlib/lib/calib/calib_basic.c + ${CMAKE_SOURCE_DIR}/external/cryptoauthlib/lib/calib/calib_command.c + ) + set_source_files_properties( + ${CMAKE_SOURCE_DIR}/external/cryptoauthlib/lib/calib/calib_basic.c + ${CMAKE_SOURCE_DIR}/external/cryptoauthlib/lib/calib/calib_command.c + PROPERTIES COMPILE_OPTIONS "-w" + ) + target_compile_options(${EXE} PRIVATE -ffunction-sections) + target_include_directories(${EXE} SYSTEM PRIVATE + ${CMAKE_SOURCE_DIR}/external + ${CMAKE_SOURCE_DIR}/external/cryptoauthlib/lib + ) + endif() add_test(NAME test_${TEST_NAME} COMMAND ${EXE}) endforeach() endif() diff --git a/test/unit-test/test_atecc_ops.c b/test/unit-test/test_atecc_ops.c new file mode 100644 index 0000000000..98f209ff0f --- /dev/null +++ b/test/unit-test/test_atecc_ops.c @@ -0,0 +1,397 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include + +#include +#include + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" +// clang-format off +#include +#include +// clang-format on +#pragma GCC diagnostic pop + +#define MAX_PACKET_SIZE 192 +#define MAX_RECEIVE_RESULTS 4 + +typedef struct { + ATCA_STATUS status; + uint8_t response[MAX_PACKET_SIZE]; + uint16_t response_len; +} receive_result_t; + +static struct atca_command _command = { + .dt = ATECC608, +}; +static ATCAIfaceCfg _iface_config = { + .iface_type = ATCA_CUSTOM_IFACE, + .devtype = ATECC608, +}; +static struct atca_iface _iface = { + .mIfaceCFG = &_iface_config, +}; +static struct atca_device _device = { + .mCommands = &_command, + .mIface = &_iface, +}; + +static ATCA_STATUS _wake_status; +static ATCA_STATUS _send_status; +static size_t _wake_calls; +static size_t _send_calls; +static size_t _receive_calls; +static size_t _idle_calls; +static uint8_t _sent_packet[MAX_PACKET_SIZE]; +static size_t _sent_packet_len; +static receive_result_t _receive_results[MAX_RECEIVE_RESULTS]; +static size_t _receive_result_count; + +static void _crc(const uint8_t* data, size_t len, uint8_t* crc_out) +{ + uint16_t crc = 0; + for (size_t i = 0; i < len; i++) { + for (uint8_t mask = 1; mask != 0; mask <<= 1) { + uint8_t data_bit = (data[i] & mask) != 0; + uint8_t crc_bit = crc >> 15; + crc <<= 1; + if (data_bit != crc_bit) { + crc ^= 0x8005; + } + } + } + crc_out[0] = (uint8_t)crc; + crc_out[1] = (uint8_t)(crc >> 8); +} + +static void _make_response(const uint8_t* data, size_t data_len, receive_result_t* result) +{ + result->status = ATCA_SUCCESS; + result->response_len = (uint16_t)(data_len + ATCA_PACKET_OVERHEAD); + result->response[0] = (uint8_t)result->response_len; + memcpy(&result->response[ATCA_RSP_DATA_IDX], data, data_len); + _crc( + result->response, + result->response_len - ATCA_CRC_SIZE, + &result->response[result->response_len - ATCA_CRC_SIZE]); +} + +static void _queue_receive_error(ATCA_STATUS status) +{ + assert_true(_receive_result_count < MAX_RECEIVE_RESULTS); + _receive_results[_receive_result_count++].status = status; +} + +static void _queue_response(const uint8_t* data, size_t data_len) +{ + assert_true(_receive_result_count < MAX_RECEIVE_RESULTS); + _make_response(data, data_len, &_receive_results[_receive_result_count++]); +} + +static void _reset_transport(void) +{ + _wake_status = ATCA_SUCCESS; + _send_status = ATCA_SUCCESS; + _wake_calls = 0; + _send_calls = 0; + _receive_calls = 0; + _idle_calls = 0; + _sent_packet_len = 0; + _receive_result_count = 0; + memset(_sent_packet, 0, sizeof(_sent_packet)); + memset(_receive_results, 0, sizeof(_receive_results)); +} + +ATCADevice atcab_get_device(void) +{ + return &_device; +} + +ATCA_STATUS atwake(ATCAIface iface) +{ + assert_ptr_equal(iface, &_iface); + _wake_calls++; + return _wake_status; +} + +ATCA_STATUS atsend(ATCAIface iface, uint8_t word_address, uint8_t* txdata, int txlength) +{ + assert_ptr_equal(iface, &_iface); + assert_int_equal(word_address, 0xff); + assert_int_equal(txlength, txdata[1]); + assert_true((size_t)txlength <= sizeof(_sent_packet)); + _send_calls++; + _sent_packet_len = (size_t)txlength; + memcpy(_sent_packet, &txdata[1], _sent_packet_len); + return _send_status; +} + +ATCA_STATUS atreceive(ATCAIface iface, uint8_t word_address, uint8_t* rxdata, uint16_t* rxlength) +{ + assert_ptr_equal(iface, &_iface); + assert_int_equal(word_address, 0); + assert_true(_receive_calls < _receive_result_count); + const receive_result_t* result = &_receive_results[_receive_calls++]; + if (result->status != ATCA_SUCCESS) { + return result->status; + } + assert_true(*rxlength >= result->response_len); + memcpy(rxdata, result->response, result->response_len); + *rxlength = result->response_len; + return ATCA_SUCCESS; +} + +ATCA_STATUS atidle(ATCAIface iface) +{ + assert_ptr_equal(iface, &_iface); + _idle_calls++; + return ATCA_SUCCESS; +} + +ATCA_STATUS atca_trace(ATCA_STATUS status) +{ + return status; +} + +ATCA_STATUS atcah_nonce(struct atca_nonce_in_out* param) +{ + (void)param; + return ATCA_SUCCESS; +} + +static void _assert_sent_packet( + uint8_t opcode, + uint8_t param1, + uint16_t param2, + const uint8_t* data, + size_t data_len) +{ + uint8_t expected[MAX_PACKET_SIZE] = {0}; + size_t expected_len = ATCA_CMD_SIZE_MIN + data_len; + assert_true(expected_len <= sizeof(expected)); + expected[ATCA_COUNT_IDX] = (uint8_t)expected_len; + expected[ATCA_OPCODE_IDX] = opcode; + expected[ATCA_PARAM1_IDX] = param1; + expected[ATCA_PARAM2_IDX] = (uint8_t)param2; + expected[ATCA_PARAM2_IDX + 1] = (uint8_t)(param2 >> 8); + if (data_len != 0) { + memcpy(&expected[ATCA_DATA_IDX], data, data_len); + } + _crc(expected, expected_len - ATCA_CRC_SIZE, &expected[expected_len - ATCA_CRC_SIZE]); + + assert_int_equal(atecc_ops_get_status(), ATECC_OPS_STATUS_BUSY); + assert_int_equal(atecc_ops_get_poll_delay_ms(), ATCA_POLLING_INIT_TIME_MSEC); + assert_int_equal(_wake_calls, 1); + assert_int_equal(_send_calls, 1); + assert_int_equal(_sent_packet_len, expected_len); + assert_memory_equal(_sent_packet, expected, expected_len); + + const uint8_t success = 0; + _queue_response(&success, sizeof(success)); + atecc_ops_poll(); + assert_int_equal(atecc_ops_get_status(), ATCA_SUCCESS); + assert_int_equal(atecc_ops_get_poll_delay_ms(), 0); + assert_int_equal(_idle_calls, 1); +} + +static void _fill_sequence(uint8_t* out, size_t len, uint8_t start) +{ + for (size_t i = 0; i < len; i++) { + out[i] = start + (uint8_t)i; + } +} + +/** Verifies every asynchronous start function emits the same complete ATECC wire packet. */ +static void test_atecc_ops_command_packets(void** state) +{ + (void)state; + uint8_t data + [CHECKMAC_CLIENT_CHALLENGE_SIZE + CHECKMAC_CLIENT_RESPONSE_SIZE + + CHECKMAC_OTHER_DATA_SIZE] = {0}; + uint8_t input[64]; + uint8_t mac[WRITE_MAC_SIZE]; + _fill_sequence(input, sizeof(input), 0x10); + _fill_sequence(mac, sizeof(mac), 0xa0); + + _reset_transport(); + assert_int_equal(atecc_cmd_start_nonce_rand(input), ATCA_SUCCESS); + _assert_sent_packet(ATCA_NONCE, NONCE_MODE_SEED_UPDATE, 0, input, NONCE_NUMIN_SIZE); + + _reset_transport(); + memcpy(&data[CHECKMAC_CLIENT_CHALLENGE_SIZE], input, CHECKMAC_CLIENT_RESPONSE_SIZE); + assert_int_equal(atecc_cmd_start_checkmac(input), ATCA_SUCCESS); + _assert_sent_packet( + ATCA_CHECKMAC, CHECKMAC_MODE_BLOCK2_TEMPKEY, ATECC_SLOT_AUTHKEY, data, sizeof(data)); + + _reset_transport(); + assert_int_equal(atecc_cmd_start_random(), ATCA_SUCCESS); + _assert_sent_packet(ATCA_RANDOM, RANDOM_SEED_UPDATE, 0, NULL, 0); + + _reset_transport(); + assert_int_equal(atecc_cmd_start_counter_read(), ATCA_SUCCESS); + _assert_sent_packet(ATCA_COUNTER, COUNTER_MODE_READ, 0, NULL, 0); + + _reset_transport(); + assert_int_equal(atecc_cmd_start_info_revision(), ATCA_SUCCESS); + _assert_sent_packet(ATCA_INFO, INFO_MODE_REVISION, 0, NULL, 0); + + _reset_transport(); + const size_t kdf_input_len = 17; + uint8_t kdf_data[KDF_DETAILS_SIZE + kdf_input_len]; + memset(kdf_data, 0, sizeof(kdf_data)); + kdf_data[0] = KDF_DETAILS_HKDF_MSG_LOC_INPUT; + kdf_data[3] = kdf_input_len; + memcpy(&kdf_data[KDF_DETAILS_SIZE], input, kdf_input_len); + assert_int_equal(atecc_cmd_start_kdf(ATECC_SLOT_KDF, input, kdf_input_len), ATCA_SUCCESS); + _assert_sent_packet( + ATCA_KDF, + KDF_MODE_SOURCE_SLOT | KDF_MODE_TARGET_OUTPUT_ENC | KDF_MODE_ALG_HKDF, + ATECC_SLOT_KDF, + kdf_data, + sizeof(kdf_data)); + + _reset_transport(); + assert_int_equal(atecc_cmd_start_derivekey_rollkey(), ATCA_SUCCESS); + _assert_sent_packet(ATCA_DERIVE_KEY, 0, ATECC_SLOT_ROLLKEY, NULL, 0); + + _reset_transport(); + assert_int_equal(atecc_cmd_start_nonce_load_msgdigest(input), ATCA_SUCCESS); + _assert_sent_packet( + ATCA_NONCE, + NONCE_MODE_PASSTHROUGH | NONCE_MODE_TARGET_MSGDIGBUF, + 0, + input, + NONCE_NUMIN_SIZE_PASSTHROUGH); + + _reset_transport(); + assert_int_equal(atecc_cmd_start_sign_attestation(), ATCA_SUCCESS); + _assert_sent_packet( + ATCA_SIGN, + SIGN_MODE_EXTERNAL | SIGN_MODE_SOURCE_MSGDIGBUF, + ATECC_SLOT_ATTESTATION, + NULL, + 0); + + _reset_transport(); + assert_int_equal(atecc_io_prepare_tempkey(input, &input[NONCE_NUMIN_SIZE]), ATCA_SUCCESS); + assert_int_equal(atecc_cmd_start_gendig_encryption_key(), ATCA_SUCCESS); + const uint8_t gendig_data[] = { + ATCA_GENDIG, + GENDIG_ZONE_DATA, + ATECC_SLOT_ENCRYPTION_KEY, + 0, + }; + _assert_sent_packet( + ATCA_GENDIG, GENDIG_ZONE_DATA, ATECC_SLOT_ENCRYPTION_KEY, gendig_data, sizeof(gendig_data)); + + _reset_transport(); + assert_int_equal(atecc_cmd_start_read_block(ATECC_SLOT_DATA0, 2), ATCA_SUCCESS); + _assert_sent_packet( + ATCA_READ, + ATCA_ZONE_DATA | ATCA_ZONE_READWRITE_32, + (2 << 8) | (ATECC_SLOT_DATA0 << 3), + NULL, + 0); + + _reset_transport(); + uint8_t write_data[ATCA_BLOCK_SIZE + WRITE_MAC_SIZE]; + memcpy(write_data, input, ATCA_BLOCK_SIZE); + memcpy(&write_data[ATCA_BLOCK_SIZE], mac, WRITE_MAC_SIZE); + assert_int_equal( + atecc_cmd_start_write_encrypted_block(ATECC_SLOT_DATA0, 2, input, mac), ATCA_SUCCESS); + _assert_sent_packet( + ATCA_WRITE, + ATCA_ZONE_DATA | ATCA_ZONE_READWRITE_32 | ATCA_ZONE_ENCRYPTED, + (2 << 8) | (ATECC_SLOT_DATA0 << 3), + write_data, + sizeof(write_data)); +} + +/** Verifies the C state needed to complete a delayed command after Rust detaches its future. */ +static void test_atecc_ops_delayed_response_for_detached_recovery(void** state) +{ + (void)state; + uint8_t expected_random[RANDOM_NUM_SIZE]; + uint8_t random_out[RANDOM_NUM_SIZE] = {0}; + _fill_sequence(expected_random, sizeof(expected_random), 0x40); + + _reset_transport(); + _queue_receive_error(ATCA_RX_NO_RESPONSE); + _queue_response(expected_random, sizeof(expected_random)); + assert_int_equal(atecc_cmd_start_random(), ATCA_SUCCESS); + + atecc_ops_poll(); + assert_int_equal(atecc_ops_get_status(), ATECC_OPS_STATUS_BUSY); + assert_int_equal(atecc_ops_get_poll_delay_ms(), ATCA_POLLING_FREQUENCY_TIME_MSEC); + assert_int_equal(_idle_calls, 0); + + // Rust keeps polling after cancellation; C must retain the command until a poll succeeds. + assert_int_equal(atecc_ops_get_status(), ATECC_OPS_STATUS_BUSY); + atecc_ops_poll(); + assert_int_equal(atecc_ops_get_status(), ATCA_SUCCESS); + assert_int_equal(_idle_calls, 1); + assert_int_equal(atecc_cmd_read_random_response(random_out), ATCA_SUCCESS); + assert_memory_equal(random_out, expected_random, sizeof(random_out)); +} + +/** Verifies a valid device error frame becomes the operation result and idles the chip. */ +static void test_atecc_ops_device_error(void** state) +{ + (void)state; + const uint8_t execution_error = 0x0f; + _reset_transport(); + _queue_response(&execution_error, sizeof(execution_error)); + + assert_int_equal(atecc_cmd_start_random(), ATCA_SUCCESS); + atecc_ops_poll(); + + assert_int_equal(atecc_ops_get_status(), ATCA_EXECUTION_ERROR); + assert_int_equal(atecc_ops_get_poll_delay_ms(), 0); + assert_int_equal(_idle_calls, 1); +} + +/** Verifies wake and send failures finish immediately and leave the next command usable. */ +static void test_atecc_ops_start_errors(void** state) +{ + (void)state; + _reset_transport(); + _wake_status = ATCA_COMM_FAIL; + assert_int_equal(atecc_cmd_start_random(), ATCA_COMM_FAIL); + assert_int_equal(atecc_ops_get_status(), ATCA_COMM_FAIL); + assert_int_equal(atecc_ops_get_poll_delay_ms(), 0); + assert_int_equal(_send_calls, 0); + assert_int_equal(_idle_calls, 1); + + _reset_transport(); + _send_status = ATCA_COMM_FAIL; + assert_int_equal(atecc_cmd_start_random(), ATCA_COMM_FAIL); + assert_int_equal(atecc_ops_get_status(), ATCA_COMM_FAIL); + assert_int_equal(atecc_ops_get_poll_delay_ms(), 0); + assert_int_equal(_send_calls, 1); + assert_int_equal(_idle_calls, 1); + + _reset_transport(); + const uint8_t success = 0; + _queue_response(&success, sizeof(success)); + assert_int_equal(atecc_cmd_start_random(), ATCA_SUCCESS); + atecc_ops_poll(); + assert_int_equal(atecc_ops_get_status(), ATCA_SUCCESS); + assert_int_equal(_idle_calls, 1); +} + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_atecc_ops_command_packets), + cmocka_unit_test(test_atecc_ops_delayed_response_for_detached_recovery), + cmocka_unit_test(test_atecc_ops_device_error), + cmocka_unit_test(test_atecc_ops_start_errors), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +}