diff --git a/firmware/lib/hmac/hmac.cpp b/firmware/lib/hmac/hmac.cpp index 6e174b2..a3903c7 100644 --- a/firmware/lib/hmac/hmac.cpp +++ b/firmware/lib/hmac/hmac.cpp @@ -35,8 +35,11 @@ static bool sha256(const uint8_t* data, size_t len, uint8_t out[32]) { return true; } -HString hmac_sign(const HString& secret_hex, const HString& device_id, - 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) { // 1. SHA256(body) uint8_t body_hash[32] = {}; 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); - // 2. Build message + // 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 = device_id + ":" + ts_buf + ":" + body_hash_hex; + HString message = method + "\n" + path + "\n" + ts_buf + "\n" + body_hash_hex; // 3. Decode secret from hex 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; const mbedtls_md_info_t* info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); mbedtls_md_init(&ctx); - int ret2 = mbedtls_md_setup(&ctx, info, 1); - if (ret2 != 0) { mbedtls_md_free(&ctx); return HString{}; } + 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); diff --git a/firmware/lib/hmac/hmac.h b/firmware/lib/hmac/hmac.h index 0b291fc..e0ca0a9 100644 --- a/firmware/lib/hmac/hmac.h +++ b/firmware/lib/hmac/hmac.h @@ -11,6 +11,10 @@ using HString = String; #endif // Returns lowercase hex-encoded HMAC-SHA256 signature. -// Message signed: device_id + ":" + timestamp_str + ":" + hex(sha256(body)) -HString hmac_sign(const HString& secret_hex, const HString& device_id, - uint32_t timestamp, const HString& body); +// Message signed: method + "\n" + path + "\n" + timestamp_str + "\n" + hex(sha256(body)) +// Matches server verify_device_hmac format: POST\n{path}\n{timestamp}\n{sha256(body)} +HString hmac_sign(const HString& secret_hex, + const HString& method, + const HString& path, + uint32_t timestamp, + const HString& body); diff --git a/firmware/src/reporter.cpp b/firmware/src/reporter.cpp index 865d028..25223f8 100644 --- a/firmware/src/reporter.cpp +++ b/firmware/src/reporter.cpp @@ -25,7 +25,7 @@ static bool post_json(const DeviceConfig& cfg, const char* path, const String& b uint32_t ts = now_ts(); // Reject if NTP hasn't synced yet (timestamp would be near epoch 0) 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 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. http.begin(url); http.addHeader("Content-Type", "application/json"); - http.addHeader("X-Device-Id", cfg.device_id); - http.addHeader("X-Timestamp", String(ts)); - http.addHeader("X-HMAC-Signature", sig); + http.addHeader("X-Device-Id", cfg.device_id); + http.addHeader("X-Timestamp", String(ts)); + http.addHeader("X-Signature", sig); int code = http.POST(body); http.end(); diff --git a/firmware/test/test_native/test_hmac.cpp b/firmware/test/test_native/test_hmac.cpp index 5bd6779..c95ae29 100644 --- a/firmware/test/test_native/test_hmac.cpp +++ b/firmware/test/test_native/test_hmac.cpp @@ -8,28 +8,31 @@ void tearDown(void) {} // Expected value derived via: // import hmac, hashlib // secret = bytes.fromhex("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20") +// method = "POST" +// path = "/api/v1/camera/events/batch" +// timestamp = 1712000000 // body = '{"location_id":"retailer-123","records":[]}' // body_hash = hashlib.sha256(body.encode()).hexdigest() -// msg = f"dc-0042:1712000000:{body_hash}" -// hmac.new(secret, msg.encode(), hashlib.sha256).hexdigest() +// message = f"{method}\n{path}\n{timestamp}\n{body_hash}" +// hmac.new(secret, message.encode(), hashlib.sha256).hexdigest() void test_hmac_known_vector() { HString secret = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"; - HString device = "dc-0042"; + HString method = "POST"; + HString path = "/api/v1/camera/events/batch"; HString body = "{\"location_id\":\"retailer-123\",\"records\":[]}"; 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() { HString secret = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"; - HString device = "dc-0042"; HString body = "{}"; - HString sig1 = hmac_sign(secret, device, 1712000000, body); - HString sig2 = hmac_sign(secret, device, 1712000001, body); + HString sig1 = hmac_sign(secret, "POST", "/api/v1/heartbeat", 1712000000, body); + HString sig2 = hmac_sign(secret, "POST", "/api/v1/heartbeat", 1712000001, body); TEST_ASSERT_NOT_EQUAL(0, sig1.compare(sig2)); }