What changed, and why it matters
This commit updates the BitBox02 Python library so it refuses to sign Bitcoin and Ethereum transactions or messages unless the hardware wallet is running a firmware version that supports the anti-klepto feature. Anti-klepto is a protocol that helps prevent a compromised device from leaking secret key material through biased randomness in signatures. The change removes older, less-protected fallback signing paths and bumps the library version to 8.0.0 because it now requires newer firmware.
Users of the py-bitbox02 library should upgrade to version 8.0.0 and ensure their BitBox02 devices are on firmware 9.4.0 or newer (9.5.0 or newer for message/ETH signing). Developers integrating the library should review any code that pins to py-bitbox02 <8.0.0 or attempts to sign with older firmware, because those calls will now raise a version error.
Security signals we found
Removal of non-anti-klepto signing fallbacks
Mandatory anti-klepto protocol for BTC/ETH signing workflows
Firmware minimum-version enforcement via _require_atleast
Breaking API/behavior change signaled by major version bump to 8.0.0
Evidence from the diff
The patch removes conditional anti-klepto support in the Python client and enforces minimum firmware versions: 9.4.0 for BTC transaction signing and 9.5.0 for BTC/ETH message and ETH transaction signing. It deletes the non-anti-klepto branches in btc_sign_msg, eth_sign, and eth_sign_msg, making the anti-klepto protocol mandatory for those workflows. btc_sign already required anti-klepto conditionally and now requires it unconditionally for non-Schnorr inputs. Ed25519 workflows and the explicit EIP-712 opt-out are left unchanged. The package version is bumped from 7.1.0 to 8.0.0.
Changed components
py/bitbox02/bitbox02/bitbox02/bitbox02.pypy/bitbox02/bitbox02/bitbox02/__init__.pypy/bitbox02/CHANGELOG.mdInspect captured patch +44 / −66
### py/bitbox02/CHANGELOG.md
@@ -1,6 +1,8 @@
# Changelog
## [Unreleased]
+- Require firmware v9.4.0 or newer for BTC transaction signing and v9.5.0 or newer for
+ BTC/ETH message and ETH transaction signing
- `device_info()`: add the installed bootloader version to the returned device info
- Add `btc_xpubs()` to fetch multiple xpubs at once
- Bitcoin: add support for OP_RETURN outputs
### py/bitbox02/bitbox02/bitbox02/__init__.py
@@ -5,7 +5,7 @@
from __future__ import print_function
import sys
-__version__ = "7.1.0"
+__version__ = "8.0.0"
if sys.version_info.major != 3 or sys.version_info.minor < 6:
print(
### py/bitbox02/bitbox02/bitbox02/bitbox02.py
@@ -472,6 +472,9 @@ def btc_sign(
"""
# pylint: disable=no-member,too-many-branches,too-many-statements
+ # Anti-klepto support for BTC transaction signing was added in v9.4.0.
+ self._require_atleast(semver.VersionInfo(9, 4, 0))
+
assert version in (1, 2)
if locktime >= 500_000_000:
@@ -495,8 +498,6 @@ def btc_sign(
# OP_RETURN supported sice v9.24.0
self._require_atleast(semver.VersionInfo(9, 24, 0))
- supports_antiklepto = self.version >= semver.VersionInfo(9, 4, 0)
-
sigs: List[Tuple[int, bytes]] = []
# Init request
@@ -535,9 +536,7 @@ def btc_sign(
# Anti-Klepto protocol not supported yet for Schnorr signatures.
input_is_schnorr = is_taproot(script_configs[tx_input["script_config_index"]])
- perform_antiklepto = (
- supports_antiklepto and is_inputs_pass2 and not input_is_schnorr
- )
+ perform_antiklepto = is_inputs_pass2 and not input_is_schnorr
if perform_antiklepto:
host_nonce = os.urandom(32)
@@ -669,7 +668,8 @@ def btc_sign_msg(
"""
# pylint: disable=no-member
- self._require_atleast(semver.VersionInfo(9, 2, 0))
+ # Anti-klepto support for BTC message signing was added in v9.5.0.
+ self._require_atleast(semver.VersionInfo(9, 5, 0))
if coin in (btc.TBTC, btc.RBTC):
self._require_atleast(semver.VersionInfo(9, 23, 0))
@@ -678,34 +678,25 @@ def btc_sign_msg(
btc.BTCSignMessageRequest(coin=coin, script_config=script_config, msg=msg)
)
- supports_antiklepto = self.version >= semver.VersionInfo(9, 5, 0)
- if supports_antiklepto:
- host_nonce = os.urandom(32)
-
- request.sign_message.host_nonce_commitment.commitment = antiklepto_host_commit(
- host_nonce
- )
- signer_commitment = self._btc_msg_query(
- request, expected_response="antiklepto_signer_commitment"
- ).antiklepto_signer_commitment.commitment
+ host_nonce = os.urandom(32)
- request = btc.BTCRequest()
- request.antiklepto_signature.CopyFrom(
- antiklepto.AntiKleptoSignatureRequest(host_nonce=host_nonce)
- )
+ request.sign_message.host_nonce_commitment.commitment = antiklepto_host_commit(host_nonce)
+ signer_commitment = self._btc_msg_query(
+ request, expected_response="antiklepto_signer_commitment"
+ ).antiklepto_signer_commitment.commitment
- signature = self._btc_msg_query(
- request, expected_response="sign_message"
- ).sign_message.signature
- antiklepto_verify(host_nonce, signer_commitment, signature[:64])
+ request = btc.BTCRequest()
+ request.antiklepto_signature.CopyFrom(
+ antiklepto.AntiKleptoSignatureRequest(host_nonce=host_nonce)
+ )
- if self.debug:
- print("Antiklepto nonce verification PASSED")
+ signature = self._btc_msg_query(
+ request, expected_response="sign_message"
+ ).sign_message.signature
+ antiklepto_verify(host_nonce, signer_commitment, signature[:64])
- else:
- signature = self._btc_msg_query(
- request, expected_response="sign_message"
- ).sign_message.signature
+ if self.debug:
+ print("Antiklepto nonce verification PASSED")
sig, recid = signature[:64], signature[64]
@@ -908,6 +899,9 @@ def eth_sign(
"""
# pylint: disable=no-member
+ # Anti-klepto support for ETH transaction signing was added in v9.5.0.
+ self._require_atleast(semver.VersionInfo(9, 5, 0))
+
is_eip1559 = transaction.startswith(b"\x02")
def handle_antiklepto(request: eth.ETHRequest) -> bytes:
@@ -1013,30 +1007,17 @@ def handle_antiklepto(request: eth.ETHRequest) -> bytes:
)
)
- supports_antiklepto = self.version >= semver.VersionInfo(9, 5, 0)
- if supports_antiklepto:
- return handle_antiklepto(request)
-
- # Non-antiklepto path: handle chunking if needed
- response = self._eth_msg_query(request)
- if require_streaming and response.WhichOneof("response") == "data_request_chunk":
- response = self._handle_eth_chunking(response, data)
- if response.WhichOneof("response") != "sign":
- raise Exception(
- f"Unexpected response after chunking: {response.WhichOneof('response')}, expected: sign"
- )
- elif response.WhichOneof("response") != "sign":
- raise Exception(
- f"Unexpected response: {response.WhichOneof('response')}, expected: sign"
- )
- return response.sign.signature
+ return handle_antiklepto(request)
def eth_sign_msg(self, msg: bytes, keypath: Sequence[int], chain_id: int = 1) -> bytes:
"""
Signs message, the msg will be prefixed with "\x19Ethereum message\n" + len(msg) in the
hardware. 27 is added to the recID to denote an uncompressed pubkey.
"""
+ # Anti-klepto support for ETH message signing was added in v9.5.0.
+ self._require_atleast(semver.VersionInfo(9, 5, 0))
+
def format_as_uncompressed(sig: bytes) -> bytes:
# 27 is the magic constant to add to the recoverable ID to denote an uncompressed
# pubkey.
@@ -1052,29 +1033,24 @@ def format_as_uncompressed(sig: bytes) -> bytes:
)
)
- supports_antiklepto = self.version >= semver.VersionInfo(9, 5, 0)
- if supports_antiklepto:
- host_nonce = os.urandom(32)
-
- request.sign_msg.host_nonce_commitment.commitment = antiklepto_host_commit(host_nonce)
- signer_commitment = self._eth_msg_query(
- request, expected_response="antiklepto_signer_commitment"
- ).antiklepto_signer_commitment.commitment
+ host_nonce = os.urandom(32)
- request = eth.ETHRequest()
- request.antiklepto_signature.CopyFrom(
- antiklepto.AntiKleptoSignatureRequest(host_nonce=host_nonce)
- )
+ request.sign_msg.host_nonce_commitment.commitment = antiklepto_host_commit(host_nonce)
+ signer_commitment = self._eth_msg_query(
+ request, expected_response="antiklepto_signer_commitment"
+ ).antiklepto_signer_commitment.commitment
- signature = self._eth_msg_query(request, expected_response="sign").sign.signature
- antiklepto_verify(host_nonce, signer_commitment, signature[:64])
+ request = eth.ETHRequest()
+ request.antiklepto_signature.CopyFrom(
+ antiklepto.AntiKleptoSignatureRequest(host_nonce=host_nonce)
+ )
- if self.debug:
- print("Antiklepto nonce verification PASSED")
+ signature = self._eth_msg_query(request, expected_response="sign").sign.signature
+ antiklepto_verify(host_nonce, signer_commitment, signature[:64])
- return format_as_uncompressed(signature)
+ if self.debug:
+ print("Antiklepto nonce verification PASSED")
- signature = self._eth_msg_query(request, expected_response="sign").sign.signature
return format_as_uncompressed(signature)
def eth_sign_typed_msg(Why this scored 34/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.