Skip to content

Commit aeec83a

Browse files
committed
Feature: Ledger wallets could not sign using ethereum
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.
1 parent e9e434f commit aeec83a

File tree

10 files changed

+134
-6
lines changed

10 files changed

+134
-6
lines changed

docker/python-3.10.dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ USER root
2929
RUN chown -R user:user /opt/aleph-sdk-python
3030

3131
RUN git config --global --add safe.directory /opt/aleph-sdk-python
32-
RUN pip install -e .[testing,ethereum,solana,tezos]
32+
RUN pip install -e .[testing,ethereum,solana,tezos,ledger]
3333

3434
RUN mkdir /data
3535
RUN chown user:user /data

docker/python-3.11.dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ USER root
2929
RUN chown -R user:user /opt/aleph-sdk-python
3030

3131
RUN git config --global --add safe.directory /opt/aleph-sdk-python
32-
RUN pip install -e .[testing,ethereum,solana,tezos]
32+
RUN pip install -e .[testing,ethereum,solana,tezos,ledger]
3333

3434
RUN mkdir /data
3535
RUN chown user:user /data

docker/python-3.9.dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ USER root
2929
RUN chown -R user:user /opt/aleph-sdk-python
3030

3131
RUN git config --global --add safe.directory /opt/aleph-sdk-python
32-
RUN pip install -e .[testing,ethereum,solana,tezos]
32+
RUN pip install -e .[testing,ethereum,solana,tezos,ledger]
3333

3434
RUN mkdir /data
3535
RUN chown user:user /data

docker/ubuntu-20.04.dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ USER root
3434
RUN chown -R user:user /opt/aleph-sdk-python
3535

3636
RUN git config --global --add safe.directory /opt/aleph-sdk-python
37-
RUN pip install -e .[testing,ethereum,solana,tezos]
37+
RUN pip install -e .[testing,ethereum,solana,tezos,ledger]
3838

3939
RUN mkdir /data
4040
RUN chown user:user /data

docker/ubuntu-22.04.dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ USER root
3434
RUN chown -R user:user /opt/aleph-sdk-python
3535

3636
RUN git config --global --add safe.directory /opt/aleph-sdk-python
37-
RUN pip install -e .[testing,ethereum,solana,tezos]
37+
RUN pip install -e .[testing,ethereum,solana,tezos,ledger]
3838

3939
RUN mkdir /data
4040
RUN chown user:user /data

setup.cfg

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ solana =
9999
tezos =
100100
pynacl
101101
aleph-pytezos==0.1.1
102+
ledger =
103+
ledgereth==0.9.0
102104
docs =
103105
sphinxcontrib-plantuml
104106

@@ -124,12 +126,14 @@ extras = True
124126
addopts =
125127
--cov aleph.sdk --cov-report term-missing
126128
--verbose
129+
-m "not ledger_hardware"
127130
norecursedirs =
128131
dist
129132
build
130133
.tox
131134
testpaths = tests
132-
135+
markers =
136+
"ledger_hardware: marks tests as requiring ledger hardware"
133137
[aliases]
134138
dists = bdist_wheel
135139

src/aleph/sdk/wallets/__init__.py

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .ethereum import LedgerETHAccount
2+
3+
__all__ = ["LedgerETHAccount"]
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from __future__ import annotations
2+
3+
from typing import Dict, List, Optional
4+
5+
from eth_typing import HexStr
6+
from ledgerblue.Dongle import Dongle
7+
from ledgereth import find_account, get_account_by_path, get_accounts
8+
from ledgereth.comms import init_dongle
9+
from ledgereth.messages import sign_message
10+
from ledgereth.objects import LedgerAccount, SignedMessage
11+
12+
from ...chains.common import BaseAccount, get_verification_buffer
13+
14+
15+
class LedgerETHAccount(BaseAccount):
16+
"""Account using the Ethereum app on Ledger hardware wallets."""
17+
18+
CHAIN = "ETH"
19+
CURVE = "secp256k1"
20+
_account: LedgerAccount
21+
_device: Dongle
22+
23+
def __init__(self, account: LedgerAccount, device: Optional[Dongle]):
24+
self._account = account
25+
self._device = device or init_dongle()
26+
27+
@staticmethod
28+
def from_address(
29+
address: str, device: Optional[Dongle] = None
30+
) -> Optional[LedgerETHAccount]:
31+
device = device or init_dongle()
32+
account = find_account(address=address, dongle=device, count=5)
33+
return LedgerETHAccount(
34+
account=account,
35+
device=device,
36+
)
37+
38+
@staticmethod
39+
def from_path(path: str, device: Optional[Dongle] = None) -> LedgerETHAccount:
40+
device = device or init_dongle()
41+
account = get_account_by_path(path_string=path, dongle=device)
42+
return LedgerETHAccount(
43+
account=account,
44+
device=device,
45+
)
46+
47+
async def sign_message(self, message: Dict) -> Dict:
48+
"""Sign a message inplace."""
49+
message: Dict = self._setup_sender(message)
50+
51+
# TODO: Check why the code without a wallet uses `encode_defunct`.
52+
msghash: bytes = get_verification_buffer(message)
53+
sig: SignedMessage = sign_message(msghash, dongle=self._device)
54+
55+
signature: HexStr = sig.signature
56+
57+
message["signature"] = signature
58+
return message
59+
60+
def get_address(self) -> str:
61+
return self._account.address
62+
63+
def get_public_key(self) -> str:
64+
raise NotImplementedError()
65+
66+
67+
def get_fallback_account() -> LedgerETHAccount:
68+
"""Returns the first account available on the device first device found."""
69+
device: Dongle = init_dongle()
70+
accounts: List[LedgerAccount] = get_accounts(dongle=device, count=1)
71+
if not accounts:
72+
raise ValueError("No account found on device")
73+
account = accounts[0]
74+
return LedgerETHAccount(account=account, device=device)

tests/unit/test_wallet_ethereum.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from dataclasses import asdict, dataclass
2+
3+
import pytest
4+
5+
from aleph.sdk.chains.common import get_verification_buffer
6+
from aleph.sdk.chains.ethereum import verify_signature
7+
from aleph.sdk.exceptions import BadSignatureError
8+
from aleph.sdk.wallets.ledger.ethereum import LedgerETHAccount, get_fallback_account
9+
10+
11+
@dataclass
12+
class Message:
13+
chain: str
14+
sender: str
15+
type: str
16+
item_hash: str
17+
18+
19+
@pytest.mark.ledger_hardware
20+
@pytest.mark.asyncio
21+
async def test_LedgerETHAccount():
22+
account: LedgerETHAccount = get_fallback_account()
23+
24+
address = account.get_address()
25+
assert address
26+
assert type(address) is str
27+
assert len(address) == 42
28+
29+
message = Message("ETH", account.get_address(), "SomeType", "ItemHash")
30+
signed = await account.sign_message(asdict(message))
31+
assert signed["signature"]
32+
assert len(signed["signature"]) == 132
33+
34+
verify_signature(
35+
signed["signature"], signed["sender"], get_verification_buffer(signed)
36+
)
37+
38+
with pytest.raises(BadSignatureError):
39+
signed["signature"] = signed["signature"][:-8] + "cafecafe"
40+
41+
verify_signature(
42+
signed["signature"], signed["sender"], get_verification_buffer(signed)
43+
)
44+
45+
# Obtaining the public key is not supported (yet ?) on hardware wallets.
46+
with pytest.raises(NotImplementedError):
47+
account.get_public_key()

0 commit comments

Comments
 (0)