Skip to content

Commit

Permalink
Merge pull request #651 from LedgerHQ/feat/apa/trusted_name_v2
Browse files Browse the repository at this point in the history
Trusted names v2 + EIP-712 trusted name filtering
  • Loading branch information
apaillier-ledger committed Sep 23, 2024
2 parents bd7f083 + 0747113 commit a18d1b7
Show file tree
Hide file tree
Showing 382 changed files with 1,279 additions and 466 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build_and_functional_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_build.yml@v1
with:
upload_app_binaries_artifact: "ragger_elfs"
flags: "CAL_TEST_KEY=1 DOMAIN_NAME_TEST_KEY=1 SET_PLUGIN_TEST_KEY=1 NFT_TEST_KEY=1"
flags: "CAL_TEST_KEY=1 TRUSTED_NAME_TEST_KEY=1 SET_PLUGIN_TEST_KEY=1 NFT_TEST_KEY=1"

ragger_tests:
name: Run ragger tests using the reusable workflow
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ ENABLE_NBGL_QRCODE = 1
# Main app configuration #
########################################

DEFINES += CHAINID_COINNAME=\"$(TICKER)\" CHAIN_ID=$(CHAIN_ID)
DEFINES += APP_TICKER=\"$(TICKER)\" APP_CHAIN_ID=$(CHAIN_ID)

# Enabled Features #
include makefile_conf/features.mk
Expand Down
95 changes: 76 additions & 19 deletions client/src/ledger_app_clients/ethereum/client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import rlp
import struct
from enum import IntEnum
from ragger.backend import BackendInterface
from ragger.firmware import Firmware
Expand Down Expand Up @@ -27,16 +28,36 @@ class StatusWord(IntEnum):
NOT_IMPLEMENTED = 0x911c


class DomainNameTag(IntEnum):
STRUCTURE_TYPE = 0x01
STRUCTURE_VERSION = 0x02
class TrustedNameType(IntEnum):
ACCOUNT = 0x01
CONTRACT = 0x02
NFT = 0x03


class TrustedNameSource(IntEnum):
LAB = 0x00
CAL = 0x01
ENS = 0x02
UD = 0x03
FN = 0x04
DNS = 0x05


class TrustedNameTag(IntEnum):
STRUCT_TYPE = 0x01
STRUCT_VERSION = 0x02
NOT_VALID_AFTER = 0x10
CHALLENGE = 0x12
SIGNER_KEY_ID = 0x13
SIGNER_ALGO = 0x14
SIGNATURE = 0x15
DOMAIN_NAME = 0x20
NAME = 0x20
COIN_TYPE = 0x21
ADDRESS = 0x22
CHAIN_ID = 0x23
NAME_TYPE = 0x70
NAME_SOURCE = 0x71
NFT_ID = 0x72


class PKIPubKeyUsage(IntEnum):
Expand Down Expand Up @@ -168,6 +189,18 @@ def eip712_filtering_amount_join_value(self, token_idx: int, name: str, sig: byt
def eip712_filtering_datetime(self, name: str, sig: bytes, discarded: bool):
return self._exchange_async(self._cmd_builder.eip712_filtering_datetime(name, sig, discarded))

def eip712_filtering_trusted_name(self,
name: str,
name_type: list[int],
name_source: list[int],
sig: bytes,
discarded: bool):
return self._exchange_async(self._cmd_builder.eip712_filtering_trusted_name(name,
name_type,
name_source,
sig,
discarded))

def eip712_filtering_raw(self, name: str, sig: bytes, discarded: bool):
return self._exchange_async(self._cmd_builder.eip712_filtering_raw(name, sig, discarded))

Expand Down Expand Up @@ -217,8 +250,7 @@ def perform_privacy_operation(self,
bip32_path,
pubkey))

def provide_domain_name(self, challenge: int, name: str, addr: bytes) -> RAPDU:

def _provide_trusted_name_common(self, payload: bytes) -> RAPDU:
if self._pki_client is None:
print(f"Ledger-PKI Not supported on '{self._firmware.name}'")
else:
Expand All @@ -234,23 +266,48 @@ def provide_domain_name(self, challenge: int, name: str, addr: bytes) -> RAPDU:
# pylint: enable=line-too-long

self._pki_client.send_certificate(PKIPubKeyUsage.PUBKEY_USAGE_COIN_META, bytes.fromhex(cert_apdu))

