Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6447ace
feat(transport): add DylibTransport for in-process libkkemu testing
BitHighlander Apr 26, 2026
2add091
test(dylib): screenshot regression for ringbuf capacity + canvas sema…
BitHighlander Apr 27, 2026
d4eda86
fix(dylib): address PR #14 review — strip-? consistency, narrow KK_TR…
BitHighlander Apr 27, 2026
e88ff15
feat(zcash): seed_fingerprint client + tests
BitHighlander Apr 29, 2026
69d28d6
test(zcash): split helper tests + cover client wrappers
BitHighlander Apr 29, 2026
1688716
Merge pull request #15 from BitHighlander/feat/zcash-seed-fingerprint
BitHighlander Apr 29, 2026
3335e6f
chore: defer planning test gates
BitHighlander Apr 30, 2026
a39dad4
test(eth): regression for EIP-1559 chunked-data signing bug (firmware…
BitHighlander Apr 28, 2026
7ecc099
test(eth): drop requires_message gate that probe-skips this test
BitHighlander Apr 28, 2026
cc0f4ae
test(ci): install pycryptodome so eth-utils.keccak has a backend
BitHighlander Apr 28, 2026
61ea6ab
test(eth): drop msg arg from assertEqual (custom 2-arg overload)
BitHighlander Apr 28, 2026
43e3b54
test(eth): gate EIP-1559 chunked-data regression on firmware 7.14.1+
BitHighlander Apr 29, 2026
fcdf6bf
release: python-keepkey 7.14.1
BitHighlander Apr 30, 2026
38b57f7
feat: add message-signing protocol bindings
BitHighlander Apr 30, 2026
297cba3
feat(7.14.2): XRP THORChain memo support + EVM depositWithExpiry reco…
BitHighlander May 15, 2026
bf870e6
Merge pull request #18 from BitHighlander/release/7.14.2-python-keepkey
BitHighlander May 15, 2026
eee4804
feat(hive): add Hive blockchain support (#19)
BitHighlander May 24, 2026
04119f3
fix(hive): regenerate messages_hive_pb2.py with old-style descriptor …
BitHighlander May 24, 2026
e338df0
fix(tests): port alpha CI test fixes to feature/hive baseline
BitHighlander May 24, 2026
4e7034e
test: skip legacy sighash test — firmware requires full tx digests
BitHighlander May 24, 2026
e717f10
test: skip all legacy sighash PCZT tests — firmware requires full tx …
BitHighlander May 24, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ jobs:
pip install --upgrade pip
pip install "protobuf>=3.20,<4"
pip install -e .
pip install pytest semver rlp requests
pip install pytest semver rlp requests eth-keys pycryptodome

- name: Wait for emulator
run: |
Expand Down
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[submodule "device-protocol"]
path = device-protocol
url = https://github.com/keepkey/device-protocol.git
url = https://github.com/BitHighlander/device-protocol.git
branch = master
[submodule "keepkeylib/eth/ethereum-lists"]
path = keepkeylib/eth/ethereum-lists
Expand Down
156 changes: 152 additions & 4 deletions keepkeylib/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
from . import messages_tron_pb2 as tron_proto
from . import messages_ton_pb2 as ton_proto
from . import messages_zcash_pb2 as zcash_proto
from . import messages_hive_pb2 as hive_proto
from . import types_pb2 as types
from . import eos
from . import nano
Expand Down Expand Up @@ -1628,9 +1629,26 @@ def solana_sign_tx(self, address_n, raw_tx):
)

@expect(solana_proto.SolanaMessageSignature)
def solana_sign_message(self, address_n, message):
def solana_sign_message(self, address_n, message, show_display=False):
return self.call(
solana_proto.SolanaSignMessage(address_n=address_n, message=message)
solana_proto.SolanaSignMessage(
address_n=address_n,
message=message,
show_display=show_display,
)
)

@expect(solana_proto.SolanaOffchainMessageSignature)
def solana_sign_offchain_message(self, address_n, message, message_format,
version=0, show_display=False):
return self.call(
solana_proto.SolanaSignOffchainMessage(
address_n=address_n,
version=version,
message_format=message_format,
message=message,
show_display=show_display,
)
)

# ── Tron ───────────────────────────────────────────────────
Expand All @@ -1646,6 +1664,37 @@ def tron_sign_tx(self, address_n, raw_tx):
tron_proto.TronSignTx(address_n=address_n, raw_tx=raw_tx)
)

@expect(tron_proto.TronMessageSignature)
def tron_sign_message(self, address_n, message, show_display=False):
return self.call(
tron_proto.TronSignMessage(
address_n=address_n,
message=message,
show_display=show_display,
)
)

@expect(proto.Success)
def tron_verify_message(self, address, signature, message):
return self.call(
tron_proto.TronVerifyMessage(
address=address,
signature=signature,
message=message,
)
)

