feat(ota): add firmware digest verification (MD5/HMAC-SHA256) - #17
Merged
Merged
Conversation
heshaoqiong-tuya
force-pushed
the
feat/ota-firmware-digest-verification
branch
from
August 20, 2026 09:41
e242ac1 to
81096df
Compare
Add streaming verifier API (iot_ota_verify_init/update/finish/abort) that validates downloaded OTA firmware against the cloud-provided md5/hmac digest. The cloud already returns these fields via iot_ota_check_upgrade(), but nothing validated the downloaded bytes against them. Algorithm matches TuyaOpen's tuya_ota.c: when hmac is present, the expected value is HMAC-SHA256(device secret_key, UPPERCASE_hex(SHA-256(image))) — the HMAC message is the 64-char uppercase hex string of the SHA-256 digest (matching TuyaOpen's hex2str). When only md5 is present, falls back to plain MD5. Comparison is case-insensitive with constant-time hex compare. Digest selection treats an empty string as absent rather than as a malformed digest: the cloud sends `"hmac": ""` for an algorithm it has not configured, so keying off non-NULL alone would make init return OPRT_INVALID_PARAMETER and leave the md5 branch unreachable, failing the upgrade outright on any device configured with md5 only. A non-empty digest of the wrong length stays a hard error — a malformed hmac must never silently downgrade to the weaker md5. New error code OPRT_OTA_VERIFY_FAILED (-0x000D) — skips -0x0008..-0x000C already used by iot_dp.h. init does not write *ctx_out on any error path, so the skip path for OPRT_NOT_SUPPORTED is `if (ctx != NULL)` around both update and finish; finish(NULL) is a parameter error, not "skip". The guide spells this out because getting it wrong fails open: an application that drops init's return value flashes and boots an unverified image. Both OTA demos verify before completing the upgrade — the ESP-IDF demo checks the digest before esp_ota_set_boot_partition, the POSIX demo re-reads the downloaded file and checks ferror() so an I/O error is not reported as a digest mismatch. Unit tests with Python-generated known-answer vectors cover match, mismatch, streamed chunking, uppercase expected, md5 fallback, empty and malformed digests, NULL guards and abort (iot_ota_verify_test 12/12). iot_ota_test 9/9, posix examples build clean. Verified end-to-end against a real Tuya cloud OTA task (firmware v2.2.8, 3254320 bytes): hmac(sec_key, SHA-256_hex_UPPER) matches the cloud value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
heshaoqiong-tuya
force-pushed
the
feat/ota-firmware-digest-verification
branch
from
August 20, 2026 09:46
81096df to
e09d502
Compare
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add streaming verifier API (iot_ota_verify_init/update/finish/abort) that validates downloaded OTA firmware against the cloud-provided md5/hmac digest. The cloud already returns these fields via iot_ota_check_upgrade(), but nothing validated the downloaded bytes against them.
Algorithm matches TuyaOpen's tuya_ota.c: when hmac is present, the expected value is HMAC-SHA256(device secret_key,
UPPERCASE_hex(SHA-256(image))) — the HMAC message is the 64-char uppercase hex string of the SHA-256 digest (matching TuyaOpen's hex2str). When only md5 is present, falls back to plain MD5. Comparison is case-insensitive with constant-time hex compare.
New error code OPRT_OTA_VERIFY_FAILED (-0x000D) — skips -0x0008..-0x000C already used by iot_dp.h. Both OTA demos (ESP-IDF + POSIX) now verify the digest before completing the upgrade. Unit tests with Python-generated known-answer vectors cover match/mismatch/streaming/edge cases.
Verified end-to-end against a real Tuya cloud OTA task (firmware v2.2.8, 3254320 bytes): hmac(sec_key, SHA-256_hex_UPPER) matches the cloud value.