1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-04-20 00:59:02 +00:00

feat(python): support bitcoin-like transaction signing anti-exfil protocol

This commit is contained in:
Ondřej Vejpustek 2025-03-05 17:16:21 +01:00
parent 723ca105d8
commit 34309b6d09

View File

@ -25,6 +25,7 @@ from typing import TYPE_CHECKING, Any, AnyStr, List, Optional, Sequence, Tuple
from typing_extensions import Protocol, TypedDict
from . import exceptions, messages
from .anti_exfil import AntiExfilSignature, commit_entropy, generate_entropy, verify
from .tools import _return_success, prepare_message_bytes, session
if TYPE_CHECKING:
@ -265,30 +266,21 @@ def verify_message(
@session
def sign_tx(
def sign_tx_common(
client: "TrezorClient",
coin_name: str,
inputs: Sequence[messages.TxInputType],
outputs: Sequence[messages.TxOutputType],
details: Optional[messages.SignTx] = None,
prev_txes: Optional["TxCacheType"] = None,
payment_reqs: Sequence[messages.TxAckPaymentRequest] = (),
preauthorized: bool = False,
unlock_path: Optional[List[int]] = None,
unlock_path_mac: Optional[bytes] = None,
details: Optional[messages.SignTx],
prev_txes: Optional["TxCacheType"],
payment_reqs: Sequence[messages.TxAckPaymentRequest],
preauthorized: bool,
unlock_path: Optional[List[int]],
unlock_path_mac: Optional[bytes],
use_anti_exfil: bool,
entropy_list: Optional[List[bytes]],
**kwargs: Any,
) -> Tuple[Sequence[Optional[bytes]], bytes]:
"""Sign a Bitcoin-like transaction.
Returns a list of signatures (one for each provided input) and the
network-serialized transaction.
In addition to the required arguments, it is possible to specify additional
transaction properties (version, lock time, expiry...). Each additional argument
must correspond to a field in the `SignTx` data type. Note that some fields
(`inputs_count`, `outputs_count`, `coin_name`) will be inferred from the arguments
and cannot be overriden by kwargs.
"""
) -> Tuple[Sequence[Optional[AntiExfilSignature]], bytes]:
if prev_txes is None:
prev_txes = {}
@ -349,18 +341,45 @@ def sign_tx(
)
R = messages.RequestType
nonce_commitment_list: List[Optional[bytes]] = [None for _ in inputs]
if use_anti_exfil and entropy_list is None:
entropy_list = [generate_entropy() for _ in inputs]
while True:
# If there's some part of signed transaction, let's add it
if res.serialized:
if res.serialized.serialized_tx:
if use_anti_exfil:
# If host uses the anti-exfil protocol, it should not rely on device to serialize the transaction correctly.
raise ValueError("Serialization is not expected")
serialized_tx += res.serialized.serialized_tx
if res.serialized.signature_index is not None:
idx = res.serialized.signature_index
sig = res.serialized.signature
assert sig is not None
if signatures[idx] is not None:
raise ValueError(f"Signature for index {idx} already filled")
signatures[idx] = sig
if use_anti_exfil:
assert entropy_list is not None
nonce_commitment = nonce_commitment_list[idx]
if nonce_commitment is None:
# Device provides a signature without commiting to the nonce before. This is a violation of the anti-exfil protocol.
raise ValueError(
f"Nonce commitment for index {idx} not provided"
)
# This function verifies that the signature includes the host's entropy and that its s value is less than half of the curve's order. However, it does not verify the signature itself, as trezorlib doesn't have the digest. The verification of the signature is the caller's responsibility.
if not verify(
None,
sig,
None,
entropy_list[idx],
nonce_commitment,
):
# This is a violation of the anti-exfil protocol.
raise ValueError(f"Invalid signature for index {idx}")
if res.request_type == R.TXFINISHED:
break
@ -387,7 +406,21 @@ def sign_tx(
msg = copy_tx_meta(current_tx)
elif res.request_type in (R.TXINPUT, R.TXORIGINPUT):
assert res.details.request_index is not None
msg.inputs = [current_tx.inputs[res.details.request_index]]
idx = res.details.request_index
msg.inputs = [current_tx.inputs[idx]]
if use_anti_exfil:
assert entropy_list is not None
msg.inputs[0].entropy_commitment = commit_entropy(entropy_list[idx])
elif use_anti_exfil and res.request_type == R.TXENTROPY:
assert res.details.request_index is not None
idx = res.details.request_index
nonce_commitment = res.details.nonce_commitment
nonce_commitment_list[idx] = nonce_commitment
if nonce_commitment is None:
# Device requests an entropy without commiting to the nonce before. This is a violation of the anti-exfil protocol.
raise ValueError(f"Nonce commitment for index {idx} not provided")
assert entropy_list is not None
msg.entropy = messages.TxEntropyType(entropy=entropy_list[idx])
elif res.request_type == R.TXOUTPUT:
assert res.details.request_index is not None
if res.details.tx_hash:
@ -416,7 +449,128 @@ def sign_tx(
if i.script_type != messages.InputScriptType.EXTERNAL and sig is None:
raise exceptions.TrezorException("Some signatures are missing!")
return signatures, serialized_tx
if use_anti_exfil:
assert entropy_list is not None
return [
(
AntiExfilSignature(
signature=sig, entropy=entropy, nonce_commitment=nonce_commitment
)
if sig is not None
else None
)
for sig, entropy, nonce_commitment in zip(
signatures, entropy_list, nonce_commitment_list
)
], serialized_tx
else:
return [
(
AntiExfilSignature(signature=sig, entropy=None, nonce_commitment=None)
if sig is not None
else None
)
for sig in signatures
], serialized_tx
@session
def sign_tx(
client: "TrezorClient",
coin_name: str,
inputs: Sequence[messages.TxInputType],
outputs: Sequence[messages.TxOutputType],
details: Optional[messages.SignTx] = None,
prev_txes: Optional["TxCacheType"] = None,
payment_reqs: Sequence[messages.TxAckPaymentRequest] = (),
preauthorized: bool = False,
unlock_path: Optional[List[int]] = None,
unlock_path_mac: Optional[bytes] = None,
**kwargs: Any,
) -> Tuple[Sequence[Optional[bytes]], bytes]:
"""Sign a Bitcoin-like transaction.
Returns a list of signatures (one for each provided input) and the
network-serialized transaction.
In addition to the required arguments, it is possible to specify additional
transaction properties (version, lock time, expiry...). Each additional argument
must correspond to a field in the `SignTx` data type. Note that some fields
(`inputs_count`, `outputs_count`, `coin_name`) will be inferred from the arguments
and cannot be overriden by kwargs.
"""
anti_exfil_signatures, serialization = sign_tx_common(
client,
coin_name,
inputs,
outputs,
details,
prev_txes,
payment_reqs,
preauthorized,
unlock_path,
unlock_path_mac,
False,
None,
**kwargs,
)
return [
s.signature if s is not None else None for s in anti_exfil_signatures
], serialization
@session
def sign_tx_new(
client: "TrezorClient",
coin_name: str,
inputs: Sequence[messages.TxInputType],
outputs: Sequence[messages.TxOutputType],
details: Optional[messages.SignTx] = None,
prev_txes: Optional["TxCacheType"] = None,
payment_reqs: Sequence[messages.TxAckPaymentRequest] = (),
preauthorized: bool = False,
unlock_path: Optional[List[int]] = None,
unlock_path_mac: Optional[bytes] = None,
use_anti_exfil: bool = True,
entropy_list: Optional[List[bytes]] = None,
**kwargs: Any,
) -> Sequence[Optional[AntiExfilSignature]]:
"""Sign a Bitcoin-like transaction.
Returns a list of `AntiExfilSignature` objects (one for each provided input).
If `use_anti_exfil` is set to `True`, the anti-exfilitration protocol will be
used. The purpose of this protocol is to prevent the device from leaking
its secrets through the signatures. In this case, `AntiExfilSignature` objects
will have non-emtpy fields `entropy` and `nonce_commitment`. It's the caller
responsibility to verify the signature and the nonce commitment. The caller
can optionally provide a list of entropies to be used in the protocol. Ideally,
the caller should provide the same list of entropies if the signing is repeated
due to a error to prevent the device to perform nonce-grinding attacks.
In addition to the required arguments, it is possible to specify additional
transaction properties (version, lock time, expiry...). Each additional argument
must correspond to a field in the `SignTx` data type. Note that some fields
(`inputs_count`, `outputs_count`, `coin_name`) will be inferred from the arguments
and cannot be overriden by kwargs.
"""
anti_exfil_signatures, serialization = sign_tx_common(
client,
coin_name,
inputs,
outputs,
details,
prev_txes,
payment_reqs,
preauthorized,
unlock_path,
unlock_path_mac,
use_anti_exfil,
entropy_list,
serialize=False,
**kwargs,
)
return anti_exfil_signatures
def authorize_coinjoin(