Skip to content

[2.8.0] - XEdDSA Public Key SigningΒ #113

Description

@jamesarich

πŸ›‘οΈ XEdDSA packet signing β€” client UI spec

Important

Show when a node and its broadcasts are cryptographically signed and verified. The radio does all the crypto and drops bad signatures before the client ever sees them β€” so there's no error state to render. Clients only ever affirm the good state: πŸ›‘οΈ shield = authentic (signed broadcast), πŸ”’ lock = private (encrypted DM). Two separate ideas, two separate marks. Never warn on unsigned traffic.

Tracking: design#113 Β· Firmware: firmware#10478 (2.8) Β· Reference build: Meshtastic-Android#5976 (merged) + #5980 + #5985

What it looks like (Android reference)

Messaging β€” per-message shield Node details β€” "Signed node" Node list β€” "Signed node"
Signed broadcast bubble with shield-check Node details with Signed node row Signed node in a list row
Top bubble (14:02) verified β€” green πŸ›‘οΈ; bottom (14:03) unsigned, no shield. "Signed node Β· Verified automatically" in the security section, paired with the manually-verified cell. Shield beside the key/lock status icon in the row β€” visible without opening details.

Trust states at a glance

State Detect Mark Label
βœ… Verified signed xeddsa_signed == true πŸ›‘οΈ shield-with-check (affirmative) "Signed"
βšͺ Unsigned no verified signature β€” (render nothing) β€”
πŸ”’ Encrypted DM PKI (separate mechanism) existing lock "Encrypted"

Goal

Show users when a node and its broadcast packets are cryptographically signed and verified (XEdDSA over the node's identity key) β€” primarily in node details and the messaging view.

Guiding principle

Affirm the good state; stay silent otherwise. The radio drops invalid and stripped-signature broadcasts from a known signer before the client sees them, so there is no "tampered/invalid" state to render. A small broadcast you can see from a signing node is verified by construction. We only ever say "this is signed and verified" β€” we never warn, flag, or downgrade unsigned traffic.

What the radio gives the client

Read-only flags β€” no client-side crypto.

Field Type Meaning
MeshPacket.xeddsa_signed bool (22) Verified signature for this received packet. Primary "verified" signal.
NodeInfo.has_xeddsa_signed bool (14) This node signs its packets (β‰₯1 verified). Node-level signal.
Data.xeddsa_signature bytes[64] (10) The raw signature. Not needed for v1 (see decisions).

What is signed: unencrypted broadcasts that fit a 64-byte signature β€” NodeInfo, position, telemetry, channel text. The signature covers from | id | portnum | payload.
Not signed: DMs (those are PKI-encrypted β€” a separate mechanism) and oversized broadcasts.

Two distinct affordances β€” shield vs lock

Signing β‰  encryption. They answer different questions ("who really sent this" vs "who can read this") and apply to disjoint traffic, so they stay separate:

  • πŸ›‘οΈ shield = authentic (a verified, signed broadcast)
  • πŸ”’ lock = private (a PKI-encrypted DM)

Use a shield-with-check mark for signing β€” each platform picks its nearest native equivalent (Android used the Material Symbols verified_user glyph). Leave the existing DM lock untouched.

Caution

Never reuse the lock for signing. Never use red / error styling anywhere in this feature β€” there is no error state.

Node details & node list

  • Signed node: when has_xeddsa_signed, show a "Signed node" πŸ›‘οΈ shield. Because NodeInfo is itself a signed broadcast, the node's name/identity is verified by extension.
  • Node details: slot a "Signed node" row into the existing key/security section, ordered most-trusted-first: manual-verified β†’ signed β†’ has-key β†’ no-key. Keep it a distinct cell from manual key verification (Android pairs it with the manually-verified cell in one row; stack or pair as suits the platform) β€” the two are earned differently. Signing is automatic trust (observed from the radio); manual verification is user-asserted. Label it accordingly (e.g. "Verified automatically").
  • Node list (rows): also show the shield in each node row's security cluster, right beside the key/lock status icon, so signing is visible at a glance without opening details. Apply to every list density (compact + full). Group it with the key icon as one affordance rather than a scattered separate badge.
  • Phrase it as observed ("signs its packets" / "Signed node"), never as a configurable setting ("signing enabled") β€” there is no such advertised flag.
  • Tap to explain: make the shield tappable (mirroring the key-status icon), opening a plain-language explanation. Suggested copy β€” title "Signed node", body "This node signs its broadcasts with XEdDSA. Broadcasts you see from it are verified by the radio using its identity key."
  • Copy: row label "Signed node", value "Verified automatically"; per-message reveal "Signed Β· verified"; message detail "Signed & verified β€” verified with their key." Keep wording consistent across clients.

Note

The shield sits on node-tinted surfaces (bubbles, rows) β€” make sure it stays legible against them (each platform's own way).

Messaging view

  • Channel/broadcast messages: a green πŸ›‘οΈ on xeddsa_signed bubbles; reveal "Signed Β· verified" on tap. Per-message β€” a node may send a signed small message and an unsigned oversized one in the same channel.
  • Direct messages: keep the existing πŸ”’; do not show the signing shield. Lock = private (DM); shield = authentic (signed broadcast).

Tip

The firmware only sets xeddsa_signed on broadcasts and never on DMs, so gating the shield on the boolean alone is sufficient β€” clients don't need to detect broadcast-vs-DM in the UI. It cannot false-positive on a DM.

  • Message detail/info: plain-language, affirmative state β€” e.g. "Signed & verified β€” sent by Alice (!a1b2c3d4), verified with their key." For unsigned, use neutral copy ("not signed β€” older firmware or too large") and never anything accusatory.

Resolved decisions (v1)

The original open questions are settled for v1:

  1. Signing vs encryption language β†’ two separate affordances (shield + lock), not a merged "security" concept.
  2. Node trust state β†’ two distinct cells (manual key-verify and signature-verify), most-trusted-first and laid out per platform (Android pairs them in one row); don't conflate them.
  3. Per-data-field signed badges (badging individual position/telemetry/sensor rows) β†’ deferred. The node-level "Signed node" row already conveys trust; the per-field visual cost wasn't justified for v1. Revisit on demand.
  4. Treatment for "unsigned" β†’ strictly silent. No icon, no label, no neutral chip.
  5. Compose-time "will be signed" indicator β†’ out for v1.
  6. "Signed but unverifiable" state (signature present, xeddsa_signed == false because the radio lacked the key) β†’ dropped for v1. It's rare and transient β€” it resolves once the key is known. Render trust from the verified boolean only; clients don't need to read Data.xeddsa_signature yet. Revisit only if it proves non-transient in the field.

Don'ts

  • Don't reuse the lock icon for signing.
  • Don't use red / error styling anywhere β€” nothing here is an error.
  • Don't flag unsigned DMs / large / legacy traffic as insecure.
  • Don't phrase node UI as "signing enabled"; phrase it as observed ("signs its packets").

Per-client status

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    Ready

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions