Skip to content

Commit

Permalink
SFT-2988: started message validation in QR signing
Browse files Browse the repository at this point in the history
  • Loading branch information
mjg-foundation committed Nov 30, 2023
1 parent bbe4f2c commit 809f672
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 8 deletions.
1 change: 1 addition & 0 deletions ports/stm32/boards/Passport/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
'flows/set_chain_flow.py',
'flows/set_initial_pin_flow.py',
'flows/show_security_words_setting_flow.py',
'flows/sign_electrum_message_flow.py',
'flows/sign_psbt_common_flow.py',
'flows/sign_psbt_microsd_flow.py',
'flows/sign_psbt_qr_flow.py',
Expand Down
1 change: 1 addition & 0 deletions ports/stm32/boards/Passport/modules/flows/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
from .set_chain_flow import *
from .set_initial_pin_flow import *
from .show_security_words_setting_flow import *
from .sign_electrum_message_flow import *
from .sign_text_file_flow import *
from .sign_psbt_common_flow import *
from .sign_psbt_microsd_flow import *
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# SPDX-FileCopyrightText: © 2023 Foundation Devices, Inc. <hello@foundationdevices.com>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# sign_electrum_message_flow.py - Sign an electrum standard message via QR

from flows import Flow
from pages import SuccessPage, ErrorPage, InsertMicroSDPage
from tasks import sign_text_file_task
from utils import spinner_task, validate_sign_text
from translations import t, T
from public_constants import AF_CLASSIC, MSG_SIGNING_MAX_LENGTH, RFC_SIGNATURE_TEMPLATE
import sys


class SignElectrumMessageFlow(Flow):
def __init__(self):
self.message = None
super().__init__(initial_state=self.scan_message, name='Sign Electrum Message Flow')

async def scan_message(self):
from flows import ScanQRFlow
from data_codecs.qr_type import QRType

result = await ScanQRFlow(qr_types=[QRType.QR],
data_description='a message').run()

if result is None:
self.set_result(None)
return

self.message = result

self.goto(self.validate_message)

async def validate_message(self):
import uio
from pages import ErrorPage
from utils import validate_sign_text

msg = uio.StringIO(self.message)
header = msg.readline()
self.message = self.message[len(header)::]

print("header:")
print(header)
print("message:")
print(self.message)

header_elements = header.split(' ')

if header_elements[0] != 'signmessage':
await ErrorPage('Not a valid message to sign').show()
self.set_result(False)
return

if header_elements[2] != 'ascii:\n':
await ErrorPage('Unsupported message type').show()
self.set_result(False)
return

(self.subpath, error) = validate_sign_text(self.message, header_elements[1], space_limit=False)

if error:
await ErrorPage(error).show()
self.set_result(False)
return

await ErrorPage('What now?').show()
self.set_result(True)

async def do_sign(self):
(signature, address, error) = await spinner_task('Signing File', sign_text_file_task,
args=[self.text, self.subpath, AF_CLASSIC])
if error is None:
self.signature = signature
self.address = address
self.goto(self.write_signed_file)
else:
# TODO: Refactor this to a simpler, common error handler page?
await ErrorPage(text='Error while signing file: {}'.format(error)).show()
self.set_result(False)
return

async def write_signed_file(self):
# complete. write out result
from ubinascii import b2a_base64
from flows import SaveToMicroSDFlow
from public_constants import RFC_SIGNATURE_TEMPLATE

orig_path, basename = self.file_path.rsplit('/', 1)
base, ext = basename.rsplit('.', 1)
filename = base + '-signed' + '.' + ext
sig = b2a_base64(self.signature).decode('ascii').strip()
data = RFC_SIGNATURE_TEMPLATE.format(addr=self.address, msg=self.text, blockchain='BITCOIN', sig=sig)
result = await SaveToMicroSDFlow(filename=filename,
data=data,
success_text="signed file",
path=orig_path,
mode='t').run()
self.set_result(result)
7 changes: 6 additions & 1 deletion ports/stm32/boards/Passport/modules/menus.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@


def manage_account_menu():
from flows import RenameAccountFlow, DeleteAccountFlow, ConnectWalletFlow, AddressExplorerFlow
from flows import (RenameAccountFlow,
DeleteAccountFlow,
ConnectWalletFlow,
AddressExplorerFlow,
SignElectrumMessageFlow)
from pages import AccountDetailsPage

return [
Expand All @@ -40,6 +44,7 @@ def manage_account_menu():
{'icon': 'ICON_VERIFY_ADDRESS', 'label': 'Explore Addresses', 'flow': AddressExplorerFlow,
'statusbar': {'title': 'LIST ADDRESSES'}},
{'icon': 'ICON_CANCEL', 'label': 'Delete Account', 'flow': DeleteAccountFlow},
{'icon': 'ICON_SCAN_QR', 'label': 'Sign a message', 'flow': SignElectrumMessageFlow},
]


Expand Down
33 changes: 26 additions & 7 deletions ports/stm32/boards/Passport/modules/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
#
# SPDX-FileCopyRightText: 2019 cryptoadvance
# SPDX-License-Identifier: MIT
#
# utils.py
#

Expand Down Expand Up @@ -1275,7 +1278,7 @@ def is_passphrase_active():
MSG_MAX_SPACES = 4


def validate_sign_text(text, subpath):
def validate_sign_text(text, subpath, space_limit=True):
# Check for leading or trailing whitespace
if text[0] == ' ':
return (subpath, 'File contains leading whitespace.')
Expand All @@ -1291,12 +1294,13 @@ def validate_sign_text(text, subpath):
if ord(ch) not in MSG_CHARSET:
return (subpath, 'File contains non-ASCII character: 0x%02x' % ord(ch))

if ch == ' ':
run += 1
if run >= MSG_MAX_SPACES:
return (subpath, 'File contains more than {} spaces in a row'.format(MSG_MAX_SPACES - 1))
else:
run = 0
if space_limit:
if ch == ' ':
run += 1
if run >= MSG_MAX_SPACES:
return (subpath, 'File contains more than {} spaces in a row'.format(MSG_MAX_SPACES - 1))
else:
run = 0

# Check subpath, if given
if subpath:
Expand Down Expand Up @@ -1492,4 +1496,19 @@ def get_single_address(xfp,
return address


# From SpecterDIY: https://github.com/cryptoadvance/specter-diy/blob/.../src/apps/signmessage/signmessage.py
def sign_message(self, derivation, msg, compressed=True):
# Sign message with private key
msghash = sha256(
sha256(
b"\x18Bitcoin Signed Message:\n" + compact.to_bytes(len(msg)) + msg
).digest()
).digest()
sig, flag = self.keystore.sign_recoverable(derivation, msghash)
c = 4 if compressed else 0
flag = bytes([27 + flag + c])
ser = flag + secp256k1.ecdsa_signature_serialize_compact(sig._sig)
return b2a_base64(ser).strip().decode()


# EOF

0 comments on commit 809f672

Please sign in to comment.