SFT-3834: added microsd message signing
What changed, and why it matters
This commit adds a new menu option that lets users sign a message using a file stored on a microSD card, alongside the existing option to sign by scanning a QR code. It reuses the same internal signing logic that was already used for health checks, just with different on-screen labels and file filters. There is no indication in the code that this weakens security or introduces a vulnerability; it appears to be a normal feature addition.
No security action required. Treat as a normal feature commit. If desired, verify that the existing sign_text_file_task and validate_sign_text functions already require user confirmation before signing, since the new microSD path should follow the same authorization model as the QR path.
Security signals we found
No new cryptographic operations introduced; existing sign_text_file_task is reused
No changes to input validation length checks or address-type handling
File filter now excludes files containing '-signed' to avoid re-signing output files
User-facing strings and menu labels changed, but flow control logic is unchanged
No privilege escalation, secret exposure, or bypass evident in the diff
Evidence from the diff
The change refactors HealthCheckCommonFlow and HealthCheckMicrosdFlow to accept a normal_signing flag. When set, the flows skip the ‘-hc’ filename filter, exclude already ‘-signed’ files, change user-facing strings from ‘health check’ to ‘message/signing’, and write the output to a ‘-signed’ file. A new sign_message_submenu() in menus.py exposes HealthCheckMicrosdFlow with normal_signing=True as ‘Sign with microSD’, while SignElectrumMessageFlow remains ‘Sign with QR Code’. The actual signing is still performed by sign_text_file_task with the same arguments (text, subpath, address type).
Changed components
ports/stm32/boards/Passport/modules/flows/health_check_common_flow.pyports/stm32/boards/Passport/modules/flows/health_check_microsd_flow.pyports/stm32/boards/Passport/modules/flows/sign_electrum_message_flow.pyports/stm32/boards/Passport/modules/menus.pyInspect captured patch +37 / −11
diff --git a/ports/stm32/boards/Passport/modules/flows/health_check_common_flow.py b/ports/stm32/boards/Passport/modules/flows/health_check_common_flow.py
index d899dfa..5f67c57 100644
--- a/ports/stm32/boards/Passport/modules/flows/health_check_common_flow.py
+++ b/ports/stm32/boards/Passport/modules/flows/health_check_common_flow.py
@@ -9,18 +9,19 @@ from public_constants import AF_CLASSIC
class HealthCheckCommonFlow(Flow):
- def __init__(self, lines):
+ def __init__(self, lines, normal_signing=False):
super().__init__(initial_state=self.validate_lines, name='HealthCheckCommonFlow')
self.lines = lines
self.text = None
self.subpath = None
self.addr_type = AF_CLASSIC
+ self.normal_signing = normal_signing
async def validate_lines(self):
from pages import ErrorPage
from utils import validate_sign_text
if len(self.lines) not in [2, 3]:
- await ErrorPage('Health check format is invalid.').show()
+ await ErrorPage('{} format is invalid.'.format('Message' if self.normal_signing else 'Health check')).show()
self.set_result(None)
return
@@ -47,7 +48,8 @@ class HealthCheckCommonFlow(Flow):
from pages import ErrorPage
from tasks import sign_text_file_task
from utils import spinner_task
- (signature, address, error) = await spinner_task('Performing Health Check', sign_text_file_task,
+ text = 'Signing message' if self.normal_signing else 'Performing health check'
+ (signature, address, error) = await spinner_task(text, sign_text_file_task,
args=[self.text, self.subpath, self.addr_type])
if error is None:
self.signature = signature
diff --git a/ports/stm32/boards/Passport/modules/flows/health_check_microsd_flow.py b/ports/stm32/boards/Passport/modules/flows/health_check_microsd_flow.py
index a4e0393..fd43f1c 100644
--- a/ports/stm32/boards/Passport/modules/flows/health_check_microsd_flow.py
+++ b/ports/stm32/boards/Passport/modules/flows/health_check_microsd_flow.py
@@ -14,21 +14,33 @@ def is_health_check(filename, path=None):
if '-hc' in filename:
return True
+
return False
+def is_signable(filename, path=None):
+ filename = filename.lower()
+
+ if '-signed' in filename:
+ return False
+
+ return True
+
+
class HealthCheckMicrosdFlow(Flow):
- def __init__(self, context=None):
+ def __init__(self, context=None, normal_signing=False):
super().__init__(initial_state=self.choose_file, name='HealthCheckMicrosdFlow')
self.file_path = None
self.lines = None
self.signed_message = None
self.service_name = context
+ self.normal_signing = normal_signing
async def choose_file(self):
from flows import FilePickerFlow
- result = await FilePickerFlow(show_folders=True, filter_fn=is_health_check).run()
+ result = await FilePickerFlow(show_folders=True,
+ filter_fn=(is_signable if self.normal_signing else is_health_check)).run()
if result is None:
self.set_result(False)
return
@@ -53,7 +65,7 @@ class HealthCheckMicrosdFlow(Flow):
async def common_flow(self):
from flows import HealthCheckCommonFlow
- self.signed_message = await HealthCheckCommonFlow(self.lines).run()
+ self.signed_message = await HealthCheckCommonFlow(self.lines, normal_signing=self.normal_signing).run()
if self.signed_message is None:
self.set_result(False)
return
@@ -65,8 +77,9 @@ class HealthCheckMicrosdFlow(Flow):
orig_path, basename = self.file_path.rsplit('/', 1)
base, ext = basename.rsplit('.', 1)
filename = base + '-signed' + '.' + ext
+ success_text = "signed message" if self.normal_signing else "updated health check"
result = await SaveToMicroSDFlow(filename=filename,
data=self.signed_message,
- success_text="updated health check",
+ success_text=success_text,
path=orig_path).run()
self.set_result(result)
diff --git a/ports/stm32/boards/Passport/modules/flows/sign_electrum_message_flow.py b/ports/stm32/boards/Passport/modules/flows/sign_electrum_message_flow.py
index 464ba4f..8e3d64e 100644
--- a/ports/stm32/boards/Passport/modules/flows/sign_electrum_message_flow.py
+++ b/ports/stm32/boards/Passport/modules/flows/sign_electrum_message_flow.py
@@ -20,7 +20,7 @@ class SignElectrumMessageFlow(Flow):
self.message = None
self.address = None
self.signature = None
- super().__init__(initial_state=self.scan_message, name='Sign Electrum Message Flow')
+ super().__init__(initial_state=self.scan_message, name='SignElectrumMessageFlow')
async def scan_message(self):
result = await ScanQRFlow(qr_types=[QRType.QR],
@@ -81,7 +81,7 @@ class SignElectrumMessageFlow(Flow):
self.goto(self.do_sign)
async def do_sign(self):
- (sig, address, error) = await spinner_task('Signing Message', sign_text_file_task,
+ (sig, address, error) = await spinner_task('Signing message', sign_text_file_task,
args=[self.message, self.subpath, self.addr_format])
if error is None:
self.signature = sig
diff --git a/ports/stm32/boards/Passport/modules/menus.py b/ports/stm32/boards/Passport/modules/menus.py
index 07f5ed6..3b5d6e3 100644
--- a/ports/stm32/boards/Passport/modules/menus.py
+++ b/ports/stm32/boards/Passport/modules/menus.py
@@ -43,12 +43,23 @@ def manage_account_menu():
]
+def sign_message_submenu():
+ from flows import SignElectrumMessageFlow, HealthCheckMicrosdFlow
+
+ return [
+ {'icon': 'ICON_SCAN_QR', 'label': 'Sign with QR Code', 'flow': SignElectrumMessageFlow,
+ 'statusbar': {'title': 'SIGN MESSAGE'}},
+ {'icon': 'ICON_MICROSD', 'label': 'Sign with microSD', 'flow': HealthCheckMicrosdFlow,
+ 'statusbar': {'title': 'SIGN MESSAGE'}, 'args': {'normal_signing': True}},
+ ]
+
+
def account_tools():
- from flows import VerifyAddressFlow, SignElectrumMessageFlow, AddressExplorerFlow
+ from flows import VerifyAddressFlow, AddressExplorerFlow
return [
{'icon': 'ICON_VERIFY_ADDRESS', 'label': 'Verify Address', 'flow': VerifyAddressFlow},
- {'icon': 'ICON_SCAN_QR', 'label': 'Sign a Message', 'flow': SignElectrumMessageFlow,
+ {'icon': 'ICON_SIGN', 'label': 'Sign a Message', 'submenu': sign_message_submenu,
'statusbar': {'title': 'SIGN MESSAGE'}},
{'icon': 'ICON_VERIFY_ADDRESS', 'label': 'Explore Addresses', 'flow': AddressExplorerFlow,
'statusbar': {'title': 'LIST ADDRESSES'}},
Why this scored 12/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.