Skip to content

Packet Signing via XEdDSA#10478

Merged
thebentern merged 36 commits into
developfrom
XEdDSA-merge
Jun 13, 2026
Merged

Packet Signing via XEdDSA#10478
thebentern merged 36 commits into
developfrom
XEdDSA-merge

Conversation

@jp-bennett

Copy link
Copy Markdown
Collaborator

New PR after a particularly ugly merge.

jp-bennett and others added 12 commits May 13, 2026 12:02
* Generate a new node identity on key generation

* Fixes

* Fixes

* Fixes

* Messed up

* Fixes

* Update src/modules/AdminModule.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/mesh/NodeDB.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Figured it out!

* Cleanup

* Update src/mesh/NodeDB.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/mesh/NodeDB.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/modules/AdminModule.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* fix: add null check for getMeshNode() in NodeInfoModule

getMeshNode() can return nullptr for unknown nodes. Dereferencing
without a check crashes the firmware when receiving NodeInfo from
a node not yet in the database.

* fix: enforce XEdDSA signature verification and prevent stripping

Previously, failed signature verification still allowed the packet
through, making signatures purely cosmetic. Now:

- Failed verification drops the packet (DECODE_FAILURE)
- Successfully verified nodes get HAS_XEDDSA_SIGNED bitfield set
- Unsigned packets from previously-signing nodes are rejected
- Log levels reduced from WARN/ERROR to DEBUG/WARN as appropriate

* fix: include packet metadata in XEdDSA signature

The signature now covers [fromNode | packetId | portnum | payload]
instead of just the payload bytes. This prevents:
- Replay attacks (different packetId fails verification)
- Reattribution (different fromNode fails verification)
- Portnum redirection (different portnum fails verification)

Also adds a key initialization check to xeddsa_sign (returns false
if XEdDSA keys are all zeros) and checks the return value in the
encode path.

* fix: handle existing key pair in AdminModule security config

When a user provides both a valid private key and public key via
admin config, the crypto engine's DH private key and owner public
key were never loaded. DMs and XEdDSA signing would silently break.

Add an else branch to load both keys into the crypto engine.

* perf: cache Ed25519 public key conversion in xeddsa_verify

curve_to_ed_pub() performs field element parsing, inversion, and
multiplication on every call. Since packets from the same node
tend to arrive in bursts, a single-entry cache avoids repeating
this expensive conversion for consecutive packets from one sender.

* fix: skip identity cleanup when node number is unchanged

createNewIdentity() was called on every generateCryptoKeyPair(),
including normal boots where the same key is regenerated. This
caused unnecessary NodeDB writes and old-node cleanup logic to
run when the node number hadn't actually changed.

Also fixes only zeroing byte[0] of the old node's public key
instead of clearing the entire array.

* fix: replace hardcoded 120 with derived XEDDSA_SIGNATURE_SIZE constant

The payload size check for XEdDSA signing used a magic number (120).
Replace with a derivation from DATA_PAYLOAD_LEN and XEDDSA_SIGNATURE_SIZE
so the limit adjusts automatically if constants change. This also
increases the max signable payload from 120 to 169 bytes, which is
still safe since the actual encoded size is checked after pb_encode.

* fix: add const qualifiers to XEdDSA verify and curve_to_ed_pub inputs

pubKey, payload, and signature parameters in xeddsa_verify are
input-only and should not be modified. Same for curve_pubkey in
curve_to_ed_pub.

* chore: remove commented-out old Crypto dependency in portduino.ini

* Leave out the admin module change for now

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
@github-actions github-actions Bot added needs-review Needs human review enhancement New feature or request labels May 14, 2026
@thebentern thebentern requested a review from Copilot May 14, 2026 19:32

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 introduces XEdDSA-based packet signing support across the firmware (signing on transmit, verification on receive, and persistence of “signer” state per node), adds unit tests for the signing primitives, and switches multiple PlatformIO environments to a Meshtastic-hosted Crypto library fork to pick up the required crypto APIs.

