mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-01-03 12:00:59 +00:00
feat(core): Implement entropy check workflow in ResetDevice.
This commit is contained in:
parent
b9b36ef737
commit
b8462580a2
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.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,6 +38,7 @@ async def get_public_key(
|
||||
if auth_msg.address_n != address_n[: len(auth_msg.address_n)]:
|
||||
raise FORBIDDEN_KEY_PATH
|
||||
|
||||
if not keychain:
|
||||
keychain = await get_keychain(curve_name, [paths.AlwaysMatchingSchema])
|
||||
|
||||
node = keychain.derive(address_n)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
prev_int_entropy = None
|
||||
while True:
|
||||
# generate internal entropy
|
||||
int_entropy = random.bytes(32, True)
|
||||
if __debug__:
|
||||
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
|
||||
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
|
||||
# For SLIP-39 this is the Encrypted Master Secret
|
||||
secret = _compute_secret_from_entropy(int_entropy, ext_entropy, msg.strength)
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user