From ef15a605bb123952af13f133982b4939eb0c25af Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Mon, 14 Nov 2022 00:24:52 +0100 Subject: [PATCH 1/7] q&d modification to sign signet challenge --- shared/auth.py | 6 +++--- shared/hsm.py | 8 ++++---- shared/psbt.py | 24 +++++++++++++----------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/shared/auth.py b/shared/auth.py index 88b6819d5..c1c2b688c 100644 --- a/shared/auth.py +++ b/shared/auth.py @@ -445,15 +445,15 @@ async def interact(self): elif wl >= 2: msg.write('(%d warnings below)\n\n' % wl) - self.output_summary_text(msg) - gc.collect() + # self.output_summary_text(msg) + # gc.collect() fee = self.psbt.calculate_fee() if fee is not None: msg.write("\nNetwork fee:\n%s %s\n" % self.chain.render_value(fee)) # NEW: show where all the change outputs are going - self.output_change_text(msg) + # self.output_change_text(msg) gc.collect() if self.psbt.warnings: diff --git a/shared/hsm.py b/shared/hsm.py index 5c1c11152..7c33f0232 100644 --- a/shared/hsm.py +++ b/shared/hsm.py @@ -886,10 +886,10 @@ async def approve_transaction(self, psbt, psbt_sha, story): # reject anything with warning, probably if psbt.warnings: - if self.warnings_ok: - log.info("Txn has warnings, but policy is to accept anyway.") - else: - raise ValueError("has %d warning(s)" % len(psbt.warnings)) + # if self.warnings_ok: + log.info("Txn has warnings, but policy is to accept anyway.") + # else: + # raise ValueError("has %d warning(s)" % len(psbt.warnings)) # See who has entered creditials already (all must be valid). users = [] diff --git a/shared/psbt.py b/shared/psbt.py index baf8e77c7..c8b9487f4 100644 --- a/shared/psbt.py +++ b/shared/psbt.py @@ -971,7 +971,7 @@ def parse_txn(self): self.txn_version, marker, flags = unpack(" 63, 'too short' + # assert self.txn[1] > 63, 'too short' # this parses the input TXN in-place for idx, txin in self.input_iter(): @@ -1199,7 +1199,9 @@ def consider_outputs(self): # check fee is reasonable - if self.total_value_out == 0: + if self.total_value_in is None: + return + elif self.total_value_out == 0: per_fee = 100 else: the_fee = self.calculate_fee() @@ -1319,8 +1321,8 @@ def consider_inputs(self): # pull out just the CTXOut object (expensive) utxo = inp.get_utxo(txi.prevout.n) - assert utxo.nValue > 0 - total_in += utxo.nValue + # assert utxo.nValue > 0, "Utxo can't have 0 value" + # total_in += utxo.nValue # Look at what kind of input this will be, and therefore what # type of signing will be required, and which key we need. @@ -1330,18 +1332,18 @@ def consider_inputs(self): # iff to UTXO is segwit, then check it's value, and also # capture that value, since it's supposed to be immutable - if inp.is_segwit: - history.verify_amount(txi.prevout, inp.amount, i) + # if inp.is_segwit: + # history.verify_amount(txi.prevout, inp.amount, i) del utxo # XXX scan witness data provided, and consider those ins signed if not multisig? - if not foreign: + # if not foreign: # no foreign inputs, we can calculate the total input value - assert total_in > 0 - self.total_value_in = total_in - else: + # assert total_in > 0 + # self.total_value_in = total_in + # else: # 1+ inputs don't belong to us, we can't calculate the total input value # OK for multi-party transactions (coinjoin etc.) self.total_value_in = None From 7adff4a202fb18a1ddb2ae094b4b7fa272f63d9f Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Fri, 3 Mar 2023 12:19:03 +0100 Subject: [PATCH 2/7] Add logging.py and displace AuditLogger there --- shared/hsm.py | 50 +--------------------------------------------- shared/logging.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 49 deletions(-) create mode 100644 shared/logging.py diff --git a/shared/hsm.py b/shared/hsm.py index 7c33f0232..78621f1d4 100644 --- a/shared/hsm.py +++ b/shared/hsm.py @@ -16,7 +16,7 @@ from ubinascii import unhexlify as a2b_hex from uhashlib import sha256 from ucollections import OrderedDict -from files import CardSlot, CardMissingError +from logging import AuditLogger from serializations import CTxOut # where we save policy/config @@ -402,54 +402,6 @@ def matches_transaction(self, psbt, users, total_out, local_oked, chain): return True -class AuditLogger: - def __init__(self, dirname, digest, never_log): - self.dirname = dirname - self.digest = digest - self.never_log = never_log - - def __enter__(self): - try: - if self.never_log: - raise NotImplementedError - - self.card = CardSlot().__enter__() - - d = self.card.get_sd_root() + '/' + self.dirname - - # mkdir if needed - try: uos.stat(d) - except: uos.mkdir(d) - - self.fname = d + '/' + b2a_hex(self.digest[-8:]).decode('ascii') + '.log' - self.fd = open(self.fname, 'a+t') # append mode - except (CardMissingError, OSError, NotImplementedError): - # may be fatal or not, depending on configuration - self.fname = self.card = None - self.fd = sys.stdout - - return self - - def __exit__(self, exc_type, exc_value, traceback): - if exc_value: - self.fd.write('\n\n---- Coldcard Exception ----\n') - sys.print_exception(exc_value, self.fd) - - self.fd.write('\n===\n\n') - - if self.card: - assert self.fd != sys.stdout - self.fd.close() - self.card.__exit__(exc_type, exc_value, traceback) - - @property - def is_unsaved(self): - return not self.card - - def info(self, msg): - print(msg, file=self.fd) - #if self.fd != sys.stdout: print(msg) - class HSMPolicy: # implements and enforces the HSM signing/activity/logging policy def __init__(self): diff --git a/shared/logging.py b/shared/logging.py new file mode 100644 index 000000000..25b77ab10 --- /dev/null +++ b/shared/logging.py @@ -0,0 +1,51 @@ +import sys, uos +from ubinascii import hexlify as b2a_hex +from files import CardSlot, CardMissingError + +class AuditLogger: + def __init__(self, dirname, digest, never_log): + self.dirname = dirname + self.digest = digest + self.never_log = never_log + + def __enter__(self): + try: + if self.never_log: + raise NotImplementedError + + self.card = CardSlot().__enter__() + + d = self.card.get_sd_root() + '/' + self.dirname + + # mkdir if needed + try: uos.stat(d) + except: uos.mkdir(d) + + self.fname = d + '/' + b2a_hex(self.digest[-8:]).decode('ascii') + '.log' + self.fd = open(self.fname, 'a+t') # append mode + except (CardMissingError, OSError, NotImplementedError): + # may be fatal or not, depending on configuration + self.fname = self.card = None + self.fd = sys.stdout + + return self + + def __exit__(self, exc_type, exc_value, traceback): + if exc_value: + self.fd.write('\n\n---- Coldcard Exception ----\n') + sys.print_exception(exc_value, self.fd) + + self.fd.write('\n===\n\n') + + if self.card: + assert self.fd != sys.stdout + self.fd.close() + self.card.__exit__(exc_type, exc_value, traceback) + + @property + def is_unsaved(self): + return not self.card + + def info(self, msg): + print(msg, file=self.fd) + #if self.fd != sys.stdout: print(msg) From 525760a4e827eaf237702619b09822a31a2c424b Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Fri, 3 Mar 2023 16:37:50 +0100 Subject: [PATCH 3/7] Logs error while signing psbt in a file --- shared/auth.py | 322 ++++++++++++++++++++++++---------------------- shared/logging.py | 22 +++- 2 files changed, 188 insertions(+), 156 deletions(-) diff --git a/shared/auth.py b/shared/auth.py index c1c2b688c..1887d8de2 100644 --- a/shared/auth.py +++ b/shared/auth.py @@ -17,6 +17,7 @@ from psbt import psbtObject, FatalPSBTIssue, FraudulentChangeOutput from exceptions import HSMDenied from version import has_psram, has_fatram, MAX_TXN_LEN +from logging import AuditLogger # Where in SPI flash/PSRAM the two PSBT files are (in and out) TXN_INPUT_OFFSET = 0 @@ -379,192 +380,209 @@ async def interact(self): # step 1: parse PSBT from sflash into in-memory objects. + policy_hash = hsm_active.hash() + hsm_rules = hsm_active.save()['rules'] try: - with SFFile(TXN_INPUT_OFFSET, length=self.psbt_len, message='Reading...') as fd: - # NOTE: psbtObject captures the file descriptor and uses it later - self.psbt = psbtObject.read_psbt(fd) - except BaseException as exc: - if isinstance(exc, MemoryError): - msg = "Transaction is too complex" - exc = None - else: - msg = "PSBT parse failed" + hsm_rules[0]['never_log'] + except KeyError: + never_log = False + with AuditLogger('logs', self.psbt_sha, never_log, policy_hash) as log: + log.info("Loading the psbt file") + try: + with SFFile(TXN_INPUT_OFFSET, length=self.psbt_len, message='Reading...') as fd: + # NOTE: psbtObject captures the file descriptor and uses it later + self.psbt = psbtObject.read_psbt(fd) + except BaseException as exc: + if isinstance(exc, MemoryError): + msg = exc.__class__.__name__ + " Transaction is too complex" + exc = None + else: + msg = exc.__class__.__name__ + " PSBT parse failed" - return await self.failure(msg, exc) + log.error(msg) + return await self.failure(msg, exc) - dis.fullscreen("Validating...") + dis.fullscreen("Validating...") - # Do some analysis/ validation - try: - await self.psbt.validate() # might do UX: accept multisig import - self.psbt.consider_inputs() - - dis.fullscreen("Validating...", percent=0.33) - self.psbt.consider_keys() - - dis.progress_bar(0.66) - self.psbt.consider_outputs() - - dis.progress_bar(0.85) - except FraudulentChangeOutput as exc: - print('FraudulentChangeOutput: ' + exc.args[0]) - return await self.failure(exc.args[0], title='Change Fraud') - except FatalPSBTIssue as exc: - print('FatalPSBTIssue: ' + exc.args[0]) - return await self.failure(exc.args[0]) - except BaseException as exc: - del self.psbt - gc.collect() + log.info("Beginning formal validation of PSBT") + # Do some analysis/ validation + try: + await self.psbt.validate() # might do UX: accept multisig import + self.psbt.consider_inputs() + + dis.fullscreen("Validating...", percent=0.33) + self.psbt.consider_keys() + + dis.progress_bar(0.66) + self.psbt.consider_outputs() + + dis.progress_bar(0.85) + except FraudulentChangeOutput as exc: + print('FraudulentChangeOutput: ' + exc.args[0]) + log.error(exc.__class__.__name__ + exc.args[0]) + return await self.failure(exc.args[0], title='Change Fraud') + except FatalPSBTIssue as exc: + print('FatalPSBTIssue: ' + exc.args[0]) + log.error(exc.__class__.__name__ + exc.args[0]) + return await self.failure(exc.args[0]) + except BaseException as exc: + log.error(exc.__class__.__name__ + exc.args[0]) + del self.psbt + gc.collect() - if isinstance(exc, MemoryError): - msg = "Transaction is too complex" - exc = None - else: - msg = "Invalid PSBT" - - return await self.failure(msg, exc) - - # step 2: figure out what we are approving, so we can get sign-off - # - outputs, amounts - # - fee - # - # notes: - # - try to handle lots of outputs - # - cannot calc fee as sat/byte, only as percent - # - somethings are 'warnings': - # - fee too big - # - inputs we can't sign (no key) - # - try: - msg = uio.StringIO() + if isinstance(exc, MemoryError): + msg = "Transaction is too complex" + log.error(exc.__class__.__name__ + exc.args[0]) + exc = None + else: + msg = "Invalid PSBT" + log.error(exc.__class__.__name__ + exc.args[0]) + + return await self.failure(msg, exc) + + log.info("Formal validation completed") + log.info("Approving content") + # step 2: figure out what we are approving, so we can get sign-off + # - outputs, amounts + # - fee + # + # notes: + # - try to handle lots of outputs + # - cannot calc fee as sat/byte, only as percent + # - somethings are 'warnings': + # - fee too big + # - inputs we can't sign (no key) + # + try: + msg = uio.StringIO() - # mention warning at top - wl= len(self.psbt.warnings) - if wl == 1: - msg.write('(1 warning below)\n\n') - elif wl >= 2: - msg.write('(%d warnings below)\n\n' % wl) + # mention warning at top + wl= len(self.psbt.warnings) + if wl == 1: + msg.write('(1 warning below)\n\n') + elif wl >= 2: + msg.write('(%d warnings below)\n\n' % wl) - # self.output_summary_text(msg) - # gc.collect() + # self.output_summary_text(msg) + # gc.collect() - fee = self.psbt.calculate_fee() - if fee is not None: - msg.write("\nNetwork fee:\n%s %s\n" % self.chain.render_value(fee)) + fee = self.psbt.calculate_fee() + if fee is not None: + msg.write("\nNetwork fee:\n%s %s\n" % self.chain.render_value(fee)) - # NEW: show where all the change outputs are going - # self.output_change_text(msg) - gc.collect() + # NEW: show where all the change outputs are going + # self.output_change_text(msg) + gc.collect() - if self.psbt.warnings: - msg.write('\n---WARNING---\n\n') + if self.psbt.warnings: + msg.write('\n---WARNING---\n\n') - for label, m in self.psbt.warnings: - msg.write('- %s: %s\n\n' % (label, m)) + for label, m in self.psbt.warnings: + msg.write('- %s: %s\n\n' % (label, m)) - if self.do_visualize: - # stop here and just return the text of approval message itself - self.result = await self.save_visualization(msg, (self.stxn_flags & STXN_SIGNED)) - del self.psbt - self.done() + if self.do_visualize: + # stop here and just return the text of approval message itself + self.result = await self.save_visualization(msg, (self.stxn_flags & STXN_SIGNED)) + del self.psbt + self.done() - return + return - if not hsm_active: - msg.write("\nPress OK to approve and sign transaction. X to abort.") - ch = await ux_show_story(msg, title="OK TO SEND?") - else: - ch = await hsm_active.approve_transaction(self.psbt, self.psbt_sha, msg.getvalue()) - dis.progress_bar(1) # finish the Validating... + if not hsm_active: + msg.write("\nPress OK to approve and sign transaction. X to abort.") + ch = await ux_show_story(msg, title="OK TO SEND?") + else: + ch = await hsm_active.approve_transaction(self.psbt, self.psbt_sha, msg.getvalue()) + dis.progress_bar(1) # finish the Validating... - except MemoryError: - # recovery? maybe. - try: - del self.psbt - del msg - except: pass # might be NameError since we don't know how far we got - gc.collect() + except MemoryError: + # recovery? maybe. + try: + del self.psbt + del msg + except: pass # might be NameError since we don't know how far we got + gc.collect() - msg = "Transaction is too complex" - return await self.failure(msg) + msg = "Transaction is too complex" + return await self.failure(msg) - if ch != 'y': - # they don't want to! - self.refused = True + if ch != 'y': + # they don't want to! + self.refused = True - await ux_dramatic_pause("Refused.", 1) + await ux_dramatic_pause("Refused.", 1) - del self.psbt + del self.psbt - self.done() - return + self.done() + return - # do the actual signing. - try: - dis.fullscreen('Wait...') - gc.collect() # visible delay caused by this but also sign_it() below - self.psbt.sign_it() - except FraudulentChangeOutput as exc: - return await self.failure(exc.args[0], title='Change Fraud') - except MemoryError: - msg = "Transaction is too complex" - return await self.failure(msg) - except BaseException as exc: - return await self.failure("Signing failed late", exc) + # do the actual signing. + try: + dis.fullscreen('Wait...') + gc.collect() # visible delay caused by this but also sign_it() below + self.psbt.sign_it() + except FraudulentChangeOutput as exc: + return await self.failure(exc.args[0], title='Change Fraud') + except MemoryError: + msg = "Transaction is too complex" + return await self.failure(msg) + except BaseException as exc: + return await self.failure("Signing failed late", exc) - if self.approved_cb: - # for micro sd case - await self.approved_cb(self.psbt) - self.done() - return + if self.approved_cb: + # for micro sd case + await self.approved_cb(self.psbt) + self.done() + return - txid = None - try: - # re-serialize the PSBT back out - with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as fd: - await fd.erase() + txid = None + try: + # re-serialize the PSBT back out + with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as fd: + await fd.erase() - if self.do_finalize: - txid = self.psbt.finalize(fd) - else: - self.psbt.serialize(fd) + if self.do_finalize: + txid = self.psbt.finalize(fd) + else: + self.psbt.serialize(fd) - fd.close() - self.result = (fd.tell(), fd.checksum.digest()) + fd.close() + self.result = (fd.tell(), fd.checksum.digest()) - self.done(redraw=(not txid)) + self.done(redraw=(not txid)) - except BaseException as exc: - return await self.failure("PSBT output failed", exc) + except BaseException as exc: + return await self.failure("PSBT output failed", exc) - from glob import NFC + from glob import NFC - if self.do_finalize and txid and not hsm_active: - while 1: - # Show txid when we can; advisory - # - maybe even as QR, hex-encoded in alnum mode - tmsg = txid + '\n\n' + if self.do_finalize and txid and not hsm_active: + while 1: + # Show txid when we can; advisory + # - maybe even as QR, hex-encoded in alnum mode + tmsg = txid + '\n\n' - if has_fatram: - tmsg += 'Press 1 for QR Code of TXID. ' - if NFC: - tmsg += 'Press 3 to share signed txn over NFC.' + if has_fatram: + tmsg += 'Press 1 for QR Code of TXID. ' + if NFC: + tmsg += 'Press 3 to share signed txn over NFC.' - ch = await ux_show_story(tmsg, "Final TXID", escape='13') + ch = await ux_show_story(tmsg, "Final TXID", escape='13') - if ch=='1' and has_fatram: - await show_qr_code(txid, True) - continue + if ch=='1' and has_fatram: + await show_qr_code(txid, True) + continue - if ch == '3' and NFC: - await NFC.share_signed_txn(txid, TXN_OUTPUT_OFFSET, - self.result[0], self.result[1]) - continue - break + if ch == '3' and NFC: + await NFC.share_signed_txn(txid, TXN_OUTPUT_OFFSET, + self.result[0], self.result[1]) + continue + break - # TODO ofter to share / or auto-share over NFC if that seems appropraite - #if NFC: - #NFC.share_signed_psbt(TXN_OUTPUT_OFFSET, self.result[0], self.result[1]) + # TODO ofter to share / or auto-share over NFC if that seems appropraite + #if NFC: + #NFC.share_signed_psbt(TXN_OUTPUT_OFFSET, self.result[0], self.result[1]) def save_visualization(self, msg, sign_text=False): # write text into spi flash, maybe signing it as we go diff --git a/shared/logging.py b/shared/logging.py index 25b77ab10..976f4c678 100644 --- a/shared/logging.py +++ b/shared/logging.py @@ -3,10 +3,11 @@ from files import CardSlot, CardMissingError class AuditLogger: - def __init__(self, dirname, digest, never_log): + def __init__(self, dirname, digest, never_log, policy_hash=None): self.dirname = dirname self.digest = digest self.never_log = never_log + self.policy_hash = policy_hash def __enter__(self): try: @@ -21,7 +22,12 @@ def __enter__(self): try: uos.stat(d) except: uos.mkdir(d) - self.fname = d + '/' + b2a_hex(self.digest[-8:]).decode('ascii') + '.log' + if self.dirname == 'psbt': + self.fname = d + '/' + b2a_hex(self.digest[-8:]).decode('ascii') + '.log' + elif self.dirname == 'logs': + self.fname = d + '/' + b2a_hex(self.policy_hash[-8:]).decode('ascii') + '.log' + else: + raise NotImplementedError self.fd = open(self.fname, 'a+t') # append mode except (CardMissingError, OSError, NotImplementedError): # may be fatal or not, depending on configuration @@ -47,5 +53,13 @@ def is_unsaved(self): return not self.card def info(self, msg): - print(msg, file=self.fd) - #if self.fd != sys.stdout: print(msg) + if self.dirname == 'psbt': + print(msg, file=self.fd) + else: + print(b2a_hex(self.digest[-8:]).decode('ascii') + ' Info: ' + msg, file=self.fd) + + def error(self, msg): + if self.dirname == 'psbt': + print(msg, file=self.fd) + else: + print(b2a_hex(self.digest[-8:]).decode('ascii') + ' Error: ' + msg, file=self.fd) From 8e1932b3b57ea466f2e44b4e8c40f225d0eea9ad Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Tue, 14 Mar 2023 18:12:37 +0100 Subject: [PATCH 4/7] Add logging to manifest.py --- shared/manifest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/shared/manifest.py b/shared/manifest.py index c9d4a6ee3..8dae2b25d 100644 --- a/shared/manifest.py +++ b/shared/manifest.py @@ -24,6 +24,7 @@ 'hsm.py', 'hsm_ux.py', 'imptask.py', + 'logging.py', 'login.py', 'main.py', 'mempad.py', From 9679366792c73ea294709593b61ecd7d1f3506d4 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 15 Mar 2023 14:14:29 +0100 Subject: [PATCH 5/7] Only one AuditLogger (EINVAL error) --- shared/auth.py | 2 +- shared/hsm.py | 146 ++++++++++++++++++++++++------------------------- 2 files changed, 73 insertions(+), 75 deletions(-) diff --git a/shared/auth.py b/shared/auth.py index 1887d8de2..470bf41f0 100644 --- a/shared/auth.py +++ b/shared/auth.py @@ -492,7 +492,7 @@ async def interact(self): msg.write("\nPress OK to approve and sign transaction. X to abort.") ch = await ux_show_story(msg, title="OK TO SEND?") else: - ch = await hsm_active.approve_transaction(self.psbt, self.psbt_sha, msg.getvalue()) + ch = await hsm_active.approve_transaction(self.psbt, self.psbt_sha, msg.getvalue(), log) dis.progress_bar(1) # finish the Validating... except MemoryError: diff --git a/shared/hsm.py b/shared/hsm.py index 78621f1d4..187e9e0f3 100644 --- a/shared/hsm.py +++ b/shared/hsm.py @@ -807,7 +807,7 @@ def _new_local_code(self): from ubinascii import b2a_base64 self.next_local_code = b2a_base64(ngu.random.bytes(15)).strip().decode('ascii') - async def approve_transaction(self, psbt, psbt_sha, story): + async def approve_transaction(self, psbt, psbt_sha, story, log): # Approve or don't a transaction. Catch assertions and other # reasons for failing/rejecting into the log. # - return 'y' or 'x' @@ -815,87 +815,85 @@ async def approve_transaction(self, psbt, psbt_sha, story): assert psbt_sha and len(psbt_sha) == 32 self.get_time_left() - with AuditLogger('psbt', psbt_sha, self.never_log) as log: + if self.must_log and log.is_unsaved: + self.refuse(log, "Could not log details, and must_log is set") + return 'x' - if self.must_log and log.is_unsaved: - self.refuse(log, "Could not log details, and must_log is set") - return 'x' + log.info('Transaction signing requested:') + log.info('SHA256(PSBT) = ' + b2a_hex(psbt_sha).decode('ascii')) + log.info('-vvv-\n%s\n-^^^-' % story) - log.info('Transaction signing requested:') - log.info('SHA256(PSBT) = ' + b2a_hex(psbt_sha).decode('ascii')) - log.info('-vvv-\n%s\n-^^^-' % story) - - # reset pending auth list and "consume" it now - auth = self.pending_auth - self.pending_auth = {} - - try: - # do this super early so always cleared even if other issues - local_ok = self.consume_local_code(psbt_sha) - - if not self.rules: - raise ValueError("no txn signing allowed") - - # reject anything with warning, probably - if psbt.warnings: - # if self.warnings_ok: - log.info("Txn has warnings, but policy is to accept anyway.") - # else: - # raise ValueError("has %d warning(s)" % len(psbt.warnings)) - - # See who has entered creditials already (all must be valid). - users = [] - for u, (token, counter) in auth.items(): - problem = Users.auth_okay(u, token, totp_time=counter, psbt_hash=psbt_sha) - if problem: - self.refuse(log, "User '%s' gave wrong auth value: %s" % (u, problem)) - return 'x' - users.append(u) - - # was right code provided locally? (also resets for next attempt) - if local_ok: - log.info("Local operator gave correct code.") - if users: - log.info("These users gave correct auth codes: " + ', '.join(users)) - - # Totals (applies to foreign) - total_out = sum(o.amount for o in psbt.outputs if not o.is_change) - - # Pick a rule to apply to this specific txn - reasons = [] - for rule in self.rules: - try: - if rule.matches_transaction(psbt, users, total_out, local_ok, chain): - break - except BaseException as exc: - # let's not share these details, except for debug; since - # they are not errors, just picking best rule in priority order - r = "rule #%d: %s" % (rule.index, str(exc) or problem_file_line(exc)) - reasons.append(r) - print(r) - else: - err = "Rejected: " + ', '.join(reasons) - self.refuse(log, err) + # reset pending auth list and "consume" it now + auth = self.pending_auth + self.pending_auth = {} + + try: + # do this super early so always cleared even if other issues + local_ok = self.consume_local_code(psbt_sha) + + if not self.rules: + raise ValueError("no txn signing allowed") + + # reject anything with warning, probably + if psbt.warnings: + # if self.warnings_ok: + log.info("Txn has warnings, but policy is to accept anyway.") + # else: + # raise ValueError("has %d warning(s)" % len(psbt.warnings)) + + # See who has entered creditials already (all must be valid). + users = [] + for u, (token, counter) in auth.items(): + problem = Users.auth_okay(u, token, totp_time=counter, psbt_hash=psbt_sha) + if problem: + self.refuse(log, "User '%s' gave wrong auth value: %s" % (u, problem)) return 'x' + users.append(u) + + # was right code provided locally? (also resets for next attempt) + if local_ok: + log.info("Local operator gave correct code.") + if users: + log.info("These users gave correct auth codes: " + ', '.join(users)) + + # Totals (applies to foreign) + total_out = sum(o.amount for o in psbt.outputs if not o.is_change) + + # Pick a rule to apply to this specific txn + reasons = [] + for rule in self.rules: + try: + if rule.matches_transaction(psbt, users, total_out, local_ok, chain): + break + except BaseException as exc: + # let's not share these details, except for debug; since + # they are not errors, just picking best rule in priority order + r = "rule #%d: %s" % (rule.index, str(exc) or problem_file_line(exc)) + reasons.append(r) + print(r) + else: + err = "Rejected: " + ', '.join(reasons) + self.refuse(log, err) + return 'x' - if users: - msg = ', '.join(auth.keys()) - if local_ok: - msg += ', and the local operator.' if msg else 'local operator' + if users: + msg = ', '.join(auth.keys()) + if local_ok: + msg += ', and the local operator.' if msg else 'local operator' - # looks good, do it - self.approve(log, "Acceptable by rule #%d" % rule.index) + # looks good, do it + self.approve(log, "Acceptable by rule #%d" % rule.index) - if rule.per_period is not None: - self.record_spend(rule, total_out) + if rule.per_period is not None: + self.record_spend(rule, total_out) - return 'y' - except BaseException as exc: - sys.print_exception(exc) - err = "Rejected: " + (str(exc) or problem_file_line(exc)) - self.refuse(log, err) + return 'y' + except BaseException as exc: + sys.print_exception(exc) + err = "Rejected: " + (str(exc) or problem_file_line(exc)) + self.refuse(log, err) - return 'x' + return 'x' def refuse(self, log, msg): # when things fail From 3c4add6feed8ebe5def6c7f86662015ff0a797ba Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 15 Mar 2023 14:24:17 +0100 Subject: [PATCH 6/7] Do not try to read hsm_active if not in hsm mode --- shared/auth.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/shared/auth.py b/shared/auth.py index 470bf41f0..31e304754 100644 --- a/shared/auth.py +++ b/shared/auth.py @@ -380,11 +380,15 @@ async def interact(self): # step 1: parse PSBT from sflash into in-memory objects. - policy_hash = hsm_active.hash() - hsm_rules = hsm_active.save()['rules'] - try: - hsm_rules[0]['never_log'] - except KeyError: + if hsm_active: + policy_hash = hsm_active.hash() + hsm_rules = hsm_active.save()['rules'] + try: + hsm_rules[0]['never_log'] + except KeyError: + never_log = False + else: + policy_hash = b'\x00'*32 never_log = False with AuditLogger('logs', self.psbt_sha, never_log, policy_hash) as log: log.info("Loading the psbt file") From f005c304fc5640692a3f89b8c607e333676a1d67 Mon Sep 17 00:00:00 2001 From: Sosthene00 <674694@protonmail.ch> Date: Wed, 15 Mar 2023 14:42:18 +0100 Subject: [PATCH 7/7] Print psbt sha before validating --- shared/auth.py | 3 ++- shared/hsm.py | 2 -- shared/logging.py | 18 +++++++----------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/shared/auth.py b/shared/auth.py index 31e304754..677def22a 100644 --- a/shared/auth.py +++ b/shared/auth.py @@ -391,6 +391,7 @@ async def interact(self): policy_hash = b'\x00'*32 never_log = False with AuditLogger('logs', self.psbt_sha, never_log, policy_hash) as log: + log.new_psbt() log.info("Loading the psbt file") try: with SFFile(TXN_INPUT_OFFSET, length=self.psbt_len, message='Reading...') as fd: @@ -408,7 +409,7 @@ async def interact(self): dis.fullscreen("Validating...") - log.info("Beginning formal validation of PSBT") + log.info("PSBT parsing successful") # Do some analysis/ validation try: await self.psbt.validate() # might do UX: accept multisig import diff --git a/shared/hsm.py b/shared/hsm.py index 187e9e0f3..005963739 100644 --- a/shared/hsm.py +++ b/shared/hsm.py @@ -819,8 +819,6 @@ async def approve_transaction(self, psbt, psbt_sha, story, log): self.refuse(log, "Could not log details, and must_log is set") return 'x' - log.info('Transaction signing requested:') - log.info('SHA256(PSBT) = ' + b2a_hex(psbt_sha).decode('ascii')) log.info('-vvv-\n%s\n-^^^-' % story) # reset pending auth list and "consume" it now diff --git a/shared/logging.py b/shared/logging.py index 976f4c678..eb3247f42 100644 --- a/shared/logging.py +++ b/shared/logging.py @@ -22,9 +22,7 @@ def __enter__(self): try: uos.stat(d) except: uos.mkdir(d) - if self.dirname == 'psbt': - self.fname = d + '/' + b2a_hex(self.digest[-8:]).decode('ascii') + '.log' - elif self.dirname == 'logs': + if self.dirname == 'logs': self.fname = d + '/' + b2a_hex(self.policy_hash[-8:]).decode('ascii') + '.log' else: raise NotImplementedError @@ -53,13 +51,11 @@ def is_unsaved(self): return not self.card def info(self, msg): - if self.dirname == 'psbt': - print(msg, file=self.fd) - else: - print(b2a_hex(self.digest[-8:]).decode('ascii') + ' Info: ' + msg, file=self.fd) + print('Info: ' + msg, file=self.fd) def error(self, msg): - if self.dirname == 'psbt': - print(msg, file=self.fd) - else: - print(b2a_hex(self.digest[-8:]).decode('ascii') + ' Error: ' + msg, file=self.fd) + print('Error: ' + msg, file=self.fd) + + def new_psbt(self): + print('Transaction signing requested:', file=self.fd) + print('SHA256(PSBT) = ' + b2a_hex(self.digest).decode('ascii'), file=self.fd)