Skip to content
This repository was archived by the owner on Nov 23, 2023. It is now read-only.

feat: Support Ethereum EIP-712 Sign Typed Data for Trezor T #983

Merged
merged 2 commits into from
Dec 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_" // allow underscored args
"argsIgnorePattern": "^_", // allow underscored args,
"varsIgnorePattern": "^_" // allow underscored variables
}
],
"no-param-reassign": "off", // TODO: needs refactor
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
### Changed
- trezor-link was replaced with @trezor/transport

### Added

- Ethereum: Support for EthereumSignTypedData operation (Trezor T only)

# 8.2.3

### Added
Expand Down
1 change: 1 addition & 0 deletions docs/methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Every method require an [`Object`](https://developer.mozilla.org/en-US/docs/Web/
* [TrezorConnect.ethereumGetAddress](methods/ethereumGetAddress.md)
* [TrezorConnect.ethereumSignTransaction](methods/ethereumSignTransaction.md)
* [TrezorConnect.ethereumSignMessage](methods/ethereumSignMessage.md)
* [TrezorConnect.ethereumSignTypedData](methods/ethereumSignTypedData.md)
* [TrezorConnect.ethereumVerifyMessage](methods/ethereumVerifyMessage.md)

### Eos
Expand Down
94 changes: 94 additions & 0 deletions docs/methods/ethereumSignTypedData.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
## Ethereum: Sign Typed Data

Asks device to sign an [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed data message using the private key derived by given BIP32 path.

User is asked to confirm all signing details on Trezor Model T.

ES6

```javascript
const result = await TrezorConnect.ethereumSignTypedData(params);
```

CommonJS

```javascript
TrezorConnect.ethereumSignTypedData(params).then(function(result) {

});
```

> :warning: **Supported only by Trezor T with Firmware 2.4.3 or higher!**

### Params

[****Optional common params****](commonParams.md)

###### [flowtype](../../src/js/types/networks/ethereum.js#L102-105)

* `path` — *obligatory* `string | Array<number>` minimum length is `3`. [read more](path.md)
* `data` - *obligatory* `Object` type of [`EthereumSignTypedDataMessage`](../../src/js/types/networks/ethereum.js#L90)`. A JSON Schema definition can be found in the [EIP-712 spec]([EIP-712](https://eips.ethereum.org/EIPS/eip-712)).
* `metamask_v4_compat` - *obligatory* `boolean` set to `true` for compatibility with [MetaMask's signTypedData_v4](https://docs.metamask.io/guide/signing-data.html#sign-typed-data-v4).

### Example

```javascript
TrezorConnect.ethereumSignMessage({
path: "m/44'/60'/0'",
data: {
types: {
EIP712Domain: [
{
name: 'name',
type: 'string',
},
],
Message: [
{
name: "Best Wallet",
type: "string"
},
{
name: "Number",
type: "uint64"
}
]
},
primaryType: 'Message',
domain: {
name: 'example.trezor.io',
},
message: {
"Best Wallet": "Trezor Model T",
// be careful with JavaScript numbers: MAX_SAFE_INTEGER is quite low
"Number": `${2n ** 55n}`,
},
},
metamask_v4_compat: true,
});
```

### Result

###### [flowtype](../../src/js/types/api.js#L257)

```javascript
{
success: true,
payload: {
address: string,
signature: string, // hexadecimal string with "0x" prefix
}
}
```

Error

```javascript
{
success: false,
payload: {
error: string // error message
}
}
```
5 changes: 5 additions & 0 deletions src/data/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@
"methods": ["signTransaction"],
"min": ["1.10.1", "2.4.0"],
"comment": [""]
},
{
"methods": ["ethereumSignTypedData"],
"min": ["0", "2.4.3"],
"comment": ["EIP-712 typed signing support added in 2.4.3"]
}
]
}
163 changes: 163 additions & 0 deletions src/js/core/methods/EthereumSignTypedData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/* @flow */

import AbstractMethod from './AbstractMethod';
import { validateParams, getFirmwareRange } from './helpers/paramsValidator';
import { validatePath } from '../../utils/pathUtils';
import { getEthereumNetwork } from '../../data/CoinInfo';
import { toChecksumAddress, getNetworkLabel } from '../../utils/ethereumUtils';
import type { CoreMessage, EthereumNetworkInfo } from '../../types';
import type { MessageResponse, EthereumTypedDataStructAck } from '../../types/trezor/protobuf';
import { ERRORS } from '../../constants';
import type { EthereumSignTypedData as EthereumSignTypedDataParams } from '../../types/networks/ethereum';
import { getFieldType, parseArrayType, encodeData } from './helpers/ethereumSignTypedData';

type Params = {
...EthereumSignTypedDataParams,
path: number[],
network?: EthereumNetworkInfo,
};

export default class EthereumSignTypedData extends AbstractMethod {
params: Params;

constructor(message: CoreMessage) {
super(message);

this.requiredPermissions = ['read', 'write'];

const { payload } = message;

// validate incoming parameters
validateParams(payload, [
{ name: 'path', obligatory: true },
{ name: 'data', type: 'object', obligatory: true },
{ name: 'metamask_v4_compat', type: 'boolean', obligatory: true },
]);

const path = validatePath(payload.path, 3);
const network = getEthereumNetwork(path);
this.firmwareRange = getFirmwareRange(this.name, network, this.firmwareRange);

this.info = getNetworkLabel('Sign #NETWORK typed data', network);

const { data, metamask_v4_compat } = payload;

this.params = {
path,
network,
data,
metamask_v4_compat,
};
}

async run() {
const cmd = this.device.getCommands();
const { path: address_n, network, data, metamask_v4_compat } = this.params;

const { types, primaryType, domain, message } = data;

let response: MessageResponse<
| 'EthereumTypedDataStructRequest'
| 'EthereumTypedDataValueRequest'
| 'EthereumTypedDataSignature',
> = await cmd.typedCall(
'EthereumSignTypedData',
// $FlowIssue typedCall problem with unions in response, TODO: accept unions
'EthereumTypedDataStructRequest|EthereumTypedDataValueRequest|EthereumTypedDataSignature',
{
address_n,
primary_type: primaryType,
metamask_v4_compat,
},
);

// sending all the type data
while (response.type === 'EthereumTypedDataStructRequest') {
// $FlowIssue disjoint union Refinements not working, TODO: check if new Flow versions fix this
const { name: typeDefinitionName } = response.message;
const typeDefinition = types[typeDefinitionName];
if (typeDefinition === undefined) {
throw ERRORS.TypedError(
'Runtime',
`Type ${typeDefinitionName} was not defined in types object`,
);
}
const dataStruckAck: EthereumTypedDataStructAck = {
members: typeDefinition.map(({ name, type: typeName }) => ({
name,
type: getFieldType(typeName, types),
})),
};
response = await cmd.typedCall(
'EthereumTypedDataStructAck',
// $FlowIssue typedCall problem with unions in response, TODO: accept unions
'EthereumTypedDataStructRequest|EthereumTypedDataValueRequest|EthereumTypedDataSignature',
dataStruckAck,
);
}

// sending the whole message to be signed
while (response.type === 'EthereumTypedDataValueRequest') {
// $FlowIssue disjoint union Refinements not working, TODO: check if new Flow versions fix this
const { member_path } = response.message;

let memberData;
let memberTypeName;

const [rootIndex, ...nestedMemberPath] = member_path;
switch (rootIndex) {
case 0:
memberData = domain;
memberTypeName = 'EIP712Domain';
break;
case 1:
memberData = message;
memberTypeName = primaryType;
break;
default:
throw ERRORS.TypedError('Runtime', 'Root index can only be 0 or 1');
}

// It can be asking for a nested structure (the member path being [X, Y, Z, ...])
for (const index of nestedMemberPath) {
if (Array.isArray(memberData)) {
memberTypeName = parseArrayType(memberTypeName).entryTypeName;
memberData = memberData[index];
} else if (typeof memberData === 'object' && memberData !== null) {
const memberTypeDefinition = types[memberTypeName][index];
memberTypeName = memberTypeDefinition.type;
memberData = memberData[memberTypeDefinition.name];
} else {
// TODO: what to do when the value is missing (for example in recursive types)?
}
}

let encodedData;
// If we were asked for a list, first sending its length and we will be receiving
// requests for individual elements later
if (Array.isArray(memberData)) {
// Sending the length as uint16
encodedData = encodeData('uint16', memberData.length);
} else {
encodedData = encodeData(memberTypeName, memberData);
}

// $FlowIssue with `await` and Promises: https://github.com/facebook/flow/issues/5294, TODO: Update flow
response = await cmd.typedCall(
'EthereumTypedDataValueAck',
// $FlowIssue typedCall problem with unions in response, TODO: accept unions
'EthereumTypedDataValueRequest|EthereumTypedDataSignature',
{
value: encodedData,
},
);
}

// $FlowIssue disjoint union Refinements not working, TODO: check if new Flow versions fix this
const { address, signature } = response.message;
return {
address: toChecksumAddress(address, network),
signature: `0x${signature}`,
};
}
}
Loading