Skip to content

feat(security): surface XEdDSA packet signing in node & messaging UI#1993

Open
bruschill wants to merge 8 commits into
meshtastic:mainfrom
bruschill:feat/xeddsa-signing-ui-1992
Open

feat(security): surface XEdDSA packet signing in node & messaging UI#1993
bruschill wants to merge 8 commits into
meshtastic:mainfrom
bruschill:feat/xeddsa-signing-ui-1992

Conversation

@bruschill

@bruschill bruschill commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Closes #1992 · Spec: design#113 · Reference: Android #5976 + #5980

What changed?

Surfaces when a node and its broadcast packets are cryptographically signed and verified (XEdDSA). Read-only — the radio does all crypto and drops invalid/stripped signatures before the client sees them, so there is no error state; we only ever affirm the good state. 🛡️ shield = authentic (verified signed broadcast); 🔒 lock = private (PKI-encrypted DM) is left untouched.

  • Messaging — broadcast bubbles: a green checkmark.shield.fill on xeddsa_signed bubbles. Per-message. Never on DMs. The lock/shield/store-forward corner badges are now laid out side-by-side so they no longer overlap when more than one applies.
  • Message info (context menu): "Signed · verified" headline and a plain-language "Verified with the sender's key." line. Neutral/affirmative copy only.
  • Node details: a green, prominent "Signed node / Verified automatically" row driven by has_xeddsa_signed, placed above the public-key (has-key) row so the security area reads most-trusted-first. Phrased as observed ("Signed node"), labelled as automatic trust — distinct from user-asserted key verification.

Wiring

  • MeshPacket.xeddsa_signed (field 22) → MessageEntity.xeddsaSigned, set in MeshPackets.textMessageAppPacket. Gated on the broadcast address as defense-in-depth so the shield can never appear on a DM even if a stray/spoofed packet carries the flag.
  • NodeInfo.has_xeddsa_signed (field 14) → NodeInfoEntity.hasXeddsaSigned, set in MeshPackets.nodeInfoPacket and UpdateSwiftData.upsertNodeInfoPacket. Latched (x = x || new) on update paths because the field means "≥1 verified" and persists — a later NodeInfo that omits the bit must not downgrade a node we've seen sign.

Why did it change?

