From 47f3f6afef753783a2fe39f3fd50f76e92a4773d Mon Sep 17 00:00:00 2001 From: Peter Woolery Date: Mon, 13 Apr 2026 14:20:24 -0700 Subject: [PATCH] feat: HMAC-SHA256 signing module with native tests Co-Authored-By: Claude Sonnet 4.6 --- firmware/lib/hmac/hmac.cpp | 64 +++++++++++++++++++++++++ firmware/lib/hmac/hmac.h | 16 +++++++ firmware/platformio.ini | 2 + firmware/test/test_native/test_hmac.cpp | 34 +++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 firmware/lib/hmac/hmac.cpp create mode 100644 firmware/lib/hmac/hmac.h create mode 100644 firmware/test/test_native/test_hmac.cpp diff --git a/firmware/lib/hmac/hmac.cpp b/firmware/lib/hmac/hmac.cpp new file mode 100644 index 0000000..899f5f2 --- /dev/null +++ b/firmware/lib/hmac/hmac.cpp @@ -0,0 +1,64 @@ +// firmware/src/hmac.cpp +#include "hmac.h" +#include "mbedtls/md.h" +#include +#include + +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 void hex_to_bytes(const HString& hex, uint8_t* out, size_t out_len) { + for (size_t i = 0; i < out_len && (i * 2 + 1) < hex.size(); i++) { + char byte_str[3] = {hex[i*2], hex[i*2+1], 0}; + out[i] = (uint8_t)strtol(byte_str, nullptr, 16); + } +} + +static void 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); + mbedtls_md_setup(&ctx, info, 0); + mbedtls_md_starts(&ctx); + mbedtls_md_update(&ctx, data, len); + mbedtls_md_finish(&ctx, out); + mbedtls_md_free(&ctx); +} + +HString hmac_sign(const HString& secret_hex, const HString& device_id, + uint32_t timestamp, const HString& body) { + // 1. SHA256(body) + uint8_t body_hash[32]; + sha256((const uint8_t*)body.c_str(), body.size(), body_hash); + HString body_hash_hex = bytes_to_hex(body_hash, 32); + + // 2. Build message + char ts_buf[12]; + snprintf(ts_buf, sizeof(ts_buf), "%u", (unsigned)timestamp); + HString message = device_id + ":" + ts_buf + ":" + body_hash_hex; + + // 3. Decode secret from hex + size_t secret_len = secret_hex.size() / 2; + uint8_t secret[64] = {}; + hex_to_bytes(secret_hex, secret, secret_len); + + // 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); + mbedtls_md_setup(&ctx, info, 1); + mbedtls_md_hmac_starts(&ctx, secret, secret_len); + mbedtls_md_hmac_update(&ctx, (const uint8_t*)message.c_str(), message.size()); + mbedtls_md_hmac_finish(&ctx, hmac_result); + mbedtls_md_free(&ctx); + + return bytes_to_hex(hmac_result, 32); +} diff --git a/firmware/lib/hmac/hmac.h b/firmware/lib/hmac/hmac.h new file mode 100644 index 0000000..0b291fc --- /dev/null +++ b/firmware/lib/hmac/hmac.h @@ -0,0 +1,16 @@ +// firmware/src/hmac.h +#pragma once +#include + +#ifdef NATIVE_TEST +#include +using HString = std::string; +#else +#include +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); diff --git a/firmware/platformio.ini b/firmware/platformio.ini index 8ac262b..fe3cbed 100644 --- a/firmware/platformio.ini +++ b/firmware/platformio.ini @@ -26,3 +26,5 @@ test_framework = unity build_flags = -std=c++17 -DNATIVE_TEST +lib_deps = + kochcodes/mbedtls@^3.6.2 diff --git a/firmware/test/test_native/test_hmac.cpp b/firmware/test/test_native/test_hmac.cpp new file mode 100644 index 0000000..9173f30 --- /dev/null +++ b/firmware/test/test_native/test_hmac.cpp @@ -0,0 +1,34 @@ +// firmware/test/test_native/test_hmac.cpp +#include +#include "hmac.h" + +void setUp(void) {} +void tearDown(void) {} + +void test_hmac_known_vector() { + HString secret = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"; + HString device = "dc-0042"; + HString body = "{\"location_id\":\"retailer-123\",\"records\":[]}"; + uint32_t ts = 1712000000; + + HString result = hmac_sign(secret, device, ts, body); + + TEST_ASSERT_EQUAL_STRING("90f5fa5fdbf7f95e7475791bf5bb90cdef7f16534d9a7d263fc588305bad0525", 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); + TEST_ASSERT_NOT_EQUAL(0, sig1.compare(sig2)); +} + +int main() { + UNITY_BEGIN(); + RUN_TEST(test_hmac_known_vector); + RUN_TEST(test_hmac_different_timestamp_gives_different_sig); + return UNITY_END(); +}