From 6e529e828b94a6bf8a745532cfbab9690927557c Mon Sep 17 00:00:00 2001 From: Julio Cesar Date: Mon, 25 May 2026 22:01:08 +0200 Subject: [PATCH] Require button authorization for OTA updates --- main/global_state.h | 1 + .../app/components/edit/edit.component.html | 14 ++++++ .../src/app/components/edit/edit.component.ts | 6 ++- .../components/update/update.component.html | 6 +++ .../axe-os/src/app/services/system.service.ts | 2 + main/http_server/http_server.c | 45 ++++++++++++++++++- main/http_server/http_server.h | 3 ++ main/http_server/openapi.yaml | 11 +++++ main/http_server/system_api_json.c | 2 + main/nvs_config.c | 1 + main/nvs_config.h | 1 + main/screen.c | 3 ++ 12 files changed, 92 insertions(+), 3 deletions(-) diff --git a/main/global_state.h b/main/global_state.h index 500409012d..961baabd13 100644 --- a/main/global_state.h +++ b/main/global_state.h @@ -104,6 +104,7 @@ typedef struct uint32_t lastClockSync; bool is_screen_active; bool is_firmware_update; + int64_t ota_authorized_until; char firmware_update_filename[20]; char firmware_update_status[20]; bool hardware_fault; diff --git a/main/http_server/axe-os/src/app/components/edit/edit.component.html b/main/http_server/axe-os/src/app/components/edit/edit.component.html index 6c2062e59c..7c7fc9efaa 100644 --- a/main/http_server/axe-os/src/app/components/edit/edit.component.html +++ b/main/http_server/axe-os/src/app/components/edit/edit.component.html @@ -168,6 +168,20 @@

Statistics

+

Update Security

+ +
+
+
+ +
+ +
+ + Press BOOT once before changing this setting or uploading firmware. + +
+
diff --git a/main/http_server/axe-os/src/app/components/edit/edit.component.ts b/main/http_server/axe-os/src/app/components/edit/edit.component.ts index 3c880622cf..25e330c3da 100644 --- a/main/http_server/axe-os/src/app/components/edit/edit.component.ts +++ b/main/http_server/axe-os/src/app/components/edit/edit.component.ts @@ -173,7 +173,8 @@ export class EditComponent implements OnInit, OnDestroy, OnChanges { Validators.required, Validators.min(0), Validators.max(this.statsFrequencyMaxValue) - ]] + ]], + otaAuthRequired: [info.otaAuthRequired, [Validators.required]] }); this.formSubject.next(this.form); @@ -342,7 +343,8 @@ export class EditComponent implements OnInit, OnDestroy, OnChanges { 'manualFanSpeed', 'temptarget', 'overheat_mode', - 'statsFrequency' + 'statsFrequency', + 'otaAuthRequired' ]; } diff --git a/main/http_server/axe-os/src/app/components/update/update.component.html b/main/http_server/axe-os/src/app/components/update/update.component.html index 7863a64ba4..9a8e35328a 100644 --- a/main/http_server/axe-os/src/app/components/update/update.component.html +++ b/main/http_server/axe-os/src/app/components/update/update.component.html @@ -5,6 +5,12 @@

Update

Current Version: {{(info$ | async)?.version}}
+ +

Latest Release: Check

