"""Functions for signing messages on the Ledger device."""
from __future__ import annotations
import binascii
import struct
from ledgereth.comms import Dongle, dongle_send_data, init_dongle
from ledgereth.constants import DATA_CHUNK_SIZE, DEFAULT_PATH_STRING
from ledgereth.objects import SignedMessage, SignedTypedMessage
from ledgereth.types import Text
from ledgereth.utils import (
chunks,
is_bip32_path,
parse_bip32_path,
)
[docs]
def sign_message(
message: Text,
sender_path: str = DEFAULT_PATH_STRING,
dongle: Dongle | None = None,
) -> SignedMessage:
"""Sign a simple text message.
The message will be prefixed by the Ethereum app on the Ledger device according to
`EIP-191`_.
:param message: (:code:`str|bytes`) - A bit of text to sign
:param sender_path: (:code:`str`) - HD derivation path for the account to
sign with.
:param dongle: (:class:`ledgerblue.Dongle.Dongle`) - The Web3 instance to use
:return: :class:`ledgereth.objects.SignedMessage`
.. _`EIP-191`: https://eips.ethereum.org/EIPS/eip-191
"""
given_dongle = dongle is not None
dongle = init_dongle(dongle)
retval = None
if isinstance(message, str):
message = message.encode("utf-8")
# Silence mypy due to type cohersion above
assert isinstance(message, bytes)
encoded = struct.pack(">I", len(message))
encoded += message
if not is_bip32_path(sender_path):
raise ValueError("Invalid sender BIP32 path given to sign_transaction")
path = parse_bip32_path(sender_path)
payload = (len(path) // 4).to_bytes(1, "big") + path + encoded
chunk_count = 0
for chunk in chunks(payload, DATA_CHUNK_SIZE):
chunk_size = len(chunk)
if chunk_count == 0:
retval = dongle_send_data(
dongle,
"SIGN_MESSAGE_FIRST_DATA",
chunk,
Lc=chunk_size.to_bytes(1, "big"),
)
else:
retval = dongle_send_data(
dongle,
"SIGN_MESSAGE_SECONDARY_DATA",
chunk,
Lc=chunk_size.to_bytes(1, "big"),
)
chunk_count += 1
if retval is None or len(retval) < 64:
raise Exception("Invalid response from Ledger")
v = int(retval[0])
r = int(binascii.hexlify(retval[1:33]), 16)
s = int(binascii.hexlify(retval[33:65]), 16)
signed = SignedMessage(message, v, r, s)
# If this func inited the dongle, then close it, otherwise core dump
if not given_dongle:
dongle.close()
return signed
[docs]
def sign_typed_data_draft(
domain_hash: Text,
message_hash: Text,
sender_path: str = DEFAULT_PATH_STRING,
dongle: Dongle | None = None,
) -> SignedTypedMessage:
"""Sign `EIP-721`_ typed data.
.. DANGER::
EIP-712 is still in DRAFT status and APIs may change, including the
Ledger app-ethereum implementation.
:param domain_hash: (:code:`str`) - Hash of the EIP-712 domain
:param message_hash: (:code:`str`) - Hash of the message
:param sender_path: (:code:`str`) - HD derivation path for the account to
sign with. Defaults to first account in the derivation path.
:param dongle: (:class:`ledgerblue.Dongle.Dongle`) - The Dongle instance to
use to communicate with the Ledger device
:return: :class:`ledgereth.objects.SignedTypedMessage` Signed message object
For a real example of usage, see how this is used with `eth_account`_ in
`ledgereth's unit tests`_.
.. _`EIP-721`: https://eips.ethereum.org/EIPS/eip-712
.. _`eth_account`: https://eth-account.readthedocs.io/
.. _`ledgereth's unit tests`: https://github.com/mikeshultz/ledger-eth-lib/blob/2e47e7b9d70136a6dda0229c7bf516ed6bbe850f/tests/test_message_signing.py#L55-L74
"""
given_dongle = dongle is not None
dongle = init_dongle(dongle)
retval = None
if isinstance(domain_hash, str):
domain_hash = domain_hash.encode("utf-8")
if isinstance(message_hash, str):
message_hash = message_hash.encode("utf-8")
# Silence mypy due to type cohersion above
assert isinstance(domain_hash, bytes)
assert isinstance(message_hash, bytes)
encoded = domain_hash + message_hash
if not is_bip32_path(sender_path):
raise ValueError("Invalid sender BIP32 path given to sign_transaction")
path = parse_bip32_path(sender_path)
payload = (len(path) // 4).to_bytes(1, "big") + path + encoded
retval = dongle_send_data(
dongle,
"SIGN_TYPED_DATA",
payload,
Lc=len(payload).to_bytes(1, "big"),
)
if retval is None or len(retval) < 64:
raise Exception("Invalid response from Ledger")
v = int(retval[0])
r = int(binascii.hexlify(retval[1:33]), 16)
s = int(binascii.hexlify(retval[33:65]), 16)
signed = SignedTypedMessage(domain_hash, message_hash, v, r, s)
# If this func inited the dongle, then close it, otherwise core dump
if not given_dongle:
dongle.close()
return signed