Firmware 2.8 signs unencrypted broadcasts with XEdDSA and verifies them on-radio. This is the iOS client side of the cross-platform tracking issue (design#113); Android already shipped it. Per the spec's v1 decisions: two affordances (shield + lock, not merged), unsigned traffic is strictly silent, no red/error styling, and the "unverifiable" state is dropped (verified-bool only).

Important

Protobuf scope. The two fields are hand-added to the generated mesh.pb.swift (accessor, storage, decode/traverse/equality, and the _NameMap bytecode token) because the pinned protobufs submodule predates 2.8 and doesn't define them. This keeps the PR focused on XEdDSA and avoids dragging in the unrelated, build-breaking churn (TrafficManagementConfig redesign, new meshBeaconApp portnum) that rides along with a full submodule bump. Resolved: upstream main has since landed the real 2.8 protobuf bump (submodule f680aace). This branch was merged with main and the hand-edit was droppedmesh.pb.swift now matches upstream's generated code, and the feature's UI/ingestion/tests merged cleanly and build green. No hand-edited protobuf remains.

How is this tested?

  • New MeshtasticTests/XEdDSASigningTests.swift (6 tests, all green): binary wire round-trip for both fields (guards the hand-edited generated code), proto3 default-omission, and entity defaults.
  • Full app + test target build green on the iOS 18 simulator.
  • High-effort code review run over the diff; findings addressed (DM guard, node-flag latching, badge collision/duplication, copy, localization).

Screenshots/Videos (when applicable)

Signing flags only populate when connected to a 2.8 node that signs; screenshots to follow once the protobufs submodule is bumped.

Checklist

  • My code adheres to the project's coding and style guidelines.
  • I have conducted a self-review of my code.
  • I have commented my code, particularly in complex areas.
  • I have verified whether these changes require updates to the in-app documentation under docs/user/ or docs/developer/, and updated accordingly. Updated docs/user/messages.md, docs/user/nodes.md, and whats-new, and regenerated the bundled HTML.
  • I have tested the change to ensure that it works as intended.

Summary by CodeRabbit

  • New Features

    • Added XEdDSA signing indicators: a green shield and “Signed · verified” in message details for verified broadcast/channel messages (including store-and-forward broadcasts).
    • Added “Signed node” rows showing “Verified automatically” in node views when a node signs its broadcasts.
    • Updated message UI elements (overlays/context menu) and accessibility to surface the new indicators.
  • Bug Fixes

    • Ensured signing status is applied only for the intended message types and is latched so node signed status isn’t downgraded.
  • Documentation / Tests

    • Updated user help and “What’s New”, and added signing/integration tests.

Affirm when a node and its broadcast packets are cryptographically
signed and verified (XEdDSA), per design#113 and mirroring Android
#5976/#5980. Read-only: the radio drops bad signatures before the
client sees them, so there is no error state — clients only ever
affirm the good state.

- Messaging: green checkmark.shield.fill on verified signed broadcast
  bubbles; "Signed · verified" + "Verified with the sender's key." in
  the message context menu. Never on DMs.
- Node details: green "Signed node / Verified automatically" row driven
  by has_xeddsa_signed, above the public-key row (most-trusted-first),
  phrased as observed.
- Wiring: MeshPacket.xeddsa_signed (22) -> MessageEntity.xeddsaSigned
  (gated on broadcast addr); NodeInfo.has_xeddsa_signed (14) ->
  NodeInfoEntity.hasXeddsaSigned (latched, "persists / >=1 verified")
  across all node-info ingest paths.
- Consolidated the bubble corner badges (lock/shield/envelope) into one
  side-by-side row so multiple badges no longer overlap.

Protobuf note: the two fields are hand-added to the generated
mesh.pb.swift because the pinned protobufs submodule predates 2.8.
Bump the submodule to a version carrying these fields before merge;
gen_protos.sh against the current pin would erase the hand-edit.

Closes meshtastic#1992
…-ui-1992

# Conflicts:
#	MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift
Update docs/user/messages.md (signing shield + Message Details copy),
docs/user/nodes.md (Signed node row), and whats-new; regenerate the
bundled in-app HTML docs via scripts/build-docs.sh.
Adds behavioral tests driving the actual MeshPackets ingestion paths:
- nodeInfoPacket sets/leaves NodeInfoEntity.hasXeddsaSigned and latches it
  across updates (a later NodeInfo omitting the bit must not downgrade).
- textMessageAppPacket sets MessageEntity.xeddsaSigned on a signed
  broadcast but never on a signed direct message (DM-safety gate).

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

Adds end-to-end support for surfacing radio-verified XEdDSA signing (signed/verified broadcasts only) across persistence, ingestion, UI affordances, tests, and user docs.

Changes:

  • Persist XEdDSA signing flags onto MessageEntity (per-message) and NodeInfoEntity (node-level, latched) during packet ingestion.
  • Update Messages UI (bubble corner badges + Message Details context) and Node Detail UI to affirm verified signing state.
  • Add Swift Testing coverage plus regenerated/updated user documentation and docs index metadata.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
MeshtasticTests/XEdDSASigningTests.swift Adds wire round-trip + ingestion tests for signing flags.
Meshtastic/Views/Nodes/Helpers/NodeDetail.swift Shows “Signed node / Verified automatically” row when observed.
Meshtastic/Views/Messages/MessageText.swift Adds shield badge and fixes corner badge overlap by laying out side-by-side.
Meshtastic/Views/Messages/MessageContextMenuItems.swift Adds “Signed · verified” + explanatory line in message details.
Meshtastic/Resources/docs/user/whats-new.html Documents new signing UI in What’s New (HTML bundle).
Meshtastic/Resources/docs/user/nodes.html Documents Signed Node behavior (HTML bundle).
Meshtastic/Resources/docs/user/messages.html Documents signing vs encryption distinction (HTML bundle).
Meshtastic/Resources/docs/markdown/user/whats-new.md What’s New entry (bundled markdown source).
Meshtastic/Resources/docs/markdown/user/nodes.md Signed Node section (bundled markdown source).
Meshtastic/Resources/docs/markdown/user/messages.md Signing section (bundled markdown source).
Meshtastic/Resources/docs/index.json Updates docs search index metadata/keywords/char counts.
Meshtastic/Persistence/UpdateSwiftData.swift Persists/latches hasXeddsaSigned during NodeInfo upsert paths.
Meshtastic/Model/NodeInfoEntity.swift Adds hasXeddsaSigned SwiftData property.
Meshtastic/Model/MessageEntity.swift Adds xeddsaSigned SwiftData property.
Meshtastic/Helpers/MeshPackets.swift Sets xeddsaSigned for broadcasts only; persists/latches node flag.
Meshtastic.xcodeproj/project.pbxproj Adds new test file to the test target.
Localizable.xcstrings Adds new localized string keys for signing UI copy.
docs/user/whats-new.md Updates user docs (repo markdown source).
docs/user/nodes.md Updates user docs (repo markdown source).
docs/user/messages.md Updates user docs (repo markdown source).

Comment thread Localizable.xcstrings
Comment on lines +4 to +8
// Covers the XEdDSA packet-signing flags surfaced in the UI (design#113 / issue #1992):
// - MeshPacket.xeddsa_signed (field 22) → MessageEntity.xeddsaSigned
// - NodeInfo.has_xeddsa_signed (field 14) → NodeInfoEntity.hasXeddsaSigned
// The protobuf fields are hand-added to the generated sources, so these tests also guard the
// binary wire round-trip for both fields.
Comment thread MeshtasticTests/XEdDSASigningTests.swift Outdated
Comment thread Meshtastic.xcodeproj/project.pbxproj
Comment thread Meshtastic/Views/Nodes/Helpers/NodeDetail.swift
- Add node-row signing shield to NodeListItem + NodeListItemCompact so the
  checkmark.shield.fill ("Signed node") appears in the Nodes list rows
  alongside the other security icons, fulfilling issue meshtastic#1992 (shield was
  previously only in Node Detail and Messages UI).
- Give the four new Strings Catalog entries explicit `en` localization units
  instead of empty objects, so they're tracked for translation.
- Replace the non-hex pbxproj object IDs for XEdDSASigningTests.swift
  (…XEDDSA…) with valid 24-char hex IDs.
- Reword test header/MARK comments that wrongly described the protobuf fields
  as hand-edited generated code; they come from the upstream 2.8 protos.
@bruschill

Copy link
Copy Markdown
Contributor Author

@thebentern all cleaned up 👍🏻

…-ui-1992

# Conflicts:
#	Meshtastic.xcodeproj/project.pbxproj
#	Meshtastic/Resources/docs/index.json
#	Meshtastic/Resources/docs/markdown/user/whats-new.md
#	Meshtastic/Resources/docs/user/whats-new.html
#	docs/user/whats-new.md
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

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: deb6d9ef-6f9c-4404-bca9-dba8233e43b7

📥 Commits

Reviewing files that changed from the base of the PR and between 34c79fa and 76081ca.

📒 Files selected for processing (7)
  • Meshtastic/Helpers/MeshPackets.swift
  • Meshtastic/Resources/docs/markdown/user/messages.md
  • Meshtastic/Resources/docs/user/messages.html
  • Meshtastic/Views/Nodes/Helpers/NodeListItem.swift
  • Meshtastic/Views/Nodes/Helpers/NodeListItemCompact.swift
  • MeshtasticTests/XEdDSASigningTests.swift
  • docs/user/messages.md
✅ Files skipped from review due to trivial changes (2)
  • Meshtastic/Resources/docs/markdown/user/messages.md
  • Meshtastic/Resources/docs/user/messages.html
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/user/messages.md
  • MeshtasticTests/XEdDSASigningTests.swift

