mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-01-23 13:51:00 +00:00
feat(core): Implement entropy check workflow in ResetDevice.
This commit is contained in:
parent
df97d8d958
commit
01a1f479a0
1
core/.changelog.d/4155.added
Normal file
1
core/.changelog.d/4155.added
Normal file
@ -0,0 +1 @@
|
|||||||
|
Entropy check workflow in ResetDevice.
|
@ -6,9 +6,13 @@ if TYPE_CHECKING:
|
|||||||
from trezor.messages import GetPublicKey, PublicKey
|
from trezor.messages import GetPublicKey, PublicKey
|
||||||
from trezor.protobuf import MessageType
|
from trezor.protobuf import MessageType
|
||||||
|
|
||||||
|
from apps.common.keychain import Keychain
|
||||||
|
|
||||||
|
|
||||||
async def get_public_key(
|
async def get_public_key(
|
||||||
msg: GetPublicKey, auth_msg: MessageType | None = None
|
msg: GetPublicKey,
|
||||||
|
auth_msg: MessageType | None = None,
|
||||||
|
keychain: Keychain | None = None,
|
||||||
) -> PublicKey:
|
) -> PublicKey:
|
||||||
from trezor import TR, wire
|
from trezor import TR, wire
|
||||||
from trezor.enums import InputScriptType
|
from trezor.enums import InputScriptType
|
||||||
@ -34,6 +38,7 @@ async def get_public_key(
|
|||||||
if auth_msg.address_n != address_n[: len(auth_msg.address_n)]:
|
if auth_msg.address_n != address_n[: len(auth_msg.address_n)]:
|
||||||
raise FORBIDDEN_KEY_PATH
|
raise FORBIDDEN_KEY_PATH
|
||||||
|
|
||||||
|
if not keychain:
|
||||||
keychain = await get_keychain(curve_name, [paths.AlwaysMatchingSchema])
|
keychain = await get_keychain(curve_name, [paths.AlwaysMatchingSchema])
|
||||||
|
|
||||||
node = keychain.derive(address_n)
|
node = keychain.derive(address_n)
|
||||||
|
@ -32,8 +32,12 @@ def is_bip39() -> bool:
|
|||||||
return get_type() == BackupType.Bip39
|
return get_type() == BackupType.Bip39
|
||||||
|
|
||||||
|
|
||||||
def get_seed(passphrase: str = "", progress_bar: bool = True) -> bytes:
|
def get_seed(
|
||||||
mnemonic_secret = get_secret()
|
passphrase: str = "",
|
||||||
|
progress_bar: bool = True,
|
||||||
|
mnemonic_secret: bytes | None = None,
|
||||||
|
) -> bytes:
|
||||||
|
mnemonic_secret = mnemonic_secret or get_secret()
|
||||||
if mnemonic_secret is None:
|
if mnemonic_secret is None:
|
||||||
raise ValueError # Mnemonic not set
|
raise ValueError # Mnemonic not set
|
||||||
|
|
||||||
|
@ -62,10 +62,10 @@ async def load_device(msg: LoadDevice) -> Success:
|
|||||||
|
|
||||||
storage_device.store_mnemonic_secret(
|
storage_device.store_mnemonic_secret(
|
||||||
secret,
|
secret,
|
||||||
backup_type,
|
|
||||||
needs_backup=msg.needs_backup is True,
|
needs_backup=msg.needs_backup is True,
|
||||||
no_backup=msg.no_backup is True,
|
no_backup=msg.no_backup is True,
|
||||||
)
|
)
|
||||||
|
storage_device.set_backup_type(backup_type)
|
||||||
storage_device.set_passphrase_enabled(bool(msg.passphrase_protection))
|
storage_device.set_passphrase_enabled(bool(msg.passphrase_protection))
|
||||||
storage_device.set_label(msg.label or "")
|
storage_device.set_label(msg.label or "")
|
||||||
if msg.pin:
|
if msg.pin:
|
||||||
|
@ -212,9 +212,8 @@ async def _finish_recovery(secret: bytes, backup_type: BackupType) -> Success:
|
|||||||
if backup_type is None:
|
if backup_type is None:
|
||||||
raise RuntimeError
|
raise RuntimeError
|
||||||
|
|
||||||
storage_device.store_mnemonic_secret(
|
storage_device.store_mnemonic_secret(secret, needs_backup=False, no_backup=False)
|
||||||
secret, backup_type, needs_backup=False, no_backup=False
|
storage_device.set_backup_type(backup_type)
|
||||||
)
|
|
||||||
if backup_types.is_slip39_backup_type(backup_type):
|
if backup_types.is_slip39_backup_type(backup_type):
|
||||||
if not backup_types.is_extendable_backup_type(backup_type):
|
if not backup_types.is_extendable_backup_type(backup_type):
|
||||||
identifier = storage_recovery.get_slip39_identifier()
|
identifier = storage_recovery.get_slip39_identifier()
|
||||||
|
@ -3,8 +3,8 @@ from typing import TYPE_CHECKING, Sequence
|
|||||||
import storage
|
import storage
|
||||||
import storage.device as storage_device
|
import storage.device as storage_device
|
||||||
from trezor import TR
|
from trezor import TR
|
||||||
from trezor.crypto import slip39
|
from trezor.crypto import hmac, slip39
|
||||||
from trezor.enums import BackupType
|
from trezor.enums import BackupType, MessageType
|
||||||
from trezor.ui.layouts import confirm_action
|
from trezor.ui.layouts import confirm_action
|
||||||
from trezor.wire import ProcessError
|
from trezor.wire import ProcessError
|
||||||
|
|
||||||
@ -63,33 +63,52 @@ async def reset_device(msg: ResetDevice) -> Success:
|
|||||||
# wipe storage to make sure the device is in a clear state
|
# wipe storage to make sure the device is in a clear state
|
||||||
storage.reset()
|
storage.reset()
|
||||||
|
|
||||||
|
# Check backup type, perform type-specific handling
|
||||||
|
if backup_types.is_slip39_backup_type(backup_type):
|
||||||
|
# set SLIP39 parameters
|
||||||
|
storage_device.set_slip39_iteration_exponent(slip39.DEFAULT_ITERATION_EXPONENT)
|
||||||
|
elif backup_type != BAK_T_BIP39:
|
||||||
|
# Unknown backup type.
|
||||||
|
raise RuntimeError
|
||||||
|
|
||||||
|
storage_device.set_backup_type(backup_type)
|
||||||
|
|
||||||
# request and set new PIN
|
# request and set new PIN
|
||||||
if msg.pin_protection:
|
if msg.pin_protection:
|
||||||
newpin = await request_pin_confirm()
|
newpin = await request_pin_confirm()
|
||||||
if not config.change_pin("", newpin, None, None):
|
if not config.change_pin("", newpin, None, None):
|
||||||
raise ProcessError("Failed to set PIN")
|
raise ProcessError("Failed to set PIN")
|
||||||
|
|
||||||
# generate and display internal entropy
|
prev_int_entropy = None
|
||||||
|
while True:
|
||||||
|
# generate internal entropy
|
||||||
int_entropy = random.bytes(32, True)
|
int_entropy = random.bytes(32, True)
|
||||||
if __debug__:
|
if __debug__:
|
||||||
storage.debug.reset_internal_entropy = int_entropy
|
storage.debug.reset_internal_entropy = int_entropy
|
||||||
|
|
||||||
|
entropy_commitment = (
|
||||||
|
hmac(hmac.SHA256, int_entropy, b"").digest() if msg.entropy_check else None
|
||||||
|
)
|
||||||
|
|
||||||
# request external entropy and compute the master secret
|
# request external entropy and compute the master secret
|
||||||
entropy_ack = await call(EntropyRequest(), EntropyAck)
|
entropy_ack = await call(
|
||||||
|
EntropyRequest(
|
||||||
|
entropy_commitment=entropy_commitment, prev_entropy=prev_int_entropy
|
||||||
|
),
|
||||||
|
EntropyAck,
|
||||||
|
)
|
||||||
ext_entropy = entropy_ack.entropy
|
ext_entropy = entropy_ack.entropy
|
||||||
# For SLIP-39 this is the Encrypted Master Secret
|
# For SLIP-39 this is the Encrypted Master Secret
|
||||||
secret = _compute_secret_from_entropy(int_entropy, ext_entropy, msg.strength)
|
secret = _compute_secret_from_entropy(int_entropy, ext_entropy, msg.strength)
|
||||||
|
|
||||||
# Check backup type, perform type-specific handling
|
|
||||||
if backup_type == BAK_T_BIP39:
|
if backup_type == BAK_T_BIP39:
|
||||||
# in BIP-39 we store mnemonic string instead of the secret
|
# in BIP-39 we store mnemonic string instead of the secret
|
||||||
secret = bip39.from_data(secret).encode()
|
secret = bip39.from_data(secret).encode()
|
||||||
elif backup_types.is_slip39_backup_type(backup_type):
|
|
||||||
# generate and set SLIP39 parameters
|
if not msg.entropy_check or await _entropy_check(secret):
|
||||||
storage_device.set_slip39_iteration_exponent(slip39.DEFAULT_ITERATION_EXPONENT)
|
break
|
||||||
else:
|
|
||||||
# Unknown backup type.
|
prev_int_entropy = int_entropy
|
||||||
raise RuntimeError
|
|
||||||
|
|
||||||
# If either of skip_backup or no_backup is specified, we are not doing backup now.
|
# If either of skip_backup or no_backup is specified, we are not doing backup now.
|
||||||
# Otherwise, we try to do it.
|
# Otherwise, we try to do it.
|
||||||
@ -112,7 +131,6 @@ async def reset_device(msg: ResetDevice) -> Success:
|
|||||||
storage_device.set_passphrase_enabled(bool(msg.passphrase_protection))
|
storage_device.set_passphrase_enabled(bool(msg.passphrase_protection))
|
||||||
storage_device.store_mnemonic_secret(
|
storage_device.store_mnemonic_secret(
|
||||||
secret, # for SLIP-39, this is the EMS
|
secret, # for SLIP-39, this is the EMS
|
||||||
backup_type,
|
|
||||||
needs_backup=not perform_backup,
|
needs_backup=not perform_backup,
|
||||||
no_backup=bool(msg.no_backup),
|
no_backup=bool(msg.no_backup),
|
||||||
)
|
)
|
||||||
@ -124,6 +142,37 @@ async def reset_device(msg: ResetDevice) -> Success:
|
|||||||
return Success(message="Initialized")
|
return Success(message="Initialized")
|
||||||
|
|
||||||
|
|
||||||
|
async def _entropy_check(secret: bytes) -> bool:
|
||||||
|
"""Returns True to indicate that entropy check loop should end."""
|
||||||
|
from trezor.messages import EntropyCheckContinue, EntropyCheckReady, GetPublicKey
|
||||||
|
from trezor.wire.context import call_any
|
||||||
|
|
||||||
|
from apps.bitcoin.get_public_key import get_public_key
|
||||||
|
from apps.common import coininfo, paths
|
||||||
|
from apps.common.keychain import Keychain
|
||||||
|
from apps.common.mnemonic import get_seed
|
||||||
|
|
||||||
|
seed = get_seed(mnemonic_secret=secret)
|
||||||
|
|
||||||
|
msg = EntropyCheckReady()
|
||||||
|
while True:
|
||||||
|
req = await call_any(
|
||||||
|
msg,
|
||||||
|
MessageType.EntropyCheckContinue,
|
||||||
|
MessageType.GetPublicKey,
|
||||||
|
)
|
||||||
|
assert req.MESSAGE_WIRE_TYPE is not None
|
||||||
|
|
||||||
|
if EntropyCheckContinue.is_type_of(req):
|
||||||
|
return req.finish
|
||||||
|
|
||||||
|
assert GetPublicKey.is_type_of(req)
|
||||||
|
req.show_display = False
|
||||||
|
curve_name = req.ecdsa_curve_name or coininfo.by_name(req.coin_name).curve_name
|
||||||
|
keychain = Keychain(seed, curve_name, [paths.AlwaysMatchingSchema])
|
||||||
|
msg = await get_public_key(req, keychain=keychain)
|
||||||
|
|
||||||
|
|
||||||
async def _backup_bip39(mnemonic: str) -> None:
|
async def _backup_bip39(mnemonic: str) -> None:
|
||||||
words = mnemonic.split()
|
words = mnemonic.split()
|
||||||
await layout.show_backup_intro(single_share=True, num_of_words=len(words))
|
await layout.show_backup_intro(single_share=True, num_of_words=len(words))
|
||||||
@ -272,7 +321,7 @@ def _validate_reset_device(msg: ResetDevice) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _compute_secret_from_entropy(
|
def _compute_secret_from_entropy(
|
||||||
int_entropy: bytes, ext_entropy: bytes, strength_in_bytes: int
|
int_entropy: bytes, ext_entropy: bytes, strength_bits: int
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
from trezor.crypto import hashlib
|
from trezor.crypto import hashlib
|
||||||
|
|
||||||
@ -282,7 +331,7 @@ def _compute_secret_from_entropy(
|
|||||||
ehash.update(ext_entropy)
|
ehash.update(ext_entropy)
|
||||||
entropy = ehash.digest()
|
entropy = ehash.digest()
|
||||||
# take a required number of bytes
|
# take a required number of bytes
|
||||||
strength = strength_in_bytes // 8
|
strength = strength_bits // 8
|
||||||
secret = entropy[:strength]
|
secret = entropy[:strength]
|
||||||
return secret
|
return secret
|
||||||
|
|
||||||
|
@ -178,13 +178,11 @@ def set_homescreen(homescreen: bytes) -> None:
|
|||||||
|
|
||||||
def store_mnemonic_secret(
|
def store_mnemonic_secret(
|
||||||
secret: bytes,
|
secret: bytes,
|
||||||
backup_type: BackupType,
|
|
||||||
needs_backup: bool = False,
|
needs_backup: bool = False,
|
||||||
no_backup: bool = False,
|
no_backup: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
set_version(common.STORAGE_VERSION_CURRENT)
|
set_version(common.STORAGE_VERSION_CURRENT)
|
||||||
common.set(_NAMESPACE, _MNEMONIC_SECRET, secret)
|
common.set(_NAMESPACE, _MNEMONIC_SECRET, secret)
|
||||||
common.set_uint8(_NAMESPACE, _BACKUP_TYPE, backup_type)
|
|
||||||
common.set_true_or_delete(_NAMESPACE, _NO_BACKUP, no_backup)
|
common.set_true_or_delete(_NAMESPACE, _NO_BACKUP, no_backup)
|
||||||
common.set_bool(_NAMESPACE, INITIALIZED, True, public=True)
|
common.set_bool(_NAMESPACE, INITIALIZED, True, public=True)
|
||||||
if not no_backup:
|
if not no_backup:
|
||||||
|
@ -239,35 +239,81 @@ example, by using PKCS7. See
|
|||||||
|
|
||||||
## ResetDevice
|
## ResetDevice
|
||||||
|
|
||||||
Reset device message performs Trezor device
|
The ResetDevice message performs Trezor device
|
||||||
setup and generates new wallet with new recovery
|
setup and generates a new wallet with a new recovery
|
||||||
seed. The device must be in unitialized
|
seed. The device must be in unitialized state, meaning that
|
||||||
state, the firmware is already installed but it has not been initialized
|
the firmware is already installed but it has not been initialized
|
||||||
yet. If it is initialized and the user wants to perform a reset device,
|
yet. If it is initialized and the user wants to perform a device reset,
|
||||||
the device must be wiped first. If the Trezor is prepared for its
|
the device must be wiped first. If the Trezor is prepared for its
|
||||||
initialization the screen is showing "Go to trezor.io". The reset device
|
initialization, the screen is showing "Go to trezor.io". The device reset
|
||||||
can be done in Trezor Wallet interface (https://trezor.io/start) and
|
can be done in the Trezor Suite interface (https://trezor.io/start) or
|
||||||
also with Python trezorctl command. After sending
|
using Python trezorctl command. After sending the ResetDevice
|
||||||
message to the device, device warn us to never make a digital copy of
|
message to the device, the device warns the user to never make a digital copy
|
||||||
your recovery seed and never upload it online, this message has to be
|
of their recovery seed and never upload it online, this message has to be
|
||||||
confirmed by pressing on "I understand" on the device. After confirmed,
|
confirmed by pressing "I understand" on the device. After confirmation,
|
||||||
the device produces internal entropy which is random of 32 bytes,
|
the device produces internal entropy which is a random value of 32 bytes,
|
||||||
requests external entropy which is produced in computer and computes
|
requests external entropy which is produced in the host computer and computes
|
||||||
mnemonic (recovery seed) using internal, external entropy and given
|
the mnemonic (recovery seed) using internal, external entropy and the given
|
||||||
strength (12, 18 or 24 words). Trezor Wallet
|
strength (12, 18 or 24 words). Trezor Suite
|
||||||
interface doesn't provide option to choose how many words there should
|
interface doesn't provide an option to choose how many words there should
|
||||||
be in the generated mnemonic (recovery seed). It is hardcoded to 12
|
be in the generated mnemonic (recovery seed). It is hardcoded to 12
|
||||||
words for Trezor Model T but if done with python's trezorctl command it
|
words for Trezor Model T but if done with python's trezorctl command it
|
||||||
can be chosen (for initialization with python's trezorctl command, 24
|
can be chosen (for initialization with python's trezorctl command, 24
|
||||||
words mnemonic is default). After showing mnemonic on the Trezor device,
|
words mnemonic is default). After showing mnemonic on the Trezor device,
|
||||||
Trezor Model T requires 2 random words to
|
Trezor Model T requires the user to enter several words at random positions
|
||||||
be entered to the device to confirm the user has written down the
|
in the mnemonic to confirm that the user has written down the
|
||||||
mnemonic properly. If there are errors in entered words, the device
|
mnemonic properly. If there are errors in the entered words, the device
|
||||||
shows the recovery seed again. If the backup check is successful, the
|
shows the recovery seed again. If the backup check is successful, the
|
||||||
setup is finished. If the Trezor Wallet interface is used, user is asked
|
setup is finished. If the Trezor Wallet interface is used, the user is asked
|
||||||
to set the label and pin (setting up the pin can be skipped) for the
|
to set the label and PIN (setting up the PIN can be skipped) for the
|
||||||
wallet, this is optional when using python trezorctl command.
|
wallet, this is optional when using python trezorctl command.
|
||||||
|
|
||||||
|
The ResetDevice command supports two types of workflows.
|
||||||
|
|
||||||
|
### Simple ResetDevice workflow
|
||||||
|
|
||||||
|
1. H -> T `ResetDevice` (Host specifies strength, backup type, etc.)
|
||||||
|
2. H <- T `EntropyRequest` (No parameters.)
|
||||||
|
3. H -> T `EntropyAck` (Host provides external entropy.)
|
||||||
|
4. H <- T `Success`
|
||||||
|
|
||||||
|
### Entropy check workflow
|
||||||
|
|
||||||
|
The purpose of this workflow is for the host to verify that when Trezor
|
||||||
|
generates the seed, it correctly includes the external entropy from the host.
|
||||||
|
The host performs a randomized test asking Trezor to generate several seeds,
|
||||||
|
checking that they were generated correctly and using the last one as the final
|
||||||
|
seed. The workflow is triggered by setting `ResetDevice.entropy_check` to true.
|
||||||
|
|
||||||
|
The host chooses a small random number *n*, e.g. from 1 to 5, and proceeds as follows:
|
||||||
|
1. H -> T `ResetDevice` (Host specifies strength, backup type, etc.)
|
||||||
|
2. H <- T `EntropyRequest` (Trezor commits to an internal entropy value.)
|
||||||
|
3. H -> T `EntropyAck` (Host provides external entropy.)
|
||||||
|
4. H <- T `EntropyCheckReady` (Trezor stores the seed in storage cache.)
|
||||||
|
5. Host obtains the XPUBs for several accounts that the user intends to use:
|
||||||
|
1. H -> T `GetPublicKey`
|
||||||
|
2. H <- T `PublicKey`
|
||||||
|
6. If this step was executed less than *n* times, then:
|
||||||
|
1. H -> T `EntropyCheckContinue(finish=False)` (Host instructs Trezor to prove seed correctness.)
|
||||||
|
2. H <- T `EntropyRequest` (Trezor reveals previous internal entropy and commits to a new internal entropy value.)
|
||||||
|
3. The host verifies that the entropy commitment is valid, derives the seed and checks that it produces the same XPUBs as Trezor provided in step 5.
|
||||||
|
4. Go to step 3.
|
||||||
|
7. Host instructs trezor to store the current seed in flash memory.
|
||||||
|
1. H -> T `EntropyCheckContinue(finish=True)`
|
||||||
|
2. H <- T `Success`
|
||||||
|
|
||||||
|
The host should record the XPUBs that it received in the last repetition of
|
||||||
|
step 5. Every time the user connects the Trezor to the host, it should verify
|
||||||
|
that the XPUBs for the given accounts remain the same in order to prevent a
|
||||||
|
fake malicious Trezor from changing the seed.
|
||||||
|
|
||||||
|
The purpose of Trezor's commitment to internal entropy is to enforce that
|
||||||
|
Trezor chooses its internal entropy before the host provides the external
|
||||||
|
entropy. This ensures that Trezor cannot choose its internal entropy based on
|
||||||
|
the external entropy and manipulate the value of the resulting seed. The
|
||||||
|
commitment is computed as
|
||||||
|
`entropy_commitment=HMAC-SHA256(key=internal_entropy, msg="")`.
|
||||||
|
|
||||||
## RecoveryDevice
|
## RecoveryDevice
|
||||||
|
|
||||||
Recovery device lets user to recover BIP39 seed into
|
Recovery device lets user to recover BIP39 seed into
|
||||||
|
Loading…
Reference in New Issue
Block a user