ota: make user validation set the ota and confirmation status
What changed, and why it matters
This commit restructures how a Blockstream Jade hardware wallet confirms firmware updates. It moves the 'user has approved' flag into the validation routine itself and adds a safety check: if the first chunk of firmware data is smaller than expected, the device now aborts rather than risk skipping the user-approval step. The change is defensive hardening, not a confirmed exploit fix, because the commit message only calls it a theoretical case.
Treat as a hardening improvement rather than an urgent vulnerability. Review whether the small-first-write path is reachable in practice and consider adding a regression test. No immediate user action is required unless the vendor later tags this as a security fix.
Security signals we found
Refactored user-confirmation state machine so the validated_confirmed flag is set in exactly one place
Added explicit abort path if the first decompressed/patched chunk is smaller than the custom header, preventing a theoretical skip of user validation
Removed an assertion that the progress bar object existed before updating it
Changed function signature from returning status to writing status into shared context
Evidence from the diff
The patch refactors OTA user validation. Previously ota_user_validation() returned an ota_status_t and callers set joctx->validated_confirmed on success. Now ota_user_validate() is void and sets validated_confirmed internally only after all checks pass and the user confirms. Error results are written to joctx->ota_return_status. Callers in ota.c and ota_delta.c now check that field and, if validation was skipped because the first write was smaller than CUSTOM_HEADER_MIN_WRITE, they set an error and abort. A JADE_ASSERT on the progress bar was also removed.
Changed components
main/process/ota.cmain/process/ota_delta.cmain/process/ota_util.cmain/process/ota_util.hInspect captured patch +28 / −19
diff --git a/main/process/ota.c b/main/process/ota.c
index 0be6b3c..eda789e 100644
--- a/main/process/ota.c
+++ b/main/process/ota.c
@@ -31,13 +31,11 @@ static int uncompressed_stream_writer(void* ctx, uint8_t* uncompressed, size_t l
jade_ota_ctx_t* joctx = (jade_ota_ctx_t*)ctx;
if (!joctx->validated_confirmed && length >= CUSTOM_HEADER_MIN_WRITE) {
- const ota_status_t res = ota_user_validation(joctx, uncompressed);
- if (res != OTA_SUCCESS) {
- JADE_LOGE("ota_user_validation() error, %u", res);
- joctx->ota_return_status = res;
- return res;
+ // We have the header: ask the user to confirm the OTA
+ ota_user_validate(joctx, uncompressed);
+ if (joctx->ota_return_status != OTA_SUCCESS) {
+ return joctx->ota_return_status;
}
- joctx->validated_confirmed = true;
}
const esp_err_t res = esp_ota_write(joctx->ota_handle, (const void*)uncompressed, length);
@@ -60,12 +58,14 @@ static int uncompressed_stream_writer(void* ctx, uint8_t* uncompressed, size_t l
JADE_ASSERT(joctx->uncompressedsize - joctx->remaining_uncompressed == joctx->fwwritten);
if (joctx->fwwritten > CUSTOM_HEADER_MIN_WRITE && !joctx->validated_confirmed) {
+ // It is theoretically possible for the writer to initially write
+ // less than the header, which would cause us to skip validation.
+ joctx->ota_return_status = OTA_ERR_DECOMPRESS;
return DEFLATE_ERROR;
}
/* Update the progress bar once the user has confirmed and upload is in progress */
if (joctx->validated_confirmed) {
- JADE_ASSERT(joctx->progress_bar.progress_bar);
update_progress_bar(&joctx->progress_bar, joctx->uncompressedsize, joctx->fwwritten);
}
diff --git a/main/process/ota_delta.c b/main/process/ota_delta.c
index a56ff08..9a635e9 100644
--- a/main/process/ota_delta.c
+++ b/main/process/ota_delta.c
@@ -100,11 +100,11 @@ static int ota_stream_writer(const struct bspatch_stream_n* stream, const void*
}
if (!joctx->validated_confirmed && length >= CUSTOM_HEADER_MIN_WRITE) {
- const ota_status_t validation = ota_user_validation(joctx, (uint8_t*)buffer);
- if (validation != OTA_SUCCESS) {
- HANDLE_NEW_ERROR(joctx, validation);
+ // We have the header: ask the user to confirm the OTA
+ ota_user_validate(joctx, (uint8_t*)buffer);
+ if (joctx->ota_return_status != OTA_SUCCESS) {
+ HANDLE_NEW_ERROR(joctx, joctx->ota_return_status);
}
- joctx->validated_confirmed = true;
}
if (joctx->hash_type == HASHTYPE_FULLFWDATA) {
@@ -119,6 +119,8 @@ static int ota_stream_writer(const struct bspatch_stream_n* stream, const void*
JADE_ASSERT(joctx->uncompressedsize - joctx->remaining_uncompressed > joctx->fwwritten);
if (joctx->fwwritten > CUSTOM_HEADER_MIN_WRITE && !joctx->validated_confirmed) {
+ // It is theoretically possible for the writer to initially write
+ // less than the header, which would cause us to skip validation.
HANDLE_NEW_ERROR(joctx, OTA_ERR_PATCH);
}
diff --git a/main/process/ota_util.c b/main/process/ota_util.c
index f13b57a..e73a843 100644
--- a/main/process/ota_util.c
+++ b/main/process/ota_util.c
@@ -307,7 +307,7 @@ static void to_lower(char* dest, const char* src)
*dest = '\0';
}
-ota_status_t ota_user_validation(jade_ota_ctx_t* joctx, const uint8_t* uncompressed)
+void ota_user_validate(jade_ota_ctx_t* joctx, const uint8_t* uncompressed)
{
JADE_ASSERT(joctx);
JADE_ASSERT(uncompressed);
@@ -317,6 +317,7 @@ ota_status_t ota_user_validation(jade_ota_ctx_t* joctx, const uint8_t* uncompres
JADE_ASSERT(joctx->update_partition);
JADE_ASSERT(joctx->running_partition);
JADE_ASSERT(joctx->ota_handle);
+ JADE_ASSERT(joctx->ota_return_status == OTA_SUCCESS);
JADE_LOGI("Running firmware version: %s", running_app_info.version);
@@ -324,7 +325,8 @@ ota_status_t ota_user_validation(jade_ota_ctx_t* joctx, const uint8_t* uncompres
const esp_image_header_t* header = (esp_image_header_t*)uncompressed;
if (header->chip_id != CONFIG_IDF_FIRMWARE_CHIP_ID) {
JADE_LOGE("Mismatch chip id, expected %d, found %d", CONFIG_IDF_FIRMWARE_CHIP_ID, header->chip_id);
- return OTA_ERR_INVALIDFW;
+ joctx->ota_return_status = OTA_ERR_INVALIDFW;
+ return;
}
const size_t app_info_offset = sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t);
@@ -334,7 +336,8 @@ ota_status_t ota_user_validation(jade_ota_ctx_t* joctx, const uint8_t* uncompres
if (esp_efuse_check_secure_version(new_app_info->secure_version) == false) {
JADE_LOGE("Secure version downgrade not allowed");
- return OTA_ERR_NODOWNGRADE;
+ joctx->ota_return_status = OTA_ERR_NODOWNGRADE;
+ return;
}
const size_t custom_info_offset = app_info_offset + sizeof(esp_app_desc_t);
@@ -344,12 +347,14 @@ ota_status_t ota_user_validation(jade_ota_ctx_t* joctx, const uint8_t* uncompres
// 'Config' is allowed to differ.
if (strcmp(JADE_OTA_BOARD_TYPE, custom_info->board_type)) {
JADE_LOGE("Firmware board type mismatch %s %s", JADE_OTA_BOARD_TYPE, custom_info->board_type);
- return OTA_ERR_INVALIDFW;
+ joctx->ota_return_status = OTA_ERR_INVALIDFW;
+ return;
}
if (strcmp(JADE_OTA_FEATURES, custom_info->features)) {
JADE_LOGE("Firmware features mismatch");
- return OTA_ERR_INVALIDFW;
+ joctx->ota_return_status = OTA_ERR_INVALIDFW;
+ return;
}
// User to confirm once new firmware version known and all checks passed
@@ -370,7 +375,8 @@ ota_status_t ota_user_validation(jade_ota_ctx_t* joctx, const uint8_t* uncompres
// Ask user to confirm
if (!show_ota_versions_activity(current_version, new_version, joctx->expected_hash_hexstr, full_fw_hash)) {
JADE_LOGW("User declined ota firmware version");
- return OTA_ERR_USERDECLINED;
+ joctx->ota_return_status = OTA_ERR_USERDECLINED;
+ return;
}
// Now user has confirmed, display the progress bar
@@ -379,7 +385,8 @@ ota_status_t ota_user_validation(jade_ota_ctx_t* joctx, const uint8_t* uncompres
gui_set_current_activity_ex(act, true); // free prior activities
vTaskDelay(100 / portTICK_PERIOD_MS); // time for screen to update
- return OTA_SUCCESS;
+ // Mark the OTA as validated/user confirmed
+ joctx->validated_confirmed = true;
}
const char* ota_get_status_text(const ota_status_t status)
diff --git a/main/process/ota_util.h b/main/process/ota_util.h
index 0e33a9c..7c7770e 100644
--- a/main/process/ota_util.h
+++ b/main/process/ota_util.h
@@ -78,7 +78,7 @@ void handle_in_bin_data(void* ctx, uint8_t* data, size_t rawsize);
jade_ota_ctx_t* ota_init(jade_process_t* process, bool is_delta);
ota_status_t post_ota_check(jade_ota_ctx_t* joctx);
-ota_status_t ota_user_validation(jade_ota_ctx_t* joctx, const uint8_t* uncompressed);
+void ota_user_validate(jade_ota_ctx_t* joctx, const uint8_t* uncompressed);
const char* ota_get_status_text(ota_status_t status);
#endif /* JADE_OTA_UTIL_H_ */
Why this scored 42/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.