You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trezor-firmware/tests/device_tests/test_session_id_and_passphr...

519 lines
21 KiB

# 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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
import random
import pytest
from trezorlib import device, exceptions, messages, models
from trezorlib.debuglink import TrezorClientDebugLink as Client
from trezorlib.exceptions import TrezorFailure
from trezorlib.messages import FailureType, SafetyCheckLevel
from trezorlib.tools import parse_path
from .. import translations as TR
XPUB_PASSPHRASES = {
"A": "xpub6CekxGcnqnJ6osfY4Rrq7W5ogFtR54KUvz4H16XzaQuukMFZCGebEpVznfq4yFcKEmYyShwj2UKjL7CazuNSuhdkofF4mHabHkLxCMVvsqG",
"B": "xpub6CFxuyQpgryoR64QC38w42dLgDv5P4qWXhn1fbaN62UYzu1wJXZyrYqGnkq5d8xPUK68RXtXFBiqp3rfLGpeQ57zLtx675ZZn5ezKMAWQfu",
"C": "xpub6BhJMNFwCjGKyRb9RUcnuHhJ2TgcnurfUrQszrmZ1rg8aadsMXLySF6LY3qf4pR7bY4vwpd1VwLPQvuCRr7BPTs8wvqrv2gexxViwj96czT",
"D": "xpub6DK1vnTBe9EkhLACJRvovv8RSUC3MSiEV64opM7XUqrowxQ8J5C2WpA6n4vt5LS3bs618aKzi7k5w7VzNCv3SfqEeSepvvHaPhRoTvRqR5u",
"E": "xpub6CqbQjHN7r68GHh7RsiAyrdAmyiZQgWvDxQtba2NxZHumvfMK31U6emVQSexYrTAHWQeLygRD1yXZQLsCs1LLJtaeSxMAnh2YUmP3ov6EQz",
"F": "xpub6CRDxB1aHVNHfqjPeYhnPBhBfkQb4b4K581uYKxwv4KnkiVsRttBCXSkZM5jtP1Vv2v3wr5FxfzqWWDApLCbutBLnfwYpkWpZUmZSp6hqg5",
"G": "xpub6DGKmAKYDF44KQEaqXY3bbJNufEDi6QPnahV4JdBxFbFCN9Vg7ZfUHxPv3uhjeeJEtPe2PjFKWRsUrEF3RDttnXf9wXq3BfYBZemwKipJ24",
"H": "xpub6Bg8zbY94d1cBbAGT2crZL7C1UM8JWCP5CCtiHMnV4tB1pE9oCfjvZxRRFLi6EiamBDyCs3ARaHwU2FLx76YYCPFRVc1YyJi6depNtWRnoJ",
"I": "xpub6DMpHuTZTTN64eEHcNpyeQwehXgWTrY668ZkRWnRfkFEGKpNv2uPR3js1dJgcFRksSmrdtpHqFDPTzFsR1HqvzNdgZwXmk9vCLt1ypwUzA3",
"J": "xpub6CVeYPTG57D4tm9BvwCcakppwGJstbXyK8Yd611agusZuHmx7og3dNvr6pjMN6e4BoaNc5MZA4TjMLjMT2h2vJRU8rYLvHFUwrEL9zDbuqe",
}
XPUB_PASSPHRASE_NONE = "xpub6BiVtCpG9fQPxnPmHXG8PhtzQdWC2Su4qWu6XW9tpWFYhxydCLJGrWBJZ5H6qTAHdPQ7pQhtpjiYZVZARo14qHiay2fvrX996oEP42u8wZy"
XPUB_CARDANO_PASSPHRASE_A = "d37eba66d6183547b11b4d0c3e08e761da9f07c3ef32183f8b79360b2b66850e47e8eb3865251784c3c471a854ee40dfc067f7f3afe47d093388ea45239606fd"
XPUB_CARDANO_PASSPHRASE_B = "d80e770f6dfc3edb58eaab68aa091b2c27b08a47583471e93437ac5f8baa61880c7af4938a941c084c19731e6e57a5710e6ad1196263291aea297ce0eec0f177"
ADDRESS_N = parse_path("m/44h/0h/0h")
XPUB_REQUEST = messages.GetPublicKey(address_n=ADDRESS_N, coin_name="Bitcoin")
SESSIONS_STORED = 10
def _init_session(client: Client, session_id=None, derive_cardano=False):
"""Call Initialize, check and return the session ID."""
response = client.call(
messages.Initialize(session_id=session_id, derive_cardano=derive_cardano)
)
assert isinstance(response, messages.Features)
assert len(response.session_id) == 32
return response.session_id
def _get_xpub(client: Client, passphrase=None):
"""Get XPUB and check that the appropriate passphrase flow has happened."""
if passphrase is not None:
expected_responses = [
messages.PassphraseRequest,
messages.ButtonRequest,
messages.ButtonRequest,
messages.PublicKey,
]
else:
expected_responses = [messages.PublicKey]
with client:
client.use_passphrase(passphrase or "")
client.set_expected_responses(expected_responses)
result = client.call(XPUB_REQUEST)
return result.xpub
@pytest.mark.setup_client(passphrase=True)
def test_session_with_passphrase(client: Client):
# Let's start the communication by calling Initialize.
session_id = _init_session(client)
# GetPublicKey requires passphrase and since it is not cached,
# Trezor will prompt for it.
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASES["A"]
# Call Initialize again, this time with the received session id and then call
# GetPublicKey. The passphrase should be cached now so Trezor must
# not ask for it again, whilst returning the same xpub.
new_session_id = _init_session(client, session_id=session_id)
assert new_session_id == session_id
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASES["A"]
# If we set session id in Initialize to None, the cache will be cleared
# and Trezor will ask for the passphrase again.
new_session_id = _init_session(client)
assert new_session_id != session_id
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASES["A"]
# Unknown session id is the same as setting it to None.
_init_session(client, session_id=b"X" * 32)
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASES["A"]
@pytest.mark.setup_client(passphrase=True)
def test_multiple_sessions(client: Client):
# start SESSIONS_STORED sessions
session_ids = []
for _ in range(SESSIONS_STORED):
session_ids.append(_init_session(client))
# Resume each session
for session_id in session_ids:
new_session_id = _init_session(client, session_id)
assert session_id == new_session_id
# Creating a new session replaces the least-recently-used session
_init_session(client)
# Resuming session 1 through SESSIONS_STORED will still work
for session_id in session_ids[1:]:
new_session_id = _init_session(client, session_id)
assert session_id == new_session_id
# Resuming session 0 will not work
new_session_id = _init_session(client, session_ids[0])
assert new_session_id != session_ids[0]
# New session bumped out the least-recently-used anonymous session.
# Resuming session 1 through SESSIONS_STORED will still work
for session_id in session_ids[1:]:
new_session_id = _init_session(client, session_id)
assert session_id == new_session_id
# Creating a new session replaces session_ids[0] again
_init_session(client)
# Resuming all sessions one by one will in turn bump out the previous session.
for session_id in session_ids:
new_session_id = _init_session(client, session_id)
assert session_id != new_session_id
@pytest.mark.setup_client(passphrase=True)
def test_multiple_passphrases(client: Client):
# start a session
session_a = _init_session(client)
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASES["A"]
# start it again wit the same session id
new_session_id = _init_session(client, session_id=session_a)
# session is the same
assert new_session_id == session_a
# passphrase is not prompted
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASES["A"]
# start a second session
session_b = _init_session(client)
# new session -> new session id and passphrase prompt
assert _get_xpub(client, passphrase="B") == XPUB_PASSPHRASES["B"]
# provide the same session id -> must not ask for passphrase again.
new_session_id = _init_session(client, session_id=session_b)
assert new_session_id == session_b
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASES["B"]
# provide the first session id -> must not ask for passphrase again and return the same result.
new_session_id = _init_session(client, session_id=session_a)
assert new_session_id == session_a
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASES["A"]
# provide the second session id -> must not ask for passphrase again and return the same result.
new_session_id = _init_session(client, session_id=session_b)
assert new_session_id == session_b
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASES["B"]
@pytest.mark.slow
@pytest.mark.setup_client(passphrase=True)
def test_max_sessions_with_passphrases(client: Client):
# for the following tests, we are using as many passphrases as there are available sessions
assert len(XPUB_PASSPHRASES) == SESSIONS_STORED
# start as many sessions as the limit is
session_ids = {}
for passphrase, xpub in XPUB_PASSPHRASES.items():
session_id = _init_session(client)
assert session_id not in session_ids.values()
session_ids[passphrase] = session_id
assert _get_xpub(client, passphrase=passphrase) == xpub
# passphrase is not prompted for the started the sessions, regardless the order
# let's try 20 different orderings
passphrases = list(XPUB_PASSPHRASES.keys())
shuffling = passphrases[:]
for _ in range(20):
random.shuffle(shuffling)
for passphrase in shuffling:
session_id = _init_session(client, session_id=session_ids[passphrase])
assert session_id == session_ids[passphrase]
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASES[passphrase]
# make sure the usage order is the reverse of the creation order
for passphrase in reversed(passphrases):
session_id = _init_session(client, session_id=session_ids[passphrase])
assert session_id == session_ids[passphrase]
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASES[passphrase]
# creating one more session will exceed the limit
_init_session(client)
# new session asks for passphrase
_get_xpub(client, passphrase="XX")
# restoring the sessions in reverse will evict the next-up session
for passphrase in reversed(passphrases):
_init_session(client, session_id=session_ids[passphrase])
_get_xpub(client, passphrase="whatever") # passphrase is prompted
def test_session_enable_passphrase(client: Client):
# Let's start the communication by calling Initialize.
session_id = _init_session(client)
# Trezor will not prompt for passphrase because it is turned off.
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASE_NONE
# Turn on passphrase.
# Emit the call explicitly to avoid ClearSession done by the library function
response = client.call(messages.ApplySettings(use_passphrase=True))
assert isinstance(response, messages.Success)
# The session id is unchanged, therefore we do not prompt for the passphrase.
new_session_id = _init_session(client, session_id=session_id)
assert session_id == new_session_id
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASE_NONE
# We clear the session id now, so the passphrase should be asked.
new_session_id = _init_session(client)
assert session_id != new_session_id
assert _get_xpub(client, passphrase="A") == XPUB_PASSPHRASES["A"]
@pytest.mark.skip_t1b1
@pytest.mark.setup_client(passphrase=True)
def test_passphrase_on_device(client: Client):
_init_session(client)
# try to get xpub with passphrase on host:
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.PassphraseRequest)
# using `client.call` to auto-skip subsequent ButtonRequests for "show passphrase"
response = client.call(messages.PassphraseAck(passphrase="A", on_device=False))
assert isinstance(response, messages.PublicKey)
assert response.xpub == XPUB_PASSPHRASES["A"]
# try to get xpub again, passphrase should be cached
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.PublicKey)
assert response.xpub == XPUB_PASSPHRASES["A"]
# make a new session
_init_session(client)
# try to get xpub with passphrase on device:
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.PassphraseRequest)
response = client.call_raw(messages.PassphraseAck(on_device=True))
# no "show passphrase" here
assert isinstance(response, messages.ButtonRequest)
client.debug.input("A")
response = client.call_raw(messages.ButtonAck())
assert isinstance(response, messages.PublicKey)
assert response.xpub == XPUB_PASSPHRASES["A"]
# try to get xpub again, passphrase should be cached
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.PublicKey)
assert response.xpub == XPUB_PASSPHRASES["A"]
@pytest.mark.skip_t1b1
@pytest.mark.setup_client(passphrase=True)
def test_passphrase_always_on_device(client: Client):
# Let's start the communication by calling Initialize.
session_id = _init_session(client)
# Force passphrase entry on Trezor.
response = client.call(messages.ApplySettings(passphrase_always_on_device=True))
assert isinstance(response, messages.Success)
# Since we enabled the always_on_device setting, Trezor will send ButtonRequests and ask for it on the device.
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.ButtonRequest)
client.debug.input("") # Input empty passphrase.
response = client.call_raw(messages.ButtonAck())
assert isinstance(response, messages.PublicKey)
assert response.xpub == XPUB_PASSPHRASE_NONE
# Passphrase will not be prompted. The session id stays the same and the passphrase is cached.
_init_session(client, session_id=session_id)
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.PublicKey)
assert response.xpub == XPUB_PASSPHRASE_NONE
# In case we want to add a new passphrase we need to send session_id = None.
_init_session(client)
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.ButtonRequest)
client.debug.input("A") # Input non-empty passphrase.
response = client.call_raw(messages.ButtonAck())
assert isinstance(response, messages.PublicKey)
assert response.xpub == XPUB_PASSPHRASES["A"]
@pytest.mark.skip_t2t1
@pytest.mark.skip_t2b1
@pytest.mark.skip_t3t1
@pytest.mark.setup_client(passphrase="")
def test_passphrase_on_device_not_possible_on_t1(client: Client):
# This setting makes no sense on T1.
response = client.call_raw(messages.ApplySettings(passphrase_always_on_device=True))
assert isinstance(response, messages.Failure)
assert response.code == FailureType.DataError
# T1 should not accept on_device request
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.PassphraseRequest)
response = client.call_raw(messages.PassphraseAck(on_device=True))
assert isinstance(response, messages.Failure)
assert response.code == FailureType.DataError
@pytest.mark.setup_client(passphrase=True)
def test_passphrase_ack_mismatch(client: Client):
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.PassphraseRequest)
response = client.call_raw(messages.PassphraseAck(passphrase="A", on_device=True))
assert isinstance(response, messages.Failure)
assert response.code == FailureType.DataError
@pytest.mark.setup_client(passphrase="")
def test_passphrase_missing(client: Client):
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.PassphraseRequest)
response = client.call_raw(messages.PassphraseAck(passphrase=None))
assert isinstance(response, messages.Failure)
assert response.code == FailureType.DataError
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.PassphraseRequest)
response = client.call_raw(messages.PassphraseAck(passphrase=None, on_device=False))
assert isinstance(response, messages.Failure)
assert response.code == FailureType.DataError
@pytest.mark.setup_client(passphrase=True)
def test_passphrase_length(client: Client):
def call(passphrase: str, expected_result: bool):
_init_session(client)
response = client.call_raw(XPUB_REQUEST)
assert isinstance(response, messages.PassphraseRequest)
try:
response = client.call(messages.PassphraseAck(passphrase=passphrase))
assert expected_result is True, "Call should have failed"
assert isinstance(response, messages.PublicKey)
except exceptions.TrezorFailure as e:
assert expected_result is False, "Call should have succeeded"
assert e.code == FailureType.DataError
# 50 is ok
call(passphrase="A" * 50, expected_result=True)
# 51 is not
call(passphrase="A" * 51, expected_result=False)
# "š" has two bytes - 48x A and "š" should be fine (50 bytes)
call(passphrase="A" * 48 + "š", expected_result=True)
# "š" has two bytes - 49x A and "š" should not (51 bytes)
call(passphrase="A" * 49 + "š", expected_result=False)
@pytest.mark.skip_t1b1
@pytest.mark.setup_client(passphrase=True)
def test_hide_passphrase_from_host(client: Client):
# Without safety checks, turning it on fails
with pytest.raises(TrezorFailure, match="Safety checks are strict"), client:
device.apply_settings(client, hide_passphrase_from_host=True)
device.apply_settings(client, safety_checks=SafetyCheckLevel.PromptTemporarily)
# Turning it on
device.apply_settings(client, hide_passphrase_from_host=True)
passphrase = "abc"
with client:
def input_flow():
yield
TR.assert_in(
client.debug.wait_layout().text_content(),
"passphrase__access_hidden_wallet",
)
if client.model in (models.T2T1, models.T3T1):
client.debug.press_yes()
elif client.model is models.T2B1:
client.debug.press_right()
client.debug.press_right()
client.debug.press_yes()
else:
raise KeyError
client.watch_layout()
client.set_input_flow(input_flow)
client.set_expected_responses(
[
messages.PassphraseRequest,
messages.ButtonRequest,
messages.PublicKey,
]
)
client.use_passphrase(passphrase)
result = client.call(XPUB_REQUEST)
assert isinstance(result, messages.PublicKey)
xpub_hidden_passphrase = result.xpub
# Turning it off
device.apply_settings(client, hide_passphrase_from_host=False)
# Starting new session, otherwise the passphrase would be cached
_init_session(client)
with client:
def input_flow():
yield
TR.assert_in(
client.debug.wait_layout().text_content(),
"passphrase__next_screen_will_show_passphrase",
)
client.debug.press_yes()
yield
TR.assert_equals(
client.debug.wait_layout().title(),
"passphrase__title_confirm",
)
assert passphrase in client.debug.wait_layout().text_content()
client.debug.press_yes()
client.watch_layout()
client.set_input_flow(input_flow)
client.set_expected_responses(
[
messages.PassphraseRequest,
messages.ButtonRequest,
messages.ButtonRequest,
messages.PublicKey,
]
)
client.use_passphrase(passphrase)
result = client.call(XPUB_REQUEST)
assert isinstance(result, messages.PublicKey)
xpub_shown_passphrase = result.xpub
assert xpub_hidden_passphrase == xpub_shown_passphrase
def _get_xpub_cardano(client: Client, passphrase):
msg = messages.CardanoGetPublicKey(
address_n=parse_path("m/44h/1815h/0h/0/0"),
derivation_type=messages.CardanoDerivationType.ICARUS,
)
response = client.call_raw(msg)
if passphrase is not None:
assert isinstance(response, messages.PassphraseRequest)
response = client.call(messages.PassphraseAck(passphrase=passphrase))
assert isinstance(response, messages.CardanoPublicKey)
return response.xpub
@pytest.mark.skip_t1b1
@pytest.mark.altcoin
@pytest.mark.setup_client(passphrase=True)
def test_cardano_passphrase(client: Client):
# Cardano has a separate derivation method that needs to access the plaintext
# of the passphrase.
# Historically, Cardano calls would ask for passphrase again. Now, they should not.
session_id = _init_session(client, derive_cardano=True)
# GetPublicKey requires passphrase and since it is not cached,
# Trezor will prompt for it.
assert _get_xpub(client, passphrase="B") == XPUB_PASSPHRASES["B"]
# The passphrase is now cached for non-Cardano coins.
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASES["B"]
# The passphrase should be cached for Cardano as well
assert _get_xpub_cardano(client, passphrase=None) == XPUB_CARDANO_PASSPHRASE_B
# Initialize with the session id does not destroy the state
_init_session(client, session_id=session_id, derive_cardano=True)
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASES["B"]
assert _get_xpub_cardano(client, passphrase=None) == XPUB_CARDANO_PASSPHRASE_B
# New session will destroy the state
_init_session(client, derive_cardano=True)
# Cardano must ask for passphrase again
assert _get_xpub_cardano(client, passphrase="A") == XPUB_CARDANO_PASSPHRASE_A
# Passphrase is now cached for Cardano
assert _get_xpub_cardano(client, passphrase=None) == XPUB_CARDANO_PASSPHRASE_A
# Passphrase is cached for non-Cardano coins too
assert _get_xpub(client, passphrase=None) == XPUB_PASSPHRASES["A"]