fix: tighten version parsing, propagate HMAC sign failure, add deployment docs
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -79,16 +79,17 @@ void ota_updater_init(const char* server_base, const char* device_id,
|
|||||||
s_last_check_ms = 0; // force first check on next call
|
s_last_check_ms = 0; // force first check on next call
|
||||||
}
|
}
|
||||||
|
|
||||||
static void add_hmac_headers(HTTPClient& http, const char* method, const char* path) {
|
static bool add_hmac_headers(HTTPClient& http, const char* method, const char* path) {
|
||||||
uint32_t ts = (uint32_t)(esp_timer_get_time() / 1000000ULL);
|
uint32_t ts = (uint32_t)(esp_timer_get_time() / 1000000ULL);
|
||||||
String sig = hmac_sign(s_hmac_secret, method, path, ts, "");
|
String sig = hmac_sign(s_hmac_secret, method, path, ts, "");
|
||||||
if (sig.isEmpty()) {
|
if (sig.isEmpty()) {
|
||||||
log_e("[OTA] HMAC sign failed");
|
log_e("[OTA] HMAC sign failed");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
http.addHeader("X-Device-Id", s_device_id);
|
http.addHeader("X-Device-Id", s_device_id);
|
||||||
http.addHeader("X-Timestamp", String(ts));
|
http.addHeader("X-Timestamp", String(ts));
|
||||||
http.addHeader("X-HMAC-Signature", sig);
|
http.addHeader("X-HMAC-Signature", sig);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool download_and_flash(const char* fw_url, size_t expected_size,
|
static bool download_and_flash(const char* fw_url, size_t expected_size,
|
||||||
@@ -111,7 +112,12 @@ static bool download_and_flash(const char* fw_url, size_t expected_size,
|
|||||||
|
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
http.begin(fw_url);
|
http.begin(fw_url);
|
||||||
add_hmac_headers(http, "GET", "/ota/firmware");
|
if (!add_hmac_headers(http, "GET", "/ota/firmware")) {
|
||||||
|
log_e("[OTA] Aborting firmware download: HMAC sign failed");
|
||||||
|
mbedtls_sha256_free(&sha_ctx);
|
||||||
|
esp_ota_abort(handle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
int code = http.GET();
|
int code = http.GET();
|
||||||
if (code != HTTP_CODE_OK) {
|
if (code != HTTP_CODE_OK) {
|
||||||
log_e("[OTA] Firmware fetch failed: HTTP %d", code);
|
log_e("[OTA] Firmware fetch failed: HTTP %d", code);
|
||||||
@@ -177,7 +183,11 @@ bool ota_updater_check_and_apply() {
|
|||||||
|
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
http.begin(check_url);
|
http.begin(check_url);
|
||||||
add_hmac_headers(http, "GET", check_path);
|
if (!add_hmac_headers(http, "GET", check_path)) {
|
||||||
|
log_e("[OTA] Aborting check: HMAC sign failed");
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
int code = http.GET();
|
int code = http.GET();
|
||||||
if (code != HTTP_CODE_OK) {
|
if (code != HTTP_CODE_OK) {
|
||||||
log_w("[OTA] Check failed: HTTP %d", code);
|
log_w("[OTA] Check failed: HTTP %d", code);
|
||||||
|
|||||||
@@ -2,18 +2,29 @@
|
|||||||
"""
|
"""
|
||||||
OTA firmware update endpoints.
|
OTA firmware update endpoints.
|
||||||
|
|
||||||
To register in the server main app:
|
Deployment workflow:
|
||||||
from server.ota_endpoint import router as ota_router
|
1. Generate signing key (one-time):
|
||||||
app.include_router(ota_router)
|
python tools/gen_signing_key.py
|
||||||
|
→ secrets/firmware_signing_key.pem (keep offline)
|
||||||
|
→ firmware/lib/ota_updater/ota_pubkey.h (commit this)
|
||||||
|
|
||||||
Route handlers have HMAC auth commented out pending import-path confirmation:
|
2. Build and deploy a new firmware version:
|
||||||
from .main import verify_device_hmac # adjust to actual module
|
pio run -e timercam # build
|
||||||
Then uncomment the Depends lines in ota_check() and ota_firmware().
|
python tools/deploy_firmware.py \\
|
||||||
|
firmware/.pio/build/timercam/firmware.bin 1.2.3
|
||||||
|
→ server/firmware/ updated with current.bin, current.sig, manifest.json
|
||||||
|
|
||||||
Firmware artifacts expected under FIRMWARE_DIR (default: server/firmware/):
|
3. Bump FW_VERSION in firmware/include/version.h before each release.
|
||||||
current.bin — raw firmware binary
|
|
||||||
current.sig — 64-byte r‖s Ed25519/ECDSA signature
|
4. Register in server main app:
|
||||||
manifest.json — {"version": "X.Y.Z", "size": N, "sha256": "hex..."}
|
from server.ota_endpoint import router as ota_router
|
||||||
|
app.include_router(ota_router)
|
||||||
|
Also uncomment Depends(verify_device_hmac) on both route handlers
|
||||||
|
and confirm the HMAC format matches hmac.cpp:
|
||||||
|
method + "\\n" + path + "\\n" + timestamp + "\\n" + sha256_hex(body)
|
||||||
|
|
||||||
|
Note: HMAC auth is currently commented out on route handlers — must be wired
|
||||||
|
before production use. verify_device_hmac must use the same format as hmac.cpp.
|
||||||
"""
|
"""
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
@@ -35,6 +46,8 @@ def _parse_version(v: str) -> tuple:
|
|||||||
"""Parse semver string to comparable tuple; returns (0,0,0) on malformed input."""
|
"""Parse semver string to comparable tuple; returns (0,0,0) on malformed input."""
|
||||||
try:
|
try:
|
||||||
parts = v.strip().split(".")
|
parts = v.strip().split(".")
|
||||||
|
if len(parts) != 3:
|
||||||
|
return (0, 0, 0)
|
||||||
return tuple(int(x) for x in parts)
|
return tuple(int(x) for x in parts)
|
||||||
except (ValueError, AttributeError):
|
except (ValueError, AttributeError):
|
||||||
return (0, 0, 0)
|
return (0, 0, 0)
|
||||||
|
|||||||
@@ -74,3 +74,10 @@ def test_check_malformed_manifest(tmp_path):
|
|||||||
(tmp_path / "manifest.json").write_text("not valid json{{{")
|
(tmp_path / "manifest.json").write_text("not valid json{{{")
|
||||||
result = ota_check_impl(current_version="1.0.0", firmware_dir=tmp_path)
|
result = ota_check_impl(current_version="1.0.0", firmware_dir=tmp_path)
|
||||||
assert result["update"] is False
|
assert result["update"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_wrong_arity_version_no_update(tmp_path):
|
||||||
|
write_firmware(tmp_path, "1.2") # wrong arity server version
|
||||||
|
result = ota_check_impl(current_version="1.0.0", firmware_dir=tmp_path)
|
||||||
|
# server "1.2" → (0,0,0) ≤ client (1,0,0) → no update
|
||||||
|
assert result["update"] is False
|
||||||
|
|||||||
Reference in New Issue
Block a user