wallet: sign_message: strip whitespaces in GUIs, do not strip in CLI
What changed, and why it matters
This commit tidies up how Electrum handles extra spaces around Bitcoin addresses, messages, and signatures when signing or verifying messages. Previously, different parts of the program (desktop GUI, mobile-style GUI, command line) behaved inconsistently—some trimmed spaces automatically, others did not. The change moves the trimming into the shared wallet code and makes it the default for GUIs, while the command-line tool now explicitly keeps spaces untouched. It is a user-experience and consistency fix, not a patch for an active security vulnerability.
No immediate security action required. Treat as a routine UX/consistency improvement. If auditing, confirm that downstream callers relying on exact whitespace behavior (scripts, plugins, tests) are updated to pass strip_inputs=False where needed.
Security signals we found
Behavior normalization across GUI and CLI interfaces
No cryptographic primitive changed
No privilege boundary crossed
No input validation removed; whitespace handling moved to shared layer
References prior UX issue #4327 and community PRs #10787/#10788
Evidence from the diff
The patch centralizes whitespace stripping for sign_message and verify_message in Abstract_Wallet, adding a strip_inputs parameter defaulting to True. GUI callers now rely on this default, while CLI callers pass strip_inputs=False to preserve exact input. The QML and Qt GUI layers stop doing their own ad-hoc stripping. This resolves long-standing inconsistency noted in issue #4327 and PRs #10787/#10788. There is no cryptographic change; the only behavioral change is whether leading/trailing whitespace is ignored.
Changed components
electrum/wallet.py - Abstract_Wallet.sign_message / verify_messageelectrum/commands.py - CLI signmessage / verifymessageelectrum/gui/qt/main_window.py - Qt sign/verify message dialogselectrum/gui/qml/qewallet.py - QML sign messageelectrum/gui/qml/qedaemon.py - QML verify messageInspect captured patch +27 / −18
diff --git a/electrum/commands.py b/electrum/commands.py
index beaaa43..0cdf457 100644
--- a/electrum/commands.py
+++ b/electrum/commands.py
@@ -918,7 +918,12 @@ class Commands(Logger):
raise UserFacingException(f"address must be a str instead of {type(address)}")
if not isinstance(message, str):
raise UserFacingException(f"message must be a str instead of {type(message)}")
- sig = wallet.sign_message(address=address, message=message, password=password)
+ sig = wallet.sign_message(
+ address=address,
+ message=message,
+ password=password,
+ strip_inputs=False, # respect whitespaces for CLI
+ )
return base64.b64encode(sig).decode('ascii')
@command('')
@@ -935,7 +940,12 @@ class Commands(Logger):
raise UserFacingException(f"signature must be a str instead of {type(signature)}")
if not isinstance(message, str):
raise UserFacingException(f"message must be a str instead of {type(message)}")
- return Abstract_Wallet.verify_message(address=address, signature=signature, message=message)
+ return Abstract_Wallet.verify_message(
+ address=address,
+ signature=signature,
+ message=message,
+ strip_inputs=False, # respect whitespaces for CLI
+ )
def _get_fee_policy(self, fee: str, feerate: str):
if fee is not None and feerate is not None:
diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py
index 749ee31..b0779c1 100644
--- a/electrum/gui/qml/qedaemon.py
+++ b/electrum/gui/qml/qedaemon.py
@@ -506,9 +506,6 @@ class QEDaemon(AuthMixin, QObject):
@pyqtSlot(str, str, str, result=bool)
def verifyMessage(self, address, message, signature):
- address = address.strip()
- message = message.strip()
- signature = signature.strip()
try:
return Abstract_Wallet.verify_message(address=address, signature=signature, message=message)
except UserFacingException as e:
diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py
index 5d3e233..88e602e 100644
--- a/electrum/gui/qml/qewallet.py
+++ b/electrum/gui/qml/qewallet.py
@@ -848,9 +848,6 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
@pyqtSlot(str, str)
@auth_protect(message=_("Sign message?"))
def signMessage(self, address, message):
- # strip, as in qt gui and in qml verifyMessage (see #4327)
- address = address.strip()
- message = message.strip()
try:
sig = self.wallet.sign_message(address=address, message=message, password=self.password)
except UserFacingException as e:
diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
index 20244bb..ffc8da7 100644
--- a/electrum/gui/qt/main_window.py
+++ b/electrum/gui/qt/main_window.py
@@ -2132,12 +2132,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
signature_e: ButtonsTextEdit,
password,
) -> None:
- address = address_e.text().strip()
- message = message_e.toPlainText().strip()
task = partial(
self.wallet.sign_message,
- address=address,
- message=message,
+ address=address_e.text(),
+ message=message_e.toPlainText(),
password=password,
)
@@ -2157,13 +2155,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
message_e: QTextEdit,
signature_e: ButtonsTextEdit,
) -> None:
- address = address_e.text().strip()
- message = message_e.toPlainText().strip()
task = partial(
self.wallet.verify_message,
- address=address,
+ address=address_e.text(),
signature=str(signature_e.toPlainText()),
- message=message,
+ message=message_e.toPlainText(),
)
def on_result(verified):
diff --git a/electrum/wallet.py b/electrum/wallet.py
index 55d8a73..0d22103 100644
--- a/electrum/wallet.py
+++ b/electrum/wallet.py
@@ -3234,10 +3234,14 @@ class Abstract_Wallet(ABC, Logger, EventListener):
def _update_password_for_keystore(self, old_pw: Optional[str], new_pw: Optional[str]) -> None:
pass
- def sign_message(self, *, address: str, message: str, password) -> bytes:
+ def sign_message(self, *, address: str, message: str, password, strip_inputs: bool = True) -> bytes:
"""Caller must handle UserFacingException."""
assert isinstance(address, str), f"address must be str. got {type(address)}"
assert isinstance(message, str), f"message must be str. got {type(message)}"
+ if strip_inputs:
+ # stripping whitespaces leads to better UX for GUIs, but it's counter-productive for CLI
+ address = address.strip()
+ message = message.strip()
if not bitcoin.is_address(address):
raise UserFacingException(_("Invalid Bitcoin address."))
if self.is_watching_only():
@@ -3260,11 +3264,16 @@ class Abstract_Wallet(ABC, Logger, EventListener):
return self.keystore.sign_message(index, message, password, script_type=txin_type)
@classmethod
- def verify_message(cls, *, address: str, signature: str, message: str) -> bool:
+ def verify_message(cls, *, address: str, signature: str, message: str, strip_inputs: bool = True) -> bool:
"""Caller must handle UserFacingException."""
assert isinstance(address, str), f"address must be str. got {type(address)}"
assert isinstance(signature, str), f"signature must be str. got {type(signature)}"
assert isinstance(message, str), f"message must be str. got {type(message)}"
+ if strip_inputs:
+ # stripping whitespaces leads to better UX for GUIs, but it's counter-productive for CLI
+ address = address.strip()
+ signature = signature.strip()
+ message = message.strip()
if not is_address(address):
raise UserFacingException(_("Invalid Bitcoin address."))
try:
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.