Skip to content

feat(signing): Tron/TON/Solana message signing via vault REST (firmware 7.14.1+)#56

Merged
BitHighlander merged 2 commits intodevelopfrom
feat/message-signing
Apr 30, 2026
Merged

feat(signing): Tron/TON/Solana message signing via vault REST (firmware 7.14.1+)#56
BitHighlander merged 2 commits intodevelopfrom
feat/message-signing

Conversation

@BitHighlander
Copy link
Copy Markdown
Collaborator

@BitHighlander BitHighlander commented Apr 30, 2026

Summary

Wires the 5 message-signing endpoints landed in vault commit 7e6dfc9:

Endpoint Purpose
POST /tron/sign-message TIP-191 personal_sign
POST /tron/verify-message TIP-191 verify (off-device)
POST /tron/sign-typed-hash TIP-712 hash mode
POST /ton/sign-message bare Ed25519 (AdvancedMode-gated)
POST /solana/sign-offchain-message domain-separated envelope

Replaces the not yet supported throw at tronHandler.ts:702 and adds parallel surfaces on TON and Solana.

Firmware gate

New chrome-extension/src/background/firmware.ts enforces a 7.14.1 minimum on every new method. 7.14.0 was an internal stop-gap that never shipped publicly — treat it as nonexistent so anyone on it sees a clean upgrade prompt instead of a Failure_UnknownMessage round-trip. Same prompt for 7.13.x and earlier.

Per-chain notes

Tron

  • tronWeb.trx.signMessage (V1, hex) and tronWeb.trx.signMessageV2 (V2, UTF-8) now route to the backend instead of throwing — only the wire encoding of the message bytes differs between V1 and V2.
  • tron_verifyMessage is reachable only via tronLink.request({ method: 'tron_verifyMessage', params: [{ address, signature, message, isText? }] }). We deliberately do NOT expose verifyMessage / verifyMessageV2 on tronWeb.trx: TronWeb V2's contract is verifyMessageV2(message, sig) returning the recovered base58 address, and our endpoint shape (address required, boolean returned) doesn't match. Verification is client-side anyway — TronWeb's static utilities cover it.
  • tron_signTypedHash is hash-only. Caller pre-computes the two 32-byte hashes per the TIP-712 spec and passes them in. We do NOT ship a struct → hash implementation, so we do NOT expose _signTypedData / signTypedData on tronWeb.trx where the contract takes (domain, types, value).

TON — Firmware fences ton_signMessage behind the AdvancedMode policy. When disabled, the vault returns a 4xx with AdvancedMode in the body; the handler rewrites that into an actionable prompt (Open Vault → Settings → Security and enable Advanced Mode).

Solanasolana_signOffchainMessage is a NEW method exposed to dApps via a vendor-namespaced Wallet Standard feature, keepkey:signOffchainMessage. dApps feature-detect:

const f = wallet.features['keepkey:signOffchainMessage'];
if (f) await f.signOffchainMessage({ message, version, messageFormat });

solana_signMessage (Wallet Standard bare-message) is unchanged. The signature returned by the off-chain method is over the envelope:

"\xff" || "solana offchain" || version (1B) || format (1B) || length (2B LE) || message

Verifiers MUST reconstruct the envelope and Ed25519-verify against THAT, not the bare bytes. The approval event carries a verifyAgainst: 'envelope' hint for any future approval-card UI. Defaults: version 0, messageFormat 1 (UTF-8); 1212-byte cap matches firmware.

Test plan

  • Tron: tronWeb.trx.signMessageV2('hello') from a dApp prompts approval and returns a 0x-prefixed 65-byte signature
  • Tron: tronWeb.trx.signMessage('0xdeadbeef') (V1, hex) prompts approval and returns a 0x-prefixed 65-byte signature
  • Tron: tronLink.request({ method: 'tron_verifyMessage', params: [{ address, signature, message, isText: true }] }) returns true for a valid round-trip and false for a tampered message
  • Tron: tronLink.request({ method: 'tron_signTypedHash', params: [{ domainSeparatorHash, messageHash }] }) with two 32-byte hex hashes signs and returns a recoverable signature
  • Tron: confirm tronWeb.trx._signTypedData and tronWeb.trx.verifyMessageV2 are intentionally absent (no compat claim)
  • TON: ton_signMessage with AdvancedMode disabled surfaces the "enable Advanced Mode" prompt (no opaque 4xx)
  • TON: ton_signMessage with AdvancedMode enabled signs and returns { publicKey, signature }
  • Solana: wallet.features['keepkey:signOffchainMessage'].signOffchainMessage({ message: 'hello' }) returns { publicKey, signature }; verifying against the bare message FAILS, verifying against the reconstructed envelope succeeds
  • Firmware 7.14.0 device hits the upgrade prompt for all 5 methods (manual: temporarily downgrade or hardcode)
  • Type-check + build clean (verified locally; CI lint failure is pre-existing repo debt, out of scope)