📝 Walkthrough

Walkthrough

Adds XEdDSA signing state to message and node models, persists it from incoming packets, displays signed indicators in message and node UI, and updates tests, localized strings, and docs.

Changes

XEdDSA Signing UI

Layer / File(s) Summary
Model fields
Meshtastic/Model/MessageEntity.swift, Meshtastic/Model/NodeInfoEntity.swift
Adds xeddsaSigned and hasXeddsaSigned, both defaulting to false.
Ingestion and persistence
Meshtastic/Helpers/MeshPackets.swift, Meshtastic/Persistence/UpdateSwiftData.swift
Persists node signing state with latch semantics and sets message signing state only for broadcast-like packets.
Message signing UI
Meshtastic/Views/Messages/MessageText.swift, Meshtastic/Views/Messages/MessageContextMenuItems.swift
Renders a shield badge for signed messages and adds signed-message detail text in the context menu.
Node signing UI
Meshtastic/Views/Nodes/Helpers/NodeDetail.swift, Meshtastic/Views/Nodes/Helpers/NodeListItem.swift, Meshtastic/Views/Nodes/Helpers/NodeListItemCompact.swift
Shows signed-node indicators in node detail and list views, including accessibility and layout updates.
Tests and project wiring
MeshtasticTests/XEdDSASigningTests.swift, Meshtastic.xcodeproj/project.pbxproj
Adds protobuf, SwiftData, and ingestion tests, and wires the new test file into the test target.
Strings and documentation
Localizable.xcstrings, Meshtastic/Resources/docs/..., docs/user/...
Adds localized signing strings and updates message/node docs, changelog entries, and search metadata.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Poem

🐰 A green shield hops into view,
For signed broadcasts and nodes anew.
The lock stays lock, the badge stays bright,
Verified by radio, shining right.
Hops and whiskers, all serene —
XEdDSA sparkles soft and green.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.04% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately highlights the main XEdDSA signing UI change across nodes and messages.
Description check ✅ Passed The description follows the template with What changed, Why, How tested, Screenshots, and Checklist sections filled in.
Linked Issues check ✅ Passed The changes implement the requested broadcast shield, signed-node UI, node list indicators, and affirmative message details, with broadcast-only gating and silent unsigned state.
Out of Scope Changes check ✅ Passed The added docs, tests, localization, and project wiring directly support the XEdDSA feature and no unrelated changes stand out.

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

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 5

