diff --git a/core/.changelog.d/4155.added b/core/.changelog.d/4155.added new file mode 100644 index 0000000000..c138763493 --- /dev/null +++ b/core/.changelog.d/4155.added @@ -0,0 +1 @@ +Entropy check workflow in ResetDevice. diff --git a/core/src/apps/bitcoin/get_public_key.py b/core/src/apps/bitcoin/get_public_key.py index f852b1d134..272aac54db 100644 --- a/core/src/apps/bitcoin/get_public_key.py +++ b/core/src/apps/bitcoin/get_public_key.py @@ -6,9 +6,13 @@ if TYPE_CHECKING: from trezor.messages import GetPublicKey, PublicKey from trezor.protobuf import MessageType + from apps.common.keychain import Keychain + async def get_public_key( - msg: GetPublicKey, auth_msg: MessageType | None = None + msg: GetPublicKey, + auth_msg: MessageType | None = None, + keychain: Keychain | None = None, ) -> PublicKey: from trezor import TR, wire from trezor.enums import InputScriptType @@ -34,7 +38,8 @@ async def get_public_key( if auth_msg.address_n != address_n[: len(auth_msg.address_n)]: raise FORBIDDEN_KEY_PATH - keychain = await get_keychain(curve_name, [paths.AlwaysMatchingSchema]) + if not keychain: + keychain = await get_keychain(curve_name, [paths.AlwaysMatchingSchema]) node = keychain.derive(address_n) diff --git a/core/src/apps/common/mnemonic.py b/core/src/apps/common/mnemonic.py index 07f669237b..4707d73e3a 100644 --- a/core/src/apps/common/mnemonic.py +++ b/core/src/apps/common/mnemonic.py @@ -32,8 +32,12 @@ def is_bip39() -> bool: return get_type() == BackupType.Bip39 -def get_seed(passphrase: str = "", progress_bar: bool = True) -> bytes: - mnemonic_secret = get_secret() +def get_seed( + passphrase: str = "", + progress_bar: bool = True, + mnemonic_secret: bytes | None = None, +) -> bytes: + mnemonic_secret = mnemonic_secret or get_secret() if mnemonic_secret is None: raise ValueError # Mnemonic not set diff --git a/core/src/apps/debug/load_device.py b/core/src/apps/debug/load_device.py index 7f7012c0b1..dbb7589abb 100644 --- a/core/src/apps/debug/load_device.py +++ b/core/src/apps/debug/load_device.py @@ -62,10 +62,10 @@ async def load_device(msg: LoadDevice) -> Success: storage_device.store_mnemonic_secret( secret, - backup_type, needs_backup=msg.needs_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_label(msg.label or "") if msg.pin: diff --git a/core/src/apps/management/recovery_device/homescreen.py b/core/src/apps/management/recovery_device/homescreen.py index 9899b3fe6d..0ae642d1e6 100644 --- a/core/src/apps/management/recovery_device/homescreen.py +++ b/core/src/apps/management/recovery_device/homescreen.py @@ -212,9 +212,8 @@ async def _finish_recovery(secret: bytes, backup_type: BackupType) -> Success: if backup_type is None: raise RuntimeError - storage_device.store_mnemonic_secret( - secret, backup_type, needs_backup=False, no_backup=False - ) + storage_device.store_mnemonic_secret(secret, needs_backup=False, no_backup=False) + storage_device.set_backup_type(backup_type) if backup_types.is_slip39_backup_type(backup_type): if not backup_types.is_extendable_backup_type(backup_type): identifier = storage_recovery.get_slip39_identifier() diff --git a/core/src/apps/management/reset_device/__init__.py b/core/src/apps/management/reset_device/__init__.py index 1977348268..b3413279c1 100644 --- a/core/src/apps/management/reset_device/__init__.py +++ b/core/src/apps/management/reset_device/__init__.py @@ -3,8 +3,8 @@ from typing import TYPE_CHECKING, Sequence import storage import storage.device as storage_device from trezor import TR -from trezor.crypto import slip39 -from trezor.enums import BackupType +from trezor.crypto import hmac, slip39 +from trezor.enums import BackupType, MessageType from trezor.ui.layouts import confirm_action 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 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 if msg.pin_protection: newpin = await request_pin_confirm() if not config.change_pin("", newpin, None, None): raise ProcessError("Failed to set PIN") - # generate and display internal entropy - int_entropy = random.bytes(32, True) - if __debug__: - storage.debug.reset_internal_entropy = int_entropy + prev_int_entropy = None + while True: + # generate internal entropy + int_entropy = random.bytes(32, True) + if __debug__: + storage.debug.reset_internal_entropy = int_entropy - # request external entropy and compute the master secret - entropy_ack = await call(EntropyRequest(), EntropyAck) - ext_entropy = entropy_ack.entropy - # For SLIP-39 this is the Encrypted Master Secret - secret = _compute_secret_from_entropy(int_entropy, ext_entropy, msg.strength) + entropy_commitment = ( + hmac(hmac.SHA256, int_entropy, b"").digest() if msg.entropy_check else None + ) - # Check backup type, perform type-specific handling - if backup_type == BAK_T_BIP39: - # in BIP-39 we store mnemonic string instead of the secret - secret = bip39.from_data(secret).encode() - elif backup_types.is_slip39_backup_type(backup_type): - # generate and set SLIP39 parameters - storage_device.set_slip39_iteration_exponent(slip39.DEFAULT_ITERATION_EXPONENT) - else: - # Unknown backup type. - raise RuntimeError + # request external entropy and compute the master secret + entropy_ack = await call( + EntropyRequest( + entropy_commitment=entropy_commitment, prev_entropy=prev_int_entropy + ), + EntropyAck, + ) + ext_entropy = entropy_ack.entropy + # For SLIP-39 this is the Encrypted Master Secret + secret = _compute_secret_from_entropy(int_entropy, ext_entropy, msg.strength) + + if backup_type == BAK_T_BIP39: + # in BIP-39 we store mnemonic string instead of the secret + secret = bip39.from_data(secret).encode() + + if not msg.entropy_check or not await _entropy_check(secret): + break + + prev_int_entropy = int_entropy # If either of skip_backup or no_backup is specified, we are not doing backup now. # 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.store_mnemonic_secret( secret, # for SLIP-39, this is the EMS - backup_type, needs_backup=not perform_backup, no_backup=bool(msg.no_backup), ) @@ -124,6 +142,44 @@ async def reset_device(msg: ResetDevice) -> Success: return Success(message="Initialized") +async def _entropy_check(secret: bytes) -> bool: + """Returns True to indicate that entropy check loop should continue.""" + from trezor.messages import GetPublicKey, Success + 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 = Success() + while True: + req = await call_any( + msg, + MessageType.GetPublicKey, + MessageType.ResetDeviceContinue, + MessageType.ResetDeviceFinish, + ) + assert req.MESSAGE_WIRE_TYPE is not None + + if req.MESSAGE_WIRE_TYPE == MessageType.ResetDeviceContinue: + return True + + if req.MESSAGE_WIRE_TYPE == MessageType.ResetDeviceFinish: + return False + + assert GetPublicKey.is_type_of(req) + req.show_display = False + curve_name = ( + req.ecdsa_curve_name + or coininfo.by_name(req.coin_name or "Bitcoin").curve_name + ) + keychain = Keychain(seed, curve_name, [paths.AlwaysMatchingSchema]) + msg = await get_public_key(req, keychain=keychain) + + async def _backup_bip39(mnemonic: str) -> None: words = mnemonic.split() await layout.show_backup_intro(single_share=True, num_of_words=len(words)) @@ -272,7 +328,7 @@ def _validate_reset_device(msg: ResetDevice) -> None: 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: from trezor.crypto import hashlib @@ -282,7 +338,7 @@ def _compute_secret_from_entropy( ehash.update(ext_entropy) entropy = ehash.digest() # take a required number of bytes - strength = strength_in_bytes // 8 + strength = strength_bits // 8 secret = entropy[:strength] return secret diff --git a/core/src/storage/device.py b/core/src/storage/device.py index 62c74e55d2..b6bb3ddb10 100644 --- a/core/src/storage/device.py +++ b/core/src/storage/device.py @@ -178,13 +178,11 @@ def set_homescreen(homescreen: bytes) -> None: def store_mnemonic_secret( secret: bytes, - backup_type: BackupType, needs_backup: bool = False, no_backup: bool = False, ) -> None: set_version(common.STORAGE_VERSION_CURRENT) 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_bool(_NAMESPACE, INITIALIZED, True, public=True) if not no_backup: diff --git a/docs/common/message-workflows.md b/docs/common/message-workflows.md index e0fbabc713..82e7473703 100644 --- a/docs/common/message-workflows.md +++ b/docs/common/message-workflows.md @@ -239,35 +239,81 @@ example, by using PKCS7. See ## ResetDevice -Reset device message performs Trezor device -setup and generates new wallet with new recovery -seed. The device must be in unitialized -state, 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, +The ResetDevice message performs Trezor device +setup and generates a new wallet with a new recovery +seed. The device must be in unitialized state, meaning that +the firmware is already installed but it has not been initialized +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 -initialization the screen is showing "Go to trezor.io". The reset device -can be done in Trezor Wallet interface (https://trezor.io/start) and -also with Python trezorctl command. After sending -message to the device, device warn us to never make a digital copy of -your recovery seed and never upload it online, this message has to be -confirmed by pressing on "I understand" on the device. After confirmed, -the device produces internal entropy which is random of 32 bytes, -requests external entropy which is produced in computer and computes -mnemonic (recovery seed) using internal, external entropy and given -strength (12, 18 or 24 words). Trezor Wallet -interface doesn't provide option to choose how many words there should +initialization, the screen is showing "Go to trezor.io". The device reset +can be done in the Trezor Suite interface (https://trezor.io/start) or +using Python trezorctl command. After sending the ResetDevice +message to the device, the device warns the user to never make a digital copy +of their recovery seed and never upload it online, this message has to be +confirmed by pressing "I understand" on the device. After confirmation, +the device produces internal entropy which is a random value of 32 bytes, +requests external entropy which is produced in the host computer and computes +the mnemonic (recovery seed) using internal, external entropy and the given +strength (12, 18 or 24 words). Trezor Suite +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 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 words mnemonic is default). After showing mnemonic on the Trezor device, -Trezor Model T requires 2 random words to -be entered to the device to confirm the user has written down the -mnemonic properly. If there are errors in entered words, the device +Trezor Model T requires the user to enter several words at random positions +in the mnemonic to confirm that the user has written down the +mnemonic properly. If there are errors in the entered words, the device 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 -to set the label and pin (setting up the pin can be skipped) for the +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 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 `Success` (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 `ResetDeviceContinue` (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 `ResetDeviceFinish` + 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 Recovery device lets user to recover BIP39 seed into