payload = format_tlv(DomainNameTag.STRUCTURE_TYPE, 3) # TrustedDomainName
payload += format_tlv(DomainNameTag.STRUCTURE_VERSION, 1)
payload += format_tlv(DomainNameTag.SIGNER_KEY_ID, 0) # test key
payload += format_tlv(DomainNameTag.SIGNER_ALGO, 1) # secp256k1
payload += format_tlv(DomainNameTag.CHALLENGE, challenge)
payload += format_tlv(DomainNameTag.COIN_TYPE, 0x3c) # ETH in slip-44
payload += format_tlv(DomainNameTag.DOMAIN_NAME, name)
payload += format_tlv(DomainNameTag.ADDRESS, addr)
payload += format_tlv(DomainNameTag.SIGNATURE,
sign_data(Key.DOMAIN_NAME, payload))

chunks = self._cmd_builder.provide_domain_name(payload)
payload += format_tlv(TrustedNameTag.STRUCT_TYPE, 3) # TrustedName
payload += format_tlv(TrustedNameTag.SIGNER_KEY_ID, 0) # test key
payload += format_tlv(TrustedNameTag.SIGNER_ALGO, 1) # secp256k1
payload += format_tlv(TrustedNameTag.SIGNATURE,
sign_data(Key.TRUSTED_NAME, payload))
chunks = self._cmd_builder.provide_trusted_name(payload)
for chunk in chunks[:-1]:
self._exchange(chunk)
return self._exchange(chunks[-1])

def provide_trusted_name_v1(self, addr: bytes, name: str, challenge: int) -> RAPDU:
payload = format_tlv(TrustedNameTag.STRUCT_VERSION, 1)
payload += format_tlv(TrustedNameTag.CHALLENGE, challenge)
payload += format_tlv(TrustedNameTag.COIN_TYPE, 0x3c) # ETH in slip-44
payload += format_tlv(TrustedNameTag.NAME, name)
payload += format_tlv(TrustedNameTag.ADDRESS, addr)
return self._provide_trusted_name_common(payload)

def provide_trusted_name_v2(self,
addr: bytes,
name: str,
name_type: TrustedNameType,
name_source: TrustedNameSource,
chain_id: int,
nft_id: Optional[int] = None,
challenge: Optional[int] = None,
not_valid_after: Optional[tuple[int]] = None) -> RAPDU:
payload = format_tlv(TrustedNameTag.STRUCT_VERSION, 2)
payload += format_tlv(TrustedNameTag.NAME, name)
payload += format_tlv(TrustedNameTag.ADDRESS, addr)
payload += format_tlv(TrustedNameTag.NAME_TYPE, name_type)
payload += format_tlv(TrustedNameTag.NAME_SOURCE, name_source)
payload += format_tlv(TrustedNameTag.CHAIN_ID, chain_id)
if nft_id is not None:
payload += format_tlv(TrustedNameTag.NFT_ID, nft_id)
if challenge is not None:
payload += format_tlv(TrustedNameTag.CHALLENGE, challenge)
if not_valid_after is not None:
assert len(not_valid_after) == 3
payload += format_tlv(TrustedNameTag.NOT_VALID_AFTER, struct.pack("BBB", *not_valid_after))
return self._provide_trusted_name_common(payload)

def set_plugin(self,
plugin_name: str,
contract_addr: bytes,
Expand Down
29 changes: 26 additions & 3 deletions client/src/ledger_app_clients/ethereum/command_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class InsType(IntEnum):
EIP712_SEND_FILTERING = 0x1e
EIP712_SIGN = 0x0c
GET_CHALLENGE = 0x20
PROVIDE_DOMAIN_NAME = 0x22
PROVIDE_TRUSTED_NAME = 0x22
EXTERNAL_PLUGIN_SETUP = 0x12


Expand All @@ -43,6 +43,7 @@ class P2Type(IntEnum):
FILTERING_ACTIVATE = 0x00
FILTERING_DISCARDED_PATH = 0x01
FILTERING_MESSAGE_INFO = 0x0f
FILTERING_TRUSTED_NAME = 0xfb
FILTERING_DATETIME = 0xfc
FILTERING_TOKEN_ADDR_CHECK = 0xfd
FILTERING_AMOUNT_FIELD = 0xfe
Expand Down Expand Up @@ -214,6 +215,28 @@ def eip712_filtering_datetime(self, name: str, sig: bytes, discarded: bool) -> b
P2Type.FILTERING_DATETIME,
self._eip712_filtering_send_name(name, sig))