Changes:

  • Switch platform lib_deps from rweather/Crypto (and STM32’s previous Crypto source) to a pinned meshtastic/Crypto GitHub zip across several variants.
  • Add XEdDSA sign/verify support in CryptoEngine and integrate signature verify + “previously signed” enforcement in Router.
  • Consolidate PKI key generation in NodeDB and add XEdDSA unit tests.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
variants/stm32/stm32.ini Switch STM32 Crypto dependency to Meshtastic fork (pinned zip).
variants/rp2350/rp2350.ini Switch RP2350 Crypto dependency to Meshtastic fork (pinned zip).
variants/rp2040/rp2040.ini Switch RP2040 Crypto dependency to Meshtastic fork (pinned zip).
variants/nrf52840/nrf52.ini Switch nRF52 Crypto dependency to Meshtastic fork (pinned zip).
variants/native/portduino.ini Switch Portduino Crypto dependency to Meshtastic fork (pinned zip).
variants/esp32c6/esp32c6.ini Switch ESP32-C6 Crypto dependency to Meshtastic fork (pinned zip).
variants/esp32/esp32-common.ini Switch ESP32 common Crypto dependency to Meshtastic fork (pinned zip).
test/test_crypto/test_main.cpp Add XEdDSA sign/verify unit tests.
src/modules/NodeInfoModule.cpp Reject unsigned NodeInfo from nodes previously observed signing.
src/modules/AdminModule.cpp Rework security config handling to use NodeDB keypair generation + force reboot/save segments.
src/mesh/TypeConversions.cpp Expose per-node “has_xeddsa_signed” status in NodeInfo conversion.
src/mesh/Router.cpp Verify signatures on decode; sign outgoing broadcasts when size allows; enforce “previously signed” policy.
src/mesh/NodeDB.h Add NodeDB crypto keypair helper + new bitfield flag/accessor for XEdDSA signing.
src/mesh/NodeDB.cpp Implement consolidated crypto key generation and identity switching logic.
src/mesh/CryptoEngine.h Add XEdDSA signing API + cache fields and Curve25519→Ed25519 pubkey conversion hook.
src/mesh/CryptoEngine.cpp Implement XEdDSA signing/verification and Curve25519→Ed25519 pubkey conversion.

Comment thread src/mesh/Router.cpp Outdated
Comment thread src/mesh/Router.cpp Outdated
Comment thread src/modules/NodeInfoModule.cpp Outdated
Comment thread src/mesh/NodeDB.cpp Outdated
Comment thread src/mesh/NodeDB.cpp
Comment thread variants/native/portduino.ini Outdated
Comment thread variants/esp32c6/esp32c6.ini Outdated
Comment thread variants/esp32/esp32-common.ini Outdated
Comment thread test/test_crypto/test_main.cpp Outdated
Comment thread src/mesh/CryptoEngine.cpp Outdated
@Xaositek

Copy link
Copy Markdown
Contributor

Should #7602 be closed in favor of this?

thebentern and others added 11 commits May 15, 2026 16:01
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@thebentern

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

