Skip to content

Commit

Permalink
feat(core): Implement entropy check workflow in ResetDevice.
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewkozlik committed Jan 2, 2025
1 parent df97d8d commit 01a1f47
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 55 deletions.
1 change: 1 addition & 0 deletions core/.changelog.d/4155.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Entropy check workflow in ResetDevice.
9 changes: 7 additions & 2 deletions core/src/apps/bitcoin/get_public_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@
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
Expand All @@ -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)

Expand Down
8 changes: 6 additions & 2 deletions core/src/apps/common/mnemonic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion core/src/apps/debug/load_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 2 additions & 3 deletions core/src/apps/management/recovery_device/homescreen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
97 changes: 73 additions & 24 deletions core/src/apps/management/reset_device/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
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

Expand Down Expand Up @@ -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 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.
Expand All @@ -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),
)
Expand All @@ -124,6 +142,37 @@ 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 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:
words = mnemonic.split()
await layout.show_backup_intro(single_share=True, num_of_words=len(words))
Expand Down Expand Up @@ -272,7 +321,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

Expand All @@ -282,7 +331,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

Expand Down
2 changes: 0 additions & 2 deletions core/src/storage/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
88 changes: 67 additions & 21 deletions docs/common/message-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `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

Recovery device lets user to recover BIP39 seed into
Expand Down

0 comments on commit 01a1f47

Please sign in to comment.