Skip to content

Harden XEdDSA unsigned-packet policy and add coverage - #10858

Merged
thebentern merged 1 commit into
developfrom
fix/xeddsa-signing-policy
Jul 2, 2026
Merged

Harden XEdDSA unsigned-packet policy and add coverage#10858
thebentern merged 1 commit into
developfrom
fix/xeddsa-signing-policy

Conversation

@thebentern

@thebentern thebentern commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Audit of the XEdDSA packet-signing implementation (#10478) turned up several issues in when unsigned packets are accepted on receive or emitted on send. This PR fixes them and adds regression coverage.

Fixes

  • Unicast NodeInfo exchange broke against signer nodes. NodeInfoModule dropped any unsigned NodeInfo from a node whose signer bit was set — but unicast NodeInfo (want_response replies, phone-initiated exchanges) is never signed by the sender, so request and reply both died. The drop is now gated to broadcasts.
  • Sign/TOO_LARGE dead band. The old sender gate (payload + 64 < DATA_PAYLOAD_LEN) admitted 167–168 B broadcasts whose signed encoding overflowed the LoRa frame, so they were signed and then failed TOO_LARGE — undeliverable, though they'd have been fine unsigned. Replaced with an exact encoded-size gate (signedDataFits), and the receive-side downgrade predicate now mirrors it byte-for-byte.
  • Plaintext-MQTT decoded downlink bypassed the policy. Already-decoded packets skip perhapsDecode's crypto path, so signature verification and downgrade protection were skipped entirely on plaintext brokers — a rogue peer could impersonate a signing node. The policy was extracted into checkXeddsaReceivePolicy() and is now applied at MQTT ingress.
  • Malformed-signature downgrade bypass. A crafted signature of length 1–63 landed in the unsigned branch while its bytes inflated the size estimate past the fit threshold, letting a forged unsigned broadcast from a known signer dodge the drop. Signatures whose length is neither 0 nor 64 are now rejected as malformed.
  • Missing lock on the MQTT verify path. xeddsa_verify mutates a shared Ed25519 key cache; the RF path holds cryptLock but the new MQTT path did not (races the BLE/proxy task on nRF52). Now locked.
  • Client-preset signatures on packets we originate are cleared on all builds (previously only inside the XEdDSA guard, so excluded builds could transmit stale signatures that fail verification everywhere).
  • Randomized (hedged) signing. XEdDSA per the Signal spec is randomized — the nonce is r = hash1(a ‖ M ‖ Z) with Z caller-supplied randomness, giving hedged signatures. The vendored library was previously a deterministic Ed25519-style variant that ignored Z. This PR bumps the meshtastic/Crypto pin to meshtastic/Crypto#3 (which makes XEdDSA::sign mix the first 32 bytes of the signature buffer into the nonce as Z) and seeds those bytes in xeddsa_sign from HardwareRNG::fill with a checked return and a seeded-CSPRNG fallback. Signing the same content twice now yields different, both-valid signatures; a weak/repeated Z still produces a signature safe against nonce reuse, so signing never fails over RNG quality.

Tests

  • test_packet_signing: groups A (receive matrix), B (send policy incl. exhaustive payload-size sweeps over two Data shapes), C (NodeInfo backstop), D (wire-format invariants), E (decoded-ingress policy incl. malformed-signature cases).
  • test_mqtt: four end-to-end plaintext-downlink cases (drop unsigned-from-signer, accept non-signer, verify + learn signer bit, drop bad signature).
  • test_crypto: deterministic-nonce pin.

All native suites pass locally: 521/521 in the Docker coverage/ASan environment and via the native-macos toolchain.

Notes

  • No behavior change for nodes that don't sign (legacy/STM32/ham/region-unset) or for DMs.

Summary by CodeRabbit

  • New Features
    • Improved XEdDSA receive/transmit policy across mesh and MQTT using exact protobuf-encoded sizing to decide when signing and downgrade protection apply.
  • Bug Fixes
    • Prevented unsigned broadcasts from previously recognized signers when they could still fit signed encoding limits.
    • Improved handling of malformed/partial/tampered signatures and ensured valid packets (including unsigned NodeInfo unicast) are treated correctly.
    • Added a safer signing randomness fallback when hardware entropy is unavailable.
  • Documentation
    • Clarified XEdDSA signature field size/encoding overhead expectations.
  • Tests
    • Expanded packet-signing, routing, and decoded-ingress coverage for boundary and downgrade scenarios.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 54504c0b-0796-476e-8432-6805c82b9426

📥 Commits

Reviewing files that changed from the base of the PR and between efe4887 and eb29e34.

📒 Files selected for processing (19)
  • src/mesh/CryptoEngine.cpp
  • src/mesh/CryptoEngine.h
  • src/mesh/Router.cpp
  • src/mesh/Router.h
  • src/modules/NodeInfoModule.cpp
  • src/mqtt/MQTT.cpp
  • test/test_crypto/test_main.cpp
  • test/test_mqtt/MQTT.cpp
  • test/test_packet_signing/test_main.cpp
  • variants/esp32/esp32-common.ini
  • variants/esp32/esp32.ini
  • variants/esp32p4/esp32p4.ini
  • variants/native/portduino.ini
  • variants/native/portduino/platformio.ini
  • variants/nrf52840/nrf52.ini
  • variants/nrf54l15/nrf54l15.ini
  • variants/rp2040/rp2040.ini
  • variants/rp2350/rp2350.ini
  • variants/stm32/stm32.ini
✅ Files skipped from review due to trivial changes (6)
  • variants/esp32/esp32.ini
  • variants/esp32/esp32-common.ini
  • variants/nrf54l15/nrf54l15.ini
  • variants/rp2350/rp2350.ini
  • variants/esp32p4/esp32p4.ini
  • variants/native/portduino.ini
🚧 Files skipped from review as they are similar to previous changes (13)
  • variants/stm32/stm32.ini
  • variants/rp2040/rp2040.ini
  • variants/nrf52840/nrf52.ini
  • src/mesh/CryptoEngine.h
  • variants/native/portduino/platformio.ini
  • src/mesh/Router.h
  • src/mesh/CryptoEngine.cpp
  • src/modules/NodeInfoModule.cpp
  • src/mqtt/MQTT.cpp
  • test/test_mqtt/MQTT.cpp
  • src/mesh/Router.cpp
  • test/test_crypto/test_main.cpp
  • test/test_packet_signing/test_main.cpp

📝 Walkthrough

Walkthrough

This PR updates XEdDSA signing and receive-policy handling, applies the shared policy in Router and MQTT, tightens NodeInfo broadcast handling, expands regression tests, and repins the meshtastic/Crypto dependency across multiple variant configs.

Changes

XEdDSA signing policy and enforcement

Layer / File(s) Summary
Signing prelude and size constant
src/mesh/CryptoEngine.cpp, src/mesh/CryptoEngine.h
The XEdDSA signing prelude now seeds the nonce input with hardware RNG when available and software RNG otherwise, and the signature field overhead constant is documented in the header.
Centralized receive policy
src/mesh/Router.h, src/mesh/Router.cpp
A shared XEdDSA receive-policy helper was added and wired into router decode paths to verify full signatures, reject malformed partial signatures, and apply downgrade checks.
Exact-fit send decision
src/mesh/Router.cpp
The router now computes whether signed protobuf data fits the LoRa payload limit, clears preset local signature state before signing decisions, and uses the exact encoded-size gate for broadcast signing.
NodeInfo and MQTT ingress
src/modules/NodeInfoModule.cpp, src/mqtt/MQTT.cpp
Unsigned NodeInfo drops are limited to broadcasts, and decoded MQTT downlinks now run through the shared XEdDSA receive-policy helper under the crypto lock.
Regression coverage
test/test_crypto/test_main.cpp, test/test_mqtt/MQTT.cpp, test/test_packet_signing/test_main.cpp
The crypto, MQTT, and packet-signing test suites were expanded to cover randomized repeated signing, MQTT ingress policy, exact encoded-size fit boundaries, downgrade dead-bands, NodeInfo unicast acceptance, and direct receive-policy outcomes.

Crypto dependency refresh

Layer / File(s) Summary
Pinned Crypto archive update
variants/esp32/*.ini, variants/esp32p4/esp32p4.ini, variants/native/portduino*.ini, variants/nrf52840/nrf52.ini, variants/nrf54l15/nrf54l15.ini, variants/rp2040/rp2040.ini, variants/rp2350/rp2350.ini, variants/stm32/stm32.ini
The Crypto dependency archive references were changed across the affected platform variant configurations.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Sender
  participant Router
  participant CryptoEngine
  participant MQTT
  participant NodeInfoModule

  Sender->>Router: perhapsEncode(packet)
  Router->>Router: signedDataFits(data)
  alt encoded+signature fits LoRa frame
    Router->>CryptoEngine: xeddsa_sign(data)
    CryptoEngine-->>Router: signature
  else
    Router->>Router: send unsigned
  end

  Router->>Router: perhapsDecode(packet)
  Router->>Router: checkXeddsaReceivePolicy(p, size)
  Router->>CryptoEngine: xeddsa_verify(signature)
  CryptoEngine-->>Router: valid/invalid
  Router-->>Router: accept / drop

  MQTT->>MQTT: onReceiveProto(decoded packet)
  MQTT->>Router: checkXeddsaReceivePolicy(p)
  Router-->>MQTT: accept / drop

  Router->>NodeInfoModule: handleReceivedProtobuf(mp)
  NodeInfoModule->>NodeInfoModule: drop if broadcast && unsigned && previously signed
Loading

Poem

A bunny hopped through code so bright,
with signed fit checks all snug and tight.
One policy now guards each gate,
and packets choose their proper fate.
🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: XEdDSA unsigned-packet policy hardening plus added test coverage.
Description check ✅ Passed The description is detailed and on-topic, covering summary, fixes, tests, and notes, though it omits the template's attestations section.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@thebentern
thebentern requested a review from jp-bennett July 2, 2026 14:19
@thebentern
thebentern force-pushed the fix/xeddsa-signing-policy branch from d00e23a to 46708cb Compare July 2, 2026 14:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/modules/NodeInfoModule.cpp (1)

54-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Shorten the policy backstop comment.

The behavior is clear; this can fit in one or two lines.

Suggested cleanup
-    // Broadcasts only: unicast NodeInfo (want_response replies, directed exchanges) is never
-    // signed by the sender, so dropping it here would break exchanges with signer nodes. This
-    // check backstops ingress paths that skip Router's downgrade drop (e.g. decoded MQTT).
+    // Broadcasts only: unicast NodeInfo is never signed; this backstops decoded-ingress paths.

As per coding guidelines, “Keep code comments minimal: one or two lines at most”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/NodeInfoModule.cpp` around lines 54 - 56, Shorten the existing
policy backstop comment in NodeInfoModule so it fits within one or two lines
while preserving the key point: only broadcasts are dropped, unicast NodeInfo
must still pass for signer-node exchanges, and this backstops ingress paths that
bypass Router’s downgrade drop. Update the comment near the NodeInfoModule logic
to be more concise without changing the behavior description.

Source: Coding guidelines

src/mqtt/MQTT.cpp (1)

136-141: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Trim the ingress-policy comment.

The code already shows the lock and policy call; keep only the non-obvious “why”.

Suggested cleanup
-        // Already-decoded downlink skips perhapsDecode's crypto path entirely, so enforce the
-        // signature policy here: verify a carried signature and apply unsigned-downgrade
-        // protection for known signers. Without this, a peer on a plaintext broker could
-        // impersonate a signing node with unsigned broadcasts. Hold cryptLock like the RF path
-        // (perhapsDecode) does - checkXeddsaReceivePolicy -> xeddsa_verify mutates shared
-        // CryptoEngine cache state, and MQTT ingress can run on a different task.
+        // Decoded MQTT skips perhapsDecode; enforce signature policy under cryptLock because
+        // verification mutates CryptoEngine cache state and may run from another task.

As per coding guidelines, “Keep code comments minimal: one or two lines at most”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mqtt/MQTT.cpp` around lines 136 - 141, The ingress-policy comment in MQTT
ingress is too verbose and repeats what the code already shows. Trim the comment
around the cryptLock and checkXeddsaReceivePolicy path in MQTT.cpp to only the
non-obvious reason for the lock/policy check, keeping it to one or two short
lines and removing the step-by-step explanation.

Source: Coding guidelines

test/test_mqtt/MQTT.cpp (1)

675-678: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Condense the audit-context comment.

The test name covers the expectation; keep the threat-model note brief.

Suggested cleanup
-// Decoded (plaintext-broker) downlink skips perhapsDecode's crypto path, so MQTT applies
-// checkXeddsaReceivePolicy at ingress. An unsigned broadcast claiming to come from a node that
-// previously signed must be dropped - without this, a rogue broker peer could impersonate any
-// signing node (audit F3).
+// Decoded MQTT must apply receive policy so unsigned broadcasts cannot spoof known signers.

As per coding guidelines, “Keep code comments minimal: one or two lines at most”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/test_mqtt/MQTT.cpp` around lines 675 - 678, Shorten the audit-context
comment in MQTT.cpp to a brief one- or two-line note, since the test name
already captures the expectation. Keep the key threat-model point only: that a
decoded plaintext downlink still hits checkXeddsaReceivePolicy at ingress and
must reject unsigned broadcasts claiming to be from a previously signing node.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/xeddsa-unsigned-packet-policy.md`:
- Around line 16-18: Update the prose in the xeddsa unsigned packet policy doc
to match the grammar/style suggestions from static analysis: in the paragraph
describing trust-on-first-use, replace “afterwards” with “afterward,” and in the
later sentence around the signing capability wording, change “is able to sign”
to “can sign” for conciseness. Keep the meaning unchanged and adjust the
surrounding sentence flow in the same section if needed.

---

Nitpick comments:
In `@src/modules/NodeInfoModule.cpp`:
- Around line 54-56: Shorten the existing policy backstop comment in
NodeInfoModule so it fits within one or two lines while preserving the key
point: only broadcasts are dropped, unicast NodeInfo must still pass for
signer-node exchanges, and this backstops ingress paths that bypass Router’s
downgrade drop. Update the comment near the NodeInfoModule logic to be more
concise without changing the behavior description.

In `@src/mqtt/MQTT.cpp`:
- Around line 136-141: The ingress-policy comment in MQTT ingress is too verbose
and repeats what the code already shows. Trim the comment around the cryptLock
and checkXeddsaReceivePolicy path in MQTT.cpp to only the non-obvious reason for
the lock/policy check, keeping it to one or two short lines and removing the
step-by-step explanation.

In `@test/test_mqtt/MQTT.cpp`:
- Around line 675-678: Shorten the audit-context comment in MQTT.cpp to a brief
one- or two-line note, since the test name already captures the expectation.
Keep the key threat-model point only: that a decoded plaintext downlink still
hits checkXeddsaReceivePolicy at ingress and must reject unsigned broadcasts
claiming to be from a previously signing node.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0b2140ec-504f-4546-8c84-f1684c17f3af

📥 Commits

Reviewing files that changed from the base of the PR and between d4db80e and d00e23a.

📒 Files selected for processing (10)
  • docs/xeddsa-unsigned-packet-policy.md
  • src/mesh/CryptoEngine.cpp
  • src/mesh/CryptoEngine.h
  • src/mesh/Router.cpp
  • src/mesh/Router.h
  • src/modules/NodeInfoModule.cpp
  • src/mqtt/MQTT.cpp
  • test/test_crypto/test_main.cpp
  • test/test_mqtt/MQTT.cpp
  • test/test_packet_signing/test_main.cpp

Comment thread docs/xeddsa-unsigned-packet-policy.md Outdated
@thebentern
thebentern requested a review from Copilot July 2, 2026 14:32
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

⚡ Try this PR in the Web Flasher

Note

Building this pull request… the flash button, badges and supported-board
list will appear here automatically once CI finishes.

@github-actions github-actions Bot added the bugfix Pull request that fixes bugs label Jul 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens Meshtastic’s XEdDSA packet-signing policy across both RF receive/decode and plaintext MQTT downlink, and adds regression coverage to prevent unsigned-packet acceptance and size-gate edge cases from reappearing.

Changes:

  • Extracts XEdDSA receive-side policy into checkXeddsaReceivePolicy() and applies it to decoded MQTT ingress (including holding cryptLock).
  • Replaces the old payload-length signing heuristic with an exact encoded-size gate (signedDataFits) and mirrors that logic on receive to avoid sign/TOO_LARGE dead bands.
  • Expands native tests to cover the receive/send policy matrix, NodeInfo broadcast-only rule, encoding invariants, decoded-ingress policy, and deterministic signing behavior.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/test_packet_signing/test_main.cpp Adds extensive policy regression tests (RF round-trips + direct helper coverage) including size-boundary and malformed-signature cases.
test/test_mqtt/MQTT.cpp Adds plaintext-decoded MQTT downlink policy tests and resets shared MockNodeDB state between tests.
test/test_crypto/test_main.cpp Pins deterministic XEdDSA signing behavior via signature equality assertions.
src/mqtt/MQTT.cpp Enforces XEdDSA receive policy (with cryptLock) on decoded MQTT ingress before enqueueing.
src/modules/NodeInfoModule.cpp Restricts “drop unsigned NodeInfo from known signer” backstop to broadcasts only.
src/mesh/Router.h Declares checkXeddsaReceivePolicy() as a reusable policy helper.
src/mesh/Router.cpp Implements checkXeddsaReceivePolicy(), integrates it into perhapsDecode(), and introduces signedDataFits() for exact send-side gating.
src/mesh/CryptoEngine.h Adds XEDDSA_SIGNATURE_FIELD_BYTES constant (pinned by new tests).
src/mesh/CryptoEngine.cpp Removes vestigial RNG prefill and documents determinism expectations.

Comment thread src/mesh/Router.cpp
Comment thread src/mesh/Router.h
@thebentern
thebentern force-pushed the fix/xeddsa-signing-policy branch from 46708cb to c8b8b9a Compare July 2, 2026 14:58
Comment thread src/mesh/CryptoEngine.cpp
Comment thread src/mesh/Router.cpp
Comment thread test/test_crypto/test_main.cpp Outdated
Comment thread test/test_crypto/test_main.cpp Outdated
@thebentern
thebentern force-pushed the fix/xeddsa-signing-policy branch from c8b8b9a to efe4887 Compare July 2, 2026 15:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/mesh/CryptoEngine.cpp (1)

119-126: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Condense this comment to match the C++ comment guideline.

The RNG seeding rationale is useful, but this 8-line block exceeds the project’s “one or two lines” comment style. Keep the why and move protocol details to docs/tests if needed. As per coding guidelines, “Keep code comments minimal: one or two lines at most, only explain the why when it is not obvious, and avoid multi-paragraph explanatory comments.”

Suggested condensation
-    // XEdDSA per the Signal spec is a *randomized* scheme: the nonce is r = hash1(a || M || Z),
-    // where Z is caller-supplied randomness, giving hedged signatures (defense against fault /
-    // bad-RNG / side-channel attacks). meshtastic/Crypto#3 made XEdDSA::sign spec-compliant - it
-    // now mixes the first 32 bytes of `signature` into r as Z - so we must seed those bytes with
-    // entropy before signing. Prefer the hardware RNG; fall back to the seeded software CSPRNG if
-    // it is unavailable. A weak or repeated Z still yields a valid signature that is safe against
-    // nonce reuse (Z is only defense-in-depth), so we never fail signing over it. XEdDSA::sign
-    // overwrites all 64 bytes with R||s on return.
+    // Seed XEdDSA's caller-supplied nonce randomness (`Z`) for hedged signatures.
+    // Fall back to the seeded software CSPRNG; `XEdDSA::sign` overwrites the buffer.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mesh/CryptoEngine.cpp` around lines 119 - 126, Condense the explanatory
block in CryptoEngine::sign/XEdDSA setup to a one- or two-line C++ comment. Keep
only the essential “why” about seeding the first 32 bytes of signature with
entropy before XEdDSA::sign and the preference for hardware RNG with software
CSPRNG fallback; move the detailed protocol/background notes out of the comment
and keep the reference to XEdDSA::sign and the signature buffer initialization
clear.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/mesh/CryptoEngine.cpp`:
- Around line 119-126: Condense the explanatory block in
CryptoEngine::sign/XEdDSA setup to a one- or two-line C++ comment. Keep only the
essential “why” about seeding the first 32 bytes of signature with entropy
before XEdDSA::sign and the preference for hardware RNG with software CSPRNG
fallback; move the detailed protocol/background notes out of the comment and
keep the reference to XEdDSA::sign and the signature buffer initialization
clear.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0c95a7a0-1b59-4cce-8265-b6187a51b2d2

📥 Commits

Reviewing files that changed from the base of the PR and between 46708cb and efe4887.

📒 Files selected for processing (19)
  • src/mesh/CryptoEngine.cpp
  • src/mesh/CryptoEngine.h
  • src/mesh/Router.cpp
  • src/mesh/Router.h
  • src/modules/NodeInfoModule.cpp
  • src/mqtt/MQTT.cpp
  • test/test_crypto/test_main.cpp
  • test/test_mqtt/MQTT.cpp
  • test/test_packet_signing/test_main.cpp
  • variants/esp32/esp32-common.ini
  • variants/esp32/esp32.ini
  • variants/esp32p4/esp32p4.ini
  • variants/native/portduino.ini
  • variants/native/portduino/platformio.ini
  • variants/nrf52840/nrf52.ini
  • variants/nrf54l15/nrf54l15.ini
  • variants/rp2040/rp2040.ini
  • variants/rp2350/rp2350.ini
  • variants/stm32/stm32.ini
✅ Files skipped from review due to trivial changes (4)
  • variants/rp2350/rp2350.ini
  • variants/nrf54l15/nrf54l15.ini
  • variants/nrf52840/nrf52.ini
  • src/mesh/CryptoEngine.h
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/mesh/Router.h
  • src/mqtt/MQTT.cpp
  • src/modules/NodeInfoModule.cpp
  • src/mesh/Router.cpp
  • test/test_mqtt/MQTT.cpp
  • test/test_packet_signing/test_main.cpp

Audit of the XEdDSA packet-signing implementation (#10478) surfaced several
issues in when unsigned packets are accepted on receive or emitted on send.
This fixes them and adds regression coverage.

- Unicast NodeInfo exchange no longer breaks against signer nodes: the
  NodeInfoModule downgrade drop is gated to broadcasts, since senders never
  sign unicast (want_response replies, directed exchanges).
- Replace the payload-size sign heuristic with an exact encoded-size gate
  (signedDataFits) and mirror it on the receive side, removing a dead band
  where 167-168 B broadcasts were signed then failed TOO_LARGE.
- Extract the receive policy into checkXeddsaReceivePolicy() and apply it to
  plaintext-MQTT decoded downlink, which previously skipped signature
  verification and downgrade protection entirely.
- Reject signatures whose length is neither 0 nor 64 as malformed, so a
  crafted partial signature can't inflate the size estimate and dodge the
  unsigned-downgrade drop.
- Hold cryptLock on the MQTT verify path (shared Ed25519 key cache).
- Clear any client-preset signature on packets we originate, on all builds.
- Randomized (hedged) signing per the Signal XEdDSA spec: bump the
  meshtastic/Crypto pin to the build where XEdDSA::sign mixes 32 bytes of
  caller randomness into the nonce as Z (meshtastic/Crypto#3), and seed those
  bytes in xeddsa_sign from HardwareRNG (checked, with a seeded-CSPRNG
  fallback). test_crypto pins that repeated signs differ and both verify.

Adds test coverage: test_packet_signing groups A-E (receive matrix, send
policy, NodeInfo backstop, encoding invariants, decoded-ingress policy),
test_mqtt end-to-end downlink cases, and a test_crypto randomization check.
@thebentern
thebentern force-pushed the fix/xeddsa-signing-policy branch from efe4887 to eb29e34 Compare July 2, 2026 15:58
@thebentern
thebentern merged commit 0e84c1a into develop Jul 2, 2026
82 checks passed
@thebentern
thebentern deleted the fix/xeddsa-signing-policy branch July 2, 2026 16:49
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Firmware Size Report

22 targets | vs develop: 20 increased, 2 decreased, net +38,624 (+37.7 KB)

Target Size vs develop
t-deck-tft 3,818,896 📈 +8,224 (+8.0 KB)
heltec-vision-master-e213-inkhud 2,231,248 📈 +3,888 (+3.8 KB)
elecrow-adv-35-tft 3,419,072 📈 +2,448 (+2.4 KB)
seeed-xiao-s3 2,278,064 📈 +1,920 (+1.9 KB)
tlora-c6 2,370,128 📈 +1,824 (+1.8 KB)
Show 17 more target(s)
Target Size vs develop
station-g2 2,267,968 📈 +1,616 (+1.6 KB)
station-g3 2,267,968 📈 +1,616 (+1.6 KB)
t-eth-elite 2,493,024 📈 +1,536 (+1.5 KB)
rak3312 2,273,760 📈 +1,520 (+1.5 KB)
heltec-ht62-esp32c3-sx1262 2,136,416 📈 +1,424 (+1.4 KB)
heltec-v3 2,265,472 📈 +1,392 (+1.4 KB)
heltec-v4 2,278,672 📈 +1,376 (+1.3 KB)
picow 1,246,256 📈 +1,376 (+1.3 KB)
pico2w 1,221,864 📈 +1,348 (+1.3 KB)
rak11200 1,861,872 📈 +1,264 (+1.2 KB)
rak11310 806,664 📈 +1,240 (+1.2 KB)
pico 783,920 📈 +1,232 (+1.2 KB)
seeed_xiao_rp2040 782,136 📈 +1,232 (+1.2 KB)
pico2 770,960 📈 +1,184 (+1.2 KB)
seeed_xiao_rp2350 769,104 📈 +1,168 (+1.1 KB)
rak3172 186,172 📉 -124
wio-e5 238,548 📉 -80

Updated for 341b167

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Pull request that fixes bugs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants