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/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-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/Cargo.toml b/src/rust/bitbox-securechip/Cargo.toml index d5589be139..cf17741731 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] @@ -25,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 f6935c12f4..ed87bb3d32 100644 --- a/src/rust/bitbox-securechip/src/atecc.rs +++ b/src/rust/bitbox-securechip/src/atecc.rs @@ -1,119 +1,753 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{Error, Model, PasswordStretchAlgo, SecureChipError}; -use alloc::boxed::Box; -use bitbox_hal::Memory; +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; -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(()), +#[path = "atecc/ops.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); + } +} + +// 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 { + 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 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) +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(Error::from_status(status)) + Err(-1) } } -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_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 reset_keys() -> Result<(), ()> { - match unsafe { bitbox_securechip_sys::atecc_reset_keys() } { - true => Ok(()), - false => Err(()), +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, +) -> Result>, Error> { + 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 fn init_new_password( - _memory: &mut impl Memory, +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 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_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)) + 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 stretch_password( - _memory: &mut impl Memory, +pub async fn stretch_password( + 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()?; + 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 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 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 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) +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 { - Err(Error::from_status(status)) + Model::ATECC_ATECC608A + }) +} + +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); } } -#[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(()), +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")), } } -#[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(()), +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"); } -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) { + // 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() -> ( + std::sync::MutexGuard<'static, ()>, + FakeMemory, + TestingRandom, + ) { + let guard = ops::testing::lock(); + ops::testing::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::testing::derivekey_rollkey_calls(), 1); + assert_eq!(ops::testing::kdf_key_write_calls(), 1); + assert!(!ops::testing::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::testing::rollkey_kdf_calls(), 1); + assert_eq!(ops::testing::kdf_calls(), KDF_NUM_ITERATIONS); + } + + #[async_test::test] + #[allow(clippy::await_holding_lock)] + async fn test_random_busy() { + let (_guard, _memory, _random) = setup_test(); + 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!( + random::().await, + Err(Error::Status(ATECC_OPS_STATUS_BUSY)), + ); + + ops::testing::unblock_chip_command(); + drop(first.await.unwrap()); + } + + #[async_test::test] + #[allow(clippy::await_holding_lock)] + async fn test_model_busy() { + let (_guard, _memory, _random) = setup_test(); + 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!(model::().await, Err(())); + + ops::testing::unblock_chip_command(); + drop(first.await.unwrap()); + } + + /// 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::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); + + 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); + } + + #[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(); + 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::testing::io_temp_key_active()); + + drop(task); + + 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 new file mode 100644 index 0000000000..794942b053 --- /dev/null +++ b/src/rust/bitbox-securechip/src/atecc/ops.rs @@ -0,0 +1,336 @@ +// 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; + +#[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; +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 { + raw::status() +} + +fn ensure_success(status: i32) -> Result<(), i32> { + if status == 0 { Ok(()) } else { Err(status) } +} + +pub(super) fn clear_io_temp_key() { + raw::clear_io_temp_key(); +} + +async fn poll_once() { + let delay_ms = raw::poll_delay_ms() as u64; + if delay_ms > 0 { + T::delay_for(Duration::from_millis(delay_ms)).await; + } + raw::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, 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, 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, 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, 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, raw::start_info_revision()).await?; + ensure_success(raw::read_info_response(&mut revision))?; + 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, 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, 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, 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, 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, raw::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, raw::start_read_block(slot, block)).await?; + ensure_success(raw::read_block_response(&mut data))?; + 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, + raw::start_write_encrypted_block(slot, block, value, mac), + ) + .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(raw::auth_compute_response( + num_in, + rand_out, + auth_key, + &mut response, + ))?; + 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(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(raw::io_prepare_tempkey(num_in, rand_out)) +} + +pub(super) fn host_gendig(encryption_key: &[u8; 32]) -> Result<(), i32> { + ensure_success(raw::io_apply_gendig(encryption_key)) +} + +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(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(raw::io_decrypt_block(data)) +} 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 9d599d6ece..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"))] @@ -535,38 +531,39 @@ pub async fn u2f_counter_inc() -> Result { Ok(counter) } -pub fn model() -> Result { +pub async fn model() -> Result { Ok(Model::OPTIGA_TRUST_M_V3) } #[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) +} 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/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/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 { 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]] 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); +}