Fix unit test fixtures and fit constrained firmware builds

This commit is contained in:
mikecarper committed 2026-10-21 14:09:51 -07:00
1 parent 329ec3848c
commit 6c3bf00691
23 files changed
+475 -125

No files matched your search

+3
View File
@@ -274,6 +274,9 @@ jobs:
mkdir -p .pio/libdeps/room-mail-test
pio pkg install --global --library 'rweather/Crypto@0.4.0' --storage-dir .pio/libdeps/room-mail-test
- name: Verify shared SHA-512 signing against the original backend and RFC8032
run: python3 -B test/test_ed25519_shared_sha512.py -v
- name: Verify ArduinoJson pin
run: |
python3 -B scripts/check_arduinojson_pin.py --self-test
+5 -6
View File
@@ -3531,8 +3531,8 @@ void MyMesh::onControlDataRecv(mesh::Packet* packet) {
const auto& secondary = profiles->secondary;
data[length - 1] = 1 | (static_cast<uint8_t>(secondary.mode) << 1)
| (profiles->secondary_temporary ? 0x08 : 0);
const uint32_t frequency = static_cast<uint32_t>(lroundf(secondary.params.freq * 1000.0f));
const uint32_t bandwidth = static_cast<uint32_t>(lroundf(secondary.params.bw * 1000.0f));
const uint32_t frequency = static_cast<uint32_t>(secondary.params.freq * 1000.0f + 0.5f);
const uint32_t bandwidth = static_cast<uint32_t>(secondary.params.bw * 1000.0f + 0.5f);
for (unsigned shift = 0; shift < 32; shift += 8) data[length++] = frequency >> shift;
for (unsigned shift = 0; shift < 32; shift += 8) data[length++] = bandwidth >> shift;
data[length++] = secondary.params.sf;
@@ -5503,12 +5503,11 @@ bool MyMesh::handleGuestCommand(const char* command, char* reply, size_t capacit
} else if (get) {
snprintf(reply, capacity, "> %s%s", guest_login.enabled() ? "on" : "off",
guest_login.healthy() ? "" : " (settings unavailable)");
} else if (!guest_login.setEnabled(strcmp(value, "on") == 0)) {
if (!guest_login.enabled()) acl.removeGuestClients();
snprintf(reply, capacity, "Err - guest setting save unconfirmed; reboot may differ");
} else {
const bool saved = guest_login.setEnabled(strcmp(value, "on") == 0);
if (!guest_login.enabled()) acl.removeGuestClients();
snprintf(reply, capacity, "OK - guest %s", guest_login.enabled() ? "on" : "off");
if (saved) snprintf(reply, capacity, "OK - guest %s", guest_login.enabled() ? "on" : "off");
else snprintf(reply, capacity, "Err - guest setting save unconfirmed; reboot may differ");
}
return true;
}
+1 -1
View File
@@ -1,7 +1,7 @@
#include "ed_25519.h"
#include "ge.h"
#include "sc.h"
#include "sha512.h"
#include "ed_sha512.h"
/* see http://crypto.stackexchange.com/a/6215/4697 */
+41
View File
@@ -0,0 +1,41 @@
#ifndef ED25519_SHA512_H
#define ED25519_SHA512_H
#include <stddef.h>
#include "fixedint.h"
#ifndef ED25519_USE_CRYPTO_SHA512
#define ED25519_USE_CRYPTO_SHA512 0
#endif
#if ED25519_USE_CRYPTO_SHA512
/* C-owned, aligned storage for the optional C++ SHA512 implementation. */
typedef union sha512_context_ {
uint64_t alignment;
unsigned char opaque[256];
} sha512_context;
#else
/* state */
typedef struct sha512_context_ {
uint64_t length, state[8];
size_t curlen;
unsigned char buf[128];
} sha512_context;
#endif
#ifdef __cplusplus
extern "C" {
#endif
/* A completed context must be initialized again before reuse. */
int sha512_init(sha512_context * md);
int sha512_final(sha512_context * md, unsigned char *out);
int sha512_update(sha512_context * md, const unsigned char *in, size_t inlen);
int sha512(const unsigned char *message, size_t message_len, unsigned char *out);
#ifdef __cplusplus
}
#endif
#endif
+48
View File
@@ -0,0 +1,48 @@
#include "ed_sha512.h"
#if ED25519_USE_CRYPTO_SHA512
#include <SHA512.h>
#include <new>
// Share the SHA-512 already used by Crypto's Ed25519 verifier. Contexts remain
// caller-owned; hashing and signing allocate no heap and share no mutable state.
static_assert(sizeof(SHA512) <= sizeof(sha512_context), "SHA512 context capacity");
static_assert(alignof(SHA512) <= alignof(sha512_context), "SHA512 context alignment");
namespace {
SHA512* implementation(sha512_context* context) {
return reinterpret_cast<SHA512*>(context->opaque);
}
}
extern "C" int sha512_init(sha512_context* context) {
if (!context) return 1;
new (context->opaque) SHA512();
return 0;
}
extern "C" int sha512_update(sha512_context* context, const unsigned char* input,
size_t length) {
if (!context || !input) return 1;
implementation(context)->SHA512::update(input, length);
return 0;
}
extern "C" int sha512_final(sha512_context* context, unsigned char* output) {
if (!context || !output) return 1;
SHA512* hash = implementation(context);
hash->SHA512::finalize(output, SHA512::HASH_SIZE);
hash->SHA512::~SHA512();
return 0;
}
extern "C" int sha512(const unsigned char* message, size_t length,
unsigned char* output) {
if (!message || !output) return 1;
sha512_context context;
sha512_init(&context);
sha512_update(&context, message, length);
sha512_final(&context, output);
return 0;
}
#endif
+1 -1
View File
@@ -1,5 +1,5 @@
#include "ed_25519.h"
#include "sha512.h"
#include "ed_sha512.h"
#include "ge.h"
+5 -1
View File
@@ -10,7 +10,9 @@
*/
#include "fixedint.h"
#include "sha512.h"
#include "ed_sha512.h"
#if !ED25519_USE_CRYPTO_SHA512
/* the K array */
static const uint64_t K[80] = {
@@ -303,3 +305,5 @@ int sha512(const unsigned char *message, size_t message_len, unsigned char *out)
if ((ret = sha512_final(&ctx, out))) return ret;
return 0;
}
#endif
-21
View File
@@ -1,21 +0,0 @@
#ifndef SHA512_H
#define SHA512_H
#include <stddef.h>
#include "fixedint.h"
/* state */
typedef struct sha512_context_ {
uint64_t length, state[8];
size_t curlen;
unsigned char buf[128];
} sha512_context;
int sha512_init(sha512_context * md);
int sha512_final(sha512_context * md, unsigned char *out);
int sha512_update(sha512_context * md, const unsigned char *in, size_t inlen);
int sha512(const unsigned char *message, size_t message_len, unsigned char *out);
#endif
+1 -1
View File
@@ -1,5 +1,5 @@
#include "ed_25519.h"
#include "sha512.h"
#include "ed_sha512.h"
#include "ge.h"
#include "sc.h"
+1 -1
View File
@@ -1,5 +1,5 @@
#include "ed_25519.h"
#include "sha512.h"
#include "ed_sha512.h"
#include "ge.h"
#include "sc.h"
+2
View File
@@ -207,6 +207,7 @@ build_flags = ${arduino_base.build_flags}
; PendSV reaches this FreeRTOS function from assembly, outside the LTO graph.
-Wl,--undefined=vTaskSwitchContext
-D NRF52_PLATFORM
-D ED25519_USE_CRYPTO_SHA512=1 ; share the SHA-512 used by signature verification
-D LFS_NO_ASSERT=1
-D EXTRAFS=1
-D MESH_NRF52_LOOP_STACK_WORDS=2048
@@ -373,6 +374,7 @@ extra_scripts = post:arch/stm32/build_hex.py
post:scripts/check_firmware_ram.py
build_flags = ${arduino_base.build_flags}
-D STM32_PLATFORM
-D ED25519_USE_CRYPTO_SHA512=1 ; avoid linking two SHA-512 implementations
-D ED25519_COMPACT_BASE=1
-I src/helpers/stm32
-I $PROJECT_PACKAGES_DIR/framework-arduinoststm32/libraries/SubGhz/src
+4 -3
View File
@@ -248,14 +248,15 @@ class RadioProfiles {
}
uint32_t listenUs(uint8_t profile, uint16_t slow_preamble = 0) const {
const uint8_t slow = slowerProfile();
const double minimum = minimumListenUs(params(profile), enabled() && profile == slow);
if (!enabled() || profile == slow) return (uint32_t)ceil(minimum);
const uint32_t minimum = minimumListenUs(params(profile), enabled() && profile == slow);
if (!enabled() || profile == slow) return minimum;
if (!slow_preamble) slow_preamble = preamble(slow, 32);
const double available = double(slow_preamble) / 2.0 * symbolUs(params(slow))
- minimumListenUs(params(slow), true) - 2.0 * switchBudgetUs() - LoopBudgetUs;
// Rejected previews must not cause an out-of-range floating-to-int cast.
// In the positive branch, truncation already rounds down to whole us.
return available > UINT32_MAX ? UINT32_MAX
: available > minimum ? (uint32_t)floor(available) : (uint32_t)minimum;
: available > minimum ? (uint32_t)available : minimum;
}
ChirpTiming chirpTiming() const {
if (!enabled()) return {};
+22
View File
@@ -0,0 +1,22 @@
#pragma once
namespace mesh {
namespace cli {
// Empty arguments select page 1. Zero signals invalid decimal page syntax.
inline unsigned parseCliPage(const char* arg) {
while (*arg == ' ' || *arg == '\t') ++arg;
unsigned page = 1;
if (*arg >= '0' && *arg <= '9') {
page = 0;
do {
page = page * 10 + unsigned(*arg++ - '0');
if (page > 65535) return 0;
} while (*arg >= '0' && *arg <= '9');
}
while (*arg == ' ' || *arg == '\t' || *arg == '\r' || *arg == '\n') ++arg;
return *arg ? 0 : page;
}
} // namespace cli
} // namespace mesh
+3 -11
View File
@@ -1,6 +1,7 @@
#pragma once
#include "ClientACL.h"
#include "CLIPage.h"
#include <Utils.h>
#include <stdio.h>
#include <string.h>
@@ -21,17 +22,8 @@ inline bool handleACLGet(const ClientACL& acl, const char* command,
if (local && *arg == 0) return false;
if (reply == nullptr || capacity == 0) return true;
while (*arg == ' ' || *arg == '\t') ++arg;
unsigned page = 1;
if (*arg >= '0' && *arg <= '9') {
page = 0;
do {
page = page * 10 + unsigned(*arg++ - '0');
if (page > 65535) break;
} while (*arg >= '0' && *arg <= '9');
}
while (*arg == ' ' || *arg == '\t' || *arg == '\r' || *arg == '\n') ++arg;
if (*arg || page == 0 || page > 65535) {
const unsigned page = parseCliPage(arg);
if (page == 0) {
snprintf(reply, capacity, "Err - usage: get acl [page]");
return true;
}
+10 -24
View File
@@ -1,6 +1,7 @@
#pragma once
#include "ClientACL.h"
#include "CLIPage.h"
#include <Utils.h>
#include <stdio.h>
#include <string.h>
@@ -29,24 +30,17 @@ inline bool handleGuestsGet(const ClientACL& acl, const char* command,
if (*arg && *arg != ' ' && *arg != '\t' && *arg != '\r' && *arg != '\n') return false;
if (reply == nullptr || capacity == 0) return true;
while (*arg == ' ' || *arg == '\t') ++arg;
unsigned page = 1;
if (*arg >= '0' && *arg <= '9') {
page = 0;
do {
page = page * 10 + unsigned(*arg++ - '0');
if (page > 65535) break;
} while (*arg >= '0' && *arg <= '9');
}
while (*arg == ' ' || *arg == '\t' || *arg == '\r' || *arg == '\n') ++arg;
if (*arg || page == 0 || page > 65535) {
const unsigned page = parseCliPage(arg);
if (page == 0) {
snprintf(reply, capacity, "Err - usage: get guests [page]");
return true;
}
unsigned total = 0;
const ClientInfo* selected = nullptr;
for (int i = 0; i < acl.getNumClients(); ++i) {
if (isLiveGuest(*acl.getClientByIdx(i))) ++total;
const auto* client = acl.getClientByIdx(i);
if (isLiveGuest(*client) && ++total == page) selected = client;
}
if (total == 0 && page == 1) {
snprintf(reply, capacity, "Guests: empty");
@@ -58,24 +52,16 @@ inline bool handleGuestsGet(const ClientACL& acl, const char* command,
return true;
}
unsigned index = 0;
for (int i = 0; i < acl.getNumClients(); ++i) {
const auto* client = acl.getClientByIdx(i);
if (!isLiveGuest(*client) || ++index != page) continue;
if (selected) {
char key[PUB_KEY_SIZE * 2 + 1];
Utils::toHex(key, client->id.pub_key, PUB_KEY_SIZE);
const int needed = snprintf(nullptr, 0, "Guests %u/%u (%u)\n%s last=%lu",
Utils::toHex(key, selected->id.pub_key, PUB_KEY_SIZE);
const int needed = snprintf(reply, capacity, "Guests %u/%u (%u)\n%s last=%lu",
page, pages, total, key,
static_cast<unsigned long>(client->last_activity));
static_cast<unsigned long>(selected->last_activity));
// A small caller buffer must never turn a complete key into a prefix.
if (needed < 0 || size_t(needed) >= capacity) {
snprintf(reply, capacity, "Err - guest reply too small");
} else {
snprintf(reply, capacity, "Guests %u/%u (%u)\n%s last=%lu",
page, pages, total, key,
static_cast<unsigned long>(client->last_activity));
}
return true;
}
return true;
}
+13 -25
View File
@@ -1,5 +1,6 @@
#pragma once
#include "CLIPage.h"
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
@@ -35,11 +36,11 @@ class LoginHistory {
while (index < _size && memcmp(_entries[index].key, key, KEY_SIZE) != 0) ++index;
Entry latest = {};
memcpy(latest.key, key, KEY_SIZE);
if (index < _size) {
latest = _entries[index];
latest.attempts = _entries[index].attempts;
if (latest.attempts != UINT32_MAX) ++latest.attempts;
} else {
memcpy(latest.key, key, KEY_SIZE);
latest.attempts = 1;
if (_size < CAPACITY) ++_size;
index = _size - 1;
@@ -82,17 +83,8 @@ inline bool handleLoginsGet(const LoginHistory& history, const char* command,
if (*arg && *arg != ' ' && *arg != '\t' && *arg != '\r' && *arg != '\n') return false;
if (reply == nullptr || capacity == 0) return true;
while (*arg == ' ' || *arg == '\t') ++arg;
unsigned page = 1;
if (*arg >= '0' && *arg <= '9') {
page = 0;
do {
page = page * 10 + unsigned(*arg++ - '0');
if (page > 65535) break;
} while (*arg >= '0' && *arg <= '9');
}
while (*arg == ' ' || *arg == '\t' || *arg == '\r' || *arg == '\n') ++arg;
if (*arg || page == 0 || page > 65535) {
const unsigned page = parseCliPage(arg);
if (page == 0) {
snprintf(reply, capacity, "Err - usage: get logins [page]");
return true;
}
@@ -108,9 +100,10 @@ inline bool handleLoginsGet(const LoginHistory& history, const char* command,
return true;
}
char rows[2][80] = {};
unsigned count = 0;
for (unsigned index = (page - 1) * 2; index < total && count < 2; ++index) {
int used = snprintf(reply, capacity, "Logins %u/%u (%u)", page, pages, total);
const unsigned end = page * 2 < total ? page * 2 : total;
for (unsigned index = (page - 1) * 2; index < end
&& used >= 0 && size_t(used) < capacity; ++index) {
const auto& entry = *history.get(index);
char key[13];
static const char hex[] = "0123456789ABCDEF";
@@ -119,20 +112,15 @@ inline bool handleLoginsGet(const LoginHistory& history, const char* command,
key[i * 2 + 1] = hex[entry.key[i] & 0x0F];
}
key[12] = 0;
snprintf(rows[count], sizeof(rows[count]), "%s n=%lu last=%lu %s", key,
const int written = snprintf(reply + used, capacity - size_t(used), "\n%s n=%lu last=%lu %s", key,
static_cast<unsigned long>(entry.attempts),
static_cast<unsigned long>(entry.timestamp), loginResultName(entry.result));
++count;
if (written < 0) { used = -1; break; }
used += written;
}
const char* separator = count == 2 ? "\n" : "";
const int needed = snprintf(nullptr, 0, "Logins %u/%u (%u)\n%s%s%s",
page, pages, total, rows[0], separator, rows[1]);
// Never accidentally expose a partly formatted public-key prefix.
if (needed < 0 || size_t(needed) >= capacity) {
if (used < 0 || size_t(used) >= capacity) {
snprintf(reply, capacity, "Err - login reply too small");
} else {
snprintf(reply, capacity, "Logins %u/%u (%u)\n%s%s%s",
page, pages, total, rows[0], separator, rows[1]);
}
return true;
}
+29 -17
View File
@@ -726,10 +726,11 @@ void RadioProfileCLI::appendChirpWarning(char* reply, size_t capacity, const Rad
const bool short_a = preview.primary.preamble && preview.primary.preamble < timing.preamble[0];
const bool short_b = preview.secondary.params.preamble && preview.secondary.params.preamble < timing.preamble[1];
const bool a = timing.preamble[0] > 32 || short_a, b = timing.preamble[1] > 32 || short_b;
if (a && b) appendReply(reply, capacity, "; WARN recommended preamble: radio=%.0f,radio2=%.0f",
timing.preamble[0], timing.preamble[1]);
else if (a || b) appendReply(reply, capacity, "; WARN recommended preamble: %s=%.0f",
a ? "radio" : "radio2", timing.preamble[a ? 0 : 1]);
if (a || b) {
appendReply(reply, capacity, "; WARN recommended preamble: %s=%.0f",
a ? "radio" : "radio2", timing.preamble[a ? 0 : 1]);
if (a && b) appendReply(reply, capacity, ",radio2=%.0f", timing.preamble[1]);
}
if (short_a || short_b)
appendReply(reply, capacity, "; short override: %s",
short_a && short_b ? "radio,radio2" : short_a ? "radio" : "radio2");
@@ -783,16 +784,26 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
memcpy(key, text, n); key[n] = 0;
const char* args = text + n;
while (*args == ' ' || *args == '\t') ++args;
const bool base = !strcmp(key, "radio2");
const bool temporary = !strcmp(key, "tempradio2");
const bool scheduled = !strcmp(key, "radioat2") || !strcmp(key, "tempradioat2");
const bool scheduled_temp = !strcmp(key, "tempradioat2");
const bool crossing = !strcmp(key, "radio2.cross");
const bool replies = infrastructure_replies_ && !strcmp(key, "tx.reply");
const bool status = !strcmp(key, "radio2.status");
const bool scan = !strcmp(key, "radio2.scan");
const bool timing = !strcmp(key, "radio2.timing") || !strcmp(key, "radio.timing");
if (!base && !temporary && !scheduled && !crossing && !replies && !status && !scan && !timing) return false;
enum { Base, Temporary, Scheduled, ScheduledTemporary, Crossing, Replies,
Status, Scan, Timing, TimingAlias, KeyCount };
static const char keys[] = "radio2\0tempradio2\0radioat2\0tempradioat2\0radio2.cross\0"
"tx.reply\0radio2.status\0radio2.scan\0radio2.timing\0radio.timing";
const char* candidate = keys;
unsigned kind = 0;
while (kind < KeyCount && strcmp(key, candidate)) {
candidate += strlen(candidate) + 1;
++kind;
}
if (kind == KeyCount || (kind == Replies && !infrastructure_replies_)) return false;
const bool base = kind == Base;
const bool temporary = kind == Temporary;
const bool scheduled = kind == Scheduled || kind == ScheduledTemporary;
const bool scheduled_temp = kind == ScheduledTemporary;
const bool crossing = kind == Crossing;
const bool replies = kind == Replies;
const bool status = kind == Status;
const bool scan = kind == Scan;
const bool timing = kind == Timing || kind == TimingAlias;
if (!radio_ || !radio_->profiles()) { cli::formatError(reply, capacity, "radio profiles unsupported"); return true; }
if (command == text && *args && verb == Get && (temporary || base)) verb = Set;
if (remote_origin && !remote_command_
@@ -802,6 +813,10 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
if (hasReplyMutation() && verb != Get) {
cli::formatError(reply, capacity, "radio acknowledgement pending"); return true;
}
if ((timing || scan || status) && (verb != Get || *args)) {
snprintf(reply, capacity, "Error: %s is read-only", timing ? "radio timing" : key);
return true;
}
if (replies) {
const auto* p = radio_->profiles();
if (verb == Get && !*args) {
@@ -836,7 +851,6 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
return true;
}
if (timing) {
if (verb != Get || *args) { cli::formatError(reply, capacity, "radio timing is read-only"); return true; }
const auto& p = *radio_->profiles();
const auto t = p.chirpTiming();
if (!p.enabled()) snprintf(reply, capacity, "> off");
@@ -872,7 +886,6 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
return true;
}
if (scan) {
if (verb != Get || *args) { cli::formatError(reply, capacity, "radio2.scan is read-only"); return true; }
const auto& p = *radio_->profiles();
if (!p.enabled()) snprintf(reply, capacity, "> off");
else {
@@ -886,7 +899,6 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
return true;
}
if (status) {
if (verb != Get || *args) { cli::formatError(reply, capacity, "radio2.status is read-only"); return true; }
if (hasReplyMutation()) {
snprintf(reply, capacity, "> radio2 change pending %s; previous channel retained",
remote_delivered_ ? "storage commit (retrying)" : "reply transmission");
+2 -1
View File
@@ -128,7 +128,8 @@ static void rejects_bad_pages() {
acl.load(&fs, self);
add(acl, 0, 3);
for (const char* command : {
"get acl 0", "get acl -1", "get acl +1", "get acl 1x", "get acl 1 2",
"get acl 0", "get acl 0000000000000000000000000000000000000000",
"get acl -1", "get acl +1", "get acl 1x", "get acl 1 2", "get acl\n1",
"get acl 1.0", "get acl page 1", "get acl 65536", "get acl 4294967296",
"get acl 9999999999999999999999999999999999", "get acl 1\nsetperm key 3"}) {
CHECK(query(acl, command) == "Err - usage: get acl [page]");
+2 -1
View File
@@ -119,7 +119,8 @@ static void rejects_bad_pages() {
acl.load(&fs, self);
add(acl, 0, PERM_ACL_GUEST);
for (const char* command : {
"get guests 0", "get guests -1", "get guests +1", "get guests 1x",
"get guests 0", "get guests 0000000000000000000000000000000000000000",
"get guests -1", "get guests +1", "get guests 1x", "get guests\n1",
"get guests 1 2", "get guests 1.0", "get guests page 1",
"get guests 65536", "get guests 4294967296",
"get guests 99999999999999999999999999", "get guests 1\nset guest off"}) {
+2 -1
View File
@@ -144,7 +144,8 @@ static void strict_page_and_command_parsing() {
LoginHistory history;
record(history, 0, 0);
const char* invalid[] = {
"get logins 0", "get logins -1", "get logins +1", "get logins 1x",
"get logins 0", "get logins 0000000000000000000000000000000000000000",
"get logins -1", "get logins +1", "get logins 1x", "get logins\n1",
"get logins 1 2", "get logins 1.0", "get logins page 1", "get logins 65536",
"get logins 4294967296", "get logins 9999999999999999999999999999",
"get logins 1\nset guest on",
+13
View File
@@ -24,6 +24,19 @@ static void checkSafePreamble(unsigned sf, float bw, uint16_t symbols) {
int main() {
Profiles fresh;
assert(fresh.cross==mesh::RadioCrossMode::Off && !fresh.canCross());
// Listen durations round upward for the minimum, downward for a larger
// fast visit, and saturate rejected previews before converting to integers.
Profiles listen_bounds;
listen_bounds.primary=params(5,1000);
assert(listen_bounds.listenUs(0)==148);
listen_bounds.secondary.params=params(5,500);
listen_bounds.secondary.mode=mesh::RadioProfileMode::Rx;
assert(listen_bounds.listenUs(0,1)==148);
assert(listen_bounds.listenUs(0,32)==729);
listen_bounds.secondary.params.bw=7.8f;
assert(listen_bounds.listenUs(0,32)==46769);
listen_bounds.secondary.params.bw=1e-30f;
assert(listen_bounds.listenUs(0,1)==UINT32_MAX);
// Sharing the read-only validation table must preserve every supported
// value and the strict float tolerance on both sides of its boundary.
for (float bw : {7.8f, 10.4f, 15.6f, 20.8f, 31.25f, 41.7f,
+239
View File
@@ -0,0 +1,239 @@
"""Exercise both Ed25519 signing hash backends with the real Crypto verifier."""
from pathlib import Path
import hashlib
import os
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
ED25519 = ROOT / "lib/ed25519"
LENGTHS = (0, 1, 63, 64, 111, 112, 127, 128, 129, 239, 240, 255,
256, 257, 1024, 4097, 16385)
HARNESS = r'''
#include "ed_sha512.h"
#include "ed_25519.h"
#include <Ed25519.h>
#include <array>
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
#if defined(_WIN32)
#include <RNG.h>
// PE linkers resolve the library's unused key-generation references before
// discarding them. Verification and seeded signing must never call this API.
RNGClass::RNGClass() {}
RNGClass::~RNGClass() {}
void RNGClass::rand(uint8_t*, size_t) { std::abort(); }
RNGClass RNG;
#endif
static void printHex(const unsigned char* bytes, size_t length) {
for (size_t i = 0; i < length; ++i) printf("%02x", bytes[i]);
putchar('\n');
}
static const unsigned char* bytes(const std::vector<unsigned char>& data) {
static const unsigned char empty = 0;
return data.empty() ? &empty : data.data();
}
static std::vector<unsigned char> message(size_t length, unsigned salt) {
std::vector<unsigned char> result(length);
for (size_t i = 0; i < length; ++i) result[i] = (i * 73 + salt) & 255;
return result;
}
static void decode(unsigned char* output, size_t length, const char* hex) {
assert(strlen(hex) == length * 2);
for (size_t i = 0; i < length; ++i) {
unsigned value;
assert(sscanf(hex + i * 2, "%2x", &value) == 1);
output[i] = value;
}
}
static void verifySignature(unsigned char* signature, const unsigned char* key,
const unsigned char* data, size_t length) {
// Retain the production C++ verification path; do not use the bundled
// C verifier which Identity deliberately excludes.
assert(Ed25519::verify(signature, key, data, length));
for (size_t index : {size_t(0), size_t(17), size_t(32), size_t(63)}) {
signature[index] ^= 1;
assert(!Ed25519::verify(signature, key, data, length));
signature[index] ^= 1;
}
std::vector<unsigned char> changed(data, data + length);
if (changed.empty()) changed.push_back(1); else changed[0] ^= 1;
assert(!Ed25519::verify(signature, key, bytes(changed), changed.size()));
unsigned char wrong_key[32];
memcpy(wrong_key, key, 32); wrong_key[0] ^= 1;
assert(!Ed25519::verify(signature, wrong_key, data, length));
}
int main() {
const size_t lengths[] = {@LENGTHS@};
const size_t chunks[] = {1, 7, 63, 64, 111, 112, 127, 128, 129, 257, 1024};
unsigned char result[64];
assert(sha512_init(nullptr) == 1);
assert(sha512_update(nullptr, result, 0) == 1);
assert(sha512_final(nullptr, result) == 1);
assert(sha512(nullptr, 0, result) == 1);
assert(sha512(result, 0, nullptr) == 1);
for (size_t length : lengths) {
const auto first = message(length, 19), second = message(length + 137, 91);
assert(sha512(bytes(first), first.size(), result) == 0);
printHex(result, sizeof(result));
sha512_context contexts[2];
size_t positions[2] = {}, turn = 0;
for (auto& context : contexts) {
assert(sha512_init(&context) == 0);
assert(sha512_update(&context, nullptr, 0) == 1);
assert(sha512_final(&context, nullptr) == 1);
assert(sha512_update(&context, result, 0) == 0);
}
while (positions[0] < first.size() || positions[1] < second.size()) {
for (unsigned i = 0; i < 2; ++i) {
const auto& data = i ? second : first;
size_t count = chunks[turn++ % (sizeof(chunks) / sizeof(chunks[0]))];
if (count > data.size() - positions[i]) count = data.size() - positions[i];
assert(sha512_update(&contexts[i], bytes(data) + positions[i], count) == 0);
positions[i] += count;
}
}
for (auto& context : contexts) {
assert(sha512_final(&context, result) == 0);
printHex(result, sizeof(result));
// Restart a completed context and reset an unfinished one. Both
// must discard the old message without touching another context.
assert(sha512_init(&context) == 0);
assert(sha512_update(&context, bytes(second), second.size()) == 0);
assert(sha512_init(&context) == 0);
assert(sha512_update(&context, bytes(first), first.size()) == 0);
assert(sha512_final(&context, result) == 0);
unsigned char expected[64];
assert(sha512(bytes(first), first.size(), expected) == 0);
assert(!memcmp(result, expected, 64));
}
}
struct Vector { const char* seed; const char* key; const char* data; const char* signature; };
// RFC 8032 section 7.1, tests 1-3.
// https://www.rfc-editor.org/rfc/rfc8032.html#section-7.1
const Vector vectors[] = {
{"9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", "",
"e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555f"
"b8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"},
{"4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb",
"3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c", "72",
"92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085"
"ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00"},
{"c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7",
"fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025", "af82",
"6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac"
"18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a"}
};
for (const auto& vector : vectors) {
unsigned char seed[32], key[32], expected_key[32], secret[64], signature[64], expected[64];
std::vector<unsigned char> data(strlen(vector.data) / 2);
decode(seed, 32, vector.seed); decode(expected_key, 32, vector.key);
decode(expected, 64, vector.signature);
if (!data.empty()) decode(data.data(), data.size(), vector.data);
ed25519_create_keypair(key, secret, seed);
assert(!memcmp(key, expected_key, 32));
ed25519_sign(signature, bytes(data), data.size(), key, secret);
assert(!memcmp(signature, expected, 64));
verifySignature(signature, key, bytes(data), data.size());
}
// Cross-backend byte-for-byte comparison covers long, irregular messages
// and existing expanded private-key storage, beyond the short RFC cases.
for (size_t length : lengths) {
unsigned char seed[32], key[32], secret[64], signature[64];
const auto data = message(length, 37);
for (unsigned i = 0; i < 32; ++i) seed[i] = (length + i * 17) & 255;
ed25519_create_keypair(key, secret, seed);
ed25519_sign(signature, bytes(data), data.size(), key, secret);
verifySignature(signature, key, bytes(data), data.size());
printHex(key, 32); printHex(secret, 64); printHex(signature, 64);
}
puts("Shared SHA512 and RFC8032 checks passed");
}
'''
class SharedSHA512Test(unittest.TestCase):
def test_c_api_hashes_and_signatures_match_original_and_rfc8032(self):
compiler = shutil.which("gcc")
cpp = shutil.which("g++")
self.assertIsNotNone(compiler, "C compiler required")
self.assertIsNotNone(cpp, "C++ compiler required")
override = os.environ.get("MESHCORE_CRYPTO_DIR")
candidates = [Path(override)] if override else sorted((ROOT / ".pio/libdeps").glob("*/Crypto"))
names = ("SHA512.cpp", "Hash.cpp", "Crypto.cpp", "Ed25519.cpp", "Curve25519.cpp",
"BigNumberUtil.cpp")
crypto = next((path for path in candidates if all((path / name).is_file() for name in names)), None)
self.assertIsNotNone(crypto, "Install rweather/Crypto or set MESHCORE_CRYPTO_DIR (no crypto mocks)")
flags = ["-O1", "-g", "-Wall", "-Wextra", "-Werror", "-DHOST_BUILD=1",
"-DED25519_NO_SEED=1", "-DED25519_SMALL_BASE=1",
"-DED25519_COMPACT_SHA512=1", "-DED25519_COMPACT_SCALAR=1",
"-DED25519_COMPACT_FIELD=1", "-ffunction-sections", "-fdata-sections",
"-I" + str(ED25519), "-I" + str(crypto)]
if os.name != "nt":
flags += ["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
with tempfile.TemporaryDirectory(prefix="ed25519-shared-sha512-") as directory:
work = Path(directory)
if os.name == "nt":
# Crypto's HOST_BUILD expects the POSIX byte-order header.
# MinGW supplies the same conversions as compiler intrinsics.
endian = "#pragma once\n#include <stdint.h>\n"
endian += "static_assert(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__, \"host byte order\");\n"
for bits in (16, 32, 64):
for name in (f"htobe{bits}", f"be{bits}toh"):
endian += f"static inline uint{bits}_t {name}(uint{bits}_t n) {{ return __builtin_bswap{bits}(n); }}\n"
for name in (f"htole{bits}", f"le{bits}toh"):
endian += f"static inline uint{bits}_t {name}(uint{bits}_t n) {{ return n; }}\n"
(work / "endian.h").write_text(endian)
flags += ["-I" + str(work)]
harness = work / "harness.cpp"
source = HARNESS.replace("@LENGTHS@", ",".join(map(str, LENGTHS)))
harness.write_text(source)
outputs = []
for shared in (0, 1):
with self.subTest(shared=shared):
build_flags = flags + [f"-DED25519_USE_CRYPTO_SHA512={shared}"]
objects = []
for name in ("fe.c", "ge.c", "keypair.c", "sc.c", "sha512.c", "sign.c"):
obj = work / (name + f"-{shared}.o")
# Legacy ref10 carries shift negative signed values.
# Keep ASan and all other UBSan checks on those files;
# the new adapter and both hash implementations retain
# the complete sanitizer set.
legacy = ["-fno-sanitize=shift"] if os.name != "nt" and name in ("fe.c", "ge.c") else []
built = subprocess.run([compiler, "-std=c99", *build_flags, *legacy, "-c", str(ED25519 / name),
"-o", str(obj)], capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
objects.append(str(obj))
binary = work / f"shared-{shared}"
built = subprocess.run([cpp, "-std=c++11", *build_flags, str(harness),
str(ED25519 / "ed_sha512_crypto.cpp"),
*[str(crypto / name) for name in names], *objects,
"-Wl,--gc-sections", "-o", str(binary)],
capture_output=True, text=True, timeout=90)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
run = subprocess.run([str(binary)], capture_output=True, text=True, timeout=60)
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
lines = run.stdout.splitlines()
expected = []
for length in LENGTHS:
first = bytes((i * 73 + 19) & 255 for i in range(length))
second = bytes((i * 73 + 91) & 255 for i in range(length + 137))
expected += [hashlib.sha512(first).hexdigest()] * 2
expected += [hashlib.sha512(second).hexdigest()]
self.assertEqual(lines[:len(expected)], expected)
self.assertEqual(lines[-1], "Shared SHA512 and RFC8032 checks passed")
outputs.append(lines)
self.assertEqual(len(outputs), 2)
self.assertEqual(outputs[0], outputs[1])
if __name__ == "__main__":
unittest.main()
+28 -10
View File
@@ -401,22 +401,40 @@ RECENT_TEST = r'''
class CooperativeOutputTest(unittest.TestCase):
def role_methods(self, text, role):
dump_start = text.index("void MyMesh::dumpLogFile()")
signature = "void MyMesh::dumpLogFile()"
dump = body(text, signature)
dump_start = text.index(dump)
guard = "#if MESH_ENABLE_PACKET_FILE_LOGGING\n"
if text[:dump_start].endswith(guard):
dump_start -= len(guard)
dump_end = text.index("\n#if MESH_USB_CONSOLE_COOPERATIVE\nbool MyMesh::hasPendingSerialOutput", dump_start)
methods = text[dump_start:dump_end] + "\n" + "\n".join(body(text, signature) for signature in (
"bool MyMesh::hasPendingSerialOutput()", "void MyMesh::cancelPendingSerialOutput()",
active_end = dump_start + len(dump)
fallback = body(text[active_end:], signature)
fallback_start = text.index(fallback, active_end)
self.assertEqual(text[active_end:fallback_start].strip(), "#else")
fallback_end = fallback_start + len(fallback)
dump_end = text.index("\n#endif", fallback_end) + len("\n#endif")
self.assertEqual(text[fallback_end:dump_end].strip(), "#endif")
dump = text[dump_start - len(guard):dump_end]
methods = dump + "\n" + "\n".join(body(text, signature) for signature in (
"void MyMesh::printAclSerial()", "bool MyMesh::hasPendingSerialOutput()",
"void MyMesh::cancelPendingSerialOutput()",
"void MyMesh::servicePendingSerialOutput()"))
if role == "simple_repeater":
format_start = text.index("static void formatLocalSnrX4(")
format_end = text.index("\nvoid MyMesh::formatRecentRepeatersReply", format_start)
recent_start = text.index("void MyMesh::printRecentRepeatersSerial()")
recent_end = text.index("\nbool MyMesh::setRecentRepeater(", recent_start)
methods = text[format_start:format_end] + "\n" + text[recent_start:recent_end] + "\n" + methods
methods = "\n".join(body(text, signature) for signature in (
"static void formatLocalSnrX4(", "void MyMesh::printRecentRepeatersSerial()"
)) + "\n" + methods
return methods
def test_role_method_extraction_ignores_adjacent_definitions(self):
for role in ("simple_repeater", "simple_room_server"):
with self.subTest(role=role):
text = (ROOT / f"examples/{role}/MyMesh.cpp").read_text()
text = text.replace("void MyMesh::printAclSerial()",
"bool MyMesh::unrelatedCommand() { return true; }\n\n"
"void MyMesh::printAclSerial()", 1)
methods = self.role_methods(text, role)
self.assertNotIn("unrelatedCommand", methods)
self.assertNotIn("handleGuestCommand", methods)
def test_usb_acl_commands_route_to_the_cooperative_pump(self):
for role in ("simple_repeater", "simple_room_server"):
with self.subTest(role=role):