def eip712_filtering_trusted_name(self,
name: str,
name_types: list[int],
name_sources: list[int],
sig: bytes,
discarded: bool) -> bytes:
data = bytearray()
data.append(len(name))
data += name.encode()
data.append(len(name_types))
for t in name_types:
data.append(t)
data.append(len(name_sources))
for s in name_sources:
data.append(s)
data.append(len(sig))
data += sig
return self._serialize(InsType.EIP712_SEND_FILTERING,
int(discarded),
P2Type.FILTERING_TRUSTED_NAME,
data)

def eip712_filtering_raw(self, name: str, sig: bytes, discarded: bool) -> bytes:
return self._serialize(InsType.EIP712_SEND_FILTERING,
int(discarded),
Expand Down Expand Up @@ -260,13 +283,13 @@ def sign(self, bip32_path: str, rlp_data: bytes, vrs: list) -> list[bytes]:
def get_challenge(self) -> bytes:
return self._serialize(InsType.GET_CHALLENGE, 0x00, 0x00)

def provide_domain_name(self, tlv_payload: bytes) -> list[bytes]:
def provide_trusted_name(self, tlv_payload: bytes) -> list[bytes]:
chunks = list()
payload = struct.pack(">H", len(tlv_payload))
payload += tlv_payload
p1 = 1
while len(payload) > 0:
chunks.append(self._serialize(InsType.PROVIDE_DOMAIN_NAME,
chunks.append(self._serialize(InsType.PROVIDE_TRUSTED_NAME,
p1,
0x00,
payload[:0xff]))
Expand Down
25 changes: 25 additions & 0 deletions client/src/ledger_app_clients/ethereum/eip712/InputData.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,12 @@ def send_filter(path: str, discarded: bool):
discarded)
elif filtering_paths[path]["type"] == "datetime":
send_filtering_datetime(path, filtering_paths[path]["name"], discarded)
elif filtering_paths[path]["type"] == "trusted_name":
send_filtering_trusted_name(path,
filtering_paths[path]["name"],
filtering_paths[path]["tn_type"],
filtering_paths[path]["tn_source"],
discarded)
elif filtering_paths[path]["type"] == "raw":
send_filtering_raw(path, filtering_paths[path]["name"], discarded)
else:
Expand Down Expand Up @@ -358,6 +364,25 @@ def send_filtering_datetime(path: str, display_name: str, discarded: bool):
pass


def send_filtering_trusted_name(path: str,
display_name: str,
name_type: list[int],
name_source: list[int],
discarded: bool):
global sig_ctx

to_sign = start_signature_payload(sig_ctx, 44)
to_sign += path.encode()
to_sign += display_name.encode()
for t in name_type:
to_sign.append(t)
for s in name_source:
to_sign.append(s)
sig = keychain.sign_data(keychain.Key.CAL, to_sign)
with app_client.eip712_filtering_trusted_name(display_name, name_type, name_source, sig, discarded):
pass


# ledgerjs doesn't actually sign anything, and instead uses already pre-computed signatures
def send_filtering_raw(path: str, display_name: str, discarded: bool):
global sig_ctx
Expand Down
2 changes: 1 addition & 1 deletion client/src/ledger_app_clients/ethereum/keychain.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# Example: for an entry in the Enum named DEV, its PEM file must be at keychain/dev.pem
class Key(Enum):
CAL = auto()
DOMAIN_NAME = auto()
TRUSTED_NAME = auto()
SET_PLUGIN = auto()
NFT = auto()

Expand Down
41 changes: 35 additions & 6 deletions doc/ethapp.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,15 @@ Application version 1.9.19 - 2022-05-17
- Update to SIGN ETH EIP712

### 1.10.2
- Add domain names support
- Add trusted names support

### 1.11.0
- Add EIP-712 amount & date/time filtering
- Add EIP-712 amount-join & date/time filtering
- PROVIDE ERC 20 TOKEN INFORMATION & PROVIDE NFT INFORMATION now send back the index where the asset has been stored

### 1.12.0
- Add EIP-712 discarded filter path support
- Add EIP-712 trusted name filtering

## About

Expand Down Expand Up @@ -858,6 +859,7 @@ The signature is computed on :
##### Amount-join value

This command should come before the corresponding *SEND STRUCT IMPLEMENTATION* and are only usable for message fields (and not domain ones).
The first byte is used so that a signature of one type cannot be valid as another type.

A token index of 0xFF indicates the token address is in the _verifyingContract_ field of the EIP712Domain so the app won't receive an amount-join token filtering APDU. This enables support for Permit (ERC-2612) messages.

Expand All @@ -868,11 +870,21 @@ The signature is computed on :
##### Date / Time

This command should come before the corresponding *SEND STRUCT IMPLEMENTATION* and are only usable for message fields (and not domain ones).
The first byte is used so that a signature of one type cannot be valid as another type.

The signature is computed on :

33 || chain ID (BE) || contract address || schema hash || field path || display name

##### Trusted name

This command should come right after the implementation of the domain has been sent with *SEND STRUCT IMPLEMENTATION*, just before sending the message implementation.
The first byte is used so that a signature of one type cannot be valid as another type.

The signature is computed on :

44 || chain ID (BE) || contract address || schema hash || field path || display name || name types || name sources

##### Show raw field

This command should come before the corresponding *SEND STRUCT IMPLEMENTATION* and are only usable for message fields (and not domain ones).
Expand All @@ -897,6 +909,8 @@ _Command_

0F : message info

FB : trusted name

FC : date/time

FD : amount-join token
Expand All @@ -922,7 +936,6 @@ None
| Path | variable
|==========================================


##### If P2 == message info

[width="80%"]
Expand All @@ -935,6 +948,21 @@ None
| Signature | variable
|==========================================

##### If P2 == trusted name

[width="80%"]
|==========================================
| *Description* | *Length (byte)*
| Display name length | 1
| Display name | variable
| Name types count | 1
| Name types | variable
| Name sources count | 1
| Name sources | variable
| Signature length | 1
| Signature | variable
|==========================================

##### If P2 == date / time

[width="80%"]
Expand Down Expand Up @@ -1014,12 +1042,13 @@ _Output data_
|===========================================


### PROVIDE DOMAIN NAME
### PROVIDE TRUSTED NAME

#### Description

This command provides a domain name (like ENS) to be displayed during transactions in place of the address it is associated to.
It shall be run just before a transaction involving the associated address that would be displayed on the device.
This command provides a trusted name (like an ENS domain) to be displayed during transactions in place of the
address it is associated to. It shall be run just before a transaction/message involving the associated
address that would be displayed on the device.

The signature is computed on the TLV payload (minus the signature obviously).

Expand Down
4 changes: 2 additions & 2 deletions ledger_app.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ sdk = "C"
devices = ["nanos", "nanox", "nanos+", "stax", "flex"]

[use_cases]
use_test_keys = "CAL_TEST_KEY=1 DOMAIN_NAME_TEST_KEY=1 SET_PLUGIN_TEST_KEY=1 NFT_TEST_KEY=1"
dbg_use_test_keys = "DEBUG=1 CAL_TEST_KEY=1 DOMAIN_NAME_TEST_KEY=1 SET_PLUGIN_TEST_KEY=1 NFT_TEST_KEY=1"
use_test_keys = "CAL_TEST_KEY=1 TRUSTED_NAME_TEST_KEY=1 SET_PLUGIN_TEST_KEY=1 NFT_TEST_KEY=1"
dbg_use_test_keys = "DEBUG=1 CAL_TEST_KEY=1 TRUSTED_NAME_TEST_KEY=1 SET_PLUGIN_TEST_KEY=1 NFT_TEST_KEY=1"
cal_bypass = "BYPASS_SIGNATURES=1"
dbg_cal_bypass = "DEBUG=1 BYPASS_SIGNATURES=1"

Expand Down
8 changes: 4 additions & 4 deletions makefile_conf/features.mk
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ endif

# ENS
ifneq ($(TARGET_NAME),TARGET_NANOS)
DEFINES += HAVE_DOMAIN_NAME
DOMAIN_NAME_TEST_KEY ?= 0
ifneq ($(DOMAIN_NAME_TEST_KEY),0)
DEFINES += HAVE_DOMAIN_NAME_TEST_KEY
DEFINES += HAVE_TRUSTED_NAME
TRUSTED_NAME_TEST_KEY ?= 0
ifneq ($(TRUSTED_NAME_TEST_KEY),0)
DEFINES += HAVE_TRUSTED_NAME_TEST_KEY
endif
endif

Expand Down
Loading

0 comments on commit a18d1b7

Please sign in to comment.