🤖 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/user/messages.md`:
- Line 80: The user-facing text is attributing “Signed · verified” to the wrong
UI surface. Update the copy in the message docs so it reflects that
MessageContextMenuItems shows “Signed · verified” in the top-level context menu,
while Message Details only contains “Verified with the sender's key.” Keep the
rest of the guidance about broadcast/channel messages and direct messages
aligned with that distinction.

In `@Meshtastic/Helpers/MeshPackets.swift`:
- Around line 1142-1145: The signing gate in MeshPackets.swift is too strict
because it only treats packets addressed to Constants.maximumNodeNum as
broadcasts, while the same receive path later already recognizes
storeForwardBroadcast as a broadcast-like message. Update the xeddsaSigned
assignment in the received packet handling logic to include the
storeForwardBroadcast case alongside the broadcast address check, using the
existing packet classification in the same method so valid signed
store-and-forward broadcasts keep their verified state.

In `@Meshtastic/Views/Nodes/Helpers/NodeListItem.swift`:
- Around line 220-226: The signed-node trust signal is only shown visually in
NodeListItem, so VoiceOver misses it because the row uses
accessibilityDescription(...) as its custom label. Update the accessibility text
path used by NodeListItem (and any shared helper it relies on) to append
node.hasXeddsaSigned with a clear “Signed node” or equivalent phrase, matching
the existing shield icon state so the spoken output mirrors the visual row.

In `@Meshtastic/Views/Nodes/Helpers/NodeListItemCompact.swift`:
- Around line 217-223: The compact row height logic in NodeListItemCompact is
missing the new signed-node display line, so signed nodes can be sized too
short. Update the height calculation that derives circleSize from lineNums to
also account for the node.hasXeddsaSigned branch, keeping it in sync with the
rendered rows in NodeListItemCompact and IconAndText so compact mode reserves
space whenever the “Signed node” row appears.

In `@MeshtasticTests/XEdDSASigningTests.swift`:
- Around line 21-58: The current tests only verify self round-tripping, so they
can miss protobuf wire-format regressions. Update the MeshPacket and NodeInfo
tests to assert the raw serialized bytes produced by `serializedData()` for the
xeddsaSigned fields, specifically checking the expected field tags for field 22
and field 14, and verify the default-false cases emit no payload on the wire.
Use the existing `MeshPacket`, `NodeInfo`, `serializedData()`, and
`serializedData(serializedData:)` paths to compare exact emitted bytes rather
than relying only on encode→decode assertions.
🪄 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: 0f72e673-dc3b-4833-9dbc-10293ef83f12

📥 Commits

Reviewing files that changed from the base of the PR and between b9615b1 and 34c79fa.

📒 Files selected for processing (22)
  • Localizable.xcstrings
  • Meshtastic.xcodeproj/project.pbxproj
  • Meshtastic/Helpers/MeshPackets.swift
  • Meshtastic/Model/MessageEntity.swift
  • Meshtastic/Model/NodeInfoEntity.swift
  • Meshtastic/Persistence/UpdateSwiftData.swift
  • Meshtastic/Resources/docs/index.json
  • Meshtastic/Resources/docs/markdown/user/messages.md
  • Meshtastic/Resources/docs/markdown/user/nodes.md
  • Meshtastic/Resources/docs/markdown/user/whats-new.md
  • Meshtastic/Resources/docs/user/messages.html
  • Meshtastic/Resources/docs/user/nodes.html
  • Meshtastic/Resources/docs/user/whats-new.html
  • Meshtastic/Views/Messages/MessageContextMenuItems.swift
  • Meshtastic/Views/Messages/MessageText.swift
  • Meshtastic/Views/Nodes/Helpers/NodeDetail.swift
  • Meshtastic/Views/Nodes/Helpers/NodeListItem.swift
  • Meshtastic/Views/Nodes/Helpers/NodeListItemCompact.swift
  • MeshtasticTests/XEdDSASigningTests.swift
  • docs/user/messages.md
  • docs/user/nodes.md
  • docs/user/whats-new.md

Comment thread docs/user/messages.md Outdated
Comment thread Meshtastic/Helpers/MeshPackets.swift Outdated
Comment thread Meshtastic/Views/Nodes/Helpers/NodeListItem.swift
Comment thread Meshtastic/Views/Nodes/Helpers/NodeListItemCompact.swift
Comment thread MeshtasticTests/XEdDSASigningTests.swift
CodeRabbit feedback:
- Treat store-and-forward router broadcasts as broadcasts in the xeddsaSigned
  gate so a valid signed S&F broadcast keeps its verified shield (they are
  addressed to the local node but classified as channel broadcasts).
- Announce the 'Signed node' trust signal to VoiceOver in the node list
  (NodeListItem) and the compact list (NodeListItemCompact), mirroring the shield.
- Count the signed-node row in the compact avatar height (lineNums).
- Correct the docs: 'Signed · verified' is the context-menu label; Message
  Details only adds 'Verified with the sender's key.'
- Assert the raw protobuf wire bytes for MeshPacket field 22 / NodeInfo field 14
  and their default omission, not just self round-trips.

Also (code-review): classify S&F broadcasts as broadcasts in the unified-log
redaction too (was logged as '(DM)'); add an S&F signed-broadcast regression
test; dedupe the test packet scaffold.

Addresses CodeRabbit + code-review feedback on meshtastic#1992.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[2.8.0] XEdDSA packet signing UI

2 participants