From 94b85efba18735805efb7baaa7e78a015f12bbe7 Mon Sep 17 00:00:00 2001 From: matejcik Date: Sat, 8 Feb 2020 11:21:37 +0100 Subject: [PATCH 01/10] python/debuglink: make pin sequences configurable --- python/src/trezorlib/debuglink.py | 20 ++++++++++---------- tests/device_tests/test_protect_call.py | 11 ++--------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py index b9848a0814..1ea180febb 100644 --- a/python/src/trezorlib/debuglink.py +++ b/python/src/trezorlib/debuglink.py @@ -221,8 +221,12 @@ class DebugUI: self.input_flow = self.INPUT_FLOW_DONE def get_pin(self, code=None): - if self.pin: - return self.pin + if isinstance(self.pin, str): + return self.debuglink.encode_pin(self.pin) + elif self.pin == []: + raise AssertionError("PIN sequence ended prematurely") + elif self.pin: + return self.debuglink.encode_pin(self.pin.pop(0)) else: return self.debuglink.read_pin_encoded() @@ -261,9 +265,6 @@ class TrezorClientDebugLink(TrezorClient): self.filters = {} - # Always press Yes and provide correct pin - self.setup_debuglink(True, True) - # Do not expect any specific response from device self.expected_responses = None self.current_response = None @@ -342,12 +343,11 @@ class TrezorClientDebugLink(TrezorClient): self.expected_responses = expected self.current_response = 0 - def setup_debuglink(self, button, pin_correct): - # self.button = button # True -> YES button, False -> NO button - if pin_correct: - self.ui.pin = None + def set_pin(self, pin): + if isinstance(pin, str): + self.ui.pin = pin else: - self.ui.pin = "444222" + self.ui.pin = list(pin) def set_passphrase(self, passphrase): self.ui.passphrase = Mnemonic.normalize_string(passphrase) diff --git a/tests/device_tests/test_protect_call.py b/tests/device_tests/test_protect_call.py index 15d2846805..123510eec8 100644 --- a/tests/device_tests/test_protect_call.py +++ b/tests/device_tests/test_protect_call.py @@ -41,25 +41,18 @@ class TestProtectCall: def test_pin(self, client): with client: assert client.debug.read_pin()[0] == "1234" - client.setup_debuglink(button=True, pin_correct=True) client.set_expected_responses([proto.PinMatrixRequest(), proto.Address()]) self._some_protected_call(client) @pytest.mark.setup_client(pin="1234") def test_incorrect_pin(self, client): - client.setup_debuglink(button=True, pin_correct=False) - with pytest.raises(PinException): - self._some_protected_call(client) - - @pytest.mark.setup_client(pin="1234") - def test_cancelled_pin(self, client): - client.setup_debuglink(button=True, pin_correct=False) # PIN cancel + client.set_pin("5678") with pytest.raises(PinException): self._some_protected_call(client) @pytest.mark.setup_client(pin="1234", passphrase=True) def test_exponential_backoff_with_reboot(self, client): - client.setup_debuglink(button=True, pin_correct=False) + client.set_pin("5678") def test_backoff(attempts, start): if attempts <= 1: From d3b88a37beb95191796c36649c1a2cdd9bea9836 Mon Sep 17 00:00:00 2001 From: matejcik Date: Sat, 8 Feb 2020 11:23:08 +0100 Subject: [PATCH 02/10] core: do not catch SystemExit in handle_session (#826) --- core/src/trezor/wire/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/trezor/wire/__init__.py b/core/src/trezor/wire/__init__.py index b826ee0949..a52f26e951 100644 --- a/core/src/trezor/wire/__init__.py +++ b/core/src/trezor/wire/__init__.py @@ -409,7 +409,7 @@ async def handle_session(iface: WireInterface, session_id: int) -> None: # Unload modules imported by the workflow. Should not raise. utils.unimport_end(modules) - except BaseException as exc: + except Exception as exc: # The session handling should never exit, just log and continue. if __debug__: log.exception(__name__, exc) From c14429c4452ab970d9c1c5d9d2ab27d4a8a60604 Mon Sep 17 00:00:00 2001 From: matejcik Date: Tue, 11 Feb 2020 11:25:09 +0100 Subject: [PATCH 03/10] all: shut down emulator on error_shutdown --- core/embed/unix/common.c | 4 +--- legacy/emulator/setup.c | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/embed/unix/common.c b/core/embed/unix/common.c index f2f56bbb2f..ef9434d4ec 100644 --- a/core/embed/unix/common.c +++ b/core/embed/unix/common.c @@ -102,9 +102,7 @@ error_shutdown(const char *line1, const char *line2, const char *line3, printf("\nPlease unplug the device.\n"); display_backlight(255); hal_delay(5000); - __shutdown(); - for (;;) - ; + exit(4); } void hal_delay(uint32_t ms) { usleep(1000 * ms); } diff --git a/legacy/emulator/setup.c b/legacy/emulator/setup.c index 27990c22af..c300b53e68 100644 --- a/legacy/emulator/setup.c +++ b/legacy/emulator/setup.c @@ -53,7 +53,8 @@ void setup(void) { } void __attribute__((noreturn)) shutdown(void) { - for (;;) pause(); + sleep(5); + exit(4); } void emulatorRandom(void *buffer, size_t size) { From 7a253a6c0bf60da56b333b3fecaa2a88abb9edfc Mon Sep 17 00:00:00 2001 From: matejcik Date: Tue, 11 Feb 2020 11:29:47 +0100 Subject: [PATCH 04/10] python/debuglink: properly clean up at end of "with client" --- python/src/trezorlib/debuglink.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py index 1ea180febb..132a631305 100644 --- a/python/src/trezorlib/debuglink.py +++ b/python/src/trezorlib/debuglink.py @@ -316,24 +316,25 @@ class TrezorClientDebugLink(TrezorClient): self.in_with_statement -= 1 # Clear input flow. - self.set_input_flow(None) + try: + if _type is not None: + # Another exception raised + return False - if _type is not None: - # Another exception raised - return False + if self.expected_responses is None: + # no need to check anything else + return False - if self.expected_responses is None: - # no need to check anything else - return False + # Evaluate missed responses in 'with' statement + if self.current_response < len(self.expected_responses): + self._raise_unexpected_response(None) - # return isinstance(value, TypeError) - # Evaluate missed responses in 'with' statement - if self.current_response < len(self.expected_responses): - self._raise_unexpected_response(None) - - # Cleanup - self.expected_responses = None - self.current_response = None + finally: + # Cleanup + self.set_input_flow(None) + self.expected_responses = None + self.current_response = None + self.ui.pin = None return False From ba3d90b9942437770bc55166700a28975a30bc82 Mon Sep 17 00:00:00 2001 From: matejcik Date: Tue, 11 Feb 2020 11:33:24 +0100 Subject: [PATCH 05/10] tests: add wipe code activation test --- .../test_msg_change_wipe_code_t2.py | 39 ------- tests/persistence_tests/test_wipe_code.py | 108 ++++++++++++++++++ 2 files changed, 108 insertions(+), 39 deletions(-) create mode 100644 tests/persistence_tests/test_wipe_code.py diff --git a/tests/device_tests/test_msg_change_wipe_code_t2.py b/tests/device_tests/test_msg_change_wipe_code_t2.py index d6ffd4df3f..04e932c2c9 100644 --- a/tests/device_tests/test_msg_change_wipe_code_t2.py +++ b/tests/device_tests/test_msg_change_wipe_code_t2.py @@ -219,42 +219,3 @@ def test_set_pin_to_wipe_code(client): ) client.set_input_flow(_input_flow_set_pin(client.debug, WIPE_CODE4)) device.change_pin(client) - - -@pytest.mark.setup_client(pin=PIN4) -def test_wipe_code_activate(client): - import time - - device_id = client.features.device_id - - # Set wipe code. - with client: - client.set_expected_responses( - [messages.ButtonRequest()] * 5 + [messages.Success(), messages.Features()] - ) - client.set_input_flow(_input_flow_set_wipe_code(client.debug, PIN4, WIPE_CODE4)) - - device.change_wipe_code(client) - - # Try to change the PIN. - ret = client.call_raw(messages.ChangePin(remove=False)) - - # Confirm change PIN. - assert isinstance(ret, messages.ButtonRequest) - client.debug.press_yes() - ret = client.call_raw(messages.ButtonAck()) - - # Enter the wipe code instead of the current PIN - assert ret == messages.ButtonRequest(code=messages.ButtonRequestType.Other) - client.debug.input(WIPE_CODE4) - client._raw_write(messages.ButtonAck()) - - # Allow the device to display wipe code popup and restart. - time.sleep(7) - - # Check that the device has been wiped. - client.init_device() - assert client.features.initialized is False - assert client.features.pin_protection is False - assert client.features.wipe_code_protection is False - assert client.features.device_id != device_id diff --git a/tests/persistence_tests/test_wipe_code.py b/tests/persistence_tests/test_wipe_code.py new file mode 100644 index 0000000000..ed2cb12b13 --- /dev/null +++ b/tests/persistence_tests/test_wipe_code.py @@ -0,0 +1,108 @@ +from trezorlib import debuglink, device, messages + +from ..common import MNEMONIC12 +from ..emulators import EmulatorWrapper +from ..upgrade_tests import core_only, legacy_only + +PIN = "1234" +WIPE_CODE = "9876" + + +def setup_device_legacy(client, pin, wipe_code): + device.wipe(client) + debuglink.load_device( + client, MNEMONIC12, pin, passphrase_protection=False, label="WIPECODE" + ) + + with client: + client.set_pin([PIN, WIPE_CODE, WIPE_CODE]) + device.change_wipe_code(client) + + +def setup_device_core(client, pin, wipe_code): + device.wipe(client) + debuglink.load_device( + client, MNEMONIC12, pin, passphrase_protection=False, label="WIPECODE" + ) + + def input_flow(): + yield # do you want to set/change the wipe_code? + client.debug.press_yes() + if pin is not None: + yield # enter current pin + client.debug.input(pin) + yield # enter new wipe code + client.debug.input(wipe_code) + yield # enter new wipe code again + client.debug.input(wipe_code) + yield # success + client.debug.press_yes() + + with client: + client.set_expected_responses( + [messages.ButtonRequest()] * 5 + [messages.Success(), messages.Features()] + ) + client.set_input_flow(input_flow) + device.change_wipe_code(client) + + +@core_only +def test_wipe_code_activate_core(): + with EmulatorWrapper("core") as emu: + # set up device + setup_device_core(emu.client, PIN, WIPE_CODE) + + emu.client.init_device() + device_id = emu.client.features.device_id + + # Initiate Change pin process + ret = emu.client.call_raw(messages.ChangePin(remove=False)) + assert isinstance(ret, messages.ButtonRequest) + emu.client.debug.press_yes() + ret = emu.client.call_raw(messages.ButtonAck()) + + # Enter the wipe code instead of the current PIN + assert ret == messages.ButtonRequest(code=messages.ButtonRequestType.Other) + emu.client._raw_write(messages.ButtonAck()) + emu.client.debug.input(WIPE_CODE) + + # wait 30 seconds for emulator to shut down + # this will raise a TimeoutError if the emulator doesn't die. + emu.wait(30) + + emu.start() + assert emu.client.features.initialized is False + assert emu.client.features.pin_protection is False + assert emu.client.features.wipe_code_protection is False + assert emu.client.features.device_id != device_id + + +@legacy_only +def test_wipe_code_activate_legacy(): + with EmulatorWrapper("legacy") as emu: + # set up device + setup_device_legacy(emu.client, PIN, WIPE_CODE) + + emu.client.init_device() + device_id = emu.client.features.device_id + + # Initiate Change pin process + ret = emu.client.call_raw(messages.ChangePin(remove=False)) + assert isinstance(ret, messages.ButtonRequest) + emu.client.debug.press_yes() + ret = emu.client.call_raw(messages.ButtonAck()) + + # Enter the wipe code instead of the current PIN + assert isinstance(ret, messages.PinMatrixRequest) + wipe_code_encoded = emu.client.debug.encode_pin(WIPE_CODE) + emu.client._raw_write(messages.PinMatrixAck(pin=wipe_code_encoded)) + + # wait 30 seconds for emulator to shut down + # this will raise a TimeoutError if the emulator doesn't die. + emu.wait(30) + + emu.start() + assert emu.client.features.initialized is False + assert emu.client.features.pin_protection is False + assert emu.client.features.wipe_code_protection is False + assert emu.client.features.device_id != device_id From 1bcf856946ddd5481c474f7fc0bee52c87664fd3 Mon Sep 17 00:00:00 2001 From: matejcik Date: Tue, 11 Feb 2020 11:36:02 +0100 Subject: [PATCH 06/10] tests: simplify change_wipe_code test with PIN queuing --- .../test_msg_change_wipe_code_t1.py | 203 +++++++----------- 1 file changed, 75 insertions(+), 128 deletions(-) diff --git a/tests/device_tests/test_msg_change_wipe_code_t1.py b/tests/device_tests/test_msg_change_wipe_code_t1.py index 8468324b99..aeff96b910 100644 --- a/tests/device_tests/test_msg_change_wipe_code_t1.py +++ b/tests/device_tests/test_msg_change_wipe_code_t1.py @@ -16,7 +16,9 @@ import pytest -from trezorlib import messages +from trezorlib import device, exceptions, messages + +PinType = messages.PinMatrixRequestType PIN4 = "1234" WIPE_CODE4 = "4321" @@ -27,77 +29,46 @@ pytestmark = pytest.mark.skip_t2 def _set_wipe_code(client, wipe_code): # Set/change wipe code. - ret = client.call_raw(messages.ChangeWipeCode()) - assert isinstance(ret, messages.ButtonRequest) + with client: + if client.features.pin_protection: + pin, _ = client.debug.read_pin() + pins = [pin, wipe_code, wipe_code] + pin_matrices = [ + messages.PinMatrixRequest(type=PinType.Current), + messages.PinMatrixRequest(type=PinType.WipeCodeFirst), + messages.PinMatrixRequest(type=PinType.WipeCodeSecond), + ] + else: + pins = [wipe_code, wipe_code] + pin_matrices = [ + messages.PinMatrixRequest(type=PinType.WipeCodeFirst), + messages.PinMatrixRequest(type=PinType.WipeCodeSecond), + ] - # Confirm intent to set/change wipe code. - client.debug.press_yes() - ret = client.call_raw(messages.ButtonAck()) - - if client.features.pin_protection: - # Send current PIN. - assert isinstance(ret, messages.PinMatrixRequest) - pin_encoded = client.debug.read_pin_encoded() - ret = client.call_raw(messages.PinMatrixAck(pin=pin_encoded)) - - # Send the new wipe code for the first time. - assert isinstance(ret, messages.PinMatrixRequest) - wipe_code_encoded = client.debug.encode_pin(wipe_code) - ret = client.call_raw(messages.PinMatrixAck(pin=wipe_code_encoded)) - - # Send the new wipe code for the second time. - assert isinstance(ret, messages.PinMatrixRequest) - wipe_code_encoded = client.debug.encode_pin(wipe_code) - ret = client.call_raw(messages.PinMatrixAck(pin=wipe_code_encoded)) - - # Now we're done. - assert isinstance(ret, messages.Success) + client.set_pin(pins) + client.set_expected_responses( + [messages.ButtonRequest()] + + pin_matrices + + [messages.Success(), messages.Features()] + ) + device.change_wipe_code(client) -def _remove_wipe_code(client): - # Remove wipe code - ret = client.call_raw(messages.ChangeWipeCode(remove=True)) - assert isinstance(ret, messages.ButtonRequest) - - # Confirm intent to remove wipe code. - client.debug.press_yes() - ret = client.call_raw(messages.ButtonAck()) - - # Send current PIN. - assert isinstance(ret, messages.PinMatrixRequest) - pin_encoded = client.debug.read_pin_encoded() - ret = client.call_raw(messages.PinMatrixAck(pin=pin_encoded)) - - # Now we're done. - assert isinstance(ret, messages.Success) +def _change_pin(client, old_pin, new_pin): + assert client.features.pin_protection is True + with client: + client.set_pin([old_pin, new_pin, new_pin]) + try: + return device.change_pin(client) + except exceptions.TrezorFailure as f: + return f.failure def _check_wipe_code(client, wipe_code): - # Try to change the PIN to the current wipe code value. The operation should fail. - ret = client.call_raw(messages.ChangePin()) - assert isinstance(ret, messages.ButtonRequest) - - # Confirm intent to change PIN. - client.debug.press_yes() - ret = client.call_raw(messages.ButtonAck()) - - # Send current PIN. - assert isinstance(ret, messages.PinMatrixRequest) - pin_encoded = client.debug.read_pin_encoded() - ret = client.call_raw(messages.PinMatrixAck(pin=pin_encoded)) - - # Send the new wipe code for the first time. - assert isinstance(ret, messages.PinMatrixRequest) - wipe_code_encoded = client.debug.encode_pin(wipe_code) - ret = client.call_raw(messages.PinMatrixAck(pin=wipe_code_encoded)) - - # Send the new wipe code for the second time. - assert isinstance(ret, messages.PinMatrixRequest) - wipe_code_encoded = client.debug.encode_pin(wipe_code) - ret = client.call_raw(messages.PinMatrixAck(pin=wipe_code_encoded)) - - # Expect failure. - assert isinstance(ret, messages.Failure) + """Check that wipe code is set by changing the PIN to it.""" + old_pin, _ = client.debug.read_pin() + f = _change_pin(client, old_pin, wipe_code) + assert isinstance(f, messages.Failure) @pytest.mark.setup_client(pin=PIN4) @@ -122,11 +93,11 @@ def test_set_remove_wipe_code(client): client.init_device() assert client.features.wipe_code_protection is True - # Check that the PIN is correct. + # Check that the wipe code is correct. _check_wipe_code(client, WIPE_CODE6) # Test remove wipe code. - _remove_wipe_code(client) + device.change_wipe_code(client, remove=True) # Check that there's no wipe code protection now. client.init_device() @@ -138,26 +109,18 @@ def test_set_wipe_code_mismatch(client): assert client.features.wipe_code_protection is False # Let's set a new wipe code. - ret = client.call_raw(messages.ChangeWipeCode()) - assert isinstance(ret, messages.ButtonRequest) - - # Confirm intent to set wipe code. - client.debug.press_yes() - ret = client.call_raw(messages.ButtonAck()) - - # Send the new wipe code for the first time. - assert isinstance(ret, messages.PinMatrixRequest) - wipe_code_encoded = client.debug.encode_pin(WIPE_CODE4) - ret = client.call_raw(messages.PinMatrixAck(pin=wipe_code_encoded)) - - # Send the new wipe code for the second time, but different. - assert isinstance(ret, messages.PinMatrixRequest) - wipe_code_encoded = client.debug.encode_pin(WIPE_CODE6) - ret = client.call_raw(messages.PinMatrixAck(pin=wipe_code_encoded)) - - # The operation should fail, because the wipe codes are different. - assert isinstance(ret, messages.Failure) - assert ret.code == messages.FailureType.WipeCodeMismatch + with client: + client.set_pin([WIPE_CODE4, WIPE_CODE6]) + client.set_expected_responses( + [ + messages.ButtonRequest(), + messages.PinMatrixRequest(type=PinType.WipeCodeFirst), + messages.PinMatrixRequest(type=PinType.WipeCodeSecond), + messages.Failure(code=messages.FailureType.WipeCodeMismatch), + ] + ) + with pytest.raises(exceptions.TrezorFailure): + device.change_wipe_code(client) # Check that there is no wipe code protection. client.init_device() @@ -170,26 +133,18 @@ def test_set_wipe_code_to_pin(client): assert client.features.wipe_code_protection is None # Let's try setting the wipe code to the curent PIN value. - ret = client.call_raw(messages.ChangeWipeCode()) - assert isinstance(ret, messages.ButtonRequest) - - # Confirm intent to set wipe code. - client.debug.press_yes() - ret = client.call_raw(messages.ButtonAck()) - - # Send current PIN. - assert isinstance(ret, messages.PinMatrixRequest) - pin_encoded = client.debug.read_pin_encoded() - ret = client.call_raw(messages.PinMatrixAck(pin=pin_encoded)) - - # Send the new wipe code. - assert isinstance(ret, messages.PinMatrixRequest) - pin_encoded = client.debug.read_pin_encoded() - ret = client.call_raw(messages.PinMatrixAck(pin=pin_encoded)) - - # The operation should fail, because the wipe code must be different from the PIN. - assert isinstance(ret, messages.Failure) - assert ret.code == messages.FailureType.ProcessError + with client: + client.set_pin([PIN4, PIN4]) + client.set_expected_responses( + [ + messages.ButtonRequest(), + messages.PinMatrixRequest(type=PinType.Current), + messages.PinMatrixRequest(type=PinType.WipeCodeFirst), + messages.Failure(code=messages.FailureType.ProcessError), + ] + ) + with pytest.raises(exceptions.TrezorFailure): + device.change_wipe_code(client) # Check that there is no wipe code protection. client.init_device() @@ -201,26 +156,18 @@ def test_set_pin_to_wipe_code(client): _set_wipe_code(client, WIPE_CODE4) # Try to set the PIN to the current wipe code value. - ret = client.call_raw(messages.ChangePin()) - assert isinstance(ret, messages.ButtonRequest) - - # Confirm intent to set PIN. - client.debug.press_yes() - ret = client.call_raw(messages.ButtonAck()) - - # Send the new PIN for the first time. - assert isinstance(ret, messages.PinMatrixRequest) - pin_encoded = client.debug.encode_pin(WIPE_CODE4) - ret = client.call_raw(messages.PinMatrixAck(pin=pin_encoded)) - - # Send the new PIN for the second time. - assert isinstance(ret, messages.PinMatrixRequest) - pin_encoded = client.debug.encode_pin(WIPE_CODE4) - ret = client.call_raw(messages.PinMatrixAck(pin=pin_encoded)) - - # The operation should fail, because the PIN must be different from the wipe code. - assert isinstance(ret, messages.Failure) - assert ret.code == messages.FailureType.ProcessError + with client: + client.set_pin([WIPE_CODE4, WIPE_CODE4]) + client.set_expected_responses( + [ + messages.ButtonRequest(), + messages.PinMatrixRequest(type=PinType.NewFirst), + messages.PinMatrixRequest(type=PinType.NewSecond), + messages.Failure(code=messages.FailureType.ProcessError), + ] + ) + with pytest.raises(exceptions.TrezorFailure): + device.change_pin(client) # Check that there is no PIN protection. client.init_device() From 271da3fa3915db30cfa99d995bfa7bd91bdce25e Mon Sep 17 00:00:00 2001 From: matejcik Date: Wed, 12 Feb 2020 11:18:58 +0100 Subject: [PATCH 07/10] python: add detailed logging to emulator runner --- core/emu.py | 10 +++++----- python/src/trezorlib/_internal/emulator.py | 22 ++++++++++++++++++++-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/core/emu.py b/core/emu.py index 245be2de19..ed39394a75 100755 --- a/core/emu.py +++ b/core/emu.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import gzip +import logging import os import platform import signal @@ -54,7 +55,6 @@ def watch_emulator(emulator): try: for _, type_names, _, _ in watch.event_gen(yield_nones=False): if "IN_CLOSE_WRITE" in type_names: - click.echo("Restarting...") emulator.restart() except KeyboardInterrupt: emulator.stop() @@ -198,6 +198,10 @@ def cli( if quiet: output = None + logger = logging.getLogger("trezorlib._internal.emulator") + logger.setLevel(logging.INFO) + logger.addHandler(logging.StreamHandler()) + emulator = CoreEmulator( executable, profile_dir, @@ -230,11 +234,7 @@ def cli( run_debugger(emulator) raise RuntimeError("run_debugger should not return") - click.echo("Waiting for emulator to come up... ", err=True) - start = time.monotonic() emulator.start() - end = time.monotonic() - click.echo(f"Emulator ready after {end - start:.3f} seconds", err=True) if mnemonics: if slip0014: diff --git a/python/src/trezorlib/_internal/emulator.py b/python/src/trezorlib/_internal/emulator.py index de95479b40..6873cd8526 100644 --- a/python/src/trezorlib/_internal/emulator.py +++ b/python/src/trezorlib/_internal/emulator.py @@ -14,6 +14,7 @@ # You should have received a copy of the License along with this library. # If not, see . +import logging import os import subprocess import time @@ -22,6 +23,10 @@ from pathlib import Path from trezorlib.debuglink import TrezorClientDebugLink from trezorlib.transport.udp import UdpTransport +LOG = logging.getLogger(__name__) + +EMULATOR_WAIT_TIME = 60 + def _rm_f(path): try: @@ -84,9 +89,10 @@ class Emulator: def _get_transport(self): return UdpTransport("127.0.0.1:{}".format(self.port)) - def wait_until_ready(self, timeout=60): + def wait_until_ready(self, timeout=EMULATOR_WAIT_TIME): transport = self._get_transport() transport.open() + LOG.info("Waiting for emulator to come up...") start = time.monotonic() try: while True: @@ -103,8 +109,11 @@ class Emulator: finally: transport.close() + LOG.info("Emulator ready after {:.3f} seconds".format(time.monotonic() - start)) + def wait(self, timeout=None): ret = self.process.wait(timeout=timeout) + self.process = None self.stop() return ret @@ -129,6 +138,7 @@ class Emulator: if self.process: if self.process.poll() is not None: # process has died, stop and start again + LOG.info("Starting from a stopped process.") self.stop() else: # process is running, no need to start again @@ -139,6 +149,9 @@ class Emulator: self.wait_until_ready() except TimeoutError: # Assuming that after the default 60-second timeout, the process is stuck + LOG.warning( + "Emulator did not come up after {} seconds".format(EMULATOR_WAIT_TIME) + ) self.process.kill() raise @@ -156,10 +169,15 @@ class Emulator: self.client = None if self.process: + LOG.info("Terminating emulator...") + start = time.monotonic() self.process.terminate() try: - self.process.wait(30) + self.process.wait(EMULATOR_WAIT_TIME) + end = time.monotonic() + LOG.info("Emulator shut down after {:.3f} seconds".format(end - start)) except subprocess.TimeoutExpired: + LOG.info("Emulator seems stuck. Sending kill signal.") self.process.kill() _rm_f(self.profile_dir / "trezor.pid") From 4c8c96272c12a6931b9701e9cb6235125a79020b Mon Sep 17 00:00:00 2001 From: matejcik Date: Wed, 12 Feb 2020 12:31:37 +0100 Subject: [PATCH 08/10] emu: fix flag options with defaults Click REALLY INSISTS you provide on/off switches for your options. You can use is_flag, but then the presence of the option changes based on the default value. Which makes sense, really: @option("-f", "foobar", is_flag=True, default=False) you would expect `./cli -f` to have `foobar is True` whereas with @option("-f", "foobar", is_flag=True, default=True) you would expect `./cli -f` to have `foobar is False`, otherwise it's a no-op this becomes fun with `default=os.environ.get("SOMETHING")`, because then the effect of the option CHANGES with a value of environment variable! there's two ways around this: a) don't use defaults, update the flag explicitly, like: foobar = foobar or os.environ.get("FOOBAR") == "1" b) forget about is_flag and specify an on/off switch, where the default value works as intended since the latter is also technically speaking more correct, i'm doing it --- core/emu.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/core/emu.py b/core/emu.py index ed39394a75..b25396b384 100755 --- a/core/emu.py +++ b/core/emu.py @@ -77,19 +77,23 @@ def run_debugger(emulator): ) +def _from_env(name): + return os.environ.get(name) == "1" + + @click.command(context_settings=dict(ignore_unknown_options=True)) # fmt: off -@click.option("-a", "--disable-animation", is_flag=True, default=os.environ.get("TREZOR_DISABLE_ANIMATION") == "1", help="Disable animation") +@click.option("-a", "--disable-animation/--enable-animation", default=_from_env("TREZOR_DISABLE_ANIMATION"), help="Disable animation") @click.option("-c", "--command", "run_command", is_flag=True, help="Run command while emulator is running") -@click.option("-d", "--production", is_flag=True, default=os.environ.get("PYOPT") == "1", help="Production mode (debuglink disabled)") +@click.option("-d", "--production/--no-production", default=_from_env("PYOPT"), help="Production mode (debuglink disabled)") @click.option("-D", "--debugger", is_flag=True, help="Run emulator in debugger (gdb/lldb)") @click.option("--executable", type=click.Path(exists=True, dir_okay=False), default=os.environ.get("MICROPYTHON"), help="Alternate emulator executable") -@click.option("-g", "--profiling", is_flag=True, default=os.environ.get("TREZOR_PROFILING"), help="Run with profiler wrapper") +@click.option("-g", "--profiling/--no-profiling", default=_from_env("TREZOR_PROFILING"), help="Run with profiler wrapper") @click.option("-h", "--headless", is_flag=True, help="Headless mode (no display)") @click.option("--heap-size", metavar="SIZE", default="20M", help="Configure heap size") @click.option("--main", help="Path to python main file") @click.option("--mnemonic", "mnemonics", multiple=True, help="Initialize device with given mnemonic. Specify multiple times for Shamir shares.") -@click.option("--log-memory", is_flag=True, default=os.environ.get("TREZOR_LOG_MEMORY") == "1", help="Print memory usage after workflows") +@click.option("--log-memory/--no-log-memory", default=_from_env("TREZOR_LOG_MEMORY"), help="Print memory usage after workflows") @click.option("-o", "--output", type=click.File("w"), default="-", help="Redirect emulator output to file") @click.option("-p", "--profile", metavar="NAME", help="Profile name or path") @click.option("-P", "--port", metavar="PORT", type=int, default=int(os.environ.get("TREZOR_UDP_PORT", 0)) or None, help="UDP port number") From 81a03edf617fef3e67c10440a629f1dbb98b4ba8 Mon Sep 17 00:00:00 2001 From: matejcik Date: Wed, 12 Feb 2020 15:38:18 +0100 Subject: [PATCH 09/10] python/debuglink: add docstrings, rename functions for clearer usage --- python/src/trezorlib/debuglink.py | 91 ++++++++++++++----- ...st_msg_cardano_get_address_slip39_basic.py | 2 +- ...msg_cardano_get_public_key_slip39_basic.py | 2 +- .../test_msg_cardano_sign_tx_slip39_basic.py | 2 +- .../test_msg_change_wipe_code_t1.py | 10 +- tests/device_tests/test_msg_loaddevice.py | 10 +- .../test_passphrase_slip39_advanced.py | 8 +- .../test_passphrase_slip39_basic.py | 4 +- tests/device_tests/test_protect_call.py | 13 +-- tests/device_tests/test_protection_levels.py | 2 +- tests/persistence_tests/test_wipe_code.py | 2 +- 11 files changed, 92 insertions(+), 54 deletions(-) diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py index 132a631305..758b9f782c 100644 --- a/python/src/trezorlib/debuglink.py +++ b/python/src/trezorlib/debuglink.py @@ -184,8 +184,11 @@ class DebugUI: def __init__(self, debuglink: DebugLink): self.debuglink = debuglink - self.pin = None - self.passphrase = "sphinx of black quartz, judge my wov" + self.clear() + + def clear(self): + self.pins = None + self.passphrase = "" self.input_flow = None def button_request(self, code): @@ -221,15 +224,15 @@ class DebugUI: self.input_flow = self.INPUT_FLOW_DONE def get_pin(self, code=None): - if isinstance(self.pin, str): - return self.debuglink.encode_pin(self.pin) - elif self.pin == []: - raise AssertionError("PIN sequence ended prematurely") - elif self.pin: - return self.debuglink.encode_pin(self.pin.pop(0)) - else: + if self.pins is None: + # respond with correct pin return self.debuglink.read_pin_encoded() + if self.pins == []: + raise AssertionError("PIN sequence ended prematurely") + else: + return self.debuglink.encode_pin(self.pins.pop(0)) + def get_passphrase(self, available_on_device): return self.passphrase @@ -269,8 +272,6 @@ class TrezorClientDebugLink(TrezorClient): self.expected_responses = None self.current_response = None - # Use blank passphrase - self.set_passphrase("") super().__init__(transport, ui=self.ui) def open(self): @@ -282,6 +283,15 @@ class TrezorClientDebugLink(TrezorClient): super().close() def set_filter(self, message_type, callback): + """Configure a filter function for a specified message type. + + The `callback` must be a function that accepts a protobuf message, and returns + a (possibly modified) protobuf message of the same type. Whenever a message + is sent or received that matches `message_type`, `callback` is invoked on the + message and its result is substituted for the original. + + Useful for test scenarios with an active malicious actor on the wire. + """ self.filters[message_type] = callback def _filter_message(self, msg): @@ -293,10 +303,30 @@ class TrezorClientDebugLink(TrezorClient): return msg def set_input_flow(self, input_flow): - if input_flow is None: - self.ui.input_flow = None - return + """Configure a sequence of input events for the current with-block. + The `input_flow` must be a generator function. A `yield` statement in the + input flow function waits for a ButtonRequest from the device, and returns + its code. + + Example usage: + + >>> def input_flow(): + >>> # wait for first button prompt + >>> code = yield + >>> assert code == ButtonRequestType.Other + >>> # press No + >>> client.debug.press_no() + >>> + >>> # wait for second button prompt + >>> yield + >>> # press Yes + >>> client.debug.press_yes() + >>> + >>> with client: + >>> client.set_input_flow(input_flow) + >>> some_call(client) + """ if not self.in_with_statement: raise RuntimeError("Must be called inside 'with' statement") @@ -331,29 +361,44 @@ class TrezorClientDebugLink(TrezorClient): finally: # Cleanup - self.set_input_flow(None) self.expected_responses = None self.current_response = None - self.ui.pin = None + self.ui.clear() return False def set_expected_responses(self, expected): + """Set a sequence of expected responses to client calls. + + Within a given with-block, the list of received responses from device must + match the list of expected responses, otherwise an AssertionError is raised. + + If an expected response is given a field value other than None, that field value + must exactly match the received field value. If a given field is None + (or unspecified) in the expected response, the received field value is not + checked. + """ if not self.in_with_statement: raise RuntimeError("Must be called inside 'with' statement") self.expected_responses = expected self.current_response = 0 - def set_pin(self, pin): - if isinstance(pin, str): - self.ui.pin = pin - else: - self.ui.pin = list(pin) + def use_pin_sequence(self, pins): + """Respond to PIN prompts from device with the provided PINs. + The sequence must be at least as long as the expected number of PIN prompts. + """ + # XXX This currently only works on T1 as a response to PinMatrixRequest, but + # if we modify trezor-core to introduce PIN prompts predictably (i.e. by + # a new ButtonRequestType), it could also be used on TT via debug.input() + self.ui.pins = list(pins) - def set_passphrase(self, passphrase): + def use_passphrase(self, passphrase): + """Respond to passphrase prompts from device with the provided passphrase.""" self.ui.passphrase = Mnemonic.normalize_string(passphrase) - def set_mnemonic(self, mnemonic): + def use_mnemonic(self, mnemonic): + """Use the provided mnemonic to respond to device. + Only applies to T1, where device prompts the host for mnemonic words.""" self.mnemonic = Mnemonic.normalize_string(mnemonic).split(" ") def _raw_read(self): diff --git a/tests/device_tests/test_msg_cardano_get_address_slip39_basic.py b/tests/device_tests/test_msg_cardano_get_address_slip39_basic.py index 2c4cb2df76..765419912e 100644 --- a/tests/device_tests/test_msg_cardano_get_address_slip39_basic.py +++ b/tests/device_tests/test_msg_cardano_get_address_slip39_basic.py @@ -46,7 +46,7 @@ from ..common import MNEMONIC_SLIP39_BASIC_20_3of6 def test_cardano_get_address(client, path, expected_address): # enter passphrase assert client.features.passphrase_protection is True - client.set_passphrase("TREZOR") + client.use_passphrase("TREZOR") address = get_address(client, parse_path(path)) assert address == expected_address diff --git a/tests/device_tests/test_msg_cardano_get_public_key_slip39_basic.py b/tests/device_tests/test_msg_cardano_get_public_key_slip39_basic.py index 7385de3446..48997326ae 100644 --- a/tests/device_tests/test_msg_cardano_get_public_key_slip39_basic.py +++ b/tests/device_tests/test_msg_cardano_get_public_key_slip39_basic.py @@ -49,7 +49,7 @@ from ..common import MNEMONIC_SLIP39_BASIC_20_3of6 def test_cardano_get_public_key(client, path, public_key, chain_code): # enter passphrase assert client.features.passphrase_protection is True - client.set_passphrase("TREZOR") + client.use_passphrase("TREZOR") key = get_public_key(client, parse_path(path)) diff --git a/tests/device_tests/test_msg_cardano_sign_tx_slip39_basic.py b/tests/device_tests/test_msg_cardano_sign_tx_slip39_basic.py index 75e5d62c05..558721616c 100644 --- a/tests/device_tests/test_msg_cardano_sign_tx_slip39_basic.py +++ b/tests/device_tests/test_msg_cardano_sign_tx_slip39_basic.py @@ -137,7 +137,7 @@ def test_cardano_sign_tx( client.debug.swipe_up() client.debug.press_yes() - client.set_passphrase("TREZOR") + client.use_passphrase("TREZOR") with client: client.set_expected_responses(expected_responses) client.set_input_flow(input_flow) diff --git a/tests/device_tests/test_msg_change_wipe_code_t1.py b/tests/device_tests/test_msg_change_wipe_code_t1.py index aeff96b910..97f3c97441 100644 --- a/tests/device_tests/test_msg_change_wipe_code_t1.py +++ b/tests/device_tests/test_msg_change_wipe_code_t1.py @@ -45,7 +45,7 @@ def _set_wipe_code(client, wipe_code): messages.PinMatrixRequest(type=PinType.WipeCodeSecond), ] - client.set_pin(pins) + client.use_pin_sequence(pins) client.set_expected_responses( [messages.ButtonRequest()] + pin_matrices @@ -57,7 +57,7 @@ def _set_wipe_code(client, wipe_code): def _change_pin(client, old_pin, new_pin): assert client.features.pin_protection is True with client: - client.set_pin([old_pin, new_pin, new_pin]) + client.use_pin_sequence([old_pin, new_pin, new_pin]) try: return device.change_pin(client) except exceptions.TrezorFailure as f: @@ -110,7 +110,7 @@ def test_set_wipe_code_mismatch(client): # Let's set a new wipe code. with client: - client.set_pin([WIPE_CODE4, WIPE_CODE6]) + client.use_pin_sequence([WIPE_CODE4, WIPE_CODE6]) client.set_expected_responses( [ messages.ButtonRequest(), @@ -134,7 +134,7 @@ def test_set_wipe_code_to_pin(client): # Let's try setting the wipe code to the curent PIN value. with client: - client.set_pin([PIN4, PIN4]) + client.use_pin_sequence([PIN4, PIN4]) client.set_expected_responses( [ messages.ButtonRequest(), @@ -157,7 +157,7 @@ def test_set_pin_to_wipe_code(client): # Try to set the PIN to the current wipe code value. with client: - client.set_pin([WIPE_CODE4, WIPE_CODE4]) + client.use_pin_sequence([WIPE_CODE4, WIPE_CODE4]) client.set_expected_responses( [ messages.ButtonRequest(), diff --git a/tests/device_tests/test_msg_loaddevice.py b/tests/device_tests/test_msg_loaddevice.py index 6ff5f0d753..06bf70c59c 100644 --- a/tests/device_tests/test_msg_loaddevice.py +++ b/tests/device_tests/test_msg_loaddevice.py @@ -52,7 +52,7 @@ class TestDeviceLoad: passphrase_protection=True, label="test", ) - client.set_passphrase("passphrase") + client.use_passphrase("passphrase") state = client.debug.state() assert state.mnemonic_secret == MNEMONIC12.encode() @@ -114,7 +114,7 @@ class TestDeviceLoad: language="en-US", skip_checksum=True, ) - client.set_passphrase(passphrase_nfkd) + client.use_passphrase(passphrase_nfkd) address_nfkd = btc.get_address(client, "Bitcoin", []) device.wipe(client) @@ -127,7 +127,7 @@ class TestDeviceLoad: language="en-US", skip_checksum=True, ) - client.set_passphrase(passphrase_nfc) + client.use_passphrase(passphrase_nfc) address_nfc = btc.get_address(client, "Bitcoin", []) device.wipe(client) @@ -140,7 +140,7 @@ class TestDeviceLoad: language="en-US", skip_checksum=True, ) - client.set_passphrase(passphrase_nfkc) + client.use_passphrase(passphrase_nfkc) address_nfkc = btc.get_address(client, "Bitcoin", []) device.wipe(client) @@ -153,7 +153,7 @@ class TestDeviceLoad: language="en-US", skip_checksum=True, ) - client.set_passphrase(passphrase_nfd) + client.use_passphrase(passphrase_nfd) address_nfd = btc.get_address(client, "Bitcoin", []) assert address_nfkd == address_nfc diff --git a/tests/device_tests/test_passphrase_slip39_advanced.py b/tests/device_tests/test_passphrase_slip39_advanced.py index 56dd68da25..4ffa1b9086 100644 --- a/tests/device_tests/test_passphrase_slip39_advanced.py +++ b/tests/device_tests/test_passphrase_slip39_advanced.py @@ -30,12 +30,12 @@ def test_128bit_passphrase(client): xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV """ assert client.features.passphrase_protection is True - client.set_passphrase("TREZOR") + client.use_passphrase("TREZOR") address = btc.get_address(client, "Bitcoin", []) assert address == "1CX5rv2vbSV8YFAZEAdMwRVqbxxswPnSPw" client.state = None client.clear_session() - client.set_passphrase("ROZERT") + client.use_passphrase("ROZERT") address_compare = btc.get_address(client, "Bitcoin", []) assert address != address_compare @@ -49,11 +49,11 @@ def test_256bit_passphrase(client): xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c """ assert client.features.passphrase_protection is True - client.set_passphrase("TREZOR") + client.use_passphrase("TREZOR") address = btc.get_address(client, "Bitcoin", []) assert address == "18oNx6UczHWASBQXc5XQqdSdAAZyhUwdQU" client.state = None client.clear_session() - client.set_passphrase("ROZERT") + client.use_passphrase("ROZERT") address_compare = btc.get_address(client, "Bitcoin", []) assert address != address_compare diff --git a/tests/device_tests/test_passphrase_slip39_basic.py b/tests/device_tests/test_passphrase_slip39_basic.py index 8520bffa04..5785d0a113 100644 --- a/tests/device_tests/test_passphrase_slip39_basic.py +++ b/tests/device_tests/test_passphrase_slip39_basic.py @@ -30,7 +30,7 @@ def test_3of6_passphrase(client): xprv9s21ZrQH143K2pMWi8jrTawHaj16uKk4CSbvo4Zt61tcrmuUDMx2o1Byzcr3saXNGNvHP8zZgXVdJHsXVdzYFPavxvCyaGyGr1WkAYG83ce """ assert client.features.passphrase_protection is True - client.set_passphrase("TREZOR") + client.use_passphrase("TREZOR") address = btc.get_address(client, "Bitcoin", []) assert address == "18oZEMRWurCZW1FeK8sWYyXuWx2bFqEKyX" @@ -50,6 +50,6 @@ def test_2of5_passphrase(client): xprv9s21ZrQH143K2o6EXEHpVy8TCYoMmkBnDCCESLdR2ieKwmcNG48ck2XJQY4waS7RUQcXqR9N7HnQbUVEDMWYyREdF1idQqxFHuCfK7fqFni """ assert client.features.passphrase_protection is True - client.set_passphrase("TREZOR") + client.use_passphrase("TREZOR") address = btc.get_address(client, "Bitcoin", []) assert address == "19Fjs9AvT13Y2Nx8GtoVfADmFWnccsPinQ" diff --git a/tests/device_tests/test_protect_call.py b/tests/device_tests/test_protect_call.py index 123510eec8..c0a1d1ee08 100644 --- a/tests/device_tests/test_protect_call.py +++ b/tests/device_tests/test_protect_call.py @@ -46,30 +46,23 @@ class TestProtectCall: @pytest.mark.setup_client(pin="1234") def test_incorrect_pin(self, client): - client.set_pin("5678") with pytest.raises(PinException): + client.use_pin_sequence(["5678"]) self._some_protected_call(client) @pytest.mark.setup_client(pin="1234", passphrase=True) def test_exponential_backoff_with_reboot(self, client): - client.set_pin("5678") - def test_backoff(attempts, start): if attempts <= 1: expected = 0 else: expected = (2 ** (attempts - 1)) - 1 got = round(time.time() - start, 2) - - msg = "Pin delay expected to be at least %s seconds, got %s" % ( - expected, - got, - ) - print(msg) assert got >= expected for attempt in range(1, 4): start = time.time() - with pytest.raises(PinException): + with client, pytest.raises(PinException): + client.use_pin_sequence(["5678"]) self._some_protected_call(client) test_backoff(attempt, start) diff --git a/tests/device_tests/test_protection_levels.py b/tests/device_tests/test_protection_levels.py index d4db71f3ef..9c2b14bab5 100644 --- a/tests/device_tests/test_protection_levels.py +++ b/tests/device_tests/test_protection_levels.py @@ -125,7 +125,7 @@ class TestProtectionLevels: @pytest.mark.setup_client(uninitialized=True) def test_recovery_device(self, client): - client.set_mnemonic(MNEMONIC12) + client.use_mnemonic(MNEMONIC12) with client: client.set_expected_responses( [proto.ButtonRequest()] diff --git a/tests/persistence_tests/test_wipe_code.py b/tests/persistence_tests/test_wipe_code.py index ed2cb12b13..ba481b72d3 100644 --- a/tests/persistence_tests/test_wipe_code.py +++ b/tests/persistence_tests/test_wipe_code.py @@ -15,7 +15,7 @@ def setup_device_legacy(client, pin, wipe_code): ) with client: - client.set_pin([PIN, WIPE_CODE, WIPE_CODE]) + client.use_pin_sequence([PIN, WIPE_CODE, WIPE_CODE]) device.change_wipe_code(client) From b6fca537c9c3fb42ba5fb399f44df86c6749c3fb Mon Sep 17 00:00:00 2001 From: matejcik Date: Wed, 12 Feb 2020 15:45:17 +0100 Subject: [PATCH 10/10] tests: remove wipe_code_activate test fixture --- tests/ui_tests/__init__.py | 2 +- tests/ui_tests/fixtures.json | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/ui_tests/__init__.py b/tests/ui_tests/__init__.py index ffd8509554..b6e80a0fee 100644 --- a/tests/ui_tests/__init__.py +++ b/tests/ui_tests/__init__.py @@ -111,4 +111,4 @@ def read_fixtures(): def write_fixtures(): - HASH_FILE.write_text(json.dumps(HASHES, indent="", sort_keys=True)) + HASH_FILE.write_text(json.dumps(HASHES, indent="", sort_keys=True) + "\n") diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index 3a37dcede9..d0a00c4dcf 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -47,7 +47,6 @@ "test_msg_change_wipe_code_t2.py::test_set_remove_wipe_code": "82c0d1acbf5ff344189761f808d3cf0e632726341231c20b2c0925ab5549b6af", "test_msg_change_wipe_code_t2.py::test_set_wipe_code_mismatch": "1642d2d15920a3bb2c666b39beca9943ba39adb59289ebc40b97d7088a4d7abf", "test_msg_change_wipe_code_t2.py::test_set_wipe_code_to_pin": "15289574ceb002b5161305b0595dcd20e437d1dd4e7561332e1aba4c1615e9ea", -"test_msg_change_wipe_code_t2.py::test_wipe_code_activate": "348ff6811029253d7d520b37a2d3ff219516a7401c8b65ab088c7a7d39bd8b2b", "test_msg_changepin_t2.py::test_change_failed": "370c59da62a84aaefa242562c36a6facac89c7f819e37d1ae8cbe2c44a2de256", "test_msg_changepin_t2.py::test_change_pin": "c42fca9bf8f3b4c330516d90231ae0cfa7419d83370be9cfcf6a81cca3f3b06c", "test_msg_changepin_t2.py::test_remove_pin": "d049eaa6cd11e88b7af193b080cf868b62271266ad6f2973bfd82944b523741d", @@ -389,4 +388,4 @@ "test_u2f_counter.py::test_u2f_counter": "7d96a4d262b9d8a2c1158ac1e5f0f7b2c3ed5f2ba9d6235a014320313f9488fe", "test_zerosig.py-test_one_zero_signature": "401aeaf7b2f565e2064a3c1a57a8ee3afe1e9bf251fba0874390685e7e0f178f", "test_zerosig.py-test_two_zero_signature": "7a01a057fb5dd3e6e38e7986875c5d07f0700bd80b519660e0b42973a9afd664" -} \ No newline at end of file +}