Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 23 additions & 107 deletions tests/test_msg_bip85.py
Original file line numberDiff line numberDiff line change
@@ -1,158 +1,74 @@
# BIP-85 child mnemonic derivation tests.
#
# Tests GetBip85Mnemonic message which derives deterministic child
# mnemonics from the device seed per the BIP-85 specification.
#
# Uses the "all" x12 mnemonic as the master seed.
"""BIP-85 display-only tests.

Firmware >= 7.14.0 derives the BIP-85 child mnemonic, displays it on the
device screen, and responds with Success (mnemonic is never sent over USB).
"""

import unittest
import common

import keepkeylib.messages_pb2 as proto

# BIP-39 English wordlist (2048 words)
# We load it inline to avoid external file dependencies.
BIP39_WORDLIST = None

def _load_bip39_wordlist():
"""Load BIP-39 English wordlist from mnemonic package or fallback."""
global BIP39_WORDLIST
if BIP39_WORDLIST is not None:
return BIP39_WORDLIST

# Try the mnemonic package first (ships with python-keepkey deps)
try:
from mnemonic import Mnemonic
m = Mnemonic("english")
BIP39_WORDLIST = m.wordlist
return BIP39_WORDLIST
except ImportError:
pass

# Fallback: accept any lowercase alpha words and skip strict validation
BIP39_WORDLIST = None
return None


def _validate_mnemonic_words(mnemonic_str):
"""Validate that each word in the mnemonic is in the BIP-39 wordlist.

Returns (is_valid, bad_words) tuple. If wordlist unavailable, returns
(True, []) -- we still validate word count and format elsewhere.
"""
wordlist = _load_bip39_wordlist()
words = mnemonic_str.split()
if wordlist is None:
# No wordlist available; just check words are lowercase alpha
bad = [w for w in words if not w.isalpha() or not w.islower()]
return (len(bad) == 0, bad)
bad = [w for w in words if w not in wordlist]
return (len(bad) == 0, bad)
import keepkeylib.types_pb2 as proto_types


class TestMsgBip85(common.KeepKeyTest):
"""Test BIP-85 child mnemonic derivation from the device."""

def test_bip85_12word(self):
"""Derive a 12-word child mnemonic at index 0."""
"""Derive a 12-word child mnemonic at index 0 — device displays, returns Success."""
self.requires_firmware("7.14.0")
self.setup_mnemonic_allallall()

resp = self.client.call(proto.GetBip85Mnemonic(word_count=12, index=0))

# Response must be a Bip85Mnemonic message
self.assertTrue(
isinstance(resp, proto.Bip85Mnemonic),
"Expected Bip85Mnemonic response, got %s" % type(resp).__name__
)

mnemonic = resp.mnemonic
words = mnemonic.split()

# Must have exactly 12 words
self.assertTrue(
len(words) == 12,
"Expected 12 words, got %d: %s" % (len(words), mnemonic)
)

# Each word must be a valid BIP-39 word
is_valid, bad_words = _validate_mnemonic_words(mnemonic)
# Firmware display-only mode returns Success
self.assertTrue(
is_valid,
"Invalid BIP-39 words found: %s" % bad_words
isinstance(resp, proto.Success),
"Expected Success response, got %s" % type(resp).__name__
)

def test_bip85_24word(self):
"""Derive a 24-word child mnemonic at index 0."""
"""Derive a 24-word child mnemonic at index 0 — device displays, returns Success."""
self.requires_firmware("7.14.0")
self.setup_mnemonic_allallall()

resp = self.client.call(proto.GetBip85Mnemonic(word_count=24, index=0))

self.assertTrue(
isinstance(resp, proto.Bip85Mnemonic),
"Expected Bip85Mnemonic response, got %s" % type(resp).__name__
)

mnemonic = resp.mnemonic
words = mnemonic.split()

# Must have exactly 24 words
self.assertTrue(
len(words) == 24,
"Expected 24 words, got %d: %s" % (len(words), mnemonic)
)

# Each word must be a valid BIP-39 word
is_valid, bad_words = _validate_mnemonic_words(mnemonic)
self.assertTrue(
is_valid,
"Invalid BIP-39 words found: %s" % bad_words
isinstance(resp, proto.Success),
"Expected Success response, got %s" % type(resp).__name__
)