@expect(tron_proto.TronTypedDataSignature)
def tron_sign_typed_hash(self, address_n, domain_separator_hash,
message_hash=None):
kwargs = dict(
address_n=address_n,
domain_separator_hash=domain_separator_hash,
)
if message_hash is not None:
kwargs['message_hash'] = message_hash
return self.call(tron_proto.TronSignTypedHash(**kwargs))

# ── TON ────────────────────────────────────────────────────
@expect(ton_proto.TonAddress)
def ton_get_address(self, address_n, show_display=False):
Expand All @@ -1659,12 +1708,40 @@ def ton_sign_tx(self, address_n, raw_tx):
ton_proto.TonSignTx(address_n=address_n, raw_tx=raw_tx)
)

@expect(ton_proto.TonMessageSignature)
def ton_sign_message(self, address_n, message, show_display=False):
return self.call(
ton_proto.TonSignMessage(
address_n=address_n,
message=message,
show_display=show_display,
)
)

# ── Zcash Address Display ─────────────────────────────────
@expect(zcash_proto.ZcashAddress)
def zcash_display_address(self, address_n, address, ak, nk, rivk, account=None):
def zcash_display_address(self, address_n, address, ak, nk, rivk,
account=None, expected_seed_fingerprint=None):
"""Display a Zcash unified address on the device for user confirmation.

Args:
address_n: ZIP-32 derivation path [32', 133', account']
address: unified address string ("u1...")
ak, nk, rivk: 32-byte FVK components for verification
account: account index (alternative to full path)
expected_seed_fingerprint: optional 32-byte ZIP-32 §6.1 seed
fingerprint. If provided, device verifies the match before
displaying and rejects with Failure on mismatch.

Returns:
ZcashAddress with .address and .seed_fingerprint of the
attesting device.
"""
kwargs = dict(address_n=address_n, address=address, ak=ak, nk=nk, rivk=rivk)
if account is not None:
kwargs['account'] = account
if expected_seed_fingerprint is not None:
kwargs['expected_seed_fingerprint'] = expected_seed_fingerprint
return self.call(zcash_proto.ZcashDisplayAddress(**kwargs))

