Skip to content

feat(messages): surface XEdDSA packet signing status (firmware 2.8+) (#3923)#3940

Draft
Yeraze wants to merge 1 commit into
mainfrom
feat/3923-xeddsa-signing
Draft

feat(messages): surface XEdDSA packet signing status (firmware 2.8+) (#3923)#3940
Yeraze wants to merge 1 commit into
mainfrom
feat/3923-xeddsa-signing

Conversation

@Yeraze

@Yeraze Yeraze commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Closes #3923

Summary

Firmware 2.8 adds XEdDSA broadcast signing. The official mobile clients show a verified-signature shield for signed broadcasts in message details; MeshMonitor exposed nothing. This surfaces the signing status end-to-end and renders a green shield 🛡️ wherever per-message metadata is displayed.

What the field is

MeshPacket.xeddsa_signed (field 22, firmware 2.8+) — set by the receiving node when it cryptographically verified the broadcast's signature. Absent/false on pre-2.8 firmware and unsigned traffic.

Changes

Protobuf

  • Bump the protobufs submodule to master HEAD. The pinned tag (v2.7.26) predates the field and no release tag carries it yet — it only exists on origin/master. Protobufs are parsed at runtime (protobufLoader.ts), so the bump alone makes the wire field decodable (verified with a round-trip encode/decode). Added xeddsaSigned to the hand-written MeshPacket TS interface.

Backend

  • Read xeddsaSigned off the decoded packet in the text-message build path (defensive camelCase + snake_case read), threaded through TextMessage / DbMessage (repo + facade) / MeshMessage.
  • Migration 113 adds a nullable xeddsaSigned boolean to the messages table across SQLite / PostgreSQL / MySQL, mirroring the existing viaMqtt / viaStoreForward pattern. Registered in the migration registry (count 112 → 113).
  • Serialized in both DB→MeshMessage paths (server.ts transformDbMessageToMeshMessage and getRecentMessages) plus the unified per-reception view, so the flag survives reloads. Realtime is covered by the built message object emitted over WebSocket.

Frontend

  • HopCountDisplay renders a green 🛡️ shield when xeddsaSigned is true, alongside the existing MQTT/S&F indicators (wired in MessagesTab ×2 and ChannelsTab).
  • UnifiedMessagesPage reception-stats modal shows the shield per signed reception.
  • i18n key messages.xeddsa_signed (English) with an inline fallback string.

Scope note

This PR covers the packet-level xeddsa_signed flag (the "green shield for verified signed broadcasts"). The separate node-level User.has_xeddsa_signed ("this node signs its packets") is a natural follow-up for the node-detail view and is intentionally out of scope here to keep the change focused.

Testing

  • HopCountDisplay.test.tsx: shield renders for true / hidden for false/undefined / co-exists with MQTT + S&F indicators / appears with hop count and with SNR-RSSI direct-message rendering.
  • messages.insert.test.ts: round-trip persistence of the column (stored as 1 / NULL; read back as true via the async path).
  • migrations.test.ts: registry count and last-migration assertions updated to 113.
  • Full Vitest suite: 0 failures (success: true, 8623 tests). Server tsc -p tsconfig.server.json clean (no new errors; only the ~pre-existing TelemetryChart ones remain). vite build OK. ESLint 0 errors on changed files.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n

…3923)

Firmware 2.8 adds XEdDSA broadcast signing. The official mobile clients
show a verified-signature shield in message details; MeshMonitor exposed
nothing. This adds the flag end-to-end and renders a green shield 🛡️
wherever per-message metadata is shown.

- Bump the `protobufs` submodule to master HEAD (v2.7.26 predates the
  field; no release tag carries it yet) to expose `MeshPacket.xeddsa_signed`
  (field 22). Protobufs are parsed at runtime, so the bump alone makes the
  wire field decodable.
- Read `xeddsaSigned` off the decoded packet in the message-build path
  (camelCase + snake_case defensive read) and thread it through the
  TextMessage / DbMessage / MeshMessage types.
- Persist it on the `messages` table via migration 113 across all three
  backends (SQLite/PostgreSQL/MySQL), mirroring the viaMqtt/viaStoreForward
  nullable-boolean pattern.
- Serialize it in both DB->MeshMessage paths (server.ts + getRecentMessages)
  and the unified per-reception view so it survives reloads, plus realtime
  WebSocket via the built message object.
- Render a shield in HopCountDisplay (MessagesTab/ChannelsTab) and in the
  UnifiedMessagesPage reception-stats modal. i18n key with English fallback.

Tests: HopCountDisplay shield rendering, message repository round-trip
persistence, migration registry count (112 -> 113). Full Vitest suite
passes (0 failures); server tsc clean (no new errors); vite build OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: feat(messages): surface XEdDSA packet signing status (firmware 2.8+)

  • Gather context and understand the PR
  • Review protobuf and backend changes
  • Review database schema and migration
  • Review frontend changes
  • Review test coverage
  • Post final review feedback

Overall this is a clean, well-structured feature addition that follows the project's existing patterns faithfully. The implementation is low-risk (additive, nullable column; no behavioral changes to existing paths). A few observations below.

Protobuf / decode path

src/server/meshtasticManager.ts:5878 — The dual-read approach for camelCase + snake_case is reasonable defensively, but protobufjs consistently outputs camelCase for field names (the snake_case variant xeddsa_signed would only appear if someone does a raw JSON conversion). Since the field is already defined in the TS interface as xeddsaSigned, the || (meshPacket as any).xeddsa_signed === true guard is dead code in practice. It doesn't hurt to keep it, but it's worth noting in case the team prefers minimal any-casts.

Backend

src/server/meshtasticManager.ts:14294 and src/server/server.ts:2517 — Both transformDbMessageToMeshMessage paths use (msg as any).xeddsaSigned ? true : undefined. This works correctly; the as any cast is necessary because the DbMessage type that flows in at those points may not yet have the new field surfaced as a known property. Consider adding the field to the shared DbMessage interface in src/db/types.ts directly (it's already there at line 131) to eliminate the any cast — the Drizzle-inferred type from insertMessage will have the column, but the transform receives a broader DbMessage which already has the field typed, so the cast may not actually be needed. Minor.

src/server/routes/unifiedRoutes.ts:770(m as any).xeddsaSigned ?? null — same observation; m here is the result of a repository query so the field should be present in the inferred type. Low risk, but worth cleaning up if the broader any-reduction effort ever lands.

Database / migration

Migration 113 is idempotent across all three backends and follows the exact pattern of the prior viaMqtt/viaStoreForward migrations. No issues.

src/db/schema/messages.ts — The column is correctly placed between viaStoreForward and rxSnr in the SQLite schema but between viaStoreForward and rxSnr in Postgres/MySQL as well. Consistent. All three backends use nullable boolean — correct.

One subtle concern: The MySQL information_schema check in migration 113 (src/server/migrations/113_add_xeddsa_signed.ts:31) doesn't filter by TABLE_SCHEMA. On a MySQL server where multiple databases share the same instance, this query could match a messages.xeddsaSigned column in a different database and skip the migration silently. The fix is to add AND TABLE_SCHEMA = DATABASE(). The existing migrations (e.g. viaMqtt, viaStoreForward) appear to have the same issue, so this is a pre-existing pattern rather than something introduced here, but it's worth flagging.

Frontend

src/components/HopCountDisplay.tsx:39-48 — The shield indicator is rendered as a <span role="img"> with both title and aria-label. The title tooltip will work correctly. The style={{ color: 'var(--success-color, #16a34a)' }} correctly falls back to an accessible green if the CSS variable isn't defined.

src/pages/UnifiedMessagesPage.tsx:675-684 — The inline shield in the receptions modal is a near-copy of the HopCountDisplay version but without role="img" or aria-label. This means the shield in the modal is invisible to screen readers. Consider extracting a shared XeddsaSignedBadge component to avoid the divergence.

Indicator ordering in HopCountDisplay.tsx — the shield renders before S&F and MQTT indicators (SignedIndicator → StoreForwardIndicator → MqttIndicator). This is a reasonable choice (authenticity before transport flags), but it differs from what the PR description implies ("alongside the existing MQTT/S&F indicators"). No functional issue; just a visual ordering decision that could use a brief comment if the team cares about canonical ordering.

Test coverage

HopCountDisplay.test.tsx — Good coverage of: shield shown/hidden for true/false/undefined, co-existence with MQTT + S&F, shield with hop count, shield with SNR/RSSI. Solid.

messages.insert.test.ts:93-112 — Round-trips both the 1/NULL SQLite storage and the true read-back via the async repo path. Exactly what's needed.

migrations.test.ts — Count and last-migration name updated correctly.

Gap: There are no tests covering transformDbMessageToMeshMessage with xeddsaSigned = true, and no test for the unified-route reception serialization. These paths are simple enough that the risk is low, but if a future refactor accidentally drops the field from those two transforms, no test would catch it. Consider adding a minimal assertion in the existing transform unit tests if they exist.

Minor nits

  • The i18n key messages.xeddsa_signed is added to en.json correctly. The inline fallback string 'Cryptographically signed (XEdDSA)' in the t() calls is good practice for the test environment where i18n isn't initialized.
  • src/server/protobufLoader.ts — the JSDoc for xeddsaSigned is clear and accurate.
  • No CLAUDE.md invariants are violated: no raw SQL outside repositories, per-source scoping is not affected (this is a per-message flag on an existing per-source table), VALID_SETTINGS_KEYS is not involved.

Summary

Approve with minor suggestions. The only actionable items worth addressing before merge:

  1. TABLE_SCHEMA = DATABASE() filter in the MySQL migration (low risk, pre-existing pattern, but easy to fix).
  2. Accessibility: add role="img" and aria-label to the inline shield in UnifiedMessagesPage.tsx:675-684.
  3. Optionally: extract a shared XeddsaSignedBadge component to avoid the divergence between the two shield renderings.

Items 1 and 2 are the only ones I'd consider blocking. The rest are polish.

@Yeraze Yeraze marked this pull request as draft July 5, 2026 20:07
@Yeraze

Yeraze commented Jul 5, 2026

Copy link
Copy Markdown
Owner Author

⏸️ Deferred pending an official protobufs release.

This PR is complete and green, but it requires bumping the vendored protobufs submodule from tag v2.7.26 → untagged master HEAD, because MeshPacket.xeddsa_signed (field 22) exists only on protobufs master — no tagged release carries it yet.

Per maintainer decision, we're holding this rather than shipping bleeding-edge protos. Unblock trigger: the next tagged meshtastic/protobufs release that includes xeddsa_signed. At that point: re-pin the submodule to that tag (instead of master HEAD), re-run the suite + system tests, and un-draft.

Marking as draft to prevent accidental merge. All other work (migration 113, decode→DB→UI threading, 🛡️ shield, i18n, tests) is done and preserved on branch feat/3923-xeddsa-signing.

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.

Feature: display packet signing status (XEdDSA, firmware 2.8+)

1 participant