Skip to content

Commit

Permalink
Feature: 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 e9e434f commit e6fb32f
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 1 deletion.
6 changes: 5 additions & 1 deletion 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 All @@ -124,12 +126,14 @@ extras = True
addopts =
--cov aleph.sdk --cov-report term-missing
--verbose
-m "not ledger_hardware"
norecursedirs =
dist
build
.tox
testpaths = tests

markers =
"ledger_hardware: marks tests as requiring ledger hardware"
[aliases]
dists = bdist_wheel

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"]
74 changes: 74 additions & 0 deletions src/aleph/sdk/wallets/ledger/ethereum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from __future__ import annotations

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: Optional[Dongle] = None
) -> Optional[LedgerETHAccount]:
device = device or init_dongle()
account = find_account(address=address, dongle=device, count=5)
return LedgerETHAccount(
account=account,
device=device,
)

@staticmethod
def from_path(path: str, device: Optional[Dongle] = None) -> LedgerETHAccount:
device = device or init_dongle()
account = get_account_by_path(path_string=path, dongle=device)
return LedgerETHAccount(
account=account,
device=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)
50 changes: 50 additions & 0 deletions tests/unit/test_wallet_ethereum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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.ledger_hardware
@pytest.mark.asyncio
async def test_LedgerETHAccount():
account: LedgerETHAccount = get_fallback_account()

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

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

verify_signature(
signed["signature"], signed["sender"], get_verification_buffer(signed)
)

with pytest.raises(BadSignatureError):
signed["signature"] = signed["signature"][:-8] + "cafecafe"

verify_signature(
signed["signature"], signed["sender"], get_verification_buffer(signed)
)

# Obtaining the public key is not supported (yet ?) on hardware wallets.
with pytest.raises(NotImplementedError):
account.get_public_key()

0 comments on commit e6fb32f

Please sign in to comment.