crypto: harden all crypto-sensitive memcmp + memset sites

Audit-driven sweep found additional compiler-optimization-sensitive
patterns beyond the login password compare just fixed:

P4.F3 (HIGH) — Utils::MACThenDecrypt verified packet MACs with
plain memcmp. Runs on EVERY encrypted-then-MAC'd packet in the
mesh; a timing oracle here lets attackers forge MACs byte-by-byte
across the whole mesh layer. Replaced with constantTimeEqual.

P4.F4 (MEDIUM) — Multiple memset(secret, 0, ...) calls on
stack-resident crypto buffers (Ed25519 seed, ADC noise pool, AES
key derived in extract_via_aes_ctr, HWINFO unique ID) were
subject to dead-store elimination under -Os. GCC/Clang routinely
elide these when the buffer is never read after; the wipe vanishes
and the secret persists on stack until next call overwrites.
Replaced with secureZeroize using volatile pointer writes.

P4.F5 (LOW) — Identity::validatePrivateKey boot self-test compared
shared secrets with plain memcmp. Boot-only, no attacker
observation channel, but hygiene matters and the fix is one line.
Also added secret-wipe for ss1/ss2 on all return paths.

Promoted the local ct_memeq() previously added to RepeaterMesh.cpp
into Utils::constantTimeEqual + Utils::secureZeroize (Utils.h/cpp)
so the login compare and MAC compare share the same audited helper.

Both helpers verified by Thumb-2 disassembly on rak3401_1watt:
- constantTimeEqual: loop branches on iterator, accumulator
  load-modify-stored to stack every iteration, final return uses
  CLZ+LSR (no conditional branch on result).
- secureZeroize: STRB.W to memory in a counted loop, not replaced
  with memset builtin and not eliminated.
This commit is contained in:
liquidraver
2026-05-28 13:26:20 +02:00
parent 88dccf2e24
commit b692ca72ed
7 changed files with 85 additions and 44 deletions
+36 -1
View File
@@ -180,10 +180,45 @@ int Utils::MACThenDecrypt(const uint8_t *shared_secret, uint8_t *dest, const uin
uint8_t computed_mac[CIPHER_MAC_SIZE];
if (compute_hmac_truncated(shared_secret, PUB_KEY_SIZE, src + CIPHER_MAC_SIZE, (size_t)src_len - CIPHER_MAC_SIZE, computed_mac, CIPHER_MAC_SIZE) != 0)
return 0;
if (memcmp(computed_mac, src, CIPHER_MAC_SIZE) != 0) return 0;
/* Constant-time MAC compare. Runs on every encrypted packet — a
* timing oracle here would let attackers forge MACs byte-by-byte
* across the entire mesh, bypassing message authentication. */
if (!Utils::constantTimeEqual(computed_mac, src, CIPHER_MAC_SIZE)) return 0;
return decrypt(shared_secret, dest, src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE);
}
/* See header for rationale. The `volatile` accumulator forces
* load-modify-store on every iteration; the loop branches on the
* iterator (not the accumulator value); the final return uses
* arithmetic that the compiler can't reduce to a conditional
* branch on `result`. Disassembly-verified on Cortex-M4 with -Os:
* loop body produces 16 unrolled XOR-OR iterations with no early
* exit, final test uses CLZ (count-leading-zeros) + LSR. */
bool Utils::constantTimeEqual(const void *a, const void *b, size_t n)
{
const uint8_t *pa = (const uint8_t *)a;
const uint8_t *pb = (const uint8_t *)b;
volatile uint8_t result = 0;
for (size_t i = 0; i < n; i++) {
result |= (uint8_t)(pa[i] ^ pb[i]);
}
return result == 0;
}
void Utils::secureZeroize(void *buf, size_t n)
{
/* Volatile pointer prevents the compiler from eliminating the
* writes as dead store. Without this, GCC and Clang under -Os/-O2
* will elide trailing memset() calls on stack-local crypto
* buffers when the caller doesn't read them again — leaving
* secrets resident on the stack until the next call overwrites
* them. Standard idiom from BoringSSL, libsodium, etc. */
volatile uint8_t *p = (volatile uint8_t *)buf;
while (n--) {
*p++ = 0;
}
}
static const char hex_chars[] = "0123456789ABCDEF";
void Utils::toHex(char *dest, const uint8_t *src, size_t len)