# This file is part of the Trezor project. # # Copyright (C) 2012-2019 SatoshiLabs and contributors # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 # as published by the Free Software Foundation. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the License along with this library. # If not, see . import pytest from trezorlib import btc, cardano, exceptions, messages, models, protocol_v1 from trezorlib.debuglink import DebugSession as Session from trezorlib.debuglink import TrezorTestContext from trezorlib.exceptions import InvalidSessionError, TrezorFailure from trezorlib.tools import parse_path from ..common import get_test_address ADDRESS_N = parse_path("m/44h/0h/0h") XPUB = "xpub6BiVtCpG9fQPxnPmHXG8PhtzQdWC2Su4qWu6XW9tpWFYhxydCLJGrWBJZ5H6qTAHdPQ7pQhtpjiYZVZARo14qHiay2fvrX996oEP42u8wZy" XPUB_XYZ = "xpub6CzEwks5KV5kxxUTWtG9frJeoWMTPs6ZX4bSABMHry16MFXukicyRmau446SzcMUZh3wVABJir3PMNhQbw1N3PJDDos6BBGogCDHScx5Nkq" PIN4 = "1234" DATA_ERROR_INVALID_MESSAGE = "Passphrase provided when it shouldn't be!" DATA_ERROR_ON_DEVICE = "Providing passphrase in message is not allowed when PASSPHRASE_ALWAYS_ON_DEVICE is True." @pytest.mark.setup_client(pin=PIN4, passphrase="") def test_clear_session(test_ctx: TrezorTestContext): is_t1 = test_ctx.model is models.T1B1 if test_ctx.is_protocol_v1(): # SessionV1.derive() init_responses = [ messages.Features, messages.PinMatrixRequest if is_t1 else messages.ButtonRequest, messages.PassphraseRequest, messages.PublicKey, messages.Features, ] else: # ThpSession.derive() init_responses = [messages.ButtonRequest, messages.Success] lock_unlock = [ messages.Success, messages.Features, messages.PinMatrixRequest if is_t1 else messages.ButtonRequest, ] cached_responses = [messages.PublicKey] with test_ctx: test_ctx.use_pin_sequence([PIN4, PIN4]) test_ctx.set_expected_responses(init_responses + lock_unlock + cached_responses) session = test_ctx.get_session() session.lock() assert btc.get_public_node(session, ADDRESS_N).xpub == XPUB with test_ctx: # pin and passphrase are cached test_ctx.set_expected_responses(cached_responses) assert btc.get_public_node(session, ADDRESS_N).xpub == XPUB session.lock() session.close() # session cache is cleared with test_ctx: test_ctx.use_pin_sequence([PIN4, PIN4]) test_ctx.set_expected_responses(init_responses + cached_responses) session = test_ctx.get_session() assert btc.get_public_node(session, ADDRESS_N).xpub == XPUB with test_ctx: # pin and passphrase are cached test_ctx.set_expected_responses(cached_responses) assert btc.get_public_node(session, ADDRESS_N).xpub == XPUB def test_end_session(test_ctx: TrezorTestContext): session = test_ctx.get_session() assert session.id is not None # get_address will succeed with test_ctx: test_ctx.set_expected_responses([messages.Address]) get_test_address(session) session.close() # avoid trezorlib's check session.is_invalid = False session.client._last_active_session = session with pytest.raises(InvalidSessionError), test_ctx: test_ctx.set_expected_responses([messages.Failure]) get_test_address(session) session = test_ctx.get_session() assert session.id is not None with test_ctx: test_ctx.set_expected_responses([messages.Address]) get_test_address(session) with test_ctx: test_ctx.set_expected_responses([messages.Success] * 2) session.call(messages.EndSession()) # end_session should succeed on empty session too session.call(messages.EndSession()) @pytest.mark.protocol("v1") def test_cannot_resume_ended_session(test_ctx: TrezorTestContext): session = test_ctx.get_session() session_id = session.id assert isinstance(session, protocol_v1.SessionV1) session.initialize() assert session.id == session_id session.close() # avoid trezorlib's check session.is_invalid = False with pytest.raises(exceptions.InvalidSessionError): session.initialize() @pytest.mark.protocol("v1") def test_end_session_only_current(test_ctx: TrezorTestContext): """test that EndSession only destroys the current session""" session_a = test_ctx.get_session() session_b = test_ctx.get_session() session_b.call(messages.EndSession(), expect=messages.Success) assert isinstance(session_a, protocol_v1.SessionV1) assert isinstance(session_b, protocol_v1.SessionV1) # resume ended session with pytest.raises(exceptions.InvalidSessionError): session_b.initialize() # resume first session that was not ended session_a.initialize() @pytest.mark.setup_client(passphrase=True) def test_session_recycling(test_ctx: TrezorTestContext): session = test_ctx.get_session(passphrase="TREZOR") with test_ctx: test_ctx.set_expected_responses([messages.Address]) address = get_test_address(session) # create and close 100 sessions - more than the session limit for _ in range(100): session_x = test_ctx.get_session() session_x.close() # it should still be possible to resume the original session with test_ctx: # passphrase should still be cached expected_responses = [messages.Address] * 3 if test_ctx.is_protocol_v1(): expected_responses = [messages.Features] + expected_responses test_ctx.set_expected_responses(expected_responses) get_test_address(session) get_test_address(session) assert address == get_test_address(session) @pytest.mark.altcoin @pytest.mark.cardano @pytest.mark.models("core") @pytest.mark.protocol("v1") def test_derive_cardano_empty_session(test_ctx: TrezorTestContext): assert isinstance(test_ctx.client, protocol_v1.TrezorClientV1) def session_id_for_derive_cardano(session_id: bytes, derive: bool | None) -> bytes: features = test_ctx.client._call( test_ctx.client._last_active_session, messages.Initialize(session_id=session_id, derive_cardano=derive), expect=messages.Features, ) assert features.session_id is not None return features.session_id # start new session session_id = session_id_for_derive_cardano(b"", True) # restarting same session should go well assert session_id == session_id_for_derive_cardano(session_id, None) # restarting same session should go well with any setting assert session_id == session_id_for_derive_cardano(session_id, False) assert session_id == session_id_for_derive_cardano(session_id, True) @pytest.mark.altcoin @pytest.mark.cardano @pytest.mark.models("core") @pytest.mark.protocol("v1") def test_derive_cardano_running_session(test_ctx: TrezorTestContext): # start new session session = test_ctx.get_session(derive_cardano=False) assert isinstance(session, protocol_v1.SessionV1) # session should not have Cardano capability with pytest.raises(TrezorFailure, match="not enabled"): cardano.get_public_key(session, parse_path("m/44h/1815h/0h")) # restarting same session should go well session.initialize() # restarting same session should go well if we _don't_ want to derive cardano session.initialize(derive_cardano=False) # restarting with derive_cardano=True should kill old session and create new one with pytest.raises(exceptions.InvalidSessionError) as e: session.initialize(derive_cardano=True) features = messages.Features.ensure_isinstance(e.value.from_message) assert features.session_id is not None assert features.session_id != session.id session_2 = protocol_v1.SessionV1(test_ctx.client, id=features.session_id) # manually activate the new session, to avoid re-sending Initialize test_ctx.client._last_active_session = session_2 # new session should have Cardano capability cardano.get_public_key(session_2, parse_path("m/44h/1815h/0h")) # restarting with derive_cardano=True should keep same session session_2.initialize(derive_cardano=True) # restarting with derive_cardano=False should kill old session and create new one with pytest.raises(exceptions.InvalidSessionError) as e: session_2.initialize(derive_cardano=False) session_2.is_invalid = False test_ctx.client._last_active_session = session_2 with pytest.raises(TrezorFailure, match="not enabled"): cardano.get_public_key(session_2, parse_path("m/44h/1815h/0h")) def _create_session_passphrase_on_device( session: Session, passphrase_on_device: str = "", passphrase_in_message: str | None = None, on_device: bool = True, error_message: str | None = None, ) -> None: res = session.call_raw( messages.ThpCreateNewSession( passphrase=passphrase_in_message, on_device=on_device ) ) if error_message is not None: # Failed to create session - expected assert isinstance(res, messages.Failure) assert res.code == messages.FailureType.DataError assert res.message == error_message return # Device waits for passphrase entry on device assert isinstance(res, messages.ButtonRequest) assert res.code == messages.ButtonRequestType.PassphraseEntry # Input passphrase on device # TODO handle empty passphrase better - check that confirm screen is displayed session.debug.input(passphrase_on_device) session.write(messages.ButtonAck()) response_success = session.read() # Session was Successfully created assert isinstance(response_success, messages.Success) @pytest.mark.models("core") @pytest.mark.protocol("thp") @pytest.mark.setup_client(passphrase=True) def test_create_session_with_passphrase_on_device(session: Session): # Set always_on_device to True msg = messages.ApplySettings(passphrase_always_on_device=True) session.call(msg, expect=messages.Success) # Assert session works res = session.client.ping("PING") assert res == "PING" _create_session_passphrase_on_device(session, passphrase_on_device="XYZ") assert btc.get_public_node(session, ADDRESS_N).xpub == XPUB_XYZ _create_session_passphrase_on_device(session) assert btc.get_public_node(session, ADDRESS_N).xpub == XPUB # Cannot use passphrase in-message when always_on_device _create_session_passphrase_on_device( session, passphrase_in_message="xyz", on_device=False, error_message=DATA_ERROR_ON_DEVICE, ) # Cannot use empty passphrase in-message when always_on_device _create_session_passphrase_on_device( session, passphrase_in_message="", on_device=False, error_message=DATA_ERROR_ON_DEVICE, ) # Message with both on_device and passphrase in-message is invalid _create_session_passphrase_on_device( session, passphrase_in_message="", on_device=True, error_message=DATA_ERROR_INVALID_MESSAGE, ) _create_session_passphrase_on_device(session, passphrase_on_device="XYZ") assert btc.get_public_node(session, ADDRESS_N).xpub == XPUB_XYZ