diff --git a/main/http_server/axe-os/src/app/services/system.service.ts b/main/http_server/axe-os/src/app/services/system.service.ts index b7ace854cf..2ac870854d 100644 --- a/main/http_server/axe-os/src/app/services/system.service.ts +++ b/main/http_server/axe-os/src/app/services/system.service.ts @@ -142,6 +142,8 @@ export class SystemApiService { boardtemp2: 40, overheat_mode: 0, statsLimit: 720, + otaAuthRequired: true, + otaAuthorized: false, blockHeight: 811111, scriptsig: "..%..h..,H...ckpool.eu/solo.ckpool.org/", diff --git a/main/http_server/http_server.c b/main/http_server/http_server.c index bc8b712087..f670c14729 100644 --- a/main/http_server/http_server.c +++ b/main/http_server/http_server.c @@ -61,11 +61,42 @@ static const char * STATS_LABEL_FAN2_RPM = "fan2Rpm"; static const char * STATS_LABEL_WIFI_RSSI = "wifiRssi"; static const char * STATS_LABEL_FREE_HEAP = "freeHeap"; static const char * STATS_LABEL_RESPONSE_TIME = "responseTime"; +#define OTA_AUTH_WINDOW_US (5LL * 60LL * 1000000LL) static int system_info_prebuffer_len = 256; static int system_statistics_prebuffer_len = 256; static int system_wifi_scan_prebuffer_len = 256; static int api_common_prebuffer_len = 256; +static GlobalState * GLOBAL_STATE; + +void HTTP_authorize_ota_update(void) +{ + if (!GLOBAL_STATE) { + return; + } + + GLOBAL_STATE->SYSTEM_MODULE.ota_authorized_until = esp_timer_get_time() + OTA_AUTH_WINDOW_US; + ESP_LOGW(TAG, "OTA updates authorized for 5 minutes by physical button press"); +} + +bool HTTP_is_ota_update_authorized(void) +{ + if (!GLOBAL_STATE || !nvs_config_get_bool(NVS_CONFIG_OTA_AUTH_REQUIRED)) { + return true; + } + + return esp_timer_get_time() <= GLOBAL_STATE->SYSTEM_MODULE.ota_authorized_until; +} + +static esp_err_t check_ota_update_authorized(httpd_req_t *req) +{ + if (HTTP_is_ota_update_authorized()) { + return ESP_OK; + } + + httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "Press the BOOT button to authorize OTA updates for 5 minutes"); + return ESP_FAIL; +} typedef enum { @@ -145,7 +176,6 @@ static esp_err_t GET_system_logs(httpd_req_t *req) return res; } -static GlobalState * GLOBAL_STATE; static httpd_handle_t server = NULL; static int stratum_protocol_from_string(const char *s, uint16_t *out) @@ -621,6 +651,13 @@ bool check_settings_and_update(const cJSON * const root) ESP_LOGW(TAG, "Invalid display rotation: '%d'", item->valueint); result = false; } + if (key == NVS_CONFIG_OTA_AUTH_REQUIRED) { + bool requested_auth_required = item->valueint != 0 || cJSON_IsTrue(item); + if (!requested_auth_required && !HTTP_is_ota_update_authorized()) { + ESP_LOGW(TAG, "Refusing to disable OTA button authorization without a recent physical button press"); + result = false; + } + } } if (result) { @@ -1083,6 +1120,9 @@ esp_err_t POST_WWW_update(httpd_req_t * req) if (is_network_allowed(req) != ESP_OK) { return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized"); } + if (check_ota_update_authorized(req) != ESP_OK) { + return ESP_OK; + } wifi_mode_t mode; esp_wifi_get_mode(&mode); @@ -1167,6 +1207,9 @@ esp_err_t POST_OTA_update(httpd_req_t * req) if (is_network_allowed(req) != ESP_OK) { return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized"); } + if (check_ota_update_authorized(req) != ESP_OK) { + return ESP_OK; + } wifi_mode_t mode; esp_wifi_get_mode(&mode); diff --git a/main/http_server/http_server.h b/main/http_server/http_server.h index 5d801479b4..2d01eaa7ed 100644 --- a/main/http_server/http_server.h +++ b/main/http_server/http_server.h @@ -2,11 +2,14 @@ #define HTTP_SERVER_H_ #include +#include #include "cJSON.h" esp_err_t is_network_allowed(httpd_req_t * req); esp_err_t start_rest_server(void *pvParameters); esp_err_t HTTP_send_json(httpd_req_t * req, const cJSON * item, int * prebuffer_len); +void HTTP_authorize_ota_update(void); +bool HTTP_is_ota_update_authorized(void); #endif /* HTTP_SERVER_H_ */ diff --git a/main/http_server/openapi.yaml b/main/http_server/openapi.yaml index 1f888c10c3..99879fba59 100644 --- a/main/http_server/openapi.yaml +++ b/main/http_server/openapi.yaml @@ -204,6 +204,8 @@ components: - hashrateMonitor - miningPaused - statsLimit + - otaAuthRequired + - otaAuthorized properties: ASICModel: type: string @@ -533,6 +535,12 @@ components: statsLimit: type: integer description: Maximum number of statistics data points + otaAuthRequired: + type: boolean + description: Whether OTA updates require recent physical button authorization + otaAuthorized: + type: boolean + description: Whether OTA updates are currently authorized by a recent physical button press SystemASIC: type: object @@ -834,6 +842,9 @@ components: minimum: 0 examples: - 120 + otaAuthRequired: + type: boolean + description: Require a recent physical BOOT button press before OTA updates. Disabling this also requires recent authorization. additionalProperties: true responses: diff --git a/main/http_server/system_api_json.c b/main/http_server/system_api_json.c index 5855e61491..752707041f 100644 --- a/main/http_server/system_api_json.c +++ b/main/http_server/system_api_json.c @@ -227,6 +227,8 @@ static void system_api_add_config(cJSON *root, GlobalState *g) { cJSON_AddFloatToObject(root, "frequency", nvs_config_get_float(NVS_CONFIG_ASIC_FREQUENCY)); cJSON_AddNumberToObject(root, "statsFrequency", nvs_config_get_u16(NVS_CONFIG_STATISTICS_FREQUENCY)); cJSON_AddNumberToObject(root, "statsLimit", MAX_STATISTICS_COUNT); + cJSON_AddBoolToObject(root, "otaAuthRequired", nvs_config_get_bool(NVS_CONFIG_OTA_AUTH_REQUIRED)); + cJSON_AddBoolToObject(root, "otaAuthorized", g->SYSTEM_MODULE.ota_authorized_until >= esp_timer_get_time()); } static void system_api_add_hashrate_monitor(cJSON *root, GlobalState *g) { diff --git a/main/nvs_config.c b/main/nvs_config.c index 2513a8be86..9f4f56b14d 100644 --- a/main/nvs_config.c +++ b/main/nvs_config.c @@ -94,6 +94,7 @@ static Settings settings[NVS_CONFIG_COUNT] = { [NVS_CONFIG_THEME_SCHEME] = {.nvs_key_name = "themescheme", .type = TYPE_STR, .default_value = {.str = DEFAULT_THEME}}, [NVS_CONFIG_THEME_COLORS] = {.nvs_key_name = "themecolors", .type = TYPE_STR, .default_value = {.str = DEFAULT_COLORS}}, [NVS_CONFIG_SCOREBOARD] = {.nvs_key_name = "scoreboard", .type = TYPE_STR, .array_size = MAX_SCOREBOARD}, + [NVS_CONFIG_OTA_AUTH_REQUIRED] = {.nvs_key_name = "otaauthreq", .type = TYPE_BOOL, .default_value = {.b = true}, .rest_name = "otaAuthRequired", .min = 0, .max = 1}, [NVS_CONFIG_BOARD_VERSION] = {.nvs_key_name = "boardversion", .type = TYPE_STR, .default_value = {.str = "000"}}, [NVS_CONFIG_DEVICE_MODEL] = {.nvs_key_name = "devicemodel", .type = TYPE_STR, .default_value = {.str = "unknown"}}, diff --git a/main/nvs_config.h b/main/nvs_config.h index e761ca4975..1ce94fe8e1 100644 --- a/main/nvs_config.h +++ b/main/nvs_config.h @@ -54,6 +54,7 @@ typedef enum { NVS_CONFIG_THEME_SCHEME, NVS_CONFIG_THEME_COLORS, NVS_CONFIG_SCOREBOARD, + NVS_CONFIG_OTA_AUTH_REQUIRED, NVS_CONFIG_BOARD_VERSION, NVS_CONFIG_DEVICE_MODEL, diff --git a/main/screen.c b/main/screen.c index 6f1fc55b4c..c8aea70936 100644 --- a/main/screen.c +++ b/main/screen.c @@ -10,6 +10,7 @@ #include "display.h" #include "connect.h" #include "esp_timer.h" +#include "http_server.h" typedef enum { SCR_SELF_TEST, @@ -651,6 +652,8 @@ static void screen_update_cb(lv_timer_t * timer) void screen_button_press() { + HTTP_authorize_ota_update(); + if (GLOBAL_STATE->SYSTEM_MODULE.identify_mode_time_ms > 0) { GLOBAL_STATE->SYSTEM_MODULE.identify_mode_time_ms = 0; } else {