hmac_sign() previously trusted whatever secret_hex came out of NVS: - Lengths >128 chars overflowed the fixed 64-byte stack buffer in hex_to_bytes (out_len was unbounded). - Non-hex characters were silently decoded to 0 via strtol with no end-pointer check, producing signatures under a corrupted key. - Empty secrets fell through to mbedtls_md_hmac_starts with len=0. flash_device.py now rejects malformed --hmac-secret at provision time, but hmac_sign should also refuse to sign under a malformed key regardless of how it ended up in NVS (legacy provisioning, partial flash, etc.). Add length, hex-charset, and even-length validation; make hex_to_bytes return bool and have hmac_sign return empty HString on any failure (callers already treat empty as failure via post_json_once). Found via adversarial review (run 2026-05-01-202910, both reviewers). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
93 lines
3.1 KiB
C++
93 lines
3.1 KiB
C++
// firmware/src/hmac.cpp
|
|
#include "hmac.h"
|
|
#include "mbedtls/md.h"
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
static HString bytes_to_hex(const uint8_t* bytes, size_t len) {
|
|
HString out;
|
|
char buf[3];
|
|
for (size_t i = 0; i < len; i++) {
|
|
snprintf(buf, sizeof(buf), "%02x", bytes[i]);
|
|
out += buf;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
static bool is_hex_char(char c) {
|
|
return (c >= '0' && c <= '9') ||
|
|
(c >= 'a' && c <= 'f') ||
|
|
(c >= 'A' && c <= 'F');
|
|
}
|
|
|
|
static bool hex_to_bytes(const HString& hex, uint8_t* out, size_t out_len) {
|
|
if (hex.length() != out_len * 2) return false;
|
|
for (size_t i = 0; i < out_len; i++) {
|
|
char a = hex[i*2], b = hex[i*2+1];
|
|
if (!is_hex_char(a) || !is_hex_char(b)) return false;
|
|
char byte_str[3] = {a, b, 0};
|
|
out[i] = (uint8_t)strtol(byte_str, nullptr, 16);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool sha256(const uint8_t* data, size_t len, uint8_t out[32]) {
|
|
mbedtls_md_context_t ctx;
|
|
const mbedtls_md_info_t* info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
|
|
mbedtls_md_init(&ctx);
|
|
int ret = mbedtls_md_setup(&ctx, info, 0);
|
|
if (ret != 0) { mbedtls_md_free(&ctx); return false; }
|
|
mbedtls_md_starts(&ctx);
|
|
mbedtls_md_update(&ctx, data, len);
|
|
mbedtls_md_finish(&ctx, out);
|
|
mbedtls_md_free(&ctx);
|
|
return true;
|
|
}
|
|
|
|
HString hmac_sign(const HString& secret_hex,
|
|
const HString& method,
|
|
const HString& path,
|
|
uint32_t timestamp,
|
|
const HString& body) {
|
|
// 1. SHA256(body)
|
|
uint8_t body_hash[32] = {};
|
|
if (!sha256((const uint8_t*)body.c_str(), body.length(), body_hash)) {
|
|
return HString{};
|
|
}
|
|
HString body_hash_hex = bytes_to_hex(body_hash, 32);
|
|
|
|
// 2. Build message: method + "\n" + path + "\n" + timestamp + "\n" + sha256(body)
|
|
char ts_buf[12];
|
|
snprintf(ts_buf, sizeof(ts_buf), "%u", (unsigned)timestamp);
|
|
HString message = method + "\n" + path + "\n" + ts_buf + "\n" + body_hash_hex;
|
|
|
|
// 3. Decode secret from hex. Reject empty / odd-length / oversized /
|
|
// non-hex inputs — flash_device.py validates at provision time, but
|
|
// hmac_sign refuses to sign under a malformed key regardless of how it
|
|
// ended up in NVS (legacy provisioning, NVS corruption, etc.).
|
|
if (secret_hex.length() == 0 ||
|
|
secret_hex.length() > 128 ||
|
|
secret_hex.length() % 2 != 0) {
|
|
return HString{};
|
|
}
|
|
size_t secret_len = secret_hex.length() / 2;
|
|
uint8_t secret[64] = {};
|
|
if (!hex_to_bytes(secret_hex, secret, secret_len)) {
|
|
return HString{};
|
|
}
|
|
|
|
// 4. HMAC-SHA256(secret, message)
|
|
uint8_t hmac_result[32];
|
|
mbedtls_md_context_t ctx;
|
|
const mbedtls_md_info_t* info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
|
|
mbedtls_md_init(&ctx);
|
|
int ret = mbedtls_md_setup(&ctx, info, 1);
|
|
if (ret != 0) { mbedtls_md_free(&ctx); return HString{}; }
|
|
mbedtls_md_hmac_starts(&ctx, secret, secret_len);
|
|
mbedtls_md_hmac_update(&ctx, (const uint8_t*)message.c_str(), message.length());
|
|
mbedtls_md_hmac_finish(&ctx, hmac_result);
|
|
mbedtls_md_free(&ctx);
|
|
|
|
return bytes_to_hex(hmac_result, 32);
|
|
}
|