# ── Zcash Orchard ──────────────────────────────────────────
Expand All @@ -1681,7 +1758,8 @@ def zcash_sign_pczt(self, address_n, actions, account=None,
header_digest=None, transparent_digest=None,
sapling_digest=None, orchard_digest=None,
orchard_flags=None, orchard_value_balance=None,
orchard_anchor=None, transparent_inputs=None):
orchard_anchor=None, transparent_inputs=None,
expected_seed_fingerprint=None):
"""Sign a Zcash Orchard shielded transaction via PCZT protocol.

Phase 2: Sends ZcashSignPCZT, then loops on ZcashPCZTActionAck
Expand Down Expand Up @@ -1737,6 +1815,8 @@ def zcash_sign_pczt(self, address_n, actions, account=None,
kwargs['orchard_value_balance'] = orchard_value_balance
if orchard_anchor is not None:
kwargs['orchard_anchor'] = orchard_anchor
if expected_seed_fingerprint is not None:
kwargs['expected_seed_fingerprint'] = expected_seed_fingerprint

resp = self.call(zcash_proto.ZcashSignPCZT(**kwargs))

Expand Down Expand Up @@ -1773,6 +1853,74 @@ def zcash_sign_pczt(self, address_n, actions, account=None,

return resp

# ── Hive ────────────────────────────────────────────────────
@expect(hive_proto.HivePublicKey)
def hive_get_public_key(self, address_n, show_display=False, role=None):
kwargs = dict(address_n=address_n, show_display=show_display)
if role is not None:
kwargs['role'] = role
return self.call(hive_proto.HiveGetPublicKey(**kwargs))

@expect(hive_proto.HivePublicKeys)
def hive_get_public_keys(self, account_index=0, show_display=False):
return self.call(
hive_proto.HiveGetPublicKeys(account_index=account_index, show_display=show_display)
)

@expect(hive_proto.HiveSignedTx)
def hive_sign_tx(self, address_n, chain_id, ref_block_num, ref_block_prefix,
expiration, sender, recipient, amount, decimals, asset_symbol, memo=''):
return self.call(hive_proto.HiveSignTx(**{
'address_n': address_n,
'chain_id': chain_id,
'ref_block_num': ref_block_num,
'ref_block_prefix': ref_block_prefix,
'expiration': expiration,
'from': sender,
'to': recipient,
'amount': amount,
'decimals': decimals,
'asset_symbol': asset_symbol,
'memo': memo,
}))

@expect(hive_proto.HiveSignedAccountCreate)
def hive_sign_account_create(self, address_n, chain_id, ref_block_num, ref_block_prefix,
expiration, creator, new_account_name, fee_amount=3000,
owner_key='', active_key='', posting_key='', memo_key=''):
return self.call(hive_proto.HiveSignAccountCreate(
address_n=address_n,
chain_id=chain_id,
ref_block_num=ref_block_num,
ref_block_prefix=ref_block_prefix,
expiration=expiration,
creator=creator,
new_account_name=new_account_name,
fee_amount=fee_amount,
owner_key=owner_key,
active_key=active_key,
posting_key=posting_key,
memo_key=memo_key,
))

@expect(hive_proto.HiveSignedAccountUpdate)
def hive_sign_account_update(self, address_n, chain_id, ref_block_num, ref_block_prefix,
expiration, account,
new_owner_key='', new_active_key='',
new_posting_key='', new_memo_key=''):
return self.call(hive_proto.HiveSignAccountUpdate(
address_n=address_n,
chain_id=chain_id,
ref_block_num=ref_block_num,
ref_block_prefix=ref_block_prefix,
expiration=expiration,
account=account,
new_owner_key=new_owner_key,
new_active_key=new_active_key,
new_posting_key=new_posting_key,
new_memo_key=new_memo_key,
))

class KeepKeyClient(ProtocolMixin, TextUIMixin, BaseClient):
pass

Expand Down
69 changes: 69 additions & 0 deletions keepkeylib/hive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from . import messages_hive_pb2 as proto


def get_public_key(client, address_n, show_display=False, role=None):
kwargs = dict(address_n=address_n, show_display=show_display)
if role is not None:
kwargs['role'] = role
return client.call(proto.HiveGetPublicKey(**kwargs))


def get_public_keys(client, account_index=0, show_display=False):
return client.call(
proto.HiveGetPublicKeys(account_index=account_index, show_display=show_display)
)


def sign_tx(client, address_n, chain_id, ref_block_num, ref_block_prefix,
expiration, sender, recipient, amount, decimals, asset_symbol, memo=''):
# 'from' is a Python keyword so use **-unpacking to set the field
return client.call(proto.HiveSignTx(**{
'address_n': address_n,
'chain_id': chain_id,
'ref_block_num': ref_block_num,
'ref_block_prefix': ref_block_prefix,
'expiration': expiration,
'from': sender,
'to': recipient,
'amount': amount,
'decimals': decimals,
'asset_symbol': asset_symbol,
'memo': memo,
}))


def sign_account_create(client, address_n, chain_id, ref_block_num, ref_block_prefix,
expiration, creator, new_account_name, fee_amount=3000,
owner_key='', active_key='', posting_key='', memo_key=''):
return client.call(proto.HiveSignAccountCreate(
address_n=address_n,
chain_id=chain_id,
ref_block_num=ref_block_num,
ref_block_prefix=ref_block_prefix,
expiration=expiration,
creator=creator,
new_account_name=new_account_name,
fee_amount=fee_amount,
owner_key=owner_key,
active_key=active_key,
posting_key=posting_key,
memo_key=memo_key,
))


def sign_account_update(client, address_n, chain_id, ref_block_num, ref_block_prefix,
expiration, account,
new_owner_key='', new_active_key='',
new_posting_key='', new_memo_key=''):
return client.call(proto.HiveSignAccountUpdate(
address_n=address_n,
chain_id=chain_id,
ref_block_num=ref_block_num,
ref_block_prefix=ref_block_prefix,
expiration=expiration,
account=account,
new_owner_key=new_owner_key,
new_active_key=new_active_key,
new_posting_key=new_posting_key,
new_memo_key=new_memo_key,
))
22 changes: 21 additions & 1 deletion keepkeylib/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from . import messages_tron_pb2 as tron_proto
from . import messages_ton_pb2 as ton_proto
from . import messages_zcash_pb2 as zcash_proto
from . import messages_hive_pb2 as hive_proto

map_type_to_class = {}
map_class_to_type = {}
Expand Down Expand Up @@ -97,4 +98,23 @@ def check_missing():
map_type_to_class[wire_id] = msg_class
map_class_to_type[msg_class] = wire_id

# check_missing() — skip: Zcash types are not in old messages_pb2 enum
# Manually register Hive messages (not in the old messages_pb2.py enum)
_hive_wire_ids = {
1600: ('HiveGetPublicKey', hive_proto),
1601: ('HivePublicKey', hive_proto),
1602: ('HiveSignTx', hive_proto),
1603: ('HiveSignedTx', hive_proto),
1604: ('HiveGetPublicKeys', hive_proto),
1605: ('HivePublicKeys', hive_proto),
1606: ('HiveSignAccountCreate', hive_proto),
1607: ('HiveSignedAccountCreate', hive_proto),
1608: ('HiveSignAccountUpdate', hive_proto),
1609: ('HiveSignedAccountUpdate', hive_proto),
}
for wire_id, (msg_name, mod) in _hive_wire_ids.items():
msg_class = getattr(mod, msg_name, None)
if msg_class is not None:
map_type_to_class[wire_id] = msg_class
map_class_to_type[msg_class] = wire_id

# check_missing() — skip: Zcash/Hive types are not in old messages_pb2 enum
Loading