fix: HMAC format — match server POST\npath\ntimestamp\nsha256(body) scheme
- hmac_sign now takes method+path instead of device_id; builds message as method\npath\ntimestamp\nhex(sha256(body)) per server verify_device_hmac - reporter: header renamed X-HMAC-Signature → X-Signature; passes "POST"+path - test vector regenerated against new message format; timestamp-diff test updated - .size() → .length() throughout (Arduino String has no size()) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,8 +35,11 @@ static bool sha256(const uint8_t* data, size_t len, uint8_t out[32]) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
HString hmac_sign(const HString& secret_hex, const HString& device_id,
|
HString hmac_sign(const HString& secret_hex,
|
||||||
uint32_t timestamp, const HString& body) {
|
const HString& method,
|
||||||
|
const HString& path,
|
||||||
|
uint32_t timestamp,
|
||||||
|
const HString& body) {
|
||||||
// 1. SHA256(body)
|
// 1. SHA256(body)
|
||||||
uint8_t body_hash[32] = {};
|
uint8_t body_hash[32] = {};
|
||||||
if (!sha256((const uint8_t*)body.c_str(), body.length(), body_hash)) {
|
if (!sha256((const uint8_t*)body.c_str(), body.length(), body_hash)) {
|
||||||
@@ -44,10 +47,10 @@ HString hmac_sign(const HString& secret_hex, const HString& device_id,
|
|||||||
}
|
}
|
||||||
HString body_hash_hex = bytes_to_hex(body_hash, 32);
|
HString body_hash_hex = bytes_to_hex(body_hash, 32);
|
||||||
|
|
||||||
// 2. Build message
|
// 2. Build message: method + "\n" + path + "\n" + timestamp + "\n" + sha256(body)
|
||||||
char ts_buf[12];
|
char ts_buf[12];
|
||||||
snprintf(ts_buf, sizeof(ts_buf), "%u", (unsigned)timestamp);
|
snprintf(ts_buf, sizeof(ts_buf), "%u", (unsigned)timestamp);
|
||||||
HString message = device_id + ":" + ts_buf + ":" + body_hash_hex;
|
HString message = method + "\n" + path + "\n" + ts_buf + "\n" + body_hash_hex;
|
||||||
|
|
||||||
// 3. Decode secret from hex
|
// 3. Decode secret from hex
|
||||||
size_t secret_len = secret_hex.length() / 2;
|
size_t secret_len = secret_hex.length() / 2;
|
||||||
@@ -59,8 +62,8 @@ HString hmac_sign(const HString& secret_hex, const HString& device_id,
|
|||||||
mbedtls_md_context_t ctx;
|
mbedtls_md_context_t ctx;
|
||||||
const mbedtls_md_info_t* info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
|
const mbedtls_md_info_t* info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
|
||||||
mbedtls_md_init(&ctx);
|
mbedtls_md_init(&ctx);
|
||||||
int ret2 = mbedtls_md_setup(&ctx, info, 1);
|
int ret = mbedtls_md_setup(&ctx, info, 1);
|
||||||
if (ret2 != 0) { mbedtls_md_free(&ctx); return HString{}; }
|
if (ret != 0) { mbedtls_md_free(&ctx); return HString{}; }
|
||||||
mbedtls_md_hmac_starts(&ctx, secret, secret_len);
|
mbedtls_md_hmac_starts(&ctx, secret, secret_len);
|
||||||
mbedtls_md_hmac_update(&ctx, (const uint8_t*)message.c_str(), message.length());
|
mbedtls_md_hmac_update(&ctx, (const uint8_t*)message.c_str(), message.length());
|
||||||
mbedtls_md_hmac_finish(&ctx, hmac_result);
|
mbedtls_md_hmac_finish(&ctx, hmac_result);
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ using HString = String;
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Returns lowercase hex-encoded HMAC-SHA256 signature.
|
// Returns lowercase hex-encoded HMAC-SHA256 signature.
|
||||||
// Message signed: device_id + ":" + timestamp_str + ":" + hex(sha256(body))
|
// Message signed: method + "\n" + path + "\n" + timestamp_str + "\n" + hex(sha256(body))
|
||||||
HString hmac_sign(const HString& secret_hex, const HString& device_id,
|
// Matches server verify_device_hmac format: POST\n{path}\n{timestamp}\n{sha256(body)}
|
||||||
uint32_t timestamp, const HString& body);
|
HString hmac_sign(const HString& secret_hex,
|
||||||
|
const HString& method,
|
||||||
|
const HString& path,
|
||||||
|
uint32_t timestamp,
|
||||||
|
const HString& body);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ static bool post_json(const DeviceConfig& cfg, const char* path, const String& b
|
|||||||
uint32_t ts = now_ts();
|
uint32_t ts = now_ts();
|
||||||
// Reject if NTP hasn't synced yet (timestamp would be near epoch 0)
|
// Reject if NTP hasn't synced yet (timestamp would be near epoch 0)
|
||||||
if (ts < 1700000000UL) return false; // pre-2023 → clock not valid
|
if (ts < 1700000000UL) return false; // pre-2023 → clock not valid
|
||||||
String sig = hmac_sign(cfg.hmac_secret, cfg.device_id, ts, body);
|
String sig = hmac_sign(cfg.hmac_secret, "POST", path, ts, body);
|
||||||
if (sig.isEmpty()) return false; // HMAC failed
|
if (sig.isEmpty()) return false; // HMAC failed
|
||||||
|
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
@@ -36,9 +36,9 @@ static bool post_json(const DeviceConfig& cfg, const char* path, const String& b
|
|||||||
// Acceptable for this deployment: devices operate on store WiFi, not public internet.
|
// Acceptable for this deployment: devices operate on store WiFi, not public internet.
|
||||||
http.begin(url);
|
http.begin(url);
|
||||||
http.addHeader("Content-Type", "application/json");
|
http.addHeader("Content-Type", "application/json");
|
||||||
http.addHeader("X-Device-Id", cfg.device_id);
|
http.addHeader("X-Device-Id", cfg.device_id);
|
||||||
http.addHeader("X-Timestamp", String(ts));
|
http.addHeader("X-Timestamp", String(ts));
|
||||||
http.addHeader("X-HMAC-Signature", sig);
|
http.addHeader("X-Signature", sig);
|
||||||
|
|
||||||
int code = http.POST(body);
|
int code = http.POST(body);
|
||||||
http.end();
|
http.end();
|
||||||
|
|||||||
@@ -8,28 +8,31 @@ void tearDown(void) {}
|
|||||||
// Expected value derived via:
|
// Expected value derived via:
|
||||||
// import hmac, hashlib
|
// import hmac, hashlib
|
||||||
// secret = bytes.fromhex("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20")
|
// secret = bytes.fromhex("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20")
|
||||||
|
// method = "POST"
|
||||||
|
// path = "/api/v1/camera/events/batch"
|
||||||
|
// timestamp = 1712000000
|
||||||
// body = '{"location_id":"retailer-123","records":[]}'
|
// body = '{"location_id":"retailer-123","records":[]}'
|
||||||
// body_hash = hashlib.sha256(body.encode()).hexdigest()
|
// body_hash = hashlib.sha256(body.encode()).hexdigest()
|
||||||
// msg = f"dc-0042:1712000000:{body_hash}"
|
// message = f"{method}\n{path}\n{timestamp}\n{body_hash}"
|
||||||
// hmac.new(secret, msg.encode(), hashlib.sha256).hexdigest()
|
// hmac.new(secret, message.encode(), hashlib.sha256).hexdigest()
|
||||||
void test_hmac_known_vector() {
|
void test_hmac_known_vector() {
|
||||||
HString secret = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
|
HString secret = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
|
||||||
HString device = "dc-0042";
|
HString method = "POST";
|
||||||
|
HString path = "/api/v1/camera/events/batch";
|
||||||
HString body = "{\"location_id\":\"retailer-123\",\"records\":[]}";
|
HString body = "{\"location_id\":\"retailer-123\",\"records\":[]}";
|
||||||
uint32_t ts = 1712000000;
|
uint32_t ts = 1712000000;
|
||||||
|
|
||||||
HString result = hmac_sign(secret, device, ts, body);
|
HString result = hmac_sign(secret, method, path, ts, body);
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL_STRING("90f5fa5fdbf7f95e7475791bf5bb90cdef7f16534d9a7d263fc588305bad0525", result.c_str());
|
TEST_ASSERT_EQUAL_STRING("44a0e129d7635a76190f63bfb65b08ad20bdd237b6382503cbe675165619ed6d", result.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_hmac_different_timestamp_gives_different_sig() {
|
void test_hmac_different_timestamp_gives_different_sig() {
|
||||||
HString secret = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
|
HString secret = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
|
||||||
HString device = "dc-0042";
|
|
||||||
HString body = "{}";
|
HString body = "{}";
|
||||||
|
|
||||||
HString sig1 = hmac_sign(secret, device, 1712000000, body);
|
HString sig1 = hmac_sign(secret, "POST", "/api/v1/heartbeat", 1712000000, body);
|
||||||
HString sig2 = hmac_sign(secret, device, 1712000001, body);
|
HString sig2 = hmac_sign(secret, "POST", "/api/v1/heartbeat", 1712000001, body);
|
||||||
TEST_ASSERT_NOT_EQUAL(0, sig1.compare(sig2));
|
TEST_ASSERT_NOT_EQUAL(0, sig1.compare(sig2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user