🤖 Generated with Claude Code

…re 7.14.1+)

Wire the 5 message-signing endpoints landed in vault commit 7e6dfc9:

  POST /tron/sign-message       (TIP-191 personal_sign)
  POST /tron/verify-message     (TIP-191 verify)
  POST /tron/sign-typed-hash    (TIP-712 hash mode)
  POST /ton/sign-message        (bare Ed25519, AdvancedMode-gated)
  POST /solana/sign-offchain-message (domain-separated envelope)

Replaces the "not yet supported" throw at tronHandler.ts:702 and adds
parallel surfaces on TON and Solana.

New: chrome-extension/src/background/firmware.ts — shared 7.14.1+ gate
called by every new handler before going to the device. 7.14.0 was an
internal stop-gap that never shipped publicly; treat the minimum as
7.14.1 so users on 7.14.0 see a clean upgrade prompt instead of a
Failure_UnknownMessage round-trip. Earlier firmware (7.13.x and below)
gets the same upgrade prompt.

Tron handler:
  - tron_signMessage / signMessage (V1, hex) and signMessageV2 (V2,
    UTF-8): both go through /tron/sign-message; V1 vs V2 only differ in
    wire encoding of the message bytes. Returns 0x-prefixed 65-byte
    recoverable signature to match TronWeb's expected return shape.
  - tron_verifyMessage / verifyMessage / verifyMessageV2: pure utility,
    doesn't touch the device — vault recovers the signer off-device.
  - tron_signTypedHash / _signTypedData: TIP-712 hash mode only. Caller
    pre-computes the 32-byte domainSeparator + message hashes; we don't
    ship a struct → hash implementation. dApps using TronWeb's
    _signTypedData should hash client-side then call this method.

Injected tron-provider.ts: signMessage / signMessageV2 /
verifyMessage / verifyMessageV2 now route to the backend instead of
throwing.

TON handler: ton_signMessage / signMessage. Firmware fences this behind
the AdvancedMode policy — when disabled, the vault returns a 4xx with
"AdvancedMode" in the body. Detect that and rewrite the error into an
actionable prompt ("Open Vault → Settings → Security and enable
Advanced Mode") instead of surfacing the raw HTTP failure.

Solana handler: solana_signOffchainMessage as a NEW method (does not
replace solana_signMessage — Wallet Standard expects bare-message
verification). The signature returned here is over the off-chain
envelope:

  "\xff" || "solana offchain" || version (1B) || format (1B)
                              || length (2B LE) || message

Verifiers MUST reconstruct the envelope and Ed25519-verify against
THAT, not the bare message bytes. The handler attaches a
`verifyAgainst: 'envelope'` hint to the approval event so any future
approval-card UI can warn the user about the verification model.
Defaults: version 0, messageFormat 1 (UTF-8). Caps at the firmware's
1212-byte limit.

Type-check: zero new errors introduced (verified vs. baseline on
develop).
…ility

1. Tron verifyMessageV2 was claiming TronWeb V2 compatibility but the
   shapes don't match: TronWeb V2 is verifyMessageV2(message, sig)
   returning the recovered base58 address; our endpoint takes an
   `address` arg and returns boolean. Standard dApps would fail on the
   2-arg call. Fix: drop verifyMessage / verifyMessageV2 from the
   tronWeb.trx shim entirely (verification is client-side anyway —
   TronWeb's static utilities cover it). Drop the verifyMessageV2 case
   alias from the background handler. Keep tron_verifyMessage as a
   non-standard utility reachable only via tronLink.request, with the
   shape changed to a single-object param so the contract is
   self-documenting.

2. The handler aliased _signTypedData to the hash-only API, but
   tronWeb.trx has no _signTypedData method exposed and TronWeb's real
   _signTypedData(domain, types, value) takes the full struct, not
   pre-computed hashes. Fix: drop the _signTypedData alias from the
   handler and from the docs — keep only tron_signTypedHash. Doc
   comment now explicitly notes we don't ship a struct → hash impl.

3. solana_signOffchainMessage was unreachable from page context — the
   Wallet Standard registration only advertised the three solana:*
   features and there's no window.keepkey.solana provider. Fix: add a
   vendor-namespaced 'keepkey:signOffchainMessage' feature to the
   wallet-standard registration. dApps can feature-detect via
   wallet.features['keepkey:signOffchainMessage'] and call
   .signOffchainMessage({ message, version?, messageFormat? }). Solana
   Wallet Standard explicitly reserves non-`solana:` namespaces for
   vendor extensions, so this is the canonical way to expose
   non-standard primitives.
@BitHighlander BitHighlander merged commit e6e84ae into develop Apr 30, 2026
3 of 4 checks passed
@BitHighlander BitHighlander deleted the feat/message-signing branch April 30, 2026 02:29
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.

1 participant