def test_bip85_different_indices(self):
"""Index 0 and index 1 must produce different child mnemonics."""
"""Index 0 and index 1 both succeed (different seeds displayed on device)."""
self.requires_firmware("7.14.0")
self.setup_mnemonic_allallall()

resp0 = self.client.call(proto.GetBip85Mnemonic(word_count=12, index=0))
resp1 = self.client.call(proto.GetBip85Mnemonic(word_count=12, index=1))

self.assertTrue(
isinstance(resp0, proto.Bip85Mnemonic),
"Expected Bip85Mnemonic for index 0, got %s" % type(resp0).__name__
isinstance(resp0, proto.Success),
"Expected Success for index 0, got %s" % type(resp0).__name__
)
self.assertTrue(
isinstance(resp1, proto.Bip85Mnemonic),
"Expected Bip85Mnemonic for index 1, got %s" % type(resp1).__name__
)

# Different indices must yield different mnemonics
self.assertTrue(
resp0.mnemonic != resp1.mnemonic,
"Index 0 and index 1 produced identical mnemonics: %s" % resp0.mnemonic
isinstance(resp1, proto.Success),
"Expected Success for index 1, got %s" % type(resp1).__name__
)

def test_bip85_deterministic(self):
"""Same parameters must produce the same child mnemonic every time."""
"""Same parameters succeed consistently (determinism verified by device display)."""
self.requires_firmware("7.14.0")
self.setup_mnemonic_allallall()

resp1 = self.client.call(proto.GetBip85Mnemonic(word_count=12, index=0))
resp2 = self.client.call(proto.GetBip85Mnemonic(word_count=12, index=0))

self.assertTrue(
isinstance(resp1, proto.Bip85Mnemonic),
"Expected Bip85Mnemonic (call 1), got %s" % type(resp1).__name__
isinstance(resp1, proto.Success),
"Expected Success (call 1), got %s" % type(resp1).__name__
)
self.assertTrue(
isinstance(resp2, proto.Bip85Mnemonic),
"Expected Bip85Mnemonic (call 2), got %s" % type(resp2).__name__
)

self.assertTrue(
resp1.mnemonic == resp2.mnemonic,
"Determinism violated: '%s' != '%s'" % (resp1.mnemonic, resp2.mnemonic)
isinstance(resp2, proto.Success),
"Expected Success (call 2), got %s" % type(resp2).__name__
)


Expand Down
1 change: 1 addition & 0 deletions tests/test_msg_ethereum_cfunc.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,7 @@ def test_sign_execTx(self):
self.requires_fullFeature()
self.requires_firmware("7.5.2")
self.setup_mnemonic_nopin_nopassphrase()
self.client.apply_policy("AdvancedMode", 1)

sig_v, sig_r, sig_s = self.client.ethereum_sign_tx(
n=[2147483692,2147483708,2147483648,0,0],
Expand Down
1 change: 1 addition & 0 deletions tests/test_msg_ethereum_erc20_0x_signtx.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -98,6 +98,7 @@ def test_sign_longdata_swap(self):
self.requires_fullFeature()
self.requires_firmware("7.0.2")
self.setup_mnemonic_nopin_nopassphrase()
self.client.apply_policy("AdvancedMode", 1)

sig_v, sig_r, sig_s = self.client.ethereum_sign_tx(
n=[2147483692,2147483708,2147483648,0,0],
Expand Down
3 changes: 2 additions & 1 deletion tests/test_msg_ethereum_signtx.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,7 +33,7 @@ class TestMsgEthereumSigntx(common.KeepKeyTest):
def test_ethereum_signtx_data(self):
self.requires_fullFeature()
self.setup_mnemonic_nopin_nopassphrase()
self.client.apply_policy("AdvancedMode", 0)
self.client.apply_policy("AdvancedMode", 1)

with self.client:
self.client.set_expected_responses(
Expand DownExpand Up@@ -441,6 +441,7 @@ def test_ethereum_signtx_data1_eip_1559(self):
self.requires_fullFeature()
self.requires_firmware("7.2.1")
self.setup_mnemonic_allallall()
self.client.apply_policy("AdvancedMode", 1)

# from trezor test vector:
# https://github.com/trezor/trezor-firmware/blob/master/common/tests/fixtures/ethereum/sign_tx_eip1559.json#L27
Expand Down