Skip to content

Commit

Permalink
WIP: Ledger wallets could not sign using ethereum
Browse files Browse the repository at this point in the history
Users could not use Ledger hardware wallets to sign messages using an Ethereum key.

The command / response scheme used by Ledger to address the device is similar to the ISO/IEC 7816-4 smartcard protocol. Each command / response packet is called an APDU (application protocol data unit).

Each APDU is specific to Ledger application, that adds the support for a chain or functionality.

Solution: Use the Ledgereth library to send the Ethereum APDUs via ledgerblue.
  • Loading branch information
hoh committed Aug 19, 2023
1 parent 70f28da commit c47228b
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 0 deletions.
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ solana =
tezos =
pynacl
aleph-pytezos==0.1.1
ledger =
ledgereth==0.9.0
docs =
sphinxcontrib-plantuml

Expand Down
Empty file.
3 changes: 3 additions & 0 deletions src/aleph/sdk/wallets/ledger/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .ethereum import LedgerETHAccount

__all__ = ["LedgerETHAccount"]
60 changes: 60 additions & 0 deletions src/aleph/sdk/wallets/ledger/ethereum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from typing import Dict, List, Optional

from eth_typing import HexStr
from ledgerblue.Dongle import Dongle
from ledgereth import find_account, get_account_by_path, get_accounts
from ledgereth.comms import init_dongle
from ledgereth.messages import sign_message
from ledgereth.objects import LedgerAccount, SignedMessage

from ...chains.common import BaseAccount, get_verification_buffer


class LedgerETHAccount(BaseAccount):
"""Account using the Ethereum app on Ledger hardware wallets."""

CHAIN = "ETH"
CURVE = "secp256k1"
_account: LedgerAccount
_device: Dongle

def __init__(self, account: LedgerAccount, device: Optional[Dongle]):
self._account = account
self._device = device or init_dongle()

@staticmethod
def from_address(address: str, device: Dongle) -> Optional[LedgerAccount]:
return find_account(address=address, dongle=device, count=5)

@staticmethod
def from_path(path: str, device: Dongle) -> LedgerAccount:
return get_account_by_path(path_string=path, dongle=device)

async def sign_message(self, message: Dict) -> Dict:
"""Sign a message inplace."""
message: Dict = self._setup_sender(message)

# TODO: Check why the code without a wallet uses `encode_defunct`.
msghash: bytes = get_verification_buffer(message)
sig: SignedMessage = sign_message(msghash, dongle=self._device)

signature: HexStr = sig.signature

message["signature"] = signature
return message

def get_address(self) -> str:
return self._account.address

def get_public_key(self) -> str:
raise NotImplementedError()


def get_fallback_account() -> LedgerETHAccount:
"""Returns the first account available on the device first device found."""
device: Dongle = init_dongle()
accounts: List[LedgerAccount] = get_accounts(dongle=device, count=1)
if not accounts:
raise ValueError("No account found on device")
account = accounts[0]
return LedgerETHAccount(account=account, device=device)
38 changes: 38 additions & 0 deletions tests/unit/test_wallet_ethereum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from dataclasses import asdict, dataclass
from pathlib import Path
from tempfile import NamedTemporaryFile

import pytest
from ledgereth.comms import init_dongle

from aleph.sdk.chains.common import get_verification_buffer
from aleph.sdk.chains.ethereum import verify_signature
from aleph.sdk.exceptions import BadSignatureError
from aleph.sdk.wallets.ledger.ethereum import LedgerETHAccount, get_fallback_account


@dataclass
class Message:
chain: str
sender: str
type: str
item_hash: str


@pytest.mark.asyncio
async def test_LedgerETHAccount(ethereum_account):
account: LedgerETHAccount = get_fallback_account()

message = Message("ETH", account.get_address(), "SomeType", "ItemHash")
signed = await account.sign_message(asdict(message))
assert signed["signature"]
assert len(signed["signature"]) == 132

address = account.get_address()
assert address
assert type(address) == str
assert len(address) == 42

pubkey = account.get_public_key()
assert type(pubkey) == str
assert len(pubkey) == 68

0 comments on commit c47228b

Please sign in to comment.