Copilot AI commented May 19, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved the merge conflict and pushed the merge commit in 80727e8.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • api.registry.nm1.platformio.org
    • Triggering command: /home/REDACTED/.local/bin/pio /home/REDACTED/.local/bin/pio test -e native -f test_crypto (dns block)
    • Triggering command: /home/REDACTED/.local/bin/pio /home/REDACTED/.local/bin/pio test -e native -f test_crypto -v ndor/bin/git (dns block)
  • api.registry.platformio.org
    • Triggering command: /home/REDACTED/.local/bin/pio /home/REDACTED/.local/bin/pio test -e native -f test_crypto (dns block)
    • Triggering command: /home/REDACTED/.local/bin/pio /home/REDACTED/.local/bin/pio test -e native -f test_crypto -v ndor/bin/git (dns block)
  • collector.platformio.org
    • Triggering command: /home/REDACTED/.local/bin/pio /home/REDACTED/.local/bin/pio test -e native -f test_crypto (dns block)
    • Triggering command: /home/REDACTED/.local/bin/pio /home/REDACTED/.local/bin/pio project config --environment tlora_c6 --json-output (dns block)
    • Triggering command: /home/REDACTED/.local/bin/pio /home/REDACTED/.local/bin/pio project config -h (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI requested a review from thebentern May 19, 2026 12:17

@adam-scott-thomas adam-scott-thomas left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Really solid direction overall. The signing boundary looks right to me, especially binding the signature to sender/packet/app context instead of just payload. My comments are mostly around making the signature preimage deterministic across ports, preserving provenance semantics for signed-but-unverifiable packets, and where a future opt-in custody/continuity layer could fit without fighting the MTU constraints.

Comment thread src/mesh/CryptoEngine.cpp
Comment thread src/mesh/Router.cpp
return DecodeState::DECODE_FAILURE;
}
} else {
LOG_DEBUG("No public key for 0x%08x, cannot verify XEdDSA signature", p->from);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

One provenance-semantics thought: a packet with a signature present but no available public key is different from both “verified signed” and “unsigned.” Even if accepting it is the right compatibility choice, it may be worth surfacing that state distinctly for apps/admin tooling: verified signed, unsigned, and signed-but-unverifiable.

Comment thread src/mesh/CryptoEngine.cpp
Comment thread src/mesh/Router.cpp
// Sign broadcast packets if payload + signature fits within the max Data payload.
// The actual encoded size is checked after pb_encode (TOO_LARGE).
if (!p->pki_encrypted && isBroadcast(p->to) &&
p->decoded.payload.size + XEDDSA_SIGNATURE_SIZE < meshtastic_Constants_DATA_PAYLOAD_LEN) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This size gate makes sense given the MTU. Longer term, this feels like the natural seam for an opt-in custody/continuity layer rather than more per-packet crypto: a small custody flag plus an 8–16 byte rolling hash/tag on custody-enabled messages, with periodic signed Merkle/batch anchors instead of attaching heavy proof material to every packet.

The Merkle side can work fully offline too: roots can be signed by a local field box with monotonic counters/GPS time, then externally anchored/timestamped later when an internet-connected path is available.

@github-actions

Copy link
Copy Markdown
Contributor

Firmware Size Report

22 targets | vs develop: 22 increased, net +484,004 (+472.7 KB)

Target Size vs develop
station-g2 2,246,160 📈 +27,488 (+26.8 KB)
heltec-v4 2,255,360 📈 +27,232 (+26.6 KB)
t-deck-tft 3,781,216 📈 +26,912 (+26.3 KB)
seeed-xiao-s3 2,245,600 📈 +26,848 (+26.2 KB)
rak3312 2,241,616 📈 +26,832 (+26.2 KB)
Show 17 more target(s)
Target Size vs develop
station-g3 2,236,576 📈 +26,528 (+25.9 KB)
rak11200 1,830,464 📈 +26,288 (+25.7 KB)
heltec-v3 2,234,848 📈 +26,208 (+25.6 KB)
t-eth-elite 2,461,200 📈 +25,744 (+25.1 KB)
elecrow-adv-35-tft 3,387,920 📈 +25,616 (+25.0 KB)
heltec-vision-master-e213-inkhud 2,195,392 📈 +25,040 (+24.5 KB)
tlora-c6 2,340,784 📈 +24,672 (+24.1 KB)
heltec-ht62-esp32c3-sx1262 2,106,880 📈 +24,464 (+23.9 KB)
picow 1,220,192 📈 +22,888 (+22.4 KB)
seeed_xiao_rp2040 758,144 📈 +22,560 (+22.0 KB)
rak11310 782,520 📈 +22,552 (+22.0 KB)
pico 759,928 📈 +22,544 (+22.0 KB)
pico2w 1,196,796 📈 +18,012 (+17.6 KB)
seeed_xiao_rp2350 746,072 📈 +17,680 (+17.3 KB)
pico2 747,920 📈 +17,672 (+17.3 KB)
rak3172 180,200 📈 +112
wio-e5 232,564 📈 +112

Updated for b699a8f

@github-actions

Copy link
Copy Markdown
Contributor

⚡ Try this PR in the Web Flasher

Flash this PR in the Web Flasher

firmware commit boards expires

Warning

This is an automated, unreviewed CI test build. Back up your device configuration
before flashing, and only flash devices you are able to recover.

Supported boards built by this PR (24)
Device Board Platform
Crowpanel Adv 3.5 TFT elecrow-adv-35-tft esp32-s3
Heltec HT62 heltec-ht62-esp32c3-sx1262 esp32-c3
Heltec Mesh Node 096 heltec-mesh-node-t096 nrf52840
Heltec Mesh Node T1 heltec-mesh-node-t1 nrf52840
Heltec Mesh Node T114 heltec-mesh-node-t114 nrf52840
Heltec V3 heltec-v3 esp32-s3
Heltec V4 heltec-v4 esp32-s3
Raspberry Pi Pico pico rp2040
Raspberry Pi Pico W picow rp2040
RAK WisMesh Tag rak_wismeshtag nrf52840
RAK WisBlock 11200 rak11200 esp32
RAK WisBlock 11310 rak11310 rp2040
RAK3312 rak3312 esp32-s3
RAK WisBlock 4631 rak4631 nrf52840
Seeed Wio Tracker L1 seeed_wio_tracker_L1 nrf52840
Seeed Xiao NRF52840 Kit seeed_xiao_nrf52840_kit nrf52840
Seeed Xiao ESP32-S3 seeed-xiao-s3 esp32-s3
Station G2 station-g2 esp32-s3
Station G3 station-g3 esp32-s3
LILYGO T-Deck t-deck-tft esp32-s3
LILYGO T-Echo t-echo nrf52840
LILYGO T-Echo Plus t-echo-plus nrf52840
LilyGo T3-C6 tlora-c6 esp32-c6
Seeed SenseCAP T1000-E tracker-t1000-e nrf52840

Build artifacts expire on 2026-07-13. Updated for 919e03a.

@thebentern thebentern merged commit 8267bb2 into develop Jun 13, 2026
88 checks passed
@thebentern thebentern deleted the XEdDSA-merge branch June 13, 2026 11:46
oscgonfer pushed a commit that referenced this pull request Jun 14, 2026
* Test commit for XEdDSA support

* Update to Crypto lib in Meshtatic org

* Generate a new node identity on key generation (#7628)

* Generate a new node identity on key generation

* Fixes

* Fixes

* Fixes

* Messed up

* Fixes

* Update src/modules/AdminModule.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/mesh/NodeDB.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Figured it out!

* Cleanup

* Update src/mesh/NodeDB.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/mesh/NodeDB.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/modules/AdminModule.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update crypto commit hash

* Some fixes for xeddsa pr (#9610)

* fix: add null check for getMeshNode() in NodeInfoModule

getMeshNode() can return nullptr for unknown nodes. Dereferencing
without a check crashes the firmware when receiving NodeInfo from
a node not yet in the database.

* fix: enforce XEdDSA signature verification and prevent stripping

Previously, failed signature verification still allowed the packet
through, making signatures purely cosmetic. Now:

- Failed verification drops the packet (DECODE_FAILURE)
- Successfully verified nodes get HAS_XEDDSA_SIGNED bitfield set
- Unsigned packets from previously-signing nodes are rejected
- Log levels reduced from WARN/ERROR to DEBUG/WARN as appropriate

* fix: include packet metadata in XEdDSA signature

The signature now covers [fromNode | packetId | portnum | payload]
instead of just the payload bytes. This prevents:
- Replay attacks (different packetId fails verification)
- Reattribution (different fromNode fails verification)
- Portnum redirection (different portnum fails verification)

Also adds a key initialization check to xeddsa_sign (returns false
if XEdDSA keys are all zeros) and checks the return value in the
encode path.

* fix: handle existing key pair in AdminModule security config

When a user provides both a valid private key and public key via
admin config, the crypto engine's DH private key and owner public
key were never loaded. DMs and XEdDSA signing would silently break.

Add an else branch to load both keys into the crypto engine.

* perf: cache Ed25519 public key conversion in xeddsa_verify

curve_to_ed_pub() performs field element parsing, inversion, and
multiplication on every call. Since packets from the same node
tend to arrive in bursts, a single-entry cache avoids repeating
this expensive conversion for consecutive packets from one sender.

* fix: skip identity cleanup when node number is unchanged

createNewIdentity() was called on every generateCryptoKeyPair(),
including normal boots where the same key is regenerated. This
caused unnecessary NodeDB writes and old-node cleanup logic to
run when the node number hadn't actually changed.

Also fixes only zeroing byte[0] of the old node's public key
instead of clearing the entire array.

* fix: replace hardcoded 120 with derived XEDDSA_SIGNATURE_SIZE constant

The payload size check for XEdDSA signing used a magic number (120).
Replace with a derivation from DATA_PAYLOAD_LEN and XEDDSA_SIGNATURE_SIZE
so the limit adjusts automatically if constants change. This also
increases the max signable payload from 120 to 169 bytes, which is
still safe since the actual encoded size is checked after pb_encode.

* fix: add const qualifiers to XEdDSA verify and curve_to_ed_pub inputs

pubKey, payload, and signature parameters in xeddsa_verify are
input-only and should not be modified. Same for curve_pubkey in
curve_to_ed_pub.

* chore: remove commented-out old Crypto dependency in portduino.ini

* Leave out the admin module change for now

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>

* trunk

* protobuf re-update

* Protobufs

* Merge resolution fix

* Put XEDDSA on the right bit

* NodeDB update to new nodeInfoLite accessors, etc

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Refine unsigned packet rejection logic in Router (#10534)

* use hardware random to fill the first 32 signature bytes with entropy prior to signing.

* Add XEdDSA packet-signing policy tests and update dependencies for macos

* Minor fixes

* integrate XEdDSA support and update dependencies across multiple modules

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Wessel <github@weebl.me>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
@Xaositek Xaositek mentioned this pull request Jun 18, 2026
58 tasks
raghumad pushed a commit to raghumad/mezulla-firmware that referenced this pull request Jun 25, 2026
* Test commit for XEdDSA support

* Update to Crypto lib in Meshtatic org

* Generate a new node identity on key generation (meshtastic#7628)

* Generate a new node identity on key generation

* Fixes

* Fixes

* Fixes

* Messed up

* Fixes

* Update src/modules/AdminModule.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/mesh/NodeDB.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Figured it out!

* Cleanup

* Update src/mesh/NodeDB.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/mesh/NodeDB.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/modules/AdminModule.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update crypto commit hash

* Some fixes for xeddsa pr (meshtastic#9610)

* fix: add null check for getMeshNode() in NodeInfoModule

getMeshNode() can return nullptr for unknown nodes. Dereferencing
without a check crashes the firmware when receiving NodeInfo from
a node not yet in the database.

* fix: enforce XEdDSA signature verification and prevent stripping

Previously, failed signature verification still allowed the packet
through, making signatures purely cosmetic. Now:

- Failed verification drops the packet (DECODE_FAILURE)
- Successfully verified nodes get HAS_XEDDSA_SIGNED bitfield set
- Unsigned packets from previously-signing nodes are rejected
- Log levels reduced from WARN/ERROR to DEBUG/WARN as appropriate

* fix: include packet metadata in XEdDSA signature

The signature now covers [fromNode | packetId | portnum | payload]
instead of just the payload bytes. This prevents:
- Replay attacks (different packetId fails verification)
- Reattribution (different fromNode fails verification)
- Portnum redirection (different portnum fails verification)

Also adds a key initialization check to xeddsa_sign (returns false
if XEdDSA keys are all zeros) and checks the return value in the
encode path.

* fix: handle existing key pair in AdminModule security config

When a user provides both a valid private key and public key via
admin config, the crypto engine's DH private key and owner public
key were never loaded. DMs and XEdDSA signing would silently break.

Add an else branch to load both keys into the crypto engine.

* perf: cache Ed25519 public key conversion in xeddsa_verify

curve_to_ed_pub() performs field element parsing, inversion, and
multiplication on every call. Since packets from the same node
tend to arrive in bursts, a single-entry cache avoids repeating
this expensive conversion for consecutive packets from one sender.

* fix: skip identity cleanup when node number is unchanged

createNewIdentity() was called on every generateCryptoKeyPair(),
including normal boots where the same key is regenerated. This
caused unnecessary NodeDB writes and old-node cleanup logic to
run when the node number hadn't actually changed.

Also fixes only zeroing byte[0] of the old node's public key
instead of clearing the entire array.

* fix: replace hardcoded 120 with derived XEDDSA_SIGNATURE_SIZE constant

The payload size check for XEdDSA signing used a magic number (120).
Replace with a derivation from DATA_PAYLOAD_LEN and XEDDSA_SIGNATURE_SIZE
so the limit adjusts automatically if constants change. This also
increases the max signable payload from 120 to 169 bytes, which is
still safe since the actual encoded size is checked after pb_encode.

* fix: add const qualifiers to XEdDSA verify and curve_to_ed_pub inputs

pubKey, payload, and signature parameters in xeddsa_verify are
input-only and should not be modified. Same for curve_pubkey in
curve_to_ed_pub.

* chore: remove commented-out old Crypto dependency in portduino.ini

* Leave out the admin module change for now

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>

* trunk

* protobuf re-update

* Protobufs

* Merge resolution fix

* Put XEDDSA on the right bit

* NodeDB update to new nodeInfoLite accessors, etc

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Refine unsigned packet rejection logic in Router (meshtastic#10534)

* use hardware random to fill the first 32 signature bytes with entropy prior to signing.

* Add XEdDSA packet-signing policy tests and update dependencies for macos

* Minor fixes

* integrate XEdDSA support and update dependencies across multiple modules

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Wessel <github@weebl.me>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
thebentern added a commit that referenced this pull request Jun 30, 2026
…ity)

A node's NodeNum is my_node_num == crc32(public_key), assigned by createNewIdentity().
Since #10478 (2.8) generateCryptoKeyPair() called createNewIdentity() after EVERY keygen -
including the normal-boot "regenerate public key from existing private key" branch where the
key is unchanged. 2.7 never tied the NodeNum to the pubkey (pickNewNodeNum kept the stored
macaddr-derived value), so the first 2.8 boot silently re-numbered every legacy node whose
stored NodeNum != crc32(pubkey) (e.g. 0x2b873e80 -> 0xfb2df592). The old number is retired as
an ignored, keyless ghost: peers keep addressing it, DMs to it NAK (PKI_SEND_FAIL_PUBLIC_KEY),
and it lingers in the node DB and is pushed to the app flagged is_ignored.

Gate identity adoption to genuinely new or imported keys: a freshKeypair flag is set in the
imported-key and generate-new-keypair branches, left false in the regenerate-from-existing
branch, and createNewIdentity() runs only when freshKeypair. Also drop the equivalent
unconditional NodeNum re-derivation in the no-keygen (#elif PKI) load path. Regenerating the
same key on a normal boot no longer changes identity; new/imported keys still adopt crc32(pubkey).

Note: a node provisioned region-after-boot (keygen via ensurePkiKeys) now retains its
macaddr-derived NodeNum instead of adopting crc32(pubkey) on the next boot - matching pre-2.8
behavior; the node is fully functional and nothing requires NodeNum == crc32(pubkey).

Adds a regression test (test_nodedb_blocked) asserting the regenerate path keeps the NodeNum.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request needs-review Needs human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants